From 50fb66a5ebe46e30be7347f4e550c1ddcd8af484 Mon Sep 17 00:00:00 2001 From: Simon Mueller Date: Wed, 25 Sep 2024 11:29:32 +0200 Subject: [PATCH] Change credentialID to bytearray --- .../openid/AuthenticationRequestParameters.kt | 103 +++++++++++++++++- .../wallet/lib/oauth2/OAuth2Client.kt | 5 + .../wallet/lib/oidvci/RqesWalletService.kt | 10 +- 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationRequestParameters.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationRequestParameters.kt index d34f009f..359d0b5b 100644 --- a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationRequestParameters.kt +++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationRequestParameters.kt @@ -4,6 +4,8 @@ import at.asitplus.KmmResult.Companion.wrap import at.asitplus.dif.PresentationDefinition import at.asitplus.dif.rqes.Enums.SignatureQualifierEnum import at.asitplus.dif.rqes.Serializer.HashesSerializer +import at.asitplus.signum.indispensable.asn1.ObjectIdentifier +import at.asitplus.signum.indispensable.io.ByteArrayBase64UrlSerializer import at.asitplus.signum.indispensable.josef.JsonWebToken import at.asitplus.signum.indispensable.josef.io.InstantLongSerializer import kotlinx.datetime.Instant @@ -271,10 +273,14 @@ data class AuthenticationRequestParameters( /** * CSC: REQUIRED-"credential" - * The identifier associated to the credential to authorize + * The identifier associated to the credential to authorize. + * This parameter value may contain characters that are reserved, unsafe or + * forbidden in URLs and therefore SHALL be url-encoded by the signature + * application */ @SerialName("credentialID") - val credentialID: String? = null, + @Serializable(ByteArrayBase64UrlSerializer::class) + val credentialID: ByteArray? = null, /** * CSC: Required-"credential" @@ -304,7 +310,7 @@ data class AuthenticationRequestParameters( * String containing the OID of the hash algorithm used to generate the hashes */ @SerialName("hashAlgorithmOID") - val hashAlgorithmOid: String? = null, + val hashAlgorithmOid: ObjectIdentifier? = null, /** * CSC: OPTIONAL @@ -335,6 +341,97 @@ data class AuthenticationRequestParameters( val clientData: String? = null, ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as AuthenticationRequestParameters + + if (responseType != other.responseType) return false + if (clientId != other.clientId) return false + if (redirectUrl != other.redirectUrl) return false + if (scope != other.scope) return false + if (state != other.state) return false + if (nonce != other.nonce) return false + if (claims != other.claims) return false + if (clientMetadata != other.clientMetadata) return false + if (clientMetadataUri != other.clientMetadataUri) return false + if (idTokenHint != other.idTokenHint) return false + if (request != other.request) return false + if (requestUri != other.requestUri) return false + if (idTokenType != other.idTokenType) return false + if (presentationDefinition != other.presentationDefinition) return false + if (presentationDefinitionUrl != other.presentationDefinitionUrl) return false + if (authorizationDetails != other.authorizationDetails) return false + if (clientIdScheme != other.clientIdScheme) return false + if (walletIssuer != other.walletIssuer) return false + if (userHint != other.userHint) return false + if (issuerState != other.issuerState) return false + if (responseMode != other.responseMode) return false + if (responseUrl != other.responseUrl) return false + if (audience != other.audience) return false + if (issuer != other.issuer) return false + if (issuedAt != other.issuedAt) return false + if (resource != other.resource) return false + if (codeChallenge != other.codeChallenge) return false + if (codeChallengeMethod != other.codeChallengeMethod) return false + if (lang != other.lang) return false + if (credentialID != null) { + if (other.credentialID == null) return false + if (!credentialID.contentEquals(other.credentialID)) return false + } else if (other.credentialID != null) return false + if (signatureQualifier != other.signatureQualifier) return false + if (numSignatures != other.numSignatures) return false + if (hashes != other.hashes) return false + if (hashAlgorithmOid != other.hashAlgorithmOid) return false + if (description != other.description) return false + if (accountToken != other.accountToken) return false + if (clientData != other.clientData) return false + + return true + } + + override fun hashCode(): Int { + var result = responseType?.hashCode() ?: 0 + result = 31 * result + (clientId?.hashCode() ?: 0) + result = 31 * result + (redirectUrl?.hashCode() ?: 0) + result = 31 * result + (scope?.hashCode() ?: 0) + result = 31 * result + (state?.hashCode() ?: 0) + result = 31 * result + (nonce?.hashCode() ?: 0) + result = 31 * result + (claims?.hashCode() ?: 0) + result = 31 * result + (clientMetadata?.hashCode() ?: 0) + result = 31 * result + (clientMetadataUri?.hashCode() ?: 0) + result = 31 * result + (idTokenHint?.hashCode() ?: 0) + result = 31 * result + (request?.hashCode() ?: 0) + result = 31 * result + (requestUri?.hashCode() ?: 0) + result = 31 * result + (idTokenType?.hashCode() ?: 0) + result = 31 * result + (presentationDefinition?.hashCode() ?: 0) + result = 31 * result + (presentationDefinitionUrl?.hashCode() ?: 0) + result = 31 * result + (authorizationDetails?.hashCode() ?: 0) + result = 31 * result + (clientIdScheme?.hashCode() ?: 0) + result = 31 * result + (walletIssuer?.hashCode() ?: 0) + result = 31 * result + (userHint?.hashCode() ?: 0) + result = 31 * result + (issuerState?.hashCode() ?: 0) + result = 31 * result + (responseMode?.hashCode() ?: 0) + result = 31 * result + (responseUrl?.hashCode() ?: 0) + result = 31 * result + (audience?.hashCode() ?: 0) + result = 31 * result + (issuer?.hashCode() ?: 0) + result = 31 * result + (issuedAt?.hashCode() ?: 0) + result = 31 * result + (resource?.hashCode() ?: 0) + result = 31 * result + (codeChallenge?.hashCode() ?: 0) + result = 31 * result + (codeChallengeMethod?.hashCode() ?: 0) + result = 31 * result + (lang?.hashCode() ?: 0) + result = 31 * result + (credentialID?.contentHashCode() ?: 0) + result = 31 * result + (signatureQualifier?.hashCode() ?: 0) + result = 31 * result + (numSignatures ?: 0) + result = 31 * result + (hashes?.hashCode() ?: 0) + result = 31 * result + (hashAlgorithmOid?.hashCode() ?: 0) + result = 31 * result + (description?.hashCode() ?: 0) + result = 31 * result + (accountToken?.hashCode() ?: 0) + result = 31 * result + (clientData?.hashCode() ?: 0) + return result + } + fun serialize() = jsonSerializer.encodeToString(this) companion object { diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2Client.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2Client.kt index 9fd16492..8e87e630 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2Client.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2Client.kt @@ -7,6 +7,8 @@ import at.asitplus.signum.indispensable.io.Base64UrlStrict import at.asitplus.wallet.lib.iso.sha256 import at.asitplus.wallet.lib.jws.JwsService import at.asitplus.wallet.lib.oidvci.* +import io.ktor.util.* +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.random.Random @@ -52,6 +54,7 @@ class OAuth2Client( * @param authorizationDetails from RFC 9396 OAuth 2.0 Rich Authorization Requests * @param resource from RFC 8707 Resource Indicators for OAuth 2.0, in OID4VCI flows the value * @param requestUri from CSC API v2.0.0.2: URI pointing to a pushed authorization request previously uploaded by the client + * @param credentialId from CSC API v2.0.0.2: The identifier associated to the credential to authorize * of [IssuerMetadata.credentialIssuer] */ suspend fun createAuthRequest( @@ -60,6 +63,7 @@ class OAuth2Client( scope: String? = null, resource: String? = null, requestUri: String? = null, + credentialId: ByteArray? = null, ) = AuthenticationRequestParameters( responseType = GRANT_TYPE_CODE, state = state, @@ -71,6 +75,7 @@ class OAuth2Client( codeChallenge = generateCodeVerifier(state), codeChallengeMethod = CODE_CHALLENGE_METHOD_SHA256, requestUri = requestUri, + credentialID = credentialId ) @OptIn(ExperimentalStdlibApi::class) diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/RqesWalletService.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/RqesWalletService.kt index 1d1bdbd5..bfaa1d5a 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/RqesWalletService.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/RqesWalletService.kt @@ -17,15 +17,17 @@ class RqesWalletService( private val oauth2Client: OAuth2Client = OAuth2Client(clientId = clientId, redirectUrl = redirectUrl), ) { - - suspend fun createOAuth2AuthenticationRequest(rqesRequest: RqesRequest): AuthenticationRequestParameters = + suspend fun createOAuth2AuthenticationRequest( + rqesRequest: RqesRequest, + credentialId: ByteArray, + ): AuthenticationRequestParameters = oauth2Client.createAuthRequest( state = uuid4().toString(), authorizationDetails = setOf(rqesRequest.toAuthorizationDetails()), - scope = RqesConstants.SCOPE + scope = RqesConstants.SCOPE, + credentialId = credentialId, ) - /** * TODO: could also use [Document] instead of [CscDocumentDigest], also [credential_id] instead of [SAD] */