Skip to content

Commit 50d728c

Browse files
committed
WIP
1 parent 75b7d52 commit 50d728c

15 files changed

Lines changed: 142 additions & 88 deletions

File tree

openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialResponseEncryption.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ data class CredentialResponseEncryption(
1818
* OID4VCI: REQUIRED. JWE (RFC7516) `alg` algorithm (RFC7518) for encrypting Credential Responses.
1919
*/
2020
@SerialName("alg")
21-
val jweAlgorithm: JweAlgorithm,
21+
val jweAlgorithm: JweAlgorithm? = null,
2222

2323
/**
2424
* OID4VCI: REQUIRED. JWE (RFC7516) `enc` algorithm (RFC7518) for encrypting Credential Responses.

openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedAlgorithmsContainer.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ data class SupportedAlgorithmsContainer(
1616
* context-specific, e.g. `EdDSA` and `ES256`.
1717
*/
1818
@SerialName("alg_values_supported")
19-
val supportedAlgorithmsStrings: Set<String>,
19+
val supportedAlgorithmsStrings: Set<String>? = null,
2020

2121
/**
2222
* OID4VCI: REQUIRED. Array containing a list of the JWE (RFC7516) encryption algorithms (enc values) (RFC7518)
@@ -32,7 +32,7 @@ data class SupportedAlgorithmsContainer(
3232
* every Credential Response and therefore the Wallet MUST provide encryption keys in the Credential Request.
3333
* If the value is `false`, the Wallet MAY choose whether it provides encryption keys or not.
3434
*/
35-
@SerialName("encryption_required")
35+
@SerialName("encryption_required") // TODO make this non-optional?!
3636
val encryptionRequired: Boolean? = null,
3737

3838
/**
@@ -52,8 +52,8 @@ data class SupportedAlgorithmsContainer(
5252
* context-specific, e.g. `EdDSA` and `ES256`.
5353
*/
5454
@Transient
55-
val supportedAlgorithms: Set<JsonWebAlgorithm> = supportedAlgorithmsStrings
56-
.mapNotNull { s -> JsonWebAlgorithm.entries.firstOrNull { it.identifier == s } }.toSet()
55+
val supportedAlgorithms: Set<JsonWebAlgorithm>? = supportedAlgorithmsStrings
56+
?.mapNotNull { s -> JsonWebAlgorithm.entries.firstOrNull { it.identifier == s } }?.toSet()
5757

5858
/**
5959
* OID4VCI: REQUIRED. Array containing a list of the JWE (RFC7516) encryption algorithms (enc values) (RFC7518)

openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedCredentialFormat.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,18 @@ data class SupportedCredentialFormat private constructor(
169169
supportedBindingMethods: Set<String>? = null,
170170
supportedProofTypes: Map<String, CredentialRequestProofSupported>? = null,
171171
credentialDefinition: SupportedCredentialFormatDefinition,
172+
vcJwtClaims: Set<ClaimDescription>,
172173
display: Set<DisplayProperties>? = null,
173174
) = SupportedCredentialFormat(
174175
format = format,
175176
scope = scope,
176177
supportedBindingMethods = supportedBindingMethods,
177178
supportedProofTypes = supportedProofTypes,
178179
credentialDefinition = credentialDefinition,
179-
credentialMetadata = display?.let {
180-
CredentialMetadata(display = display)
181-
}
180+
credentialMetadata = CredentialMetadata(
181+
claimDescription = vcJwtClaims,
182+
display = display,
183+
)
182184
)
183185

184186
}

openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedCredentialFormatDefinition.kt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,4 @@ data class SupportedCredentialFormatDefinition(
1717
@SerialName("type")
1818
val types: Set<String>? = null,
1919

20-
/**
21-
* OID4VCI:
22-
* W3C VC: OPTIONAL. Object containing a list of name/value pairs, where each name identifies
23-
* a claim offered in the Credential. The value can be another such object (nested data structures), or an array
24-
* of such objects
25-
*/
26-
@SerialName("credentialSubject")
27-
val credentialSubject: Map<String, CredentialSubjectMetadataSingle>? = null,
28-
2920
)

