Skip to content

Commit bb9e833

Browse files
authored
Merge pull request #1971 from walt-id/fix/invalid-proof
Fix/invalid proof + nonce
2 parents 50b1d9e + 1ba6b85 commit bb9e833

50 files changed

Lines changed: 1642 additions & 195 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

waltid-libraries/protocols/waltid-openid4vc-wallet/src/commonMain/kotlin/id/walt/wallet2/handlers/WalletIssuanceHandler.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,9 @@ object WalletIssuanceHandler {
10491049
methods: Set<CryptographicBindingMethod>?
10501050
): Boolean {
10511051
if (methods.isNullOrEmpty()) return false
1052-
val supportsJwk = methods.any { it is CryptographicBindingMethod.Jwk }
1052+
val supportsJwk = methods.any {
1053+
it is CryptographicBindingMethod.Jwk || it is CryptographicBindingMethod.CoseKey
1054+
}
10531055
val supportsDid = methods.any { it is CryptographicBindingMethod.Did }
10541056
return supportsJwk && !supportsDid
10551057
}

waltid-libraries/protocols/waltid-openid4vci/src/commonMain/kotlin/id/walt/openid4vci/core/Config.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import id.walt.openid4vci.handlers.endpoints.credential.CredentialEndpointHandle
99
import id.walt.openid4vci.handlers.endpoints.par.PushedAuthorizationEndpointHandlers
1010
import id.walt.openid4vci.handlers.endpoints.token.TokenEndpointHandlers
1111
import id.walt.openid4vci.preauthorized.PreAuthorizedCodeIssuer
12+
import id.walt.openid4vci.proofs.CredentialProofVerifier
13+
import id.walt.openid4vci.proofs.DefaultCredentialProofVerifier
1214
import id.walt.openid4vci.repository.authorization.AuthorizationCodeRepository
1315
import id.walt.openid4vci.repository.par.PARRepository
1416
import id.walt.openid4vci.repository.preauthorized.PreAuthorizedCodeRepository
@@ -71,6 +73,7 @@ data class OAuth2ProviderConfig(
7173

7274
val credentialRequestValidator: CredentialRequestValidator,
7375
val credentialRequestDecryptor: CredentialRequestDecryptor? = null,
76+
val credentialProofVerifier: CredentialProofVerifier? = DefaultCredentialProofVerifier(),
7477
val credentialEndpointHandlers: CredentialEndpointHandlers,
7578
val credentialResponseEncryptor: CredentialResponseEncryptor = JweCredentialResponseEncryptor,
7679
)

waltid-libraries/protocols/waltid-openid4vci/src/commonMain/kotlin/id/walt/openid4vci/core/DefaultOAuth2Provider.kt

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,14 @@ import id.walt.openid4vci.requests.credential.CredentialRequestResult
4343
import id.walt.openid4vci.metadata.issuer.CredentialConfiguration
4444
import id.walt.openid4vci.metadata.issuer.CredentialDisplay
4545
import id.walt.mdoc.dataelement.json.JsonObjectToCborMappingConfig as LegacyMdocJsonObjectToCborMappingConfig
46+
import id.walt.openid4vci.proofs.CredentialProofValidationContext
47+
import id.walt.openid4vci.proofs.CredentialProofValidationException
4648
import id.walt.crypto.keys.Key
4749
import id.walt.mdoc.objects.mso.Status
4850
import id.walt.openid4vci.tokens.access.AccessTokenAuthorizationScheme
4951
import id.walt.openid4vci.tokens.access.CredentialAccessTokenContext
5052
import id.walt.openid4vci.tokens.access.dpopJwkThumbprint
53+
import id.walt.openid4vci.tokens.jwt.JwtPayloadClaims
5154
import id.walt.sdjwt.SDMap
5255
import id.walt.x509.CertificateDer
5356
import kotlinx.coroutines.CancellationException
@@ -56,6 +59,8 @@ import kotlinx.serialization.json.JsonNull
5659
import kotlinx.serialization.json.JsonObject
5760
import kotlinx.serialization.json.JsonPrimitive
5861
import kotlinx.serialization.json.buildJsonArray
62+
import kotlinx.serialization.json.contentOrNull
63+
import kotlinx.serialization.json.jsonPrimitive
5964
import kotlin.time.Clock
6065
import kotlin.time.Instant
6166

