From 903c30c2eea107f62aeb17e32ac4adefce16e710 Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Fri, 23 Aug 2024 10:42:52 +0300 Subject: [PATCH 1/3] Fix issues with Credential Response Encryption. 1. RequestedResponseEncryption.Required now accepts JWK with keyUse null as well. When keyUse is null, the JWK can be used for all purposes, including encryption. 2. Properly verify during issuance that RequestedResponseEncryption is supported by the issuers configured CredentialResponseEncryption. Previously when CredentialResponseEncryption was set to Required, but the CredentialRequestTO contained no CredentialResponseEncryptionTO, the issuance would continue and an unencrypted credential would be issued, even though the issuer was configured to require encryption. --- .../pidissuer/domain/CredentialRequest.kt | 3 +- .../pidissuer/port/input/IssueCredential.kt | 84 ++--- .../adapter/input/web/WalletApiTest.kt | 305 +++++++++++++++--- 3 files changed, 305 insertions(+), 87 deletions(-) diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt index 055a2d97..51e20465 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt @@ -128,7 +128,8 @@ sealed interface RequestedResponseEncryption { ) : RequestedResponseEncryption { init { require(!encryptionJwk.isPrivate) { "encryptionJwk must not contain a private key" } - require(KeyUse.ENCRYPTION == encryptionJwk.keyUse) { + // When keyUse is null, the JWK can be used for all purposes, including but not limited to ENCRYPTION. + require(encryptionJwk.keyUse == null || encryptionJwk.keyUse == KeyUse.ENCRYPTION) { "encryptionJwk cannot be used for encryption" } require(encryptionAlgorithm in JWEAlgorithm.Family.ASYMMETRIC) { 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 30791d83..0b5bbda5 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 @@ -257,6 +257,7 @@ class IssueCredential( when (unresolvedRequest) { is UnresolvedCredentialRequest.ByFormat -> unresolvedRequest.credentialRequest to null + is UnresolvedCredentialRequest.ByCredentialIdentifier -> resolve(unresolvedRequest) to unresolvedRequest.credentialIdentifier } @@ -370,8 +371,8 @@ private fun CredentialRequestTO.toDomain( supported: CredentialResponseEncryption, ): UnresolvedCredentialRequest { val proof = ensureNotNull(proof) { MissingProof }.toDomain() - val credentialResponseEncryption = - credentialResponseEncryption?.toDomain(supported) ?: RequestedResponseEncryption.NotRequired + val credentialResponseEncryption = credentialResponseEncryption?.toDomain() ?: RequestedResponseEncryption.NotRequired + credentialResponseEncryption.ensureIsSupported(supported) fun credentialRequestByFormat(format: FormatTO): UnresolvedCredentialRequest.ByFormat = when (format) { @@ -459,54 +460,63 @@ private fun ProofTo.toDomain(): UnvalidatedProof = when (type) { } /** - * Gets the [RequestedResponseEncryption] that corresponds to the provided values. + * Verifies this [RequestedResponseEncryption] is supported by the provided [CredentialResponseEncryption], otherwise + * raises an [InvalidEncryptionParameters]. */ context(Raise) -private fun CredentialResponseEncryptionTO.toDomain(supported: CredentialResponseEncryption): RequestedResponseEncryption.Required = - withError({ InvalidEncryptionParameters(it) }) { - fun RequestedResponseEncryption.ensureIsSupported() { - when (supported) { - is CredentialResponseEncryption.NotSupported -> { - if (this is RequestedResponseEncryption.Required) { - // credential response encryption not supported by issuer but required by client - raise(IllegalArgumentException("credential response encryption is not supported")) - } - } - - is CredentialResponseEncryption.Optional -> { - if (this is RequestedResponseEncryption.Required) { - // credential response encryption supported by issuer and required by client - // ensure provided parameters are supported - if (encryptionAlgorithm !in supported.parameters.algorithmsSupported) { - raise(IllegalArgumentException("jwe encryption algorithm '${encryptionAlgorithm.name}' is not supported")) - } - if (encryptionMethod !in supported.parameters.methodsSupported) { - raise(IllegalArgumentException("jwe encryption method '${encryptionMethod.name}' is not supported")) - } - } +private fun RequestedResponseEncryption.ensureIsSupported(supported: CredentialResponseEncryption) { + try { + when (supported) { + is CredentialResponseEncryption.NotSupported -> { + if (this is RequestedResponseEncryption.Required) { + // credential response encryption not supported by issuer but required by client + throw IllegalArgumentException("credential response encryption is not supported") } + } - is CredentialResponseEncryption.Required -> { - if (this !is RequestedResponseEncryption.Required) { - // credential response encryption required by issuer but not required by client - raise(IllegalArgumentException("credential response encryption is required")) - } - + is CredentialResponseEncryption.Optional -> { + if (this is RequestedResponseEncryption.Required) { + // credential response encryption supported by issuer and required by client // ensure provided parameters are supported if (encryptionAlgorithm !in supported.parameters.algorithmsSupported) { - raise(IllegalArgumentException("jwe encryption algorithm '${encryptionAlgorithm.name}' is not supported")) + throw IllegalArgumentException("jwe encryption algorithm '${encryptionAlgorithm.name}' is not supported") } if (encryptionMethod !in supported.parameters.methodsSupported) { - raise(IllegalArgumentException("jwe encryption method '${encryptionMethod.name}' is not supported")) + throw IllegalArgumentException("jwe encryption method '${encryptionMethod.name}' is not supported") } } } - } - RequestedResponseEncryption.Required(Json.encodeToString(key), algorithm, method) - .bind() - .also { it.ensureIsSupported() } + is CredentialResponseEncryption.Required -> { + if (this !is RequestedResponseEncryption.Required) { + // credential response encryption required by issuer but not required by client + throw IllegalArgumentException("credential response encryption is required") + } + + // ensure provided parameters are supported + if (encryptionAlgorithm !in supported.parameters.algorithmsSupported) { + throw IllegalArgumentException("jwe encryption algorithm '${encryptionAlgorithm.name}' is not supported") + } + if (encryptionMethod !in supported.parameters.methodsSupported) { + throw IllegalArgumentException("jwe encryption method '${encryptionMethod.name}' is not supported") + } + } + } + } catch (error: Exception) { + raise(InvalidEncryptionParameters(error)) } +} + +/** + * Gets the [RequestedResponseEncryption] that corresponds to the provided values. + */ +context(Raise) +private fun CredentialResponseEncryptionTO.toDomain(): RequestedResponseEncryption.Required = + RequestedResponseEncryption.Required( + Json.encodeToString(key), + algorithm, + method, + ).getOrElse { raise(InvalidEncryptionParameters(it)) } fun CredentialResponse.toTO(nonce: CNonce): IssueCredentialResponse.PlainTO = when (this) { 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 e81215ad..9422adcd 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 @@ -19,9 +19,13 @@ import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.factories.DefaultJWEDecrypterFactory import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.ECKeyGenerator +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.nimbusds.jwt.EncryptedJWT import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import com.nimbusds.oauth2.sdk.token.DPoPAccessToken @@ -30,14 +34,12 @@ import eu.europa.ec.eudi.pidissuer.adapter.input.web.security.DPoPConfigurationP import eu.europa.ec.eudi.pidissuer.adapter.input.web.security.DPoPTokenAuthentication import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryCNonceRepository 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.domain.* import eu.europa.ec.eudi.pidissuer.port.input.* import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateCNonce import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.contentOrNull import org.springframework.beans.factory.annotation.Autowired @@ -66,26 +68,28 @@ import java.time.Month import java.util.* import kotlin.test.* -@PidIssuerApplicationTest(classes = [WalletApiTest.WalletApiTestConfig::class]) -@TestPropertySource(properties = ["issuer.credentialResponseEncryption.required=false"]) -internal class WalletApiTest { +/** + * Base class for [WalletApi] tests. + */ +@PidIssuerApplicationTest(classes = [BaseWalletApiTest.WalletApiTestConfig::class]) +internal class BaseWalletApiTest { @Autowired - private lateinit var applicationContext: ApplicationContext + protected lateinit var applicationContext: ApplicationContext @Autowired - private lateinit var clock: Clock + protected lateinit var clock: Clock @Autowired - private lateinit var cNonceRepository: InMemoryCNonceRepository + protected lateinit var cNonceRepository: InMemoryCNonceRepository @Autowired - private lateinit var generateCNonce: GenerateCNonce + protected lateinit var generateCNonce: GenerateCNonce @Autowired - private lateinit var credentialIssuerMetadata: CredentialIssuerMetaData + protected lateinit var credentialIssuerMetadata: CredentialIssuerMetaData - private fun client(): WebTestClient = + protected final fun client(): WebTestClient = WebTestClient.bindToApplicationContext(applicationContext) .apply(springSecurity()) .configureClient() @@ -96,6 +100,54 @@ internal class WalletApiTest { cNonceRepository.clear() } + @TestConfiguration + class WalletApiTestConfig { + + @Bean + @Primary + fun dPoPConfigurationProperties(): DPoPConfigurationProperties = + DPoPConfigurationProperties( + emptySet(), + Duration.ofMinutes(1L), + Duration.ofMinutes(10L), + null, + ) + + @Bean + @Primary + fun getPidData(): GetPidData = + GetPidData { + val pid = Pid( + familyName = FamilyName("Surname"), + givenName = GivenName("Firstname"), + birthDate = LocalDate.of(1989, Month.AUGUST, 22), + ageOver18 = true, + ) + val issuingCountry = IsoCountry("GR") + val pidMetaData = PidMetaData( + issuanceDate = LocalDate.now(), + expiryDate = LocalDate.of(2030, 11, 10), + documentNumber = null, + issuingAuthority = IssuingAuthority.MemberState(issuingCountry), + administrativeNumber = null, + issuingCountry = issuingCountry, + issuingJurisdiction = null, + ) + pid to pidMetaData + } + + @Bean + @Primary + fun encodePidInCbor(): EncodePidInCbor = EncodePidInCbor { _, _, _ -> "PID" } + } +} + +/** + * Test cases for [WalletApi] when encryption is optional. + */ +@TestPropertySource(properties = ["issuer.credentialResponseEncryption.required=false"]) +internal class WalletApiEncryptionOptionalTest : BaseWalletApiTest() { + /** * Verifies credential endpoint is not accessible by anonymous users. * No CNonce is expected to be generated. @@ -340,46 +392,190 @@ internal class WalletApiTest { assertEquals(newCNonce.nonce, response.nonce) assertEquals(newCNonce.expiresIn.seconds, response.nonceExpiresIn) } +} - @TestConfiguration - internal class WalletApiTestConfig { +/** + * Test cases for [WalletApi] when encryption is required. + */ +@TestPropertySource(properties = ["issuer.credentialResponseEncryption.required=true"]) +internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { - @Bean - @Primary - fun dPoPConfigurationProperties(): DPoPConfigurationProperties = - DPoPConfigurationProperties( - emptySet(), - Duration.ofMinutes(1L), - Duration.ofMinutes(10L), - null, - ) + /** + * Verifies issuance fails when encryption is not requested. + * Creates a CNonce value before doing the request. + * Does the request. + * Verifies a new CNonce has been generated. + * Verifies response values. + */ + @Test + fun `issuance failure by format when encryption is not requested`() = runTest { + val authentication = dPoPTokenAuthentication(clock = clock) + val previousCNonce = generateCNonce(authentication.accessToken.toAuthorizationHeader(), clock) + cNonceRepository.upsertCNonce(previousCNonce) - @Bean - @Primary - fun getPidData(): GetPidData = - GetPidData { - val pid = Pid( - familyName = FamilyName("Surname"), - givenName = GivenName("Firstname"), - birthDate = LocalDate.of(1989, Month.AUGUST, 22), - ageOver18 = true, - ) - val issuingCountry = IsoCountry("GR") - val pidMetaData = PidMetaData( - issuanceDate = LocalDate.now(), - expiryDate = LocalDate.of(2030, 11, 10), - documentNumber = null, - issuingAuthority = IssuingAuthority.MemberState(issuingCountry), - administrativeNumber = null, - issuingCountry = issuingCountry, - issuingJurisdiction = null, - ) - pid to pidMetaData - } + val key = ECKeyGenerator(Curve.P_256).generate() + val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, key) { + jwk(key.toPublicJWK()) + } - @Bean - @Primary - fun encodePidInCbor(): EncodePidInCbor = EncodePidInCbor { _, _, _ -> "PID" } + val response = client() + .mutateWith(mockAuthentication(authentication)) + .post() + .uri(WalletApi.CREDENTIAL_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestByFormat(proof)) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest() + .expectBody() + .returnResult() + .let { assertNotNull(it.responseBody) } + + val newCNonce = + checkNotNull(cNonceRepository.loadCNonceByAccessToken(authentication.accessToken.toAuthorizationHeader())) + assertNotEquals(previousCNonce, newCNonce) + assertEquals(CredentialErrorTypeTo.INVALID_ENCRYPTION_PARAMETERS, response.type) + assertEquals("Invalid Credential Response Encryption Parameters", response.errorDescription) + assertEquals(newCNonce.nonce, response.nonce) + assertEquals(newCNonce.expiresIn.seconds, response.nonceExpiresIn) + } + + /** + * Verifies issuance succeeds when encryption is requested. + * Creates a CNonce value before doing the request. + * Does the request. + * Verifies a new CNonce has been generated. + * Verifies response values. + */ + @Test + fun `issuance success by format when encryption is requested`() = runTest { + val authentication = dPoPTokenAuthentication(clock = clock) + val previousCNonce = generateCNonce(authentication.accessToken.toAuthorizationHeader(), clock) + cNonceRepository.upsertCNonce(previousCNonce) + + val walletKey = ECKeyGenerator(Curve.P_256).generate() + val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, walletKey) { + jwk(walletKey.toPublicJWK()) + } + val encryptionKey = RSAKeyGenerator(4096).generate() + val encryptionParameters = encryptionParameters(encryptionKey) + + val response = client() + .mutateWith(mockAuthentication(authentication)) + .post() + .uri(WalletApi.CREDENTIAL_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestByFormat(proof, encryptionParameters)) + .accept(MediaType.parseMediaType("application/jwt")) + .exchange() + .expectStatus().isOk() + .expectBody() + .returnResult() + .let { assertNotNull(it.responseBody) } + + val newCNonce = + checkNotNull(cNonceRepository.loadCNonceByAccessToken(authentication.accessToken.toAuthorizationHeader())) + assertNotEquals(previousCNonce, newCNonce) + + val claims = run { + val jwt = EncryptedJWT.parse(response) + .also { + it.decrypt(DefaultJWEDecrypterFactory().createJWEDecrypter(it.header, encryptionKey.toRSAPrivateKey())) + } + jwt.jwtClaimsSet + } + assertEquals("PID", claims.getStringClaim("credential")) + assertEquals(newCNonce.nonce, claims.getStringClaim("c_nonce")) + assertEquals(newCNonce.expiresIn.seconds, claims.getLongClaim("c_nonce_expires_in")) + } + + /** + * Verifies issuance fails when encryption is not requested. + * Creates a CNonce value before doing the request. + * Does the request. + * Verifies a new CNonce has been generated. + * Verifies response values. + */ + @Test + fun `issuance failure by credential identifier when encryption is not requested`() = runTest { + val authentication = dPoPTokenAuthentication(clock = clock) + val previousCNonce = generateCNonce(authentication.accessToken.toAuthorizationHeader(), clock) + cNonceRepository.upsertCNonce(previousCNonce) + + val key = ECKeyGenerator(Curve.P_256).generate() + val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, key) { + jwk(key.toPublicJWK()) + } + + val response = client() + .mutateWith(mockAuthentication(authentication)) + .post() + .uri(WalletApi.CREDENTIAL_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestByCredentialIdentifier(proof)) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest() + .expectBody() + .returnResult() + .let { assertNotNull(it.responseBody) } + + val newCNonce = + checkNotNull(cNonceRepository.loadCNonceByAccessToken(authentication.accessToken.toAuthorizationHeader())) + assertNotEquals(previousCNonce, newCNonce) + assertEquals(CredentialErrorTypeTo.INVALID_ENCRYPTION_PARAMETERS, response.type) + assertEquals("Invalid Credential Response Encryption Parameters", response.errorDescription) + assertEquals(newCNonce.nonce, response.nonce) + assertEquals(newCNonce.expiresIn.seconds, response.nonceExpiresIn) + } + + /** + * Verifies issuance succeeds when encryption is requested. + * Creates a CNonce value before doing the request. + * Does the request. + * Verifies a new CNonce has been generated. + * Verifies response values. + */ + @Test + fun `issuance success by credential identifier when encryption is requested`() = runTest { + val authentication = dPoPTokenAuthentication(clock = clock) + val previousCNonce = generateCNonce(authentication.accessToken.toAuthorizationHeader(), clock) + cNonceRepository.upsertCNonce(previousCNonce) + + val walletKey = ECKeyGenerator(Curve.P_256).generate() + val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, walletKey) { + jwk(walletKey.toPublicJWK()) + } + val encryptionKey = RSAKeyGenerator(4096).generate() + val encryptionParameters = encryptionParameters(encryptionKey) + + val response = client() + .mutateWith(mockAuthentication(authentication)) + .post() + .uri(WalletApi.CREDENTIAL_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestByCredentialIdentifier(proof, encryptionParameters)) + .accept(MediaType.parseMediaType("application/jwt")) + .exchange() + .expectStatus().isOk() + .expectBody() + .returnResult() + .let { assertNotNull(it.responseBody) } + + val newCNonce = + checkNotNull(cNonceRepository.loadCNonceByAccessToken(authentication.accessToken.toAuthorizationHeader())) + assertNotEquals(previousCNonce, newCNonce) + + val claims = run { + val jwt = EncryptedJWT.parse(response) + .also { + it.decrypt(DefaultJWEDecrypterFactory().createJWEDecrypter(it.header, encryptionKey.toRSAPrivateKey())) + } + jwt.jwtClaimsSet + } + assertEquals("PID", claims.getStringClaim("credential")) + assertEquals(newCNonce.nonce, claims.getStringClaim("c_nonce")) + assertEquals(newCNonce.expiresIn.seconds, claims.getLongClaim("c_nonce_expires_in")) } } @@ -419,19 +615,30 @@ private fun dPoPTokenAuthentication( private fun requestByFormat( proof: ProofTo? = ProofTo(type = ProofTypeTO.JWT, jwt = "123456"), + credentialResponseEncryption: CredentialResponseEncryptionTO? = null, ): CredentialRequestTO = CredentialRequestTO( format = FormatTO.MsoMdoc, docType = "eu.europa.ec.eudi.pid.1", proof = proof, + credentialResponseEncryption = credentialResponseEncryption, ) private fun requestByCredentialIdentifier( proof: ProofTo? = ProofTo(type = ProofTypeTO.JWT, jwt = "123456"), + credentialResponseEncryption: CredentialResponseEncryptionTO? = null, ): CredentialRequestTO = CredentialRequestTO( credentialIdentifier = "eu.europa.ec.eudi.pid_mso_mdoc", proof = proof, + credentialResponseEncryption = credentialResponseEncryption, + ) + +private fun encryptionParameters(key: RSAKey): CredentialResponseEncryptionTO = + CredentialResponseEncryptionTO( + key = Json.decodeFromString(key.toPublicJWK().toJSONString()), + algorithm = "RSA-OAEP-256", + method = "A128CBC-HS256", ) private fun jwtProof( From 974e1c015a9c6afd99304c583b48d54d0ad0b61a Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Fri, 23 Aug 2024 11:42:41 +0300 Subject: [PATCH 2/3] Do not accept JWKs with keyUse null for Credential Response Encryption. --- .../eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt | 3 +-- .../ec/eudi/pidissuer/adapter/input/web/WalletApiTest.kt | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt index 51e20465..caff84d3 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialRequest.kt @@ -128,8 +128,7 @@ sealed interface RequestedResponseEncryption { ) : RequestedResponseEncryption { init { require(!encryptionJwk.isPrivate) { "encryptionJwk must not contain a private key" } - // When keyUse is null, the JWK can be used for all purposes, including but not limited to ENCRYPTION. - require(encryptionJwk.keyUse == null || encryptionJwk.keyUse == KeyUse.ENCRYPTION) { + require(encryptionJwk.keyUse == KeyUse.ENCRYPTION) { "encryptionJwk cannot be used for encryption" } require(encryptionAlgorithm in JWEAlgorithm.Family.ASYMMETRIC) { 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 9422adcd..fb0b5f08 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 @@ -22,6 +22,7 @@ import com.nimbusds.jose.crypto.ECDSASigner import com.nimbusds.jose.crypto.factories.DefaultJWEDecrypterFactory import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.KeyUse import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.nimbusds.jose.jwk.gen.RSAKeyGenerator @@ -457,7 +458,7 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, walletKey) { jwk(walletKey.toPublicJWK()) } - val encryptionKey = RSAKeyGenerator(4096).generate() + val encryptionKey = RSAKeyGenerator(4096).keyUse(KeyUse.ENCRYPTION).generate() val encryptionParameters = encryptionParameters(encryptionKey) val response = client() @@ -546,7 +547,7 @@ internal class WalletApiEncryptionRequiredTest : BaseWalletApiTest() { val proof = jwtProof(credentialIssuerMetadata.id, clock, previousCNonce, walletKey) { jwk(walletKey.toPublicJWK()) } - val encryptionKey = RSAKeyGenerator(4096).generate() + val encryptionKey = RSAKeyGenerator(4096).keyUse(KeyUse.ENCRYPTION).generate() val encryptionParameters = encryptionParameters(encryptionKey) val response = client() From efc6dee7868549c53ed3e2fbbed1b111c131446a Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Fri, 23 Aug 2024 12:33:34 +0300 Subject: [PATCH 3/3] Express checks using ensure instead of using try/catch/throw/raise. --- .../pidissuer/port/input/IssueCredential.kt | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) 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 0b5bbda5..34a1aac1 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 @@ -465,45 +465,49 @@ private fun ProofTo.toDomain(): UnvalidatedProof = when (type) { */ context(Raise) private fun RequestedResponseEncryption.ensureIsSupported(supported: CredentialResponseEncryption) { - try { - when (supported) { - is CredentialResponseEncryption.NotSupported -> { - if (this is RequestedResponseEncryption.Required) { - // credential response encryption not supported by issuer but required by client - throw IllegalArgumentException("credential response encryption is not supported") - } + when (supported) { + is CredentialResponseEncryption.NotSupported -> { + ensure(this !is RequestedResponseEncryption.Required) { + // credential response encryption not supported by issuer but required by client + InvalidEncryptionParameters(IllegalArgumentException("credential response encryption is not supported")) } + } - is CredentialResponseEncryption.Optional -> { - if (this is RequestedResponseEncryption.Required) { - // credential response encryption supported by issuer and required by client - // ensure provided parameters are supported - if (encryptionAlgorithm !in supported.parameters.algorithmsSupported) { - throw IllegalArgumentException("jwe encryption algorithm '${encryptionAlgorithm.name}' is not supported") - } - if (encryptionMethod !in supported.parameters.methodsSupported) { - throw IllegalArgumentException("jwe encryption method '${encryptionMethod.name}' is not supported") - } + is CredentialResponseEncryption.Optional -> { + if (this is RequestedResponseEncryption.Required) { + // credential response encryption supported by issuer and required by client + // ensure provided parameters are supported + ensure(encryptionAlgorithm in supported.parameters.algorithmsSupported) { + InvalidEncryptionParameters( + IllegalArgumentException("jwe encryption algorithm '${encryptionAlgorithm.name}' is not supported"), + ) + } + ensure(encryptionMethod in supported.parameters.methodsSupported) { + InvalidEncryptionParameters( + IllegalArgumentException("jwe encryption method '${encryptionMethod.name}' is not supported"), + ) } } + } - is CredentialResponseEncryption.Required -> { - if (this !is RequestedResponseEncryption.Required) { - // credential response encryption required by issuer but not required by client - throw IllegalArgumentException("credential response encryption is required") - } + is CredentialResponseEncryption.Required -> { + ensure(this is RequestedResponseEncryption.Required) { + // credential response encryption required by issuer but not required by client + InvalidEncryptionParameters(IllegalArgumentException("credential response encryption is required")) + } - // ensure provided parameters are supported - if (encryptionAlgorithm !in supported.parameters.algorithmsSupported) { - throw IllegalArgumentException("jwe encryption algorithm '${encryptionAlgorithm.name}' is not supported") - } - if (encryptionMethod !in supported.parameters.methodsSupported) { - throw IllegalArgumentException("jwe encryption method '${encryptionMethod.name}' is not supported") - } + // ensure provided parameters are supported + ensure(encryptionAlgorithm in supported.parameters.algorithmsSupported) { + InvalidEncryptionParameters( + IllegalArgumentException("jwe encryption algorithm '${encryptionAlgorithm.name}' is not supported"), + ) + } + ensure(encryptionMethod in supported.parameters.methodsSupported) { + InvalidEncryptionParameters( + IllegalArgumentException("jwe encryption method '${encryptionMethod.name}' is not supported"), + ) } } - } catch (error: Exception) { - raise(InvalidEncryptionParameters(error)) } }