Skip to content

Commit 99143cd

Browse files
committed
[PNV Demo Part 1] Match a phone number verfication
1 parent 2bcd157 commit 99143cd

File tree

11 files changed

+2267
-1765
lines changed

11 files changed

+2267
-1765
lines changed

app/src/main/assets/database.json

Lines changed: 0 additions & 135 deletions
This file was deleted.

app/src/main/assets/databasenew.json

Lines changed: 1625 additions & 1595 deletions
Large diffs are not rendered by default.

app/src/main/assets/pnv.wasm

69.6 KB
Binary file not shown.

app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import kotlinx.coroutines.CoroutineScope
1818
import kotlinx.coroutines.Dispatchers
1919
import kotlinx.coroutines.SupervisorJob
2020
import kotlinx.coroutines.launch
21+
import org.json.JSONObject
2122
import kotlin.io.encoding.ExperimentalEncodingApi
2223

2324
class CmWalletApplication : Application() {
@@ -110,6 +111,9 @@ class CmWalletApplication : Application() {
110111
openId4VP1_0Matcher
111112
) {}
112113
)
114+
115+
// Phone number verification demo
116+
credentialRepo.registerPhoneNumberVerification(registryManager, loadPhoneNumberMatcher())
113117
}
114118
}
115119

@@ -157,8 +161,8 @@ class CmWalletApplication : Application() {
157161
return readAsset("provision_hardcoded.wasm")
158162
}
159163

