diff --git a/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/CWTProofSignerTest.kt b/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/CWTProofSignerTest.kt index a74a9f0d..70fbf7af 100644 --- a/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/CWTProofSignerTest.kt +++ b/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/CWTProofSignerTest.kt @@ -21,11 +21,11 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import eu.europa.ec.eudi.openid4vci.* -import eu.europa.ec.eudi.wallet.document.Constants.EU_PID_DOCTYPE import eu.europa.ec.eudi.wallet.document.CreateIssuanceRequestResult import eu.europa.ec.eudi.wallet.document.DocumentManager import eu.europa.ec.eudi.wallet.issue.openid4vci.CWTProofSigner import eu.europa.ec.eudi.wallet.issue.openid4vci.ProofSigner +import eu.europa.ec.eudi.wallet.issue.openid4vci.SupportedProofAlgorithm import eu.europa.ec.eudi.wallet.issue.openid4vci.UserAuthRequiredException import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Assert @@ -67,13 +67,13 @@ class CWTProofSignerTest { .build() - val issuanceRequestResult = documentManager.createIssuanceRequest(EU_PID_DOCTYPE, false) + val issuanceRequestResult = documentManager.createIssuanceRequest("eu.europa.ec.eudiw.pid.1", false) assertTrue(issuanceRequestResult is CreateIssuanceRequestResult.Success) val issuanceRequest = (issuanceRequestResult as CreateIssuanceRequestResult.Success).issuanceRequest - val proofSigner = CWTProofSigner(issuanceRequest, CoseAlgorithm.ES256, CoseCurve.P_256) + val proofSigner = CWTProofSigner(issuanceRequest, SupportedProofAlgorithm.Cose.ES256_P_256) assertEquals(CoseAlgorithm.ES256, proofSigner.popSigner.algorithm) assertEquals(CoseCurve.P_256, proofSigner.popSigner.curve) @@ -97,13 +97,13 @@ class CWTProofSignerTest { .build() - val issuanceRequestResult = documentManager.createIssuanceRequest(EU_PID_DOCTYPE, false) + val issuanceRequestResult = documentManager.createIssuanceRequest("eu.europa.ec.eudiw.pid.1", false) assertTrue(issuanceRequestResult is CreateIssuanceRequestResult.Success) val issuanceRequest = (issuanceRequestResult as CreateIssuanceRequestResult.Success).issuanceRequest - val proofSigner = CWTProofSigner(issuanceRequest, CoseAlgorithm.ES256, CoseCurve.P_256) + val proofSigner = CWTProofSigner(issuanceRequest, SupportedProofAlgorithm.Cose.ES256_P_256) assertEquals(CoseAlgorithm.ES256, proofSigner.popSigner.algorithm) assertEquals(CoseCurve.P_256, proofSigner.popSigner.curve) diff --git a/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/JWSProofSignerTest.kt b/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/JwsProofSignerTest.kt similarity index 93% rename from wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/JWSProofSignerTest.kt rename to wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/JwsProofSignerTest.kt index 6a8bc021..80dc6073 100644 --- a/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/JWSProofSignerTest.kt +++ b/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/JwsProofSignerTest.kt @@ -21,18 +21,17 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.nimbusds.jose.JOSEObjectType -import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSAVerifier import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import eu.europa.ec.eudi.openid4vci.CNonce import eu.europa.ec.eudi.openid4vci.JwtBindingKey -import eu.europa.ec.eudi.wallet.document.Constants.EU_PID_DOCTYPE import eu.europa.ec.eudi.wallet.document.CreateIssuanceRequestResult import eu.europa.ec.eudi.wallet.document.DocumentManager import eu.europa.ec.eudi.wallet.issue.openid4vci.JWSProofSigner import eu.europa.ec.eudi.wallet.issue.openid4vci.ProofSigner +import eu.europa.ec.eudi.wallet.issue.openid4vci.SupportedProofAlgorithm import eu.europa.ec.eudi.wallet.issue.openid4vci.UserAuthRequiredException import org.junit.Assert import org.junit.Assert.assertEquals @@ -69,13 +68,13 @@ class JWSProofSignerTest { .build() - val issuanceRequestResult = documentManager.createIssuanceRequest(EU_PID_DOCTYPE, false) + val issuanceRequestResult = documentManager.createIssuanceRequest("eu.europa.ec.eudiw.pid.1", false) assertTrue(issuanceRequestResult is CreateIssuanceRequestResult.Success) val issuanceRequest = (issuanceRequestResult as CreateIssuanceRequestResult.Success).issuanceRequest - val proofSigner = JWSProofSigner(issuanceRequest, JWSAlgorithm.ES256) + val proofSigner = JWSProofSigner(issuanceRequest, SupportedProofAlgorithm.Jws.ES256) val algorithm = proofSigner.popSigner.algorithm assertTrue(proofSigner.popSigner.bindingKey is JwtBindingKey.Jwk) @@ -108,13 +107,13 @@ class JWSProofSignerTest { .enableUserAuth(false) .build() - val issuanceRequestResult = documentManager.createIssuanceRequest(EU_PID_DOCTYPE, false) + val issuanceRequestResult = documentManager.createIssuanceRequest("eu.europa.ec.eudiw.pid.1", false) assertTrue(issuanceRequestResult is CreateIssuanceRequestResult.Success) val issuanceRequest = (issuanceRequestResult as CreateIssuanceRequestResult.Success).issuanceRequest - val proofSigner = JWSProofSigner(issuanceRequest, JWSAlgorithm.ES256) + val proofSigner = JWSProofSigner(issuanceRequest, SupportedProofAlgorithm.Jws.ES256) val algorithm = proofSigner.popSigner.algorithm assertTrue(proofSigner.popSigner.bindingKey is JwtBindingKey.Jwk) diff --git a/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/ProofSigner1Test.kt b/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/ProofSigner1Test.kt deleted file mode 100644 index 9d0af329..00000000 --- a/wallet-core/src/androidTest/java/eu/europa/ec/eudi/wallet/document/issue/opeid4vci/ProofSigner1Test.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2023-2024 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. - */ \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/document/Constants.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/document/Constants.kt index 3157c529..7c222d64 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/document/Constants.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/document/Constants.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2023 European Commission + * Copyright (c) 2023-2024 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 + * 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 + * 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. + * 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.wallet.document @@ -21,6 +21,7 @@ package eu.europa.ec.eudi.wallet.document * * @constructor Create empty Constants */ +@Deprecated(message = "Will be removed completely in following version. Replace with actual values.") object Constants { // EU PID diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CWTProofSigner.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CWTProofSigner.kt index 970b36d8..8f02e0e2 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CWTProofSigner.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CWTProofSigner.kt @@ -17,11 +17,8 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import com.nimbusds.jose.jwk.JWK -import eu.europa.ec.eudi.openid4vci.CoseAlgorithm -import eu.europa.ec.eudi.openid4vci.CoseCurve import eu.europa.ec.eudi.openid4vci.CwtBindingKey import eu.europa.ec.eudi.openid4vci.PopSigner -import eu.europa.ec.eudi.wallet.document.Algorithm import eu.europa.ec.eudi.wallet.document.IssuanceRequest /** @@ -36,8 +33,7 @@ import eu.europa.ec.eudi.wallet.document.IssuanceRequest */ internal class CWTProofSigner( private val issuanceRequest: IssuanceRequest, - private val coseAlgorithm: CoseAlgorithm, - private val coseCurve: CoseCurve + private val supportedProofAlgorithm: SupportedProofAlgorithm.Cose ) : ProofSigner() { /** @@ -45,17 +41,12 @@ internal class CWTProofSigner( */ private val jwk = JWK.parseFromPEMEncodedObjects(issuanceRequest.publicKey.pem) override val popSigner: PopSigner.Cwt = PopSigner.Cwt( - algorithm = coseAlgorithm, - curve = coseCurve, + algorithm = supportedProofAlgorithm.coseAlgorithm, + curve = supportedProofAlgorithm.coseCurve, bindingKey = CwtBindingKey.CoseKey(jwk), sign = this::sign ) - private val algorithm - get() = Pair(coseAlgorithm, coseCurve).let { - CWTAlgorithmMap[it] ?: throw UnsupportedAlgorithmException() - } - /** * Signs the signing input with the authentication key from issuance request for the given algorithm and curve. * @param signingInput The input to sign. @@ -64,12 +55,6 @@ internal class CWTProofSigner( * @return The signature of the signing input. */ fun sign(signingInput: ByteArray): ByteArray { - return doSign(issuanceRequest, signingInput, algorithm) - } - - companion object { - private val CWTAlgorithmMap = mapOf( - Pair(CoseAlgorithm.ES256, CoseCurve.P_256) to Algorithm.SHA256withECDSA, - ) + return doSign(issuanceRequest, signingInput, supportedProofAlgorithm.signAlgorithmName) } } \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt new file mode 100644 index 00000000..39eede76 --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 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.wallet.issue.openid4vci + +import eu.europa.ec.eudi.openid4vci.CredentialConfiguration +import eu.europa.ec.eudi.openid4vci.MsoMdocCredential +import kotlin.reflect.KClass + +internal fun interface CredentialConfigurationFilter { + operator fun invoke(conf: CredentialConfiguration): Boolean + + /** + * + */ + class DocTypeFilterFactory(private val docType: String) : CredentialConfigurationFilter { + override fun invoke(conf: CredentialConfiguration): Boolean { + return conf is MsoMdocCredential && conf.docType == docType + } + } + + class FormatFilterFactory(private val format: KClass) : + CredentialConfigurationFilter { + override fun invoke(conf: CredentialConfiguration): Boolean { + return format.isInstance(conf) + } + } + + /** + * Default [CredentialConfigurationFilter] implementations for credential format and proof type. + * @property MsoMdocFormatFilter filter for credential format + * @property ProofTypeFilter filter for proof type + */ + companion object { + @JvmSynthetic + internal val MsoMdocFormatFilter: CredentialConfigurationFilter = + FormatFilterFactory(MsoMdocCredential::class) + + @JvmSynthetic + internal val ProofTypeFilter: CredentialConfigurationFilter = CredentialConfigurationFilter { conf -> + SupportedProofType.selectProofType(conf)?.selectAlgorithm(conf)?.let { true } ?: false + } + + + } +} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt index b65b5b42..823a6ca1 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt @@ -27,6 +27,9 @@ import eu.europa.ec.eudi.wallet.document.DocumentId import eu.europa.ec.eudi.wallet.document.DocumentManager import eu.europa.ec.eudi.wallet.document.IssuanceRequest import eu.europa.ec.eudi.wallet.internal.mainExecutor +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.MsoMdocFormatFilter +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.ProofTypeFilter +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.DocTypeFilterFactory import eu.europa.ec.eudi.wallet.issue.openid4vci.IssueEvent.Companion.failure import eu.europa.ec.eudi.wallet.issue.openid4vci.ProofSigner.UserAuthStatus import kotlinx.coroutines.* @@ -59,7 +62,7 @@ internal class DefaultOpenId4VciManager( val credentialConfigurationId = credentialIssuerMetadata.credentialConfigurationsSupported.filter { (_, conf) -> listOf( - FormatFilter, + MsoMdocFormatFilter, DocTypeFilterFactory(docType), ProofTypeFilter ).all { filter -> filter(conf) } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Exceptions.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Exceptions.kt index 119de4a9..94b0f2dc 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Exceptions.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Exceptions.kt @@ -16,6 +16,32 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci +import com.nimbusds.jose.JWSAlgorithm +import eu.europa.ec.eudi.openid4vci.CoseAlgorithm +import eu.europa.ec.eudi.openid4vci.CoseCurve +import eu.europa.ec.eudi.openid4vci.ProofType + internal class UserAuthRequiredException : Throwable() -class UnsupportedAlgorithmException : Throwable() -class UnsupportedProofTypeException : Throwable() \ No newline at end of file +class UnsupportedAlgorithmException internal constructor(supportedAlgorithms: Collection? = null) : Throwable( + message = supportedAlgorithms?.joinToString(", ") { + when (it) { + is Pair<*, *> -> when (val alg = it.first) { + is CoseAlgorithm -> when (val crv = it.second) { + is CoseCurve -> "${alg.name()}:${crv.name()}" + else -> "" + } + + else -> null + } + + is JWSAlgorithm -> it.name + is String -> it + else -> null + }?.let { msg -> "Supported algorithms are: $msg" } ?: "" + } +) + +class UnsupportedProofTypeException internal constructor(supportedProofTypes: Collection? = null) : + Throwable( + message = "Supported proof types are: " + supportedProofTypes?.joinToString(", ") { it.name } + ) \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/JWSProofSigner.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/JWSProofSigner.kt index 2a9201a9..f8627e0a 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/JWSProofSigner.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/JWSProofSigner.kt @@ -26,9 +26,6 @@ import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.util.Base64URL import eu.europa.ec.eudi.openid4vci.JwtBindingKey import eu.europa.ec.eudi.openid4vci.PopSigner -import eu.europa.ec.eudi.openid4vci.ProofType -import eu.europa.ec.eudi.openid4vci.ProofTypeMeta -import eu.europa.ec.eudi.wallet.document.Algorithm import eu.europa.ec.eudi.wallet.document.IssuanceRequest /** @@ -41,7 +38,7 @@ import eu.europa.ec.eudi.wallet.document.IssuanceRequest */ internal class JWSProofSigner( private val issuanceRequest: IssuanceRequest, - jwsAlgorithm: JWSAlgorithm + private val supportedProofAlgorithm: SupportedProofAlgorithm.Jws ) : ProofSigner(), JWSSigner { private val jcaContext = JCAContext() @@ -52,7 +49,7 @@ internal class JWSProofSigner( private val jwk = JWK.parseFromPEMEncodedObjects(issuanceRequest.publicKey.pem) override val popSigner: PopSigner.Jwt = PopSigner.Jwt( - algorithm = jwsAlgorithm, + algorithm = supportedProofAlgorithm.algorithm, bindingKey = JwtBindingKey.Jwk(jwk), jwsSigner = this ) @@ -60,24 +57,21 @@ internal class JWSProofSigner( override fun getJCAContext(): JCAContext = jcaContext override fun supportedJWSAlgorithms(): MutableSet { - return (SupportedProofTypes[ProofType.JWT] as ProofTypeMeta.Jwt).algorithms.toMutableSet() + return SupportedProofType.SupportedProofTypes.filterIsInstance() + .firstOrNull()?.jwsAlgorithms?.toMutableSet() ?: mutableSetOf(supportedProofAlgorithm.algorithm) } override fun sign(header: JWSHeader, signingInput: ByteArray): Base64URL { - val alg = JWSAlgorithmsMap[header.algorithm] ?: throw JOSEException( - AlgorithmSupportMessage.unsupportedJWSAlgorithm( - header.algorithm, - supportedJWSAlgorithms() + if (header.algorithm != supportedProofAlgorithm.algorithm) { + throw JOSEException( + AlgorithmSupportMessage.unsupportedJWSAlgorithm( + header.algorithm, + supportedJWSAlgorithms() + ) ) - ) - return doSign(issuanceRequest, signingInput, alg).let { signature -> + } + return doSign(issuanceRequest, signingInput, supportedProofAlgorithm.signAlgorithmName).let { signature -> Base64URL.encode(signature.derToJose(header.algorithm)) } } - - companion object { - private val JWSAlgorithmsMap = mapOf( - JWSAlgorithm.ES256 to Algorithm.SHA256withECDSA, - ) - } } \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt index 38f19231..4aef24d6 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt @@ -22,6 +22,8 @@ import eu.europa.ec.eudi.openid4vci.CredentialConfiguration import eu.europa.ec.eudi.openid4vci.CredentialConfigurationIdentifier import eu.europa.ec.eudi.openid4vci.CredentialOffer import eu.europa.ec.eudi.openid4vci.MsoMdocCredential +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.MsoMdocFormatFilter +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.ProofTypeFilter /** * An offer of credentials to be issued. @@ -53,7 +55,10 @@ interface Offer { internal data class DefaultOffer( @JvmSynthetic val credentialOffer: CredentialOffer, - @JvmSynthetic val filterConfigurations: List = listOf(FormatFilter, ProofTypeFilter) + @JvmSynthetic val filterConfigurations: List = listOf( + MsoMdocFormatFilter, + ProofTypeFilter + ) ) : Offer { override val issuerName: String diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSigner.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSigner.kt index ed7e4864..a80b8c65 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSigner.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSigner.kt @@ -17,8 +17,9 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import androidx.biometric.BiometricPrompt.CryptoObject -import com.nimbusds.jose.JWSAlgorithm -import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.openid4vci.CredentialConfiguration +import eu.europa.ec.eudi.openid4vci.PopSigner +import eu.europa.ec.eudi.openid4vci.ProofTypesSupported import eu.europa.ec.eudi.wallet.document.Algorithm import eu.europa.ec.eudi.wallet.document.IssuanceRequest import eu.europa.ec.eudi.wallet.document.SignedWithAuthKeyResult @@ -43,7 +44,7 @@ internal abstract class ProofSigner { * @throws Throwable If an error occurs during signing. * @return The signature of the signing input. */ - protected fun doSign( + fun doSign( issuanceRequest: IssuanceRequest, signingInput: ByteArray, @Algorithm algorithm: String @@ -63,58 +64,8 @@ internal abstract class ProofSigner { /** * Companion object for the ProofSigner class. - * @property SupportedProofTypes The supported proof types. */ companion object { - val SupportedProofTypes = mapOf( - ProofType.JWT to ProofTypeMeta.Jwt(listOf(JWSAlgorithm.ES256)), - ProofType.CWT to ProofTypeMeta.Cwt(listOf(CoseAlgorithm.ES256), listOf(CoseCurve.P_256)) - ) - - /** - * Selects the supported proof type from the issuer supported proof types. - * @param issuerSupportedProofTypes The issuer supported proof types. - * @return The supported proof type or null if none is supported. - */ - @JvmStatic - fun selectSupportedProofType(issuerSupportedProofTypes: ProofTypesSupported): ProofTypeMeta? { - return issuerSupportedProofTypes.values - .firstOrNull { it.type() in SupportedProofTypes.keys } - ?.let { proofType -> - when (proofType.type()) { - ProofType.JWT -> selectSupportedAlgorithm(proofType as ProofTypeMeta.Jwt)?.let { proofType } - ProofType.CWT -> selectSupportedAlgorithm(proofType as ProofTypeMeta.Cwt)?.let { proofType } - ProofType.LDP_VP -> null - } - } - } - - /** - * Selects the supported algorithm from given JWT proof type metadata. - * @param metadata The proof type metadata. - * @return The supported algorithm or null if none is supported. - */ - @JvmStatic - fun selectSupportedAlgorithm(metadata: ProofTypeMeta.Jwt): JWSAlgorithm? { - val supportedType = SupportedProofTypes[ProofType.JWT] as ProofTypeMeta.Jwt - return supportedType.algorithms.firstOrNull { it in metadata.algorithms } - } - - /** - * Selects the supported algorithm and curve from given CWT proof type metadata. - * @param metadata The proof type metadata. - * @return The supported algorithm and curve or null if none is supported. - */ - @JvmStatic - fun selectSupportedAlgorithm(metadata: ProofTypeMeta.Cwt): Pair? { - val supportedType = SupportedProofTypes[ProofType.CWT] as ProofTypeMeta.Cwt - return supportedType.algorithms.firstOrNull { it in metadata.algorithms } - ?.let { algorithm -> - supportedType.curves.firstOrNull { it in metadata.curves } - ?.let { curve -> algorithm to curve } - } - - } /** * Creates a proof signer for the given issuance request and credential configuration. @@ -130,7 +81,7 @@ internal abstract class ProofSigner { /** * Creates a proof signer for the given issuance request and issuer supported proof types. - * The proof signer is selected based on the issuer supported proof types and the [SupportedProofTypes] + * The proof signer is selected based on the issuer supported proof types and the [SupportedProofType.SupportedProofTypes] * of the [ProofSigner]. * @param issuanceRequest The issuance request. * @param issuerSupportedProofTypes The issuer supported proof types. @@ -141,22 +92,18 @@ internal abstract class ProofSigner { issuanceRequest: IssuanceRequest, issuerSupportedProofTypes: ProofTypesSupported, ): Result { - val selectedProofType = selectSupportedProofType(issuerSupportedProofTypes) + val selectedProofType = SupportedProofType.selectProofType(issuerSupportedProofTypes) ?: return Result.failure(UnsupportedProofTypeException()) - return when (selectedProofType) { - is ProofTypeMeta.Jwt -> selectSupportedAlgorithm(selectedProofType)?.let { alg -> - JWSProofSigner(issuanceRequest, alg) - } - - is ProofTypeMeta.Cwt -> selectSupportedAlgorithm(selectedProofType)?.let { (alg, crv) -> - CWTProofSigner(issuanceRequest, alg, crv) - } - - else -> null // should never happen. LDP_VP is not supported in [SupportedProofTypes] - }?.let { Result.success(it) } - ?: Result.failure(UnsupportedAlgorithmException()) + val supportedAlgorithm = + selectedProofType.selectAlgorithm(issuerSupportedProofTypes) ?: return Result.failure( + UnsupportedAlgorithmException() + ) + return when (supportedAlgorithm) { + is SupportedProofAlgorithm.Cose -> CWTProofSigner(issuanceRequest, supportedAlgorithm) + is SupportedProofAlgorithm.Jws -> JWSProofSigner(issuanceRequest, supportedAlgorithm) + }.let { Result.success(it) } } } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofType.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofType.kt new file mode 100644 index 00000000..4a0a1e85 --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofType.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024 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. + */ +@file:JvmMultifileClass + +package eu.europa.ec.eudi.wallet.issue.openid4vci + +import com.nimbusds.jose.JWSAlgorithm +import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.wallet.document.Algorithm +import eu.europa.ec.eudi.wallet.issue.openid4vci.SupportedProofAlgorithm.Cose +import eu.europa.ec.eudi.wallet.issue.openid4vci.SupportedProofAlgorithm.Jws + +internal sealed interface SupportedProofType { + val algorithms: List + + data class Jwt(override val algorithms: List) : SupportedProofType { + val jwsAlgorithms: List + get() = algorithms.map { it.algorithm }.toList() + + override fun toString(): String { + return name + } + } + + data class Cwt(override val algorithms: List) : SupportedProofType { + override fun toString(): String { + return name + } + + val coseAlgorithms: List + get() = algorithms.map { it.coseAlgorithm }.toList() + + val coseCurves: List + get() = algorithms.map { it.coseCurve }.toList() + } + + val proofType: ProofType + get() = when (this) { + is Jwt -> ProofType.JWT + is Cwt -> ProofType.CWT + } + + val proofTypeMeta: ProofTypeMeta + get() = when (this) { + is Jwt -> ProofTypeMeta.Jwt(algorithms.map { it.algorithm }) + is Cwt -> ProofTypeMeta.Cwt(algorithms.map { it.coseAlgorithm }, algorithms.map { it.coseCurve }) + } + + val name: String + get() = when (this) { + is Jwt -> "JWT for ${algorithms.map { it.name }}" + is Cwt -> "CWT for ${algorithms.map { it.name }}" + } + + fun selectAlgorithm(issuerProofTypesSupported: ProofTypesSupported): SupportedProofAlgorithm? { + val issuerSupported = issuerProofTypesSupported.values.associateBy { it.type() } + val credentialProof = issuerSupported[proofType] ?: return null + + return when (this) { + is Jwt -> credentialProof.takeIf { it is ProofTypeMeta.Jwt } + ?.let { it as ProofTypeMeta.Jwt } + ?.let { cProof -> algorithms.firstOrNull { it.algorithm in cProof.algorithms } } + + is Cwt -> credentialProof.takeIf { it is ProofTypeMeta.Cwt } + ?.let { it as ProofTypeMeta.Cwt } + ?.let { cProof -> + algorithms.firstOrNull { it.coseAlgorithm in cProof.algorithms && it.coseCurve in cProof.curves } + } + } + } + + fun selectAlgorithm(credentialConfiguration: CredentialConfiguration): SupportedProofAlgorithm? = + selectAlgorithm(credentialConfiguration.proofTypesSupported) + + companion object { + val SupportedProofTypes: List = listOf( + Jwt(listOf(Jws.ES256)), + Cwt(listOf(Cose.ES256_P_256)) + ) + + @JvmStatic + fun selectProofType(issuerProofTypesSupported: ProofTypesSupported): SupportedProofType? { + val issuerSupported = issuerProofTypesSupported.values.associateBy { it.type() } + return SupportedProofTypes.firstOrNull { it.proofType in issuerSupported.keys } + } + + @JvmStatic + fun selectProofType(credentialConfiguration: CredentialConfiguration): SupportedProofType? = + selectProofType(credentialConfiguration.proofTypesSupported) + } +} + +sealed interface SupportedProofAlgorithm { + val name: String + + @get:Algorithm + val signAlgorithmName: String + + data class Jws(val algorithm: JWSAlgorithm, @Algorithm override val signAlgorithmName: String) : + SupportedProofAlgorithm { + override val name: String + get() = algorithm.name + + companion object { + val ES256 = Jws(JWSAlgorithm.ES256, Algorithm.SHA256withECDSA) + } + } + + data class Cose( + val coseAlgorithm: CoseAlgorithm, + val coseCurve: CoseCurve, + @Algorithm override val signAlgorithmName: String + ) : SupportedProofAlgorithm { + override val name: String + get() = "${coseAlgorithm.name()} with curve ${coseCurve.name()}" + + companion object { + val ES256_P_256 = Cose(CoseAlgorithm.ES256, CoseCurve.P_256, Algorithm.SHA256withECDSA) + } + } + + +} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Utils.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Utils.kt index 9390c0bd..5d3e0c91 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Utils.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Utils.kt @@ -18,37 +18,14 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.crypto.impl.ECDSA -import eu.europa.ec.eudi.openid4vci.CredentialConfiguration -import eu.europa.ec.eudi.openid4vci.MsoMdocCredential import eu.europa.ec.eudi.wallet.document.CreateIssuanceRequestResult import eu.europa.ec.eudi.wallet.document.DocumentManager import eu.europa.ec.eudi.wallet.document.IssuanceRequest -import eu.europa.ec.eudi.wallet.issue.openid4vci.ProofSigner.Companion.selectSupportedProofType import org.bouncycastle.util.io.pem.PemObject import org.bouncycastle.util.io.pem.PemWriter import java.io.StringWriter import java.security.PublicKey -internal fun interface CredentialConfigurationFilter { - operator fun invoke(conf: CredentialConfiguration): Boolean -} - -@JvmSynthetic -internal val FormatFilter: CredentialConfigurationFilter = CredentialConfigurationFilter { conf -> - conf is MsoMdocCredential -} - -@JvmSynthetic -internal val ProofTypeFilter: CredentialConfigurationFilter = CredentialConfigurationFilter { conf -> - selectSupportedProofType(conf.proofTypesSupported) != null -} - -internal class DocTypeFilterFactory(private val docType: String) : CredentialConfigurationFilter { - override fun invoke(conf: CredentialConfiguration): Boolean { - return conf is MsoMdocCredential && conf.docType == docType - } -} - internal val CreateIssuanceRequestResult.result: Result @JvmSynthetic get() = when (this) { is CreateIssuanceRequestResult.Success -> Result.success(issuanceRequest) diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/documentsTest/util/DocType.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/documentsTest/util/DocType.kt index c2f9edde..9a4df7c9 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/documentsTest/util/DocType.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/documentsTest/util/DocType.kt @@ -1,23 +1,22 @@ /* - * Copyright (c) 2023 European Commission + * Copyright (c) 2023-2024 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 + * 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 + * 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. + * 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.wallet.documentsTest.util import eu.europa.ec.eudi.wallet.R -import eu.europa.ec.eudi.wallet.document.Constants as DocConstants /** * Document Type, e.g. MDL, PID, MICOV, MVR @@ -33,14 +32,14 @@ enum class DocType(val docTypeName: String, val userFriendlyName: Int) { * * @constructor Create empty Pid */ - PID(DocConstants.EU_PID_DOCTYPE, R.string.eu_pid_doctype_name), + PID("eu.europa.ec.eudiw.pid.1", R.string.eu_pid_doctype_name), /** * MDL Doc Type * * @constructor Create empty Mdl */ - MDL(DocConstants.MDL_DOCTYPE, R.string.mdl_doctype_name); + MDL("org.iso.18013.5.1.mDL", R.string.mdl_doctype_name); companion object { infix fun from(name: String): DocType? = diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt index 63111910..2cbbe950 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt @@ -21,6 +21,9 @@ import eu.europa.ec.eudi.openid4vci.MsoMdocCredential import eu.europa.ec.eudi.openid4vci.ProofTypeMeta import eu.europa.ec.eudi.openid4vci.ProofTypesSupported import eu.europa.ec.eudi.openid4vci.SdJwtVcCredential +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.MsoMdocFormatFilter +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.ProofTypeFilter +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.DocTypeFilterFactory import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertFalse @@ -32,13 +35,13 @@ class CredentialConfigurationFilterTest { @Test fun `FormatFilter returns true for MsoMdocCredential`() { val credentialConfiguration = mockk(relaxed = true) - assertTrue(FormatFilter(credentialConfiguration)) + assertTrue(MsoMdocFormatFilter(credentialConfiguration)) } @Test fun `FormatFilter returns false for non-MsoMdocCredential`() { val credentialConfiguration = mockk(relaxed = true) - assertFalse(FormatFilter(credentialConfiguration)) + assertFalse(MsoMdocFormatFilter(credentialConfiguration)) } diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSignerTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSignerTest.kt new file mode 100644 index 00000000..3132acfe --- /dev/null +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSignerTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023-2024 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. + */ + +import androidx.biometric.BiometricPrompt.CryptoObject +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.jwk.JWK +import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.wallet.document.Algorithm +import eu.europa.ec.eudi.wallet.document.IssuanceRequest +import eu.europa.ec.eudi.wallet.document.SignedWithAuthKeyResult +import eu.europa.ec.eudi.wallet.issue.openid4vci.* +import eu.europa.ec.eudi.wallet.issue.openid4vci.ProofSigner +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.Assert.* +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test + +class ProofSignerTest { + + private lateinit var issuanceRequest: IssuanceRequest + + @Before + fun setup() { + issuanceRequest = mockk(relaxed = true) + every { issuanceRequest.publicKey } returns mockk(relaxed = true) + } + + companion object { + + @BeforeClass + @JvmStatic + fun setupClass() { + mockkStatic(JWK::class) + every { JWK.parseFromPEMEncodedObjects(any()) } returns mockk(relaxed = true) + + } + } + + @Test + fun `invoke returns JWTProofSigner when supported jwt proof type and algorithm are available`() { + val issuerProofTypesSupported = ProofTypesSupported( + setOf( + ProofTypeMeta.Jwt(listOf(JWSAlgorithm.ES256)) + ) + ) + + val result = ProofSigner(issuanceRequest, issuerProofTypesSupported) + assertTrue(result.isSuccess) + val signer = result.getOrThrow() + assertTrue(signer is JWSProofSigner) + } + + @Test + fun `invoke returns CWTProofSigner when supported cwt proof type and algorithm are available`() { + val issuerProofTypesSupported = ProofTypesSupported( + setOf( + ProofTypeMeta.Cwt(listOf(CoseAlgorithm.ES256), listOf(CoseCurve.P_256)) + ) + ) + + val result = ProofSigner(issuanceRequest, issuerProofTypesSupported) + assertTrue(result.isSuccess) + val signer = result.getOrThrow() + assertTrue(signer is CWTProofSigner) + } + + @Test(expected = UnsupportedProofTypeException::class) + fun `invoke throws UnsupportedProofTypeException when no supported proof type is available`() { + val issuerProofTypesSupported = ProofTypesSupported( + setOf( + ProofTypeMeta.LdpVp + ) + ) + + val result = ProofSigner(issuanceRequest, issuerProofTypesSupported) + assertTrue(result.isFailure) + result.getOrThrow() + } + + @Test(expected = UnsupportedAlgorithmException::class) + fun `invoke throws UnsupportedAlgorithmException when no supported algorithm is available`() { + val issuerProofTypesSupported = ProofTypesSupported( + setOf( + ProofTypeMeta.Cwt(listOf(CoseAlgorithm.ES384), listOf(CoseCurve.P_384)) + ) + ) + + val result = ProofSigner(issuanceRequest, issuerProofTypesSupported) + assertTrue(result.isFailure) + result.getOrThrow() + } + + @Test + fun `doSign returns the signature when issuanceRequest signWithAuthKey succeeds`() { + val okInput = byteArrayOf() + val okSignature = byteArrayOf() + + + + every { + issuanceRequest.signWithAuthKey( + okInput, + Algorithm.SHA256withECDSA + ) + } returns SignedWithAuthKeyResult.Success(okSignature) + + val signer = object : ProofSigner() { + override val popSigner: PopSigner = mockk(relaxed = true) + } + val signature = signer.doSign(issuanceRequest, okInput, Algorithm.SHA256withECDSA) + assertSame(okSignature, signature) + } + + @Test + fun `doSign throws UserAuthRequiredException when issuanceRequest signWithAuthKey requires auth`() { + val userAuthRequiredInput = byteArrayOf() + val cryptoObject = mockk() + every { + issuanceRequest.signWithAuthKey( + userAuthRequiredInput, + Algorithm.SHA256withECDSA + ) + } returns SignedWithAuthKeyResult.UserAuthRequired(cryptoObject) + + val signer = object : ProofSigner() { + override val popSigner: PopSigner = mockk(relaxed = true) + } + assertEquals(ProofSigner.UserAuthStatus.NotRequired, signer.userAuthStatus) + assertThrows(UserAuthRequiredException::class.java) { + signer.doSign(issuanceRequest, userAuthRequiredInput, Algorithm.SHA256withECDSA) + } + assertTrue(signer.userAuthStatus is ProofSigner.UserAuthStatus.Required) + val status = signer.userAuthStatus as ProofSigner.UserAuthStatus.Required + assertSame(cryptoObject, status.cryptoObject) + } + + @Test + fun `doSign throws exception when issuanceRequest signWithAuthKey fails`() { + val errorInput = byteArrayOf() + val exception = mockk() + every { + issuanceRequest.signWithAuthKey( + errorInput, + Algorithm.SHA256withECDSA + ) + } returns SignedWithAuthKeyResult.Failure(exception) + + val signer = object : ProofSigner() { + override val popSigner: PopSigner = mockk(relaxed = true) + } + val e = assertThrows(Exception::class.java) { + signer.doSign(issuanceRequest, errorInput, Algorithm.SHA256withECDSA) + } + assertSame(exception, e) + } +} \ No newline at end of file