From ebfe10e9bd2659f5ccbb3a88fc16deeea4dcfc62 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Thu, 26 Sep 2024 09:37:14 +0200 Subject: [PATCH 1/6] SIOPv2: Support requesting multiple credentials --- CHANGELOG.md | 3 + .../wallet/lib/oidc/OidcSiopVerifier.kt | 91 ++--- ...ationRequestParameterFromSerializerTest.kt | 9 +- .../lib/oidc/OidcSiopCombinedProtocolTest.kt | 345 ++++++------------ .../OidcSiopCombinedProtocolTwoStepTest.kt | 140 ++----- .../wallet/lib/oidc/OidcSiopInteropTest.kt | 10 +- .../lib/oidc/OidcSiopIsoProtocolTest.kt | 52 ++- .../wallet/lib/oidc/OidcSiopProtocolTest.kt | 51 ++- .../lib/oidc/OidcSiopSdJwtProtocolTest.kt | 55 +-- .../oidc/OidcSiopWalletScopeSupportTest.kt | 34 +- .../wallet/lib/oidc/OidcSiopX509SanDnsTest.kt | 9 +- 11 files changed, 319 insertions(+), 480 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 986718ce..ad5275a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ Release NEXT: - Introudce `OAuth2Client` to extract creating authentication requests and token requests from OID4VCI `WalletService` - Refactor `SimpleAuthorizationService` to extract actual authentication and authorization into `AuthorizationServiceStrategy` - Implement JWE encryption with AES-CBC-HMAC algorithms + - SIOPv2/OpenID4VP: Support requesting and receiving claims from different credentials, i.e. a combined presentation + - Require request options on every method in `OidcSiopVerifier` + - Move `credentialScheme`, `representation`, `requestedAttributes` from `RequestOptions` to `RequestOptionsCredentials` Release 4.1.2: * In `OidcSiopVerifier` add parameter `nonceService` to externalize creation and validation of nonces, e.g. for deployments in load-balanced environments diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index 2237d3a7..aa26b2e6 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -161,26 +161,17 @@ class OidcSiopVerifier private constructor( data class RequestOptions( /** - * Response mode to request, see [OpenIdConstants.ResponseMode] + * Requested credentials, should be at least one */ - val responseMode: OpenIdConstants.ResponseMode? = null, + val credentials: Set, /** - * Required representation, see [ConstantIndex.CredentialRepresentation] + * Response mode to request, see [OpenIdConstants.ResponseMode] */ - val representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT, + val responseMode: OpenIdConstants.ResponseMode? = null, /** * Opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult] */ val state: String? = uuid4().toString(), - /** - * Credential type to request, or `null` to make no restrictions - */ - val credentialScheme: ConstantIndex.CredentialScheme? = null, - /** - * List of attributes that shall be requested explicitly (selective disclosure), - * or `null` to make no restrictions - */ - val requestedAttributes: List? = null, /** * Optional URL to include [metadata] by reference instead of by value (directly embedding in authn request) */ @@ -192,12 +183,28 @@ class OidcSiopVerifier private constructor( val encryption: Boolean = false, ) + data class RequestOptionsCredential( + /** + * Credential type to request, or `null` to make no restrictions + */ + val credentialScheme: ConstantIndex.CredentialScheme, + /** + * Required representation, see [ConstantIndex.CredentialRepresentation] + */ + val representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT, + /** + * List of attributes that shall be requested explicitly (selective disclosure), + * or `null` to make no restrictions + */ + val requestedAttributes: List? = null, + ) + /** * Creates an OIDC Authentication Request, encoded as query parameters to the [walletUrl]. */ suspend fun createAuthnRequestUrl( walletUrl: String, - requestOptions: RequestOptions = RequestOptions(), + requestOptions: RequestOptions, ): String { val urlBuilder = URLBuilder(walletUrl) createAuthnRequest(requestOptions).encodeToParameters() @@ -211,7 +218,7 @@ class OidcSiopVerifier private constructor( */ suspend fun createAuthnRequestUrlWithRequestObject( walletUrl: String, - requestOptions: RequestOptions = RequestOptions() + requestOptions: RequestOptions, ): KmmResult = catching { val jar = createAuthnRequestAsSignedRequestObject(requestOptions).getOrThrow() val urlBuilder = URLBuilder(walletUrl) @@ -234,7 +241,7 @@ class OidcSiopVerifier private constructor( suspend fun createAuthnRequestUrlWithRequestObjectByReference( walletUrl: String, requestUrl: String, - requestOptions: RequestOptions = RequestOptions() + requestOptions: RequestOptions, ): KmmResult> = catching { val jar = createAuthnRequestAsSignedRequestObject(requestOptions).getOrThrow() val urlBuilder = URLBuilder(walletUrl) @@ -260,7 +267,7 @@ class OidcSiopVerifier private constructor( * ``` */ suspend fun createAuthnRequestAsSignedRequestObject( - requestOptions: RequestOptions = RequestOptions(), + requestOptions: RequestOptions, ): KmmResult = catching { val requestObject = createAuthnRequest(requestOptions) val requestObjectSerialized = jsonSerializer.encodeToString( @@ -286,9 +293,9 @@ class OidcSiopVerifier private constructor( * Callers may serialize the result with `result.encodeToParameters().formUrlEncode()` */ suspend fun createAuthnRequest( - requestOptions: RequestOptions = RequestOptions(), + requestOptions: RequestOptions, ) = AuthenticationRequestParameters( - responseType = "$ID_TOKEN $VP_TOKEN", + responseType = "$ID_TOKEN $VP_TOKEN", // TODO move to RequestOptions clientId = clientId, redirectUrl = requestOptions.buildRedirectUrl(), responseUrl = responseUrl, @@ -303,20 +310,17 @@ class OidcSiopVerifier private constructor( state = requestOptions.state, presentationDefinition = PresentationDefinition( id = uuid4().toString(), - formats = requestOptions.representation.toFormatHolder(), - inputDescriptors = listOf( - requestOptions.toInputDescriptor() - ), + inputDescriptors = requestOptions.credentials.map { + it.toInputDescriptor() + }, ), ) - private fun RequestOptions.buildScope() = listOfNotNull( - SCOPE_OPENID, - SCOPE_PROFILE, - credentialScheme?.sdJwtType, - credentialScheme?.vcType, - credentialScheme?.isoNamespace - ) + private fun RequestOptions.buildScope() = ( + listOf(SCOPE_OPENID, SCOPE_PROFILE) + + credentials.mapNotNull { it.credentialScheme.sdJwtType } + + credentials.mapNotNull { it.credentialScheme.vcType } + + credentials.mapNotNull { it.credentialScheme.isoNamespace }) .joinToString(" ") private val clientId: String? by lazy { @@ -333,8 +337,9 @@ class OidcSiopVerifier private constructor( ) null else relyingPartyUrl //TODO extend for InputDescriptor interface in case QES - private fun RequestOptions.toInputDescriptor() = DifInputDescriptor( + private fun RequestOptionsCredential.toInputDescriptor() = DifInputDescriptor( id = buildId(), + format = toFormatHolder(), constraints = toConstraint(), ) @@ -343,26 +348,24 @@ class OidcSiopVerifier private constructor( * encoding it into the descriptor id as in the following non-normative example fow now: * https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#appendix-A.3.1-4 */ - private fun RequestOptions.buildId() = - if (credentialScheme?.isoDocType != null && representation == ConstantIndex.CredentialRepresentation.ISO_MDOC) + private fun RequestOptionsCredential.buildId() = + if (credentialScheme.isoDocType != null && representation == ConstantIndex.CredentialRepresentation.ISO_MDOC) credentialScheme.isoDocType!! else uuid4().toString() - private fun RequestOptions.toConstraint() = + private fun RequestOptionsCredential.toConstraint() = Constraint(fields = (toAttributeConstraints() + toTypeConstraint()).filterNotNull()) - private fun RequestOptions.toAttributeConstraints() = + private fun RequestOptionsCredential.toAttributeConstraints() = requestedAttributes?.createConstraints(representation, credentialScheme) ?: listOf() - private fun RequestOptions.toTypeConstraint() = credentialScheme?.let { - when (representation) { - ConstantIndex.CredentialRepresentation.PLAIN_JWT -> it.toVcConstraint() - ConstantIndex.CredentialRepresentation.SD_JWT -> it.toSdJwtConstraint() - ConstantIndex.CredentialRepresentation.ISO_MDOC -> null - } + private fun RequestOptionsCredential.toTypeConstraint() = when (representation) { + ConstantIndex.CredentialRepresentation.PLAIN_JWT -> this.credentialScheme.toVcConstraint() + ConstantIndex.CredentialRepresentation.SD_JWT -> this.credentialScheme.toSdJwtConstraint() + ConstantIndex.CredentialRepresentation.ISO_MDOC -> null } - private fun ConstantIndex.CredentialRepresentation.toFormatHolder() = when (this) { + private fun RequestOptionsCredential.toFormatHolder() = when (representation) { ConstantIndex.CredentialRepresentation.PLAIN_JWT -> FormatHolder(jwtVp = containerJwt) ConstantIndex.CredentialRepresentation.SD_JWT -> FormatHolder(jwtSd = containerJwt) ConstantIndex.CredentialRepresentation.ISO_MDOC -> FormatHolder(msoMdoc = containerJwt) @@ -387,10 +390,10 @@ class OidcSiopVerifier private constructor( ) else null private fun List.createConstraints( - credentialRepresentation: ConstantIndex.CredentialRepresentation, + representation: ConstantIndex.CredentialRepresentation, credentialScheme: ConstantIndex.CredentialScheme?, ): Collection = map { - if (credentialRepresentation == ConstantIndex.CredentialRepresentation.ISO_MDOC) + if (representation == ConstantIndex.CredentialRepresentation.ISO_MDOC) credentialScheme.toConstraintField(it) else ConstraintField(path = listOf("\$[${it.quote()}]")) diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt index 1307b4b6..570948a0 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt @@ -1,8 +1,8 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.openid.AuthenticationRequestParameters -import at.asitplus.wallet.lib.agent.HolderAgent import at.asitplus.wallet.lib.agent.EphemeralKeyWithoutCert +import at.asitplus.wallet.lib.agent.HolderAgent import at.asitplus.wallet.lib.agent.VerifierAgent import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.oidvci.decodeFromUrlQuery @@ -39,8 +39,11 @@ class AuthenticationRequestParameterFromSerializerTest : FreeSpec({ representations.forEach { representation -> val reqOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = representation, + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, representation + ) + ) ) "URL test $representation" { diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTest.kt index 586f71ff..1c8cea0c 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTest.kt @@ -1,21 +1,19 @@ package at.asitplus.wallet.lib.oidc -import at.asitplus.dif.DifInputDescriptor +import at.asitplus.wallet.eupid.EuPidScheme +import at.asitplus.wallet.lib.agent.* import at.asitplus.wallet.lib.data.AtomicAttribute2023 import at.asitplus.wallet.lib.data.ConstantIndex -import at.asitplus.dif.FormatHolder -import at.asitplus.wallet.lib.agent.* +import at.asitplus.wallet.lib.data.ConstantIndex.CredentialRepresentation import at.asitplus.wallet.lib.oidvci.OAuth2Exception import at.asitplus.wallet.mdl.MobileDrivingLicenceScheme import com.benasher44.uuid.uuid4 -import io.github.aakira.napier.Napier import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldNotBeEmpty import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf -import kotlinx.coroutines.runBlocking class OidcSiopCombinedProtocolTest : FreeSpec({ @@ -25,7 +23,6 @@ class OidcSiopCombinedProtocolTest : FreeSpec({ lateinit var verifierKeyMaterial: KeyMaterial lateinit var holderAgent: Holder - lateinit var verifierAgent: Verifier lateinit var holderSiop: OidcSiopWallet lateinit var verifierSiop: OidcSiopVerifier @@ -35,7 +32,6 @@ class OidcSiopCombinedProtocolTest : FreeSpec({ verifierKeyMaterial = EphemeralKeyWithoutCert() relyingPartyUrl = "https://example.com/rp/${uuid4()}" holderAgent = HolderAgent(holderKeyMaterial) - verifierAgent = VerifierAgent(verifierKeyMaterial) holderSiop = OidcSiopWallet( keyMaterial = holderKeyMaterial, @@ -47,79 +43,50 @@ class OidcSiopCombinedProtocolTest : FreeSpec({ ) } - "test support for format holder specification" - { + "support for format holder specification" - { - "test support for plain jwt credential request" - { + "support for plain jwt credential request" - { "if not available despite others with correct format or correct attribute, but not both" { - runBlocking { - holderAgent.storeJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme) - holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.PLAIN_JWT, - ) - ).let { request -> - request.copy( - clientMetadata = request.clientMetadata?.let { clientMetadata -> - clientMetadata.copy( - vpFormats = FormatHolder( - // only allow plain jwt - jwtVp = clientMetadata.vpFormats?.jwtVp - ) + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, CredentialRepresentation.PLAIN_JWT ) - } + ) ) - } - + ) shouldThrow { holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() } } "if available despite others" { - runBlocking { - holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme) - holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - } - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - ) - ).let { request -> - request.copy( - clientMetadata = request.clientMetadata?.let { clientMetadata -> - clientMetadata.copy( - vpFormats = FormatHolder( - // only allow plain jwt - jwtVp = clientMetadata.vpFormats?.jwtVp - ) + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + CredentialRepresentation.PLAIN_JWT ) - } + ) ) - } + ) val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() - authnResponse.shouldBeInstanceOf() - .also { println(it) } + .shouldBeInstanceOf() val result = verifierSiop.validateAuthnResponse(authnResponse.url) - result.shouldBeInstanceOf() + .shouldBeInstanceOf() result.vp.verifiableCredentials.shouldNotBeEmpty() result.vp.verifiableCredentials.forEach { it.vc.credentialSubject.shouldBeInstanceOf() @@ -127,230 +94,150 @@ class OidcSiopCombinedProtocolTest : FreeSpec({ } } - "test support for sd jwt credential request" - { + "support for sd jwt credential request" - { "if not available despite others with correct format or correct attribute, but not both" { - runBlocking { - holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeSdJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme) - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeSdJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.SD_JWT, - ) - ).let { request -> - request.copy( - presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - formats = FormatHolder( - // only support SD_JWT here - jwtSd = presentationDefinition.formats?.jwtSd, - ), - inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> - (inputDescriptor as DifInputDescriptor).copy( - format = null - ) - } + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, CredentialRepresentation.SD_JWT ) - }, + ) ) - } - + ) shouldThrow { holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() } } "if available despite others with correct format or correct attribute, but not both" { - runBlocking { - holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeSdJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme) - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeSdJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.SD_JWT, - ) - ).let { request -> - request.copy( - presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - formats = FormatHolder( - // only support SD_JWT here - jwtSd = presentationDefinition.formats?.jwtSd, - ), - inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> - (inputDescriptor as DifInputDescriptor).copy( - format = null - ) - } + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, CredentialRepresentation.SD_JWT ) - }, + ) ) - } - + ) val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() - authnResponse.shouldBeInstanceOf() - .also { println(it) } + .shouldBeInstanceOf() val result = verifierSiop.validateAuthnResponse(authnResponse.url) - result.shouldBeInstanceOf() + .shouldBeInstanceOf() result.sdJwt.type?.shouldContain(ConstantIndex.AtomicAttribute2023.vcType) } } - "test support for mso credential request" - { + "support for mso credential request" - { "if not available despite others with correct format or correct attribute, but not both" { - runBlocking { - holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme) val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - ), - ).let { request -> - request.copy( - presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - // only support msoMdoc here - formats = FormatHolder( - msoMdoc = presentationDefinition.formats?.msoMdoc - ), - inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> - (inputDescriptor as DifInputDescriptor).copy( - format = null - ) - } + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, CredentialRepresentation.ISO_MDOC ) - }, - ) - } - Napier.d("Create response") - + ) + ), + ) shouldThrow { - holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow().also { - Napier.d("response: $it") - } + holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() } } "if available despite others with correct format or correct attribute, but not both" { - runBlocking { - holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme) val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - ), - ).let { request -> - request.copy( - presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - formats = FormatHolder( - // only support msoMdoc here - msoMdoc = presentationDefinition.formats?.msoMdoc - ), - inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> - (inputDescriptor as DifInputDescriptor).copy( - format = null - ) - } + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + CredentialRepresentation.ISO_MDOC ) - }, - ) - } - - Napier.d("request: $authnRequest") + ) + ), + ) val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() - authnResponse.shouldBeInstanceOf() - .also { println(it) } + .shouldBeInstanceOf() - val result = verifierSiop.validateAuthnResponse(authnResponse.url) - result.shouldBeInstanceOf() + verifierSiop.validateAuthnResponse(authnResponse.url) + .shouldBeInstanceOf() } } } - "test presentation of multiple credentials with different formats" { - runBlocking { - holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + "presentation of multiple credentials with different formats in one request/response" { + holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme) - val authnRequest1 = verifierSiop.createAuthnRequest( + val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.PLAIN_JWT, - ), - ) - val authnRequest2 = verifierSiop.createAuthnRequest( - requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = MobileDrivingLicenceScheme, - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, CredentialRepresentation.PLAIN_JWT + ), + OidcSiopVerifier.RequestOptionsCredential( + MobileDrivingLicenceScheme, CredentialRepresentation.ISO_MDOC + ) + ) ), ) - val inputDescriptors2 = authnRequest2.presentationDefinition?.inputDescriptors ?: listOf() - - val authnRequest = authnRequest1.copy( - presentationDefinition = authnRequest1.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - inputDescriptors = presentationDefinition.inputDescriptors.map { - (it as DifInputDescriptor).copy(format = presentationDefinition.formats) - } + inputDescriptors2.map { - (it as DifInputDescriptor).copy(format = authnRequest2.presentationDefinition?.formats) - }, - formats = null, + val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() + .shouldBeInstanceOf() + + val validationResults = verifierSiop.validateAuthnResponse(authnResponse.url) + .shouldBeInstanceOf() + validationResults.validationResults.size shouldBe 2 + } + + "presentation of multiple SD-JWT credentials in one request/response" { + holderAgent.storeSdJwtCredential(holderKeyMaterial, EuPidScheme) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + + val requestOptions = OidcSiopVerifier.RequestOptions( + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + credentialScheme = ConstantIndex.AtomicAttribute2023, + representation = CredentialRepresentation.SD_JWT, + requestedAttributes = listOf("date_of_birth"), + ), + OidcSiopVerifier.RequestOptionsCredential( + credentialScheme = EuPidScheme, + representation = CredentialRepresentation.SD_JWT, + requestedAttributes = listOf( + EuPidScheme.Attributes.FAMILY_NAME, + EuPidScheme.Attributes.GIVEN_NAME + ), ) - } + ) ) + val authnRequest = verifierSiop.createAuthnRequest(requestOptions) val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() - authnResponse.shouldBeInstanceOf() - .also { println(it) } + .shouldBeInstanceOf() val validationResults = verifierSiop.validateAuthnResponse(authnResponse.url) - validationResults.shouldBeInstanceOf() + .shouldBeInstanceOf() validationResults.validationResults.size shouldBe 2 + // TODO verify that the correct credentials are actually returned } }) @@ -365,7 +252,7 @@ private suspend fun Holder.storeJwtCredential( ).issueCredential( holderKeyMaterial.publicKey, credentialScheme, - ConstantIndex.CredentialRepresentation.PLAIN_JWT, + CredentialRepresentation.PLAIN_JWT, ).getOrThrow().toStoreCredentialInput() ) } @@ -381,7 +268,7 @@ private suspend fun Holder.storeSdJwtCredential( ).issueCredential( holderKeyMaterial.publicKey, credentialScheme, - ConstantIndex.CredentialRepresentation.SD_JWT, + CredentialRepresentation.SD_JWT, ).getOrThrow().toStoreCredentialInput() ) } @@ -396,6 +283,6 @@ private suspend fun Holder.storeIsoCredential( ).issueCredential( holderKeyMaterial.publicKey, credentialScheme, - ConstantIndex.CredentialRepresentation.ISO_MDOC, + CredentialRepresentation.ISO_MDOC, ).getOrThrow().toStoreCredentialInput() ) diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt index af82947d..661c7661 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt @@ -1,9 +1,7 @@ package at.asitplus.wallet.lib.oidc -import at.asitplus.dif.DifInputDescriptor -import at.asitplus.wallet.lib.data.ConstantIndex -import at.asitplus.dif.FormatHolder import at.asitplus.wallet.lib.agent.* +import at.asitplus.wallet.lib.data.ConstantIndex import com.benasher44.uuid.uuid4 import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrowAny @@ -11,7 +9,6 @@ import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.maps.shouldHaveSize import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.types.shouldBeInstanceOf -import kotlinx.coroutines.runBlocking class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ @@ -21,17 +18,15 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ lateinit var verifierKeyMaterial: KeyMaterial lateinit var holderAgent: Holder - lateinit var verifierAgent: Verifier lateinit var holderSiop: OidcSiopWallet lateinit var verifierSiop: OidcSiopVerifier beforeEach { - holderKeyMaterial = EphemeralKeyWithoutCert() + holderKeyMaterial = EphemeralKeyWithoutCert() verifierKeyMaterial = EphemeralKeyWithoutCert() relyingPartyUrl = "https://example.com/rp/${uuid4()}" holderAgent = HolderAgent(holderKeyMaterial) - verifierAgent = VerifierAgent(verifierKeyMaterial) holderSiop = OidcSiopWallet( keyMaterial = holderKeyMaterial, @@ -45,37 +40,20 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ "test credential matching" - { "only credentials of the correct format are matched" { - runBlocking { - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - ) - ).let { request -> - request.copy( - presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - // only support msoMdoc here - formats = FormatHolder( - msoMdoc = presentationDefinition.formats?.msoMdoc - ), - inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> - (inputDescriptor as DifInputDescriptor).copy(format = null) - } + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + ConstantIndex.CredentialRepresentation.ISO_MDOC ) - }, + ) ) - } + ) val preparationState = holderSiop.startAuthorizationResponsePreparation(authnRequest.serialize()) .getOrThrow() val presentationDefinition = preparationState.presentationDefinition.shouldNotBeNull() @@ -94,41 +72,22 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ } "test credential submission" - { - "submission requirements need to macth" - { + "submission requirements need to match" - { "all credentials matching an input descriptor should be presentable" { - runBlocking { - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - ) - ).let { request -> - request.copy( - presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - // only support msoMdoc here - formats = FormatHolder( - msoMdoc = presentationDefinition.formats?.msoMdoc - ), - inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> - (inputDescriptor as DifInputDescriptor).copy( - format = null - ) - } + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + ConstantIndex.CredentialRepresentation.ISO_MDOC ) - }, + ) ) - } + ) val params = holderSiop.parseAuthenticationRequestParameters(authnRequest.serialize()).getOrThrow() val preparationState = holderSiop.startAuthorizationResponsePreparation(params).getOrThrow() @@ -164,39 +123,20 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ } } "credentials not matching an input descriptor should not yield a valid submission" { - runBlocking { - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) - } - - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) + holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023) val sdJwtMatches = run { val authnRequestSdJwt = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.SD_JWT, - ) - ).let { request -> - request.copy( - presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - // only support msoMdoc here - inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> - (inputDescriptor as DifInputDescriptor).copy( - format = FormatHolder( - jwtSd = presentationDefinition.formats?.jwtSd - ), - ) - } + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, ConstantIndex.CredentialRepresentation.SD_JWT ) - }, + ) ) - } + ) val preparationStateSdJwt = holderSiop.startAuthorizationResponsePreparation( holderSiop.parseAuthenticationRequestParameters(authnRequestSdJwt.serialize()).getOrThrow() @@ -220,25 +160,13 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ val authnRequest = verifierSiop.createAuthnRequest( requestOptions = OidcSiopVerifier.RequestOptions( - credentialScheme = ConstantIndex.AtomicAttribute2023, - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - ) - ).let { request -> - request.copy( - presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> - presentationDefinition.copy( - // only support msoMdoc here - inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> - (inputDescriptor as DifInputDescriptor).copy( - format = FormatHolder( - msoMdoc = presentationDefinition.formats?.msoMdoc - ), - ) - } + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, ConstantIndex.CredentialRepresentation.ISO_MDOC ) - }, + ) ) - } + ) val params = holderSiop.parseAuthenticationRequestParameters(authnRequest.serialize()).getOrThrow() val preparationState = holderSiop.startAuthorizationResponsePreparation(params).getOrThrow() diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt index f420e907..c069c149 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt @@ -337,9 +337,13 @@ class OidcSiopInteropTest : FreeSpec({ requestUrl = requestUrl, requestOptions = OidcSiopVerifier.RequestOptions( responseMode = OpenIdConstants.ResponseMode.DIRECT_POST, - representation = ConstantIndex.CredentialRepresentation.SD_JWT, - credentialScheme = ConstantIndex.AtomicAttribute2023, - requestedAttributes = listOf("family_name", "given_name") + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + ConstantIndex.CredentialRepresentation.SD_JWT, + listOf("family_name", "given_name") + ) + ) ) ).getOrThrow().also { println(it) } diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt index 415d1e4e..3605f866 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt @@ -31,7 +31,7 @@ class OidcSiopIsoProtocolTest : FreeSpec({ lateinit var verifierSiop: OidcSiopVerifier beforeEach { - holderKeyMaterial = EphemeralKeyWithoutCert() + holderKeyMaterial = EphemeralKeyWithoutCert() verifierKeyMaterial = EphemeralKeyWithoutCert() relyingPartyUrl = "https://example.com/rp/${uuid4()}" walletUrl = "https://example.com/wallet/${uuid4()}" @@ -73,11 +73,13 @@ class OidcSiopIsoProtocolTest : FreeSpec({ verifierSiop, walletUrl, OidcSiopVerifier.RequestOptions( - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - credentialScheme = MobileDrivingLicenceScheme, - requestedAttributes = listOf( - MobileDrivingLicenceDataElements.GIVEN_NAME - ), + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + MobileDrivingLicenceScheme, ConstantIndex.CredentialRepresentation.ISO_MDOC, listOf( + MobileDrivingLicenceDataElements.GIVEN_NAME + ) + ) + ) ), holderSiop ) @@ -95,9 +97,13 @@ class OidcSiopIsoProtocolTest : FreeSpec({ verifierSiop, walletUrl, OidcSiopVerifier.RequestOptions( - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - credentialScheme = ConstantIndex.AtomicAttribute2023, - requestedAttributes = listOf("given_name"), + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + ConstantIndex.CredentialRepresentation.ISO_MDOC, + listOf("given_name") + ) + ) ), holderSiop ) @@ -116,9 +122,13 @@ class OidcSiopIsoProtocolTest : FreeSpec({ verifierSiop, walletUrl, OidcSiopVerifier.RequestOptions( - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - credentialScheme = MobileDrivingLicenceScheme, - requestedAttributes = listOf(requestedClaim), + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + MobileDrivingLicenceScheme, + ConstantIndex.CredentialRepresentation.ISO_MDOC, + listOf(requestedClaim) + ) + ) ), holderSiop, ) @@ -137,9 +147,11 @@ class OidcSiopIsoProtocolTest : FreeSpec({ responseUrl = relyingPartyUrl + "/${uuid4()}" ) val requestOptions = OidcSiopVerifier.RequestOptions( - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - credentialScheme = MobileDrivingLicenceScheme, - requestedAttributes = listOf(requestedClaim), + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + MobileDrivingLicenceScheme, ConstantIndex.CredentialRepresentation.ISO_MDOC, listOf(requestedClaim) + ) + ), responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT, encryption = true ) @@ -171,9 +183,13 @@ class OidcSiopIsoProtocolTest : FreeSpec({ verifierSiop, walletUrl, OidcSiopVerifier.RequestOptions( - representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, - credentialScheme = MobileDrivingLicenceScheme, - requestedAttributes = listOf(MobileDrivingLicenceDataElements.FAMILY_NAME) + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + MobileDrivingLicenceScheme, + ConstantIndex.CredentialRepresentation.ISO_MDOC, + listOf(MobileDrivingLicenceDataElements.FAMILY_NAME) + ) + ) ), holderSiop, ) diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt index d4b8487d..9aa0719a 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt @@ -75,7 +75,7 @@ class OidcSiopProtocolTest : FreeSpec({ } "test with Fragment" { - val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl = walletUrl) + val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl, defaultRequestOptions) val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() authnResponse.shouldBeInstanceOf() @@ -110,7 +110,7 @@ class OidcSiopProtocolTest : FreeSpec({ } } ) - val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl = walletUrl) + val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl, defaultRequestOptions) val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() authnResponse.shouldBeInstanceOf() @@ -133,8 +133,8 @@ class OidcSiopProtocolTest : FreeSpec({ .also { println(it) } DefaultVerifierJwsService().verifyJwsObject(metadataObject).shouldBeTrue() - val authnRequestUrl = - verifierSiop.createAuthnRequestUrlWithRequestObject(walletUrl).getOrThrow() + val authnRequestUrl = verifierSiop.createAuthnRequestUrlWithRequestObject(walletUrl, defaultRequestOptions) + .getOrThrow() val authnRequest: AuthenticationRequestParameters = Url(authnRequestUrl).encodedQuery.decodeFromUrlQuery() authnRequest.clientId shouldBe relyingPartyUrl @@ -152,7 +152,10 @@ class OidcSiopProtocolTest : FreeSpec({ "test with direct_post" { val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, - requestOptions = RequestOptions(responseMode = OpenIdConstants.ResponseMode.DIRECT_POST) + requestOptions = RequestOptions( + credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)), + responseMode = OpenIdConstants.ResponseMode.DIRECT_POST + ) ) val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() @@ -168,7 +171,10 @@ class OidcSiopProtocolTest : FreeSpec({ "test with direct_post_jwt" { val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, - requestOptions = RequestOptions(responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT) + requestOptions = RequestOptions( + credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)), + responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT + ) ) val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() @@ -189,6 +195,7 @@ class OidcSiopProtocolTest : FreeSpec({ val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, requestOptions = RequestOptions( + credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)), responseMode = OpenIdConstants.ResponseMode.QUERY, state = expectedState ) @@ -208,7 +215,7 @@ class OidcSiopProtocolTest : FreeSpec({ } "test with deserializing" { - val authnRequest = verifierSiop.createAuthnRequest() + val authnRequest = verifierSiop.createAuthnRequest(defaultRequestOptions) val authnRequestUrlParams = authnRequest.encodeToParameters().formUrlEncode() val parsedAuthnRequest: AuthenticationRequestParameters = @@ -231,7 +238,7 @@ class OidcSiopProtocolTest : FreeSpec({ "test specific credential" { val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, - requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023) + requestOptions = requestOptionsAtomicAttribute() ) val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() @@ -249,7 +256,7 @@ class OidcSiopProtocolTest : FreeSpec({ "test with request object" { val authnRequestWithRequestObject = verifierSiop.createAuthnRequestUrlWithRequestObject( walletUrl = walletUrl, - requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023) + requestOptions = requestOptionsAtomicAttribute() ).getOrThrow() val authnResponse = holderSiop.createAuthnResponse(authnRequestWithRequestObject).getOrThrow() @@ -273,7 +280,7 @@ class OidcSiopProtocolTest : FreeSpec({ ) val authnRequestWithRequestObject = verifierSiop.createAuthnRequestUrlWithRequestObject( walletUrl = walletUrl, - requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023) + requestOptions = requestOptionsAtomicAttribute() ).getOrThrow() holderSiop = OidcSiopWallet( @@ -301,7 +308,7 @@ class OidcSiopProtocolTest : FreeSpec({ ) val authnRequestWithRequestObject = verifierSiop.createAuthnRequestUrlWithRequestObject( walletUrl = walletUrl, - requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023) + requestOptions = requestOptionsAtomicAttribute() ).getOrThrow() holderSiop = OidcSiopWallet( @@ -316,7 +323,7 @@ class OidcSiopProtocolTest : FreeSpec({ "test with request object from request_uri as URL query parameters" { val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, - requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023) + requestOptions = requestOptionsAtomicAttribute() ) val clientId = Url(authnRequest).parameters["client_id"]!! @@ -347,7 +354,7 @@ class OidcSiopProtocolTest : FreeSpec({ "test with request object from request_uri as JWS" { val jar = verifierSiop.createAuthnRequestAsSignedRequestObject( - requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023) + requestOptions = requestOptionsAtomicAttribute() ).getOrThrow() val requestUrl = "https://www.example.com/request/${uuid4()}" @@ -377,7 +384,7 @@ class OidcSiopProtocolTest : FreeSpec({ "test with request object not verified" { val jar = verifierSiop.createAuthnRequestAsSignedRequestObject( - requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023) + requestOptions = requestOptionsAtomicAttribute() ).getOrThrow() val requestUrl = "https://www.example.com/request/${uuid4()}" @@ -400,6 +407,14 @@ class OidcSiopProtocolTest : FreeSpec({ } }) +private fun requestOptionsAtomicAttribute() = RequestOptions( + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + ) + ), +) + private suspend fun buildAttestationJwt( sprsCryptoService: DefaultCryptoService, relyingPartyUrl: String, @@ -438,10 +453,16 @@ private suspend fun verifySecondProtocolRun( walletUrl: String, holderSiop: OidcSiopWallet ) { - val authnRequestUrl = verifierSiop.createAuthnRequestUrl(walletUrl = walletUrl) + val authnRequestUrl = verifierSiop.createAuthnRequestUrl(walletUrl, defaultRequestOptions) val authnResponse = holderSiop.createAuthnResponse(authnRequestUrl) val validation = verifierSiop.validateAuthnResponse( (authnResponse.getOrThrow() as AuthenticationResponseResult.Redirect).url ) validation.shouldBeInstanceOf() } + +private val defaultRequestOptions = OidcSiopVerifier.RequestOptions( + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023) + ) +) \ No newline at end of file diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt index 469c00bf..0351fbcd 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt @@ -54,40 +54,20 @@ class OidcSiopSdJwtProtocolTest : FreeSpec({ ) } - "test with Fragment" { - val authnRequest = verifierSiop.createAuthnRequestUrl( - walletUrl = walletUrl, - requestOptions = RequestOptions( - representation = ConstantIndex.CredentialRepresentation.SD_JWT, - credentialScheme = ConstantIndex.AtomicAttribute2023, - requestedAttributes = listOf("given_name") - ), - ).also { println(it) } - - val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() - authnResponse.shouldBeInstanceOf().also { println(it) } - - val result = verifierSiop.validateAuthnResponse(authnResponse.url) - result.shouldBeInstanceOf() - result.disclosures.shouldNotBeEmpty() - - assertSecondRun(verifierSiop, holderSiop, walletUrl) - } - "Selective Disclosure with custom credential" { val requestedClaim = "given_name" - verifierSiop = OidcSiopVerifier( - keyMaterial = verifierKeyMaterial, - relyingPartyUrl = relyingPartyUrl, - ) val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, requestOptions = RequestOptions( - representation = ConstantIndex.CredentialRepresentation.SD_JWT, - credentialScheme = ConstantIndex.AtomicAttribute2023, - requestedAttributes = listOf(requestedClaim) + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + ConstantIndex.CredentialRepresentation.SD_JWT, + listOf(requestedClaim) + ) + ) ) - ).also { println(it) } + ) authnRequest shouldContain requestedClaim val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() @@ -95,27 +75,10 @@ class OidcSiopSdJwtProtocolTest : FreeSpec({ val result = verifierSiop.validateAuthnResponse(authnResponse.url) result.shouldBeInstanceOf() - val sdJwt = result.sdJwt.also { println(it) } - + result.sdJwt.shouldNotBeNull() result.disclosures.shouldNotBeEmpty() result.disclosures.shouldBeSingleton() result.disclosures.shouldHaveSingleElement { it.claimName == requestedClaim } - sdJwt.shouldNotBeNull() } }) - -private suspend fun assertSecondRun( - verifierSiop: OidcSiopVerifier, - holderSiop: OidcSiopWallet, - walletUrl: String -) { - val authnRequestUrl = verifierSiop.createAuthnRequestUrl( - walletUrl = walletUrl, - requestOptions = RequestOptions(representation = ConstantIndex.CredentialRepresentation.SD_JWT) - ) - val authnResponse = holderSiop.createAuthnResponse(authnRequestUrl) - val url = (authnResponse.getOrThrow() as AuthenticationResponseResult.Redirect).url - val validation = verifierSiop.validateAuthnResponse(url) - validation.shouldBeInstanceOf() -} \ No newline at end of file diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWalletScopeSupportTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWalletScopeSupportTest.kt index 908b5551..fda12381 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWalletScopeSupportTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWalletScopeSupportTest.kt @@ -1,13 +1,13 @@ package at.asitplus.wallet.lib.oidc -import at.asitplus.jsonpath.core.NormalizedJsonPath -import at.asitplus.jsonpath.core.NormalizedJsonPathSegment -import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.dif.Constraint import at.asitplus.dif.ConstraintField import at.asitplus.dif.DifInputDescriptor import at.asitplus.dif.PresentationDefinition +import at.asitplus.jsonpath.core.NormalizedJsonPath +import at.asitplus.jsonpath.core.NormalizedJsonPathSegment import at.asitplus.wallet.lib.agent.* +import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.oidvci.OAuth2Exception import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements import at.asitplus.wallet.mdl.MobileDrivingLicenceScheme @@ -29,15 +29,15 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({ "test scopes" - { val testScopes = object { - val EmptyPresentationRequest: String = "emptyPresentationRequest" - val MdocMdlWithGivenName: String = "mdocMdlWithGivenName" + val emptyPresentationRequest: String = "emptyPresentationRequest" + val mdocMdlWithGivenName: String = "mdocMdlWithGivenName" } val testScopePresentationDefinitionRetriever = mapOf( - testScopes.EmptyPresentationRequest to PresentationDefinition( + testScopes.emptyPresentationRequest to PresentationDefinition( id = uuid4().toString(), inputDescriptors = listOf() ), - testScopes.MdocMdlWithGivenName to PresentationDefinition( + testScopes.mdocMdlWithGivenName to PresentationDefinition( id = uuid4().toString(), inputDescriptors = listOf( DifInputDescriptor( @@ -101,10 +101,10 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({ ).getOrThrow().toStoreCredentialInput() ) - val authnRequest = verifierSiop.createAuthnRequest().let { request -> + val authnRequest = verifierSiop.createAuthnRequest(defaultRequestOptions).let { request -> request.copy( presentationDefinition = null, - scope = request.scope + " " + testScopes.EmptyPresentationRequest + scope = request.scope + " " + testScopes.emptyPresentationRequest ) } @@ -117,10 +117,10 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({ } "get MdocMdlWithGivenName scope without available credentials fails" { - val authnRequest = verifierSiop.createAuthnRequest().let { request -> + val authnRequest = verifierSiop.createAuthnRequest(defaultRequestOptions).let { request -> request.copy( presentationDefinition = null, - scope = request.scope + " " + testScopes.MdocMdlWithGivenName + scope = request.scope + " " + testScopes.mdocMdlWithGivenName ) } @@ -145,10 +145,10 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({ ) - val authnRequest = verifierSiop.createAuthnRequest().let { request -> + val authnRequest = verifierSiop.createAuthnRequest(defaultRequestOptions).let { request -> request.copy( presentationDefinition = null, - scope = request.scope + " " + testScopes.MdocMdlWithGivenName + scope = request.scope + " " + testScopes.mdocMdlWithGivenName ) } @@ -160,4 +160,10 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({ result.document.validItems.shouldNotBeEmpty() } } -}) \ No newline at end of file +}) + +private val defaultRequestOptions = OidcSiopVerifier.RequestOptions( + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023) + ) +) \ No newline at end of file diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt index 22758b87..6134246f 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt @@ -71,9 +71,14 @@ class OidcSiopX509SanDnsTest : FreeSpec({ "test with Fragment" { val authnRequest = verifierSiop.createAuthnRequestAsSignedRequestObject( requestOptions = RequestOptions( - representation = ConstantIndex.CredentialRepresentation.SD_JWT, + credentials = setOf( + OidcSiopVerifier.RequestOptionsCredential( + ConstantIndex.AtomicAttribute2023, + ConstantIndex.CredentialRepresentation.SD_JWT, + listOf("given_name") + ) + ), responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT, - requestedAttributes = listOf("given_name") ) ).also { println(it) }.getOrThrow() From 23fa4f8157335f33ea046a7fd02201db90968e89 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Thu, 26 Sep 2024 09:43:20 +0200 Subject: [PATCH 2/6] Refactor code --- .../wallet/lib/oidc/OidcSiopVerifier.kt | 4 +- .../asitplus/wallet/lib/agent/HolderAgent.kt | 30 ++++----- .../wallet/lib/data/dif/InputEvaluator.kt | 12 +--- .../wallet/lib/agent/AgentSdJwtTest.kt | 67 +++++++++---------- 4 files changed, 50 insertions(+), 63 deletions(-) diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index aa26b2e6..c8142779 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -320,8 +320,8 @@ class OidcSiopVerifier private constructor( listOf(SCOPE_OPENID, SCOPE_PROFILE) + credentials.mapNotNull { it.credentialScheme.sdJwtType } + credentials.mapNotNull { it.credentialScheme.vcType } - + credentials.mapNotNull { it.credentialScheme.isoNamespace }) - .joinToString(" ") + + credentials.mapNotNull { it.credentialScheme.isoNamespace } + ).joinToString(" ") private val clientId: String? by lazy { clientIdFromCertificateChain ?: relyingPartyUrl diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt index b349cbfc..924a781e 100644 --- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt +++ b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt @@ -3,20 +3,15 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult import at.asitplus.KmmResult.Companion.wrap import at.asitplus.catching +import at.asitplus.dif.* +import at.asitplus.jsonpath.core.NormalizedJsonPath import at.asitplus.signum.indispensable.cosef.CoseKey import at.asitplus.signum.indispensable.cosef.toCoseKey import at.asitplus.signum.indispensable.pki.X509Certificate -import at.asitplus.jsonpath.core.NormalizedJsonPath import at.asitplus.wallet.lib.cbor.CoseService import at.asitplus.wallet.lib.cbor.DefaultCoseService import at.asitplus.wallet.lib.data.CredentialToJsonConverter -import at.asitplus.dif.ClaimFormatEnum -import at.asitplus.dif.FormatHolder -import at.asitplus.dif.InputDescriptor import at.asitplus.wallet.lib.data.dif.InputEvaluator -import at.asitplus.dif.PresentationDefinition -import at.asitplus.dif.PresentationSubmission -import at.asitplus.dif.PresentationSubmissionDescriptor import at.asitplus.wallet.lib.data.dif.PresentationSubmissionValidator import at.asitplus.wallet.lib.jws.DefaultJwsService import at.asitplus.wallet.lib.jws.JwsService @@ -141,17 +136,18 @@ class HolderAgent( /** * Gets a list of all valid stored credentials sorted by preference */ - private suspend fun getValidCredentialsByPriority() = getCredentials()?.filter { - it.status != Validator.RevocationStatus.REVOKED - }?.map { it.storeEntry }?.sortedBy { - // prefer iso credentials and sd jwt credentials over plain vc credentials - // -> they support selective disclosure! - when (it) { - is SubjectCredentialStore.StoreEntry.Vc -> 2 - is SubjectCredentialStore.StoreEntry.SdJwt -> 1 - is SubjectCredentialStore.StoreEntry.Iso -> 1 + private suspend fun getValidCredentialsByPriority() = getCredentials() + ?.filter { it.status != Validator.RevocationStatus.REVOKED } + ?.map { it.storeEntry } + ?.sortedBy { + // prefer iso credentials and sd jwt credentials over plain vc credentials + // -> they support selective disclosure! + when (it) { + is SubjectCredentialStore.StoreEntry.Vc -> 2 + is SubjectCredentialStore.StoreEntry.SdJwt -> 1 + is SubjectCredentialStore.StoreEntry.Iso -> 1 + } } - } override suspend fun createPresentation( diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/InputEvaluator.kt b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/InputEvaluator.kt index 3480bfff..94e0b001 100644 --- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/InputEvaluator.kt +++ b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/InputEvaluator.kt @@ -10,14 +10,7 @@ import at.asitplus.jsonpath.JsonPath import at.asitplus.jsonpath.core.NodeList import at.asitplus.jsonpath.core.NormalizedJsonPath import io.github.aakira.napier.Napier -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.booleanOrNull -import kotlinx.serialization.json.doubleOrNull -import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.* /** * Specification: https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-evaluation @@ -65,7 +58,8 @@ internal fun JsonElement.satisfiesConstraintFilter(filter: ConstraintFilter): Bo // source: https://json-schema.org/draft-07/schema# // this currently is only a tentative implementation val typeMatchesElement = when (this) { - is JsonArray -> filter.type == "array" // TODO: need recursive type check; Need element count check (minItems = 1) for root, need check for unique items at root (whatever that means) + // TODO: need recursive type check; Need element count check (minItems = 1) for root, need check for unique items at root (whatever that means) + is JsonArray -> filter.type == "array" is JsonObject -> filter.type == "object" is JsonPrimitive -> when (filter.type) { "string" -> this.isString diff --git a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentSdJwtTest.kt b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentSdJwtTest.kt index 3c51bc07..61beb0c9 100644 --- a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentSdJwtTest.kt +++ b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentSdJwtTest.kt @@ -70,10 +70,9 @@ class AgentSdJwtTest : FreeSpec({ ) "simple walk-through success" { - val presentationParameters = holder.createPresentation( - challenge, - verifier.keyMaterial.identifier, + challenge = challenge, + audienceId = verifier.keyMaterial.identifier, presentationDefinition = givenNamePresentationDefinition ).getOrThrow() @@ -89,13 +88,14 @@ class AgentSdJwtTest : FreeSpec({ } "keyBindingJws contains more JWK attributes, still verifies" { + val credential = holderCredentialStore.getCredentials().getOrThrow() + .filterIsInstance().first() val sdJwt = createSdJwtPresentation( - DefaultJwsService(DefaultCryptoService(holderKeyMaterial)), - verifier.keyMaterial.identifier, - challenge, - holderCredentialStore.getCredentials().getOrThrow() - .filterIsInstance().first(), - "given_name" + jwsService = DefaultJwsService(DefaultCryptoService(holderKeyMaterial)), + audienceId = verifier.keyMaterial.identifier, + challenge = challenge, + validSdJwtCredential = credential, + claimName = "given_name" ).sdJwt val verified = verifier.verifyPresentation(sdJwt, challenge) .shouldBeInstanceOf() @@ -106,18 +106,16 @@ class AgentSdJwtTest : FreeSpec({ "wrong key binding jwt" { val presentationParameters = holder.createPresentation( - challenge, - verifier.keyMaterial.identifier, - givenNamePresentationDefinition + challenge = challenge, + audienceId = verifier.keyMaterial.identifier, + presentationDefinition = givenNamePresentationDefinition ).getOrThrow() val vp = presentationParameters.presentationResults.firstOrNull() .shouldBeInstanceOf() // replace key binding of original vp.sdJwt (i.e. the part after the last `~`) - val malformedVpSdJwt = vp.sdJwt.replaceAfterLast( - "~", - createFreshSdJwtKeyBinding(challenge, verifier.keyMaterial.identifier).substringAfterLast("~") - ) + val freshKbJwt = createFreshSdJwtKeyBinding(challenge, verifier.keyMaterial.identifier) + val malformedVpSdJwt = vp.sdJwt.replaceAfterLast("~", freshKbJwt.substringAfterLast("~")) verifier.verifyPresentation(malformedVpSdJwt, challenge) .shouldBeInstanceOf() @@ -126,8 +124,8 @@ class AgentSdJwtTest : FreeSpec({ "wrong challenge in key binding jwt" { val malformedChallenge = challenge.reversed() val presentationParameters = holder.createPresentation( - malformedChallenge, - verifier.keyMaterial.identifier, + challenge = malformedChallenge, + audienceId = verifier.keyMaterial.identifier, presentationDefinition = givenNamePresentationDefinition ).getOrThrow() @@ -140,18 +138,18 @@ class AgentSdJwtTest : FreeSpec({ "revoked sd jwt" { val presentationParameters = holder.createPresentation( - challenge, - verifier.keyMaterial.identifier, + challenge = challenge, + audienceId = verifier.keyMaterial.identifier, presentationDefinition = givenNamePresentationDefinition ).getOrThrow() val vp = presentationParameters.presentationResults.firstOrNull() .shouldBeInstanceOf() - issuer.revokeCredentialsWithId( - holderCredentialStore.getCredentials().getOrThrow() - .filterIsInstance() - .associate { it.sdJwt.jwtId!! to it.sdJwt.notBefore!! }) shouldBe true + val listOfJwtId = holderCredentialStore.getCredentials().getOrThrow() + .filterIsInstance() + .associate { it.sdJwt.jwtId!! to it.sdJwt.notBefore!! } + issuer.revokeCredentialsWithId(listOfJwtId) shouldBe true verifier.setRevocationList(issuer.issueRevocationListCredential()!!) shouldBe true val verified = verifier.verifyPresentation(vp.sdJwt, challenge) .shouldBeInstanceOf() @@ -179,7 +177,8 @@ suspend fun createFreshSdJwtKeyBinding(challenge: String, verifierId: String): S inputDescriptors = listOf(DifInputDescriptor(id = uuid4().toString())) ), ).getOrThrow() - return (presentationResult.presentationResults.first() as Holder.CreatePresentationResult.SdJwt).sdJwt + val sdJwt = presentationResult.presentationResults.first() as Holder.CreatePresentationResult.SdJwt + return sdJwt.sdJwt } private suspend fun createSdJwtPresentation( @@ -189,17 +188,15 @@ private suspend fun createSdJwtPresentation( validSdJwtCredential: SubjectCredentialStore.StoreEntry.SdJwt, claimName: String, ): Holder.CreatePresentationResult.SdJwt { - val filteredDisclosures = validSdJwtCredential.disclosures.filter { it.value!!.claimName == claimName }.keys - val issuerJwtPlusDisclosures = - SdJwtSigned.sdHashInput(validSdJwtCredential, filteredDisclosures) + val filteredDisclosures = validSdJwtCredential.disclosures + .filter { it.value!!.claimName == claimName }.keys + val issuerJwtPlusDisclosures = SdJwtSigned.sdHashInput(validSdJwtCredential, filteredDisclosures) val keyBinding = createKeyBindingJws(jwsService, audienceId, challenge, issuerJwtPlusDisclosures) - val jwsFromIssuer = - JwsSigned.parse(validSdJwtCredential.vcSerialized.substringBefore("~")).getOrElse { - Napier.w("Could not re-create JWS from stored SD-JWT", it) - throw PresentationException(it) - } - val sdJwt = - SdJwtSigned.serializePresentation(jwsFromIssuer, filteredDisclosures, keyBinding) + val jwsFromIssuer = JwsSigned.parse(validSdJwtCredential.vcSerialized.substringBefore("~")).getOrElse { + Napier.w("Could not re-create JWS from stored SD-JWT", it) + throw PresentationException(it) + } + val sdJwt = SdJwtSigned.serializePresentation(jwsFromIssuer, filteredDisclosures, keyBinding) return Holder.CreatePresentationResult.SdJwt(sdJwt) } From c0e8025a603e5bfc84cf90175cdd936d86eed598 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Thu, 26 Sep 2024 09:46:43 +0200 Subject: [PATCH 3/6] Mark field removed in current spec as deprecated --- .../at/asitplus/dif/PresentationDefinition.kt | 2 ++ .../wallet/lib/aries/PresentProofProtocol.kt | 22 +++------------- .../lib/oidc/helper/PresentationFactory.kt | 26 +++++++++---------- .../OidcSiopCombinedProtocolTwoStepTest.kt | 8 ++---- 4 files changed, 20 insertions(+), 38 deletions(-) diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationDefinition.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationDefinition.kt index d21f4c00..c5b7e7ec 100644 --- a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationDefinition.kt +++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationDefinition.kt @@ -20,11 +20,13 @@ data class PresentationDefinition( val purpose: String? = null, @SerialName("input_descriptors") val inputDescriptors: Collection, + @Deprecated(message = "Removed in DIF Presentation Exchange 2.0.0", ReplaceWith("inputDescriptors.format")) @SerialName("format") val formats: FormatHolder? = null, @SerialName("submission_requirements") val submissionRequirements: Collection? = null, ) { + @Deprecated(message = "Removed in DIF Presentation Exchange 2.0.0") constructor( inputDescriptors: Collection, formats: FormatHolder diff --git a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt index 7189e20a..962c4ede 100644 --- a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt +++ b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt @@ -1,5 +1,6 @@ package at.asitplus.wallet.lib.aries +import at.asitplus.dif.* import at.asitplus.signum.indispensable.josef.JsonWebKey import at.asitplus.signum.indispensable.josef.JwsAlgorithm import at.asitplus.wallet.lib.agent.Holder @@ -7,25 +8,7 @@ import at.asitplus.wallet.lib.agent.Verifier import at.asitplus.wallet.lib.data.AriesGoalCodeParser import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.SchemaIndex -import at.asitplus.dif.Constraint -import at.asitplus.dif.ConstraintField -import at.asitplus.dif.ConstraintFilter -import at.asitplus.dif.DifInputDescriptor -import at.asitplus.dif.FormatContainerJwt -import at.asitplus.dif.FormatHolder -import at.asitplus.dif.PresentationDefinition -import at.asitplus.wallet.lib.msg.AttachmentFormatReference -import at.asitplus.wallet.lib.msg.JsonWebMessage -import at.asitplus.wallet.lib.msg.JwmAttachment -import at.asitplus.wallet.lib.msg.OutOfBandInvitation -import at.asitplus.wallet.lib.msg.OutOfBandInvitationBody -import at.asitplus.wallet.lib.msg.OutOfBandService -import at.asitplus.wallet.lib.msg.Presentation -import at.asitplus.wallet.lib.msg.PresentationBody -import at.asitplus.wallet.lib.msg.RequestPresentation -import at.asitplus.wallet.lib.msg.RequestPresentationAttachment -import at.asitplus.wallet.lib.msg.RequestPresentationAttachmentOptions -import at.asitplus.wallet.lib.msg.RequestPresentationBody +import at.asitplus.wallet.lib.msg.* import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier import kotlinx.serialization.encodeToString @@ -203,6 +186,7 @@ class PresentProofProtocol( .also { this.state = State.REQUEST_PRESENTATION_SENT } } + @Suppress("DEPRECATION") private fun buildRequestPresentationMessage( credentialScheme: ConstantIndex.CredentialScheme, parentThreadId: String? = null, diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/PresentationFactory.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/PresentationFactory.kt index adf5b002..c6efcd9b 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/PresentationFactory.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/PresentationFactory.kt @@ -2,24 +2,24 @@ package at.asitplus.wallet.lib.oidc.helper import at.asitplus.KmmResult import at.asitplus.catching -import at.asitplus.signum.indispensable.CryptoPublicKey -import at.asitplus.signum.indispensable.josef.JwsSigned -import at.asitplus.signum.indispensable.josef.toJsonWebKey -import at.asitplus.wallet.lib.agent.CredentialSubmission -import at.asitplus.wallet.lib.agent.Holder -import at.asitplus.wallet.lib.agent.toDefaultSubmission import at.asitplus.dif.ClaimFormatEnum import at.asitplus.dif.FormatHolder import at.asitplus.dif.PresentationDefinition -import at.asitplus.wallet.lib.data.dif.PresentationSubmissionValidator -import at.asitplus.wallet.lib.jws.JwsService import at.asitplus.openid.AuthenticationRequestParameters -import at.asitplus.wallet.lib.oidc.AuthenticationRequestParametersFrom import at.asitplus.openid.IdToken import at.asitplus.openid.OpenIdConstants.Errors import at.asitplus.openid.OpenIdConstants.ID_TOKEN import at.asitplus.openid.OpenIdConstants.VP_TOKEN import at.asitplus.openid.RelyingPartyMetadata +import at.asitplus.signum.indispensable.CryptoPublicKey +import at.asitplus.signum.indispensable.josef.JwsSigned +import at.asitplus.signum.indispensable.josef.toJsonWebKey +import at.asitplus.wallet.lib.agent.CredentialSubmission +import at.asitplus.wallet.lib.agent.Holder +import at.asitplus.wallet.lib.agent.toDefaultSubmission +import at.asitplus.wallet.lib.data.dif.PresentationSubmissionValidator +import at.asitplus.wallet.lib.jws.JwsService +import at.asitplus.wallet.lib.oidc.AuthenticationRequestParametersFrom import at.asitplus.wallet.lib.oidvci.OAuth2Exception import io.github.aakira.napier.Napier import kotlinx.datetime.Clock @@ -44,7 +44,7 @@ internal class PresentationFactory( val credentialSubmissions = inputDescriptorSubmissions ?: holder.matchInputDescriptorsAgainstCredentialStore( inputDescriptors = presentationDefinition.inputDescriptors, - fallbackFormatHolder = presentationDefinition.formats ?: clientMetadata?.vpFormats, + fallbackFormatHolder = clientMetadata?.vpFormats, ).getOrThrow().toDefaultSubmission() presentationDefinition.validateSubmission( @@ -129,9 +129,9 @@ internal class PresentationFactory( } val constraintFieldMatches = holder.evaluateInputDescriptorAgainstCredential( - inputDescriptor, - submission.value.credential, - fallbackFormatHolder = this.formats ?: clientMetadata?.vpFormats, + inputDescriptor = inputDescriptor, + credential = submission.value.credential, + fallbackFormatHolder = clientMetadata?.vpFormats, pathAuthorizationValidator = { true }, ).getOrThrow() diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt index 661c7661..9bf6f58e 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt @@ -60,8 +60,7 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ val inputDescriptorId = presentationDefinition.inputDescriptors.first().id val matches = holderAgent.matchInputDescriptorsAgainstCredentialStore( - presentationDefinition.inputDescriptors, - presentationDefinition.formats, + presentationDefinition.inputDescriptors ).getOrThrow() val inputDescriptorMatches = matches[inputDescriptorId].shouldNotBeNull() inputDescriptorMatches shouldHaveSize 2 @@ -94,8 +93,7 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ val presentationDefinition = preparationState.presentationDefinition.shouldNotBeNull() val inputDescriptorId = presentationDefinition.inputDescriptors.first().id val matches = holderAgent.matchInputDescriptorsAgainstCredentialStore( - presentationDefinition.inputDescriptors, - presentationDefinition.formats, + presentationDefinition.inputDescriptors ).getOrThrow().also { it shouldHaveSize 1 } @@ -145,7 +143,6 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ holderAgent.matchInputDescriptorsAgainstCredentialStore( presentationDefinitionSdJwt.inputDescriptors, - presentationDefinitionSdJwt.formats, ).getOrThrow().also { it.shouldHaveSize(1) it.entries.first().value.let { @@ -175,7 +172,6 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({ val matches = holderAgent.matchInputDescriptorsAgainstCredentialStore( presentationDefinition.inputDescriptors, - presentationDefinition.formats, ).getOrThrow().also { it shouldHaveSize 1 } From e8c484bb148a9c617195f3ac0282414b1a377e66 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Thu, 26 Sep 2024 12:15:21 +0200 Subject: [PATCH 4/6] SIOP: Move parameters from constructor to request options --- CHANGELOG.md | 1 + .../wallet/lib/oidc/OidcSiopVerifier.kt | 22 +++++++++++-------- ...ationRequestParameterFromSerializerTest.kt | 2 -- .../wallet/lib/oidc/OidcSiopInteropTest.kt | 18 +++++++-------- .../lib/oidc/OidcSiopIsoProtocolTest.kt | 12 +++++----- .../wallet/lib/oidc/OidcSiopProtocolTest.kt | 17 +++++--------- .../wallet/lib/oidc/OidcSiopX509SanDnsTest.kt | 19 ++++++---------- 7 files changed, 39 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5275a8..7fbc09da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Release NEXT: - SIOPv2/OpenID4VP: Support requesting and receiving claims from different credentials, i.e. a combined presentation - Require request options on every method in `OidcSiopVerifier` - Move `credentialScheme`, `representation`, `requestedAttributes` from `RequestOptions` to `RequestOptionsCredentials` + - In `OidcSiopVerifier` move `responseUrl` from constructor parameter to `RequestOptions` Release 4.1.2: * In `OidcSiopVerifier` add parameter `nonceService` to externalize creation and validation of nonces, e.g. for deployments in load-balanced environments diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index c8142779..1d0d40af 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -50,7 +50,6 @@ import kotlin.time.toDuration class OidcSiopVerifier private constructor( private val verifier: Verifier, private val relyingPartyUrl: String?, - private val responseUrl: String?, private val jwsService: JwsService, private val verifierJwsService: VerifierJwsService, timeLeewaySeconds: Long = 300L, @@ -83,7 +82,6 @@ class OidcSiopVerifier private constructor( keyMaterial: KeyMaterial = EphemeralKeyWithoutCert(), verifier: Verifier = VerifierAgent(keyMaterial), relyingPartyUrl: String? = null, - responseUrl: String? = null, verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(DefaultVerifierCryptoService()), jwsService: JwsService = DefaultJwsService(DefaultCryptoService(keyMaterial)), timeLeewaySeconds: Long = 300L, @@ -93,7 +91,6 @@ class OidcSiopVerifier private constructor( ) : this( verifier = verifier, relyingPartyUrl = relyingPartyUrl, - responseUrl = responseUrl, jwsService = jwsService, verifierJwsService = verifierJwsService, timeLeewaySeconds = timeLeewaySeconds, @@ -167,18 +164,22 @@ class OidcSiopVerifier private constructor( /** * Response mode to request, see [OpenIdConstants.ResponseMode] */ - val responseMode: OpenIdConstants.ResponseMode? = null, + val responseMode: OpenIdConstants.ResponseMode = OpenIdConstants.ResponseMode.FRAGMENT, + /** + * Response URL to set in the [AuthenticationRequestParameters] + */ + val responseUrl: String? = null, /** * Opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult] */ - val state: String? = uuid4().toString(), + val state: String = uuid4().toString(), /** * Optional URL to include [metadata] by reference instead of by value (directly embedding in authn request) */ val clientMetadataUrl: String? = null, /** * Set this value to include metadata with encryption parameters set. Beware if setting this value and also - * [clientMetadataUrl], that the URL shall point to [getCreateMetadataWithEncryption]. + * [clientMetadataUrl], that the URL shall point to [OidcSiopVerifier.metadataWithEncryption]. */ val encryption: Boolean = false, ) @@ -298,12 +299,15 @@ class OidcSiopVerifier private constructor( responseType = "$ID_TOKEN $VP_TOKEN", // TODO move to RequestOptions clientId = clientId, redirectUrl = requestOptions.buildRedirectUrl(), - responseUrl = responseUrl, + responseUrl = requestOptions.responseUrl, clientIdScheme = clientIdScheme.clientIdScheme, scope = requestOptions.buildScope(), nonce = nonceService.provideNonce(), - clientMetadata = requestOptions.clientMetadataUrl?.let { null } - ?: if (requestOptions.encryption) metadataWithEncryption else metadata, + clientMetadata = if (requestOptions.clientMetadataUrl != null) { + null + } else { + if (requestOptions.encryption) metadataWithEncryption else metadata + }, clientMetadataUri = requestOptions.clientMetadataUrl, idTokenType = IdTokenType.SUBJECT_SIGNED.text, responseMode = requestOptions.responseMode, diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt index 570948a0..7f877af5 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt @@ -16,7 +16,6 @@ class AuthenticationRequestParameterFromSerializerTest : FreeSpec({ val relyingPartyUrl = "https://example.com/rp/${uuid4()}" val walletUrl = "https://example.com/wallet/${uuid4()}" - val responseUrl = "https://example.com/rp/${uuid4()}" val holderKeyMaterial = EphemeralKeyWithoutCert() val oidcSiopWallet = OidcSiopWallet( @@ -27,7 +26,6 @@ class AuthenticationRequestParameterFromSerializerTest : FreeSpec({ val verifierSiop = OidcSiopVerifier( verifier = VerifierAgent(EphemeralKeyWithoutCert()), relyingPartyUrl = relyingPartyUrl, - responseUrl = responseUrl, ) val representations = listOf( diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt index c069c149..78c707b6 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt @@ -2,7 +2,10 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.openid.AuthenticationRequestParameters import at.asitplus.openid.OpenIdConstants -import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.Asn1EncapsulatingOctetString +import at.asitplus.signum.indispensable.asn1.Asn1Primitive +import at.asitplus.signum.indispensable.asn1.Asn1String +import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.asn1.encoding.Asn1 import at.asitplus.signum.indispensable.josef.JweAlgorithm import at.asitplus.signum.indispensable.josef.JweEncryption @@ -36,7 +39,6 @@ class OidcSiopInteropTest : FreeSpec({ lateinit var holderAgent: Holder lateinit var holderSiop: OidcSiopWallet lateinit var verifierKeyMaterial: KeyMaterial - lateinit var verifierAgent: Verifier lateinit var verifierSiop: OidcSiopVerifier beforeEach { @@ -172,7 +174,6 @@ class OidcSiopInteropTest : FreeSpec({ ) holderSiop.parseAuthenticationRequestParameters(url).getOrThrow() - .also { println(it) } } "EUDI AuthnRequest can be parsed" { @@ -323,11 +324,9 @@ class OidcSiopInteropTest : FreeSpec({ } )))) verifierKeyMaterial = EphemeralKeyWithSelfSignedCert(extensions = extensions) - verifierAgent = VerifierAgent(verifierKeyMaterial) verifierSiop = OidcSiopVerifier( keyMaterial = verifierKeyMaterial, relyingPartyUrl = "https://example.com/rp", - responseUrl = "https://example.com/response", clientIdScheme = OidcSiopVerifier.ClientIdScheme.CertificateSanDns(listOf(verifierKeyMaterial.getCertificate()!!)), ) val nonce = uuid4().toString() @@ -337,6 +336,7 @@ class OidcSiopInteropTest : FreeSpec({ requestUrl = requestUrl, requestOptions = OidcSiopVerifier.RequestOptions( responseMode = OpenIdConstants.ResponseMode.DIRECT_POST, + responseUrl = "https://example.com/response", credentials = setOf( OidcSiopVerifier.RequestOptionsCredential( ConstantIndex.AtomicAttribute2023, @@ -345,7 +345,7 @@ class OidcSiopInteropTest : FreeSpec({ ) ) ) - ).getOrThrow().also { println(it) } + ).getOrThrow() holderSiop = OidcSiopWallet( @@ -354,13 +354,11 @@ class OidcSiopInteropTest : FreeSpec({ remoteResourceRetriever = { if (it == requestUrl) requestUrlForWallet.second else null }) val parameters = holderSiop.parseAuthenticationRequestParameters(requestUrlForWallet.first).getOrThrow() - val stae = holderSiop.startAuthorizationResponsePreparation(parameters).getOrThrow() - val response = holderSiop.finalizeAuthorizationResponse(parameters, stae).getOrThrow() + val preparation = holderSiop.startAuthorizationResponsePreparation(parameters).getOrThrow() + val response = holderSiop.finalizeAuthorizationResponse(parameters, preparation).getOrThrow() .shouldBeInstanceOf() - .also { println(it) } verifierSiop.validateAuthnResponse(params = response.params.decode()) .shouldBeInstanceOf() - .also { println(it) } } }) diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt index 3605f866..7d2fe3ae 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt @@ -25,7 +25,6 @@ class OidcSiopIsoProtocolTest : FreeSpec({ lateinit var verifierKeyMaterial: KeyMaterial lateinit var holderAgent: Holder - lateinit var verifierAgent: Verifier lateinit var holderSiop: OidcSiopWallet lateinit var verifierSiop: OidcSiopVerifier @@ -36,7 +35,6 @@ class OidcSiopIsoProtocolTest : FreeSpec({ relyingPartyUrl = "https://example.com/rp/${uuid4()}" walletUrl = "https://example.com/wallet/${uuid4()}" holderAgent = HolderAgent(holderKeyMaterial) - verifierAgent = VerifierAgent(verifierKeyMaterial) val issuerAgent = IssuerAgent( EphemeralKeyWithSelfSignedCert(), @@ -144,7 +142,6 @@ class OidcSiopIsoProtocolTest : FreeSpec({ verifierSiop = OidcSiopVerifier( keyMaterial = verifierKeyMaterial, relyingPartyUrl = relyingPartyUrl, - responseUrl = relyingPartyUrl + "/${uuid4()}" ) val requestOptions = OidcSiopVerifier.RequestOptions( credentials = setOf( @@ -153,12 +150,13 @@ class OidcSiopIsoProtocolTest : FreeSpec({ ) ), responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT, + responseUrl = "https://example.com/response", encryption = true ) val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, requestOptions = requestOptions - ).also { println(it) } + ) val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() authnResponse.shouldBeInstanceOf() @@ -211,12 +209,12 @@ private suspend fun runProcess( val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, requestOptions = requestOptions - ).also { println(it) } + ) val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() - authnResponse.shouldBeInstanceOf().also { println(it) } + authnResponse.shouldBeInstanceOf() val result = verifierSiop.validateAuthnResponse(authnResponse.url) result.shouldBeInstanceOf() - return result.document.also { println(it) } + return result.document } diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt index 9aa0719a..131f11e9 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt @@ -31,14 +31,12 @@ import kotlin.time.Duration.Companion.seconds class OidcSiopProtocolTest : FreeSpec({ lateinit var relyingPartyUrl: String - lateinit var responseUrl: String lateinit var walletUrl: String lateinit var holderKeyMaterial: KeyMaterial lateinit var verifierKeyMaterial: KeyMaterial lateinit var holderAgent: Holder - lateinit var verifierAgent: Verifier lateinit var holderSiop: OidcSiopWallet lateinit var verifierSiop: OidcSiopVerifier @@ -48,10 +46,8 @@ class OidcSiopProtocolTest : FreeSpec({ verifierKeyMaterial = EphemeralKeyWithoutCert() val rpUUID = uuid4() relyingPartyUrl = "https://example.com/rp/$rpUUID" - responseUrl = "https://example.com/rp/$rpUUID" walletUrl = "https://example.com/wallet/${uuid4()}" holderAgent = HolderAgent(holderKeyMaterial) - verifierAgent = VerifierAgent(verifierKeyMaterial) holderAgent.storeCredential( IssuerAgent( @@ -70,7 +66,6 @@ class OidcSiopProtocolTest : FreeSpec({ verifierSiop = OidcSiopVerifier( keyMaterial = verifierKeyMaterial, relyingPartyUrl = relyingPartyUrl, - responseUrl = responseUrl, ) } @@ -95,7 +90,6 @@ class OidcSiopProtocolTest : FreeSpec({ verifierSiop = OidcSiopVerifier( keyMaterial = verifierKeyMaterial, relyingPartyUrl = relyingPartyUrl, - responseUrl = responseUrl, nonceService = object : NonceService { override suspend fun provideNonce(): String { return uuid4().toString() @@ -130,7 +124,6 @@ class OidcSiopProtocolTest : FreeSpec({ qrcode shouldContain requestUrlNonce val metadataObject = verifierSiop.createSignedMetadata().getOrThrow() - .also { println(it) } DefaultVerifierJwsService().verifyJwsObject(metadataObject).shouldBeTrue() val authnRequestUrl = verifierSiop.createAuthnRequestUrlWithRequestObject(walletUrl, defaultRequestOptions) @@ -154,7 +147,8 @@ class OidcSiopProtocolTest : FreeSpec({ walletUrl = walletUrl, requestOptions = RequestOptions( credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)), - responseMode = OpenIdConstants.ResponseMode.DIRECT_POST + responseMode = OpenIdConstants.ResponseMode.DIRECT_POST, + responseUrl = relyingPartyUrl, ) ) @@ -173,7 +167,8 @@ class OidcSiopProtocolTest : FreeSpec({ walletUrl = walletUrl, requestOptions = RequestOptions( credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)), - responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT + responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT, + responseUrl = relyingPartyUrl, ) ) @@ -243,7 +238,6 @@ class OidcSiopProtocolTest : FreeSpec({ val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() authnResponse.shouldBeInstanceOf() - .also { println(it) } val result = verifierSiop.validateAuthnResponse(authnResponse.url) result.shouldBeInstanceOf() @@ -372,7 +366,6 @@ class OidcSiopProtocolTest : FreeSpec({ val authnResponse = holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow() authnResponse.shouldBeInstanceOf() - .also { println(it) } val result = verifierSiop.validateAuthnResponse(authnResponse.url) result.shouldBeInstanceOf() @@ -461,7 +454,7 @@ private suspend fun verifySecondProtocolRun( validation.shouldBeInstanceOf() } -private val defaultRequestOptions = OidcSiopVerifier.RequestOptions( +private val defaultRequestOptions = RequestOptions( credentials = setOf( OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023) ) diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt index 6134246f..f8a37c41 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt @@ -1,7 +1,10 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.openid.OpenIdConstants -import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.Asn1EncapsulatingOctetString +import at.asitplus.signum.indispensable.asn1.Asn1Primitive +import at.asitplus.signum.indispensable.asn1.Asn1String +import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.asn1.encoding.Asn1 import at.asitplus.signum.indispensable.pki.SubjectAltNameImplicitTags import at.asitplus.signum.indispensable.pki.X509CertificateExtension @@ -9,21 +12,16 @@ import at.asitplus.wallet.lib.agent.* import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.oidc.OidcSiopVerifier.RequestOptions import at.asitplus.wallet.lib.oidvci.formUrlEncode -import com.benasher44.uuid.uuid4 import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldNotBeEmpty import io.kotest.matchers.types.shouldBeInstanceOf class OidcSiopX509SanDnsTest : FreeSpec({ - lateinit var responseUrl: String - lateinit var walletUrl: String - lateinit var holderKeyMaterial: KeyMaterial lateinit var verifierKeyMaterial: KeyMaterial lateinit var holderAgent: Holder - lateinit var verifierAgent: Verifier lateinit var holderSiop: OidcSiopWallet lateinit var verifierSiop: OidcSiopVerifier @@ -42,10 +40,7 @@ class OidcSiopX509SanDnsTest : FreeSpec({ )))) holderKeyMaterial = EphemeralKeyWithoutCert() verifierKeyMaterial = EphemeralKeyWithSelfSignedCert(extensions = extensions) - responseUrl = "https://example.com" - walletUrl = "https://example.com/wallet/${uuid4()}" holderAgent = HolderAgent(holderKeyMaterial) - verifierAgent = VerifierAgent(verifierKeyMaterial) holderAgent.storeCredential( IssuerAgent( EphemeralKeyWithoutCert(), @@ -63,7 +58,6 @@ class OidcSiopX509SanDnsTest : FreeSpec({ ) verifierSiop = OidcSiopVerifier( keyMaterial = verifierKeyMaterial, - responseUrl = responseUrl, clientIdScheme = OidcSiopVerifier.ClientIdScheme.CertificateSanDns(listOf(verifierKeyMaterial.getCertificate()!!)), ) } @@ -79,11 +73,12 @@ class OidcSiopX509SanDnsTest : FreeSpec({ ) ), responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT, + responseUrl = "https://example.com/response", ) - ).also { println(it) }.getOrThrow() + ).getOrThrow() val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow() - authnResponse.shouldBeInstanceOf().also { println(it) } + authnResponse.shouldBeInstanceOf() val result = verifierSiop.validateAuthnResponseFromPost(authnResponse.params.formUrlEncode()) result.shouldBeInstanceOf() From 2f172c6e30332adc75df05890d3aeeb6432acd9a Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Thu, 26 Sep 2024 13:14:51 +0200 Subject: [PATCH 5/6] SIOP: Add option to set response type for requests --- .../asitplus/wallet/lib/oidc/OidcSiopVerifier.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index 1d0d40af..b1b16008 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -162,13 +162,23 @@ class OidcSiopVerifier private constructor( */ val credentials: Set, /** - * Response mode to request, see [OpenIdConstants.ResponseMode] + * Response mode to request, see [OpenIdConstants.ResponseMode], + * by default [OpenIdConstants.ResponseMode.FRAGMENT]. + * Setting this to any other value may require setting [responseUrl] too. */ val responseMode: OpenIdConstants.ResponseMode = OpenIdConstants.ResponseMode.FRAGMENT, /** - * Response URL to set in the [AuthenticationRequestParameters] + * Response URL to set in the [AuthenticationRequestParameters.responseUrl], + * required if [responseMode] is set to [OpenIdConstants.ResponseMode.DIRECT_POST] or + * [OpenIdConstants.ResponseMode.DIRECT_POST_JWT]. */ val responseUrl: String? = null, + /** + * Response type to set in [AuthenticationRequestParameters.responseType], + * by default only `vp_token` (as per OpenID4VP spec). + * Be sure to separate values by a space, e.g. `vp_token id_token`. + */ + val responseType: String = VP_TOKEN, /** * Opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult] */ @@ -296,7 +306,7 @@ class OidcSiopVerifier private constructor( suspend fun createAuthnRequest( requestOptions: RequestOptions, ) = AuthenticationRequestParameters( - responseType = "$ID_TOKEN $VP_TOKEN", // TODO move to RequestOptions + responseType = requestOptions.responseType, clientId = clientId, redirectUrl = requestOptions.buildRedirectUrl(), responseUrl = requestOptions.responseUrl, From c650799648d1d780dfdbea97a02d58a44bca23c8 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 27 Sep 2024 09:22:14 +0200 Subject: [PATCH 6/6] SIOP: Add validations in case only id token was requested --- CHANGELOG.md | 1 + .../wallet/lib/oidc/OidcSiopVerifier.kt | 204 ++++++++++-------- .../wallet/lib/oidc/OidcSiopProtocolTest.kt | 42 ++-- 3 files changed, 148 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fbc09da..f67ff2e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Release NEXT: - Require request options on every method in `OidcSiopVerifier` - Move `credentialScheme`, `representation`, `requestedAttributes` from `RequestOptions` to `RequestOptionsCredentials` - In `OidcSiopVerifier` move `responseUrl` from constructor parameter to `RequestOptions` + - Add `IdToken` as result case to `OidcSiopVerifier.AuthnResponseResult`, when only an `id_token` is requested and received Release 4.1.2: * In `OidcSiopVerifier` add parameter `nonceService` to externalize creation and validation of nonces, e.g. for deployments in load-balanced environments diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index b1b16008..ec4d3cd6 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -8,9 +8,7 @@ import at.asitplus.jsonpath.core.NormalizedJsonPath import at.asitplus.jsonpath.core.NormalizedJsonPathSegment import at.asitplus.openid.* import at.asitplus.openid.OpenIdConstants.BINDING_METHOD_JWK -import at.asitplus.openid.OpenIdConstants.ClientIdScheme.REDIRECT_URI -import at.asitplus.openid.OpenIdConstants.ClientIdScheme.VERIFIER_ATTESTATION -import at.asitplus.openid.OpenIdConstants.ClientIdScheme.X509_SAN_DNS +import at.asitplus.openid.OpenIdConstants.ClientIdScheme.* import at.asitplus.openid.OpenIdConstants.ID_TOKEN import at.asitplus.openid.OpenIdConstants.PREFIX_DID_KEY import at.asitplus.openid.OpenIdConstants.SCOPE_OPENID @@ -36,6 +34,7 @@ import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive +import kotlin.coroutines.cancellation.CancellationException import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -56,6 +55,8 @@ class OidcSiopVerifier private constructor( private val clock: Clock = Clock.System, private val nonceService: NonceService = DefaultNonceService(), private val clientIdScheme: ClientIdScheme = ClientIdScheme.RedirectUri, + private val stateToNonceStore: MapStore = DefaultMapStore(), + private val stateToResponseTypeStore: MapStore = DefaultMapStore(), ) { private val timeLeeway = timeLeewaySeconds.toDuration(DurationUnit.SECONDS) @@ -64,17 +65,21 @@ class OidcSiopVerifier private constructor( /** * Verifier Attestation JWT to include (in header `jwt`) when creating request objects as JWS, * to allow the Wallet to verify the authenticity of this Verifier. - * OID4VP client id scheme "verifier attestation", - * see [at.asitplus.wallet.lib.oidc.OpenIdConstants.ClientIdScheme.VERIFIER_ATTESTATION]. + * OID4VP client id scheme `verifier attestation`, + * see [at.asitplus.openid.OpenIdConstants.ClientIdScheme.VERIFIER_ATTESTATION]. */ data class VerifierAttestation(val attestationJwt: JwsSigned) : ClientIdScheme(VERIFIER_ATTESTATION) /** * Certificate chain to include in JWS headers and to extract `client_id` from (in SAN extension), from OID4VP - * client id scheme "x509_san_dns", - * see [at.asitplus.wallet.lib.oidc.OpenIdConstants.ClientIdScheme.X509_SAN_DNS]. + * client id scheme `x509_san_dns`, + * see [at.asitplus.openid.OpenIdConstants.ClientIdScheme.X509_SAN_DNS]. */ data class CertificateSanDns(val chain: CertificateChain) : ClientIdScheme(X509_SAN_DNS) + + /** + * Simple: `redirect_uri` has to match `client_id` + */ data object RedirectUri : ClientIdScheme(REDIRECT_URI) } @@ -87,7 +92,14 @@ class OidcSiopVerifier private constructor( timeLeewaySeconds: Long = 300L, clock: Clock = Clock.System, nonceService: NonceService = DefaultNonceService(), - clientIdScheme: ClientIdScheme = ClientIdScheme.RedirectUri + clientIdScheme: ClientIdScheme = ClientIdScheme.RedirectUri, + /** + * Used to store the nonce, associated to the state, to first send [AuthenticationRequestParameters.nonce], + * and then verify the challenge in the submitted verifiable presentation in + * [AuthenticationResponseParameters.vpToken]. + */ + stateToNonceStore: MapStore = DefaultMapStore(), + stateToResponseTypeStore: MapStore = DefaultMapStore(), ) : this( verifier = verifier, relyingPartyUrl = relyingPartyUrl, @@ -97,6 +109,8 @@ class OidcSiopVerifier private constructor( clock = clock, nonceService = nonceService, clientIdScheme = clientIdScheme, + stateToNonceStore = stateToNonceStore, + stateToResponseTypeStore = stateToResponseTypeStore, ) private val containerJwt = @@ -306,13 +320,15 @@ class OidcSiopVerifier private constructor( suspend fun createAuthnRequest( requestOptions: RequestOptions, ) = AuthenticationRequestParameters( - responseType = requestOptions.responseType, + responseType = requestOptions.responseType + .also { stateToResponseTypeStore.put(requestOptions.state, it) }, clientId = clientId, redirectUrl = requestOptions.buildRedirectUrl(), responseUrl = requestOptions.responseUrl, clientIdScheme = clientIdScheme.clientIdScheme, scope = requestOptions.buildScope(), - nonce = nonceService.provideNonce(), + nonce = nonceService.provideNonce() + .also { stateToNonceStore.put(requestOptions.state, it) }, clientMetadata = if (requestOptions.clientMetadataUrl != null) { null } else { @@ -435,6 +451,11 @@ class OidcSiopVerifier private constructor( */ data class ValidationError(val field: String, val state: String?) : AuthnResponseResult() + /** + * Wallet provided an `id_token`, no `vp_token` (as requested by us!) + */ + data class IdToken(val idToken: at.asitplus.openid.IdToken, val state: String?) : AuthnResponseResult() + /** * Validation results of all returned verifiable presentations */ @@ -496,10 +517,13 @@ class OidcSiopVerifier private constructor( * Validates [AuthenticationResponseParameters] from the Wallet */ suspend fun validateAuthnResponse(params: AuthenticationResponseParameters): AuthnResponseResult { + val state = params.state + ?: return AuthnResponseResult.ValidationError("state", params.state) + .also { Napier.w("Invalid state: ${params.state}") } params.response?.let { response -> JwsSigned.parse(response).getOrNull()?.let { jarmResponse -> if (!verifierJwsService.verifyJwsObject(jarmResponse)) { - return AuthnResponseResult.ValidationError("response", params.state) + return AuthnResponseResult.ValidationError("response", state) .also { Napier.w { "JWS of response not verified: ${params.response}" } } } AuthenticationResponseParameters.deserialize(jarmResponse.payload.decodeToString()) @@ -512,78 +536,113 @@ class OidcSiopVerifier private constructor( } } } - val idTokenJws = params.idToken - ?: return AuthnResponseResult.ValidationError("idToken", params.state) - .also { Napier.w("Could not parse idToken: $params") } + val responseType = stateToResponseTypeStore.get(state) + ?: return AuthnResponseResult.ValidationError("state", state) + .also { Napier.w("State not associated with response type: $state") } + + val idToken: IdToken? = if (responseType.contains(ID_TOKEN)) { + params.idToken?.let { idToken -> + catching { + extractValidatedIdToken(idToken) + }.getOrElse { + return AuthnResponseResult.ValidationError("idToken", state) + } + } ?: return AuthnResponseResult.ValidationError("idToken", state) + .also { Napier.w("State not associated with response type: $state") } + } else null + + if (responseType.contains(VP_TOKEN)) { + val expectedNonce = stateToNonceStore.get(state) + ?: return AuthnResponseResult.ValidationError("state", state) + .also { Napier.w("State not associated with nonce: $state") } + val presentationSubmission = params.presentationSubmission + ?: return AuthnResponseResult.ValidationError("presentation_submission", state) + .also { Napier.w("presentation_submission empty") } + val descriptors = presentationSubmission.descriptorMap + ?: return AuthnResponseResult.ValidationError("presentation_submission", state) + .also { Napier.w("presentation_submission contains no descriptors") } + val verifiablePresentation = params.vpToken + ?: return AuthnResponseResult.ValidationError("vp_token is null", state) + .also { Napier.w("No VP in response") } + + val validationResults = descriptors.map { descriptor -> + val relatedPresentation = + JsonPath(descriptor.cumulativeJsonPath).query(verifiablePresentation).first().value + val result = runCatching { + verifyPresentationResult(descriptor, relatedPresentation, expectedNonce) + }.getOrElse { + return AuthnResponseResult.ValidationError("Invalid presentation format", state) + .also { Napier.w("Invalid presentation format: $relatedPresentation") } + } + result.mapToAuthnResponseResult(state) + } + + return if (validationResults.size != 1) { + AuthnResponseResult.VerifiablePresentationValidationResults(validationResults) + } else validationResults[0] + } + + return idToken?.let { AuthnResponseResult.IdToken(it, state) } + ?: AuthnResponseResult.Error("Neither id_token nor vp_token", state) + } + + + @Throws(IllegalArgumentException::class, CancellationException::class) + private suspend fun extractValidatedIdToken(idTokenJws: String): IdToken { val jwsSigned = JwsSigned.parse(idTokenJws).getOrNull() - ?: return AuthnResponseResult.ValidationError("idToken", params.state) + ?: throw IllegalArgumentException("idToken") .also { Napier.w("Could not parse JWS from idToken: $idTokenJws") } if (!verifierJwsService.verifyJwsObject(jwsSigned)) - return AuthnResponseResult.ValidationError("idToken", params.state) + throw IllegalArgumentException("idToken") .also { Napier.w { "JWS of idToken not verified: $idTokenJws" } } - val idToken = IdToken.deserialize(jwsSigned.payload.decodeToString()).getOrElse { ex -> - return AuthnResponseResult.ValidationError("idToken", params.state) - .also { Napier.w("Could not deserialize idToken: $idTokenJws", ex) } - } + val idToken = IdToken.deserialize(jwsSigned.payload.decodeToString()).getOrThrow() if (idToken.issuer != idToken.subject) - return AuthnResponseResult.ValidationError("iss", params.state) + throw IllegalArgumentException("idToken.iss") .also { Napier.d("Wrong issuer: ${idToken.issuer}, expected: ${idToken.subject}") } val validAudiences = listOfNotNull(relyingPartyUrl, clientIdFromCertificateChain) if (idToken.audience !in validAudiences) - return AuthnResponseResult.ValidationError("aud", params.state) + throw IllegalArgumentException("idToken.aud") .also { Napier.d("audience not valid: ${idToken.audience}") } if (idToken.expiration < (clock.now() - timeLeeway)) - return AuthnResponseResult.ValidationError("exp", params.state) + throw IllegalArgumentException("idToken.exp") .also { Napier.d("expirationDate before now: ${idToken.expiration}") } if (idToken.issuedAt > (clock.now() + timeLeeway)) - return AuthnResponseResult.ValidationError("iat", params.state) + throw IllegalArgumentException("idToken.iat") .also { Napier.d("issuedAt after now: ${idToken.issuedAt}") } if (!nonceService.verifyAndRemoveNonce(idToken.nonce)) { - return AuthnResponseResult.ValidationError("nonce", params.state) + throw IllegalArgumentException("idToken.nonce") .also { Napier.d("nonce not valid: ${idToken.nonce}, not known to us") } } if (idToken.subjectJwk == null) - return AuthnResponseResult.ValidationError("nonce", params.state) + throw IllegalArgumentException("idToken.sub_jwk") .also { Napier.d("sub_jwk is null") } if (idToken.subject != idToken.subjectJwk!!.jwkThumbprint) - return AuthnResponseResult.ValidationError("sub", params.state) + throw IllegalArgumentException("idToken.sub") .also { Napier.d("subject does not equal thumbprint of sub_jwk: ${idToken.subject}") } + return idToken + } - val presentationSubmission = params.presentationSubmission - ?: return AuthnResponseResult.ValidationError("presentation_submission", params.state) - .also { Napier.w("presentation_submission empty") } - val descriptors = presentationSubmission.descriptorMap - ?: return AuthnResponseResult.ValidationError("presentation_submission", params.state) - .also { Napier.w("presentation_submission contains no descriptors") } - val verifiablePresentation = params.vpToken - ?: return AuthnResponseResult.ValidationError("vp_token is null", params.state) - .also { Napier.w("No VP in response") } - - val validationResults = descriptors.map { descriptor -> - val relatedPresentation = - JsonPath(descriptor.cumulativeJsonPath).query(verifiablePresentation).first().value - val result = runCatching { - when (descriptor.format) { - ClaimFormatEnum.JWT_VP -> verifyJwtVpResult(relatedPresentation, idToken) - ClaimFormatEnum.JWT_SD -> verifyJwtSdResult(relatedPresentation, idToken) - ClaimFormatEnum.MSO_MDOC -> verifyMsoMdocResult(relatedPresentation, idToken) - else -> throw IllegalArgumentException() - } - }.getOrElse { - return AuthnResponseResult.ValidationError("Invalid presentation format", params.state) - .also { Napier.w("Invalid presentation format: $relatedPresentation") } - } - result.mapToAuthnResponseResult(params.state) + /** + * Extract and verifies verifiable presentations, according to format defined in + * [OpenID for VCI](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html), + * as referenced by [OpenID for VP](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). + */ + private fun verifyPresentationResult( + descriptor: PresentationSubmissionDescriptor, + relatedPresentation: JsonElement, + challenge: String + ) = when (descriptor.format) { + ClaimFormatEnum.JWT_SD, + ClaimFormatEnum.MSO_MDOC, + ClaimFormatEnum.JWT_VP -> when (relatedPresentation) { + is JsonPrimitive -> verifier.verifyPresentation(relatedPresentation.content, challenge) + else -> throw IllegalArgumentException() } - return if (validationResults.size != 1) { - AuthnResponseResult.VerifiablePresentationValidationResults(validationResults) - } else validationResults[0] + else -> throw IllegalArgumentException() } - private fun Verifier.VerifyPresentationResult.mapToAuthnResponseResult( - state: String? - ) = when (this) { + private fun Verifier.VerifyPresentationResult.mapToAuthnResponseResult(state: String) = when (this) { is Verifier.VerifyPresentationResult.InvalidStructure -> AuthnResponseResult.Error("parse vp failed", state) .also { Napier.w("VP error: $this") } @@ -605,35 +664,6 @@ class OidcSiopVerifier private constructor( .also { Napier.i("VP success: $this") } } - private fun verifyMsoMdocResult( - relatedPresentation: JsonElement, - idToken: IdToken - ) = when (relatedPresentation) { - // must be a string - // source: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#appendix-A.2.5-1 - is JsonPrimitive -> verifier.verifyPresentation(relatedPresentation.content, idToken.nonce) - else -> throw IllegalArgumentException() - } - - private fun verifyJwtSdResult( - relatedPresentation: JsonElement, - idToken: IdToken - ) = when (relatedPresentation) { - // must be a string - // source: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#appendix-A.3.5-1 - is JsonPrimitive -> verifier.verifyPresentation(relatedPresentation.content, idToken.nonce) - else -> throw IllegalArgumentException() - } - - private fun verifyJwtVpResult( - relatedPresentation: JsonElement, - idToken: IdToken - ) = when (relatedPresentation) { - // must be a string - // source: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#appendix-A.1.1.5-1 - is JsonPrimitive -> verifier.verifyPresentation(relatedPresentation.content, idToken.nonce) - else -> throw IllegalArgumentException() - } } diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt index 131f11e9..fd1bc7c7 100644 --- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt +++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt @@ -3,6 +3,8 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.openid.AuthenticationRequestParameters import at.asitplus.openid.AuthenticationResponseParameters import at.asitplus.openid.OpenIdConstants +import at.asitplus.openid.OpenIdConstants.ID_TOKEN +import at.asitplus.openid.OpenIdConstants.VP_TOKEN import at.asitplus.signum.indispensable.josef.* import at.asitplus.wallet.lib.agent.* import at.asitplus.wallet.lib.data.AtomicAttribute2023 @@ -86,23 +88,39 @@ class OidcSiopProtocolTest : FreeSpec({ verifySecondProtocolRun(verifierSiop, walletUrl, holderSiop) } - "wrong client nonce should lead to error" { + "wrong client nonce in id_token should lead to error" { verifierSiop = OidcSiopVerifier( keyMaterial = verifierKeyMaterial, relyingPartyUrl = relyingPartyUrl, nonceService = object : NonceService { - override suspend fun provideNonce(): String { - return uuid4().toString() - } + override suspend fun provideNonce() = uuid4().toString() + override suspend fun verifyNonce(it: String) = false + override suspend fun verifyAndRemoveNonce(it: String) = false + } + ) + val requestOptions = RequestOptions( + credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)), + responseType = "$ID_TOKEN $VP_TOKEN" + ) + val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl, requestOptions) - override suspend fun verifyNonce(it: String): Boolean { - return false - } + val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() + authnResponse.shouldBeInstanceOf() - override suspend fun verifyAndRemoveNonce(it: String): Boolean { - return false - } - } + val result = verifierSiop.validateAuthnResponse(authnResponse.url) + result.shouldBeInstanceOf() + result.field shouldBe "idToken" + } + + "wrong client nonce in vp_token should lead to error" { + verifierSiop = OidcSiopVerifier( + keyMaterial = verifierKeyMaterial, + relyingPartyUrl = relyingPartyUrl, + stateToNonceStore = object : MapStore { + override suspend fun put(key: String, value: String) {} + override suspend fun get(key: String): String? = null + override suspend fun remove(key: String): String? = null + }, ) val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl, defaultRequestOptions) @@ -111,7 +129,7 @@ class OidcSiopProtocolTest : FreeSpec({ val result = verifierSiop.validateAuthnResponse(authnResponse.url) result.shouldBeInstanceOf() - result.field shouldBe "nonce" + result.field shouldBe "state" } "test with QR Code" {