160-
private fun loadTestCredentials(): ByteArray {
161-
return readAsset("database.json")
164+
private fun loadPhoneNumberMatcher(): ByteArray {
165+
return readAsset("pnv.wasm")
162166
}
163167

164168
private fun loadTestCredentialsNew(): ByteArray {

app/src/main/java/com/credman/cmwallet/data/repository/CredentialRepository.kt

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ package com.credman.cmwallet.data.repository
22

33
import android.os.Build
44
import android.util.Log
5+
import androidx.credentials.DigitalCredential
6+
import androidx.credentials.ExperimentalDigitalCredentialApi
7+
import androidx.credentials.registry.provider.RegisterCredentialsRequest
8+
import androidx.credentials.registry.provider.RegistryManager
9+
import com.credman.cmwallet.data.model.CredentialDisplayData
510
import com.credman.cmwallet.data.model.CredentialItem
611
import com.credman.cmwallet.data.model.CredentialKeySoftware
712
import com.credman.cmwallet.data.source.CredentialDatabaseDataSource
@@ -13,6 +18,7 @@ import com.credman.cmwallet.openid4vci.OpenId4VCI
1318
import com.credman.cmwallet.openid4vci.data.CredentialConfigurationMDoc
1419
import com.credman.cmwallet.openid4vci.data.CredentialConfigurationSdJwtVc
1520
import com.credman.cmwallet.openid4vci.data.CredentialConfigurationUnknownFormat
21+
import com.credman.cmwallet.pnv.PnvTokenRegistry
1622
import com.credman.cmwallet.sdjwt.SdJwt
1723
import kotlinx.coroutines.flow.Flow
1824
import kotlinx.coroutines.flow.combine
@@ -73,6 +79,24 @@ class CredentialRepository {
7379
privAppsJson = appsJson
7480
}
7581

82+
@OptIn(ExperimentalDigitalCredentialApi::class)
83+
suspend fun registerPhoneNumberVerification(registryManager: RegistryManager, pnvMatcher: ByteArray) {
84+
val testPhoneNumberTokens = listOf(
85+
PnvTokenRegistry.TEST_PNV_1_GET_PHONE_NUMBER,
86+
PnvTokenRegistry.TEST_PNV_1_VERIFY_PHONE_NUMBER,
87+
PnvTokenRegistry.TEST_PNV_2_GET_PHONE_NUMBER
88+
)
89+
90+
registryManager.registerCredentials(
91+
request = object : RegisterCredentialsRequest(
92+
DigitalCredential.TYPE_DIGITAL_CREDENTIAL,
93+
"openid4vp1.0-pnv",
94+
PnvTokenRegistry.buildRegistryDatabase(testPhoneNumberTokens),
95+
pnvMatcher
96+
) {}
97+
)
98+
}
99+
76100
suspend fun issueCredential(requestJson: String) {
77101
val openId4VCI = OpenId4VCI(requestJson)
78102

@@ -83,20 +107,20 @@ class CredentialRepository {
83107
var iconOffset: Int = 0
84108
)
85109

86-
private fun JSONObject.putCommon(item: CredentialItem, iconMap: Map<String, RegistryIcon>) {
87-
put(ID, item.id)
88-
put(TITLE, item.displayData.title)
89-
putOpt(SUBTITLE, item.displayData.subtitle)
110+
private fun JSONObject.putCommon(itemId: String, itemDisplayData: CredentialDisplayData, iconMap: Map<String, RegistryIcon>) {
111+
put(ID, itemId)
112+
put(TITLE, itemDisplayData.title)
113+
putOpt(SUBTITLE, itemDisplayData.subtitle)
90114
val iconJson = JSONObject().apply {
91-
put(START, iconMap[item.id]!!.iconOffset)
92-
put(LENGTH, iconMap[item.id]!!.iconValue.size)
115+
put(START, iconMap[itemId]!!.iconOffset)
116+
put(LENGTH, iconMap[itemId]!!.iconValue.size)
93117
}
94118
put(ICON, iconJson)
95119
}
96120

97121
private fun constructJwtForRegistry(
98122
rawJwt: JSONObject,
99-
displayConfig: CredentialConfigurationSdJwtVc,
123+
displayConfig: CredentialConfigurationSdJwtVc?,
100124
path: JSONArray,
101125
): JSONObject {
102126
val result = JSONObject()
@@ -113,7 +137,7 @@ class CredentialRepository {
113137
result.put(
114138
key,
115139
JSONObject().apply {
116-
val displayName = displayConfig.claims?.firstOrNull{
140+
val displayName = displayConfig?.claims?.firstOrNull{
117141
JSONArray(it.path) == currPath
118142
}?.display?.first()?.name
119143
putOpt(DISPLAY, displayName)
@@ -167,7 +191,7 @@ class CredentialRepository {
167191
when (item.config) {
168192
is CredentialConfigurationSdJwtVc -> {
169193
val credJson = JSONObject()
170-
credJson.putCommon(item, iconMap)
194+
credJson.putCommon(item.id, item.displayData, iconMap)
171195
val sdJwtVc = SdJwt(item.credentials.first().credential, (item.credentials.first().key as CredentialKeySoftware).privateKey)
172196
val rawJwt = sdJwtVc.verifiedResult.processedJwt
173197
val jwtWithDisplay = constructJwtForRegistry(rawJwt, item.config, JSONArray())
@@ -182,7 +206,7 @@ class CredentialRepository {
182206
}
183207
is CredentialConfigurationMDoc -> {
184208
val credJson = JSONObject()
185-
credJson.putCommon(item, iconMap)
209+
credJson.putCommon(item.id, item.displayData, iconMap)
186210
val mdoc = MDoc(item.credentials.first().credential.decodeBase64UrlNoPadding())
187211
if (mdoc.issuerSignedNamespaces.isNotEmpty()) {
188212
val pathJson = JSONObject()
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package com.credman.cmwallet.pnv
2+
3+
import org.json.JSONArray
4+
import org.json.JSONObject
5+
import java.io.ByteArrayOutputStream
6+
import java.nio.ByteBuffer
7+
import java.nio.ByteOrder
8+
9+
/**
10+
* A phone number verification entry to be registered with the Credential Manager.
11+
*
12+
* @param tokenId The Securely generated ID that will be used to identify a user selection
13+
* @param title The display title for this TS43 token entry. Ideally this should be an obfuscated
14+
* phone number, such as ***-***-1234, or some other information that allows the user to
15+
* disambiguate this entry from others, especially in the multi-sim use case.
16+
* @param subscriptionId The subscription ID of the SIM card that this token is associated with.
17+
* @param carrierId The carrier ID of the SIM card that this token is associated with.
18+
* @param useCases The set of use cases that this token is. Allowed values are:
19+
* [USE_CASE_VERIFY_PHONE_NUMBER], [USE_CASE_GET_PHONE_NUMBER], [USE_CASE_GET_SUBSCRIBER_INFO]
20+
*/
21+
data class PnvTokenRegistry(
22+
val tokenId: String,
23+
val vct: String,
24+
val title: String,
25+
val providerConsent: String?,
26+
val subscriptionHint: Int?,
27+
val carrierHint: String?,
28+
val phoneNumberHint: String?
29+
) {
30+
/** Converts this TS43 entry to the more generic SD-JWT registry item(s). */
31+
private fun toSdJwtRegistryItems(): SdJwtRegistryItem {
32+
return SdJwtRegistryItem(
33+
id = tokenId,
34+
vct = vct,
35+
claims =
36+
listOf(
37+
RegistryClaim("subscription_hint", null, subscriptionHint),
38+
RegistryClaim("carrier_hint", null, carrierHint),
39+
RegistryClaim("phone_number_hint", null, phoneNumberHint),
40+
),
41+
displayData = ItemDisplayData(title = title, subtitle = null, description = providerConsent),
42+
)
43+
}
44+
45+
companion object {
46+
const val VCT_GET_PHONE_NUMBER = "number-verification/device-phone-number/ts43"
47+
const val VCT_VERIFY_PHONE_NUMBER = "number-verification/verify/ts43"
48+
const val PNV_CRED_FORMAT = "dc+sd-jwt-pnv"
49+
50+
internal const val CREDENTIALS = "credentials"
51+
internal const val ID = "id"
52+
internal const val TITLE = "title"
53+
internal const val SUBTITLE = "subtitle"
54+
internal const val DISCLAIMER = "disclaimer"
55+
internal const val PATHS = "paths"
56+
internal const val VALUE = "value"
57+
internal const val DISPLAY = "display"
58+
59+
val TEST_PNV_1_GET_PHONE_NUMBER = PnvTokenRegistry(
60+
tokenId = "pnv_1",
61+
vct = VCT_GET_PHONE_NUMBER,
62+
title = "Phone Number",
63+
providerConsent = "CMWallet will enable your carrier {carrier name} to share your phone number iwth {app/domain name}",
64+
subscriptionHint = 1,
65+
carrierHint = "310250",
66+
phoneNumberHint = "+16502154321"
67+
)
68+
val TEST_PNV_1_VERIFY_PHONE_NUMBER = TEST_PNV_1_GET_PHONE_NUMBER.copy(
69+
vct = VCT_VERIFY_PHONE_NUMBER,
70+
)
71+
val TEST_PNV_2_GET_PHONE_NUMBER = PnvTokenRegistry(
72+
tokenId = "pnv_2",
73+
vct = VCT_GET_PHONE_NUMBER,
74+
title = "Phone Number",
75+
providerConsent = "CMWallet will enable your carrier MOCK-CARRIER-2 to share your phone number",
76+
subscriptionHint = 2,
77+
carrierHint = "380250",
78+
phoneNumberHint = "+16502157890"
79+
)
80+
81+
fun buildRegistryDatabase(items: List<PnvTokenRegistry>): ByteArray {
82+
val out = ByteArrayOutputStream()
83+
84+
// We don't support icon for phone number tokens, yet
85+
// Write the offset to the json
86+
val jsonOffset = 4
87+
val buffer = ByteBuffer.allocate(4)
88+
buffer.order(ByteOrder.LITTLE_ENDIAN)
89+
buffer.putInt(jsonOffset)
90+
out.write(buffer.array())
91+
92+
val sdJwtCredentials = JSONObject()
93+
for (item in items.map { it.toSdJwtRegistryItems() }) {
94+
val credJson = JSONObject()
95+
credJson.put(ID, item.id)
96+
credJson.put(TITLE, item.displayData.title)
97+
credJson.putOpt(SUBTITLE, item.displayData.subtitle)
98+
credJson.putOpt(DISCLAIMER, item.displayData.description)
99+
val paths = JSONObject()
100+
for (claim in item.claims) {
101+
paths.put(claim.path, JSONObject().putOpt(DISPLAY, claim.display).putOpt(VALUE, claim.value))
102+
}
103+
104+
credJson.put(PATHS, paths)
105+
val vctType = item.vct
106+
when (val current = sdJwtCredentials.opt(vctType) ?: JSONArray()) {
107+
is JSONArray -> sdJwtCredentials.put(vctType, current.put(credJson))
108+
else -> throw IllegalStateException("Unexpected type ${current::class.java}")
109+
}
110+
}
111+
val registryCredentials = JSONObject()
112+
registryCredentials.put(PNV_CRED_FORMAT, sdJwtCredentials)
113+
val registryJson = JSONObject()
114+
registryJson.put(CREDENTIALS, registryCredentials)
115+
out.write(registryJson.toString().toByteArray())
116+
return out.toByteArray()
117+
}
118+
}
119+
}
120+
121+
private class RegistryClaim(
122+
val path: String, // Single depth only
123+
val display: String?,
124+
val value: Any?,
125+
)
126+
127+
private class ItemDisplayData(val title: String, val subtitle: String?, val description: String?)
128+
129+
private class SdJwtRegistryItem(
130+
val id: String,
131+
val vct: String,
132+
val claims: List<RegistryClaim>,
133+
val displayData: ItemDisplayData,
134+
)

0 commit comments

Comments
 (0)