From 844e14c5cc2a08d63a7668267696806e600f214b Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Fri, 15 Nov 2024 18:15:34 +0200 Subject: [PATCH] Simplify ports and refactor. --- .../ec/eudi/pidissuer/PidIssuerApplication.kt | 21 ++-- .../DecryptCNonceWithNimbusAndVerify.kt} | 33 +++---- .../out/credential/DefaultGenerateCNonce.kt | 33 ------- .../GenerateCNonceAndEncryptWithNimbus.kt} | 30 +++--- .../adapter/out/jose/ValidateJwtProof.kt | 13 +-- .../adapter/out/jose/ValidateProofs.kt | 29 +++--- .../out/mdl/IssueMobileDrivingLicence.kt | 4 +- .../adapter/out/pid/IssueMsoMdocPid.kt | 4 +- .../adapter/out/pid/IssueSdJwtVcPid.kt | 4 +- .../pidissuer/port/input/IssueCredential.kt | 17 +--- .../port/out/credential/GenerateCNonce.kt | 6 +- .../out/credential/VerifyCNonce.kt} | 28 ++++-- .../pidissuer/port/out/jose/DecryptCNonce.kt | 25 ----- .../pidissuer/port/out/jose/EncryptCNonce.kt | 25 ----- .../adapter/input/web/WalletApiTest.kt | 97 +++++++------------ .../adapter/out/jose/ValidateJwtProofTest.kt | 58 +++-------- .../adapter/out/jose/ValidateProofTest.kt | 16 +-- 17 files changed, 152 insertions(+), 291 deletions(-) rename src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/{jose/DecryptCNonceWithNimbus.kt => credential/DecryptCNonceWithNimbusAndVerify.kt} (73%) delete mode 100644 src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/DefaultGenerateCNonce.kt rename src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/{jose/EncryptCNonceWithNimbus.kt => credential/GenerateCNonceAndEncryptWithNimbus.kt} (67%) rename src/main/kotlin/eu/europa/ec/eudi/pidissuer/{domain/CNonce.kt => port/out/credential/VerifyCNonce.kt} (50%) delete mode 100644 src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/DecryptCNonce.kt delete mode 100644 src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptCNonce.kt diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt index 9b58c92d..cbc83402 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt @@ -37,10 +37,10 @@ import eu.europa.ec.eudi.pidissuer.adapter.input.web.WalletApi import eu.europa.ec.eudi.pidissuer.adapter.input.web.security.* import eu.europa.ec.eudi.pidissuer.adapter.out.IssuerSigningKey import eu.europa.ec.eudi.pidissuer.adapter.out.credential.CredentialRequestFactory -import eu.europa.ec.eudi.pidissuer.adapter.out.credential.DefaultGenerateCNonce +import eu.europa.ec.eudi.pidissuer.adapter.out.credential.DecryptCNonceWithNimbusAndVerify import eu.europa.ec.eudi.pidissuer.adapter.out.credential.DefaultResolveCredentialRequestByCredentialIdentifier +import eu.europa.ec.eudi.pidissuer.adapter.out.credential.GenerateCNonceAndEncryptWithNimbus import eu.europa.ec.eudi.pidissuer.adapter.out.jose.* -import eu.europa.ec.eudi.pidissuer.adapter.out.jose.EncryptCNonceWithNimbus import eu.europa.ec.eudi.pidissuer.adapter.out.mdl.* import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryDeferredCredentialRepository import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryIssuedCredentialRepository @@ -361,9 +361,8 @@ fun beans(clock: Clock) = beans { // // CNonce // - bean { DefaultGenerateCNonce(clock = clock, expiresIn = Duration.ofMinutes(5L)) } - bean { EncryptCNonceWithNimbus(issuerPublicUrl, ref("cnonce-encryption-key")) } - bean { DecryptCNonceWithNimbus(issuerPublicUrl, ref("cnonce-encryption-key")) } + bean { GenerateCNonceAndEncryptWithNimbus(clock, issuerPublicUrl, ref("cnonce-encryption-key")) } + bean { DecryptCNonceWithNimbusAndVerify(issuerPublicUrl, ref("cnonce-encryption-key")) } // // Credentials @@ -388,7 +387,7 @@ fun beans(clock: Clock) = beans { // bean { ValidateJwtProof(issuerPublicUrl) } bean { DefaultExtractJwkFromCredentialKey } - bean { ValidateProofs(ref(), clock, ref()) } + bean { ValidateProofs(ref(), ref(), ref()) } bean { CredentialIssuerMetaData( id = issuerPublicUrl, @@ -408,7 +407,6 @@ fun beans(clock: Clock) = beans { clock = clock, storeIssuedCredentials = ref(), validateProofs = ref(), - decryptCNonce = ref(), ) add(issueMsoMdocPid) } @@ -439,7 +437,6 @@ fun beans(clock: Clock) = beans { generateNotificationId = ref(), storeIssuedCredentials = ref(), validateProofs = ref(), - decryptCNonce = ref(), ) val deferred = env.getProperty("issuer.pid.sd_jwt_vc.deferred") ?: false @@ -458,7 +455,6 @@ fun beans(clock: Clock) = beans { clock = clock, storeIssuedCredentials = ref(), validateProofs = ref(), - decryptCNonce = ref(), ) add(mdlIssuer) } @@ -480,7 +476,12 @@ fun beans(clock: Clock) = beans { // bean(::GetCredentialIssuerMetaData) bean { - IssueCredential(ref(), ref(), ref(), ref(), ref()) + IssueCredential( + credentialIssuerMetadata = ref(), + resolveCredentialRequestByCredentialIdentifier = ref(), + generateCNonce = ref(), + encryptCredentialResponse = ref(), + ) } bean { GetDeferredCredential(ref(), ref()) diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/DecryptCNonceWithNimbus.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/DecryptCNonceWithNimbusAndVerify.kt similarity index 73% rename from src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/DecryptCNonceWithNimbus.kt rename to src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/DecryptCNonceWithNimbusAndVerify.kt index 25658e07..ad3ce487 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/DecryptCNonceWithNimbus.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/DecryptCNonceWithNimbusAndVerify.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package eu.europa.ec.eudi.pidissuer.adapter.out.jose +package eu.europa.ec.eudi.pidissuer.adapter.out.credential import arrow.core.raise.result import com.nimbusds.jose.EncryptionMethod @@ -30,19 +30,18 @@ import com.nimbusds.jwt.EncryptedJWT import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier import com.nimbusds.jwt.proc.DefaultJWTProcessor -import eu.europa.ec.eudi.pidissuer.domain.CNonce import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerId -import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce +import eu.europa.ec.eudi.pidissuer.port.out.credential.VerifyCNonce import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.time.Duration +import java.time.Instant /** - * Nimbus implementation of [DecryptCNonce] that decrypts an [EncryptedJWT] as a [CNonce]. + * Decrypts an [EncryptedJWT] using Nimbus and verifies it's still active. */ -internal class DecryptCNonceWithNimbus( +internal class DecryptCNonceWithNimbusAndVerify( private val issuer: CredentialIssuerId, private val decryptionKey: RSAKey, -) : DecryptCNonce { +) : VerifyCNonce { init { require(decryptionKey.isPrivate) { "a private key is required for decryption" } } @@ -65,17 +64,17 @@ internal class DecryptCNonceWithNimbus( .issuer(issuer.externalForm) .audience(issuer.externalForm) .build(), - setOf("iss", "aud", "cnonce", "exi", "iat"), + setOf("iss", "aud", "cnonce", "iat", "exp"), ) } - override suspend fun invoke(encrypted: String): Result = result { - val jwt = EncryptedJWT.parse(encrypted) - val claimSet = processor.process(jwt, null) - CNonce( - requireNotNull(claimSet.getStringClaim("cnonce")) { "missing 'cnonce' claim" }, - requireNotNull(claimSet.issueTime.toInstant()) { "missing 'iat' claim" }, - requireNotNull(claimSet.getLongClaim("exi")?.let { Duration.ofSeconds(it) }) { "missing 'exi' claim" }, - ) - } + override suspend fun invoke(value: String?, at: Instant): Boolean = + value?.let { + result { + val jwt = EncryptedJWT.parse(it) + val claimSet = processor.process(jwt, null) + val expiresAt = requireNotNull(claimSet.expirationTime) { "expirationTime is required" } + at < expiresAt.toInstant() + }.getOrElse { false } + } ?: false } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/DefaultGenerateCNonce.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/DefaultGenerateCNonce.kt deleted file mode 100644 index 1232071c..00000000 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/DefaultGenerateCNonce.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.credential - -import com.nimbusds.openid.connect.sdk.Nonce -import eu.europa.ec.eudi.pidissuer.domain.CNonce -import eu.europa.ec.eudi.pidissuer.port.out.credential.GenerateCNonce -import java.time.Clock -import java.time.Duration - -/** - * Default implementation for [GenerateCNonce]. - */ -internal class DefaultGenerateCNonce( - private val clock: Clock, - private val expiresIn: Duration, - private val generator: suspend () -> String = { Nonce(128).value }, -) : GenerateCNonce { - override suspend fun invoke(): CNonce = CNonce(generator(), clock.instant(), expiresIn) -} diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCNonceWithNimbus.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/GenerateCNonceAndEncryptWithNimbus.kt similarity index 67% rename from src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCNonceWithNimbus.kt rename to src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/GenerateCNonceAndEncryptWithNimbus.kt index 57830df6..c7b44b54 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCNonceWithNimbus.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/credential/GenerateCNonceAndEncryptWithNimbus.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package eu.europa.ec.eudi.pidissuer.adapter.out.jose +package eu.europa.ec.eudi.pidissuer.adapter.out.credential import com.nimbusds.jose.EncryptionMethod import com.nimbusds.jose.JOSEObjectType @@ -23,27 +23,34 @@ import com.nimbusds.jose.crypto.RSAEncrypter import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jwt.EncryptedJWT import com.nimbusds.jwt.JWTClaimsSet -import eu.europa.ec.eudi.pidissuer.domain.CNonce +import com.nimbusds.openid.connect.sdk.Nonce import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerId -import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptCNonce +import eu.europa.ec.eudi.pidissuer.port.out.credential.GenerateCNonce import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.time.Clock +import java.time.Duration import java.util.* /** - * Nimbus implementation of [EncryptCNonce] that encrypts [CNonce] as an [EncryptedJWT]. + * Generates a CNonce and encrypts it as a [EncryptedJWT] with Nimbus. */ -internal class EncryptCNonceWithNimbus( +internal class GenerateCNonceAndEncryptWithNimbus( + private val clock: Clock, private val issuer: CredentialIssuerId, encryptionKey: RSAKey, -) : EncryptCNonce { + private val generator: suspend () -> String = { Nonce(128).value }, +) : GenerateCNonce { private val encrypter = RSAEncrypter(encryptionKey) .apply { jcaContext.provider = BouncyCastleProvider() } - override suspend fun invoke(cnonce: CNonce): String = - EncryptedJWT( + override suspend fun invoke(cnonceExpiresIn: Duration): String { + val issuedAt = clock.instant() + val expiresAt = issuedAt + cnonceExpiresIn + + return EncryptedJWT( JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_512, EncryptionMethod.XC20P) .type(JOSEObjectType("cnonce+jwt")) .build(), @@ -51,12 +58,13 @@ internal class EncryptCNonceWithNimbus( .apply { issuer(issuer.externalForm) audience(issuer.externalForm) - claim("cnonce", cnonce.nonce) - claim("exi", cnonce.expiresIn.seconds) - issueTime(Date.from(cnonce.activatedAt)) + claim("cnonce", generator()) + issueTime(Date.from(issuedAt)) + expirationTime(Date.from(expiresAt)) } .build(), ).apply { encrypt(encrypter) }.serialize() + } } 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 bbd89dc4..277024c9 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 @@ -37,7 +37,6 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor import com.nimbusds.jwt.proc.JWTProcessor import eu.europa.ec.eudi.pidissuer.domain.* import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError -import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce import java.security.interfaces.ECPublicKey import java.security.interfaces.EdECPublicKey import java.security.interfaces.RSAPublicKey @@ -51,23 +50,17 @@ internal class ValidateJwtProof( private val credentialIssuerId: CredentialIssuerId, ) { context(Raise) - suspend operator fun invoke( + operator fun invoke( unvalidatedProof: UnvalidatedProof.Jwt, credentialConfiguration: CredentialConfiguration, - decryptCNonce: DecryptCNonce?, - ): Pair { + ): Pair { 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 (credentialKey, nonce) = credentialKeyAndNonce(unvalidatedProof, proofType) - val cnonce = decryptCNonce?.let { - ensureNotNull(nonce) { IssueCredentialError.InvalidProof("Missing CNonce") } - decryptCNonce(nonce).getOrElse { raise(IssueCredentialError.InvalidProof("Invalid CNonce", it)) } - } - return credentialKey to cnonce + return credentialKeyAndNonce(unvalidatedProof, proofType) } context(Raise) diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateProofs.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateProofs.kt index 1b0db1d5..8a887167 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateProofs.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateProofs.kt @@ -22,20 +22,17 @@ import arrow.core.toNonEmptyListOrNull import com.nimbusds.jose.jwk.JWK import eu.europa.ec.eudi.pidissuer.domain.CredentialConfiguration import eu.europa.ec.eudi.pidissuer.domain.UnvalidatedProof -import eu.europa.ec.eudi.pidissuer.domain.isExpired import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.InvalidProof -import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import java.time.Clock +import eu.europa.ec.eudi.pidissuer.port.out.credential.VerifyCNonce +import kotlinx.coroutines.* +import java.time.Instant /** * Validators for Proofs. */ internal class ValidateProofs( private val validateJwtProof: ValidateJwtProof, - private val clock: Clock, + private val verifyCNonce: VerifyCNonce, private val extractJwkFromCredentialKey: ExtractJwkFromCredentialKey, ) { @@ -43,25 +40,23 @@ internal class ValidateProofs( suspend operator fun invoke( unvalidatedProofs: NonEmptyList, credentialConfiguration: CredentialConfiguration, - decryptCNonce: DecryptCNonce?, + at: Instant, ): NonEmptyList = coroutineScope { val credentialKeysAndCNonces = unvalidatedProofs.map { when (it) { - is UnvalidatedProof.Jwt -> async { validateJwtProof(it, credentialConfiguration, decryptCNonce) } + is UnvalidatedProof.Jwt -> async { + withContext(Dispatchers.Default) { + validateJwtProof(it, credentialConfiguration) + } + } is UnvalidatedProof.LdpVp -> raise(InvalidProof("Supporting only JWT proof")) } }.awaitAll() val cnonces = credentialKeysAndCNonces.map { it.second }.toNonEmptyListOrNull() checkNotNull(cnonces) - ensure(cnonces.distinct().size == 1) { - InvalidProof("The Proofs of a Credential Request must contain the same CNonce") - } - - cnonces.head?.let { cnonce -> - ensure(!cnonce.isExpired(clock.instant())) { - InvalidProof("CNonce is expired") - } + ensure(verifyCNonce(cnonces, at)) { + InvalidProof("CNonce is not valid") } val jwks = credentialKeysAndCNonces.map { 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 a29200de..2e818446 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 @@ -28,7 +28,6 @@ import eu.europa.ec.eudi.pidissuer.port.input.AuthorizationContext import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.InvalidProof import eu.europa.ec.eudi.pidissuer.port.out.IssueSpecificCredential -import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateNotificationId import eu.europa.ec.eudi.pidissuer.port.out.persistence.StoreIssuedCredentials import kotlinx.coroutines.* @@ -303,7 +302,6 @@ internal class IssueMobileDrivingLicence( private val generateNotificationId: GenerateNotificationId, private val clock: Clock, private val storeIssuedCredentials: StoreIssuedCredentials, - private val decryptCNonce: DecryptCNonce, ) : IssueSpecificCredential { override val supportedCredential: MsoMdocCredentialConfiguration @@ -319,7 +317,7 @@ internal class IssueMobileDrivingLicence( credentialIdentifier: CredentialIdentifier?, ): CredentialResponse = coroutineScope { log.info("Issuing mDL") - val holderKeys = validateProofs(request.unvalidatedProofs, supportedCredential, decryptCNonce) + val holderKeys = validateProofs(request.unvalidatedProofs, supportedCredential, clock.instant()) .map { it.toECKeyOrFail { InvalidProof("Only EC Key is supported") } } val licence = ensureNotNull(getMobileDrivingLicenceData(authorizationContext)) { IssueCredentialError.Unexpected("Unable to fetch mDL data") 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 f53fa0ee..c4e876f5 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 @@ -28,7 +28,6 @@ import eu.europa.ec.eudi.pidissuer.port.input.AuthorizationContext import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.InvalidProof import eu.europa.ec.eudi.pidissuer.port.out.IssueSpecificCredential -import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateNotificationId import eu.europa.ec.eudi.pidissuer.port.out.persistence.StoreIssuedCredentials import kotlinx.coroutines.async @@ -264,7 +263,6 @@ internal class IssueMsoMdocPid( private val generateNotificationId: GenerateNotificationId, private val clock: Clock, private val storeIssuedCredentials: StoreIssuedCredentials, - private val decryptCNonce: DecryptCNonce, ) : IssueSpecificCredential { private val log = LoggerFactory.getLogger(IssueMsoMdocPid::class.java) @@ -280,7 +278,7 @@ internal class IssueMsoMdocPid( credentialIdentifier: CredentialIdentifier?, ): CredentialResponse = coroutineScope { log.info("Handling issuance request ...") - val holderPubKeys = validateProofs(request.unvalidatedProofs, supportedCredential, decryptCNonce) + val holderPubKeys = validateProofs(request.unvalidatedProofs, supportedCredential, clock.instant()) .map { it.toECKeyOrFail { InvalidProof("Only EC Key is supported") } } val pidData = async { getPidData(authorizationContext) } 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 eeafe8d4..ac417a63 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 @@ -29,7 +29,6 @@ import eu.europa.ec.eudi.pidissuer.domain.* import eu.europa.ec.eudi.pidissuer.port.input.AuthorizationContext import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError import eu.europa.ec.eudi.pidissuer.port.out.IssueSpecificCredential -import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateNotificationId import eu.europa.ec.eudi.pidissuer.port.out.persistence.StoreIssuedCredentials import eu.europa.ec.eudi.sdjwt.HashAlgorithm @@ -117,7 +116,6 @@ internal class IssueSdJwtVcPid( private val notificationsEnabled: Boolean, private val generateNotificationId: GenerateNotificationId, private val storeIssuedCredentials: StoreIssuedCredentials, - private val decryptCNonce: DecryptCNonce, ) : IssueSpecificCredential { override val supportedCredential: SdJwtVcCredentialConfiguration = pidSdJwtVcV1(issuerSigningKey.signingAlgorithm) @@ -141,7 +139,7 @@ internal class IssueSdJwtVcPid( credentialIdentifier: CredentialIdentifier?, ): CredentialResponse = coroutineScope { log.info("Handling issuance request ...") - val holderPubKeys = validateProofs(request.unvalidatedProofs, supportedCredential, decryptCNonce) + val holderPubKeys = validateProofs(request.unvalidatedProofs, supportedCredential, clock.instant()) val pidData = async { getPidData(authorizationContext) } val (pid, pidMetaData) = pidData.await() diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt index 961d93f8..008643aa 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt @@ -25,7 +25,6 @@ import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.* import eu.europa.ec.eudi.pidissuer.port.out.IssueSpecificCredential import eu.europa.ec.eudi.pidissuer.port.out.credential.GenerateCNonce import eu.europa.ec.eudi.pidissuer.port.out.credential.ResolveCredentialRequestByCredentialIdentifier -import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptCNonce import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptCredentialResponse import kotlinx.coroutines.coroutineScope import kotlinx.serialization.Required @@ -304,7 +303,7 @@ class IssueCredential( private val credentialIssuerMetadata: CredentialIssuerMetaData, private val resolveCredentialRequestByCredentialIdentifier: ResolveCredentialRequestByCredentialIdentifier, private val generateCNonce: GenerateCNonce, - private val encryptCNonce: EncryptCNonce, + private val cnonceExpiresIn: Duration = Duration.ofMinutes(5L), private val encryptCredentialResponse: EncryptCredentialResponse, ) { @@ -379,8 +378,8 @@ class IssueCredential( request: CredentialRequest, credential: CredentialResponse, ): IssueCredentialResponse { - val (newCNonce, newCNonceExpiresIn) = newCNonce() - val plain = credential.toTO(newCNonce, newCNonceExpiresIn) + val newCNonce = generateCNonce(cnonceExpiresIn) + val plain = credential.toTO(newCNonce, cnonceExpiresIn) return when (val encryption = request.credentialResponseEncryption) { RequestedResponseEncryption.NotRequired -> plain is RequestedResponseEncryption.Required -> encryptCredentialResponse(plain, encryption).getOrThrow() @@ -391,14 +390,8 @@ class IssueCredential( error: IssueCredentialError, ): IssueCredentialResponse { log.warn("Issuance failed: $error") - val (newCNonce, newCNonceExpiresIn) = newCNonce() - return error.toTO(newCNonce, newCNonceExpiresIn) - } - - private suspend fun newCNonce(): Pair { - val cnonce = generateCNonce() - val encryptedCNonce = encryptCNonce(cnonce) - return encryptedCNonce to cnonce.expiresIn + val newCNonce = generateCNonce(cnonceExpiresIn) + return error.toTO(newCNonce, cnonceExpiresIn) } } // diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/credential/GenerateCNonce.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/credential/GenerateCNonce.kt index 7d34d18f..89187d0f 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/credential/GenerateCNonce.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/credential/GenerateCNonce.kt @@ -15,11 +15,11 @@ */ package eu.europa.ec.eudi.pidissuer.port.out.credential -import eu.europa.ec.eudi.pidissuer.domain.CNonce +import java.time.Duration /** - * Generates a new [CNonce]. + * Generates a new CNonce that expires after a specific [Duration]. */ fun interface GenerateCNonce { - suspend operator fun invoke(): CNonce + suspend operator fun invoke(cnonceExpiresIn: Duration): String } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CNonce.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/credential/VerifyCNonce.kt similarity index 50% rename from src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CNonce.kt rename to src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/credential/VerifyCNonce.kt index 150cb755..2f76eb3c 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CNonce.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/credential/VerifyCNonce.kt @@ -13,15 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package eu.europa.ec.eudi.pidissuer.domain +package eu.europa.ec.eudi.pidissuer.port.out.credential -import java.time.Duration import java.time.Instant -data class CNonce( - val nonce: String, - val activatedAt: Instant, - val expiresIn: Duration, -) +/** + * Verifies a CNonce value is valid at a specific [Instant]. + */ +fun interface VerifyCNonce { + suspend operator fun invoke(value: String?, at: Instant): Boolean + + suspend operator fun invoke(values: List, at: Instant): Boolean = + when (values.distinct().size) { + 1 -> this(values.first(), at) + else -> false + } + + companion object { -fun CNonce.isExpired(at: Instant): Boolean = (activatedAt + expiresIn) <= at + /** + * Gets a [VerifyCNonce] that perform no verification. + */ + fun noCNonceRequired(): VerifyCNonce = VerifyCNonce { _, _ -> true } + } +} diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/DecryptCNonce.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/DecryptCNonce.kt deleted file mode 100644 index db239746..00000000 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/DecryptCNonce.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.port.out.jose - -import eu.europa.ec.eudi.pidissuer.domain.CNonce - -/** - * Decrypts a CNonce. - */ -fun interface DecryptCNonce { - suspend operator fun invoke(encrypted: String): Result -} diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptCNonce.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptCNonce.kt deleted file mode 100644 index 3e396c45..00000000 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptCNonce.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.port.out.jose - -import eu.europa.ec.eudi.pidissuer.domain.CNonce - -/** - * Encrypts a CNonce. - */ -fun interface EncryptCNonce { - suspend operator fun invoke(cnonce: CNonce): String -} diff --git a/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/WalletApiTest.kt b/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/WalletApiTest.kt index 8020bc6e..69fc158f 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/WalletApiTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/WalletApiTest.kt @@ -34,14 +34,11 @@ import eu.europa.ec.eudi.pidissuer.PidIssuerApplicationTest import eu.europa.ec.eudi.pidissuer.adapter.input.web.security.DPoPConfigurationProperties import eu.europa.ec.eudi.pidissuer.adapter.input.web.security.DPoPTokenAuthentication import eu.europa.ec.eudi.pidissuer.adapter.out.pid.* -import eu.europa.ec.eudi.pidissuer.domain.CNonce import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerId import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerMetaData import eu.europa.ec.eudi.pidissuer.domain.Scope import eu.europa.ec.eudi.pidissuer.port.input.* import eu.europa.ec.eudi.pidissuer.port.out.credential.GenerateCNonce -import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce -import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptCNonce import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive @@ -87,12 +84,6 @@ internal class BaseWalletApiTest { @Autowired protected lateinit var generateCNonce: GenerateCNonce - @Autowired - protected lateinit var encryptCNonce: EncryptCNonce - - @Autowired - protected lateinit var decryptCNonce: DecryptCNonce - @Autowired protected lateinit var credentialIssuerMetadata: CredentialIssuerMetaData @@ -105,7 +96,7 @@ internal class BaseWalletApiTest { protected suspend fun jwtProof( audience: CredentialIssuerId, clock: Clock, - nonce: CNonce, + nonce: String, key: ECKey, headerCustomizer: JWSHeader.Builder.() -> Unit = { }, ): SignedJWT { @@ -116,7 +107,7 @@ internal class BaseWalletApiTest { val claims = JWTClaimsSet.Builder() .audience(audience.externalForm) .issueTime(Date.from(clock.instant())) - .claim("nonce", encryptCNonce(nonce)) + .claim("nonce", nonce) .build() val jwt = SignedJWT(header, claims) jwt.sign(ECDSASigner(key)) @@ -297,7 +288,7 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { @Test fun `fails when both proof and proofs is provided`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val key = ECKeyGenerator(Curve.P_256).generate() val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, key) { @@ -322,11 +313,9 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { assertEquals(CredentialErrorTypeTo.INVALID_PROOF, response.type) assertEquals("Only one of `proof` or `proofs` is allowed", response.errorDescription) - val encryptedNewCNonce = assertNotNull(response.nonce) - assertNotNull(response.nonceExpiresIn) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(response.nonce) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(response.nonceExpiresIn) } @Test @@ -357,7 +346,7 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { @Test fun `fails when providing more proofs than allowed batch_size`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val keys = List(5) { ECKeyGenerator(Curve.P_256).generate() } val proofs = keys.map { key -> @@ -381,11 +370,9 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { assertEquals(CredentialErrorTypeTo.INVALID_PROOF, response.type) assertEquals("You can provide at most '3' proofs", response.errorDescription) - val encryptedNewCNonce = assertNotNull(response.nonce) - assertNotNull(response.nonceExpiresIn) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(response.nonce) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(response.nonceExpiresIn) } @Test @@ -394,7 +381,7 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { val keys = List(2) { ECKeyGenerator(Curve.P_256).generate() } val proofs = keys.map { key -> - jwtProof(credentialIssuerMetadata.id, clock, generateCNonce(), key) { + jwtProof(credentialIssuerMetadata.id, clock, generateCNonce(Duration.ofMinutes(5L)), key) { jwk(key.toPublicJWK()) } }.toProofs() @@ -413,7 +400,7 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { .let { assertNotNull(it.responseBody) } assertEquals(CredentialErrorTypeTo.INVALID_PROOF, response.type) - assertEquals("The Proofs of a Credential Request must contain the same CNonce", response.errorDescription) + assertEquals("CNonce is not valid", response.errorDescription) assertNotNull(response.nonce) assertNotNull(response.nonceExpiresIn) } @@ -428,7 +415,7 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { @Test fun `issuance success by format`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val key = ECKeyGenerator(Curve.P_256).generate() val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, key) { @@ -452,11 +439,9 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { assertEquals("PID", issuedCredential.contentOrNull) assertNull(response.transactionId) - val encryptedNewCNonce = assertNotNull(response.nonce) - assertNotNull(response.nonceExpiresIn) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(response.nonce) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(response.nonceExpiresIn) } /** @@ -469,7 +454,7 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { @Test fun `batch issuance success by format`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val keys = List(2) { ECKeyGenerator(Curve.P_256).generate() } val proofs = keys.map { key -> @@ -500,11 +485,9 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { } assertNull(response.transactionId) - val encryptedNewCNonce = assertNotNull(response.nonce) - assertNotNull(response.nonceExpiresIn) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(response.nonce) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(response.nonceExpiresIn) } /** @@ -517,7 +500,7 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { @Test fun `issuance success by credential identifier`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val key = ECKeyGenerator(Curve.P_256).generate() val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, key) { @@ -540,11 +523,9 @@ internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { val issuedCredential = assertIs(response.credential) assertEquals("PID", issuedCredential.contentOrNull) assertNull(response.transactionId) - val encryptedNewCNonce = assertNotNull(response.nonce) - assertNotNull(response.nonceExpiresIn) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(response.nonce) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(response.nonceExpiresIn) } } @@ -570,7 +551,7 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { @Test fun `issuance failure by format when encryption is not requested`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val key = ECKeyGenerator(Curve.P_256).generate() val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, key) { @@ -593,11 +574,9 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { assertEquals(CredentialErrorTypeTo.INVALID_ENCRYPTION_PARAMETERS, response.type) assertEquals("Invalid Credential Response Encryption Parameters", response.errorDescription) - val encryptedNewCNonce = assertNotNull(response.nonce) - assertNotNull(response.nonceExpiresIn) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(response.nonce) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(response.nonceExpiresIn) } /** @@ -610,7 +589,7 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { @Test fun `issuance success by format when encryption is requested`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val walletKey = ECKeyGenerator(Curve.P_256).generate() val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, walletKey) { @@ -646,11 +625,9 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { } assertEquals("PID", claims.getStringClaim("credential")) - val encryptedNewCNonce = assertNotNull(claims.getStringClaim("c_nonce")) - assertNotNull(claims.getLongClaim("c_nonce_expires_in")) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(claims.getStringClaim("c_nonce")) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(claims.getLongClaim("c_nonce_expires_in")) } /** @@ -663,7 +640,7 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { @Test fun `batch issuance success by format when encryption is requested`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val walletKeys = List(2) { ECKeyGenerator(Curve.P_256).generate() } val proofs = walletKeys.map { walletKey -> @@ -706,11 +683,9 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { assertEquals("PID", it) } - val encryptedNewCNonce = assertNotNull(claims.getStringClaim("c_nonce")) - assertNotNull(claims.getLongClaim("c_nonce_expires_in")) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(claims.getStringClaim("c_nonce")) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(claims.getLongClaim("c_nonce_expires_in")) } /** @@ -723,7 +698,7 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { @Test fun `issuance failure by credential identifier when encryption is not requested`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val key = ECKeyGenerator(Curve.P_256).generate() val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, key) { @@ -746,11 +721,9 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { assertEquals(CredentialErrorTypeTo.INVALID_ENCRYPTION_PARAMETERS, response.type) assertEquals("Invalid Credential Response Encryption Parameters", response.errorDescription) - val encryptedNewCNonce = assertNotNull(response.nonce) - assertNotNull(response.nonceExpiresIn) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(response.nonce) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(response.nonceExpiresIn) } /** @@ -763,7 +736,7 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { @Test fun `issuance success by credential identifier when encryption is requested`() = runTest { val authentication = dPoPTokenAuthentication(clock = clock) - val previousCNonce = generateCNonce() + val previousCNonce = generateCNonce(Duration.ofMinutes(5L)) val walletKey = ECKeyGenerator(Curve.P_256).generate() val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, walletKey) { @@ -804,11 +777,9 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { } assertEquals("PID", claims.getStringClaim("credential")) - val encryptedNewCNonce = assertNotNull(claims.getStringClaim("c_nonce")) - assertNotNull(claims.getLongClaim("c_nonce_expires_in")) - - val newCNonce = decryptCNonce(encryptedNewCNonce).getOrThrow() + val newCNonce = assertNotNull(claims.getStringClaim("c_nonce")) assertNotEquals(previousCNonce, newCNonce) + assertNotNull(claims.getLongClaim("c_nonce_expires_in")) } } 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 e5cc4e20..b244dfc1 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 @@ -41,16 +41,11 @@ import java.security.cert.X509Certificate import java.time.Clock import java.util.* import kotlin.test.* -import kotlin.time.Duration.Companion.minutes -import kotlin.time.toJavaDuration internal class ValidateJwtProofTest { private val issuer = CredentialIssuerId.unsafe("https://eudi.ec.europa.eu/issuer") private val clock = Clock.systemDefaultZone() - private val key = RSAKeyGenerator(4096, false).generate() - private val encryptCNonce = EncryptCNonceWithNimbus(issuer, key) - private val decryptCNonce = DecryptCNonceWithNimbus(issuer, key) private val validateJwtProof = ValidateJwtProof(issuer) private val credentialConfiguration = MobileDrivingLicenceV1.copy( proofTypesSupported = ProofTypesSupported( @@ -63,9 +58,8 @@ internal class ValidateJwtProofTest { @Test internal fun `proof validation fails with incorrect 'typ'`() = runTest { val key = loadKey() - val nonce = generateCNonce() val signedJwt = - generateSignedJwt(key, nonce) { + generateSignedJwt(key, "nonce") { type(JOSEObjectType.JWT) jwk(key.toPublicJWK()) } @@ -73,7 +67,6 @@ internal class ValidateJwtProofTest { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) } assert(result.isLeft()) @@ -82,14 +75,12 @@ internal class ValidateJwtProofTest { @Test internal fun `proof validation fails when header contains neither 'jwk' nor 'x5c'`() = runTest { val key = loadKey() - val nonce = generateCNonce() - val signedJwt = generateSignedJwt(key, nonce) + val signedJwt = generateSignedJwt(key, "nonce") val result = either { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) } assert(result.isLeft()) @@ -99,8 +90,7 @@ internal class ValidateJwtProofTest { internal fun `proof validation fails when header contains both 'jwk' and 'x5c'`() = runTest { val key = loadKey() val chain = loadChain() - val nonce = generateCNonce() - val signedJwt = generateSignedJwt(key, nonce) { + val signedJwt = generateSignedJwt(key, "nonce") { jwk(key.toPublicJWK()) x509CertChain(chain.map { Base64.encode(it.encoded) }) } @@ -109,7 +99,6 @@ internal class ValidateJwtProofTest { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) } assertTrue { result.isLeft() } @@ -119,9 +108,8 @@ internal class ValidateJwtProofTest { internal fun `proof validation with 'x5c' in header succeeds`() = runTest { val key = loadKey() val chain = loadChain() - val nonce = generateCNonce() val signedJwt = - generateSignedJwt(key, nonce) { + generateSignedJwt(key, "nonce") { x509CertChain(chain.map { Base64.encode(it.encoded) }) } @@ -129,12 +117,11 @@ internal class ValidateJwtProofTest { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) }.fold( ifLeft = { fail("Unexpected $it", it.cause) }, ifRight = { credentialKey -> - val x5c = assertIs>(credentialKey, "expected 'x5c' credential key") + val x5c = assertIs>(credentialKey, "expected 'x5c' credential key") assertEquals(chain, x5c.first.chain) }, ) @@ -143,9 +130,8 @@ internal class ValidateJwtProofTest { @Test internal fun `proof validation with 'jwk' in header succeeds`() = runTest { val key = loadKey() - val nonce = generateCNonce() val signedJwt = - generateSignedJwt(key, nonce) { + generateSignedJwt(key, "nonce") { jwk(key.toPublicJWK()) } @@ -153,12 +139,11 @@ internal class ValidateJwtProofTest { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) }.fold( ifLeft = { fail("Unexpected $it", it.cause) }, ifRight = { credentialKey -> - val jwk = assertIs>(credentialKey, "expected 'jwk' credential key") + val jwk = assertIs>(credentialKey, "expected 'jwk' credential key") assertEquals(key.toPublicJWK(), jwk.first.value) }, ) @@ -167,9 +152,8 @@ internal class ValidateJwtProofTest { @Test internal fun `proof validation with 'kid' in header succeeds`() = runTest { val key = loadKey() - val nonce = generateCNonce() val signedJwt = - generateSignedJwt(key, nonce) { + generateSignedJwt(key, "nonce") { keyID("did:jwk:${Base64URL.encode(key.toPublicJWK().toJSONString())}#0") } @@ -177,12 +161,11 @@ internal class ValidateJwtProofTest { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) }.fold( ifLeft = { fail("Unexpected $it", it.cause) }, ifRight = { credentialKey -> - val jwk = assertIs>(credentialKey, "expected 'jwk' credential key") + val jwk = assertIs>(credentialKey, "expected 'jwk' credential key") assertEquals(key.toPublicJWK(), jwk.first.jwk) }, ) @@ -192,9 +175,8 @@ internal class ValidateJwtProofTest { internal fun `proof validation fails with incorrect 'jwk' in header`() = runTest { val key = loadKey() val incorrectKey = RSAKeyGenerator(2048, false).generate() - val nonce = generateCNonce() val signedJwt = - generateSignedJwt(key, nonce) { + generateSignedJwt(key, "nonce") { jwk(incorrectKey.toPublicJWK()) } @@ -202,7 +184,6 @@ internal class ValidateJwtProofTest { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) } assertTrue { result.isLeft() } @@ -212,9 +193,8 @@ internal class ValidateJwtProofTest { internal fun `proof validation fails with incorrect 'x5c' in header`() = runTest { val key = loadKey() val incorrectKey = RSAKeyGenerator(2048, false).generate() - val nonce = generateCNonce() val signedJwt = - generateSignedJwt(key, nonce) { + generateSignedJwt(key, "nonce") { x509CertChain(incorrectKey.toPublicJWK().x509CertChain) } @@ -222,7 +202,6 @@ internal class ValidateJwtProofTest { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) } assertTrue { result.isLeft() } @@ -232,26 +211,22 @@ internal class ValidateJwtProofTest { internal fun `proof validation fails with incorrect 'kid' in header`() = runTest { val key = loadKey() val incorrectKey = RSAKeyGenerator(2048, false).generate() - val nonce = generateCNonce() val signedJwt = - generateSignedJwt(key, nonce) { + generateSignedJwt(key, "nonce") { keyID("did:jwk:${Base64URL.encode(incorrectKey.toPublicJWK().toJSONString())}#0") } val result = either { validateJwtProof( UnvalidatedProof.Jwt(signedJwt.serialize()), credentialConfiguration, - decryptCNonce, ) } assertTrue { result.isLeft() } } - private fun generateCNonce(): CNonce = CNonce(UUID.randomUUID().toString(), clock.instant(), 5.minutes.toJavaDuration()) - - private suspend fun generateSignedJwt( + private fun generateSignedJwt( key: RSAKey, - nonce: CNonce, + nonce: String, algorithm: JWSAlgorithm = RSASSASigner.SUPPORTED_ALGORITHMS.first(), headersProvider: JWSHeader.Builder.() -> Unit = {}, ): SignedJWT { @@ -262,9 +237,8 @@ internal class ValidateJwtProofTest { val claims = JWTClaimsSet.Builder() .audience(issuer.externalForm) - .issueTime(Date.from(nonce.activatedAt)) - .expirationTime(Date.from(nonce.activatedAt + nonce.expiresIn)) - .claim("nonce", encryptCNonce(nonce)) + .issueTime(Date.from(clock.instant())) + .claim("nonce", nonce) .build() return SignedJWT(header, claims).apply { sign(RSASSASigner(key)) } diff --git a/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateProofTest.kt b/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateProofTest.kt index 32bd04f5..76c9707a 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateProofTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/ValidateProofTest.kt @@ -29,18 +29,22 @@ class ValidateProofTest { private val issuer = CredentialIssuerId.unsafe("https://eudi.ec.europa.eu/issuer") private val clock = Clock.systemDefaultZone() - private val validateProofs = ValidateProofs(ValidateJwtProof(issuer), clock) { _ -> - fail("ExtractJwkFromCredentialKey should not have been invoked.") - } + private val validateProofs = ValidateProofs( + validateJwtProof = ValidateJwtProof(issuer), + verifyCNonce = { _, _ -> + fail("VerifyCNonce should not have been invoked") + }, + extractJwkFromCredentialKey = { _ -> + fail("ExtractJwkFromCredentialKey should not have been invoked.") + }, + ) @Test internal fun `fails with unsupported proof type`() = runTest { val proof = UnvalidatedProof.LdpVp("foo") val result = either { - validateProofs(nonEmptyListOf(proof), PidMsoMdocV1) { - fail("DecryptCNonce should not have been invoked.") - } + validateProofs(nonEmptyListOf(proof), PidMsoMdocV1, clock.instant()) } assert(result.isLeft())