@@ -504,7 +509,10 @@ class DefaultOAuth2Provider(
504509
session: Session?,
505510
accessTokenContext: CredentialAccessTokenContext?
506511
): CredentialRequestResult {
507-
verifyCredentialAccessToken(accessTokenContext)?.let { return it }
512+
val tokenClaims = when (val tokenResult = verifyCredentialAccessToken(accessTokenContext)) {
513+
is CredentialAccessTokenVerification.Success -> tokenResult.claims
514+
is CredentialAccessTokenVerification.Failure -> return tokenResult.result
515+
}
508516
return when (val result = config.credentialRequestValidator.validate(parameters, session ?: DefaultSession())) {
509517
is CredentialRequestResult.Success ->
510518
if (result.request.credentialResponseEncryption != null) {
@@ -515,7 +523,7 @@ class DefaultOAuth2Provider(
515523
)
516524
)
517525
} else {
518-
result
526+
CredentialRequestResult.Success(result.request.withAccessTokenClient(tokenClaims))
519527
}
520528

521529
is CredentialRequestResult.Failure -> result
@@ -528,7 +536,10 @@ class DefaultOAuth2Provider(
528536
session: Session?,
529537
accessTokenContext: CredentialAccessTokenContext?
530538
): CredentialRequestResult {
531-
verifyCredentialAccessToken(accessTokenContext)?.let { return it }
539+
val tokenClaims = when (val tokenResult = verifyCredentialAccessToken(accessTokenContext)) {
540+
is CredentialAccessTokenVerification.Success -> tokenResult.claims
541+
is CredentialAccessTokenVerification.Failure -> return tokenResult.result
542+
}
532543
val decryptor = config.credentialRequestDecryptor
533544
?: return CredentialRequestResult.Failure(
534545
CredentialError(
@@ -545,7 +556,12 @@ class DefaultOAuth2Provider(
545556
} catch (e: Exception) {
546557
return CredentialRequestResult.Failure(CredentialError(CredentialErrorCodes.INVALID_CREDENTIAL_REQUEST, e.message))
547558
}
548-
return config.credentialRequestValidator.validate(parameters, session ?: DefaultSession())
559+
return when (val result = config.credentialRequestValidator.validate(parameters, session ?: DefaultSession())) {
560+
is CredentialRequestResult.Success ->
561+
CredentialRequestResult.Success(result.request.withAccessTokenClient(tokenClaims))
562+
is CredentialRequestResult.Failure -> result
563+
is CredentialRequestResult.OAuthFailure -> result
564+
}
549565
}
550566

551567
override suspend fun createCredentialResponse(
@@ -563,7 +579,19 @@ class DefaultOAuth2Provider(
563579
credentialStatus: Status?,
564580
validFrom: Instant?,
565581
validUntil: Instant?,
582+
proofValidationContext: CredentialProofValidationContext?,
566583
): CredentialResponseResult {
584+
val verifiedProofs = when (
585+
val proofResult = verifyCredentialProofs(
586+
request = request,
587+
configuration = configuration,
588+
proofValidationContext = proofValidationContext,
589+
)
590+
) {
591+
is CredentialProofVerification.Success -> proofResult.proofs
592+
is CredentialProofVerification.Failure -> return CredentialResponseResult.Failure(proofResult.error)
593+
}
594+
567595
val handler = config.credentialEndpointHandlers.get(configuration.format)
568596
?: return CredentialResponseResult.Failure(
569597
CredentialError(
@@ -586,6 +614,7 @@ class DefaultOAuth2Provider(
586614
credentialStatus = credentialStatus,
587615
validFrom = validFrom,
588616
validUntil = validUntil,
617+
verifiedProofs = verifiedProofs,
589618
)
590619
}
591620

@@ -638,11 +667,13 @@ class DefaultOAuth2Provider(
638667

639668
private suspend fun verifyCredentialAccessToken(
640669
accessTokenContext: CredentialAccessTokenContext?,
641-
): CredentialRequestResult.OAuthFailure? {
642-
if (accessTokenContext == null) return null
670+
): CredentialAccessTokenVerification {
671+
if (accessTokenContext == null) return CredentialAccessTokenVerification.Success(null)
643672
val verifier = config.accessTokenVerifier
644-
?: return CredentialRequestResult.OAuthFailure(
645-
OAuthError(OAuthErrorCodes.SERVER_ERROR, "access token verifier not configured")
673+
?: return CredentialAccessTokenVerification.Failure(
674+
CredentialRequestResult.OAuthFailure(
675+
OAuthError(OAuthErrorCodes.SERVER_ERROR, "access token verifier not configured")
676+
)
646677
)
647678
return try {
648679
val claims = verifier.verify(
@@ -651,10 +682,61 @@ class DefaultOAuth2Provider(
651682
expectedAudience = accessTokenContext.expectedAudience,
652683
)
653684
verifyCredentialAccessTokenBinding(accessTokenContext, claims)
685+
?.let { CredentialAccessTokenVerification.Failure(it) }
686+
?: CredentialAccessTokenVerification.Success(claims)
687+
} catch (e: CancellationException) {
688+
throw e
689+
} catch (e: Exception) {
690+
CredentialAccessTokenVerification.Failure(
691+
invalidCredentialAccessToken(e.message ?: "Access token is invalid"),
692+
)
693+
}
694+
}
695+
696+
private fun CredentialRequest.withAccessTokenClient(tokenClaims: JsonObject?): CredentialRequest {
697+
val clientId = tokenClaims?.get(JwtPayloadClaims.CLIENT_ID)
698+
?.jsonPrimitive
699+
?.contentOrNull
700+
?.takeIf { it.isNotBlank() }
701+
val anonymousPreAuthorizedAccess = clientId == null &&
702+
tokenClaims?.get(JwtPayloadClaims.PRE_AUTHORIZED_CODE)?.jsonPrimitive?.contentOrNull != null
703+
return withAccessTokenClient(clientId, anonymousPreAuthorizedAccess)
704+
}
705+
706+
private suspend fun verifyCredentialProofs(
707+
request: CredentialRequest,
708+
configuration: CredentialConfiguration,
709+
proofValidationContext: CredentialProofValidationContext?,
710+
): CredentialProofVerification {
711+
val shouldVerifyProofs = configuration.proofTypesSupported != null ||
712+
(proofValidationContext != null && request.proofs != null)
713+
if (!shouldVerifyProofs) return CredentialProofVerification.Success(emptyList())
714+
715+
val context = proofValidationContext
716+
?: return CredentialProofVerification.Failure(
717+
CredentialError(CredentialErrorCodes.INVALID_PROOF, "Credential proof validation context is required"),
718+
)
719+
val verifier = config.credentialProofVerifier
720+
?: return CredentialProofVerification.Failure(
721+
CredentialError(CredentialErrorCodes.INVALID_PROOF, "Credential proof verification is not configured"),
722+
)
723+
724+
return try {
725+
CredentialProofVerification.Success(
726+
verifier.verify(
727+
credentialRequest = request,
728+
credentialConfiguration = configuration,
729+
context = context,
730+
)
731+
)
654732
} catch (e: CancellationException) {
655733
throw e
734+
} catch (e: CredentialProofValidationException) {
735+
CredentialProofVerification.Failure(CredentialError(e.errorCode, e.message))
656736
} catch (e: Exception) {
657-
invalidCredentialAccessToken(e.message ?: "Access token is invalid")
737+
CredentialProofVerification.Failure(
738+
CredentialError(CredentialErrorCodes.INVALID_PROOF, e.message ?: "Invalid credential proof"),
739+
)
658740
}
659741
}
660742

@@ -663,14 +745,11 @@ class DefaultOAuth2Provider(
663745
claims: JsonObject,
664746
): CredentialRequestResult.OAuthFailure? {
665747
val boundJwkThumbprint = claims.dpopJwkThumbprint()
666-
667-
if (boundJwkThumbprint == null) {
668-
return if (context.authorization.scheme == AccessTokenAuthorizationScheme.BEARER) {
748+
?: return if (context.authorization.scheme == AccessTokenAuthorizationScheme.BEARER) {
669749
null
670750
} else {
671751
invalidCredentialAccessToken("Access token is not DPoP-bound")
672752
}
673-
}
674753

675754
if (context.authorization.scheme != AccessTokenAuthorizationScheme.DPOP) {
676755
return invalidCredentialAccessToken("DPoP-bound access token must use the DPoP authorization scheme")
@@ -715,6 +794,18 @@ class DefaultOAuth2Provider(
715794
OAuthError(OAuthErrorCodes.INVALID_DPOP_PROOF, description),
716795
)
717796

797+
private sealed class CredentialAccessTokenVerification {
798+
data class Success(val claims: JsonObject?) : CredentialAccessTokenVerification()
799+
data class Failure(val result: CredentialRequestResult) : CredentialAccessTokenVerification()
800+
}
801+
802+
private sealed class CredentialProofVerification {
803+
data class Success(val proofs: List<id.walt.openid4vci.proofs.VerifiedCredentialProof>) :
804+
CredentialProofVerification()
805+
806+
data class Failure(val error: CredentialError) : CredentialProofVerification()
807+
}
808+
718809
private fun dpopAuthenticationChallenge(error: OAuthError): String = buildString {
719810
append(TOKEN_TYPE_DPOP)
720811
append(" error=\"").append(error.error.escapeAuthenticationParameter()).append('"')

waltid-libraries/protocols/waltid-openid4vci/src/commonMain/kotlin/id/walt/openid4vci/core/OAuth2Provider.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import id.walt.openid4vci.responses.credential.CredentialResponse
2626
import id.walt.openid4vci.responses.credential.CredentialResponseHttp
2727
import id.walt.crypto.keys.Key
2828
import id.walt.mdoc.objects.mso.Status
29+
import id.walt.openid4vci.proofs.CredentialProofValidationContext
2930
import id.walt.openid4vci.tokens.access.CredentialAccessTokenContext
3031
import id.walt.openid4vci.metadata.issuer.CredentialDisplay
3132
import id.walt.sdjwt.SDMap
@@ -146,6 +147,7 @@ interface OAuth2Provider {
146147
credentialStatus: Status? = null,
147148
validFrom: Instant? = null,
148149
validUntil: Instant? = null,
150+
proofValidationContext: CredentialProofValidationContext? = null,
149151
): CredentialResponseResult
150152

151153
fun writeCredentialError(error: CredentialError): CredentialResponseHttp

waltid-libraries/protocols/waltid-openid4vci/src/commonMain/kotlin/id/walt/openid4vci/errors/OAuthError.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package id.walt.openid4vci.errors
22

3+
import kotlinx.serialization.SerialName
34
import kotlinx.serialization.Serializable
45

56
@Serializable
67
data class OAuthError(
78
val error: String,
9+
@SerialName("error_description")
810
val description: String? = null,
911
)
1012

waltid-libraries/protocols/waltid-openid4vci/src/commonMain/kotlin/id/walt/openid4vci/handlers/credential/MdocCredentialHandler.kt

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import id.walt.openid4vci.handlers.endpoints.credential.CredentialEndpointHandle
1010
import id.walt.openid4vci.metadata.issuer.CredentialConfiguration
1111
import id.walt.openid4vci.metadata.issuer.CredentialDisplay
1212
import id.walt.mdoc.dataelement.json.JsonObjectToCborMappingConfig as LegacyMdocJsonObjectToCborMappingConfig
13+
import id.walt.openid4vci.proofs.VerifiedCredentialProof
1314
import id.walt.openid4vci.requests.credential.CredentialRequest
1415
import id.walt.openid4vci.responses.credential.CredentialResponse
1516
import id.walt.openid4vci.responses.credential.CredentialResponseResult
@@ -46,6 +47,7 @@ class MdocCredentialHandler : CredentialEndpointHandler {
4647
credentialStatus: Status?,
4748
validFrom: Instant?,
4849
validUntil: Instant?,
50+
verifiedProofs: List<VerifiedCredentialProof>,
4951
): CredentialResponseResult {
5052
return try {
5153
if (configuration.format != CredentialFormat.MSO_MDOC) {
@@ -67,6 +69,7 @@ class MdocCredentialHandler : CredentialEndpointHandler {
6769
credentialStatus = credentialStatus,
6870
validFrom = validFrom,
6971
validUntil = validUntil,
72+
verifiedProofs = verifiedProofs,
7073
)
7174
} catch (e: Exception) {
7275
CredentialResponseResult.Failure(e.toCredentialHandlerError())
@@ -84,6 +87,7 @@ class MdocCredentialHandler : CredentialEndpointHandler {
8487
credentialStatus: Status?,
8588
validFrom: Instant?,
8689
validUntil: Instant?,
90+
verifiedProofs: List<VerifiedCredentialProof>,
8791
): CredentialResponseResult.Success {
8892
val docType = configuration.doctype
8993
?: throw IllegalArgumentException("Missing doctype for mDoc credential configuration")
@@ -108,23 +112,29 @@ class MdocCredentialHandler : CredentialEndpointHandler {
108112
"mDoc issuance requests require that the x5Chain parameter contains at least one entry"
109113
}.map { CoseCertificate(it.bytes.toByteArray()) }
110114

111-
val issuedCredential = MdocCredentialSigner.generateMdocCredential(
112-
credentialRequest = request,
113-
credentialData = credentialData,
114-
issuerKey = issuerKey,
115-
issuerCertificate = issuerCertificateChain,
116-
docType = docType,
117-
validFrom = validFrom,
118-
validUntil = resolveValidUntil(request, validUntil),
119-
status = credentialStatus,
120-
mDocNameSpacesDataMappingConfig = mDocNameSpacesDataMappingConfig,
121-
)
115+
val proofsToIssue = if (verifiedProofs.isEmpty()) {
116+
listOf<VerifiedCredentialProof?>(null)
117+
} else {
118+
verifiedProofs
119+
}
120+
val issuedCredentials = proofsToIssue.map { verifiedProof ->
121+
MdocCredentialSigner.generateMdocCredential(
122+
credentialRequest = request,
123+
credentialData = credentialData,
124+
issuerKey = issuerKey,
125+
issuerCertificate = issuerCertificateChain,
126+
docType = docType,
127+
validFrom = validFrom,
128+
validUntil = resolveValidUntil(request, validUntil),
129+
status = credentialStatus,
130+
mDocNameSpacesDataMappingConfig = mDocNameSpacesDataMappingConfig,
131+
verifiedProof = verifiedProof,
132+
)
133+
}
122134

123135
return CredentialResponseResult.Success(
124136
CredentialResponse(
125-
credentials = listOf(
126-
IssuedCredential(credential = JsonPrimitive(issuedCredential)),
127-
),
137+
credentials = issuedCredentials.map { IssuedCredential(credential = JsonPrimitive(it)) },
128138
),
129139
)
130140
}

waltid-libraries/protocols/waltid-openid4vci/src/commonMain/kotlin/id/walt/openid4vci/handlers/credential/MdocCredentialSigner.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ package id.walt.openid4vci.handlers.credential
33
import id.walt.cose.CoseCertificate
44
import id.walt.cose.CoseKey
55
import id.walt.cose.coseCompliantCbor
6+
import id.walt.cose.JWKKeyCoseTransform.getCosePublicKey
67
import id.walt.crypto.keys.Key
78
import id.walt.crypto.keys.KeyType
9+
import id.walt.crypto.keys.jwk.JWKKey
810
import id.walt.crypto.utils.Base64Utils.encodeToBase64Url
911
import id.walt.mdoc.dataelement.json.JsonObjectToCborMappingConfig as LegacyMdocJsonObjectToCborMappingConfig
1012
import id.walt.mdoc.dataelement.DataElement as LegacyMdocDataElement
1113
import id.walt.mdoc.issuance.MdocIssuer
1214
import id.walt.mdoc.objects.mso.Status
1315
import id.walt.mdoc.schema.MdocsSchemaMappingFunction.toCborElement
16+
import id.walt.openid4vci.proofs.VerifiedCredentialProof
1417
import id.walt.openid4vci.requests.credential.CredentialRequest
1518
import kotlinx.serialization.ExperimentalSerializationApi
1619
import kotlinx.serialization.cbor.Cbor
@@ -35,14 +38,15 @@ object MdocCredentialSigner {
3538
validUntil: Instant = Clock.System.now().plus(1.days * 365 * 10),
3639
status: Status? = null,
3740
mDocNameSpacesDataMappingConfig: Map<String, LegacyMdocJsonObjectToCborMappingConfig>? = null,
41+
verifiedProof: VerifiedCredentialProof? = null,
3842
valueMappingFunction: (
3943
docType: String,
4044
namespace: String,
4145
elementIdentifier: String,
4246
elementValueJson: JsonElement
4347
) -> CborElement? = defaultSchemalessMappingFunction,
4448
): String {
45-
val holderKey = resolveHolderKey(credentialRequest)
49+
val holderKey = verifiedProof?.toCosePublicKey() ?: resolveHolderKey(credentialRequest)
4650
validateIssuerKey(issuerKey)
4751
val namespaces = credentialData.mapValues { (namespace, namespaceData) ->
4852
requireNotNull(namespaceData as? JsonObject) {
@@ -82,6 +86,9 @@ object MdocCredentialSigner {
8286
return JwtProofUtils.resolveHolderKey(jwtProof)
8387
}
8488

89+
private suspend fun VerifiedCredentialProof.toCosePublicKey(): CoseKey =
90+
JWKKey.importJWK(holderKey.exportJWK()).getOrThrow().getCosePublicKey()
91+
8592
private fun validateIssuerKey(issuerKey: Key) {
8693
require(issuerKey.keyType == KeyType.secp256r1) {
8794
"Issuer key must be EC secp256r1"

0 commit comments

Comments
 (0)