Skip to content

Commit fb916b1

Browse files
committed
[PNV Demo Part 2] Process a phone number request
1 parent 99143cd commit fb916b1

4 files changed

Lines changed: 195 additions & 8 deletions

File tree

app/src/main/java/com/credman/cmwallet/getcred/GetCredentialActivity.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.credman.cmwallet.openid4vp.OpenId4VP
3939
import com.credman.cmwallet.openid4vp.OpenId4VPMatchedCredential
4040
import com.credman.cmwallet.openid4vp.OpenId4VPMatchedMDocClaims
4141
import com.credman.cmwallet.openid4vp.OpenId4VPMatchedSdJwtClaims
42+
import com.credman.cmwallet.pnv.maybeHandlePnv
4243
import com.credman.cmwallet.sdjwt.SdJwt
4344
import com.credman.cmwallet.toBase64UrlNoPadding
4445
import com.google.android.gms.identitycredentials.Credential
@@ -238,6 +239,27 @@ class GetCredentialActivity : FragmentActivity() {
238239
val selectedId = selectedEntryId.getString("id")
239240
val dqclCredId = selectedEntryId.getString("dcql_cred_id")
240241

242+
val pnvResponse = maybeHandlePnv(
243+
it.requestJson,
244+
providerIdx,
245+
selectedId,
246+
dqclCredId,
247+
webOriginOrAppOrigin(
248+
origin,
249+
request.callingAppInfo.signingInfoCompat.signingCertificateHistory[0].toByteArray()
250+
),
251+
request.callingAppInfo
252+
)
253+
if (pnvResponse != null) {
254+
PendingIntentHandler.setGetCredentialResponse(
255+
resultData,
256+
GetCredentialResponse(DigitalCredential(pnvResponse.responseJsonModern))
257+
)
258+
setResult(RESULT_OK, resultData)
259+
finish()
260+
return
261+
}
262+
241263
val response = processDigitalCredentialOption(
242264
it.requestJson,
243265
providerIdx,
@@ -435,8 +457,6 @@ class GetCredentialActivity : FragmentActivity() {
435457
openId4VPRequest.performQueryOnCredential(selectedCredential, dcqlCredId)
436458
Log.i("GetCredentialActivity", "matchedCredential $matchedCredential")
437459

438-
439-
440460
return createOpenID4VPResponse(
441461
openId4VPRequest,
442462
origin,

app/src/main/java/com/credman/cmwallet/openid4vp/DCQL.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ data class MatchedCredential(
1919
val matchedClaims: MutableList<MatchedClaim> = mutableListOf()
2020
)
2121

22+
fun getDqclCredentialById(query: JSONObject, id: String): JSONObject? {
23+
require(query.has("credentials")) { "dcql_query must contain a credentials" }
24+
val credentials = query.getJSONArray("credentials")
25+
for (i in 0..<credentials.length()) {
26+
val dcqlCred = credentials.getJSONObject(i)
27+
if (dcqlCred.optString("id") == id) {
28+
return dcqlCred
29+
}
30+
}
31+
return null
32+
}
33+
2234
fun performQueryOnCredential(
2335
query: JSONObject,
2436
selectedCredential: CredentialItem,

app/src/main/java/com/credman/cmwallet/openid4vp/OpenId4VP.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ class OpenId4VP(
151151
return DCQLQuery(dcqlQuery, credentialStore)
152152
}
153153

154+
fun getDcqlCredentialObject(dcqlId: String): JSONObject? = getDqclCredentialById(dcqlQuery, dcqlId)
155+
156+
154157
fun performQueryOnCredential(selectedCredential: CredentialItem, dcqlCredId: String? = null): OpenId4VPMatchedCredential {
155158
return performQueryOnCredential(dcqlQuery, selectedCredential, dcqlCredId)
156159
}

app/src/main/java/com/credman/cmwallet/pnv/PnvTokenRegistry.kt

Lines changed: 158 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
11
package com.credman.cmwallet.pnv
22

3+
import android.util.Base64
4+
import android.util.Log
5+
import androidx.credentials.provider.CallingAppInfo
6+
import com.credman.cmwallet.CmWalletApplication.Companion.TAG
7+
import com.credman.cmwallet.CmWalletApplication.Companion.computeClientId
8+
import com.credman.cmwallet.createJWTES256
9+
import com.credman.cmwallet.getcred.GetCredentialActivity.DigitalCredentialRequestOptions
10+
import com.credman.cmwallet.getcred.GetCredentialActivity.DigitalCredentialResult
11+
import com.credman.cmwallet.jweSerialization
12+
import com.credman.cmwallet.jwsDeserialization
13+
import com.credman.cmwallet.loadECPrivateKey
14+
import com.credman.cmwallet.openid4vp.OpenId4VP
15+
import com.credman.cmwallet.openid4vp.OpenId4VP.Companion.IDENTIFIERS_1_0
16+
import com.credman.cmwallet.openid4vp.OpenId4VPMatchedCredential
17+
import com.credman.cmwallet.openid4vp.OpenId4VPMatchedMDocClaims
18+
import com.credman.cmwallet.openid4vp.OpenId4VPMatchedSdJwtClaims
19+
import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.TEST_PNV_1_GET_PHONE_NUMBER
20+
import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.TEST_PNV_1_VERIFY_PHONE_NUMBER
21+
import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.TEST_PNV_2_GET_PHONE_NUMBER
22+
import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.VCT_GET_PHONE_NUMBER
23+
import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.VCT_VERIFY_PHONE_NUMBER
24+
import com.credman.cmwallet.toJWK
25+
import kotlinx.serialization.json.buildJsonObject
26+
import kotlinx.serialization.json.put
327
import org.json.JSONArray
428
import org.json.JSONObject
529
import java.io.ByteArrayOutputStream
630
import java.nio.ByteBuffer
731
import java.nio.ByteOrder
32+
import java.security.interfaces.ECPrivateKey
33+
import java.time.Instant
834

935
/**
1036
* A phone number verification entry to be registered with the Credential Manager.
@@ -23,9 +49,10 @@ data class PnvTokenRegistry(
2349
val vct: String,
2450
val title: String,
2551
val providerConsent: String?,
26-
val subscriptionHint: Int?,
27-
val carrierHint: String?,
28-
val phoneNumberHint: String?
52+
val subscriptionHint: Int,
53+
val carrierHint: String,
54+
val phoneNumberHint: String?,
55+
val iss: String,
2956
) {
3057
/** Converts this TS43 entry to the more generic SD-JWT registry item(s). */
3158
private fun toSdJwtRegistryItems(): SdJwtRegistryItem {
@@ -63,7 +90,8 @@ data class PnvTokenRegistry(
6390
providerConsent = "CMWallet will enable your carrier {carrier name} to share your phone number iwth {app/domain name}",
6491
subscriptionHint = 1,
6592
carrierHint = "310250",
66-
phoneNumberHint = "+16502154321"
93+
phoneNumberHint = "+16502154321",
94+
iss = "https://example-carrier2.com",
6795
)
6896
val TEST_PNV_1_VERIFY_PHONE_NUMBER = TEST_PNV_1_GET_PHONE_NUMBER.copy(
6997
vct = VCT_VERIFY_PHONE_NUMBER,
@@ -75,7 +103,8 @@ data class PnvTokenRegistry(
75103
providerConsent = "CMWallet will enable your carrier MOCK-CARRIER-2 to share your phone number",
76104
subscriptionHint = 2,
77105
carrierHint = "380250",
78-
phoneNumberHint = "+16502157890"
106+
phoneNumberHint = "+16502157890",
107+
iss = "https://example-carrier2.com"
79108
)
80109

81110
fun buildRegistryDatabase(items: List<PnvTokenRegistry>): ByteArray {
@@ -131,4 +160,127 @@ private class SdJwtRegistryItem(
131160
val vct: String,
132161
val claims: List<RegistryClaim>,
133162
val displayData: ItemDisplayData,
134-
)
163+
)
164+
165+
fun maybeHandlePnv(
166+
requestJson: String,
167+
providerIdx: Int,
168+
selectedID: String,
169+
dcqlCredId: String,
170+
origin: String, // Either the web origin or the calling app sha
171+
callingAppInfo: CallingAppInfo
172+
): DigitalCredentialResult? {
173+
if (selectedID != TEST_PNV_1_GET_PHONE_NUMBER.tokenId && selectedID != TEST_PNV_2_GET_PHONE_NUMBER.tokenId) {
174+
return null
175+
}
176+
val digitalCredentialOptions = DigitalCredentialRequestOptions.createFrom(requestJson)
177+
val requestProtocol = DigitalCredentialRequestOptions.getRequestProtocolAtIndex(
178+
digitalCredentialOptions, providerIdx
179+
)
180+
val requestData: JSONObject = DigitalCredentialRequestOptions.getRequestDataAtIndex(
181+
digitalCredentialOptions, providerIdx
182+
)
183+
Log.i(PNV_TAG, "processDigitalCredentialOption protocol $requestProtocol")
184+
require(IDENTIFIERS_1_0.contains(requestProtocol)) {"Unsupported protocol identifier $requestProtocol"}
185+
val openId4VPRequest = OpenId4VP(requestData, computeClientId(callingAppInfo), requestProtocol)
186+
Log.i(PNV_TAG, "nonce ${openId4VPRequest.nonce}")
187+
188+
val dcqlObject = openId4VPRequest.getDcqlCredentialObject(dcqlCredId)!!
189+
Log.i(PNV_TAG, "dqcl $dcqlObject")
190+
val vct = dcqlObject.getJSONObject("meta").getJSONArray("vct_values").let {
191+
for (i in 0..<it.length()) {
192+
val currVct = it.getString(i)
193+
if (currVct == VCT_GET_PHONE_NUMBER || currVct == VCT_VERIFY_PHONE_NUMBER) {
194+
return@let currVct
195+
}
196+
}
197+
throw IllegalStateException("Could not find a valid vct value for pnv")
198+
}
199+
200+
val selectedCred = when (selectedID) {
201+
TEST_PNV_1_GET_PHONE_NUMBER.tokenId -> if (vct == TEST_PNV_1_GET_PHONE_NUMBER.vct) TEST_PNV_1_GET_PHONE_NUMBER else TEST_PNV_1_VERIFY_PHONE_NUMBER
202+
TEST_PNV_2_GET_PHONE_NUMBER.tokenId -> TEST_PNV_2_GET_PHONE_NUMBER
203+
else -> return null
204+
}
205+
206+
val credAuthJwt = dcqlObject.getJSONObject("meta").getString("credential_authorization_jwt")
207+
val (credAuthJwtHeader, credAuthJwtpayload) = jwsDeserialization(credAuthJwt)
208+
209+
require(credAuthJwtHeader.has("x5c")) { "Missing aggregator cert" }
210+
val aggregatorCertChain = credAuthJwtHeader.getJSONArray("x5c") // See the x5c cert chain defined at https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6
211+
// TODO: validate the aggregator cert is allowed to request phone number verification for the given carrier
212+
213+
val consentData = credAuthJwtpayload.optString("consent_data")
214+
// TODO: when the matcher renders the consent data, create the consent data digest to sign over
215+
// and prove that it has been displayed.
216+
val consentDataHash: String? = null
217+
218+
val aggregatorNonce = credAuthJwtpayload.getString("nonce")
219+
require(aggregatorNonce == openId4VPRequest.nonce) { "Aggregator nonce should match the verifier nonce" }
220+
221+
val aggregatorJwks = credAuthJwtpayload.getJSONObject("jwks").getJSONArray("keys")
222+
val aggregatorEncKey = aggregatorJwks.let {
223+
for (i in 0..<it.length()) {
224+
val jwk = it[i] as JSONObject
225+
if (jwk.has("use")
226+
&& jwk["use"] == "enc"
227+
&& jwk["kty"] == "EC"
228+
&& jwk["crv"] == "P-256"
229+
) {
230+
return@let jwk
231+
}
232+
}
233+
throw IllegalArgumentException("Given aggregator did not provide a valid encryption key")
234+
}
235+
236+
// Generate the phone number token SD-JWT
237+
val tempTokenJson = buildJsonObject {
238+
put("iss", selectedCred.iss)
239+
put("vct", selectedCred.vct)
240+
put("temp_token", getTempTokenForCredential(selectedCred))
241+
put("subscription_hint", selectedCred.subscriptionHint)
242+
put("carrier_hint", selectedCred.carrierHint)
243+
}
244+
val encryptedTempTokenJwe = jweSerialization(aggregatorEncKey, tempTokenJson.toString())
245+
246+
val tmpKey =
247+
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg6ef4-enmfQHRWUW40-Soj3aFB0rsEOp3tYMW-HJPBvChRANCAAT5N1NLZcub4bOgWfBwF8MHPGkfJ8Dm300cioatq9XovaLgG205FEXUOuNMEMQuLbrn8oiOC0nTnNIVn-OtSmSb"
248+
val privateKey =
249+
loadECPrivateKey(Base64.decode(tmpKey, Base64.URL_SAFE)) as ECPrivateKey
250+
val tempTokenDcJwt = createJWTES256(
251+
header = buildJsonObject {
252+
put("alg", "ES256")
253+
},
254+
payload = buildJsonObject {
255+
put("nonce", openId4VPRequest.nonce)
256+
put("origin", origin)
257+
put("encrypted_credential", encryptedTempTokenJwe)
258+
},
259+
privateKey = privateKey
260+
)
261+
262+
// We don't use selective disclosure, so the sd-jwt is simply jwt + "~"
263+
val tempTokenDcSdJwt = "${tempTokenDcJwt}~"
264+
265+
val vpToken = JSONObject().apply {
266+
put(dcqlCredId, tempTokenDcSdJwt)
267+
}
268+
val response = openId4VPRequest.generateResponse(vpToken)
269+
Log.d(PNV_TAG, "Returning $response")
270+
271+
return DigitalCredentialResult(
272+
responseJsonLegacy = "",
273+
authenticationTitle = "",
274+
authenticationSubtitle = null,
275+
responseJsonModern = JSONObject().apply {
276+
put("protocol", openId4VPRequest.protocolIdentifier)
277+
put("data", JSONObject(response))
278+
}.toString()
279+
)
280+
}
281+
282+
fun getTempTokenForCredential(cred: PnvTokenRegistry): String {
283+
return "TODO: generate temp token"
284+
}
285+
286+
private const val PNV_TAG = "PnvHandler"

0 commit comments

Comments
 (0)