@@ -5,23 +5,27 @@ import at.asitplus.catching
55import at.asitplus.dcapi.DCAPIHandover
66import at.asitplus.dcapi.DCAPIHandover.Companion.TYPE_DCAPI
77import at.asitplus.dcapi.DCAPIInfo
8+ import at.asitplus.dcapi.DCAPIResponse
89import at.asitplus.dcapi.DigitalCredentialInterface
910import at.asitplus.dcapi.IsoMdocResponse
1011import at.asitplus.dcapi.OpenID4VPDCAPIHandoverInfo
1112import at.asitplus.dcapi.OpenId4VpResponse
1213import at.asitplus.dcapi.SessionTranscriptContentHashable
14+ import at.asitplus.dcapi.request.IsoMdocRequest
1315import at.asitplus.dcapi.request.verifier.CredentialRequestOptions
14- import at.asitplus.dcapi.request.verifier.DigitalCredentialGetRequest
16+ import at.asitplus.dcapi.request.verifier.DigitalCredentialGetRequest.*
17+ import at.asitplus.dcapi.request.verifier.DigitalCredentialGetRequest.OpenId4Vp.SignedDataElement
1518import at.asitplus.dif.ClaimFormat
1619import at.asitplus.dif.DifInputDescriptor
1720import at.asitplus.dif.FormatContainerJwt
1821import at.asitplus.dif.FormatContainerSdJwt
1922import at.asitplus.dif.PresentationSubmissionDescriptor
2023import at.asitplus.iso.DeviceResponse
24+ import at.asitplus.iso.EncryptionInfo
25+ import at.asitplus.iso.EncryptionParameters
2126import at.asitplus.iso.SessionTranscript
2227import at.asitplus.iso.serializeOrigin
2328import at.asitplus.iso.sha256
24- import at.asitplus.jsonpath.JsonPath
2529import at.asitplus.openid.AuthenticationRequestParameters
2630import at.asitplus.openid.CredentialFormatEnum
2731import at.asitplus.openid.IdTokenType
@@ -36,10 +40,14 @@ import at.asitplus.openid.VpFormatsSupported
3640import at.asitplus.openid.dcql.DCQLCredentialQueryIdentifier
3741import at.asitplus.openid.dcql.DCQLQuery
3842import at.asitplus.openid.dcql.DCQLQueryResponse
43+ import at.asitplus.openid.dcql.toIso180137AnnexCDeviceRequest
3944import at.asitplus.rfc6749OAuth2AuthorizationFramework.ResponseType
45+ import at.asitplus.signum.indispensable.CryptoPrivateKey
46+ import at.asitplus.signum.indispensable.SecretExposure
4047import at.asitplus.signum.indispensable.SignatureAlgorithm
4148import at.asitplus.signum.indispensable.cosef.io.coseCompliantSerializer
4249import at.asitplus.signum.indispensable.cosef.toCoseAlgorithm
50+ import at.asitplus.signum.indispensable.cosef.toCoseKey
4351import at.asitplus.signum.indispensable.io.Base64UrlStrict
4452import at.asitplus.signum.indispensable.josef.JsonWebKey
4553import at.asitplus.signum.indispensable.josef.JsonWebKeySet
@@ -61,6 +69,7 @@ import at.asitplus.wallet.lib.cbor.VerifyCoseSignatureWithKey
6169import at.asitplus.wallet.lib.cbor.VerifyCoseSignatureWithKeyFun
6270import at.asitplus.wallet.lib.data.CredentialPresentationRequest.DCQLRequest
6371import at.asitplus.wallet.lib.data.CredentialPresentationRequest.PresentationExchangeRequest
72+ import at.asitplus.wallet.lib.data.IsoDocumentParsed
6473import at.asitplus.wallet.lib.data.VerifiablePresentationJws
6574import at.asitplus.wallet.lib.data.toBase64UrlJsonString
6675import at.asitplus.wallet.lib.extensions.sessionTranscriptThumbprint
@@ -76,6 +85,7 @@ import at.asitplus.wallet.lib.procedures.dcql.DCQLQueryAdapter
7685import at.asitplus.wallet.lib.utils.DefaultMapStore
7786import at.asitplus.wallet.lib.utils.MapStore
7887import io.github.aakira.napier.Napier
88+ import io.ktor.utils.io.core.*
7989import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
8090import kotlinx.serialization.decodeFromByteArray
8191import kotlinx.serialization.encodeToByteArray
@@ -124,8 +134,15 @@ class DcApiVerifier @JvmOverloads constructor(
124134 override val nonceService : NonceService = DefaultNonceService (),
125135 /* * Used to store issued authn requests to verify the authn response to it */
126136 private val stateToAuthnRequestStore : MapStore <String , AuthenticationRequestParameters > = DefaultMapStore (),
137+ /* * Used to store issued requests to verify the response to it */
138+ private val stateToIsoMdocRequestStore : MapStore <String , IsoMdocRequest > = DefaultMapStore (),
127139 /* * Algorithms supported to decrypt responses from wallets, for [metadataWithEncryption]. */
128140 private val supportedJweEncryptionAlgorithms : Set <JweEncryption > = JweEncryption .entries.toSet(),
141+ /* *
142+ * Workaround until signum is ready. Required for ISO 18013-7 Annex decryption.
143+ * Parameters: Serialized ephemeral key, cipher text, decryption key, encoded session transcript
144+ */
145+ private val decryptHpke : (suspend (ByteArray , ByteArray , CryptoPrivateKey .EC .WithPublicKey , ByteArray ) -> ByteArray )? = null ,
129146) : AbstractMdocVerifier() {
130147
131148 private val nonceAwareVerifier = NonceChallengeVerifier (
@@ -200,16 +217,20 @@ class DcApiVerifier @JvmOverloads constructor(
200217 CredentialRequestOptions .create(
201218 listOf (
202219 when (creationOptions) {
203- is DcApiCreationOptions .OpenId4VpUnsigned -> DigitalCredentialGetRequest . OpenId4VpUnsigned (
220+ is DcApiCreationOptions .OpenId4VpUnsigned -> OpenId4VpUnsigned (
204221 // client_id MUST be omitted in unsigned requests, per OpenID4VP 1.0 Appendix A.3.1
205222 createPlainAuthnRequest(requestOptions.copy(populateClientId = false ))
206223 )
207224
208- is DcApiCreationOptions .OpenId4VpSigned -> DigitalCredentialGetRequest . OpenId4VpSigned (
209- DigitalCredentialGetRequest . OpenId4Vp . SignedDataElement (
225+ is DcApiCreationOptions .OpenId4VpSigned -> OpenId4VpSigned (
226+ SignedDataElement (
210227 createSignedRequestObject(requestOptions).getOrThrow().jws
211228 )
212229 )
230+
231+ DcApiCreationOptions .Iso180137AnnexC -> IsoMdoc (
232+ createIsoMdocRequest(requestOptions)
233+ )
213234 }
214235 )
215236 )
@@ -241,6 +262,21 @@ class DcApiVerifier @JvmOverloads constructor(
241262 storeAuthnRequest(it, requestOptions.state)
242263 }
243264
265+ private suspend fun createIsoMdocRequest (
266+ requestOptions : OpenId4VpRequestOptions ,
267+ ): IsoMdocRequest {
268+ val deviceRequest = ((requestOptions.presentationRequest as ? DCQLRequest )?.dcqlQuery
269+ ? : throw IllegalArgumentException (" ISO 18013-7 Annex C requires a DCQL presentation request" ))
270+ .toIso180137AnnexCDeviceRequest()
271+
272+ val encryptionParameters = EncryptionParameters (
273+ nonceService.provideNonce().toByteArray(),
274+ decryptionKeyMaterial.publicKey.toCoseKey().getOrThrow()
275+ )
276+ return IsoMdocRequest (deviceRequest, EncryptionInfo (TYPE_DCAPI , encryptionParameters))
277+ .also { stateToIsoMdocRequestStore.put(requestOptions.state, it) }
278+ }
279+
244280 private suspend fun OpenId4VpRequestOptions.toAuthnRequest (): AuthenticationRequestParameters =
245281 AuthenticationRequestParameters (
246282 responseType = responseType,
@@ -311,7 +347,7 @@ class DcApiVerifier @JvmOverloads constructor(
311347 suspend fun validateAuthnResponse (
312348 input : String ,
313349 externalId : String ,
314- ): KmmResult <AuthnResponseResult > = catching {
350+ ): KmmResult <DcApiResponseResult > = catching {
315351 validateAuthnResponse(
316352 input = joseCompliantSerializer.decodeFromString<DigitalCredentialInterface >(input),
317353 externalId = externalId
@@ -326,9 +362,14 @@ class DcApiVerifier @JvmOverloads constructor(
326362 suspend fun validateAuthnResponse (
327363 input : DigitalCredentialInterface ,
328364 externalId : String ,
329- ): KmmResult <AuthnResponseResult > = catching {
365+ ): KmmResult <DcApiResponseResult > = catching {
330366 when (input) {
331- is IsoMdocResponse -> TODO ()
367+ is IsoMdocResponse -> validateResponse(
368+ receivedData = input.data,
369+ externalId = externalId,
370+ expectedOrigin = " TODO"
371+ ).getOrThrow()
372+
332373 else -> validateAuthnResponse(
333374 input = responseParser.parseAuthnResponse(input as OpenId4VpResponse ),
334375 externalId = externalId
@@ -375,6 +416,46 @@ class DcApiVerifier @JvmOverloads constructor(
375416 }
376417 }
377418
419+ @OptIn(SecretExposure ::class )
420+ suspend fun validateResponse (
421+ receivedData : DCAPIResponse ,
422+ externalId : String ,
423+ expectedOrigin : String
424+ ): KmmResult <Iso180137AnnexCWrapper > = catching {
425+ val isoMdocRequest = stateToIsoMdocRequestStore.get(externalId)!!
426+ val privateKey = decryptionKeyMaterial.exportPrivateKey().getOrThrow()
427+ as ? CryptoPrivateKey .EC .WithPublicKey
428+ ? : throw IllegalStateException (" Expected EC private key" )
429+
430+ val encryptedResponseData = receivedData.response.encryptedResponseData
431+ val serializedOrigin = expectedOrigin.serializeOrigin()
432+ ? : throw IllegalStateException (" Expected origin invalid" )
433+
434+ val sessionTranscript = createDcApiSessionTranscriptAnnexC(
435+ DCAPIInfo (
436+ encryptionInfo = isoMdocRequest.encryptionInfo,
437+ serializedOrigin = serializedOrigin,
438+ )
439+ )
440+ val encodedSessionTranscript = coseCompliantSerializer.encodeToByteArray(sessionTranscript)
441+ val encodedDeviceResponse = decryptHpke!! (
442+ encryptedResponseData.enc,
443+ encryptedResponseData.cipherText,
444+ privateKey,
445+ encodedSessionTranscript
446+ )
447+ val deviceResponse = coseCompliantSerializer.decodeFromByteArray<DeviceResponse >(encodedDeviceResponse)
448+
449+ Iso180137AnnexCWrapper (
450+ verifier.verifyPresentationIsoMdoc(
451+ input = deviceResponse,
452+ verifyDocument = verifyDocument(
453+ sessionTranscript = sessionTranscript
454+ )
455+ ).getOrThrow().documents
456+ )
457+ }
458+
378459 private fun AuthnResponseResult.isFullyValid (): Boolean =
379460 idTokenValidationResult?.isFailure != true &&
380461 vpTokenValidationResult?.isFailure != true &&
@@ -416,35 +497,13 @@ class DcApiVerifier @JvmOverloads constructor(
416497 val vpToken = responseParameters.parameters.vpToken
417498 ? : throw IllegalArgumentException (" vp_token not present in ${responseParameters.parameters} " )
418499 val clientIdRequired = responseParameters.clientIdRequired
419-
420500 val originalResponseParameters = responseParameters.originalResponseParameters
421-
422- (originalResponseParameters as ? ResponseParametersFrom .DcApi )?.let {
423- authnRequest.verifyExpectedOrigin(it.origin)
501+ require(originalResponseParameters is ResponseParametersFrom .DcApi ) {
502+ " Unsupported response parameters: $originalResponseParameters "
424503 }
504+ authnRequest.verifyExpectedOrigin(originalResponseParameters.origin)
425505
426- authnRequest.presentationDefinition?.let {
427- val presentationSubmission = responseParameters.parameters.presentationSubmission?.descriptorMap
428- ? : throw IllegalArgumentException (" Presentation Exchange need to present a presentation submission." )
429-
430- val presentation = presentationSubmission.associate { descriptor ->
431- descriptor.id to verifyPresentationResult(
432- claimFormat = descriptor.format,
433- relatedPresentation = descriptor.relatedPresentation(vpToken),
434- expectedNonce = expectedNonce,
435- input = responseParameters,
436- clientId = authnRequest.clientId,
437- responseUrl = authnRequest.responseUrl ? : authnRequest.redirectUrlExtracted,
438- transactionData = authnRequest.transactionData,
439- clientIdRequired = clientIdRequired,
440- origin = (originalResponseParameters as ? ResponseParametersFrom .DcApi )?.origin,
441- )
442- }
443-
444- VpTokenValidationResultPresentationExchange (
445- inputDescriptorResponseValidations = presentation,
446- )
447- } ? : authnRequest.dcqlQuery?.let { query ->
506+ authnRequest.dcqlQuery?.let { query ->
448507 val presentation = vpToken.jsonObject.mapKeys {
449508 DCQLCredentialQueryIdentifier (it.key)
450509 }.mapValues { (credentialQueryId, relatedPresentation) ->
@@ -462,7 +521,7 @@ class DcApiVerifier @JvmOverloads constructor(
462521 ? : authnRequest.redirectUrlExtracted,
463522 transactionData = authnRequest.transactionData,
464523 clientIdRequired = clientIdRequired,
465- origin = ( originalResponseParameters as ? ResponseParametersFrom . DcApi )? .origin,
524+ origin = originalResponseParameters.origin,
466525 requireCryptographicHolderBinding = query.credentialQuery(credentialQueryId)?.requireCryptographicHolderBinding,
467526 )
468527 }
@@ -490,9 +549,6 @@ class DcApiVerifier @JvmOverloads constructor(
490549 private fun DCQLQuery.credentialQuery (id : DCQLCredentialQueryIdentifier ) =
491550 credentials.associateBy { it.id }[id]
492551
493- private fun PresentationSubmissionDescriptor.relatedPresentation (vpToken : JsonElement ) =
494- JsonPath (cumulativeJsonPath).query(vpToken).first().value
495-
496552 private fun CredentialFormatEnum.toClaimFormat (): ClaimFormat = when (this ) {
497553 CredentialFormatEnum .JWT_VC -> ClaimFormat .JWT_VP
498554 CredentialFormatEnum .DC_SD_JWT -> ClaimFormat .SD_JWT
@@ -648,3 +704,9 @@ private val PresentationSubmissionDescriptor.cumulativeJsonPath: String
648704 }
649705 return cummulativeJsonPath
650706 }
707+
708+ sealed class DcApiResponseResult {
709+
710+ }
711+
712+ data class Iso180137AnnexCWrapper (val documents : Collection <IsoDocumentParsed >) : DcApiResponseResult()
0 commit comments