Skip to content

Commit

Permalink
select JWT or CWT proofType with priority from configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
vkanellopoulos committed Jun 6, 2024
1 parent 8b4f0ed commit c28840c
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 49 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
17 changes: 17 additions & 0 deletions wallet-core/build.gradle
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ internal class DefaultOpenId4VciManager(
addedDocuments: MutableSet<DocumentId>,
onEvent: OpenId4VciManager.OnResult<IssueEvent>
) {
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))
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -176,18 +177,24 @@ interface OpenId4VciManager {
val useStrongBoxIfSupported: Boolean,
val useDPoPIfSupported: Boolean,
@ParUsage val parUsage: Int,
val proofTypes: List<ProofType>
) {

@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
Expand All @@ -208,6 +215,8 @@ interface OpenId4VciManager {
@ParUsage
var parUsage: Int = ParUsage.IF_SUPPORTED

private var proofTypes: List<ProofType> = listOf(ProofType.JWT, ProofType.CWT)

/**
* Set the issuer url
*/
Expand Down Expand Up @@ -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
Expand All @@ -252,7 +269,8 @@ interface OpenId4VciManager {
authFlowRedirectionURI!!,
useStrongBoxIfSupported,
useDPoPIfSupported,
parUsage
parUsage,
proofTypes
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<OpenId4VciManager.Config.ProofType>? = null,
): Result<ProofSigner> {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProofAlgorithm>
Expand All @@ -41,15 +42,32 @@ internal sealed interface SupportedProofType {
}

companion object {
val SupportedProofTypes: List<SupportedProofType> = listOf(
Jwt(listOf(Jws.ES256)),
Cwt(listOf(Cose.ES256_P_256))
)

private var priority: List<ProofType>? = null

private val supportedProofTypes: List<SupportedProofType>
get() = listOf(
Jwt(listOf(Jws.ES256)),
Cwt(listOf(Cose.ES256_P_256))
).prioritize()

private fun List<SupportedProofType>.prioritize(): List<SupportedProofType> {
return priority?.let { order ->
val supportedProofTypesMap = associateBy { it.proofType }
order.mapNotNull { supportedProofTypesMap[it] }
} ?: this

}

fun prioritize(supportedProofTypesPrioritized: List<ConfigProofType>) = 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) {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit c28840c

Please sign in to comment.