Skip to content

Commit

Permalink
fix use Concat instead of DER for signature in CWT proof signer; add …
Browse files Browse the repository at this point in the history
…extensive logging for DefaultOpenId4VciManager
  • Loading branch information
vkanellopoulos committed Jun 10, 2024
1 parent aaf647f commit 0681480
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 125 deletions.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ espresso-contrib = "3.5.1"
espresso-core = "3.5.1"
eudi-document-manager = "0.3.0-SNAPSHOT"
eudi-iso18013-data-transfer = "0.2.0-SNAPSHOT"
eudi-lib-jvm-openid4vci-kt = "0.3.0-SNAPSHOT"
eudi-lib-jvm-openid4vci-kt = "0.3.0"
eudi-lib-jvm-siop-openid4vp-kt = "0.4.2"
gradle-plugin = "7.4.0"
identity-credential = "20231002"
Expand Down
6 changes: 6 additions & 0 deletions wallet-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ android {
}
}

testOptions {
unitTests.all {
useJUnitPlatform()
}
}

// publishing {
// singleVariant('release') {
// withSourcesJar()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,11 @@

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 com.nimbusds.jose.crypto.impl.ECDSA
import eu.europa.ec.eudi.openid4vci.*
import eu.europa.ec.eudi.wallet.document.CreateIssuanceRequestResult
import eu.europa.ec.eudi.wallet.document.DocumentManager
Expand All @@ -37,7 +33,6 @@ 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 @@ -117,6 +112,7 @@ class CWTProofSignerTest {

val payload = "some random bytes".toByteArray()
val proofSignature = proofSigner.sign(payload)
.let { ECDSA.transcodeSignatureToDER(it) } // convert it back to DER
val publicKey = (proofSigner.popSigner.bindingKey as CwtBindingKey.CoseKey).jwk.toECKey().toPublicKey()

val result = Signature.getInstance("SHA256withECDSA", BC).apply {
Expand All @@ -126,46 +122,4 @@ 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 @@ -52,5 +52,6 @@ internal class CWTProofSigner(
*/
fun sign(signingInput: ByteArray): ByteArray {
return doSign(issuanceRequest, signingInput, supportedProofAlgorithm.signAlgorithmName)
.derToConcat(supportedProofAlgorithm)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
import android.util.Log
import com.nimbusds.jose.jwk.Curve
import eu.europa.ec.eudi.openid4vci.*
import eu.europa.ec.eudi.wallet.document.AddDocumentResult
Expand All @@ -34,6 +35,7 @@ import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.C
import eu.europa.ec.eudi.wallet.issue.openid4vci.IssueEvent.Companion.failure
import eu.europa.ec.eudi.wallet.issue.openid4vci.ProofSigner.UserAuthStatus
import kotlinx.coroutines.*
import org.bouncycastle.util.encoders.Hex
import java.net.URI
import java.util.*
import java.util.concurrent.Executor
Expand Down Expand Up @@ -92,6 +94,7 @@ internal class DefaultOpenId4VciManager(
doIssueDocumentByOffer(offer, config, listener)

} catch (e: Throwable) {
Log.e(TAG, "issueDocumentByDocType failed", e)
listener(failure(e))
coroutineScope.cancel("issueDocumentByDocType failed", e)
}
Expand All @@ -109,6 +112,7 @@ internal class DefaultOpenId4VciManager(
try {
doIssueDocumentByOffer(offer, config, listener)
} catch (e: Throwable) {
Log.e(TAG, "issueDocumentByOffer failed", e)
listener(failure(e))
coroutineScope.cancel("issueDocumentByOffer failed", e)
}
Expand All @@ -125,18 +129,20 @@ internal class DefaultOpenId4VciManager(
clearStateThen {
launch(onIssueEvent.wrap(executor)) { coroutineScope, listener ->
try {
val offer = offerUriCache[offerUri]
?: CredentialOfferRequestResolver().resolve(offerUri).getOrThrow()
.let {
DefaultOffer(
it, Compose(
MsoMdocFormatFilter,
ProofTypeFilter(config.proofTypes)
)
val offer = offerUriCache[offerUri].also {
Log.d(TAG, "OfferUri $offerUri cache hit")
} ?: CredentialOfferRequestResolver().resolve(offerUri).getOrThrow()
.let {
DefaultOffer(
it, Compose(
MsoMdocFormatFilter,
ProofTypeFilter(config.proofTypes)
)
}
)
}.also { offerUriCache[offerUri] = it }
doIssueDocumentByOffer(offer, config, listener)
} catch (e: Throwable) {
Log.e(TAG, "issueDocumentByOfferUri failed", e)
listener(failure(e))
coroutineScope.cancel("issueDocumentByOffer failed", e)
}
Expand All @@ -155,10 +161,12 @@ internal class DefaultOpenId4VciManager(
val offer =
DefaultOffer(credentialOffer, Compose(MsoMdocFormatFilter, ProofTypeFilter(config.proofTypes)))
offerUriCache[offerUri] = offer
Log.d(TAG, "OfferUri $offerUri resolved")
callback(OfferResult.Success(offer))
coroutineScope.cancel("resolveDocumentOffer succeeded")
} catch (e: Throwable) {
offerUriCache.remove(offerUri)
Log.e(TAG, "OfferUri $offerUri resolution failed", e)
callback(OfferResult.Failure(e))
coroutineScope.cancel("resolveDocumentOffer failed", e)
}
Expand Down Expand Up @@ -209,6 +217,9 @@ internal class DefaultOpenId4VciManager(
val issuanceRequest = documentManager
.createIssuanceRequest(item, config.useStrongBoxIfSupported)
.getOrThrow()

Log.d(TAG, "Issuing document: ${issuanceRequest.documentId} for ${issuanceRequest.docType}")

doIssueCredential(
authorizedRequest,
item.configurationIdentifier,
Expand Down Expand Up @@ -302,15 +313,19 @@ internal class DefaultOpenId4VciManager(
addedDocuments: MutableSet<DocumentId>,
onEvent: OpenId4VciManager.OnResult<IssueEvent>
) {
Log.d(TAG, "doRequestSingleNoProof for ${issuanceRequest.documentId}")
when (val outcome = authRequest.requestSingle(payload).getOrThrow()) {
is SubmittedRequest.InvalidProof -> doRequestSingleWithProof(
authRequest.handleInvalidProof(outcome.cNonce),
payload,
credentialConfiguration,
issuanceRequest,
addedDocuments,
onEvent
)
is SubmittedRequest.InvalidProof -> {
Log.d(TAG, "doRequestSingleNoProof invalid proof")
doRequestSingleWithProof(
authRequest.handleInvalidProof(outcome.cNonce),
payload,
credentialConfiguration,
issuanceRequest,
addedDocuments,
onEvent
)
}

is SubmittedRequest.Failed -> onEvent(IssueEvent.DocumentFailed(issuanceRequest, outcome.error))
is SubmittedRequest.Success -> storeIssuedCredential(
Expand Down Expand Up @@ -341,7 +356,9 @@ internal class DefaultOpenId4VciManager(
addedDocuments: MutableSet<DocumentId>,
onEvent: OpenId4VciManager.OnResult<IssueEvent>
) {
Log.d(TAG, "doRequestSingleWithProof for ${issuanceRequest.documentId}")
val proofSigner = ProofSigner(issuanceRequest, credentialConfiguration, config.proofTypes).getOrThrow()
Log.d(TAG, "doRequestSingleWithProof proofSigner: ${proofSigner::class.java.name}")
try {
when (val outcome = authRequest.requestSingle(payload, proofSigner.popSigner).getOrThrow()) {
is SubmittedRequest.Failed -> onEvent(IssueEvent.DocumentFailed(issuanceRequest, outcome.error))
Expand All @@ -363,8 +380,10 @@ internal class DefaultOpenId4VciManager(
} catch (e: Throwable) {
when (val status = proofSigner.userAuthStatus) {
is UserAuthStatus.Required -> {
Log.d(TAG, "doRequestSingleWithProof userAuthStatus: $status")
val event = object : IssueEvent.DocumentRequiresUserAuth(issuanceRequest, status.cryptoObject) {
override fun resume() {
Log.d(TAG, "doRequestSingleWithProof resume from user auth")
runBlocking {
doRequestSingleWithProof(
authRequest,
Expand All @@ -378,6 +397,7 @@ internal class DefaultOpenId4VciManager(
}

override fun cancel() {
Log.e(TAG, "doRequestSingleWithProof cancel from user auth")
onEvent(IssueEvent.DocumentFailed(issuanceRequest, e.cause ?: e))
}
}
Expand Down Expand Up @@ -412,6 +432,10 @@ internal class DefaultOpenId4VciManager(

is IssuedCredential.Issued -> {
val cbor = Base64.getUrlDecoder().decode(issuedCredential.credential)

Log.d(TAG, "storeIssuedCredential for ${issuanceRequest.documentId}")
Log.d(TAG, "storeIssuedCredential cbor: ${Hex.toHexString(cbor)}")

when (val addResult = documentManager.addDocument(issuanceRequest, cbor)) {
is AddDocumentResult.Failure -> {
documentManager.deleteDocumentById(issuanceRequest.documentId)
Expand All @@ -433,11 +457,49 @@ internal class DefaultOpenId4VciManager(
* @receiver The [OpenId4VciManager.OnResult].
* @return The wrapped [OpenId4VciManager.OnResult].
*/
private fun <R : OpenId4VciManager.OnResult<V>, V> R.wrap(executor: Executor?): OpenId4VciManager.OnResult<V> {
private inline fun <R : OpenId4VciManager.OnResult<V>, reified V> R.wrap(executor: Executor?): OpenId4VciManager.OnResult<V> {
return OpenId4VciManager.OnResult { result: V ->
(executor ?: context.mainExecutor()).execute {
this@wrap.onResult(result)
}
}.logResult()
}

private inline fun <R : OpenId4VciManager.OnResult<V>, reified V> R.logResult(): OpenId4VciManager.OnResult<V> {
return OpenId4VciManager.OnResult { result: V ->
when (result) {
is IssueEvent.DocumentIssued -> Log.d(
TAG,
"${IssueEvent.DocumentIssued::class.java.name} for ${result.documentId}"
)

is IssueEvent.DocumentFailed -> Log.e(
TAG,
IssueEvent.DocumentFailed::class.java.simpleName,
result.cause
)

is IssueEvent.DocumentRequiresUserAuth -> Log.d(
TAG,
IssueEvent.DocumentRequiresUserAuth::class.java.simpleName
)

is IssueEvent.Started -> Log.d(
TAG,
"${IssueEvent.Started::class.java.name} for ${result.total} documents"
)

is IssueEvent.Finished -> Log.d(
TAG,
"${IssueEvent.Finished::class.java.name} for ${result.issuedDocuments}"
)

is IssueEvent.Failure -> Log.e(TAG, IssueEvent.Failure::class.java.simpleName, result.cause)
is OfferResult.Failure -> Log.e(TAG, OfferResult.Failure::class.java.simpleName, result.error)
is OfferResult.Success -> Log.d(TAG, "${OfferResult.Success::class.java.name} for ${result.offer}")
else -> Log.d(TAG, V::class.java.simpleName)
}
this.onResult(result)
}
}

Expand All @@ -461,7 +523,7 @@ internal class DefaultOpenId4VciManager(
}

companion object {
private const val TAG = "DefaultOpenId4VciManage"
internal const val TAG = "DefaultOpenId4VciManage"

/**
* Converts the [OpenId4VciManager.Config] to [OpenId4VCIConfig].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal class JWSProofSigner(
)
}
return doSign(issuanceRequest, signingInput, supportedProofAlgorithm.signAlgorithmName).let { signature ->
Base64URL.encode(signature.derToJose(header.algorithm))
Base64URL.encode(signature.derToConcat(supportedProofAlgorithm))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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.*
import eu.europa.ec.eudi.wallet.document.Algorithm
import eu.europa.ec.eudi.wallet.issue.openid4vci.SupportedProofType.ProofAlgorithm.Cose
Expand Down Expand Up @@ -153,6 +154,7 @@ internal sealed interface SupportedProofType {
* Proof algorithm for the supported proof type.
* @property name the name of the proof algorithm
* @property signAlgorithmName the name of the sign algorithm that it is used in the [eu.europa.ec.eudi.wallet.document.IssuanceRequest.signWithAuthKey]
* @property signatureByteArrayLength the length of the signature byte array to be use when converting the signature from DER to Concat format
* method
*/
sealed interface ProofAlgorithm {
Expand All @@ -161,6 +163,8 @@ internal sealed interface SupportedProofType {
@get:Algorithm
val signAlgorithmName: String

val signatureByteArrayLength: Int

/**
* Proof algorithm for signing JWT with JWS.
* @property algorithm the JWS algorithm
Expand All @@ -170,9 +174,11 @@ internal sealed interface SupportedProofType {
data class Jws(
val algorithm: JWSAlgorithm,
@Algorithm override val signAlgorithmName: String,
override val name: String = algorithm.name
override val name: String = algorithm.name,
override val signatureByteArrayLength: Int = ECDSA.getSignatureByteArrayLength(algorithm)
) : ProofAlgorithm {


/**
* Companion object for [Jws] instances.
* @property ES256 the ES256 proof algorithm
Expand All @@ -194,15 +200,17 @@ internal sealed interface SupportedProofType {
val coseAlgorithm: CoseAlgorithm,
val coseCurve: CoseCurve,
@Algorithm override val signAlgorithmName: String,
override val name: String = "${coseAlgorithm.name()}_${coseCurve.name()}"
override val name: String = "${coseAlgorithm.name()}_${coseCurve.name()}",
override val signatureByteArrayLength: Int
) : ProofAlgorithm {

/**
* Companion object for [Cose] instances.
* @property ES256_P_256 the ES256_P_256 proof algorithm for COSE ES256 with P-256 curve
*/
companion object {
val ES256_P_256 = Cose(CoseAlgorithm.ES256, CoseCurve.P_256, Algorithm.SHA256withECDSA)
val ES256_P_256 =
Cose(CoseAlgorithm.ES256, CoseCurve.P_256, Algorithm.SHA256withECDSA, signatureByteArrayLength = 64)
}
}
}
Expand Down
Loading

0 comments on commit 0681480

Please sign in to comment.