From c28840c5f94dde1625405db373fa092986ca351e Mon Sep 17 00:00:00 2001 From: vkanellopoulos Date: Thu, 6 Jun 2024 23:11:01 +0300 Subject: [PATCH] select JWT or CWT proofType with priority from configuration --- gradle.properties | 2 +- gradle/libs.versions.toml | 1 + wallet-core/build.gradle | 17 ++++ .../issue/opeid4vci/CWTProofSignerTest.kt | 48 ++++++++++ .../openid4vci/DefaultOpenId4VciManager.kt | 4 +- .../issue/openid4vci/OpenId4VciManager.kt | 24 ++++- .../wallet/issue/openid4vci/ProofSigner.kt | 22 ++--- .../issue/openid4vci/SupportedProofType.kt | 38 +++++--- .../ec/eudi/wallet/TestSign1ForAuthlete.kt | 49 ++++++++++ .../OpenId4VciManagerBuilderTest.kt | 3 +- .../OpenId4VciManagerConfigBuilderTest.kt | 50 +++++++++++ .../issue/openid4vci/ProofSignerTest.kt | 89 ++++++++++++++++--- 12 files changed, 298 insertions(+), 49 deletions(-) create mode 100644 wallet-core/src/test/java/eu/europa/ec/eudi/wallet/TestSign1ForAuthlete.kt diff --git a/gradle.properties b/gradle.properties index c9a17341..a8b034b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -42,7 +42,7 @@ systemProp.sonar.host.url=https://sonarcloud.io systemProp.sonar.gradle.skipCompile=true systemProp.sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/testDebugUnitTestCoverage/testDebugUnitTestCoverage.xml,build/reports/jacoco/testReleaseUnitTestCoverage/testReleaseUnitTestCoverage.xml systemProp.sonar.projectName=eudi-lib-android-wallet-core -VERSION_NAME=0.9.2-SNAPSHOT +VERSION_NAME=0.9.3-SNAPSHOT SONATYPE_HOST=S01 SONATYPE_AUTOMATIC_RELEASE=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ccf3eb7..a1f1644d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ eudi-lib-jvm-siop-openid4vp-kt = { module = "eu.europa.ec.eudi:eudi-lib-jvm-siop identity-credential = { module = "com.android.identity:identity-credential", version.ref = "identity-credential" } json = { module = "org.json:json", version.ref = "json" } junit = { module = "junit:junit", version.ref = "junit" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } mockito-android = { module = "org.mockito:mockito-android", version.ref = "mockito-android" } mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito-inline" } diff --git a/wallet-core/build.gradle b/wallet-core/build.gradle index 4bc5c646..84871766 100644 --- a/wallet-core/build.gradle +++ b/wallet-core/build.gradle @@ -1,3 +1,19 @@ +/* + * 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. + */ + import com.github.jk1.license.filter.ExcludeTransitiveDependenciesFilter import com.github.jk1.license.filter.LicenseBundleNormalizer import com.github.jk1.license.filter.ReduceDuplicateLicensesFilter @@ -119,6 +135,7 @@ dependencies { implementation libs.cose.java testImplementation libs.junit + testImplementation libs.junit.jupiter.params testImplementation libs.json testImplementation libs.mockk testImplementation libs.mockito.inline 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 75970a27..30be2781 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 @@ -16,10 +16,15 @@ package eu.europa.ec.eudi.wallet.document.issue.opeid4vci +import COSE.AlgorithmID +import COSE.Message +import COSE.MessageTag +import COSE.OneKey import android.app.KeyguardManager import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.upokecenter.cbor.CBORObject import eu.europa.ec.eudi.openid4vci.* import eu.europa.ec.eudi.wallet.document.CreateIssuanceRequestResult import eu.europa.ec.eudi.wallet.document.DocumentManager @@ -32,6 +37,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assume.assumeTrue import org.junit.BeforeClass +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import java.io.IOException @@ -120,4 +126,46 @@ class CWTProofSignerTest { assertTrue(result) documentManager.deleteDocumentById(issuanceRequest.documentId) } + + @Ignore("This is not for CI/CD use. This is just to test the sign1 message") + @Test + fun test_doSign_when_used_in_sign1_message_is_verified() { + val documentManager = DocumentManager.Builder(context) + .enableUserAuth(false) + .build() + + val issuanceRequestResult = documentManager.createIssuanceRequest("eu.europa.ec.eudiw.pid.1", false) + assertTrue(issuanceRequestResult is CreateIssuanceRequestResult.Success) + + val issuanceRequest = + (issuanceRequestResult as CreateIssuanceRequestResult.Success).issuanceRequest + + val cwtSigner = CWTProofSigner(issuanceRequest, SupportedProofAlgorithm.Cose.ES256_P_256) + val oneKey = OneKey(issuanceRequest.publicKey, null).AsCBOR() + val protectedHeaders = CBORObject.NewMap() + .Add(CBORObject.FromObject(1), AlgorithmID.ECDSA_256.AsCBOR()) + .Add(CBORObject.FromObject(3), "openid4vci-proof+cwt") + .Add("COSE_Key", oneKey) + .EncodeToBytes() + val unProtectedHeaders = CBORObject.NewMap() + val payload = byteArrayOf(1, 2, 3) + val structureToSign = CBORObject.NewArray() + .Add(protectedHeaders) + .Add(payload) + .EncodeToBytes() + val signature = cwtSigner.sign(structureToSign) + + val sign1Bytes = CBORObject.FromObjectAndTag( + CBORObject.NewArray().apply { + Add(protectedHeaders) + Add(unProtectedHeaders) + Add(payload) + Add(signature) + }, MessageTag.Sign1.value + ).EncodeToBytes() + + val sign1 = Message.DecodeFromBytes(sign1Bytes, MessageTag.Sign1) as COSE.Sign1Message + + sign1.validate(OneKey(sign1.protectedAttributes.get("COSE_Key"))) + } } \ 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 ef5534ca..0f598304 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 @@ -278,7 +278,7 @@ internal class DefaultOpenId4VciManager( addedDocuments: MutableSet, onEvent: OpenId4VciManager.OnResult ) { - val proofSigner = ProofSigner(issuanceRequest, credentialConfiguration).getOrThrow() + val proofSigner = ProofSigner(issuanceRequest, credentialConfiguration, config.proofTypes).getOrThrow() try { when (val outcome = authRequest.requestSingle(payload, proofSigner.popSigner).getOrThrow()) { is SubmittedRequest.Failed -> onEvent(IssueEvent.DocumentFailed(issuanceRequest, outcome.error)) @@ -391,7 +391,7 @@ internal class DefaultOpenId4VciManager( dPoPSigner = if (useDPoPIfSupported) JWSDPoPSigner().getOrNull() else null, parUsage = when (parUsage) { OpenId4VciManager.Config.ParUsage.IF_SUPPORTED -> ParUsage.IfSupported - OpenId4VciManager.Config.ParUsage.ALWAYS -> ParUsage.Always + OpenId4VciManager.Config.ParUsage.REQUIRED -> ParUsage.Required OpenId4VciManager.Config.ParUsage.NEVER -> ParUsage.Never else -> ParUsage.IfSupported } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManager.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManager.kt index b19e3b8f..b67d887b 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManager.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManager.kt @@ -22,6 +22,7 @@ import android.net.Uri import androidx.annotation.IntDef import eu.europa.ec.eudi.wallet.document.DocumentManager import java.util.concurrent.Executor +import eu.europa.ec.eudi.openid4vci.ProofType as InternalProofType /** * OpenId4VciManager is the main entry point to issue documents using the OpenId4Vci protocol @@ -176,18 +177,24 @@ interface OpenId4VciManager { val useStrongBoxIfSupported: Boolean, val useDPoPIfSupported: Boolean, @ParUsage val parUsage: Int, + val proofTypes: List ) { @Retention(AnnotationRetention.SOURCE) - @IntDef(value = [ParUsage.IF_SUPPORTED, ParUsage.ALWAYS, ParUsage.NEVER]) + @IntDef(value = [ParUsage.IF_SUPPORTED, ParUsage.REQUIRED, ParUsage.NEVER]) annotation class ParUsage { companion object { const val IF_SUPPORTED = 2 - const val ALWAYS = 4 + const val REQUIRED = 4 const val NEVER = 0 } } + enum class ProofType(@JvmSynthetic internal val type: InternalProofType) { + JWT(InternalProofType.JWT), + CWT(InternalProofType.CWT) + } + /** * Builder to create an instance of [Config] * @property issuerUrl the issuer url @@ -208,6 +215,8 @@ interface OpenId4VciManager { @ParUsage var parUsage: Int = ParUsage.IF_SUPPORTED + private var proofTypes: List = listOf(ProofType.JWT, ProofType.CWT) + /** * Set the issuer url */ @@ -237,6 +246,14 @@ interface OpenId4VciManager { fun parUsage(@ParUsage parUsage: Int) = apply { this.parUsage = parUsage } + fun proofTypes(vararg proofType: ProofType) = apply { + val distinctList = proofType.toList().distinct() + require(distinctList.size <= 2) { + "Only 2 proof types are supported. You provided ${proofType.toList().size}" + } + this.proofTypes = distinctList + } + /** * Build the [Config] * @throws [IllegalStateException] if issuerUrl, clientId or authFlowRedirectionURI is not set @@ -252,7 +269,8 @@ interface OpenId4VciManager { authFlowRedirectionURI!!, useStrongBoxIfSupported, useDPoPIfSupported, - parUsage + parUsage, + proofTypes ) } 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 96c5989d..9cda18a4 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 @@ -19,7 +19,6 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import androidx.biometric.BiometricPrompt.CryptoObject 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 @@ -71,30 +70,21 @@ internal abstract class ProofSigner { * Creates a proof signer for the given issuance request and credential configuration. * @param issuanceRequest The issuance request. * @param credentialConfiguration The credential configuration. + * @param supportedProofTypesPrioritized The supported proof types prioritized. * @return The proof signer or a failure if the proof type is not supported. */ @JvmStatic operator fun invoke( issuanceRequest: IssuanceRequest, credentialConfiguration: CredentialConfiguration, - ) = invoke(issuanceRequest, credentialConfiguration.proofTypesSupported) - - /** - * 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 [SupportedProofType.SupportedProofTypes] - * of the [ProofSigner]. - * @param issuanceRequest The issuance request. - * @param issuerSupportedProofTypes The issuer supported proof types. - * @return The proof signer or a failure if the proof type is not supported. - */ - @JvmStatic - operator fun invoke( - issuanceRequest: IssuanceRequest, - issuerSupportedProofTypes: ProofTypesSupported, + supportedProofTypesPrioritized: List? = null, ): Result { return try { Result.success( - SupportedProofType.selectProofType(issuerSupportedProofTypes).createProofSigner(issuanceRequest) + SupportedProofType + .apply { supportedProofTypesPrioritized?.let { prioritize(it) } } + .selectProofType(credentialConfiguration) + .createProofSigner(issuanceRequest) ) } catch (e: Throwable) { Result.failure(e) 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 index b0ba6b1e..7414ab0e 100644 --- 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 @@ -22,6 +22,7 @@ import eu.europa.ec.eudi.openid4vci.* import eu.europa.ec.eudi.wallet.document.Algorithm import eu.europa.ec.eudi.wallet.issue.openid4vci.SupportedProofType.ProofAlgorithm.Cose import eu.europa.ec.eudi.wallet.issue.openid4vci.SupportedProofType.ProofAlgorithm.Jws +import eu.europa.ec.eudi.wallet.issue.openid4vci.OpenId4VciManager.Config.ProofType as ConfigProofType internal sealed interface SupportedProofType { val algorithms: List @@ -41,15 +42,32 @@ internal sealed interface SupportedProofType { } companion object { - val SupportedProofTypes: List = listOf( - Jwt(listOf(Jws.ES256)), - Cwt(listOf(Cose.ES256_P_256)) - ) + + private var priority: List? = null + + private val supportedProofTypes: List + get() = listOf( + Jwt(listOf(Jws.ES256)), + Cwt(listOf(Cose.ES256_P_256)) + ).prioritize() + + private fun List.prioritize(): List { + return priority?.let { order -> + val supportedProofTypesMap = associateBy { it.proofType } + order.mapNotNull { supportedProofTypesMap[it] } + } ?: this + + } + + fun prioritize(supportedProofTypesPrioritized: List) = apply { + priority = supportedProofTypesPrioritized.map { it.type } + } @JvmStatic - fun selectProofType(issuerProofTypesSupported: ProofTypesSupported): SelectedProofType { - val issuerProofTypesSupportedMap = issuerProofTypesSupported.values.associateBy { it.type() } - for (supportedProofType in SupportedProofTypes) { + fun selectProofType(credentialConfiguration: CredentialConfiguration): SelectedProofType { + val issuerProofTypesSupportedMap = + credentialConfiguration.proofTypesSupported.values.associateBy { it.type() } + for (supportedProofType in supportedProofTypes) { val issuerProofTypeMeta = issuerProofTypesSupportedMap[supportedProofType.proofType] ?: continue when (issuerProofTypeMeta) { @@ -79,12 +97,8 @@ internal sealed interface SupportedProofType { else -> continue } } - throw UnsupportedProofTypeException(SupportedProofTypes) + throw UnsupportedProofTypeException(supportedProofTypes) } - - @JvmStatic - fun selectProofType(credentialConfiguration: CredentialConfiguration): SelectedProofType = - selectProofType(credentialConfiguration.proofTypesSupported) } sealed interface ProofAlgorithm { diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/TestSign1ForAuthlete.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/TestSign1ForAuthlete.kt new file mode 100644 index 00000000..1d3f3a91 --- /dev/null +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/TestSign1ForAuthlete.kt @@ -0,0 +1,49 @@ +/* + * 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 + +import COSE.Message +import COSE.MessageTag +import COSE.OneKey +import COSE.Sign1Message +import com.upokecenter.cbor.CBORObject +import org.bouncycastle.util.encoders.Hex +import org.junit.Assert +import org.junit.Ignore +import org.junit.Test + +class TestSign1ForAuthlete { + + @Ignore("This test is ignored because it is meant to be run only by hand. It is not a unit test.") + @Test + fun `test sign1 cwt that we send to authlete is verified`() { + + val sign1Hex = + // from example +// "d83dd284589da3012603746f70656e6964347663692d70726f6f662b63777468434f53455f4b6579a6010202582b3165354159394579423031586e557a61364c704a7a6b30326e36595f416d6d6e5362304642654e56567255032620012158203d2c50ac3db3974fe65dc02acf59a0a92781a018679b1db2c41129ac163c176d225820c55f1f6e2d454a8b14ba72deb8b36e2e4262aa663a4ca88c9eeafe1a7db0475da05860a4016c747261636b315f6c6967687403781a68747470733a2f2f747269616c2e617574686c6574652e6e6574061a665fdcab0a582b762d31622d6e38326b454a476248524f53656b47736d522d784575616d4378595f5430745874514e2d64595840461c38c8127b70ca2e491478da59c8764255513e9b15968d17367276ba2cecdaf4da83a343bef866d86f6fa516264859805d4d6cf7d836b6152eda61a42bf759" + + // from test + "d83dd284586da3012603746f70656e6964347663692d70726f6f662b63777468434f53455f4b6579a401022001215820c584947cb27885c927ee1d3062edbbb71b25f19b92d968f5767cb91cd7ff4abc225820cd2a0a3c52a3e94650a161272042d348f174e6b770a0f596ba8f1410f3629d68a05860a4016c747261636b315f6c6967687403781a68747470733a2f2f747269616c2e617574686c6574652e6e6574061a6661a0910a582b373432523836737067344e576c754c52483043716a6d303877477772747a3653325359553253587733636f584830460221009c14113a04660a35829efe0efd3c2278c95ef9983fab46f9c35eae54c26ae152022100aa789b9051d50757a4438574ac2a23ed9255f631f0efcc3447b5ba1e9e4237a6" + val sign1Bytes = Hex.decode(sign1Hex) + val cwtUntagged = CBORObject.DecodeFromBytes(sign1Bytes).let { + if (it.isTagged) it.Untag() else it + }.EncodeToBytes() + val sign1Message = Message.DecodeFromBytes(cwtUntagged, MessageTag.Sign1) as Sign1Message + val oneKey = OneKey(sign1Message.protectedAttributes.get("COSE_Key")) + Assert.assertTrue(sign1Message.validate(oneKey)) + } +} \ No newline at end of file diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManagerBuilderTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManagerBuilderTest.kt index abdf082a..e820cbe4 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManagerBuilderTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManagerBuilderTest.kt @@ -33,7 +33,8 @@ class OpenId4VciManagerBuilderTest { authFlowRedirectionURI = "app://redirect", useStrongBoxIfSupported = true, useDPoPIfSupported = true, - parUsage = OpenId4VciManager.Config.ParUsage.IF_SUPPORTED + parUsage = OpenId4VciManager.Config.ParUsage.IF_SUPPORTED, + proofTypes = listOf(OpenId4VciManager.Config.ProofType.JWT), ) @Test diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManagerConfigBuilderTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManagerConfigBuilderTest.kt index 06bc0bc3..570c3bdb 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManagerConfigBuilderTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OpenId4VciManagerConfigBuilderTest.kt @@ -18,6 +18,9 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import org.junit.Assert.* import org.junit.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource class OpenId4VciManagerConfigBuilderTest { @@ -107,4 +110,51 @@ class OpenId4VciManagerConfigBuilderTest { assertTrue(config.useDPoPIfSupported) } + + @ParameterizedTest(name = "parUsage: {0}") + @MethodSource("parUsageArgs") + fun `ConfigBuilder set the parUsage property correctly`(parUsage: Int) { + val builder = OpenId4VciManager.Config.Builder() + .issuerUrl("https://issuer.example.com") + .clientId("testClientId") + .authFlowRedirectionURI("app://redirect") + .parUsage(parUsage) + + val config = builder.build() + + assertEquals(parUsage, config.parUsage) + } + + @ParameterizedTest(name = "proofTypes: {0}") + @MethodSource("proofTypesArgs") + fun `ConfigBuilder sets the proofTypes property correctly with same order as they given`(proofTypes: List) { + val builder = OpenId4VciManager.Config.Builder() + .issuerUrl("https://issuer.example.com") + .clientId("testClientId") + .authFlowRedirectionURI("app://redirect") + .proofTypes(*proofTypes.toTypedArray()) + + val config = builder.build() + + assertEquals(proofTypes, config.proofTypes) + + } + + companion object { + + @JvmStatic + fun parUsageArgs() = listOf( + OpenId4VciManager.Config.ParUsage.IF_SUPPORTED, + OpenId4VciManager.Config.ParUsage.REQUIRED, + OpenId4VciManager.Config.ParUsage.NEVER + ).map { Arguments.of(it) } + + @JvmStatic + fun proofTypesArgs() = listOf( + listOf(OpenId4VciManager.Config.ProofType.JWT), + listOf(OpenId4VciManager.Config.ProofType.CWT), + listOf(OpenId4VciManager.Config.ProofType.JWT, OpenId4VciManager.Config.ProofType.CWT), + listOf(OpenId4VciManager.Config.ProofType.CWT, OpenId4VciManager.Config.ProofType.JWT) + ).map { Arguments.of(it) } + } } \ No newline at end of file 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 index 2539f055..7b3a99fc 100644 --- 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 @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package eu.europa.ec.eudi.wallet.issue.openid4vci +import COSE.AlgorithmID +import COSE.OneKey import androidx.biometric.BiometricPrompt.CryptoObject import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.JWK @@ -21,8 +24,6 @@ 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 @@ -30,14 +31,20 @@ import org.junit.Assert.* import org.junit.Before import org.junit.BeforeClass import org.junit.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import eu.europa.ec.eudi.wallet.issue.openid4vci.OpenId4VciManager.Config.ProofType as ConfigProofType class ProofSignerTest { private lateinit var issuanceRequest: IssuanceRequest + private lateinit var credentialConfiguration: CredentialConfiguration @Before fun setup() { issuanceRequest = mockk(relaxed = true) + credentialConfiguration = mockk(relaxed = true) every { issuanceRequest.publicKey } returns mockk(relaxed = true) } @@ -50,17 +57,22 @@ class ProofSignerTest { every { JWK.parseFromPEMEncodedObjects(any()) } returns mockk(relaxed = true) } + + @JvmStatic + fun proofTypesArgs() = listOf( + listOf(listOf(ConfigProofType.CWT, ConfigProofType.JWT), CWTProofSigner::class.java), + listOf(listOf(ConfigProofType.JWT, ConfigProofType.CWT), JWSProofSigner::class.java), + ).map { Arguments.of(*it.toTypedArray()) } } @Test fun `invoke returns JWTProofSigner when supported jwt proof type and algorithm are available`() { - val issuerProofTypesSupported = ProofTypesSupported( - setOf( - ProofTypeMeta.Jwt(listOf(JWSAlgorithm.ES256)) - ) - ) + every { + credentialConfiguration.proofTypesSupported + } returns ProofTypesSupported(setOf(ProofTypeMeta.Jwt(listOf(JWSAlgorithm.ES256)))) - val result = ProofSigner(issuanceRequest, issuerProofTypesSupported) + + val result = ProofSigner(issuanceRequest, credentialConfiguration) assertTrue(result.isSuccess) val signer = result.getOrThrow() assertTrue(signer is JWSProofSigner) @@ -68,13 +80,15 @@ class ProofSignerTest { @Test fun `invoke returns CWTProofSigner when supported cwt proof type and algorithm are available`() { - val issuerProofTypesSupported = ProofTypesSupported( + every { + credentialConfiguration.proofTypesSupported + } returns ProofTypesSupported( setOf( ProofTypeMeta.Cwt(listOf(CoseAlgorithm.ES256), listOf(CoseCurve.P_256)) ) ) - val result = ProofSigner(issuanceRequest, issuerProofTypesSupported) + val result = ProofSigner(issuanceRequest, credentialConfiguration) assertTrue(result.isSuccess) val signer = result.getOrThrow() assertTrue(signer is CWTProofSigner) @@ -82,30 +96,75 @@ class ProofSignerTest { @Test(expected = UnsupportedProofTypeException::class) fun `invoke return failure result when no supported proof type is available`() { - val issuerProofTypesSupported = ProofTypesSupported( + every { + credentialConfiguration.proofTypesSupported + } returns ProofTypesSupported( setOf( ProofTypeMeta.LdpVp ) ) - val result = ProofSigner(issuanceRequest, issuerProofTypesSupported) + val result = ProofSigner(issuanceRequest, credentialConfiguration) assertTrue(result.isFailure) result.getOrThrow() } @Test(expected = UnsupportedProofTypeException::class) fun `invoke returns failure result when no supported algorithm is available`() { - val issuerProofTypesSupported = ProofTypesSupported( + every { + credentialConfiguration.proofTypesSupported + } returns ProofTypesSupported( setOf( ProofTypeMeta.Cwt(listOf(CoseAlgorithm.ES384), listOf(CoseCurve.P_384)) ) ) - val result = ProofSigner(issuanceRequest, issuerProofTypesSupported) + val result = ProofSigner(issuanceRequest, credentialConfiguration) assertTrue(result.isFailure) result.getOrThrow() } + @Test + fun `invoke returns failure with UnsupportedProofTypeException when configured proof type is not supported`() { + every { + credentialConfiguration.proofTypesSupported + } returns ProofTypesSupported( + setOf( + ProofTypeMeta.Cwt(listOf(CoseAlgorithm.ES256), listOf(CoseCurve.P_256)) + ) + ) + + val result = ProofSigner(issuanceRequest, credentialConfiguration, listOf(ConfigProofType.JWT)) + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is UnsupportedProofTypeException) + } + + @ParameterizedTest(name = "ordered proofTypes: {0} -> {1}") + @MethodSource("proofTypesArgs") + internal fun `invoke returns JWTProofSigner when prioritized over cwt proof type and algorithm are available`( + orderedTypes: List, signerClass: Class + ) { + val issuanceRequest = mockk(relaxed = true) + val credentialConfiguration = mockk(relaxed = true) + every { issuanceRequest.publicKey } returns OneKey.generateKey(AlgorithmID.ECDSA_256).AsPublicKey() + every { + credentialConfiguration.proofTypesSupported + } returns ProofTypesSupported( + setOf( + ProofTypeMeta.Jwt(listOf(JWSAlgorithm.ES256)), + ProofTypeMeta.Cwt( + listOf(CoseAlgorithm.ES256), listOf(CoseCurve.P_256) + ) + ) + ) + + + val result = ProofSigner(issuanceRequest, credentialConfiguration, orderedTypes) + assertTrue(result.isSuccess) + val signer = result.getOrThrow() + assertTrue(signerClass.isAssignableFrom(signer::class.java)) + } + @Test fun `doSign returns the signature when issuanceRequest signWithAuthKey succeeds`() { val okInput = byteArrayOf() @@ -167,4 +226,6 @@ class ProofSignerTest { } assertSame(exception, e) } + + } \ No newline at end of file