Skip to content
This repository was archived by the owner on Dec 13, 2023. It is now read-only.

Commit

Permalink
Fixed x5chain header parameter value type when using multiple certifi…
Browse files Browse the repository at this point in the history
…cates and cert path validation.
  • Loading branch information
aarmam committed Oct 6, 2023
1 parent aec1ebe commit 609cce3
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 38 deletions.
13 changes: 9 additions & 4 deletions src/commonMain/kotlin/id/walt/mdoc/cose/COSESimpleBase.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package id.walt.mdoc.cose

import cbor.Cbor
import id.walt.mdoc.dataelement.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException

/**
Expand Down Expand Up @@ -31,8 +29,15 @@ abstract class COSESimpleBase<T: COSESimpleBase<T>>() {
get() {
if (data.size != 4) throw SerializationException("Invalid COSE_Sign1/COSE_Mac0 array")
val unprotectedHeader = data[1] as? MapElement ?: throw SerializationException("Missing COSE_Sign1 unprotected header")
val x5Chain = unprotectedHeader.value[MapKey(X5_CHAIN)] as? ByteStringElement
return x5Chain?.value
return when (val headerParameter = unprotectedHeader.value[MapKey(X5_CHAIN)]) {
is ByteStringElement -> headerParameter.value
is ListElement -> {
val byteArrays = headerParameter.value.map { (it as? ByteStringElement)?.value ?: ByteArray(0) }
byteArrays.reduceOrNull { acc, bytes -> acc + bytes }
}

else -> null
}
}

/**
Expand Down
35 changes: 22 additions & 13 deletions src/jvmMain/kotlin/id/walt/mdoc/SimpleCOSECryptoProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import id.walt.mdoc.cose.*
import kotlinx.serialization.ExperimentalSerializationApi
import java.io.ByteArrayInputStream
import java.security.KeyStore
import java.security.PrivateKey
import java.security.PublicKey
import java.security.cert.*
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
Expand All @@ -29,12 +27,23 @@ class SimpleCOSECryptoProvider(keys: List<COSECryptoProviderKeyInfo>): COSECrypt
val keyInfo = keyID?.let { keyMap[it] } ?: throw Exception("No key ID given, or key with given ID not found")
val sign1Msg = Sign1Message()
sign1Msg.addAttribute(HeaderKeys.Algorithm, keyInfo.algorithmID.AsCBOR(), Attribute.PROTECTED)
CBORObject.FromObject(keyInfo.x5Chain.map { it.encoded }.reduceOrNull { acc, bytes -> acc + bytes })?.let {
sign1Msg.addAttribute(
CBORObject.FromObject(X5_CHAIN),
it,
Attribute.UNPROTECTED
)
if (keyInfo.x5Chain.size == 1) {
CBORObject.FromObject(keyInfo.x5Chain.map { it.encoded }.reduceOrNull { acc, bytes -> acc + bytes })?.let {
sign1Msg.addAttribute(
CBORObject.FromObject(X5_CHAIN),
it,
Attribute.UNPROTECTED
)
}
} else {
CBORObject.FromObject(keyInfo.x5Chain.map { CBORObject.FromObject(it.encoded) }.toTypedArray<CBORObject?>())
?.let {
sign1Msg.addAttribute(
CBORObject.FromObject(X5_CHAIN),
it,
Attribute.UNPROTECTED
)
}
}
sign1Msg.SetContent(payload)
sign1Msg.sign(OneKey(keyInfo.publicKey, keyInfo.privateKey))
Expand All @@ -58,14 +67,14 @@ class SimpleCOSECryptoProvider(keys: List<COSECryptoProviderKeyInfo>): COSECrypt
.flatMap { it.acceptedIssuers.toList() }
.plus(additionalTrustedRootCAs)
.firstOrNull {
cert.issuerX500Principal.name.equals(it.subjectX500Principal.name)
}
cert.issuerX500Principal.name.equals(it.subjectX500Principal.name)
}
}

private fun validateCertificateChain(certChain: List<X509Certificate>, keyInfo: COSECryptoProviderKeyInfo): Boolean {
val certPath = CertificateFactory.getInstance("X509").generateCertPath(certChain)
val cpv = CertPathValidator.getInstance("PKIX")
val trustAnchorCert = findRootCA(certChain.first(), keyInfo.trustedRootCAs) ?: return false
val trustAnchorCert = findRootCA(certChain.last(), keyInfo.trustedRootCAs) ?: return false
cpv.validate(certPath, PKIXParameters(setOf(TrustAnchor(trustAnchorCert, null))).apply {
isRevocationEnabled = false
})
Expand All @@ -77,8 +86,8 @@ class SimpleCOSECryptoProvider(keys: List<COSECryptoProviderKeyInfo>): COSECrypt
val keyInfo = keyID?.let { keyMap[it] } ?: throw Exception("No key ID given, or key with given ID not found")
return coseSign1.x5Chain?.let {
val certChain = CertificateFactory.getInstance("X509").generateCertificates(ByteArrayInputStream(it)).map { it as X509Certificate }
return certChain.isNotEmpty() && certChain.last().publicKey.encoded.contentEquals(keyInfo.publicKey.encoded) &&
validateCertificateChain(certChain.toList(), keyInfo)
return certChain.isNotEmpty() && certChain.first().publicKey.encoded.contentEquals(keyInfo.publicKey.encoded) &&
validateCertificateChain(certChain.toList(), keyInfo)
} ?: false
}

Expand Down
97 changes: 76 additions & 21 deletions src/jvmTest/kotlin/id/walt/mdoc/JVMMdocTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,62 +52,117 @@ class JVMMdocTest: AnnotationSpec() {
val ISSUER_KEY_ID = "ISSUER_KEY"
val DEVICE_KEY_ID = "DEVICE_KEY"
val READER_KEY_ID = "READER_KEY"
lateinit var caKeyPair: KeyPair
lateinit var rootCaKeyPair: KeyPair
lateinit var intermCaKeyPair: KeyPair
lateinit var issuerKeyPair: KeyPair
lateinit var intermIssuerKeyPair: KeyPair
lateinit var deviceKeyPair: KeyPair
lateinit var readerKeyPair: KeyPair
lateinit var caCertificate: X509Certificate
lateinit var rootCaCertificate: X509Certificate
lateinit var intermCaCertificate: X509Certificate
lateinit var issuerCertificate: X509Certificate
lateinit var intermIssuerCertificate: X509Certificate

@BeforeAll
fun initializeIssuerKeys() {
Security.addProvider(BouncyCastleProvider())
val kpg = KeyPairGenerator.getInstance("EC")
kpg.initialize(256)
// create key pair for test CA
caKeyPair = kpg.genKeyPair()
rootCaKeyPair = kpg.genKeyPair()
intermCaKeyPair = kpg.genKeyPair()
// create key pair for test signer/issuer
issuerKeyPair = kpg.genKeyPair()
intermIssuerKeyPair = kpg.genKeyPair()
// create key pair for mdoc auth (device/holder key)
deviceKeyPair = kpg.genKeyPair()
readerKeyPair = kpg.genKeyPair()

// create CA certificate
caCertificate = X509v3CertificateBuilder(
X500Name("CN=MDOC Test CSP"), BigInteger.valueOf(SecureRandom().nextLong()),
Date(), Date(System.currentTimeMillis() + 24L * 3600 * 1000), X500Name("CN=MDOC Test CA"),
SubjectPublicKeyInfo.getInstance(caKeyPair.public.encoded)
rootCaCertificate = X509v3CertificateBuilder(
X500Name("CN=MDOC ROOT CSP"), BigInteger.valueOf(SecureRandom().nextLong()),
Date(), Date(System.currentTimeMillis() + 24L * 3600 * 1000), X500Name("CN=MDOC ROOT CA"),
SubjectPublicKeyInfo.getInstance(rootCaKeyPair.public.encoded)
) .addExtension(Extension.basicConstraints, true, BasicConstraints(false)) // TODO: Should be CA! Should not pass validation when false!
.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign)) // Key usage not validated.
.build(JcaContentSignerBuilder("SHA256withECDSA").setProvider("BC").build(rootCaKeyPair.private)).let {
JcaX509CertificateConverter().setProvider("BC").getCertificate(it)
}

intermCaCertificate = X509v3CertificateBuilder(
X500Name("CN=MDOC ROOT CA"), BigInteger.valueOf(SecureRandom().nextLong()),
Date(), Date(System.currentTimeMillis() + 24L * 3600 * 1000), X500Name("CN=MDOC Iterm CA"),
SubjectPublicKeyInfo.getInstance(intermCaKeyPair.public.encoded)
) .addExtension(Extension.basicConstraints, true, BasicConstraints(true)) // When set to false will not pass validation as expected!
.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign))

.build(JcaContentSignerBuilder("SHA256withECDSA").setProvider("BC").build(rootCaKeyPair.private)).let {
JcaX509CertificateConverter().setProvider("BC").getCertificate(it)
}

// create intermediate CA issuer certificate
intermIssuerCertificate = X509v3CertificateBuilder(X500Name("CN=MDOC Iterm CA"), BigInteger.valueOf(SecureRandom().nextLong()),
Date(), Date(System.currentTimeMillis() + 24L * 3600 * 1000), X500Name("CN=MDOC Iterm Test Issuer"),
SubjectPublicKeyInfo.getInstance(intermIssuerKeyPair.public.encoded)
) .addExtension(Extension.basicConstraints, true, BasicConstraints(false))
.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.digitalSignature))
.build(JcaContentSignerBuilder("SHA256withECDSA").setProvider("BC").build(caKeyPair.private)).let {
.build(JcaContentSignerBuilder("SHA256withECDSA").setProvider("BC").build(intermCaKeyPair.private)).let {
JcaX509CertificateConverter().setProvider("BC").getCertificate(it)
}

// create issuer certificate
issuerCertificate = X509v3CertificateBuilder(X500Name("CN=MDOC Test CA"), BigInteger.valueOf(SecureRandom().nextLong()),
issuerCertificate = X509v3CertificateBuilder(X500Name("CN=MDOC ROOT CA"), BigInteger.valueOf(SecureRandom().nextLong()),
Date(), Date(System.currentTimeMillis() + 24L * 3600 * 1000), X500Name("CN=MDOC Test Issuer"),
SubjectPublicKeyInfo.getInstance(issuerKeyPair.public.encoded)
) .addExtension(Extension.basicConstraints, true, BasicConstraints(false))
.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.digitalSignature))
.build(JcaContentSignerBuilder("SHA256withECDSA").setProvider("BC").build(caKeyPair.private)).let {
.build(JcaContentSignerBuilder("SHA256withECDSA").setProvider("BC").build(rootCaKeyPair.private)).let {
JcaX509CertificateConverter().setProvider("BC").getCertificate(it)
}
}

@OptIn(ExperimentalSerializationApi::class)
@Test
fun testSigningMdl() {
// ISO-IEC_18013-5:2021
// Personal identification — ISO-compliant driving licence
// Part 5: Mobile driving licence (mDL) application
fun testSigningMdlWithIssuer() {
// instantiate simple cose crypto provider for issuer keys and certificates
val cryptoProvider = SimpleCOSECryptoProvider(
listOf(
COSECryptoProviderKeyInfo(ISSUER_KEY_ID, AlgorithmID.ECDSA_256, issuerKeyPair.public, issuerKeyPair.private, listOf(issuerCertificate), listOf(rootCaCertificate)),
COSECryptoProviderKeyInfo(DEVICE_KEY_ID, AlgorithmID.ECDSA_256, deviceKeyPair.public, deviceKeyPair.private)
)
)
testSigningMdl(cryptoProvider)
}

@Test
fun testSigningMdlWithIntermediateIssuer() {
// instantiate simple cose crypto provider for issuer keys and certificates
val cryptoProvider = SimpleCOSECryptoProvider(
listOf(
COSECryptoProviderKeyInfo(ISSUER_KEY_ID, AlgorithmID.ECDSA_256, issuerKeyPair.public, issuerKeyPair.private, listOf(issuerCertificate), listOf(caCertificate)),
COSECryptoProviderKeyInfo(ISSUER_KEY_ID, AlgorithmID.ECDSA_256, intermIssuerKeyPair.public, intermIssuerKeyPair.private, listOf(intermIssuerCertificate, intermCaCertificate), listOf(rootCaCertificate)),
COSECryptoProviderKeyInfo(DEVICE_KEY_ID, AlgorithmID.ECDSA_256, deviceKeyPair.public, deviceKeyPair.private)
)
)
testSigningMdl(cryptoProvider)
}

@Test
fun testSigningMdlWithOnlyTheIntermediateIssuerCertInX5Chain() {
// instantiate simple cose crypto provider for issuer keys and certificates
val cryptoProvider = SimpleCOSECryptoProvider(
listOf(
COSECryptoProviderKeyInfo(ISSUER_KEY_ID, AlgorithmID.ECDSA_256, intermIssuerKeyPair.public, intermIssuerKeyPair.private, listOf(intermIssuerCertificate), listOf(rootCaCertificate, intermCaCertificate)),
COSECryptoProviderKeyInfo(DEVICE_KEY_ID, AlgorithmID.ECDSA_256, deviceKeyPair.public, deviceKeyPair.private)
)
)
testSigningMdl(cryptoProvider)
}

@OptIn(ExperimentalSerializationApi::class)
fun testSigningMdl(cryptoProvider: SimpleCOSECryptoProvider) {
// ISO-IEC_18013-5:2021
// Personal identification — ISO-compliant driving licence
// Part 5: Mobile driving licence (mDL) application

// create device key info structure of device public key, for holder binding
val deviceKeyInfo = DeviceKeyInfo(DataElement.fromCBOR(OneKey(deviceKeyPair.public, null).AsCBOR().EncodeToBytes()))

Expand Down Expand Up @@ -232,7 +287,7 @@ class JVMMdocTest: AnnotationSpec() {
val readerAuthentication = EncodedCBORElement.fromEncodedCBORElementData(readerAuthenticationBytes).decode<ReaderAuthentication>()
(readerAuthentication.data[0] as? StringElement)?.value shouldBe "ReaderAuthentication"

val certificateDER = devRequest.docRequests[0].readerAuth!!.x5Chain!!
val certificateDER = devRequest.docRequests[0].readerAuth!!.x5Chain
val cert = CertificateFactory.getInstance("X509").generateCertificate(ByteArrayInputStream(certificateDER)) as X509Certificate
val cryptoProvider = SimpleCOSECryptoProvider(listOf(
COSECryptoProviderKeyInfo(READER_KEY_ID, AlgorithmID.ECDSA_256, cert.publicKey, null, listOf(cert))
Expand All @@ -241,10 +296,10 @@ class JVMMdocTest: AnnotationSpec() {
// test with all fields allowed to retain
devRequest.docRequests[0].verify(
MDocRequestVerificationParams(
true, READER_KEY_ID, allowedToRetain = mapOf(
true, READER_KEY_ID, allowedToRetain = mapOf(
"org.iso.18013.5.1" to setOf("family_name", "document_number", "driving_privileges", "issue_date", "expiry_date")
), readerAuthentication
), cryptoProvider) shouldBe true
), readerAuthentication
), cryptoProvider) shouldBe true

// test with restricted fields allowed to retain
devRequest.docRequests[0].verify(
Expand Down Expand Up @@ -314,7 +369,7 @@ class JVMMdocTest: AnnotationSpec() {
// instantiate simple cose crypto provider for issuer keys and certificates
val cryptoProvider = SimpleCOSECryptoProvider(
listOf(
COSECryptoProviderKeyInfo(ISSUER_KEY_ID, AlgorithmID.ECDSA_256, issuerKeyPair.public, issuerKeyPair.private, listOf(issuerCertificate), listOf(caCertificate)),
COSECryptoProviderKeyInfo(ISSUER_KEY_ID, AlgorithmID.ECDSA_256, issuerKeyPair.public, issuerKeyPair.private, listOf(issuerCertificate), listOf(rootCaCertificate)),
COSECryptoProviderKeyInfo(DEVICE_KEY_ID, AlgorithmID.ECDSA_256, deviceKeyPair.public, deviceKeyPair.private)
)
)
Expand Down

0 comments on commit 609cce3

Please sign in to comment.