diff --git a/build.gradle.kts b/build.gradle.kts index 378db00e..6df35ad6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,6 +78,9 @@ dependencies { implementation(libs.keycloak.admin.client) { because("To be able to fetch user attributes") } + implementation(libs.authlete.cbor) { + because("To implement CWT proof") + } implementation(libs.waltid.mdoc.credentials) { because("To sign CBOR credentials") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7da48c0e..d26ae6f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ resultMonad = "1.4.0" keycloak = "24.0.3" waltid = "0.3.1" uri-kmp = "0.0.18" +authlete-cbor = "1.18" [libraries] kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -44,6 +45,7 @@ result-monad = { module = "org.erwinkok.result:result-monad", version.ref = "res keycloak-admin-client = { module = "org.keycloak:keycloak-admin-client", version.ref = "keycloak" } waltid-mdoc-credentials = { module = "id.walt:waltid-mdoc-credentials-jvm", version.ref = "waltid" } uri-kmp = { module = "com.eygraber:uri-kmp", version.ref = "uri-kmp" } +authlete-cbor = { module = "com.authlete:cbor", version.ref = "authlete-cbor" } [plugins] foojay-resolver-convention = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "foojay" } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateCwtProof.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateCwtProof.kt new file mode 100644 index 00000000..126c37a4 --- /dev/null +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateCwtProof.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.europa.ec.eudi.pidissuer.adapter.out.jose + +import arrow.core.raise.Raise +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptyListOrNull +import com.authlete.cbor.* +import com.authlete.cose.COSEEC2Key +import com.authlete.cose.COSEProtectedHeader +import com.authlete.cose.COSESign1 +import com.authlete.cose.COSEVerifier +import com.authlete.cwt.CWT +import com.authlete.cwt.CWTClaimsSet +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK +import eu.europa.ec.eudi.pidissuer.domain.* +import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError +import java.time.Clock +import java.time.Instant +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration.Companion.minutes + +context (Raise) +fun validateCwtProof( + credentialIssuerId: CredentialIssuerId, + unvalidatedProof: UnvalidatedProof.Cwt, + expectedCNonce: CNonce, + credentialConfiguration: CredentialConfiguration, +): CredentialKey { + val proofType = credentialConfiguration.proofTypesSupported[ProofTypeEnum.CWT] + ensureNotNull(proofType) { + IssueCredentialError.InvalidProof("credential configuration '${credentialConfiguration.id.value}' doesn't support 'jwt' proofs") + } + check(proofType is ProofType.Cwt) + + return validateCwtProof(credentialIssuerId, unvalidatedProof, expectedCNonce, proofType) +} + +context (Raise) +fun validateCwtProof( + credentialIssuerId: CredentialIssuerId, + unvalidatedProof: UnvalidatedProof.Cwt, + expectedCNonce: CNonce, + proofType: ProofType.Cwt, +): CredentialKey { + return CwtProofValidator.isValid( + Clock.systemDefaultZone(), + iss = null, + aud = credentialIssuerId, + nonce = expectedCNonce, + p = unvalidatedProof, + ).getOrElse { + raise(IssueCredentialError.InvalidProof("Reason: " + it.message)) + } +} + +internal object CwtProofValidator { + + fun isValid( + clock: Clock, + iss: String?, + aud: CredentialIssuerId, + nonce: CNonce, + p: UnvalidatedProof.Cwt, + ): Result = runCatching { + val (credentialKey, claimSet) = claimSet(p) + if (iss != null) { + require(iss == claimSet.iss) { + "Invalid CWT proof. Expecting iss=$iss found ${claimSet.iss}" + } + } + require(aud.toString() == claimSet.aud) { + "Invalid CWT proof. Expecting aud=$aud found ${claimSet.aud}" + } + require(nonce.nonce == claimSet.nonce.toString(Charsets.UTF_8)) { + "Invalid CWT proof. Expecting nonce=${nonce.nonce}" + } + val claimSetIat = requireNotNull(claimSet.iat) { + "Invalid CWT proof. Missing iat" + } + val now = Instant.now() + val skew = 3.minutes.inWholeSeconds // seconds + val range = now.minusSeconds(skew).epochSecond..now.plusSeconds(skew).epochSecond + require(claimSetIat.toInstant().epochSecond in range) { + "Invalid CWT proof. Invalid iat" + } + credentialKey + } + + private fun claimSet(p: UnvalidatedProof.Cwt): Pair { + val cwt = ensureIsCWT(p) + val sign1 = ensureContainsSignOneMessage(cwt) + val credentialKey = verifySignature(sign1) + return credentialKey to claimSet(sign1) + } + + @OptIn(ExperimentalEncodingApi::class) + private fun ensureIsCWT(p: UnvalidatedProof.Cwt): CWT { + val cwtInBytes = Base64.UrlSafe.decode(p.cwt) + val cborItem = CBORDecoder(cwtInBytes).next() + require(cborItem is CWT) { "Not CBOR CWT" } + return cborItem + } + + private fun ensureContainsSignOneMessage(cwt: CWT): COSESign1 { + val message = cwt.message + require(message is COSESign1) { "CWT does not contain a COSE Sign one message" } + return message + } + + private fun verifySignature(sign1: COSESign1): CredentialKey { + val credentialKey = ensureValidProtectedHeader(sign1) + val coseKey = when (credentialKey) { + is CredentialKey.DIDUrl -> error("Unsupported") + is CredentialKey.Jwk -> { + require(credentialKey.value is ECKey) + credentialKey.value.toPublicKey() + } + + is CredentialKey.X5c -> { + credentialKey.certificate.publicKey + } + } + require(COSEVerifier(coseKey).verify(sign1)) { "Invalid signature" } + return credentialKey + } + + private fun ensureValidProtectedHeader(sign1: COSESign1): CredentialKey { + val pHeader: COSEProtectedHeader = sign1.protectedHeader + require("openid4vci-proof+cwt" == pHeader.contentType) { "Invalid content type ${pHeader.contentType}" } + + val coseKey = run { + val coseKeyAsByteString = pHeader.pairs.firstOrNull { (key, value) -> + key is CBORString && key.value == "COSE_Key" && + value is CBORByteArray + }?.value as CBORByteArray? + val cborItem = coseKeyAsByteString?.let { CBORDecoder(it.value).next() } + + cborItem?.takeIf { it is CBORPairList }?.let { item -> + check(item is CBORPairList) + COSEEC2Key(item.pairs) + } + } + + val x5cChain = pHeader.x5Chain.orEmpty() + require(!(null != coseKey && x5cChain.isNotEmpty())) { + "Cannot have both a COSE_Key and x5c chain" + } + return if (coseKey != null) { + val authJwk = coseKey.toJwk() + val jwk: JWK = JWK.parse(authJwk) + require(jwk is ECKey) + CredentialKey.Jwk(jwk) + } else { + CredentialKey.X5c(checkNotNull(x5cChain.toNonEmptyListOrNull())) + } + } + + private fun claimSet(sign1: COSESign1): CWTClaimsSet { + val payload = sign1.payload + val parsed = payload.parse() + val listOfPairs = CBORDecoder(parsed as ByteArray).next() as CBORPairList + return CWTClaimsSet(listOfPairs.pairs) + } +} + +private operator fun CBORPair.component1(): CBORItem = key +private operator fun CBORPair.component2(): CBORItem = value diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateJwtProof.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateJwtProof.kt index 7fce2f67..314bed2f 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateJwtProof.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateJwtProof.kt @@ -19,7 +19,6 @@ import arrow.core.NonEmptySet import arrow.core.raise.Raise import arrow.core.raise.ensureNotNull import arrow.core.raise.result -import arrow.core.toNonEmptySetOrNull import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader @@ -51,15 +50,13 @@ fun validateJwtProof( expectedCNonce: CNonce, credentialConfiguration: CredentialConfiguration, ): CredentialKey { - val proofTypes = credentialConfiguration.proofTypesSupported - .filterIsInstance() - .toNonEmptySetOrNull() - ensureNotNull(proofTypes) { + val proofType = credentialConfiguration.proofTypesSupported[ProofTypeEnum.JWT] + ensureNotNull(proofType) { IssueCredentialError.InvalidProof("credential configuration '${credentialConfiguration.id.value}' doesn't support 'jwt' proofs") } + check(proofType is ProofType.Jwt) - val supportedAlgorithms = proofTypes.flatMap { it.signingAlgorithmsSupported }.toNonEmptySet() - return validateJwtProof(credentialIssuerId, unvalidatedProof, expectedCNonce, supportedAlgorithms) + return validateJwtProof(credentialIssuerId, unvalidatedProof, expectedCNonce, proofType) } context (Raise) @@ -67,10 +64,10 @@ fun validateJwtProof( credentialIssuerId: CredentialIssuerId, unvalidatedProof: UnvalidatedProof.Jwt, expectedCNonce: CNonce, - supportedAlgorithms: NonEmptySet, + proofType: ProofType.Jwt, ): CredentialKey = result { val signedJwt = SignedJWT.parse(unvalidatedProof.jwt) - val (algorithm, credentialKey) = algorithmAndCredentialKey(signedJwt.header, supportedAlgorithms) + val (algorithm, credentialKey) = algorithmAndCredentialKey(signedJwt.header, proofType.signingAlgorithmsSupported) val keySelector = keySelector(credentialKey, algorithm) val processor = processor(expectedCNonce, credentialIssuerId, keySelector) processor.process(signedJwt, null) diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/mdl/IssueMobileDrivingLicence.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/mdl/IssueMobileDrivingLicence.kt index 3c275dea..4d02ffdb 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/mdl/IssueMobileDrivingLicence.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/mdl/IssueMobileDrivingLicence.kt @@ -280,7 +280,18 @@ val MobileDrivingLicenceV1: MsoMdocCredentialConfiguration = cryptographicBindingMethodsSupported = emptySet(), credentialSigningAlgorithmsSupported = emptySet(), scope = MobileDrivingLicenceV1Scope, - proofTypesSupported = nonEmptySetOf(ProofType.Jwt(nonEmptySetOf(JWSAlgorithm.ES256))), + proofTypesSupported = ProofTypesSupported( + nonEmptySetOf( + ProofType.Jwt( + signingAlgorithmsSupported = nonEmptySetOf(JWSAlgorithm.ES256), + ), + ProofType.Cwt( + algorithms = nonEmptySetOf(CoseAlgorithm.ES256), + curves = nonEmptySetOf(CoseCurve.P_256), + ), + ), + ), + policy = MsoMdocPolicy(oneTimeUse = false, batchSize = 2), ) /** diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueMsoMdocPid.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueMsoMdocPid.kt index 8c6d95d6..35a43f6c 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueMsoMdocPid.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueMsoMdocPid.kt @@ -234,7 +234,7 @@ val PidMsoMdocV1: MsoMdocCredentialConfiguration = cryptographicBindingMethodsSupported = emptySet(), credentialSigningAlgorithmsSupported = emptySet(), scope = PidMsoMdocScope, - proofTypesSupported = nonEmptySetOf(ProofType.Jwt(nonEmptySetOf(JWSAlgorithm.ES256))), + proofTypesSupported = ProofTypesSupported(nonEmptySetOf(ProofType.Jwt(nonEmptySetOf(JWSAlgorithm.ES256)))), ) // diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueSdJwtVcPid.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueSdJwtVcPid.kt index 2838268f..aa96d50d 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueSdJwtVcPid.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueSdJwtVcPid.kt @@ -96,7 +96,7 @@ fun pidSdJwtVcV1(signingAlgorithm: JWSAlgorithm): SdJwtVcCredentialConfiguration cryptographicBindingMethodsSupported = nonEmptySetOf(CryptographicBindingMethod.Jwk), credentialSigningAlgorithmsSupported = nonEmptySetOf(signingAlgorithm), scope = PidSdJwtVcScope, - proofTypesSupported = nonEmptySetOf(ProofType.Jwt(nonEmptySetOf(JWSAlgorithm.RS256, JWSAlgorithm.ES256))), + proofTypesSupported = ProofTypesSupported(nonEmptySetOf(ProofType.Jwt(nonEmptySetOf(JWSAlgorithm.RS256, JWSAlgorithm.ES256)))), ) typealias TimeDependant = (ZonedDateTime) -> F diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialConfiguration.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialConfiguration.kt index d07894b4..857b74b0 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialConfiguration.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialConfiguration.kt @@ -26,22 +26,39 @@ value class CredentialConfigurationId(val value: String) sealed interface ProofType { - val signingAlgorithmsSupported: NonEmptySet - /** * A JWT is used as proof of possession. */ - data class Jwt(override val signingAlgorithmsSupported: NonEmptySet) : ProofType + data class Jwt(val signingAlgorithmsSupported: NonEmptySet) : ProofType /** * A CWT is used as proof of possession. */ - data class Cwt(override val signingAlgorithmsSupported: NonEmptySet) : ProofType + data class Cwt(val algorithms: NonEmptySet, val curves: NonEmptySet) : ProofType +} - /** - * A W3C Verifiable Presentation object signed using the Data Integrity Proof is used as proof of possession. - */ - data class LdpVp(override val signingAlgorithmsSupported: NonEmptySet) : ProofType +fun ProofType.type(): ProofTypeEnum = when (this) { + is ProofType.Cwt -> ProofTypeEnum.CWT + is ProofType.Jwt -> ProofTypeEnum.JWT +} +enum class ProofTypeEnum { + JWT, CWT +} + +@JvmInline +value class ProofTypesSupported private constructor(val values: Set) { + + operator fun get(type: ProofTypeEnum): ProofType? = values.firstOrNull { it.type() == type } + + companion object { + val Empty: ProofTypesSupported = ProofTypesSupported(emptySet()) + operator fun invoke(values: Set): ProofTypesSupported { + require(values.groupBy(ProofType::type).all { (_, instances) -> instances.size == 1 }) { + "Multiple instance of the same proof type are not allowed" + } + return ProofTypesSupported(values) + } + } } /** @@ -54,5 +71,5 @@ sealed interface CredentialConfiguration { val display: List val cryptographicBindingMethodsSupported: Set val credentialSigningAlgorithmsSupported: Set - val proofTypesSupported: Set + val proofTypesSupported: ProofTypesSupported } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/JwtVcJsonProfile.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/JwtVcJsonProfile.kt index 06ebd355..26da0231 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/JwtVcJsonProfile.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/JwtVcJsonProfile.kt @@ -34,7 +34,7 @@ data class JwtVcJsonCredentialConfiguration( override val cryptographicBindingMethodsSupported: Set, override val credentialSigningAlgorithmsSupported: Set, override val display: List, - override val proofTypesSupported: Set, + override val proofTypesSupported: ProofTypesSupported = ProofTypesSupported.Empty, ) : CredentialConfiguration // diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/MsoMdocProfile.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/MsoMdocProfile.kt index 909f938f..07598518 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/MsoMdocProfile.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/MsoMdocProfile.kt @@ -15,11 +15,11 @@ */ package eu.europa.ec.eudi.pidissuer.domain -import arrow.core.NonEmptySet import arrow.core.raise.Raise import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull import com.nimbusds.jose.JWSAlgorithm +import kotlinx.serialization.SerialName // // Credential MetaData @@ -32,6 +32,11 @@ const val MSO_MDOC_FORMAT_VALUE = "mso_mdoc" val MSO_MDOC_FORMAT = Format(MSO_MDOC_FORMAT_VALUE) typealias MsoClaims = Map> +data class MsoMdocPolicy( + @SerialName("one_time_use") val oneTimeUse: Boolean, + @SerialName("batch_size") val batchSize: Int? = null, +) + /** * @param docType string identifying the credential type as defined in ISO.18013-5. */ @@ -43,7 +48,8 @@ data class MsoMdocCredentialConfiguration( override val scope: Scope? = null, override val display: List = emptyList(), val msoClaims: MsoClaims = emptyMap(), - override val proofTypesSupported: NonEmptySet, + override val proofTypesSupported: ProofTypesSupported = ProofTypesSupported.Empty, + val policy: MsoMdocPolicy? = null, ) : CredentialConfiguration // diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/SdJwtVcProfile.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/SdJwtVcProfile.kt index 0e535516..dd7afcdf 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/SdJwtVcProfile.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/SdJwtVcProfile.kt @@ -37,7 +37,7 @@ data class SdJwtVcCredentialConfiguration( override val credentialSigningAlgorithmsSupported: NonEmptySet, override val display: List, val claims: List, - override val proofTypesSupported: NonEmptySet, + override val proofTypesSupported: ProofTypesSupported = ProofTypesSupported.Empty, ) : CredentialConfiguration // diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/Types.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/Types.kt index e9685716..6b8acc44 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/Types.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/Types.kt @@ -15,6 +15,8 @@ */ package eu.europa.ec.eudi.pidissuer.domain +import com.authlete.cose.constants.COSEAlgorithms +import com.authlete.cose.constants.COSEEllipticCurves import com.nimbusds.jose.jwk.JWK import org.slf4j.LoggerFactory import java.net.MalformedURLException @@ -121,3 +123,53 @@ data class IssuedCredential( */ @JvmInline value class CredentialIdentifier(val value: String) + +@JvmInline +value class CoseAlgorithm private constructor(val value: Int) { + + fun name(): String = + checkNotNull(COSEAlgorithms.getNameByValue(value)) { "Cannot find name for COSE algorithm $value" } + + companion object { + + val ES256 = CoseAlgorithm(COSEAlgorithms.ES256) + val ES384 = CoseAlgorithm(COSEAlgorithms.ES384) + val ES512 = CoseAlgorithm(COSEAlgorithms.ES512) + + operator fun invoke(value: Int): Result = runCatching { + require(COSEAlgorithms.getNameByValue(value) != null) { "Unsupported COSE algorithm $value" } + CoseAlgorithm(value) + } + + operator fun invoke(name: String): Result = runCatching { + val value = COSEAlgorithms.getValueByName(name) + require(value != 0) { "Unsupported COSE algorithm $name" } + CoseAlgorithm(value) + } + } +} + +@JvmInline +value class CoseCurve private constructor(val value: Int) { + + fun name(): String = + checkNotNull(COSEEllipticCurves.getNameByValue(value)) { "Cannot find name for COSE Curve $value" } + + companion object { + + val P_256 = CoseCurve(COSEEllipticCurves.P_256) + val P_384 = CoseCurve(COSEEllipticCurves.P_384) + val P_521 = CoseCurve(COSEEllipticCurves.P_521) + + operator fun invoke(value: Int): Result = runCatching { + require(COSEEllipticCurves.getNameByValue(value) != null) { "Unsupported COSE Curve $value" } + CoseCurve(value) + } + + operator fun invoke(name: String): Result = runCatching { + val value = COSEEllipticCurves.getValueByName(name) + require(value != 0) { "Unsupported COSE Curve $name" } + CoseCurve(value) + } + } +} diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetCredentialIssuerMetaData.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetCredentialIssuerMetaData.kt index fd8f24aa..3e9e1e84 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetCredentialIssuerMetaData.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetCredentialIssuerMetaData.kt @@ -154,10 +154,10 @@ private fun credentialMetaDataJson(d: CredentialConfiguration): JsonObject = bui addAll(credentialSigningAlgorithmsSupported.map { it.name }) } } - d.proofTypesSupported.takeIf { it.isNotEmpty() } + d.proofTypesSupported.takeIf { it != ProofTypesSupported.Empty } ?.let { proofTypesSupported -> putJsonObject("proof_types_supported") { - proofTypesSupported.forEach { + proofTypesSupported.values.forEach { put(it.proofTypeName(), it.toJsonObject()) } } @@ -181,14 +181,28 @@ private fun ProofType.proofTypeName(): String = when (this) { is ProofType.Jwt -> "jwt" is ProofType.Cwt -> "cwt" - is ProofType.LdpVp -> "ldp_vp" } @OptIn(ExperimentalSerializationApi::class) private fun ProofType.toJsonObject(): JsonObject = buildJsonObject { - putJsonArray("proof_signing_alg_values_supported") { - addAll(signingAlgorithmsSupported.map { it.name }) + when (this@toJsonObject) { + is ProofType.Cwt -> { + putJsonArray("proof_signing_alg_values_supported") { + addAll(this@toJsonObject.algorithms.map { it.value }) + } + putJsonArray("proof_alg_values_supported") { + addAll(this@toJsonObject.algorithms.map { it.value }) + } + putJsonArray("proof_crv_values_supported") { + addAll(this@toJsonObject.curves.map { it.value }) + } + } + is ProofType.Jwt -> { + putJsonArray("proof_signing_alg_values_supported") { + addAll(signingAlgorithmsSupported.map { it.name }) + } + } } } @@ -201,6 +215,12 @@ internal fun MsoMdocCredentialConfiguration.toTransferObject(isOffer: Boolean): addAll(display.map { it.toTransferObject() }) } } + if (policy != null) { + putJsonObject("policy") { + put("one_time_use", policy.oneTimeUse) + policy.batchSize?.let { put("batch_size", it) } + } + } putJsonObject("claims") { msoClaims.forEach { (nameSpace, attributes) -> diff --git a/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateJwtProofTest.kt b/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateJwtProofTest.kt index c0a812b2..dafb4e74 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateJwtProofTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateJwtProofTest.kt @@ -30,10 +30,7 @@ import com.nimbusds.jose.util.Base64URL import com.nimbusds.jose.util.X509CertChainUtils import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import eu.europa.ec.eudi.pidissuer.domain.CNonce -import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerId -import eu.europa.ec.eudi.pidissuer.domain.CredentialKey -import eu.europa.ec.eudi.pidissuer.domain.UnvalidatedProof +import eu.europa.ec.eudi.pidissuer.domain.* import eu.europa.ec.eudi.pidissuer.loadResource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -61,12 +58,13 @@ internal class ValidateJwtProofTest { type(JOSEObjectType.JWT) jwk(key.toPublicJWK()) } + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) val result = either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) } assert(result.isLeft()) @@ -78,12 +76,13 @@ internal class ValidateJwtProofTest { val nonce = generateCNonce() val signedJwt = generateSignedJwt(key, nonce) + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) val result = either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) } assert(result.isLeft()) @@ -99,12 +98,13 @@ internal class ValidateJwtProofTest { x509CertChain(chain.map { Base64.encode(it.encoded) }) } + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) val result = either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) } assertTrue { result.isLeft() } @@ -120,12 +120,14 @@ internal class ValidateJwtProofTest { x509CertChain(chain.map { Base64.encode(it.encoded) }) } + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) + either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) }.fold( ifLeft = { fail("Unexpected $it") }, @@ -145,12 +147,13 @@ internal class ValidateJwtProofTest { jwk(key.toPublicJWK()) } + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) }.fold( ifLeft = { fail("Unexpected $it") }, @@ -170,12 +173,13 @@ internal class ValidateJwtProofTest { keyID("did:jwk:${Base64URL.encode(key.toPublicJWK().toJSONString())}#0") } + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) }.fold( ifLeft = { fail("Unexpected $it") }, @@ -196,12 +200,13 @@ internal class ValidateJwtProofTest { jwk(incorrectKey.toPublicJWK()) } + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) val result = either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) } assertTrue { result.isLeft() } @@ -217,12 +222,13 @@ internal class ValidateJwtProofTest { x509CertChain(incorrectKey.toPublicJWK().x509CertChain) } + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) val result = either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) } assertTrue { result.isLeft() } @@ -237,13 +243,14 @@ internal class ValidateJwtProofTest { generateSignedJwt(key, nonce) { keyID("did:jwk:${Base64URL.encode(incorrectKey.toPublicJWK().toJSONString())}#0") } + val proofType = ProofType.Jwt(checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull())) val result = either { validateJwtProof( issuer, UnvalidatedProof.Jwt(signedJwt.serialize()), nonce, - checkNotNull(RSASSASigner.SUPPORTED_ALGORITHMS.toNonEmptySetOrNull()), + proofType, ) } assertTrue { result.isLeft() }