vck-openid-ktor/src/commonMain/kotlin/at/asitplus/wallet/lib/ktor/openid/Extensions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fun OAuth2Error?.dpopNonce(response: HttpResponse) = runCatching {
6161
authorizationServerProvidedNonce(response)
6262
?: resourceServerProvidedNonce(response)
6363
}.getOrNull()
64-
64+
// todo need to handle "wrong nonce"?
6565
/** [RFC 9449 8.](https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid) */
6666
private fun OAuth2Error?.authorizationServerProvidedNonce(response: HttpResponse): String? =
6767
this?.error.takeIf { it == USE_DPOP_NONCE }?.let { response.headers[HttpHeaders.DPoPNonce] }

vck-openid-ktor/src/commonMain/kotlin/at/asitplus/wallet/lib/ktor/openid/OpenId4VciClient.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ class OpenId4VciClient(
271271
tokenResponse = tokenResponse,
272272
credentialFormat = credentialFormat,
273273
credentialScheme = credentialScheme,
274-
dpopNonce = dpopNonce
274+
dpopNonce = dpopNonce ?: tokenResponse.dpopNonce,
275275
)
276276
}
277277
return CredentialIssuanceResult.Success(

vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import at.asitplus.openid.OidcUserInfoExtended
1212
import at.asitplus.openid.OpenIdConstants
1313
import at.asitplus.signum.indispensable.SignatureAlgorithm
1414
import at.asitplus.signum.indispensable.josef.JsonWebKeySet
15+
import at.asitplus.signum.indispensable.josef.JweEncrypted
1516
import at.asitplus.signum.indispensable.josef.JwsSigned
1617
import at.asitplus.wallet.lib.agent.EphemeralKeyWithoutCert
1718
import at.asitplus.wallet.lib.agent.Issuer
@@ -70,6 +71,20 @@ class CredentialIssuer(
7071
private val encryptionService: IssuerEncryptionService = IssuerEncryptionService(),
7172
) {
7273

74+
sealed interface CredentialResponse {
75+
/**
76+
* Send [response] as JSON-serialized content to the client with media
77+
* type `application/json` (see [at.asitplus.wallet.lib.data.MediaTypes.Application.JSON]).
78+
*/
79+
data class Plain(val response: CredentialResponseParameters) : CredentialResponse
80+
81+
/**
82+
* Send [response] as JWE-serialized content to the client with media
83+
* type `application/jwt` (see [at.asitplus.wallet.lib.data.MediaTypes.Application.JWT]).
84+
*/
85+
data class Encrypted(val response: JweEncrypted) : CredentialResponse
86+
}
87+
7388
private val supportedCredentialConfigurations = credentialSchemes
7489
.flatMap { it.toSupportedCredentialFormat().entries }
7590
.associate {
@@ -165,12 +180,12 @@ class CredentialIssuer(
165180
input: String,
166181
credentialDataProvider: CredentialDataProviderFun,
167182
request: RequestInfo? = null,
168-
): KmmResult<CredentialResponseParameters> = catching {
183+
): KmmResult<CredentialResponse> = catching {
169184
credentialInternal(
170185
authorizationHeader = authorizationHeader,
171-
params = encryptionService.decrypt(input).getOrThrow(),
186+
request = encryptionService.decrypt(input).getOrThrow(),
172187
credentialDataProvider = credentialDataProvider,
173-
request = request,
188+
requestInfo = request,
174189
hasBeenEncrypted = true,
175190
).getOrThrow()
176191
}
@@ -196,11 +211,11 @@ class CredentialIssuer(
196211
params: CredentialRequestParameters,
197212
credentialDataProvider: CredentialDataProviderFun,
198213
request: RequestInfo? = null,
199-
): KmmResult<CredentialResponseParameters> = credentialInternal(
214+
): KmmResult<CredentialResponse> = credentialInternal(
200215
authorizationHeader = authorizationHeader,
201-
params = params,
216+
request = params,
202217
credentialDataProvider = credentialDataProvider,
203-
request = request,
218+
requestInfo = request,
204219
hasBeenEncrypted = false,
205220
)
206221

@@ -225,12 +240,12 @@ class CredentialIssuer(
225240
params: WalletService.CredentialRequest,
226241
credentialDataProvider: CredentialDataProviderFun,
227242
request: RequestInfo? = null,
228-
): KmmResult<CredentialResponseParameters> = catching {
243+
): KmmResult<CredentialResponse> = catching {
229244
credentialInternal(
230245
authorizationHeader = authorizationHeader,
231-
params = params.decryptIfNeeded(),
246+
request = params.decryptIfNeeded(),
232247
credentialDataProvider = credentialDataProvider,
233-
request = request,
248+
requestInfo = request,
234249
hasBeenEncrypted = false,
235250
).getOrThrow()
236251
}
@@ -242,19 +257,19 @@ class CredentialIssuer(
242257

243258
private suspend fun credentialInternal(
244259
authorizationHeader: String,
245-
params: CredentialRequestParameters,
260+
request: CredentialRequestParameters,
246261
credentialDataProvider: CredentialDataProviderFun,
247-
request: RequestInfo? = null,
262+
requestInfo: RequestInfo? = null,
248263
hasBeenEncrypted: Boolean = false,
249-
): KmmResult<CredentialResponseParameters> = catching {
264+
): KmmResult<CredentialResponse> = catching {
250265
Napier.i("credential called")
251-
Napier.d("credential called with $authorizationHeader, $params")
266+
Napier.d("credential called with $authorizationHeader, $request")
252267
if (!hasBeenEncrypted && encryptionService.requireRequestEncryption)
253268
throw InvalidEncryptionParameters("Credential request has not been encrypted")
254-
authorizationService.validateAccessToken(authorizationHeader, request).getOrThrow()
255-
val userInfo = params.introspectTokenLoadUserInfo(authorizationHeader, request)
256-
val (scheme, representation) = params.extractCredentialRepresentation()
257-
proofValidator.validateProofExtractSubjectPublicKeys(params).map { subjectPublicKey ->
269+
authorizationService.validateAccessToken(authorizationHeader, requestInfo).getOrThrow()
270+
val userInfo = request.introspectTokenLoadUserInfo(authorizationHeader, requestInfo)
271+
val (scheme, representation) = request.extractCredentialRepresentation()
272+
val responseParameters = proofValidator.validateProofExtractSubjectPublicKeys(request).map { subjectPublicKey ->
258273
issuer.issueCredential(
259274
credentialDataProvider(
260275
CredentialDataProviderInput(
@@ -269,7 +284,8 @@ class CredentialIssuer(
269284
).getOrElse {
270285
throw CredentialRequestDenied("No credential from issuer", it)
271286
}
272-
}.toCredentialResponseParameters(encryptionService.encryptResponseIfNecessary(params))
287+
}.toCredentialResponseParameters()
288+
encryptionService.encryptResponse(responseParameters, request)
273289
.also { Napier.i("credential returns"); Napier.d("credential returns $it") }
274290
}
275291

@@ -299,10 +315,16 @@ class CredentialIssuer(
299315
if (!it.validCredentialIdentifiers.contains(credentialIdentifier))
300316
throw InvalidToken("credential_identifier $credentialIdentifier expected to be in $it")
301317
} ?: credentialConfigurationId?.let { credentialConfigurationId ->
302-
if (it.scope == null)
303-
throw InvalidToken("no scope stored for access token $authorizationHeader")
304-
if (!it.scope.contains(credentialConfigurationId))
305-
throw InvalidToken("credential_configuration_id $credentialConfigurationId expected to be $it")
318+
// TODO Test this
319+
if (it.scope != null) {
320+
if (!it.scope.contains(credentialConfigurationId))
321+
throw InvalidToken("credential_configuration_id $credentialConfigurationId expected to be $it")
322+
} else if (it.authorizationDetails != null) {
323+
if (!it.validCredentialConfigurationIds.contains(credentialConfigurationId))
324+
throw InvalidToken("credential_configuration_id $credentialConfigurationId expected to be in $it")
325+
} else {
326+
throw InvalidToken("neither scope nor authorization details stored $authorizationHeader")
327+
}
306328
} ?: authorizationHeader.run {
307329
throw InvalidToken("neither credential_identifier nor credential_configuration_id set")
308330
}

vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialSchemeMapping.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ object CredentialSchemeMapping {
4949
scope = this,
5050
credentialDefinition = SupportedCredentialFormatDefinition(
5151
types = setOf(VcDataModelConstants.VERIFIABLE_CREDENTIAL, vcType!!),
52-
credentialSubject = claimNames.associateWith { CredentialSubjectMetadataSingle() }
5352
),
5453
supportedBindingMethods = setOf(BINDING_METHOD_JWK, URN_TYPE_JWK_THUMBPRINT),
54+
vcJwtClaims = claimNames.map {
55+
ClaimDescription(path = it.split("."))
56+
}.toSet()
5557
)
5658
}
5759
} else null

vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/Extensions.kt

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,48 +27,33 @@ fun CredentialFormatEnum.toRepresentation() = when (this) {
2727
else -> CredentialRepresentation.PLAIN_JWT
2828
}
2929

30-
/**
31-
* @param transformer may be used to encrypt the credentials before serializing
32-
*/
33-
suspend fun Issuer.IssuedCredential.toCredentialResponseParameters(
34-
transformer: (suspend (String) -> String) = { it },
35-
) = when (this) {
30+
suspend fun Issuer.IssuedCredential.toCredentialResponseParameters() = when (this) {
3631
is Issuer.IssuedCredential.Iso -> CredentialResponseParameters(
37-
credentials = setOf(toCredentialResponseSingleCredential(transformer)),
32+
credentials = setOf(toCredentialResponseSingleCredential()),
3833
)
3934

4035
is Issuer.IssuedCredential.VcJwt -> CredentialResponseParameters(
41-
credentials = setOf(toCredentialResponseSingleCredential(transformer)),
36+
credentials = setOf(toCredentialResponseSingleCredential()),
4237
)
4338

4439
is Issuer.IssuedCredential.VcSdJwt -> CredentialResponseParameters(
45-
credentials = setOf(toCredentialResponseSingleCredential(transformer)),
40+
credentials = setOf(toCredentialResponseSingleCredential()),
4641
)
4742
}
4843

49-
/**
50-
* @param transformer may be used to encrypt the credentials before serializing
51-
*/
52-
suspend fun Collection<Issuer.IssuedCredential>.toCredentialResponseParameters(
53-
transformer: (suspend (String) -> String) = { it },
54-
) = if (size == 1) {
55-
first().toCredentialResponseParameters(transformer)
44+
suspend fun Collection<Issuer.IssuedCredential>.toCredentialResponseParameters() = if (size == 1) {
45+
first().toCredentialResponseParameters()
5646
} else {
5747
CredentialResponseParameters(
58-
credentials = this.map { it.toCredentialResponseSingleCredential(transformer) }.toSet()
48+
credentials = this.map { it.toCredentialResponseSingleCredential() }.toSet()
5949
)
6050
}
6151

62-
/**
63-
* @param transformer may be used to encrypt the credentials before serializing
64-
*/
65-
suspend fun Issuer.IssuedCredential.toCredentialResponseSingleCredential(
66-
transformer: (suspend (String) -> String) = { it },
67-
): CredentialResponseSingleCredential = CredentialResponseSingleCredential(
52+
fun Issuer.IssuedCredential.toCredentialResponseSingleCredential() = CredentialResponseSingleCredential(
6853
when (this) {
69-
is Issuer.IssuedCredential.Iso -> JsonPrimitive(transformer(toBase64UrlStrict()))
70-
is Issuer.IssuedCredential.VcJwt -> JsonPrimitive(transformer(signedVcJws.serialize()))
71-
is Issuer.IssuedCredential.VcSdJwt -> JsonPrimitive(transformer(signedSdJwtVc.serialize()))
54+
is Issuer.IssuedCredential.Iso -> JsonPrimitive(toBase64UrlStrict())
55+
is Issuer.IssuedCredential.VcJwt -> JsonPrimitive(signedVcJws.serialize())
56+
is Issuer.IssuedCredential.VcSdJwt -> JsonPrimitive(signedSdJwtVc.serialize())
7257
}
7358
)
7459

0 commit comments

Comments
 (0)