diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ec14ec8..9eca2c6bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,10 @@ Release NEXT: - Fix name shadowing of gradle plugins by renaming file `Plugin.kt` -> `VcLibConventions.kt` - Fix: Add missing iOS exports - Add switch to disable composite build (useful for publishing) - + - Get rid of arrays in serializable types and use collections instead + - Improve interoperability with verifiers and issuers from + - `OidcSiopVerifier`: Move `credentialScheme` from constructor to `createAuthnRequest` + - `OidcSiopWallet`: Add constructor parameter to fetch JSON Web Key Sets Release 3.4.0: - Target Java 17 diff --git a/settings.gradle.kts b/settings.gradle.kts index 42b9d7781..7e9cc69a3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,3 +28,11 @@ rootProject.name = "vclibrary" include(":vclib") include(":vclib-aries") include(":vclib-openid") + +includeBuild("kmp-crypto") { + dependencySubstitution { + substitute(module("at.asitplus.crypto:datatypes")).using(project(":datatypes")) + substitute(module("at.asitplus.crypto:datatypes-jws")).using(project(":datatypes-jws")) + substitute(module("at.asitplus.crypto:datatypes-cose")).using(project(":datatypes-cose")) + } +} diff --git a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt index d921ba610..f5ad8acb4 100644 --- a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt +++ b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt @@ -208,17 +208,17 @@ class PresentProofProtocol( val claimsConstraints = requestedClaims?.map(this::buildConstraintFieldForClaim) ?: listOf() val typeConstraints = buildConstraintFieldForType(credentialScheme.vcType) val presentationDefinition = PresentationDefinition( - inputDescriptors = arrayOf( + inputDescriptors = listOf( InputDescriptor( name = credentialScheme.vcType, schema = SchemaReference(uri = credentialScheme.schemaUri), constraints = Constraint( - fields = (claimsConstraints + typeConstraints).toTypedArray() + fields = claimsConstraints + typeConstraints ) ) ), formats = FormatHolder( - jwtVp = FormatContainerJwt(arrayOf(JwsAlgorithm.ES256.identifier)) + jwtVp = FormatContainerJwt(listOf(JwsAlgorithm.ES256.identifier)) ) ) val requestPresentation = RequestPresentationAttachment( @@ -245,12 +245,12 @@ class PresentProofProtocol( } private fun buildConstraintFieldForType(attributeType: String) = ConstraintField( - path = arrayOf("\$.vc[*].type", "\$.type"), + path = listOf("\$.vc[*].type", "\$.type"), filter = ConstraintFilter(type = "string", const = attributeType) ) private fun buildConstraintFieldForClaim(claimName: String) = ConstraintField( - path = arrayOf("\$.vc[*].name", "\$.type"), + path = listOf("\$.vc[*].name", "\$.type"), filter = ConstraintFilter(type = "string", const = claimName) ) diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameters.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameters.kt index 08f968a42..17b97f857 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameters.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameters.kt @@ -1,9 +1,13 @@ package at.asitplus.wallet.lib.oidc +import at.asitplus.wallet.lib.data.InstantLongSerializer import at.asitplus.wallet.lib.data.dif.PresentationDefinition import at.asitplus.wallet.lib.oidvci.AuthorizationDetails +import io.github.aakira.napier.Napier +import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString /** * Contents of an OIDC Authentication Request. @@ -157,18 +161,25 @@ data class AuthenticationRequestParameters( val clientIdScheme: String? = null, /** - * May contain the Wallet's OIDC issuer URL, for discovery. - * Recommended in Dynamic Credential Request. + * OID4VP: OPTIONAL. String containing the Wallet's identifier. The Credential Issuer can use the discovery process + * defined in SIOPv2 to determine the Wallet's capabilities and endpoints, using the `wallet_issuer` value as the + * Issuer Identifier referred to in SIOPv2. This is RECOMMENDED in Dynamic Credential Requests. */ @SerialName("wallet_issuer") val walletIssuer: String? = null, /** - * Recommended in Dynamic Credential Request + * OID4VP: OPTIONAL. String containing an opaque End-User hint that the Wallet MAY use in subsequent callbacks to + * optimize the End-User's experience. This is RECOMMENDED in Dynamic Credential Requests. */ @SerialName("user_hint") val userHint: String? = null, + /** + * OID4VP: OPTIONAL. String value identifying a certain processing context at the Credential Issuer. A value for + * this parameter is typically passed in a Credential Offer from the Credential Issuer to the Wallet. This request + * parameter is used to pass the issuer_state value back to the Credential Issuer. + */ @SerialName("issuer_state") val issuerState: String? = null, @@ -209,4 +220,23 @@ data class AuthenticationRequestParameters( */ @SerialName("iss") val issuer: String? = null, -) + + /** + * OPTIONAL. Time at which the request was issued. + */ + @SerialName("iat") + @Serializable(with = InstantLongSerializer::class) + val issuedAt: Instant? = null, +) { + + fun serialize() = jsonSerializer.encodeToString(this) + + companion object { + fun deserialize(it: String) = kotlin.runCatching { + jsonSerializer.decodeFromString(it) + }.getOrElse { + Napier.w("deserialize failed", it) + null + } + } +} diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt index e8deaee99..4e7c1e84e 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt @@ -1,8 +1,10 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.wallet.lib.data.dif.PresentationSubmission +import io.github.aakira.napier.Napier import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString /** * Contents of an OIDC Authentication Response. @@ -61,4 +63,16 @@ data class AuthenticationResponseParameters( */ @SerialName("state") val state: String? = null, -) \ No newline at end of file +) { + + fun serialize() = jsonSerializer.encodeToString(this) + + companion object { + fun deserialize(it: String) = kotlin.runCatching { + jsonSerializer.decodeFromString(it) + }.getOrElse { + Napier.w("deserialize failed", it) + null + } + } +} diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/JsonWebKeySet.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/JsonWebKeySet.kt new file mode 100644 index 000000000..a8503c281 --- /dev/null +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/JsonWebKeySet.kt @@ -0,0 +1,26 @@ +package at.asitplus.wallet.lib.oidc + +import at.asitplus.crypto.datatypes.jws.JsonWebKey +import io.github.aakira.napier.Napier +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString + +@Serializable +data class JsonWebKeySet( + @SerialName("keys") + val keys: Collection, +) { + + fun serialize() = jsonSerializer.encodeToString(this) + + companion object { + fun deserialize(it: String) = kotlin.runCatching { + jsonSerializer.decodeFromString(it) + }.getOrElse { + Napier.w("deserialize failed", it) + null + } + } + +} \ No newline at end of file diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index 2c2c5ba07..073cf9a54 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -61,7 +61,6 @@ class OidcSiopVerifier( private val verifierJwsService: VerifierJwsService, timeLeewaySeconds: Long = 300L, private val clock: Clock = Clock.System, - private val credentialScheme: ConstantIndex.CredentialScheme? = null, ) { private val timeLeeway = timeLeewaySeconds.toDuration(DurationUnit.SECONDS) @@ -77,7 +76,6 @@ class OidcSiopVerifier( jwsService: JwsService = DefaultJwsService(cryptoService), timeLeewaySeconds: Long = 300L, clock: Clock = Clock.System, - credentialScheme: ConstantIndex.CredentialScheme? = null, ) = OidcSiopVerifier( verifier = verifier, relyingPartyUrl = relyingPartyUrl, @@ -86,18 +84,17 @@ class OidcSiopVerifier( verifierJwsService = verifierJwsService, timeLeewaySeconds = timeLeewaySeconds, clock = clock, - credentialScheme = credentialScheme, ) } private val containerJwt = - FormatContainerJwt(algorithms = verifierJwsService.supportedAlgorithms.map { it.identifier }.toTypedArray()) + FormatContainerJwt(algorithms = verifierJwsService.supportedAlgorithms.map { it.identifier }) private val metadata by lazy { RelyingPartyMetadata( - redirectUris = arrayOf(relyingPartyUrl), - jsonWebKeySet = JsonWebKeySet(arrayOf(agentPublicKey.toJsonWebKey())), - subjectSyntaxTypesSupported = arrayOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY), + redirectUris = listOf(relyingPartyUrl), + jsonWebKeySet = JsonWebKeySet(listOf(agentPublicKey.toJsonWebKey())), + subjectSyntaxTypesSupported = listOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY), vpFormats = FormatHolder( msoMdoc = containerJwt, jwtVp = containerJwt, @@ -139,6 +136,7 @@ class OidcSiopVerifier( * * @param responseMode which response mode to request, see [OpenIdConstants.ResponseModes] * @param representation specifies the required representation, see [ConstantIndex.CredentialRepresentation] + * @param credentialScheme which credential type to request, or `null` to make no restrictions * @param requestedAttributes list of attributes that shall be requested explicitly (selective disclosure) */ suspend fun createAuthnRequestUrl( @@ -146,6 +144,7 @@ class OidcSiopVerifier( responseMode: String? = null, representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT, state: String? = uuid4().toString(), + credentialScheme: ConstantIndex.CredentialScheme? = null, requestedAttributes: List? = null, ): String { val urlBuilder = URLBuilder(walletUrl) @@ -153,6 +152,7 @@ class OidcSiopVerifier( responseMode = responseMode, representation = representation, state = state, + credentialScheme = credentialScheme, requestedAttributes = requestedAttributes, ).encodeToParameters() .forEach { urlBuilder.parameters.append(it.key, it.value) } @@ -173,6 +173,7 @@ class OidcSiopVerifier( responseMode: String? = null, representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT, state: String? = uuid4().toString(), + credentialScheme: ConstantIndex.CredentialScheme? = null, requestedAttributes: List? = null, ): KmmResult { val urlBuilder = URLBuilder(walletUrl) @@ -180,6 +181,7 @@ class OidcSiopVerifier( responseMode = responseMode, representation = representation, state = state, + credentialScheme = credentialScheme, requestedAttributes = requestedAttributes, ).getOrElse { return KmmResult.failure(it) @@ -194,18 +196,21 @@ class OidcSiopVerifier( * @param responseMode which response mode to request, see [OpenIdConstants.ResponseModes] * @param representation specifies the required representation, see [ConstantIndex.CredentialRepresentation] * @param state opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult] + * @param credentialScheme which credential type to request, or `null` to make no restrictions * @param requestedAttributes list of attributes that shall be requested explicitly (selective disclosure) */ suspend fun createAuthnRequestAsRequestObject( responseMode: String? = null, representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT, state: String? = uuid4().toString(), + credentialScheme: ConstantIndex.CredentialScheme? = null, requestedAttributes: List? = null, ): KmmResult { val requestObject = createAuthnRequest( responseMode = responseMode, representation = representation, state = state, + credentialScheme = credentialScheme, requestedAttributes = requestedAttributes, ) val requestObjectSerialized = jsonSerializer.encodeToString( @@ -235,12 +240,14 @@ class OidcSiopVerifier( * @param responseMode which response mode to request, see [OpenIdConstants.ResponseModes] * @param representation specifies the required representation, see [ConstantIndex.CredentialRepresentation] * @param state opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult] + * @param credentialScheme which credential type to request, or `null` to make no restrictions * @param requestedAttributes list of attributes that shall be requested explicitly (selective disclosure) */ suspend fun createAuthnRequest( responseMode: String? = null, representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT, state: String? = uuid4().toString(), + credentialScheme: ConstantIndex.CredentialScheme? = null, requestedAttributes: List? = null, ): AuthenticationRequestParameters { val typeConstraint = credentialScheme?.let { @@ -250,8 +257,8 @@ class OidcSiopVerifier( ConstantIndex.CredentialRepresentation.ISO_MDOC -> it.isoConstraint() } } - val attributeConstraint = requestedAttributes?.let { createConstraints(representation, it) } ?: arrayOf() - val constraintFields = listOfNotNull(typeConstraint, *attributeConstraint).toTypedArray() + val attributeConstraint = requestedAttributes?.let { createConstraints(representation, it) } ?: listOf() + val constraintFields = attributeConstraint + typeConstraint return AuthenticationRequestParameters( responseType = "$ID_TOKEN $VP_TOKEN", clientId = relyingPartyUrl, @@ -266,11 +273,11 @@ class OidcSiopVerifier( presentationDefinition = PresentationDefinition( id = uuid4().toString(), formats = representation.toFormatHolder(), - inputDescriptors = arrayOf( + inputDescriptors = listOf( InputDescriptor( id = uuid4().toString(), - schema = arrayOf(SchemaReference(credentialScheme?.schemaUri ?: "https://example.com")), - constraints = Constraint(fields = constraintFields), + schema = listOf(SchemaReference(credentialScheme?.schemaUri ?: "https://example.com")), + constraints = Constraint(fields = constraintFields.filterNotNull()), ) ), ), @@ -284,7 +291,7 @@ class OidcSiopVerifier( } private fun ConstantIndex.CredentialScheme.vcConstraint() = ConstraintField( - path = arrayOf("$.type"), + path = listOf("$.type"), filter = ConstraintFilter( type = "string", pattern = vcType, @@ -292,7 +299,7 @@ class OidcSiopVerifier( ) private fun ConstantIndex.CredentialScheme.isoConstraint() = ConstraintField( - path = arrayOf("$.mdoc.doctype"), + path = listOf("$.mdoc.doctype"), filter = ConstraintFilter( type = "string", pattern = isoDocType, @@ -302,12 +309,12 @@ class OidcSiopVerifier( private fun createConstraints( credentialRepresentation: ConstantIndex.CredentialRepresentation, attributeTypes: List, - ): Array = attributeTypes.map { + ): Collection = attributeTypes.map { if (credentialRepresentation == ConstantIndex.CredentialRepresentation.ISO_MDOC) - ConstraintField(path = arrayOf("\$.mdoc.$it"), intentToRetain = false) + ConstraintField(path = listOf("\$.mdoc.$it"), intentToRetain = false) else - ConstraintField(path = arrayOf("\$.$it")) - }.toTypedArray() + ConstraintField(path = listOf("\$.$it")) + } sealed class AuthnResponseResult { @@ -410,7 +417,7 @@ class OidcSiopVerifier( val presentationSubmission = params.presentationSubmission ?: return AuthnResponseResult.ValidationError("presentation_submission", params.state) .also { Napier.w("presentation_submission empty") } - val descriptor = presentationSubmission.descriptorMap?.get(0) + val descriptor = presentationSubmission.descriptorMap?.firstOrNull() ?: return AuthnResponseResult.ValidationError("presentation_submission", params.state) .also { Napier.w("presentation_submission contains no descriptors") } val vp = params.vpToken diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt index 4cbf081ce..d18c4e092 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt @@ -2,12 +2,12 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.KmmResult import at.asitplus.crypto.datatypes.CryptoPublicKey -import at.asitplus.crypto.datatypes.jws.JwsAlgorithm import at.asitplus.crypto.datatypes.jws.JwsSigned import at.asitplus.crypto.datatypes.jws.toJsonWebKey import at.asitplus.wallet.lib.agent.CryptoService import at.asitplus.wallet.lib.agent.Holder import at.asitplus.wallet.lib.data.AttributeIndex +import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.dif.ClaimFormatEnum import at.asitplus.wallet.lib.data.dif.PresentationSubmission import at.asitplus.wallet.lib.data.dif.PresentationSubmissionDescriptor @@ -53,7 +53,18 @@ class OidcSiopWallet( private val jwsService: JwsService, private val verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(), private val clock: Clock = Clock.System, - private val clientId: String = "https://wallet.a-sit.at/" + private val clientId: String = "https://wallet.a-sit.at/", + /** + * Need to implement if JSON web keys are not specified directly as `jwks` in authn requests, + * but need to be retrieved from the `jwks_uri`. Implementations need to fetch the URL passed and return the + * content parsed as [JsonWebKeySet]. + */ + private val jwkSetRetriever: (String) -> JsonWebKeySet? = { null }, + /** + * Need to implement if `request_uri` parameters are used, i.e. the actual authn request can be retrieved + * from that URL. Implementations need to fetch the URL and return the content. + */ + private val requestRetriever: (String) -> String? = { null }, ) { companion object { @@ -63,7 +74,9 @@ class OidcSiopWallet( jwsService: JwsService = DefaultJwsService(cryptoService), verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(), clock: Clock = Clock.System, - clientId: String = "https://wallet.a-sit.at/" + clientId: String = "https://wallet.a-sit.at/", + jwkSetRetriever: (String) -> JsonWebKeySet? = { null }, + requestRetriever: (String) -> String? = { null }, ) = OidcSiopWallet( holder = holder, agentPublicKey = cryptoService.publicKey, @@ -71,6 +84,8 @@ class OidcSiopWallet( verifierJwsService = verifierJwsService, clock = clock, clientId = clientId, + jwkSetRetriever = jwkSetRetriever, + requestRetriever = requestRetriever, ) } @@ -126,6 +141,11 @@ class OidcSiopWallet( params.request?.let { requestObject -> return parseRequestObjectJws(requestObject) } + params.requestUri?.let { uri -> + requestRetriever.invoke(uri)?.let { requestObject -> + return parseRequestObjectJws(requestObject) + } + } return null } @@ -133,7 +153,7 @@ class OidcSiopWallet( JwsSigned.parse(requestObject)?.let { jws -> if (verifierJwsService.verifyJwsObject(jws)) { return kotlin.runCatching { - jsonSerializer.decodeFromString(jws.payload.decodeToString()) + AuthenticationRequestParameters.deserialize(jws.payload.decodeToString()) }.getOrNull() } } @@ -154,17 +174,17 @@ class OidcSiopWallet( if (!request.responseType.contains(ID_TOKEN) && !request.responseType.contains(VP_TOKEN)) { return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) } - if (request.responseMode?.contains(POST) == true) { + if (request.responseMode?.startsWith(POST) == true) { if (request.redirectUrl == null) return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) val body = responseParams.encodeToParameters().formUrlEncode() - KmmResult.success(AuthenticationResponseResult.Post(request.redirectUrl, body)) - } else if (request.responseMode?.contains(DIRECT_POST) == true) { + return KmmResult.success(AuthenticationResponseResult.Post(request.redirectUrl, body)) + } else if (request.responseMode?.startsWith(DIRECT_POST) == true) { if (request.responseUrl == null || request.redirectUrl != null) return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) val body = responseParams.encodeToParameters().formUrlEncode() - KmmResult.success(AuthenticationResponseResult.Post(request.responseUrl, body)) - } else if (request.responseMode?.contains(QUERY) == true) { + return KmmResult.success(AuthenticationResponseResult.Post(request.responseUrl, body)) + } else if (request.responseMode?.startsWith(QUERY) == true) { if (request.redirectUrl == null) return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) val url = URLBuilder(request.redirectUrl) @@ -174,7 +194,7 @@ class OidcSiopWallet( } } .buildString() - KmmResult.success(AuthenticationResponseResult.Redirect(url)) + return KmmResult.success(AuthenticationResponseResult.Redirect(url)) } else { // default for vp_token and id_token is fragment if (request.redirectUrl == null) @@ -182,7 +202,7 @@ class OidcSiopWallet( val url = URLBuilder(request.redirectUrl) .apply { encodedFragment = responseParams.encodeToParameters().formUrlEncode() } .buildString() - KmmResult.success(AuthenticationResponseResult.Redirect(url)) + return KmmResult.success(AuthenticationResponseResult.Redirect(url)) } }, { return KmmResult.failure(it) @@ -195,45 +215,55 @@ class OidcSiopWallet( suspend fun createAuthnResponseParams( params: AuthenticationRequestParameters ): KmmResult { - val audience = params.clientMetadata?.jsonWebKeySet?.keys?.get(0)?.identifier + if (params.clientMetadata == null) { + return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) + .also { Napier.w("client metadata is not specified") } + } + val audience = params.clientMetadata.jsonWebKeySet?.keys?.firstOrNull()?.identifier + ?: params.clientMetadata.jsonWebKeySetUrl?.let { jwkSetRetriever.invoke(it)?.keys?.firstOrNull()?.identifier } ?: return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) .also { Napier.w("Could not parse audience") } if (URN_TYPE_JWK_THUMBPRINT !in params.clientMetadata.subjectSyntaxTypesSupported) return KmmResult.failure(OAuth2Exception(Errors.SUBJECT_SYNTAX_TYPES_NOT_SUPPORTED)) .also { Napier.w("Incompatible subject syntax types algorithms") } - if (params.clientId != params.redirectUrl) + if (params.clientIdScheme == OpenIdConstants.ClientIdSchemes.REDIRECT_URI && params.redirectUrl == null) { return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) - .also { Napier.w("client_id does not match redirect_uri") } - if (params.responseType?.contains(ID_TOKEN) != true) + .also { Napier.w("client_id_scheme is redirect_uri, but that is not set") } + } + if (params.redirectUrl != null) { + if (params.clientId != params.redirectUrl) + return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) + .also { Napier.w("client_id does not match redirect_uri") } + } + if (params.responseType == null) return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) - .also { Napier.w("response_type is not \"$ID_TOKEN\"") } + .also { Napier.w("response_type is not specified") } if (!params.responseType.contains(VP_TOKEN) && params.presentationDefinition == null) return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) .also { Napier.w("vp_token not requested") } - // TODO Client shall send the client_id_scheme, which needs to be supported by the Wallet - if (params.clientMetadata.vpFormats == null) - return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) - .also { Napier.w("Incompatible subject syntax types algorithms") } - if (params.clientMetadata.vpFormats.jwtVp?.algorithms?.contains(jwsService.algorithm.identifier) != true) - return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) - .also { Napier.w("Incompatible JWT algorithms") } - if (params.clientMetadata.vpFormats.jwtSd?.algorithms?.contains(jwsService.algorithm.identifier) != true) - return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) - .also { Napier.w("Incompatible JWT algorithms") } - if (params.clientMetadata.vpFormats.msoMdoc?.algorithms?.contains(jwsService.algorithm.identifier) != true) - return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) - .also { Napier.w("Incompatible JWT algorithms") } + if (params.clientMetadata.vpFormats != null) { + if (params.clientMetadata.vpFormats.jwtVp?.algorithms?.contains(jwsService.algorithm.identifier) != true) + return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) + .also { Napier.w("Incompatible JWT algorithms") } + if (params.clientMetadata.vpFormats.jwtSd?.algorithms?.contains(jwsService.algorithm.identifier) != true) + return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) + .also { Napier.w("Incompatible JWT algorithms") } + if (params.clientMetadata.vpFormats.msoMdoc?.algorithms?.contains(jwsService.algorithm.identifier) != true) + return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) + .also { Napier.w("Incompatible JWT algorithms") } + } if (params.nonce == null) return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) .also { Napier.w("nonce is null") } val now = clock.now() // we'll assume jwk-thumbprint + val agentJsonWebKey = agentPublicKey.toJsonWebKey() val idToken = IdToken( - issuer = agentPublicKey.toJsonWebKey().jwkThumbprint, - subject = agentPublicKey.toJsonWebKey().jwkThumbprint, - subjectJwk = agentPublicKey.toJsonWebKey(), - audience = params.redirectUrl, + issuer = agentJsonWebKey.jwkThumbprint, + subject = agentJsonWebKey.jwkThumbprint, + subjectJwk = agentJsonWebKey, + audience = params.redirectUrl ?: params.clientId, issuedAt = now, expiration = now + 60.seconds, nonce = params.nonce, @@ -244,12 +274,28 @@ class OidcSiopWallet( return KmmResult.failure(OAuth2Exception(Errors.USER_CANCELLED)) } - val requestedScopes = (params.scope ?: "").split(" ") + val requestedAttributeTypes = (params.scope ?: "").split(" ") .filterNot { it == SCOPE_OPENID }.filterNot { it == SCOPE_PROFILE } - .mapNotNull { AttributeIndex.resolveAttributeType(it) } - .toList() + .filter { it.isNotEmpty() } + val requestedNamespace = params.presentationDefinition?.inputDescriptors + ?.mapNotNull { it.constraints } + ?.flatMap { it.fields?.toList() ?: listOf() } + ?.firstOrNull { it.path.toList().contains("$.mdoc.namespace") } + ?.filter?.const + val requestedSchemes = mutableListOf() + if (requestedNamespace != null) { + requestedSchemes.add(AttributeIndex.resolveIsoNamespace(requestedNamespace) + ?: return KmmResult.failure(OAuth2Exception(Errors.USER_CANCELLED)) + .also { Napier.w("Could not resolve requested namespace $requestedNamespace") }) + requestedAttributeTypes.forEach { requestedAttributeTyp -> + requestedSchemes.add(AttributeIndex.resolveAttributeType(requestedAttributeTyp) + ?: return KmmResult.failure(OAuth2Exception(Errors.USER_CANCELLED)) + .also { Napier.w("Could not resolve requested attribute type $it") }) + } + } val requestedClaims = params.presentationDefinition?.inputDescriptors - ?.mapNotNull { it.constraints }?.flatMap { it.fields?.toList() ?: listOf() } + ?.mapNotNull { it.constraints } + ?.flatMap { it.fields?.toList() ?: listOf() } ?.flatMap { it.path.toList() } ?.filter { it != "$.type" } ?.filter { it != "$.mdoc.doctype" } @@ -259,7 +305,7 @@ class OidcSiopWallet( val vp = holder.createPresentation( challenge = params.nonce, audienceId = audience, - credentialSchemes = requestedScopes.ifEmpty { null }, + credentialSchemes = requestedSchemes.toList().ifEmpty { null }, requestedClaims = requestedClaims.ifEmpty { null } ) ?: return KmmResult.failure(OAuth2Exception(Errors.USER_CANCELLED)) .also { Napier.w("Could not create presentation") } @@ -280,7 +326,7 @@ class OidcSiopWallet( path = "\$.verifiableCredential[0]" ), ) - }?.toTypedArray() + } ) return KmmResult.success( AuthenticationResponseParameters( @@ -302,7 +348,7 @@ class OidcSiopWallet( format = ClaimFormatEnum.JWT_SD, path = "\$", ) - }?.toTypedArray() + } ) return KmmResult.success( AuthenticationResponseParameters( @@ -324,7 +370,7 @@ class OidcSiopWallet( format = ClaimFormatEnum.MSO_MDOC, path = "\$", ) - }?.toTypedArray() + } ) return KmmResult.success( AuthenticationResponseParameters( @@ -339,5 +385,4 @@ class OidcSiopWallet( } } - } diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/RelyingPartyMetadata.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/RelyingPartyMetadata.kt index 9c739beee..27c8b2bb3 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/RelyingPartyMetadata.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/RelyingPartyMetadata.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.oidc -import at.asitplus.crypto.datatypes.jws.JsonWebKey +import at.asitplus.crypto.datatypes.jws.JweAlgorithm +import at.asitplus.crypto.datatypes.jws.JwsAlgorithm import at.asitplus.wallet.lib.data.dif.FormatHolder import io.github.aakira.napier.Napier import kotlinx.serialization.SerialName @@ -9,10 +10,92 @@ import kotlinx.serialization.encodeToString @Serializable data class RelyingPartyMetadata( + /** + * OIDC Registration: REQUIRED. Array of Redirection URI values used by the Client. One of these registered + * Redirection URI values MUST exactly match the `redirect_uri` parameter value used in each Authorization Request, + * with the matching performed as described in Section 6.2.1 of (RFC3986) (Simple String Comparison). + */ @SerialName("redirect_uris") - val redirectUris: Array, + val redirectUris: List? = null, + + /** + * OIDC Registration: OPTIONAL. Client's JWK Set document, passed by value. The semantics of the `jwks` parameter + * are the same as the [jsonWebKeySetUrl] parameter, other than that the JWK Set is passed by value, rather than by + * reference. This parameter is intended only to be used by Clients that, for some reason, are unable to use the + * [jsonWebKeySetUrl] parameter, for instance, by native applications that might not have a location to host the + * contents of the JWK Set. If a Client can use [jsonWebKeySetUrl], it MUST NOT use [jsonWebKeySet]. One significant + * downside of [jsonWebKeySet] is that it does not enable key rotation (which [jsonWebKeySetUrl] does, as described + * in Section 10 of OpenID Connect Core 1.0). The [jsonWebKeySetUrl] and [jsonWebKeySet] parameters MUST NOT be used + * together. The JWK Set MUST NOT contain private or symmetric key values. + */ @SerialName("jwks") - val jsonWebKeySet: JsonWebKeySet, + val jsonWebKeySet: JsonWebKeySet? = null, + + /** + * OIDC Registration: OPTIONAL. URL for the Client's JWK Set document, which MUST use the https scheme. If the + * Client signs requests to the Server, it contains the signing key(s) the Server uses to validate signatures from + * the Client. The JWK Set MAY also contain the Client's encryption keys(s), which are used by the Server to encrypt + * responses to the Client. When both signing and encryption keys are made available, a use (public key use) + * parameter value is REQUIRED for all keys in the referenced JWK Set to indicate each key's intended usage. + * Although some algorithms allow the same key to be used for both signatures and encryption, doing so is + * NOT RECOMMENDED, as it is less secure. The JWK `x5c` parameter MAY be used to provide X.509 representations of + * keys provided. When used, the bare key values MUST still be present and MUST match those in the certificate. + * The JWK Set MUST NOT contain private or symmetric key values. + */ + @SerialName("jwks_uri") + val jsonWebKeySetUrl: String? = null, + + /** + * OIDC Registration: OPTIONAL. JWS alg algorithm REQUIRED for signing the ID Token issued to this Client. + * The value none MUST NOT be used as the ID Token alg value unless the Client uses only Response Types that return + * no ID Token from the Authorization Endpoint (such as when only using the Authorization Code Flow). + * The default, if omitted, is RS256. + * The public key for validating the signature is provided by retrieving the JWK Set referenced by the `jwks_uri` + * element from OpenID Connect Discovery 1.0. + */ + @SerialName("id_token_signed_response_alg") + val idTokenSignedResponseAlg: JwsAlgorithm? = null, + + /** + * OID JARM: JWS (RFC7515) `alg` algorithm JWA (RFC7518). REQUIRED for signing authorization responses. + * If this is specified, the response will be signed using JWS and the configured algorithm. + * The algorithm `none` is not allowed. The default, if omitted, is RS256. + */ + @SerialName("authorization_signed_response_alg") + val authorizationSignedResponseAlg: JwsAlgorithm? = null, + + /** + * OID JARM: JWE (RFC7516) `alg` algorithm JWA (RFC7518). REQUIRED for encrypting authorization responses. + * If both signing and encryption are requested, the response will be signed then encrypted, with the result being + * a Nested JWT, as defined in JWT (RFC7519). The default, if omitted, is that no encryption is performed. + */ + @SerialName("authorization_encrypted_response_alg") + val authorizationEncryptedResponseAlg: JweAlgorithm? = null, + + /** + * OID JARM: JWE (RFC7516) `enc` algorithm JWA (RFC7518). REQUIRED for encrypting authorization responses. + * If [authorizationEncryptedResponseAlg] is specified, the default for this value is A128CBC-HS256. + * When [authorizationEncryptedResponseEncoding] is included, [authorizationEncryptedResponseAlg] MUST also be + * provided. + */ + @SerialName("authorization_encrypted_response_enc") + val authorizationEncryptedResponseEncoding: String? = null, + + /** + * OIDC Registration: OPTIONAL. JWE alg algorithm REQUIRED for encrypting the ID Token issued to this Client. + * If this is requested, the response will be signed then encrypted, with the result being a Nested JWT. + * The default, if omitted, is that no encryption is performed. + */ + @SerialName("id_token_encrypted_response_alg") + val idTokenEncryptedResponseAlg: JweAlgorithm? = null, + + /** + * OIDC Registration: OPTIONAL. JWE enc algorithm REQUIRED for encrypting the ID Token issued to this Client. + * If [idTokenEncryptedResponseAlg] is specified, the default value is A128CBC-HS256. + * When [idTokenEncryptedResponseEncoding] is included, [idTokenEncryptedResponseAlg] MUST also be provided. + */ + @SerialName("id_token_encrypted_response_enc") + val idTokenEncryptedResponseEncoding: String? = null, /** * OIDC SIOPv2: REQUIRED. A JSON array of strings representing URI scheme identifiers and optionally method names of @@ -20,7 +103,7 @@ data class RelyingPartyMetadata( * Valid values include `urn:ietf:params:oauth:jwk-thumbprint`, `did:example` and others. */ @SerialName("subject_syntax_types_supported") - val subjectSyntaxTypesSupported: Array, + val subjectSyntaxTypesSupported: List, /** * OID4VP: REQUIRED. An object defining the formats and proof types of Verifiable Presentations and Verifiable @@ -37,34 +120,10 @@ data class RelyingPartyMetadata( */ @SerialName("client_id_scheme") val clientIdScheme: String? = "pre-registered", - ) { fun serialize() = jsonSerializer.encodeToString(this) - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as RelyingPartyMetadata - - if (!redirectUris.contentEquals(other.redirectUris)) return false - if (jsonWebKeySet != other.jsonWebKeySet) return false - if (!subjectSyntaxTypesSupported.contentEquals(other.subjectSyntaxTypesSupported)) return false - if (vpFormats != other.vpFormats) return false - return clientIdScheme == other.clientIdScheme - } - - override fun hashCode(): Int { - var result = redirectUris.contentHashCode() - result = 31 * result + jsonWebKeySet.hashCode() - result = 31 * result + subjectSyntaxTypesSupported.contentHashCode() - result = 31 * result + (vpFormats?.hashCode() ?: 0) - result = 31 * result + (clientIdScheme?.hashCode() ?: 0) - return result - } - - companion object { fun deserialize(it: String) = kotlin.runCatching { jsonSerializer.decodeFromString(it) @@ -73,40 +132,4 @@ data class RelyingPartyMetadata( null } } - -} - - -@Serializable -data class JsonWebKeySet( - @SerialName("keys") - val keys: Array, -) { - - fun serialize() = jsonSerializer.encodeToString(this) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as JsonWebKeySet - - if (!keys.contentEquals(other.keys)) return false - - return true - } - - override fun hashCode(): Int { - return keys.contentHashCode() - } - - companion object { - fun deserialize(it: String) = kotlin.runCatching { - jsonSerializer.decodeFromString(it) - }.getOrElse { - Napier.w("deserialize failed", it) - null - } - } - } diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/mdl/ClaimDisplayProperties.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/mdl/ClaimDisplayProperties.kt index 715090ce2..a766db37e 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/mdl/ClaimDisplayProperties.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/mdl/ClaimDisplayProperties.kt @@ -6,15 +6,14 @@ import kotlinx.serialization.Serializable @Serializable data class ClaimDisplayProperties( /** - * OID4VCI: - * OPTIONAL. String value of a display name for the claim. + * OID4VCI: OPTIONAL. String value of a display name for the claim. */ @SerialName("name") val name: String? = null, /** - * OID4VCI: - * OPTIONAL. String value that identifies language of this object represented as language tag values defined in BCP47 [RFC5646]. + * OID4VCI: OPTIONAL. String value that identifies language of this object represented as language tag values + * defined in BCP47 (RFC5646). */ @SerialName("locale") val locale: String? = null, diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt index c9715bcab..fea590b56 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt @@ -2,7 +2,6 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.KmmResult import at.asitplus.crypto.datatypes.CryptoPublicKey -import at.asitplus.crypto.datatypes.jws.toJsonWebKey import at.asitplus.wallet.lib.agent.ClaimToBeIssued import at.asitplus.wallet.lib.agent.CredentialToBeIssued import at.asitplus.wallet.lib.agent.Issuer @@ -106,6 +105,34 @@ class DummyCredentialDataProvider( ) ) } + + if (credentialScheme == EudiwPidCredentialScheme) { + val subjectId = subjectPublicKey.didEncoded + val claims = listOfNotNull( + optionalClaim(claimNames, "family_name", "someone"), + ) + credentials += when (representation) { + ConstantIndex.CredentialRepresentation.SD_JWT -> listOf( + CredentialToBeIssued.VcSd(claims = claims, expiration = expiration) + ) + + ConstantIndex.CredentialRepresentation.PLAIN_JWT -> listOf( + CredentialToBeIssued.VcJwt( + subject = EudiwPid1(subjectId, "someone"), + expiration = expiration, + ) + ) + + ConstantIndex.CredentialRepresentation.ISO_MDOC -> listOf( + CredentialToBeIssued.Iso( + issuerSignedItems = claims.mapIndexed { index, claim -> + issuerSignedItem(claim.name, claim.value, index.toUInt()) + }, + expiration = expiration, + ) + ) + } + } return KmmResult.success(credentials) } diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EqualityTests.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EqualityTests.kt new file mode 100644 index 000000000..b330e3eff --- /dev/null +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EqualityTests.kt @@ -0,0 +1,73 @@ +package at.asitplus.wallet.lib.oidc + +import at.asitplus.crypto.datatypes.jws.JsonWebKey +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.booleans.shouldBeTrue +import kotlinx.serialization.SerialName +import kotlin.random.Random + +class EqualityTests : FreeSpec({ + + lateinit var jwk1: JsonWebKey + lateinit var jwk2: JsonWebKey + + beforeEach { + jwk1 = JsonWebKey(x = Random.Default.nextBytes(32)) + jwk2 = JsonWebKey(x = Random.Default.nextBytes(32)) + } + + "JsonWebKeySet new" { + val first = JsonWebKeySet(keys = listOf(jwk1, jwk2)) + val second = JsonWebKeySet(keys = listOf(jwk1, jwk2)) + + val equals = first == second + + equals.shouldBeTrue() + } + + "JsonWebKeySet new unordered" { + val first = JsonWebKeySet(keys = setOf(jwk1, jwk2)) + val second = JsonWebKeySet(keys = setOf(jwk2, jwk1)) + + val equals = first == second + + equals.shouldBeTrue() + } + + "JsonWebKeySet old" { + val first = OldJsonWebKeySet(keys = arrayOf(jwk1, jwk2)) + val second = OldJsonWebKeySet(keys = arrayOf(jwk1, jwk2)) + + val equals = first == second + + equals.shouldBeTrue() + } + + "JsonWebKeySet old unordered" { + val first = OldJsonWebKeySet(keys = arrayOf(jwk1, jwk2)) + val second = OldJsonWebKeySet(keys = arrayOf(jwk1, jwk2).reversedArray()) + + val equals = first == second + + // this is false, because the order matters on arrays + //equals.shouldBeTrue() + } +}) + +data class OldJsonWebKeySet( + @SerialName("keys") + val keys: Array, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as OldJsonWebKeySet + + return keys.contentEquals(other.keys) + } + + override fun hashCode(): Int { + return keys.contentHashCode() + } +} \ No newline at end of file diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EudiwPid1.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EudiwPid1.kt new file mode 100644 index 000000000..1b4e501b2 --- /dev/null +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EudiwPid1.kt @@ -0,0 +1,14 @@ +package at.asitplus.wallet.lib.oidc + +import at.asitplus.wallet.lib.data.CredentialSubject +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("EudiwPid1") +data class EudiwPid1( + override val id: String, + + @SerialName("family_name") + val familyName: String, +) : CredentialSubject() \ No newline at end of file diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EudiwPidCredentialScheme.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EudiwPidCredentialScheme.kt new file mode 100644 index 000000000..85f7f29b4 --- /dev/null +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/EudiwPidCredentialScheme.kt @@ -0,0 +1,11 @@ +package at.asitplus.wallet.lib.oidc + +import at.asitplus.wallet.lib.data.ConstantIndex + +object EudiwPidCredentialScheme : ConstantIndex.CredentialScheme { + override val schemaUri: String = "https://wallet.a-sit.at/schemas/1.0.0/EudiwPid1.json" + override val vcType: String = "EudiwPid1" + override val isoNamespace: String = "eu.europa.ec.eudiw.pid.1" + override val isoDocType: String = "eu.europa.ec.eudiw.pid.1" + override val claimNames: Collection = listOf("family_name") +} \ No newline at end of file diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt new file mode 100644 index 000000000..0dddfc7c9 --- /dev/null +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt @@ -0,0 +1,300 @@ +package at.asitplus.wallet.lib.oidc + +import at.asitplus.crypto.datatypes.jws.JweAlgorithm +import at.asitplus.crypto.datatypes.jws.JwsAlgorithm +import at.asitplus.wallet.lib.LibraryInitializer +import at.asitplus.wallet.lib.agent.CryptoService +import at.asitplus.wallet.lib.agent.DefaultCryptoService +import at.asitplus.wallet.lib.agent.Holder +import at.asitplus.wallet.lib.agent.HolderAgent +import at.asitplus.wallet.lib.agent.IssuerAgent +import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.data.CredentialSubject +import at.asitplus.wallet.lib.oidvci.decodeFromPostBody +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Instant +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass + +/** + * Tests our SIOP implementation against EUDI Ref Impl., + * see [https://verifier.eudiw.dev/cbor-selectable/verifiable](https://verifier.eudiw.dev/cbor-selectable/verifiable) + */ +class OidcSiopInteropTest : FreeSpec({ + + lateinit var holderCryptoService: CryptoService + lateinit var holderAgent: Holder + lateinit var holderSiop: OidcSiopWallet + + beforeSpec { + LibraryInitializer.registerExtensionLibrary( + LibraryInitializer.ExtensionLibraryInfo( + credentialScheme = EudiwPidCredentialScheme, + serializersModule = SerializersModule { + polymorphic(CredentialSubject::class) { + subclass(EudiwPid1::class) + } + }, + ) + ) + } + + beforeEach { + holderCryptoService = DefaultCryptoService() + holderAgent = HolderAgent.newDefaultInstance(holderCryptoService) + runBlocking { + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(EudiwPidCredentialScheme.vcType), + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).toStoreCredentialInput() + ) + } + } + + "EUDI from URL" { + val url = """ + eudi-openid4vp://verifier-backend.eudiw.dev?client_id=verifier-backend.eudiw.dev&request_uri=https%3A%2F%2F + verifier-backend.eudiw.dev%2Fwallet%2Frequest.jwt%2FWLFJEn9AGbJfAcEyaQTzzxueqmeRazmsHIkxMRTkGRL1zyI7un + -KJWaXtulrfiSS38LlU5ABDB9Zdsfq_11r8Q + """.trimIndent().replace("\n", "") + + val requestObject = """ + eyJ4NWMiOlsiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVUkxha05EUVhKRFowRjNTVUpCWjBsVlpuazVkVFpU + VEhSblRuVm1PVkJZV1dKb0wxRkVjWFZZZWpVd2QwTm5XVWxMYjFwSmVtb3dSVUYzU1hkWVJFVmxUVUozUjBFeFZVVkJkM2RXVlVWc1JVbEZi + SHBqTTFac1kybENSRkZUUVhSSlJsWlZTVVJCZUUxVE1IZExkMWxFVmxGUlMwUkRVa1pXVlZKS1NVWmthR0pIZUd4a1EwSlRXbGRhYkdOdFZu + VlpNbFZuVTFjeGQySkhWblJhVnpVd1dWaFNjR0l5TkhoRGVrRktRbWRPVmtKQldWUkJiRlpWVFVJMFdFUlVTVEJOUkVsNVRtcEJlVTE2V1hw + Tk1XOVlSRlJKTWsxRVNYbE9WRUY1VFhwWmVrMXNiM2RoVkVWa1RVSnpSMEV4VlVWQmQzZFZVbFpXUlZOVFFsTmFWekYyWkVkVloxWnRWbmxo + VjFwd1dsaEplRVJFUVV0Q1owNVdRa0ZWVkVGNlFYZE5WRVYwVFVOelIwRXhWVVZEWjNkclVsWldSVk5UUWxoWlYzaHpXbGhSWjFWdFZtMWFX + RXBzWW0xT2JFbEZiSFJqUjNoc1lsZFdkV1JIUmpCaFZ6bDFUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsWkVRbHBOUWsxSFFubHhSMU5OTkRsQlow + VkhRME54UjFOTk5EbEJkMFZJUVRCSlFVSk5ZbGRDUVVNeFIyb3JSMFJQTDNsRFUySm5Za1ozY0dsMlVGbFhUSHBGZGtsTVRuUmtRM1kzVkhn + eFJYTjRVRU40UW5BelJGcENORVpKY2pSQ2JHMVdXWFJIWVZWaWIxWkphV2hTUW1sUlJHOHpUWEJYYVdwblowWkNUVWxKUWxCVVFVMUNaMDVX + U0ZKTlFrRm1PRVZCYWtGQlRVSTRSMEV4VldSSmQxRlpUVUpoUVVaTVRuTjFTa1ZZU0U1bGEwZHRXWGhvTUV4b2FUaENRWHBLVldKTlExVkhR + VEZWWkVWUlVXVk5RbmxEUjI1YWJHTnRiRzFoVjFaNVRGZEthRmt5ZEd4aWJWRjFXbGhXYTJGWVkzVmFSMVl5VFVKSlIwRXhWV1JLVVZGTVRV + RnJSMEo1YVVKcVJqQkdRVkZaZDFGM1dVUldVakJtUWtSM2QwOXFRVFJ2UkdGblRrbFplV0ZJVWpCalNFMDJUSGs1ZDJOdFZuZGpiVGxyVEc1 + Q2NtRlROV3hrVjFKd1pIazFhMXBZV1haWk0wcHpURE5DY0ZwR09VUlJWamxXVmtZNGQwMVROV3BqYlhkM1NGRlpSRlpTTUU5Q1FsbEZSa1pu + YlVGbmRVSlRkbE51YlRZNFducHZOVWxUZEVsMk1tWk5NazFCTkVkQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQ1pFSm5UbFpJVWtsRlZtcENW + V2hzU205a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFphV0ZWMFdrZHNibUZZVW1oaVF6RndXa2RXZFdSSGJEQmxVekV6V1ZkNGMx + cFlVWFpaV0VwcVlVZHNNRnBYVGpCa1dFcHNURmRHZFZwRE1YbGFWMXBzWTIxV2RWa3lWWFJhYmtwb1lsZFdNMkl6U25KTlFXOUhRME54UjFO + Tk5EbENRVTFEUVRKblFVMUhWVU5OVVVSSFptZE1TMjVpUzJocFQxWkdNM2hUVlRCaFpXcDFMMjVsUjFGVlZuVk9Zbk5SZHpCTVpVUkVkMGxY + SzNKTVlYUmxZbEpuYnpsb1RWaEVZek4zY214VlEwMUJTVnA1U2pkc1VsSldaWGxOY2pOM2FuRnJRa1l5YkRsWllqQjNUMUZ3YzI1YVFrRldW + VUZRZVVrMWVHaFhXREpUUVdGNmIyMHlTbXB6VGk5aFMwRnJVVDA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzA9IiwiTFMw + dExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVUklWRU5EUVhGUFowRjNTVUpCWjBsVlZuRnFaM1JLY1dZMGFGVlpTbXR4 + WkZsNmFTc3dlSGRvZDBaWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhkWVJFVmxUVUozUjBFeFZVVkJkM2RXVlVWc1JVbEZiSHBqTTFac1kybENS + RkZUUVhSSlJsWlZTVVJCZUUxVE1IZExkMWxFVmxGUlMwUkRVa1pXVlZKS1NVWmthR0pIZUd4a1EwSlRXbGRhYkdOdFZuVlpNbFZuVTFjeGQy + SkhWblJhVnpVd1dWaFNjR0l5TkhoRGVrRktRbWRPVmtKQldWUkJiRlpWVFVJMFdFUlVTWHBOUkd0M1RWUkZORTE2VVhoT01XOVlSRlJOZVUx + VVJYbE9la1UwVFhwUmVFNXNiM2RZUkVWbFRVSjNSMEV4VlVWQmQzZFdWVVZzUlVsRmJIcGpNMVpzWTJsQ1JGRlRRWFJKUmxaVlNVUkJlRTFU + TUhkTGQxbEVWbEZSUzBSRFVrWldWVkpLU1Vaa2FHSkhlR3hrUTBKVFdsZGFiR050Vm5WWk1sVm5VMWN4ZDJKSFZuUmFWelV3V1ZoU2NHSXlO + SGhEZWtGS1FtZE9Wa0pCV1ZSQmJGWlZUVWhaZDBWQldVaExiMXBKZW1vd1EwRlJXVVpMTkVWRlFVTkpSRmxuUVVWR1p6VlRhR1p6ZUhBMVVp + OVZSa2xGUzFNelRESTNaSGR1Um1odWFsTm5WV2d5WW5STFQxRkZibVppTTJSdmVXVnhUVUYyUW5SVlRXeERiR2h6UmpOMVpXWkxhVzVEZHpB + NFRrSXpNWEozUXl0a2RHbzJXQzlNUlROdU1rTTVhbEpQU1ZWT09GQnlibXhNVXpWUmN6UlNjelJhVlRWUFNXZDZkRzloVHpoSE9XODBTVUpL + UkVORFFWTkJkMFZuV1VSV1VqQlVRVkZJTDBKQlozZENaMFZDTDNkSlFrRkVRV1pDWjA1V1NGTk5SVWRFUVZkblFsTjZZa3hwVWtaNGVsaHdR + bkJ0VFZsa1F6Ulpka0ZSVFhsV1IzcEJWMEpuVGxaSVUxVkNRV1k0UlVSRVFVdENaMmR5WjFGSlEwRkJRVUpDZWtKRVFtZE9Wa2hTT0VWUVJF + RTJUVVJwWjA1eFFUQm9ha3B2WkVoU2QyTjZiM1pNTTBKNVdsaENlV0l5VVhWalIzUndURzFXTVZwSGJETk1iVkpzWkdrNWFtTnRkM1pqUjJ4 + cldEQk9RbGd4VmxWWWVrRjRURzFPZVdKRVFXUkNaMDVXU0ZFMFJVWm5VVlZ6TW5rMGExSmpZekUyVVdGYWFrZElVWFZIVEhkRlJFMXNVbk4z + UkdkWlJGWlNNRkJCVVVndlFrRlJSRUZuUlVkTlJqQkhRVEZWWkVWblVsZE5SbE5IVlcxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZ + bE01YkdSVE1XdGhWMlJ3WkVkR2MweFhiR3RhVnpVd1lWaFNOVXhZWkdoaVIzaHNaRU01YUdOdFRtOWhXRkpzV1ROU01XTnRWWFJaVnpWclRG + aEtiRnB0Vm5sYVZ6VnFXbE14YldOdFJuUmFXR1IyWTIxemQwTm5XVWxMYjFwSmVtb3dSVUYzVFVSaFFVRjNXbEZKZDJGWVZVRXphaXNyZUd3 + dmRHUkVOelowV0VWWFEybHJaazB4UTJGU2VqUjJla0pETjA1VE1IZERaRWwwUzJsNk5raGFaVlk0UlZCMFRrTnVjMlpMY0U1QmFrVkJjWEpr + WlV0RWJuSTFTM2RtT0VKQk4zUkJWR1ZvZUU1c1QxWTBTRzVqTVRCWVR6RllWVXgwYVdkRGQySTBPVkp3YTNGc1V6SklkV3dyUkhCeFQySlZj + d290TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHQiXSwidHlwIjoib2F1dGgtYXV0aHotcmVxK2p3dCIsImFsZyI6IkVTMjU2In0. + eyJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLWJhY2tlbmQuZXVkaXcuZGV2L3dhbGxldC9kaXJlY3RfcG9zdCIsImNsaWVudF9p + ZF9zY2hlbWUiOiJ4NTA5X3Nhbl9kbnMiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJub25jZSI6Im5vbmNlIiwiY2xpZW50X2lkIjoi + dmVyaWZpZXItYmFja2VuZC5ldWRpdy5kZXYiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3Quand0IiwiYXVkIjoiaHR0cHM6Ly9zZWxm + LWlzc3VlZC5tZS92MiIsInNjb3BlIjoiIiwicHJlc2VudGF0aW9uX2RlZmluaXRpb24iOnsiaWQiOiIzMmY1NDE2My03MTY2LTQ4ZjEtOTNk + OC1mZjIxN2JkYjA2NTMiLCJpbnB1dF9kZXNjcmlwdG9ycyI6W3siaWQiOiJldWRpX3BpZCIsIm5hbWUiOiJFVURJIFBJRCIsInB1cnBvc2Ui + OiJXZSBuZWVkIHRvIHZlcmlmeSB5b3VyIGlkZW50aXR5IiwiY29uc3RyYWludHMiOnsiZmllbGRzIjpbeyJwYXRoIjpbIiQubWRvYy5kb2N0 + eXBlIl0sImZpbHRlciI6eyJ0eXBlIjoic3RyaW5nIiwiY29uc3QiOiJldS5ldXJvcGEuZWMuZXVkaXcucGlkLjEifX0seyJwYXRoIjpbIiQu + bWRvYy5uYW1lc3BhY2UiXSwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJjb25zdCI6ImV1LmV1cm9wYS5lYy5ldWRpdy5waWQuMSJ9fSx7 + InBhdGgiOlsiJC5tZG9jLmZhbWlseV9uYW1lIl0sImludGVudF90b19yZXRhaW4iOmZhbHNlfSx7InBhdGgiOlsiJC5tZG9jLmdpdmVuX25h + bWUiXSwiaW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MuYmlydGhfZGF0ZSJdLCJpbnRlbnRfdG9fcmV0YWluIjpm + YWxzZX0seyJwYXRoIjpbIiQubWRvYy5hZ2Vfb3Zlcl8xOCJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX0seyJwYXRoIjpbIiQubWRvYy5h + Z2VfaW5feWVhcnMiXSwiaW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MuYWdlX2JpcnRoX3llYXIiXSwiaW50ZW50 + X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MuZmFtaWx5X25hbWVfYmlydGgiXSwiaW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9 + LHsicGF0aCI6WyIkLm1kb2MuZ2l2ZW5fbmFtZV9iaXJ0aCJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX0seyJwYXRoIjpbIiQubWRvYy5i + aXJ0aF9wbGFjZSJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX0seyJwYXRoIjpbIiQubWRvYy5iaXJ0aF9jb3VudHJ5Il0sImludGVudF90 + b19yZXRhaW4iOmZhbHNlfSx7InBhdGgiOlsiJC5tZG9jLmJpcnRoX3N0YXRlIl0sImludGVudF90b19yZXRhaW4iOmZhbHNlfSx7InBhdGgi + OlsiJC5tZG9jLmJpcnRoX2NpdHkiXSwiaW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MucmVzaWRlbnRfYWRkcmVz + cyJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX0seyJwYXRoIjpbIiQubWRvYy5yZXNpZGVudF9jb3VudHJ5Il0sImludGVudF90b19yZXRh + aW4iOmZhbHNlfSx7InBhdGgiOlsiJC5tZG9jLnJlc2lkZW50X3N0YXRlIl0sImludGVudF90b19yZXRhaW4iOmZhbHNlfSx7InBhdGgiOlsi + JC5tZG9jLnJlc2lkZW50X2NpdHkiXSwiaW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MucmVzaWRlbnRfcG9zdGFs + X2NvZGUiXSwiaW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MucmVzaWRlbnRfc3RyZWV0Il0sImludGVudF90b19y + ZXRhaW4iOmZhbHNlfSx7InBhdGgiOlsiJC5tZG9jLnJlc2lkZW50X2hvdXNlX251bWJlciJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX0s + eyJwYXRoIjpbIiQubWRvYy5nZW5kZXIiXSwiaW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MubmF0aW9uYWxpdHki + XSwiaW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MuaXNzdWFuY2VfZGF0ZSJdLCJpbnRlbnRfdG9fcmV0YWluIjpm + YWxzZX0seyJwYXRoIjpbIiQubWRvYy5leHBpcnlfZGF0ZSJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX0seyJwYXRoIjpbIiQubWRvYy5p + c3N1aW5nX2F1dGhvcml0eSJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX0seyJwYXRoIjpbIiQubWRvYy5kb2N1bWVudF9udW1iZXIiXSwi + aW50ZW50X3RvX3JldGFpbiI6ZmFsc2V9LHsicGF0aCI6WyIkLm1kb2MuYWRtaW5pc3RyYXRpdmVfbnVtYmVyIl0sImludGVudF90b19yZXRh + aW4iOmZhbHNlfSx7InBhdGgiOlsiJC5tZG9jLmlzc3VpbmdfY291bnRyeSJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX0seyJwYXRoIjpb + IiQubWRvYy5pc3N1aW5nX2p1cmlzZGljdGlvbiJdLCJpbnRlbnRfdG9fcmV0YWluIjpmYWxzZX1dfX1dfSwic3RhdGUiOiJXTEZKRW45QUdi + SmZBY0V5YVFUenp4dWVxbWVSYXptc0hJa3hNUlRrR1JMMXp5STd1bi1LSldhWHR1bHJmaVNTMzhMbFU1QUJEQjlaZHNmcV8xMXI4USIsImlh + dCI6MTcxMDc2NjI5NCwiY2xpZW50X21ldGFkYXRhIjp7ImF1dGhvcml6YXRpb25fZW5jcnlwdGVkX3Jlc3BvbnNlX2FsZyI6IkVDREgtRVMi + LCJhdXRob3JpemF0aW9uX2VuY3J5cHRlZF9yZXNwb25zZV9lbmMiOiJBMTI4Q0JDLUhTMjU2IiwiaWRfdG9rZW5fZW5jcnlwdGVkX3Jlc3Bv + bnNlX2FsZyI6IlJTQS1PQUVQLTI1NiIsImlkX3Rva2VuX2VuY3J5cHRlZF9yZXNwb25zZV9lbmMiOiJBMTI4Q0JDLUhTMjU2Iiwiandrc191 + cmkiOiJodHRwczovL3ZlcmlmaWVyLWJhY2tlbmQuZXVkaXcuZGV2L3dhbGxldC9qYXJtL1dMRkpFbjlBR2JKZkFjRXlhUVR6enh1ZXFtZVJh + em1zSElreE1SVGtHUkwxenlJN3VuLUtKV2FYdHVscmZpU1MzOExsVTVBQkRCOVpkc2ZxXzExcjhRL2p3a3MuanNvbiIsInN1YmplY3Rfc3lu + dGF4X3R5cGVzX3N1cHBvcnRlZCI6WyJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQiXSwiaWRfdG9rZW5fc2lnbmVkX3Jl + c3BvbnNlX2FsZyI6IlJTMjU2In19.a5UzXIoRZzNQFAWFblAhkYocrR05hB-GIO7nRdqRnFrqxjvBVP6HfFhPyASRmhSgE0vUe0TPN0-TbQk + Yh0-LeA + """.trimIndent() + + val jwkset = """ + { + "keys": [ + { + "alg": "ECDH-ES", + "crv": "P-256", + "kid": "1835c633-bd3f-429e-8dfa-64596b83aa0c", + "kty": "EC", + "use": "enc", + "x": "lBeONku60ShqCvndUdFVubOCCuvMjWTmElaxgHWbuMo", + "y": "NHYLE--QpTqc9vGrTLoq1dm2c86AC6af6xiHiLpKjdk" + } + ] + } + + """.trimIndent() + + holderSiop = OidcSiopWallet.newInstance( + holder = holderAgent, + cryptoService = holderCryptoService, + jwkSetRetriever = { it -> + if (it == "https://verifier-backend.eudiw.dev/wallet/jarm/" + + "WLFJEn9AGbJfAcEyaQTzzxueqmeRazmsHIkxMRTkGRL1zyI7un-KJWaXtulrfiSS38LlU5ABDB9Zdsfq_11r8Q/jwks.json" + ) + JsonWebKeySet.deserialize(jwkset) else null + }, + requestRetriever = { it -> + if (it == "https://verifier-backend.eudiw.dev/wallet/request.jwt/" + + "WLFJEn9AGbJfAcEyaQTzzxueqmeRazmsHIkxMRTkGRL1zyI7un-KJWaXtulrfiSS38LlU5ABDB9Zdsfq_11r8Q" + ) + requestObject else null + } + ) + + val response = holderSiop.createAuthnResponse(url).getOrThrow() + + response.shouldBeInstanceOf() + val params = response.content.decodeFromPostBody() + params.presentationSubmission.shouldNotBeNull() + params.vpToken.shouldNotBeNull() + params.idToken.shouldNotBeNull() + } + + "EUDI AuthnRequest can be parsed" { + val input = """ + { + "response_uri": "https://verifier-backend.eudiw.dev/wallet/direct_post", + "client_id_scheme": "x509_san_dns", + "response_type": "vp_token", + "nonce": "nonce", + "client_id": "verifier-backend.eudiw.dev", + "response_mode": "direct_post.jwt", + "aud": "https://self-issued.me/v2", + "scope": "", + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "input_descriptors": [ + { + "id": "eudi_pid", + "name": "EUDI PID", + "purpose": "We need to verify your identity", + "constraints": { + "fields": [ + { + "path": [ + "${'$'}.mdoc.doctype" + ], + "filter": { + "type": "string", + "const": "eu.europa.ec.eudiw.pid.1" + } + }, + { + "path": [ + "${'$'}.mdoc.namespace" + ], + "filter": { + "type": "string", + "const": "eu.europa.ec.eudiw.pid.1" + } + }, + { + "path": [ + "${'$'}.mdoc.given_name" + ], + "intent_to_retain": false + } + ] + } + } + ] + }, + "state": "xgagB1vsIrWhMLixoJTCVZZvOHsZ8QrulEFxc0bjJdMRyzqO6j2-UB00gmOZraocfoknlxXY-kaoLlX8kygqxw", + "iat": 1710313534, + "client_metadata": { + "authorization_encrypted_response_alg": "ECDH-ES", + "authorization_encrypted_response_enc": "A128CBC-HS256", + "id_token_encrypted_response_alg": "RSA-OAEP-256", + "id_token_encrypted_response_enc": "A128CBC-HS256", + "jwks_uri": "https://verifier-backend.eudiw.dev/wallet/jarm/xgagB1vsIrWhMLixoJTCVZZvOHsZ8QrulEFxc0bjJdMRyzqO6j2-UB00gmOZraocfoknlxXY-kaoLlX8kygqxw/jwks.json", + "subject_syntax_types_supported": [ + "urn:ietf:params:oauth:jwk-thumbprint" + ], + "id_token_signed_response_alg": "RS256" + } + } + """.trimIndent() + + val parsed = jsonSerializer.decodeFromString(input) + parsed.shouldNotBeNull() + + parsed.responseUrl shouldBe "https://verifier-backend.eudiw.dev/wallet/direct_post" + parsed.clientIdScheme shouldBe "x509_san_dns" + parsed.responseType shouldBe "vp_token" + parsed.nonce shouldBe "nonce" + parsed.clientId shouldBe "verifier-backend.eudiw.dev" + parsed.responseMode shouldBe "direct_post.jwt" + parsed.audience shouldBe "https://self-issued.me/v2" + parsed.scope shouldBe "" + val pd = parsed.presentationDefinition + pd.shouldNotBeNull() + pd.id shouldBe "32f54163-7166-48f1-93d8-ff217bdb0653" + val id = pd.inputDescriptors.firstOrNull() + id.shouldNotBeNull() + id.id shouldBe "eudi_pid" + id.name shouldBe "EUDI PID" + id.purpose shouldBe "We need to verify your identity" + val fields = id.constraints?.fields + fields.shouldNotBeNull() + fields.filter { it.path.contains("$.mdoc.doctype") }.shouldBeSingleton() + fields.filter { it.path.contains("$.mdoc.namespace") }.shouldBeSingleton() + fields.filter { it.path.contains("$.mdoc.given_name") }.shouldBeSingleton() + parsed.state shouldBe "xgagB1vsIrWhMLixoJTCVZZvOHsZ8QrulEFxc0bjJdMRyzqO6j2-UB00gmOZraocfoknlxXY-kaoLlX8kygqxw" + parsed.issuedAt shouldBe Instant.fromEpochSeconds(1710313534) + val cm = parsed.clientMetadata + cm.shouldNotBeNull() + cm.subjectSyntaxTypesSupported shouldHaveSingleElement "urn:ietf:params:oauth:jwk-thumbprint" + cm.authorizationEncryptedResponseAlg shouldBe JweAlgorithm.ECDH_ES + cm.authorizationEncryptedResponseEncoding shouldBe "A128CBC-HS256" + cm.idTokenEncryptedResponseAlg shouldBe JweAlgorithm.RSA_OAEP_256 + cm.idTokenEncryptedResponseEncoding shouldBe "A128CBC-HS256" + cm.idTokenSignedResponseAlg shouldBe JwsAlgorithm.RS256 + cm.jsonWebKeySetUrl shouldBe "https://verifier-backend.eudiw.dev/wallet/jarm/" + + "xgagB1vsIrWhMLixoJTCVZZvOHsZ8QrulEFxc0bjJdMRyzqO6j2-UB00gmOZraocfoknlxXY-kaoLlX8kygqxw/jwks.json" + } + +}) + + diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt index 3309a00e8..b721fcfff 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt @@ -72,9 +72,14 @@ class OidcSiopIsoProtocolTest : FreeSpec({ verifier = verifierAgent, cryptoService = verifierCryptoService, relyingPartyUrl = relyingPartyUrl, - credentialScheme = ConstantIndex.MobileDrivingLicence2023, ) - val document = runProcess(verifierSiop, walletUrl, ConstantIndex.CredentialRepresentation.ISO_MDOC, holderSiop) + val document = runProcess( + verifierSiop, + walletUrl, + ConstantIndex.CredentialRepresentation.ISO_MDOC, + ConstantIndex.MobileDrivingLicence2023, + holderSiop + ) document.validItems.shouldNotBeEmpty() document.invalidItems.shouldBeEmpty() @@ -85,9 +90,14 @@ class OidcSiopIsoProtocolTest : FreeSpec({ verifier = verifierAgent, cryptoService = verifierCryptoService, relyingPartyUrl = relyingPartyUrl, - credentialScheme = ConstantIndex.AtomicAttribute2023, ) - val document = runProcess(verifierSiop, walletUrl, ConstantIndex.CredentialRepresentation.ISO_MDOC, holderSiop) + val document = runProcess( + verifierSiop, + walletUrl, + ConstantIndex.CredentialRepresentation.ISO_MDOC, + ConstantIndex.AtomicAttribute2023, + holderSiop + ) document.validItems.shouldNotBeEmpty() document.invalidItems.shouldBeEmpty() @@ -99,12 +109,12 @@ class OidcSiopIsoProtocolTest : FreeSpec({ verifier = verifierAgent, cryptoService = verifierCryptoService, relyingPartyUrl = relyingPartyUrl, - credentialScheme = ConstantIndex.MobileDrivingLicence2023, ) val document = runProcess( verifierSiop, walletUrl, ConstantIndex.CredentialRepresentation.ISO_MDOC, + ConstantIndex.MobileDrivingLicence2023, holderSiop, listOf(requestedClaim) ) @@ -121,13 +131,15 @@ private suspend fun runProcess( verifierSiop: OidcSiopVerifier, walletUrl: String, credentialRepresentation: ConstantIndex.CredentialRepresentation, + credentialScheme: ConstantIndex.CredentialScheme, holderSiop: OidcSiopWallet, requestedAttributes: List? = null, ): IsoDocumentParsed { val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, representation = credentialRepresentation, - requestedAttributes = requestedAttributes + credentialScheme = credentialScheme, + requestedAttributes = requestedAttributes, ).also { println(it) } val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt index 8af43ad71..ac4150cde 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt @@ -171,10 +171,12 @@ class OidcSiopProtocolTest : FreeSpec({ verifier = verifierAgent, cryptoService = verifierCryptoService, relyingPartyUrl = relyingPartyUrl, - credentialScheme = ConstantIndex.AtomicAttribute2023, ) - val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl = walletUrl).also { println(it) } + val authnRequest = verifierSiop.createAuthnRequestUrl( + walletUrl = walletUrl, + credentialScheme = ConstantIndex.AtomicAttribute2023 + ).also { println(it) } val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow() authnResponse.shouldBeInstanceOf().also { println(it) } diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt index 2a4af1bdd..844253121 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt @@ -85,11 +85,11 @@ class OidcSiopSdJwtProtocolTest : FreeSpec({ verifier = verifierAgent, cryptoService = verifierCryptoService, relyingPartyUrl = relyingPartyUrl, - credentialScheme = ConstantIndex.AtomicAttribute2023, ) val authnRequest = verifierSiop.createAuthnRequestUrl( walletUrl = walletUrl, representation = ConstantIndex.CredentialRepresentation.SD_JWT, + credentialScheme = ConstantIndex.AtomicAttribute2023, requestedAttributes = listOf(requestedClaim), ).also { println(it) } authnRequest shouldContain "jwt_sd" diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt index 734f43670..820c8888a 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt @@ -283,7 +283,7 @@ class HolderAgent( challenge: String, audienceId: String, ): Holder.CreatePresentationResult? { - val vp = VerifiablePresentation(validCredentials.toTypedArray()) + val vp = VerifiablePresentation(validCredentials) val vpSerialized = vp.toJws(challenge, identifier, audienceId).serialize() val jwsPayload = vpSerialized.encodeToByteArray() val jws = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload).getOrElse { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt index 41911abc8..75c09dac3 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt @@ -277,7 +277,7 @@ class IssuerAgent( expiration = expirationDate, jwtId = vcId, disclosureDigests = disclosureDigests, - type = arrayOf(VcDataModelConstants.VERIFIABLE_CREDENTIAL, scheme.vcType), + type = listOf(VcDataModelConstants.VERIFIABLE_CREDENTIAL, scheme.vcType), selectiveDisclosureAlgorithm = "sha-256", confirmationKey = subjectPublicKey.toJsonWebKey(), credentialStatus = credentialStatus, diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt index ceb7b1391..dfc5fef17 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt @@ -183,9 +183,9 @@ class Validator( val vp = VerifiablePresentationParsed( id = parsedVp.jws.vp.id, type = parsedVp.jws.vp.type, - verifiableCredentials = validVcList.toTypedArray(), - revokedVerifiableCredentials = revokedVcList.toTypedArray(), - invalidVerifiableCredentials = invalidVcList.toTypedArray(), + verifiableCredentials = validVcList, + revokedVerifiableCredentials = revokedVcList, + invalidVerifiableCredentials = invalidVcList, ) Napier.d("VP: Valid") return Verifier.VerifyPresentationResult.Success(vp) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/AttributeIndex.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/AttributeIndex.kt index e50503063..b33e174fd 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/AttributeIndex.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/AttributeIndex.kt @@ -27,4 +27,12 @@ object AttributeIndex { return schemeSet.firstOrNull { it.vcType == type } } + /** + * Matches the passed [namespace] against all known namespace from [ConstantIndex.CredentialScheme.isoNamespace] + */ + fun resolveIsoNamespace(namespace: String): ConstantIndex.CredentialScheme? { + // allow for extension to the namespace by appending ".countryname" or anything else, according to spec + return schemeSet.firstOrNull { it.isoNamespace.startsWith(namespace) || namespace.startsWith(it.isoNamespace) } + } + } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiableCredential.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiableCredential.kt index f44840dfd..d7436824d 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiableCredential.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiableCredential.kt @@ -17,7 +17,7 @@ data class VerifiableCredential( @SerialName("id") val id: String, @SerialName("type") - val type: Array, + val type: Collection, @SerialName("issuer") val issuer: String, @Serializable(with = InstantStringSerializer::class) @@ -43,7 +43,7 @@ data class VerifiableCredential( expirationDate: Instant? = Clock.System.now() + lifetime, ) : this( id = id, - type = arrayOf(VERIFIABLE_CREDENTIAL, credentialType), + type = listOf(VERIFIABLE_CREDENTIAL, credentialType), issuer = issuer, issuanceDate = issuanceDate, expirationDate = expirationDate, @@ -61,7 +61,7 @@ data class VerifiableCredential( credentialType: String, ) : this( id = id, - type = arrayOf(VERIFIABLE_CREDENTIAL, credentialType), + type = listOf(VERIFIABLE_CREDENTIAL, credentialType), issuer = issuer, issuanceDate = issuanceDate, expirationDate = expirationDate, @@ -77,37 +77,11 @@ data class VerifiableCredential( credentialSubject: RevocationListSubject, ) : this( id = id, - type = arrayOf(VERIFIABLE_CREDENTIAL, REVOCATION_LIST_2020), + type = listOf(VERIFIABLE_CREDENTIAL, REVOCATION_LIST_2020), issuer = issuer, issuanceDate = issuanceDate, expirationDate = issuanceDate + lifetime, credentialStatus = null, credentialSubject = credentialSubject, ) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as VerifiableCredential - - if (id != other.id) return false - if (!type.contentEquals(other.type)) return false - if (issuer != other.issuer) return false - if (issuanceDate != other.issuanceDate) return false - if (expirationDate != other.expirationDate) return false - if (credentialStatus != other.credentialStatus) return false - return credentialSubject == other.credentialSubject - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + type.contentHashCode() - result = 31 * result + issuer.hashCode() - result = 31 * result + issuanceDate.hashCode() - result = 31 * result + (expirationDate?.hashCode() ?: 0) - result = 31 * result + (credentialStatus?.hashCode() ?: 0) - result = 31 * result + credentialSubject.hashCode() - return result - } -} +} \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiableCredentialSdJwt.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiableCredentialSdJwt.kt index 5720a3c85..d53458ed8 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiableCredentialSdJwt.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiableCredentialSdJwt.kt @@ -27,7 +27,7 @@ data class VerifiableCredentialSdJwt( @SerialName("_sd") val disclosureDigests: List, @SerialName("type") - val type: Array, + val type: Collection, @SerialName("credentialStatus") val credentialStatus: CredentialStatus? = null, @SerialName("_sd_alg") @@ -38,40 +38,6 @@ data class VerifiableCredentialSdJwt( fun serialize() = jsonSerializer.encodeToString(this) - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as VerifiableCredentialSdJwt - - if (subject != other.subject) return false - if (notBefore != other.notBefore) return false - if (issuer != other.issuer) return false - if (expiration != other.expiration) return false - if (jwtId != other.jwtId) return false - if (disclosureDigests != other.disclosureDigests) return false - if (!type.contentEquals(other.type)) return false - if (credentialStatus != other.credentialStatus) return false - if (selectiveDisclosureAlgorithm != other.selectiveDisclosureAlgorithm) return false - if (confirmationKey != other.confirmationKey) return false - - return true - } - - override fun hashCode(): Int { - var result = subject.hashCode() - result = 31 * result + notBefore.hashCode() - result = 31 * result + issuer.hashCode() - result = 31 * result + (expiration?.hashCode() ?: 0) - result = 31 * result + jwtId.hashCode() - result = 31 * result + disclosureDigests.hashCode() - result = 31 * result + type.contentHashCode() - result = 31 * result + (credentialStatus?.hashCode() ?: 0) - result = 31 * result + selectiveDisclosureAlgorithm.hashCode() - result = 31 * result + (confirmationKey?.hashCode() ?: 0) - return result - } - companion object { fun deserialize(it: String) = kotlin.runCatching { jsonSerializer.decodeFromString(it) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentation.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentation.kt index 95de1cb88..6e9d61a52 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentation.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentation.kt @@ -14,10 +14,10 @@ data class VerifiablePresentation( @SerialName("type") val type: String, @SerialName("verifiableCredential") - val verifiableCredential: Array, + val verifiableCredential: Collection, ) { - constructor(verifiableCredential: Array) : this( + constructor(verifiableCredential: Collection) : this( id = "urn:uuid:${uuid4()}", type = "VerifiablePresentation", verifiableCredential = verifiableCredential @@ -30,25 +30,4 @@ data class VerifiablePresentation( audience = audienceId, jwtId = id ) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as VerifiablePresentation - - if (id != other.id) return false - if (type != other.type) return false - if (!verifiableCredential.contentEquals(other.verifiableCredential)) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + type.hashCode() - result = 31 * result + verifiableCredential.contentHashCode() - return result - } - } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentationParsed.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentationParsed.kt index e059e04a0..56e33de52 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentationParsed.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentationParsed.kt @@ -7,31 +7,7 @@ package at.asitplus.wallet.lib.data data class VerifiablePresentationParsed( val id: String, val type: String, - val verifiableCredentials: Array = arrayOf(), - val revokedVerifiableCredentials: Array = arrayOf(), - val invalidVerifiableCredentials: Array = arrayOf(), -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as VerifiablePresentationParsed - - if (id != other.id) return false - if (type != other.type) return false - if (!verifiableCredentials.contentEquals(other.verifiableCredentials)) return false - if (!revokedVerifiableCredentials.contentEquals(other.revokedVerifiableCredentials)) return false - if (!invalidVerifiableCredentials.contentEquals(other.invalidVerifiableCredentials)) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + type.hashCode() - result = 31 * result + verifiableCredentials.contentHashCode() - result = 31 * result + revokedVerifiableCredentials.contentHashCode() - result = 31 * result + invalidVerifiableCredentials.contentHashCode() - return result - } -} \ No newline at end of file + val verifiableCredentials: Collection = listOf(), + val revokedVerifiableCredentials: Collection = listOf(), + val invalidVerifiableCredentials: Collection = listOf(), +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentationSdJwtParsed.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentationSdJwtParsed.kt index 2963443da..7ceadf6ca 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentationSdJwtParsed.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentationSdJwtParsed.kt @@ -7,31 +7,7 @@ package at.asitplus.wallet.lib.data data class VerifiablePresentationSdJwtParsed( val id: String, val type: String, - val verifiableCredentials: Array = arrayOf(), - val revokedVerifiableCredentials: Array = arrayOf(), - val invalidVerifiableCredentials: Array = arrayOf(), -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as VerifiablePresentationSdJwtParsed - - if (id != other.id) return false - if (type != other.type) return false - if (!verifiableCredentials.contentEquals(other.verifiableCredentials)) return false - if (!revokedVerifiableCredentials.contentEquals(other.revokedVerifiableCredentials)) return false - if (!invalidVerifiableCredentials.contentEquals(other.invalidVerifiableCredentials)) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + type.hashCode() - result = 31 * result + verifiableCredentials.contentHashCode() - result = 31 * result + revokedVerifiableCredentials.contentHashCode() - result = 31 * result + invalidVerifiableCredentials.contentHashCode() - return result - } -} \ No newline at end of file + val verifiableCredentials: Collection = listOf(), + val revokedVerifiableCredentials: Collection = listOf(), + val invalidVerifiableCredentials: Collection = listOf(), +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/Constraint.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/Constraint.kt index a2d19bb49..3213763fc 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/Constraint.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/Constraint.kt @@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable @Serializable data class Constraint( @SerialName("fields") - val fields: Array? = null, + val fields: Collection? = null, @SerialName("limit_disclosure") val limitDisclosure: RequirementEnum? = null, @SerialName("statuses") @@ -18,42 +18,7 @@ data class Constraint( @SerialName("subject_is_issuer") val subjectIsIssuer: RequirementEnum? = null, @SerialName("is_holder") - val isHolder: Array? = null, + val isHolder: Collection? = null, @SerialName("same_subject") - val sameSubject: Array? = null, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as Constraint - - if (fields != null) { - if (other.fields == null) return false - if (!fields.contentEquals(other.fields)) return false - } else if (other.fields != null) return false - if (limitDisclosure != other.limitDisclosure) return false - if (statuses != other.statuses) return false - if (subjectIsIssuer != other.subjectIsIssuer) return false - if (isHolder != null) { - if (other.isHolder == null) return false - if (!isHolder.contentEquals(other.isHolder)) return false - } else if (other.isHolder != null) return false - if (sameSubject != null) { - if (other.sameSubject == null) return false - if (!sameSubject.contentEquals(other.sameSubject)) return false - } else if (other.sameSubject != null) return false - - return true - } - - override fun hashCode(): Int { - var result = fields?.contentHashCode() ?: 0 - result = 31 * result + (limitDisclosure?.hashCode() ?: 0) - result = 31 * result + (statuses?.hashCode() ?: 0) - result = 31 * result + (subjectIsIssuer?.hashCode() ?: 0) - result = 31 * result + (isHolder?.contentHashCode() ?: 0) - result = 31 * result + (sameSubject?.contentHashCode() ?: 0) - return result - } -} \ No newline at end of file + val sameSubject: Collection? = null, +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintField.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintField.kt index 766add87c..d05b928ea 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintField.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintField.kt @@ -17,33 +17,9 @@ data class ConstraintField( val predicate: RequirementEnum? = null, @SerialName("path") // should be JSONPath - val path: Array, + val path: List, @SerialName("filter") val filter: ConstraintFilter? = null, @SerialName("intent_to_retain") val intentToRetain: Boolean? = null, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as ConstraintField - - if (id != other.id) return false - if (purpose != other.purpose) return false - if (predicate != other.predicate) return false - if (!path.contentEquals(other.path)) return false - if (filter != other.filter) return false - return intentToRetain == other.intentToRetain - } - - override fun hashCode(): Int { - var result = id?.hashCode() ?: 0 - result = 31 * result + (purpose?.hashCode() ?: 0) - result = 31 * result + (predicate?.hashCode() ?: 0) - result = 31 * result + path.contentHashCode() - result = 31 * result + (filter?.hashCode() ?: 0) - result = 31 * result + (intentToRetain?.hashCode() ?: 0) - return result - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintFilter.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintFilter.kt index e29df72a7..e6efdf6cb 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintFilter.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintFilter.kt @@ -30,49 +30,7 @@ data class ConstraintFilter( @SerialName("maxLength") val maxLength: Int? = null, @SerialName("enum") - val enum: Array? = null, + val enum: Collection? = null, @SerialName("not") val not: ConstraintNotFilter? = null, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as ConstraintFilter - - if (type != other.type) return false - if (format != other.format) return false - if (const != other.const) return false - if (pattern != other.pattern) return false - if (exclusiveMinimum != other.exclusiveMinimum) return false - if (exclusiveMaximum != other.exclusiveMaximum) return false - if (minimum != other.minimum) return false - if (maximum != other.maximum) return false - if (minLength != other.minLength) return false - if (maxLength != other.maxLength) return false - if (enum != null) { - if (other.enum == null) return false - if (!enum.contentEquals(other.enum)) return false - } else if (other.enum != null) return false - if (not != other.not) return false - - return true - } - - override fun hashCode(): Int { - var result = type.hashCode() - result = 31 * result + (format?.hashCode() ?: 0) - result = 31 * result + (const?.hashCode() ?: 0) - result = 31 * result + (pattern?.hashCode() ?: 0) - result = 31 * result + (exclusiveMinimum ?: 0) - result = 31 * result + (exclusiveMaximum ?: 0) - result = 31 * result + (minimum ?: 0) - result = 31 * result + (maximum ?: 0) - result = 31 * result + (minLength ?: 0) - result = 31 * result + (maxLength ?: 0) - result = 31 * result + (enum?.contentHashCode() ?: 0) - result = 31 * result + (not?.hashCode() ?: 0) - return result - } -} - +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintHolder.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintHolder.kt index f5439c669..a3f6f6149 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintHolder.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintHolder.kt @@ -10,25 +10,7 @@ import kotlinx.serialization.Serializable @Serializable data class ConstraintHolder( @SerialName("field_id") - val fieldIds: Array, + val fieldIds: Collection, @SerialName("directive") val directive: RequirementEnum, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as ConstraintHolder - - if (!fieldIds.contentEquals(other.fieldIds)) return false - if (directive != other.directive) return false - - return true - } - - override fun hashCode(): Int { - var result = fieldIds.contentHashCode() - result = 31 * result + directive.hashCode() - return result - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintNotFilter.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintNotFilter.kt index d26641509..0b662bab9 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintNotFilter.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintNotFilter.kt @@ -12,26 +12,5 @@ data class ConstraintNotFilter( @SerialName("const") val const: String? = null, @SerialName("enum") - val enum: Array? = null, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as ConstraintNotFilter - - if (const != other.const) return false - if (enum != null) { - if (other.enum == null) return false - if (!enum.contentEquals(other.enum)) return false - } else if (other.enum != null) return false - - return true - } - - override fun hashCode(): Int { - var result = const?.hashCode() ?: 0 - result = 31 * result + (enum?.contentHashCode() ?: 0) - return result - } -} \ No newline at end of file + val enum: Collection? = null, +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerJwt.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerJwt.kt index aaaa8c22d..158730b87 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerJwt.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerJwt.kt @@ -10,20 +10,5 @@ import kotlinx.serialization.Serializable @Serializable data class FormatContainerJwt( @SerialName("alg") - val algorithms: Array, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as FormatContainerJwt - - if (!algorithms.contentEquals(other.algorithms)) return false - - return true - } - - override fun hashCode(): Int { - return algorithms.contentHashCode() - } -} \ No newline at end of file + val algorithms: Collection, +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerLdp.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerLdp.kt index 490c8cd01..b50acf36b 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerLdp.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerLdp.kt @@ -10,20 +10,5 @@ import kotlinx.serialization.Serializable @Serializable data class FormatContainerLdp( @SerialName("proof_type") - val proofType: Array, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as FormatContainerLdp - - if (!proofType.contentEquals(other.proofType)) return false - - return true - } - - override fun hashCode(): Int { - return proofType.contentHashCode() - } -} \ No newline at end of file + val proofType: Collection, +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/InputDescriptor.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/InputDescriptor.kt index cb352151c..8ae6f96b2 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/InputDescriptor.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/InputDescriptor.kt @@ -21,42 +21,14 @@ data class InputDescriptor( @SerialName("format") val format: FormatHolder? = null, @SerialName("schema") - val schema: Array, + val schema: Collection? = null, @SerialName("constraints") val constraints: Constraint? = null, ) { constructor(name: String, schema: SchemaReference, constraints: Constraint? = null) : this( id = uuid4().toString(), name = name, - schema = arrayOf(schema), + schema = listOf(schema), constraints = constraints, ) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as InputDescriptor - - if (id != other.id) return false - if (group != other.group) return false - if (name != other.name) return false - if (purpose != other.purpose) return false - if (format != other.format) return false - if (!schema.contentEquals(other.schema)) return false - if (constraints != other.constraints) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + (group?.hashCode() ?: 0) - result = 31 * result + (name?.hashCode() ?: 0) - result = 31 * result + (purpose?.hashCode() ?: 0) - result = 31 * result + (format?.hashCode() ?: 0) - result = 31 * result + schema.contentHashCode() - result = 31 * result + (constraints?.hashCode() ?: 0) - return result - } } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationDefinition.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationDefinition.kt index 17abfb3cf..28e311246 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationDefinition.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationDefinition.kt @@ -20,48 +20,19 @@ data class PresentationDefinition( @SerialName("purpose") val purpose: String? = null, @SerialName("input_descriptors") - val inputDescriptors: Array, + val inputDescriptors: Collection, @SerialName("format") val formats: FormatHolder? = null, @SerialName("submission_requirements") - val submissionRequirements: Array? = null, + val submissionRequirements: Collection? = null, ) { constructor( - inputDescriptors: Array, + inputDescriptors: Collection, formats: FormatHolder ) : this(id = uuid4().toString(), inputDescriptors = inputDescriptors, formats = formats) fun serialize() = jsonSerializer.encodeToString(this) - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as PresentationDefinition - - if (id != other.id) return false - if (name != other.name) return false - if (purpose != other.purpose) return false - if (!inputDescriptors.contentEquals(other.inputDescriptors)) return false - if (formats != other.formats) return false - if (submissionRequirements != null) { - if (other.submissionRequirements == null) return false - if (!submissionRequirements.contentEquals(other.submissionRequirements)) return false - } else if (other.submissionRequirements != null) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + (name?.hashCode() ?: 0) - result = 31 * result + (purpose?.hashCode() ?: 0) - result = 31 * result + inputDescriptors.contentHashCode() - result = 31 * result + (formats?.hashCode() ?: 0) - result = 31 * result + (submissionRequirements?.contentHashCode() ?: 0) - return result - } - companion object { fun deserialize(it: String) = kotlin.runCatching { jsonSerializer.decodeFromString(it) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmission.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmission.kt index 2ddfde4bf..fbc354a33 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmission.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmission.kt @@ -14,28 +14,5 @@ data class PresentationSubmission( @SerialName("definition_id") val definitionId: String, @SerialName("descriptor_map") - val descriptorMap: Array? = null, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as PresentationSubmission - - if (id != other.id) return false - if (definitionId != other.definitionId) return false - if (descriptorMap != null) { - if (other.descriptorMap == null) return false - if (!descriptorMap.contentEquals(other.descriptorMap)) return false - } else if (other.descriptorMap != null) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + definitionId.hashCode() - result = 31 * result + (descriptorMap?.contentHashCode() ?: 0) - return result - } -} \ No newline at end of file + val descriptorMap: Collection? = null, +) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirement.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirement.kt index 01d58b8d4..bff934538 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirement.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirement.kt @@ -24,38 +24,5 @@ data class SubmissionRequirement( @SerialName("from") val from: String? = null, @SerialName("from_nested") - val fromNested: Array? = null, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as SubmissionRequirement - - if (name != other.name) return false - if (purpose != other.purpose) return false - if (rule != other.rule) return false - if (count != other.count) return false - if (min != other.min) return false - if (max != other.max) return false - if (from != other.from) return false - if (fromNested != null) { - if (other.fromNested == null) return false - if (!fromNested.contentEquals(other.fromNested)) return false - } else if (other.fromNested != null) return false - - return true - } - - override fun hashCode(): Int { - var result = name?.hashCode() ?: 0 - result = 31 * result + (purpose?.hashCode() ?: 0) - result = 31 * result + (rule?.hashCode() ?: 0) - result = 31 * result + (count ?: 0) - result = 31 * result + (min ?: 0) - result = 31 * result + (max ?: 0) - result = 31 * result + (from?.hashCode() ?: 0) - result = 31 * result + (fromNested?.contentHashCode() ?: 0) - return result - } -} \ No newline at end of file + val fromNested: Collection? = null, +) \ No newline at end of file diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt index d9768ad12..b5d2235f0 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt @@ -201,26 +201,6 @@ class ValidatorVcTest : FreeSpec() { } } - "Invalid type in credential is not valid" - { - withData( - nameFn = ::credentialNameFn, - dataProvider.getCredential( - verifierCryptoService.publicKey, - ConstantIndex.AtomicAttribute2023, - ConstantIndex.CredentialRepresentation.PLAIN_JWT - ).getOrThrow() - ) { - issueCredential(it) - .also { it.type[0] = "fakeCredential" } - .let { wrapVcInJws(it) } - .let { signJws(it) } - ?.let { - verifier.verifyVcJws(it) - .shouldBeInstanceOf() - } - } - } - "Invalid expiration in credential is not valid" - { withData( nameFn = ::credentialNameFn, @@ -373,7 +353,8 @@ class ValidatorVcTest : FreeSpec() { private fun issueCredential( credential: CredentialToBeIssued, issuanceDate: Instant = Clock.System.now(), - expirationDate: Instant? = Clock.System.now() + 60.seconds + expirationDate: Instant? = Clock.System.now() + 60.seconds, + type: String = ConstantIndex.AtomicAttribute2023.vcType, ): VerifiableCredential { credential.shouldBeInstanceOf() val sub = credential.subject @@ -393,7 +374,7 @@ class ValidatorVcTest : FreeSpec() { issuer = issuer.identifier, credentialStatus = credentialStatus, credentialSubject = sub, - credentialType = ConstantIndex.AtomicAttribute2023.vcType, + credentialType = type, issuanceDate = issuanceDate, expirationDate = expirationDate, ) diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt index 1d2dae765..bdfd05b27 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt @@ -129,7 +129,7 @@ class ValidatorVpTest : FreeSpec({ .map { it.vcSerialized } (validCredentials.isEmpty()) shouldBe false - val vp = VerifiablePresentation(validCredentials.toTypedArray()) + val vp = VerifiablePresentation(validCredentials) val vpSerialized = vp.toJws( challenge = challenge, issuerId = holder.identifier, @@ -148,7 +148,7 @@ class ValidatorVpTest : FreeSpec({ .filterIsInstance() .map { it.vcSerialized } - val vp = VerifiablePresentation(credentials.toTypedArray()) + val vp = VerifiablePresentation(credentials) val vpSerialized = VerifiablePresentationJws( vp = vp, challenge = challenge, @@ -168,7 +168,7 @@ class ValidatorVpTest : FreeSpec({ val credentials = holderCredentialStore.getCredentials().getOrThrow() .filterIsInstance() .map { it.vcSerialized } - val vp = VerifiablePresentation(credentials.toTypedArray()) + val vp = VerifiablePresentation(credentials) val vpSerialized = VerifiablePresentationJws( vp = vp, challenge = challenge, @@ -191,7 +191,7 @@ class ValidatorVpTest : FreeSpec({ val vp = VerifiablePresentation( id = "urn:uuid:${uuid4()}", type = "wrong_type", - verifiableCredential = credentials.toTypedArray() + verifiableCredential = credentials ) val vpSerialized = vp.toJws( challenge = challenge,