11package 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
327import org.json.JSONArray
428import org.json.JSONObject
529import java.io.ByteArrayOutputStream
630import java.nio.ByteBuffer
731import 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