From dadbb95dc1303703e8d34ac3067a5f8f710de390 Mon Sep 17 00:00:00 2001 From: David Zeuthen Date: Wed, 4 Dec 2024 15:16:16 -0500 Subject: [PATCH] New ASN.1 parser/generator and migrate X509Cert and TrustManager. This adds a new ASN1 package for parsing/encoding ASN.1 using DER encoding. Also port X509Cert, X509CertChain for both parsing and generation. Also migrate TrustManager to use this and since it's now Kotlin Multiplatform make use of TrustManager in samples/testapp for both reader and issuer authentication. With X509Cert now being multiplatform, move all certificate generation code to MdocUtils.kt in identity-mdoc library so it's available to apps. Similarly, make SignedVical.create() multi-platform. Also remove the drag handle on ConsentModalBottomSheet since the user is never expected to use it. Add JVM-unit tests to check that all public accessors on X509Cert agree with X509Certificate. Test: New unit tests and all unit tests pass. Test: Manually tested in samples/testapp on both Android and iOS. Test: Manually tested appholder, appverifier, wallet. Signed-off-by: David Zeuthen --- .../fragment/TransferDocumentFragment.kt | 9 +- .../fragment/ShowDeviceResponseFragment.kt | 11 +- .../fragment/ShowDocumentFragment.kt | 10 +- .../securearea/cloud/CloudSecureAreaTest.kt | 52 +- .../securearea/cloud/CloudSecureArea.kt | 7 +- .../DeviceRetrievalHelperTest.kt | 31 +- .../ui/consent/ConsentModalBottomSheet.kt | 3 +- .../securearea/cloud/CloudSecureAreaServer.kt | 76 +- .../issuance/authenticationUtilities.kt | 22 +- .../android/identity/mdoc/util/MdocUtil.kt | 222 ++++++ .../identity/mdoc/vical/SignedVical.kt | 60 ++ .../request/DeviceRequestGeneratorTest.kt | 48 +- .../mdoc/request/DeviceRequestParserTest.kt | 45 +- .../response/DeviceResponseGeneratorTest.kt | 51 +- .../mdoc/response/DeviceResponseParserTest.kt | 2 +- .../identity/mdoc/util/MdocUtilTest.kt | 105 +++ .../identity/mdoc/vical/VicalGeneratorTest.kt | 73 +- .../identity/mdoc/vical/VicalParserTest.kt | 44 +- .../identity/mdoc/vical/SignedVicalJvm.kt | 86 --- .../com/android/identity/sdjwt/SdJwtVcTest.kt | 31 +- .../SwiftBridge/SwiftBridge/SwiftBridge.swift | 5 + .../native/SwiftCrypto/KotlinWrappers.swift | 287 -------- .../kotlin/com/android/identity/asn1/ASN1.kt | 326 +++++++++ .../android/identity/asn1/ASN1BitString.kt | 92 +++ .../com/android/identity/asn1/ASN1Boolean.kt | 35 + .../com/android/identity/asn1/ASN1Encoding.kt | 15 + .../com/android/identity/asn1/ASN1Integer.kt | 111 +++ .../android/identity/asn1/ASN1IntegerTag.kt | 6 + .../com/android/identity/asn1/ASN1Null.kt | 27 + .../com/android/identity/asn1/ASN1Object.kt | 19 + .../identity/asn1/ASN1ObjectIdentifier.kt | 62 ++ .../android/identity/asn1/ASN1OctetString.kt | 30 + .../identity/asn1/ASN1PrimitiveValue.kt | 9 + .../android/identity/asn1/ASN1RawObject.kt | 29 + .../com/android/identity/asn1/ASN1Sequence.kt | 46 ++ .../com/android/identity/asn1/ASN1Set.kt | 45 ++ .../com/android/identity/asn1/ASN1String.kt | 30 + .../android/identity/asn1/ASN1StringTag.kt | 16 + .../com/android/identity/asn1/ASN1TagClass.kt | 17 + .../android/identity/asn1/ASN1TaggedObject.kt | 44 ++ .../com/android/identity/asn1/ASN1Time.kt | 133 ++++ .../com/android/identity/asn1/ASN1TimeTag.kt | 6 + .../kotlin/com/android/identity/asn1/OID.kt | 57 ++ .../com/android/identity/crypto/Algorithm.kt | 3 + .../com/android/identity/crypto/Crypto.kt | 3 + .../android/identity/crypto/EcSignature.kt | 46 ++ .../com/android/identity/crypto/X500Name.kt | 106 +++ .../com/android/identity/crypto/X509Cert.kt | 612 +++++++++++++++- .../android/identity/crypto/X509CertChain.kt | 7 + .../android/identity/crypto/X509KeyUsage.kt | 61 ++ .../identity/trustmanagement/TrustManager.kt | 95 +-- .../trustmanagement/TrustManagerUtil.kt | 92 +++ .../com/android/identity/asn1/ASN1Tests.kt | 661 ++++++++++++++++++ .../android/identity/crypto/X500NameTests.kt | 64 ++ .../android/identity/crypto/X509CertTests.kt | 124 +++- .../trustmanagement/TrustManagerTest.kt | 265 +++++++ .../com/android/identity/crypto/CryptoIos.kt | 3 + .../android/identity/crypto/X509CertIos.kt | 80 --- .../com/android/identity/crypto/CryptoJvm.kt | 35 +- .../android/identity/crypto/EcSignatureJvm.kt | 62 -- .../android/identity/crypto/X509CertJvm.kt | 270 ------- .../trustmanagement/TrustManagerUtil.kt | 163 ----- .../com/android/identity/asn1/ASN1TestsJvm.kt | 24 + .../identity/crypto/X509CertTestsJvm.kt | 190 +++-- .../trustmanagement/TrustManagerTest.kt | 246 ------- .../identity/identityctl/IdentityCtl.kt | 326 ++------- .../android/identity/testapp/TestAppUtils.kt | 178 ++++- .../ui/IsoMdocProximityReadingScreen.kt | 11 +- .../ui/IsoMdocProximitySharingScreen.kt | 15 +- .../identity/server/openid4vci/openid4vp.kt | 50 +- .../wallet/server/CloudSecureAreaServlet.kt | 57 +- .../identity/wallet/server/VerifierServlet.kt | 105 +-- .../wallet/PresentationActivity.kt | 3 +- .../identity_credential/wallet/ReaderModel.kt | 8 +- .../wallet/WalletApplication.kt | 5 +- .../credman/CredmanPresentationActivity.kt | 3 +- .../OpenID4VPPresentationActivity.kt | 14 +- .../wallet/presentation/TransferHelper.kt | 3 +- .../wallet/OpenID4VPTest.kt | 57 +- 79 files changed, 4312 insertions(+), 2170 deletions(-) rename identity-mdoc/src/{jvmTest => commonTest}/kotlin/com/android/identity/mdoc/request/DeviceRequestGeneratorTest.kt (83%) rename identity-mdoc/src/{jvmTest => commonTest}/kotlin/com/android/identity/mdoc/request/DeviceRequestParserTest.kt (92%) rename identity-mdoc/src/{jvmTest => commonTest}/kotlin/com/android/identity/mdoc/response/DeviceResponseGeneratorTest.kt (95%) rename identity-mdoc/src/{jvmTest => commonTest}/kotlin/com/android/identity/mdoc/vical/VicalGeneratorTest.kt (72%) rename identity-mdoc/src/{jvmTest => commonTest}/kotlin/com/android/identity/mdoc/vical/VicalParserTest.kt (98%) delete mode 100644 identity-mdoc/src/jvmMain/kotlin/com/android/identity/mdoc/vical/SignedVicalJvm.kt delete mode 100644 identity/native/SwiftCrypto/KotlinWrappers.swift create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1BitString.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Boolean.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Encoding.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Integer.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1IntegerTag.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Null.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Object.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1ObjectIdentifier.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1OctetString.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1PrimitiveValue.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1RawObject.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Sequence.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Set.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1String.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1StringTag.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TagClass.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TaggedObject.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Time.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TimeTag.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/asn1/OID.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/crypto/X500Name.kt create mode 100644 identity/src/commonMain/kotlin/com/android/identity/crypto/X509KeyUsage.kt rename identity/src/{javaSharedMain => commonMain}/kotlin/com/android/identity/trustmanagement/TrustManager.kt (62%) create mode 100644 identity/src/commonMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt create mode 100644 identity/src/commonTest/kotlin/com/android/identity/asn1/ASN1Tests.kt create mode 100644 identity/src/commonTest/kotlin/com/android/identity/crypto/X500NameTests.kt create mode 100644 identity/src/commonTest/kotlin/com/android/identity/trustmanagement/TrustManagerTest.kt delete mode 100644 identity/src/iosMain/kotlin/com/android/identity/crypto/X509CertIos.kt delete mode 100644 identity/src/javaSharedMain/kotlin/com/android/identity/crypto/EcSignatureJvm.kt delete mode 100644 identity/src/javaSharedMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt create mode 100644 identity/src/jvmTest/kotlin/com/android/identity/asn1/ASN1TestsJvm.kt delete mode 100644 identity/src/jvmTest/kotlin/com/android/identity/trustmanagement/TrustManagerTest.kt diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt index e12b6fb29..cc33ee4e6 100644 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt +++ b/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt @@ -102,19 +102,14 @@ class TransferDocumentFragment : Fragment() { } val doc = viewModel.getSelectedDocuments().first { reqDoc.docType == it.docType } if (reqDoc.readerAuth != null && reqDoc.readerAuthenticated) { - var certChain: List = - reqDoc.readerCertificateChain!!.certificates.map { it.javaX509Certificate } - .toList() val customValidators = CustomValidators.getByDocType(doc.docType) val result = HolderApp.trustManagerInstance.verify( - chain = certChain, - customValidators = customValidators + chain = reqDoc.readerCertificateChain!!.certificates, ) trusted = result.isTrusted if (result.trustChain.any()) { - certChain = result.trustChain + commonName = result.trustChain.last().issuer.name } - commonName = certChain.last().issuerX500Principal.getCommonName("") // Add some information about the reader certificate used if (result.isTrusted) { diff --git a/appverifier/src/main/java/com/android/mdl/appreader/fragment/ShowDeviceResponseFragment.kt b/appverifier/src/main/java/com/android/mdl/appreader/fragment/ShowDeviceResponseFragment.kt index a39b92644..1f6164dfe 100644 --- a/appverifier/src/main/java/com/android/mdl/appreader/fragment/ShowDeviceResponseFragment.kt +++ b/appverifier/src/main/java/com/android/mdl/appreader/fragment/ShowDeviceResponseFragment.kt @@ -23,6 +23,7 @@ import com.android.identity.crypto.Algorithm import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPublicKeyDoubleCoordinate +import com.android.identity.crypto.X509CertChain import com.android.identity.crypto.javaPublicKey import com.android.identity.crypto.javaX509Certificates import com.android.identity.crypto.toEcPrivateKey @@ -163,19 +164,19 @@ class ShowDeviceResponseFragment : Fragment() { ) sb.append("

Doctype: ${doc.docType}

") - var certChain = doc.issuerCertificateChain.javaX509Certificates + var certChain = doc.issuerCertificateChain val customValidators = CustomValidators.getByDocType(doc.docType) val result = VerifierApp.trustManagerInstance.verify( - chain = certChain, - customValidators = customValidators + chain = certChain.certificates, + //customValidators = customValidators ) if (result.trustChain.any()){ - certChain = result.trustChain + certChain = X509CertChain(result.trustChain) } if (!result.isTrusted) { sb.append("${getFormattedCheck(false)}Error in certificate chain validation: ${result.error?.message}
") } - val commonName = certChain.last().issuerX500Principal.getCommonName("") + val commonName = certChain.certificates.last().issuer.name sb.append("${getFormattedCheck(result.isTrusted)}Issuer’s DS Key Recognized: ($commonName)
") sb.append("${getFormattedCheck(doc.issuerSignedAuthenticated)}Issuer Signed Authenticated
") diff --git a/appverifier/src/main/java/com/android/mdl/appreader/fragment/ShowDocumentFragment.kt b/appverifier/src/main/java/com/android/mdl/appreader/fragment/ShowDocumentFragment.kt index 230f8601c..489f045c7 100644 --- a/appverifier/src/main/java/com/android/mdl/appreader/fragment/ShowDocumentFragment.kt +++ b/appverifier/src/main/java/com/android/mdl/appreader/fragment/ShowDocumentFragment.kt @@ -16,6 +16,7 @@ import androidx.annotation.AttrRes import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.android.identity.cbor.Cbor +import com.android.identity.crypto.X509Cert import com.android.identity.documenttype.DocumentAttributeType import com.android.identity.documenttype.MdocDataElement import com.android.identity.crypto.javaPublicKey @@ -207,13 +208,12 @@ class ShowDocumentFragment : Fragment() { for (doc in documents) { sb.append("

Doctype: ${doc.docType}

") - val cc = mutableListOf() - doc.issuerCertificateChain.certificates.forEach() { c -> cc.add(c.javaX509Certificate) } - var certChain: List = cc + val cc = doc.issuerCertificateChain.certificates + var certChain: List = cc val customValidators = CustomValidators.getByDocType(doc.docType) val result = VerifierApp.trustManagerInstance.verify( chain = certChain, - customValidators = customValidators + //customValidators = customValidators ) if (result.trustChain.any()) { certChain = result.trustChain @@ -222,7 +222,7 @@ class ShowDocumentFragment : Fragment() { sb.append("${getFormattedCheck(false)}Error in certificate chain validation: ${result.error?.message}
") } - val commonName = certChain.last().issuerX500Principal.getCommonName("") + val commonName = certChain.last().issuer.name sb.append("${getFormattedCheck(result.isTrusted)}Issuer’s DS Key Recognized: ($commonName)
") sb.append("${getFormattedCheck(doc.issuerSignedAuthenticated)}Issuer Signed Authenticated
") var macOrSignatureString = "MAC" diff --git a/identity-android-csa/src/androidTest/java/com/android/identity/android/securearea/cloud/CloudSecureAreaTest.kt b/identity-android-csa/src/androidTest/java/com/android/identity/android/securearea/cloud/CloudSecureAreaTest.kt index 1c272252a..2fcd824a1 100644 --- a/identity-android-csa/src/androidTest/java/com/android/identity/android/securearea/cloud/CloudSecureAreaTest.kt +++ b/identity-android-csa/src/androidTest/java/com/android/identity/android/securearea/cloud/CloudSecureAreaTest.kt @@ -3,12 +3,14 @@ package com.android.identity.android.securearea.cloud import android.content.Context import android.content.pm.PackageManager import androidx.test.InstrumentationRegistry +import com.android.identity.asn1.ASN1Integer import com.android.identity.crypto.Algorithm import com.android.identity.crypto.X509CertChain import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.create +import com.android.identity.crypto.X509KeyUsage import com.android.identity.crypto.javaX509Certificate import com.android.identity.securearea.AttestationExtension import com.android.identity.securearea.CreateKeySettings @@ -69,19 +71,19 @@ class CloudSecureAreaTest { val attestationKeySignatureAlgorithm = attestationKey.curve.defaultSigningAlgorithm val attestationKeyCertificates = X509CertChain( listOf( - X509Cert.create( - attestationKey.publicKey, - attestationKey, - null, - attestationKeySignatureAlgorithm, - "1", - attestationKeySubject, - attestationKeySubject, - attestationKeyValidFrom, - attestationKeyValidUntil, - setOf(), - listOf() + X509Cert.Builder( + publicKey = attestationKey.publicKey, + signingKey = attestationKey, + signatureAlgorithm = attestationKeySignatureAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName(attestationKeySubject), + issuer = X500Name.fromName(attestationKeySubject), + validFrom = attestationKeyValidFrom, + validUntil = attestationKeyValidUntil ) + .includeSubjectKeyIdentifier() + .setKeyUsage(setOf(X509KeyUsage.KEY_CERT_SIGN)) + .build(), ) ) @@ -92,19 +94,19 @@ class CloudSecureAreaTest { val cloudBindingKeySignatureAlgorithm = cloudBindingKeyAttestationKey.curve.defaultSigningAlgorithm val cloudBindingKeyAttestationCertificates = X509CertChain( listOf( - X509Cert.create( - cloudBindingKeyAttestationKey.publicKey, - cloudBindingKeyAttestationKey, - null, - cloudBindingKeySignatureAlgorithm, - "1", - cloudBindingKeySubject, - cloudBindingKeySubject, - cloudBindingKeyValidFrom, - cloudBindingKeyValidUntil, - setOf(), - listOf() + X509Cert.Builder( + publicKey = cloudBindingKeyAttestationKey.publicKey, + signingKey = cloudBindingKeyAttestationKey, + signatureAlgorithm = cloudBindingKeySignatureAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName(cloudBindingKeySubject), + issuer = X500Name.fromName(cloudBindingKeySubject), + validFrom = cloudBindingKeyValidFrom, + validUntil = cloudBindingKeyValidUntil ) + .includeSubjectKeyIdentifier() + .setKeyUsage(setOf(X509KeyUsage.KEY_CERT_SIGN)) + .build(), ) ) diff --git a/identity-android-csa/src/main/java/com/android/identity/android/securearea/cloud/CloudSecureArea.kt b/identity-android-csa/src/main/java/com/android/identity/android/securearea/cloud/CloudSecureArea.kt index 15e0681a4..0eda4f150 100644 --- a/identity-android-csa/src/main/java/com/android/identity/android/securearea/cloud/CloudSecureArea.kt +++ b/identity-android-csa/src/main/java/com/android/identity/android/securearea/cloud/CloudSecureArea.kt @@ -16,7 +16,6 @@ import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPublicKey import com.android.identity.crypto.EcSignature -import com.android.identity.crypto.fromDer import com.android.identity.crypto.fromJavaX509Certificates import com.android.identity.crypto.javaX509Certificate import com.android.identity.securearea.AttestationExtension @@ -356,7 +355,7 @@ open class CloudSecureArea( val request1 = E2EESetupRequest1( eDeviceKey.publicKey.toCoseKey(), deviceNonce, - EcSignature.fromDer(EcCurve.P256, derSignature), + EcSignature.fromDerEncoded(EcCurve.P256.bitSize, derSignature), response0.serverState ) response = runBlocking { communicate(serverUrl, request1.toCbor()) } @@ -620,7 +619,7 @@ open class CloudSecureArea( val derSignatureLocal = s.sign() val request1 = SignRequest1( - EcSignature.fromDer(EcCurve.P256, derSignatureLocal), + EcSignature.fromDerEncoded(EcCurve.P256.bitSize, derSignatureLocal), (keyUnlockData as? CloudKeyUnlockData)?.passphrase, response0.serverState ) @@ -741,7 +740,7 @@ open class CloudSecureArea( val derSignatureLocal = s.sign() val request1 = KeyAgreementRequest1( - EcSignature.fromDer(EcCurve.P256, derSignatureLocal), + EcSignature.fromDerEncoded(EcCurve.P256.bitSize, derSignatureLocal), (keyUnlockData as? CloudKeyUnlockData)?.passphrase, response0.serverState ) diff --git a/identity-android/src/androidTest/java/com/android/identity/android/mdoc/deviceretrieval/DeviceRetrievalHelperTest.kt b/identity-android/src/androidTest/java/com/android/identity/android/mdoc/deviceretrieval/DeviceRetrievalHelperTest.kt index ef915fc11..3e241170c 100644 --- a/identity-android/src/androidTest/java/com/android/identity/android/mdoc/deviceretrieval/DeviceRetrievalHelperTest.kt +++ b/identity-android/src/androidTest/java/com/android/identity/android/mdoc/deviceretrieval/DeviceRetrievalHelperTest.kt @@ -21,6 +21,7 @@ import com.android.identity.android.mdoc.engagement.QrEngagementHelper import com.android.identity.android.mdoc.transport.DataTransport import com.android.identity.android.mdoc.transport.DataTransportOptions import com.android.identity.android.mdoc.transport.DataTransportTcp +import com.android.identity.asn1.ASN1Integer import com.android.identity.cbor.Bstr import com.android.identity.cbor.Cbor.encode import com.android.identity.cbor.CborArray @@ -41,9 +42,10 @@ import com.android.identity.crypto.Crypto.createEcPrivateKey import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey import com.android.identity.crypto.EcPublicKey +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.create +import com.android.identity.crypto.X509KeyUsage import com.android.identity.mdoc.credential.MdocCredential import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator import com.android.identity.mdoc.mso.StaticAuthDataGenerator @@ -81,6 +83,7 @@ import java.util.Calendar import java.util.concurrent.Executor import java.util.concurrent.Executors import kotlin.random.Random +import kotlin.time.Duration.Companion.days @Suppress("deprecation") class DeviceRetrievalHelperTest { @@ -174,21 +177,21 @@ class DeviceRetrievalHelperTest { msoGenerator.addDigestIdsForNamespace(nameSpaceName, digests) } val validFrom = now() - val validUntil = fromEpochMilliseconds( - validFrom.toEpochMilliseconds() + 5L * 365 * 24 * 60 * 60 * 1000 - ) + val validUntil = validFrom + 5.days documentSignerKey = createEcPrivateKey(EcCurve.P256) - documentSignerCert = X509Cert.create( - documentSignerKey.publicKey, - documentSignerKey, - null, - Algorithm.ES256, - "1", - "CN=State Of Utopia", - "CN=State Of Utopia", - validFrom, - validUntil, setOf(), listOf() + documentSignerCert = X509Cert.Builder( + publicKey = documentSignerKey.publicKey, + signingKey = documentSignerKey, + signatureAlgorithm = documentSignerKey.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=State of Utopia"), + issuer = X500Name.fromName("CN=State of Utopia"), + validFrom = validFrom, + validUntil = validUntil ) + .includeSubjectKeyIdentifier() + .setKeyUsage(setOf(X509KeyUsage.DIGITAL_SIGNATURE)) + .build() val mso = msoGenerator.generate() val taggedEncodedMso = encode(Tagged(24, Bstr(mso))) diff --git a/identity-appsupport/src/commonMain/kotlin/com/android/identity/appsupport/ui/consent/ConsentModalBottomSheet.kt b/identity-appsupport/src/commonMain/kotlin/com/android/identity/appsupport/ui/consent/ConsentModalBottomSheet.kt index a5ff722c3..1e7c8ba80 100644 --- a/identity-appsupport/src/commonMain/kotlin/com/android/identity/appsupport/ui/consent/ConsentModalBottomSheet.kt +++ b/identity-appsupport/src/commonMain/kotlin/com/android/identity/appsupport/ui/consent/ConsentModalBottomSheet.kt @@ -91,6 +91,7 @@ fun ConsentModalBottomSheet( ModalBottomSheet( onDismissRequest = { onCancel() }, sheetState = sheetState, + dragHandle = null, containerColor = MaterialTheme.colorScheme.surface ) { Column( @@ -172,7 +173,7 @@ private fun ButtonSection( @Composable private fun RelyingPartySection(relyingParty: ConsentRelyingParty) { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { if (relyingParty.trustPoint != null) { diff --git a/identity-csa/src/main/java/com/android/identity/securearea/cloud/CloudSecureAreaServer.kt b/identity-csa/src/main/java/com/android/identity/securearea/cloud/CloudSecureAreaServer.kt index b56f42f8e..5f201fc5c 100644 --- a/identity-csa/src/main/java/com/android/identity/securearea/cloud/CloudSecureAreaServer.kt +++ b/identity-csa/src/main/java/com/android/identity/securearea/cloud/CloudSecureAreaServer.kt @@ -1,5 +1,6 @@ package com.android.identity.securearea.cloud +import com.android.identity.asn1.ASN1Integer import com.android.identity.cbor.Cbor import com.android.identity.cbor.CborArray import com.android.identity.cbor.annotation.CborSerializable @@ -8,10 +9,9 @@ import com.android.identity.crypto.Algorithm import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.X509CertificateExtension -import com.android.identity.crypto.create import com.android.identity.crypto.javaX509Certificates import com.android.identity.securearea.AttestationExtension import com.android.identity.securearea.KeyPurpose @@ -233,27 +233,26 @@ class CloudSecureAreaServer( DateTimePeriod(years = 10), TimeZone.currentSystemDefault() ) - val attestationExtension = - X509CertificateExtension( - AttestationExtension.ATTESTATION_OID, - false, - AttestationExtension.encode(request1.deviceChallenge) - ) val cloudBindingKeyAttestation = X509CertChain( listOf( - X509Cert.create( - cloudBindingKey.publicKey, - cloudRootAttestationKey, - null, - cloudRootAttestationKeySignatureAlgorithm, - "1", - "CN=Cloud Secure Area Cloud Binding Key", - cloudRootAttestationKeyIssuer, - cloudBindingKeyValidFrom, - cloudBindingKeyValidUntil, - setOf(), - listOf(attestationExtension) - ), + X509Cert.Builder( + publicKey = cloudBindingKey.publicKey, + signingKey = cloudRootAttestationKey, + signatureAlgorithm = cloudRootAttestationKeySignatureAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Cloud Secure Area Cloud Binding Key"), + issuer = X500Name.fromName(cloudRootAttestationKeyIssuer), + validFrom = cloudBindingKeyValidFrom, + validUntil = cloudBindingKeyValidUntil + ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(cloudRootAttestationKeyCertification.certificates[0]) + .addExtension( + oid = AttestationExtension.ATTESTATION_OID, + critical = false, + value = AttestationExtension.encode(request1.deviceChallenge) + ) + .build() ) + cloudRootAttestationKeyCertification.certificates ) state.cloudBindingKey = cloudBindingKey.toCoseKey() @@ -477,25 +476,24 @@ class CloudSecureAreaServer( val keyInfo = secureArea.getKeyInfo("CloudKey") - val attestationCert = X509Cert.create( - keyInfo.publicKey, - attestationKey, - attestationKeyCertification.certificates[0], - attestationKeySignatureAlgorithm, - "1", - "CN=Cloud Secure Area Key", - attestationKeyIssuer, - Instant.fromEpochMilliseconds(state.validFromMillis), - Instant.fromEpochMilliseconds(state.validUntilMillis), - setOf(), - listOf( - X509CertificateExtension( - AttestationExtension.ATTESTATION_OID, - false, - AttestationExtension.encode(state.challenge!!) - ) + val attestationCert = X509Cert.Builder( + publicKey = keyInfo.publicKey, + signingKey = attestationKey, + signatureAlgorithm = attestationKeySignatureAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Cloud Secure Area Key"), + issuer = X500Name.fromName(attestationKeyIssuer), + validFrom = Instant.fromEpochMilliseconds(state.validFromMillis), + validUntil = Instant.fromEpochMilliseconds(state.validUntilMillis) ) - ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(attestationKeyCertification.certificates[0]) + .addExtension( + oid = AttestationExtension.ATTESTATION_OID, + critical = false, + value = AttestationExtension.encode(state.challenge!!) + ) + .build() state.cloudKeyStorage = storageEngine.toCbor() val response1 = CreateKeyResponse1( diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/authenticationUtilities.kt b/identity-issuance/src/main/java/com/android/identity/issuance/authenticationUtilities.kt index 3c4ceb808..a0f02feb1 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/authenticationUtilities.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/authenticationUtilities.kt @@ -31,20 +31,6 @@ import org.bouncycastle.asn1.ASN1OctetString private const val TAG = "authenticationUtilities" -private fun X509CertChain.validate() { - val certs = certificates - // Check that all the certificates sign each other... - for (n in 0 until certs.size - 1) { - val cert = certs[n] - val nextCert = certs[n + 1] - try { - cert.verify(nextCert) - } catch (e: Throwable) { - throw IllegalArgumentException("Attestation error: error validating certificate chain", e) - } - } -} - fun validateAndroidKeyAttestation( chain: X509CertChain, nonce: ByteString?, @@ -52,7 +38,9 @@ fun validateAndroidKeyAttestation( requireVerifiedBootGreen: Boolean, requireAppSignatureCertificateDigests: List, ) { - chain.validate() + check(chain.validate()) { + "Certificate chain did not validate" + } val x509certs = chain.javaX509Certificates val rootCertificatePublicKey = x509certs.last().publicKey @@ -117,7 +105,9 @@ fun validateCloudKeyAttestation( nonce: ByteString, trustedRootKeys: Set ) { - chain.validate() + check(chain.validate()) { + "Certificate chain did not validate" + } val certificates = chain.certificates val leafX509Cert = certificates.first().javaX509Certificate val extensionDerEncodedString = leafX509Cert.getExtensionValue(AttestationExtension.ATTESTATION_OID) diff --git a/identity-mdoc/src/commonMain/kotlin/com/android/identity/mdoc/util/MdocUtil.kt b/identity-mdoc/src/commonMain/kotlin/com/android/identity/mdoc/util/MdocUtil.kt index 45da31b57..1479eba9b 100644 --- a/identity-mdoc/src/commonMain/kotlin/com/android/identity/mdoc/util/MdocUtil.kt +++ b/identity-mdoc/src/commonMain/kotlin/com/android/identity/mdoc/util/MdocUtil.kt @@ -15,6 +15,14 @@ */ package com.android.identity.mdoc.util +import com.android.identity.asn1.ASN1 +import com.android.identity.asn1.ASN1Encoding +import com.android.identity.asn1.ASN1Integer +import com.android.identity.asn1.ASN1ObjectIdentifier +import com.android.identity.asn1.ASN1Sequence +import com.android.identity.asn1.ASN1TagClass +import com.android.identity.asn1.ASN1TaggedObject +import com.android.identity.asn1.OID import com.android.identity.cbor.Bstr import com.android.identity.cbor.Cbor import com.android.identity.cbor.CborMap @@ -26,9 +34,15 @@ import com.android.identity.document.DocumentRequest.DataElement import com.android.identity.document.NameSpacedData import com.android.identity.crypto.Algorithm import com.android.identity.crypto.Crypto +import com.android.identity.crypto.EcPrivateKey +import com.android.identity.crypto.EcPublicKey +import com.android.identity.crypto.X500Name +import com.android.identity.crypto.X509Cert +import com.android.identity.crypto.X509KeyUsage import com.android.identity.mdoc.mso.StaticAuthDataParser.StaticAuthData import com.android.identity.mdoc.request.DeviceRequestParser import com.android.identity.util.Logger +import kotlinx.datetime.Instant import kotlin.random.Random /** @@ -394,4 +408,212 @@ object MdocUtil { } return Cbor.encode(builder.end().build()) } + + /** + * Generates a self-signed IACA certificate according to ISO/IEC 18013-5:2021 Annex B.1.2. + * + * @param iacaKey the private key. + * @param subject the value to use for subject and issuer, e.g. "CN=Test IACA,C=ZZ". + * @param serial the serial number to use for the certificate. + * @param validFrom the point in time the certificate should be valid from. + * @param validUntil the point in time the certificate should be valid until. + * @param issuerAltNameUrl the issuer alternative name (see RFC 5280 section 4.2.1.7), + * e.g. "http://issuer.example.com/informative/web/page". + * @param crlUrl the URL for revocation (see RFC 5280 section 4.2.1.13). + * @return a [X509Cert] with all the required extensions. + */ + fun generateIacaCertificate( + iacaKey: EcPrivateKey, + subject: X500Name, + serial: ASN1Integer, + validFrom: Instant, + validUntil: Instant, + issuerAltNameUrl: String, + crlUrl: String + ): X509Cert { + return X509Cert.Builder( + publicKey = iacaKey.publicKey, + signingKey = iacaKey, + signatureAlgorithm = iacaKey.curve.defaultSigningAlgorithm, + serialNumber = serial, + subject = subject, + issuer = subject, + validFrom = validFrom, + validUntil = validUntil + ) + .includeSubjectKeyIdentifier() + // From 18013-5 table B.1: critical: Key certificate signature + CRL signature bits set + .setKeyUsage(setOf(X509KeyUsage.CRL_SIGN, X509KeyUsage.KEY_CERT_SIGN)) + // From 18013-5 table B.1: critical, CA=true, pathLenConstraint=0 + .setBasicConstraints(true, 0) + // From 18013-5 table B.1: non-critical, Email or URL + .addExtension( + OID.X509_EXTENSION_ISSUER_ALT_NAME.oid, + false, + ASN1.encode( + ASN1Sequence(listOf( + ASN1TaggedObject( + ASN1TagClass.CONTEXT_SPECIFIC, + ASN1Encoding.PRIMITIVE, + 6, + issuerAltNameUrl.encodeToByteArray() + ) + )) + ) + ) + // From 18013-5 table B.1: non-critical, The ‘reasons’ and ‘cRL Issuer’ + // fields shall not be used. + .addExtension( + OID.X509_EXTENSION_CRL_DISTRIBUTION_POINTS.oid, + false, + ASN1.encode( + ASN1Sequence(listOf( + ASN1Sequence(listOf( + ASN1TaggedObject(ASN1TagClass.CONTEXT_SPECIFIC, ASN1Encoding.CONSTRUCTED, 0, ASN1.encode( + ASN1TaggedObject(ASN1TagClass.CONTEXT_SPECIFIC, ASN1Encoding.CONSTRUCTED, 0, ASN1.encode( + ASN1TaggedObject(ASN1TagClass.CONTEXT_SPECIFIC, ASN1Encoding.PRIMITIVE, 6, + crlUrl.encodeToByteArray() + ) + )) + )) + )) + )) + ) + ) + .build() + } + + /** + * Generates a Document Signing certificate according to ISO/IEC 18013-5:2021 Annex B.1.4. + * + * @param iacaCert the IACA certificate. + * @param iacaKey the private key for the IACA certificate. + * @param dsKey the public part of the DS key. + * @param subject the value to use for subject, e.g. "CN=Test DS,C=ZZ". + * @param serial the serial number to use for the certificate. + * @param validFrom the point in time the certificate should be valid from. + * @param validUntil the point in time the certificate should be valid until. + * @return a [X509Cert] with all the required extensions. + */ + fun generateDsCertificate( + iacaCert: X509Cert, + iacaKey: EcPrivateKey, + dsKey: EcPublicKey, + subject: X500Name, + serial: ASN1Integer, + validFrom: Instant, + validUntil: Instant, + ): X509Cert { + return X509Cert.Builder( + publicKey = dsKey, + signingKey = iacaKey, + signatureAlgorithm = iacaKey.curve.defaultSigningAlgorithm, + serialNumber = serial, + subject = subject, + issuer = iacaCert.subject, + validFrom = validFrom, + validUntil = validUntil + ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(iacaCert) + // From 18013-5 table B.3: critical: Key certificate signature + CRL signature bits set + .setKeyUsage(setOf(X509KeyUsage.DIGITAL_SIGNATURE)) + // From 18013-5 table B.3: non-critical, Extended Key usage + .addExtension( + OID.X509_EXTENSION_EXTENDED_KEY_USAGE.oid, + true, + ASN1.encode(ASN1Sequence(listOf( + ASN1ObjectIdentifier(OID.ISO_18013_5_MDL_DS.oid) + ))) + ) + // From 18013-5 table B.3: non-critical, Email or URL + .addExtension( + OID.X509_EXTENSION_ISSUER_ALT_NAME.oid, + false, + iacaCert.getExtensionValue(OID.X509_EXTENSION_ISSUER_ALT_NAME.oid)!! + ) + // From 18013-5 table B.3: non-critical, The ‘reasons’ and ‘cRL Issuer’ + // fields shall not be used. + .addExtension( + OID.X509_EXTENSION_CRL_DISTRIBUTION_POINTS.oid, + false, + iacaCert.getExtensionValue(OID.X509_EXTENSION_CRL_DISTRIBUTION_POINTS.oid)!! + ) + .build() + } + + /** + * Generates a self-signed reader root certificate. + * + * Note that there are no requirements in ISO/IEC 18013-5:2021 for reader certificates. + * + * @param readerRootKey the private key. + * @param subject the value to use for subject and issuer, e.g. "CN=Test Reader Root,C=ZZ". + * @param serial the serial number to use for the certificate. + * @param validFrom the point in time the certificate should be valid from. + * @param validUntil the point in time the certificate should be valid until. + * @return a [X509Cert]. + */ + fun generateReaderRootCertificate( + readerRootKey: EcPrivateKey, + subject: X500Name, + serial: ASN1Integer, + validFrom: Instant, + validUntil: Instant, + ): X509Cert { + return X509Cert.Builder( + publicKey = readerRootKey.publicKey, + signingKey = readerRootKey, + signatureAlgorithm = readerRootKey.curve.defaultSigningAlgorithm, + serialNumber = serial, + subject = subject, + issuer = subject, + validFrom = validFrom, + validUntil = validUntil + ) + .includeSubjectKeyIdentifier() + .setKeyUsage(setOf(X509KeyUsage.CRL_SIGN, X509KeyUsage.KEY_CERT_SIGN)) + .setBasicConstraints(true, 0) + .build() + } + + /** + * Generates a reader certificate. + * + * Note that there are no requirements in ISO/IEC 18013-5:2021 for reader certificates. + * + * @param readerRootCert the reader root certificate. + * @param readerRootKey the private key for the reader root certificate. + * @param readerKey the public part of the reader key. + * @param subject the value to use for subject, e.g. "CN=Test Reader,C=ZZ". + * @param serial the serial number to use for the certificate. + * @param validFrom the point in time the certificate should be valid from. + * @param validUntil the point in time the certificate should be valid until. + * @return a [X509Cert] with all the required extensions. + */ + fun generateReaderCertificate( + readerRootCert: X509Cert, + readerRootKey: EcPrivateKey, + readerKey: EcPublicKey, + subject: X500Name, + serial: ASN1Integer, + validFrom: Instant, + validUntil: Instant, + ): X509Cert { + return X509Cert.Builder( + publicKey = readerKey, + signingKey = readerRootKey, + signatureAlgorithm = readerRootKey.curve.defaultSigningAlgorithm, + serialNumber = serial, + subject = subject, + issuer = readerRootCert.subject, + validFrom = validFrom, + validUntil = validUntil + ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(readerRootCert) + .setKeyUsage(setOf(X509KeyUsage.DIGITAL_SIGNATURE)) + .build() + } + } diff --git a/identity-mdoc/src/commonMain/kotlin/com/android/identity/mdoc/vical/SignedVical.kt b/identity-mdoc/src/commonMain/kotlin/com/android/identity/mdoc/vical/SignedVical.kt index a6409bdaa..a89066c51 100644 --- a/identity-mdoc/src/commonMain/kotlin/com/android/identity/mdoc/vical/SignedVical.kt +++ b/identity-mdoc/src/commonMain/kotlin/com/android/identity/mdoc/vical/SignedVical.kt @@ -1,12 +1,19 @@ package com.android.identity.mdoc.vical +import com.android.identity.cbor.Bstr import com.android.identity.cbor.Cbor import com.android.identity.cbor.CborArray +import com.android.identity.cbor.CborMap import com.android.identity.cbor.DiagnosticOption +import com.android.identity.cbor.Tagged +import com.android.identity.cbor.toDataItem +import com.android.identity.cbor.toDataItemDateTimeString import com.android.identity.cose.Cose import com.android.identity.cose.CoseNumberLabel import com.android.identity.cose.CoseSign1 import com.android.identity.crypto.Algorithm +import com.android.identity.crypto.EcPrivateKey +import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain /** @@ -19,6 +26,59 @@ data class SignedVical( val vical: Vical, val vicalProviderCertificateChain: X509CertChain, ) { + /** + * Generates a VICAL + * + * @param signingKey the key used to sign the VICAL. This must match the public key in the leaf + * certificate in `vicalProviderCertificateChain`. + * @param signingAlgorithm the algorithm used to make the signature + * @return the bytes of the CBOR encoded COSE_Sign1 with the VICAL. + */ + fun generate( + signingKey: EcPrivateKey, + signingAlgorithm: Algorithm + ): ByteArray { + val certInfosBuilder = CborArray.builder() + for (certInfo in vical.certificateInfos) { + val cert = X509Cert(certInfo.certificate) + + val docTypesBuilder = CborArray.builder() + certInfo.docType.forEach { docTypesBuilder.add(it) } + + certInfosBuilder.addMap() + .put("certificate", certInfo.certificate) + .put("serialNumber", Tagged(2, Bstr(cert.serialNumber.value))) + .put("ski", cert.subjectKeyIdentifier!!) + .put("docType", docTypesBuilder.end().build()) + .end() + } + + val vicalBuilder = CborMap.builder() + .put("version", vical.version) + .put("vicalProvider", vical.vicalProvider) + .put("date", vical.date.toDataItemDateTimeString()) + vical.nextUpdate?.let { vicalBuilder.put("nextUpdate", it.toDataItemDateTimeString())} + vical.vicalIssueID?.let { vicalBuilder.put("vicalIssueID", it.toDataItem()) } + vicalBuilder.put("certificateInfos", certInfosBuilder.end().build()) + + val encodedVical = Cbor.encode(vicalBuilder.end().build()) + + val signature = Cose.coseSign1Sign( + key = signingKey, + dataToSign = encodedVical, + includeDataInPayload = true, + signatureAlgorithm = signingAlgorithm, + protectedHeaders = mapOf( + Pair(CoseNumberLabel(Cose.COSE_LABEL_ALG), signingAlgorithm.coseAlgorithmIdentifier.toDataItem()) + ), + unprotectedHeaders = mapOf( + Pair(CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN), vicalProviderCertificateChain.toDataItem()) + ) + ) + + return Cbor.encode(signature.toDataItem()) + } + companion object { private const val TAG = "SignedVical" diff --git a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/request/DeviceRequestGeneratorTest.kt b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/request/DeviceRequestGeneratorTest.kt similarity index 83% rename from identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/request/DeviceRequestGeneratorTest.kt rename to identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/request/DeviceRequestGeneratorTest.kt index 46e8f20af..f1f359c72 100644 --- a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/request/DeviceRequestGeneratorTest.kt +++ b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/request/DeviceRequestGeneratorTest.kt @@ -15,6 +15,7 @@ */ package com.android.identity.mdoc.request +import com.android.identity.asn1.ASN1Integer import com.android.identity.cbor.Bstr import com.android.identity.cbor.Cbor import com.android.identity.cbor.Tstr @@ -23,13 +24,10 @@ import com.android.identity.crypto.Algorithm import com.android.identity.crypto.X509CertChain import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.create import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.security.Security -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals @@ -41,27 +39,20 @@ import kotlin.test.assertTrue // TODO: Add test which generates the exact bytes of TestVectors#ISO_18013_5_ANNEX_D_DEVICE_REQUEST // -// NOTE: This is a Jvm test b/c we need to X509Cert.create() is JVM-only for now. - class DeviceRequestGeneratorTest { - @BeforeTest - fun setUp() { - Security.insertProviderAt(BouncyCastleProvider(), 1) - } - @Test fun testDeviceRequestBuilder() { val encodedSessionTranscript = Cbor.encode(Bstr(byteArrayOf(0x01, 0x02))) - val mdlItemsToRequest: MutableMap> = HashMap() - val mdlNsItems: MutableMap = HashMap() + val mdlItemsToRequest = mutableMapOf>() + val mdlNsItems = mutableMapOf() mdlNsItems["family_name"] = true mdlNsItems["portrait"] = false mdlItemsToRequest[MDL_NAMESPACE] = mdlNsItems - val aamvaNsItems: MutableMap = HashMap() + val aamvaNsItems = mutableMapOf() aamvaNsItems["real_id"] = false mdlItemsToRequest[AAMVA_NAMESPACE] = aamvaNsItems - val mvrItemsToRequest: MutableMap> = HashMap() - val mvrNsItems: MutableMap = HashMap() + val mvrItemsToRequest = mutableMapOf>() + val mvrNsItems = mutableMapOf() mvrNsItems["vehicle_number"] = true mvrItemsToRequest[MVR_NAMESPACE] = mvrNsItems val readerKey = Crypto.createEcPrivateKey(EcCurve.P256) @@ -69,21 +60,18 @@ class DeviceRequestGeneratorTest { val validUntil = Instant.fromEpochMilliseconds( validFrom.toEpochMilliseconds() + 30L * 24 * 60 * 60 * 1000 ) - val readerCert = X509Cert.create( - readerKey.publicKey, - readerKey, - null, - Algorithm.ES256, - "1", - "CN=Test Key", - "CN=Test Key", - validFrom, - validUntil, - setOf(), - listOf() - ) + val readerCert = X509Cert.Builder( + publicKey = readerKey.publicKey, + signingKey = readerKey, + signatureAlgorithm = Algorithm.ES256, + serialNumber = ASN1Integer(1), + subject = X500Name.fromName("CN=Test Key"), + issuer = X500Name.fromName("CN=Test Key"), + validFrom = validFrom, + validUntil = validUntil + ).build() val readerCertChain = X509CertChain(listOf(readerCert)) - val mdlRequestInfo: MutableMap = HashMap() + val mdlRequestInfo = mutableMapOf() mdlRequestInfo["foo"] = Cbor.encode(Tstr("bar")) mdlRequestInfo["bar"] = Cbor.encode(42.toDataItem()) val encodedDeviceRequest = DeviceRequestGenerator(encodedSessionTranscript) diff --git a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/request/DeviceRequestParserTest.kt b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/request/DeviceRequestParserTest.kt similarity index 92% rename from identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/request/DeviceRequestParserTest.kt rename to identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/request/DeviceRequestParserTest.kt index a484e1f3c..b07c00e36 100644 --- a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/request/DeviceRequestParserTest.kt +++ b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/request/DeviceRequestParserTest.kt @@ -15,6 +15,7 @@ */ package com.android.identity.mdoc.request +import com.android.identity.asn1.ASN1Integer import com.android.identity.cbor.Bstr import com.android.identity.cbor.Cbor import com.android.identity.cbor.Tstr @@ -23,14 +24,12 @@ import com.android.identity.crypto.Algorithm import com.android.identity.crypto.X509CertChain import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.create import com.android.identity.mdoc.TestVectors import com.android.identity.util.fromHex import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.security.Security import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertContentEquals @@ -38,14 +37,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -// NOTE: This is a Jvm test b/c we need to X509Cert.create() is JVM-only for now. - class DeviceRequestParserTest { - @BeforeTest - fun setUp() { - Security.insertProviderAt(BouncyCastleProvider(), 1) - } - @Test fun testDeviceRequestParserWithVectors() { // Strip the #6.24 tag since our APIs expects just the bytes of SessionTranscript. @@ -138,6 +130,12 @@ class DeviceRequestParserTest { } fun testDeviceRequestParserReaderAuthHelper(curve: EcCurve) { + // TODO: use assumeTrue() when available in kotlin-test + if (!Crypto.supportedCurves.contains(curve)) { + println("Curve $curve not supported on platform") + return + } + val encodedSessionTranscript = Cbor.encode(Bstr(byteArrayOf(0x01, 0x02))) val mdlItemsToRequest: MutableMap> = HashMap() val mdlNsItems: MutableMap = HashMap() @@ -150,20 +148,17 @@ class DeviceRequestParserTest { val validUntil = Instant.fromEpochMilliseconds( validFrom.toEpochMilliseconds() + 5L * 365 * 24 * 60 * 60 * 1000 ) - val certificate = X509Cert.create( - readerKey.publicKey, - trustPoint, - null, - Algorithm.ES256, - "42", - "CN=Some Reader Key", - "CN=Some Reader Authority", - validFrom, - validUntil, - setOf(), - listOf() - ) - val readerCertChain = X509CertChain(listOf(certificate)) + val readerCert = X509Cert.Builder( + publicKey = readerKey.publicKey, + signingKey = readerKey, + signatureAlgorithm = Algorithm.ES256, + serialNumber = ASN1Integer(1), + subject = X500Name.fromName("CN=Test Key"), + issuer = X500Name.fromName("CN=Test Key"), + validFrom = validFrom, + validUntil = validUntil + ).build() + val readerCertChain = X509CertChain(listOf(readerCert)) val mdlRequestInfo: MutableMap = HashMap() mdlRequestInfo["foo"] = Cbor.encode(Tstr("bar")) mdlRequestInfo["bar"] = Cbor.encode(42.toDataItem()) @@ -230,7 +225,7 @@ class DeviceRequestParserTest { fun testDeviceRequestParserReaderAuth_Ed448() { testDeviceRequestParserReaderAuthHelper(EcCurve.ED448) } - + // TODO: Have a request signed by an unsupported curve and make sure DeviceRequestParser // fails gracefully.. that is, should successfully parse the request message but the // getReaderAuthenticated() method should return false. diff --git a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/response/DeviceResponseGeneratorTest.kt b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/response/DeviceResponseGeneratorTest.kt similarity index 95% rename from identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/response/DeviceResponseGeneratorTest.kt rename to identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/response/DeviceResponseGeneratorTest.kt index b9b993cc8..965addba6 100644 --- a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/response/DeviceResponseGeneratorTest.kt +++ b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/response/DeviceResponseGeneratorTest.kt @@ -15,6 +15,7 @@ */ package com.android.identity.mdoc.response +import com.android.identity.asn1.ASN1Integer import com.android.identity.cbor.Bstr import com.android.identity.cbor.Cbor import com.android.identity.cbor.DataItem @@ -37,7 +38,7 @@ import com.android.identity.crypto.X509CertChain import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey -import com.android.identity.crypto.create +import com.android.identity.crypto.X500Name import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator import com.android.identity.mdoc.mso.StaticAuthDataGenerator import com.android.identity.mdoc.mso.StaticAuthDataParser @@ -51,8 +52,6 @@ import com.android.identity.storage.EphemeralStorageEngine import com.android.identity.storage.StorageEngine import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.security.Security import kotlin.random.Random import kotlin.test.BeforeTest import kotlin.test.Test @@ -61,8 +60,6 @@ import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue -// NOTE: This is a Jvm test b/c we need to X509Cert.create() is JVM-only for now. - class DeviceResponseGeneratorTest { private lateinit var storageEngine: StorageEngine private lateinit var secureArea: SecureArea @@ -74,19 +71,18 @@ class DeviceResponseGeneratorTest { private lateinit var timeSigned: Instant private lateinit var timeValidityBegin: Instant private lateinit var timeValidityEnd: Instant - private lateinit var documentSignerKey: EcPrivateKey - private lateinit var documentSignerCert: X509Cert - + private lateinit var dsKey: EcPrivateKey + private lateinit var dsCert: X509Cert + @BeforeTest fun setup() { - Security.insertProviderAt(BouncyCastleProvider(), 1) storageEngine = EphemeralStorageEngine() secureAreaRepository = SecureAreaRepository() secureArea = SoftwareSecureArea(storageEngine) secureAreaRepository.addImplementation(secureArea) credentialFactory = CredentialFactory() credentialFactory.addCredentialImplementation(MdocCredential::class) { - document, dataItem -> MdocCredential(document, dataItem) + document, dataItem -> MdocCredential(document, dataItem) } provisionDocument() } @@ -94,7 +90,7 @@ class DeviceResponseGeneratorTest { // This isn't really used, we only use a single domain. private val AUTH_KEY_DOMAIN = "domain" private val MDOC_CREDENTIAL_IDENTIFIER = "MdocCredential" - + private fun provisionDocument() { val documentStore = DocumentStore( storageEngine, @@ -165,20 +161,17 @@ class DeviceResponseGeneratorTest { val validUntil = Instant.fromEpochMilliseconds( validFrom.toEpochMilliseconds() + 5L * 365 * 24 * 60 * 60 * 1000 ) - documentSignerKey = Crypto.createEcPrivateKey(EcCurve.P256) - documentSignerCert = X509Cert.create( - documentSignerKey.publicKey, - documentSignerKey, - null, - Algorithm.ES256, - "1", - "CN=State Of Utopia", - "CN=State Of Utopia", - validFrom, - validUntil, - setOf(), - listOf() - ) + dsKey = Crypto.createEcPrivateKey(EcCurve.P256) + dsCert = X509Cert.Builder( + publicKey = dsKey.publicKey, + signingKey = dsKey, + signatureAlgorithm = Algorithm.ES256, + serialNumber = ASN1Integer(1), + subject = X500Name.fromName("CN=State of Utopia DS Key"), + issuer = X500Name.fromName("CN=State of Utopia DS Key"), + validFrom = validFrom, + validUntil = validUntil + ).build() val mso = msoGenerator.generate() val taggedEncodedMso = Cbor.encode(Tagged(24, Bstr(mso))) @@ -195,12 +188,12 @@ class DeviceResponseGeneratorTest { val unprotectedHeaders = mapOf( Pair( CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN), - X509CertChain(listOf(documentSignerCert)).toDataItem() + X509CertChain(listOf(dsCert)).toDataItem() ) ) val encodedIssuerAuth = Cbor.encode( Cose.coseSign1Sign( - documentSignerKey, + dsKey, taggedEncodedMso, true, Algorithm.ES256, @@ -269,7 +262,7 @@ class DeviceResponseGeneratorTest { // Check the MSO was properly signed. assertEquals(1, doc.issuerCertificateChain.certificates.size.toLong()) - assertEquals(documentSignerCert, doc.issuerCertificateChain.certificates[0]) + assertEquals(dsCert, doc.issuerCertificateChain.certificates[0]) assertEquals(DOC_TYPE, doc.docType) assertEquals(timeSigned, doc.validityInfoSigned) assertEquals(timeValidityBegin, doc.validityInfoValidFrom) @@ -526,7 +519,7 @@ class DeviceResponseGeneratorTest { // Check the MSO was properly signed. assertEquals(1, doc.issuerCertificateChain.certificates.size.toLong()) - assertEquals(documentSignerCert, doc.issuerCertificateChain.certificates[0]) + assertEquals(dsCert, doc.issuerCertificateChain.certificates[0]) assertEquals(DOC_TYPE, doc.docType) assertEquals(timeSigned, doc.validityInfoSigned) assertEquals(timeValidityBegin, doc.validityInfoValidFrom) diff --git a/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/response/DeviceResponseParserTest.kt b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/response/DeviceResponseParserTest.kt index ea840ab7c..326ad9d47 100644 --- a/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/response/DeviceResponseParserTest.kt +++ b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/response/DeviceResponseParserTest.kt @@ -310,4 +310,4 @@ class DeviceResponseParserTest { private const val MDL_DOCTYPE = "org.iso.18013.5.1.mDL" private const val MDL_NAMESPACE = "org.iso.18013.5.1" } -} +} \ No newline at end of file diff --git a/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/util/MdocUtilTest.kt b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/util/MdocUtilTest.kt index c1ee0a763..b5b8c80f9 100644 --- a/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/util/MdocUtilTest.kt +++ b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/util/MdocUtilTest.kt @@ -15,12 +15,18 @@ */ package com.android.identity.mdoc.util +import com.android.identity.asn1.ASN1 +import com.android.identity.asn1.ASN1Integer +import com.android.identity.asn1.OID import com.android.identity.cbor.Cbor import com.android.identity.cbor.DiagnosticOption import com.android.identity.cbor.Tstr import com.android.identity.document.DocumentRequest.DataElement import com.android.identity.document.NameSpacedData import com.android.identity.crypto.Algorithm +import com.android.identity.crypto.Crypto +import com.android.identity.crypto.EcCurve +import com.android.identity.crypto.X500Name import com.android.identity.mdoc.TestVectors import com.android.identity.mdoc.mso.MobileSecurityObjectParser import com.android.identity.mdoc.request.DeviceRequestParser @@ -30,6 +36,9 @@ import com.android.identity.mdoc.util.MdocUtil.generateIssuerNameSpaces import com.android.identity.mdoc.util.MdocUtil.stripIssuerNameSpaces import com.android.identity.util.fromHex import com.android.identity.util.toHex +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertContentEquals @@ -288,4 +297,100 @@ class MdocUtilTest { assertEquals(intentToRetain, intentToRetain1) } } + + // Checks the correct extensions are present and that they are formatted correctly. + @Test + fun testGenerateIacaCertificate() { + val iacaKey = Crypto.createEcPrivateKey(EcCurve.P384) + val iacaCert = MdocUtil.generateIacaCertificate( + iacaKey = iacaKey, + subject = X500Name.fromName("CN=TEST IACA Certificate,C=XG-US,ST=MA"), + serial = ASN1Integer(1), + validFrom = LocalDateTime(2024, 1, 1, 0, 0, 0, 0).toInstant(TimeZone.UTC), + validUntil = LocalDateTime(2029, 1, 1, 0, 0, 0, 0).toInstant(TimeZone.UTC), + issuerAltNameUrl = "http://www.example.com/issuer", + crlUrl = "http://www.example.com/issuer/crl" + ) + assertEquals( + "BIT STRING (7 bit) 0000011", + ASN1.print(ASN1.decode(iacaCert.getExtensionValue( + OID.X509_EXTENSION_KEY_USAGE.oid)!!)!!).trim() + ) + assertEquals( + """ + SEQUENCE (2 elem) + BOOLEAN true + INTEGER 0 + """.trimIndent(), + ASN1.print(ASN1.decode(iacaCert.getExtensionValue( + OID.X509_EXTENSION_BASIC_CONSTRAINTS.oid)!!)!!).trim() + ) + assertEquals( + """ + SEQUENCE (1 elem) + [6] (1 elem) + (29 byte) 687474703a2f2f7777772e6578616d706c652e636f6d2f697373756572 + """.trimIndent(), + ASN1.print(ASN1.decode(iacaCert.getExtensionValue( + OID.X509_EXTENSION_ISSUER_ALT_NAME.oid)!!)!!).trim() + ) + assertEquals( + """ + SEQUENCE (1 elem) + SEQUENCE (1 elem) + [0] (1 elem) + [0] (1 elem) + [6] (1 elem) + (33 byte) 687474703a2f2f7777772e6578616d706c652e636f6d2f6973737565722f63726c + """.trimIndent(), + ASN1.print(ASN1.decode(iacaCert.getExtensionValue( + OID.X509_EXTENSION_CRL_DISTRIBUTION_POINTS.oid)!!)!!).trim() + ) + } + + @Test + fun testGenerateDsCertificate() { + val iacaKey = Crypto.createEcPrivateKey(EcCurve.P384) + val iacaCert = MdocUtil.generateIacaCertificate( + iacaKey = iacaKey, + subject = X500Name.fromName("CN=TEST IACA Certificate,C=XG-US,ST=MA"), + serial = ASN1Integer(1), + validFrom = LocalDateTime(2024, 1, 1, 0, 0, 0, 0).toInstant(TimeZone.UTC), + validUntil = LocalDateTime(2029, 1, 1, 0, 0, 0, 0).toInstant(TimeZone.UTC), + issuerAltNameUrl = "http://www.example.com/issuer", + crlUrl = "http://www.example.com/issuer/crl" + ) + val dsKey = Crypto.createEcPrivateKey(EcCurve.P384) + val dsCert = MdocUtil.generateDsCertificate( + iacaCert = iacaCert, + iacaKey = iacaKey, + dsKey = dsKey.publicKey, + subject = X500Name.fromName("CN=TEST DS Certificate,C=XG-US,ST=MA"), + serial = ASN1Integer(1), + validFrom = LocalDateTime(2024, 1, 1, 0, 0, 0, 0).toInstant(TimeZone.UTC), + validUntil = LocalDateTime(2029, 1, 1, 0, 0, 0, 0).toInstant(TimeZone.UTC), + ) + assertEquals( + "BIT STRING (7 bit) 0000011", + ASN1.print(ASN1.decode(iacaCert.getExtensionValue( + OID.X509_EXTENSION_KEY_USAGE.oid)!!)!!).trim() + ) + assertEquals( + """ + SEQUENCE (2 elem) + BOOLEAN true + INTEGER 0 + """.trimIndent(), + ASN1.print(ASN1.decode(iacaCert.getExtensionValue( + OID.X509_EXTENSION_BASIC_CONSTRAINTS.oid)!!)!!).trim() + ) + assertContentEquals( + dsCert.getExtensionValue(OID.X509_EXTENSION_ISSUER_ALT_NAME.oid)!!, + iacaCert.getExtensionValue(OID.X509_EXTENSION_ISSUER_ALT_NAME.oid)!! + ) + assertContentEquals( + dsCert.getExtensionValue(OID.X509_EXTENSION_CRL_DISTRIBUTION_POINTS.oid)!!, + iacaCert.getExtensionValue(OID.X509_EXTENSION_CRL_DISTRIBUTION_POINTS.oid)!! + ) + } } diff --git a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/vical/VicalGeneratorTest.kt b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/vical/VicalGeneratorTest.kt similarity index 72% rename from identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/vical/VicalGeneratorTest.kt rename to identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/vical/VicalGeneratorTest.kt index 1e6d43d63..a304a2c0e 100644 --- a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/vical/VicalGeneratorTest.kt +++ b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/vical/VicalGeneratorTest.kt @@ -1,55 +1,52 @@ package com.android.identity.mdoc.vical +import com.android.identity.asn1.ASN1Integer import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.X509CertificateCreateOption -import com.android.identity.crypto.create import kotlinx.datetime.Clock import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.minutes -import kotlin.time.Duration.Companion.seconds class VicalGeneratorTest { private fun createSelfsignedCert( key: EcPrivateKey, - subject: String + subjectAndIssuer: X500Name ): X509Cert { val now = Clock.System.now() val validFrom = now - 10.minutes val validUntil = now + 10.minutes - return X509Cert.create( + + return X509Cert.Builder( publicKey = key.publicKey, signingKey = key, - signingKeyCertificate = null, signatureAlgorithm = key.curve.defaultSigningAlgorithm, - serial = "1", - subject = subject, - issuer = subject, + serialNumber = ASN1Integer(1), + subject = subjectAndIssuer, + issuer = subjectAndIssuer, validFrom = validFrom, - validUntil = validUntil, - options = setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_AS_SUBJECT_KEY_IDENTIFIER - ), - additionalExtensions = listOf() - ) + validUntil = validUntil + ).includeSubjectKeyIdentifier().build() } @Test fun testVicalGenerator() { val vicalKey = Crypto.createEcPrivateKey(EcCurve.P256) - val vicalCert = createSelfsignedCert(vicalKey, "CN=Test VICAL") + val vicalCert = createSelfsignedCert(vicalKey, X500Name.fromName("CN=Test VICAL")) - val issuer1Cert = createSelfsignedCert(Crypto.createEcPrivateKey(EcCurve.P256), "CN=Issuer 1 IACA") - val issuer2Cert = createSelfsignedCert(Crypto.createEcPrivateKey(EcCurve.P256), "CN=Issuer 2 IACA") - val issuer3Cert = createSelfsignedCert(Crypto.createEcPrivateKey(EcCurve.P256), "CN=Issuer 3 IACA") + val issuer1Cert = createSelfsignedCert( + Crypto.createEcPrivateKey(EcCurve.P256), X500Name.fromName("CN=Issuer 1 IACA")) + val issuer2Cert = createSelfsignedCert( + Crypto.createEcPrivateKey(EcCurve.P256), X500Name.fromName("CN=Issuer 2 IACA")) + val issuer3Cert = createSelfsignedCert( + Crypto.createEcPrivateKey(EcCurve.P256), X500Name.fromName("CN=Issuer 3 IACA")) val vicalDate = Clock.System.now() val vicalNextUpdate = vicalDate + 30.days @@ -99,22 +96,34 @@ class VicalGeneratorTest { assertEquals(vicalIssueID, decodedSignedVical.vical.vicalIssueID) assertEquals(3, decodedSignedVical.vical.certificateInfos.size) - assertContentEquals(issuer1Cert.encodedCertificate, - decodedSignedVical.vical.certificateInfos[0].certificate) - assertContentEquals(listOf("org.iso.18013.5.1.mDL"), - decodedSignedVical.vical.certificateInfos[0].docType) + assertContentEquals( + issuer1Cert.encodedCertificate, + decodedSignedVical.vical.certificateInfos[0].certificate + ) + assertContentEquals( + listOf("org.iso.18013.5.1.mDL"), + decodedSignedVical.vical.certificateInfos[0].docType + ) assertEquals(null, decodedSignedVical.vical.certificateInfos[0].certificateProfiles) - assertContentEquals(issuer2Cert.encodedCertificate, - decodedSignedVical.vical.certificateInfos[1].certificate) - assertContentEquals(listOf("org.iso.18013.5.1.mDL"), - decodedSignedVical.vical.certificateInfos[1].docType) + assertContentEquals( + issuer2Cert.encodedCertificate, + decodedSignedVical.vical.certificateInfos[1].certificate + ) + assertContentEquals( + listOf("org.iso.18013.5.1.mDL"), + decodedSignedVical.vical.certificateInfos[1].docType + ) assertEquals(null, decodedSignedVical.vical.certificateInfos[1].certificateProfiles) - assertContentEquals(issuer3Cert.encodedCertificate, - decodedSignedVical.vical.certificateInfos[2].certificate) - assertContentEquals(listOf("org.iso.18013.5.1.mDL", "eu.europa.ec.eudi.pid.1"), - decodedSignedVical.vical.certificateInfos[2].docType) + assertContentEquals( + issuer3Cert.encodedCertificate, + decodedSignedVical.vical.certificateInfos[2].certificate + ) + assertContentEquals( + listOf("org.iso.18013.5.1.mDL", "eu.europa.ec.eudi.pid.1"), + decodedSignedVical.vical.certificateInfos[2].docType + ) assertEquals(null, decodedSignedVical.vical.certificateInfos[2].certificateProfiles) } } \ No newline at end of file diff --git a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/vical/VicalParserTest.kt b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/vical/VicalParserTest.kt similarity index 98% rename from identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/vical/VicalParserTest.kt rename to identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/vical/VicalParserTest.kt index df7bfce7d..06f7c8838 100644 --- a/identity-mdoc/src/jvmTest/kotlin/com/android/identity/mdoc/vical/VicalParserTest.kt +++ b/identity-mdoc/src/commonTest/kotlin/com/android/identity/mdoc/vical/VicalParserTest.kt @@ -1,12 +1,16 @@ package com.android.identity.mdoc.vical import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.javaX509Certificate import com.android.identity.util.fromBase64Url +import kotlin.collections.joinToString import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertNull +import kotlin.text.replace +import kotlin.text.toRegex +import kotlin.text.trimIndent +import kotlin.toString class VicalParserTest { @@ -936,8 +940,8 @@ yPxFAiAaQMxnrcRJopU6SRrNTq1x29UlFJdaE7XHvdXu1sXnDA== assertEquals(1, signedVical.vicalProviderCertificateChain.certificates.size) // ... and this certificate is signed by the VICAL root - signedVical.vicalProviderCertificateChain.certificates.get(0).javaX509Certificate - .verify(X509Cert.fromPem(AUSTROADS_VICAL_ROOT_PEM).javaX509Certificate.publicKey) + signedVical.vicalProviderCertificateChain.certificates.get(0) + .verify(X509Cert.fromPem(AUSTROADS_VICAL_ROOT_PEM).ecPublicKey) // Check VICAL data val v = signedVical.vical @@ -955,7 +959,7 @@ yPxFAiAaQMxnrcRJopU6SRrNTq1x29UlFJdaE7XHvdXu1sXnDA== val ci = v.certificateInfos.get(1) assertEquals( X509Cert.fromPem( - """ + """ -----BEGIN CERTIFICATE----- MIICujCCAj+gAwIBAgIQWlUtc8+HqDS3PvCqXIlyYDAKBggqhkjOPQQDAzA5MSow KAYDVQQDDCFPV0YgSWRlbnRpdHkgQ3JlZGVudGlhbCBURVNUIElBQ0ExCzAJBgNV @@ -973,16 +977,18 @@ ZGVudGlhbDAKBggqhkjOPQQDAwNpADBmAjEAil9jZ+deFSg1/ESWDEuA3gSU43XC O2t4MirhUlQqSRYlOVBlD0sel7tyuiSPxEldAjEA1eTa/5yCZ67jjg6f2gbbJ8Zz Mbff+DlHy77+wXISb35NiZ8FdVHgC2ut4fDQTRN4 -----END CERTIFICATE----- - """.trimIndent()), - X509Cert(ci.certificate)) - val ciCert = X509Cert(ci.certificate).javaX509Certificate + """.trimIndent() + ), + X509Cert(ci.certificate) + ) + val ciCert = X509Cert(ci.certificate) assertEquals( - "C=ZZ, CN=OWF Identity Credential TEST IACA", - ciCert.subjectX500Principal.toString() + "C=ZZ,CN=OWF Identity Credential TEST IACA", + ciCert.subject.name ) assertEquals( - "C=ZZ, CN=OWF Identity Credential TEST IACA", - ciCert.issuerX500Principal.toString() + "C=ZZ,CN=OWF Identity Credential TEST IACA", + ciCert.issuer.name ) assertEquals( "org.iso.18013.5.1.mDL, org.iso.23220.photoid.1, org.micov.1, org.iso.7367.1.mVRC", @@ -1036,16 +1042,18 @@ ZWJTZXJ2aWNlcy9DUkwvbURML3Jldm9jYXRpb25zLmNybDAQBgkrBgEEAYPFIQEE A01EUDAKBggqhkjOPQQDAgNIADBFAiEAnX3+E4E5dQ+5G1rmStJTW79ZAiDTabyL 8lJuYL/nDxMCIHHkAyIJcQlQmKDUVkBr3heUd5N9Y8GWdbWnbHuwe7Om -----END CERTIFICATE----- - """.trimIndent()), - X509Cert(ci.certificate)) - val ciCert = X509Cert(ci.certificate).javaX509Certificate + """.trimIndent() + ), + X509Cert(ci.certificate) + ) + val ciCert = X509Cert(ci.certificate) assertEquals( - "CN=Fast Enterprises Root, O=Maryland MVA, L=Glen Burnie, C=US, ST=US-MD", - ciCert.subjectX500Principal.toString() + "CN=Fast Enterprises Root,O=Maryland MVA,L=Glen Burnie,C=US,ST=US-MD", + ciCert.subject.name ) assertEquals( - "CN=Fast Enterprises Root, O=Maryland MVA, L=Glen Burnie, C=US, ST=US-MD", - ciCert.issuerX500Principal.toString() + "CN=Fast Enterprises Root,O=Maryland MVA,L=Glen Burnie,C=US,ST=US-MD", + ciCert.issuer.name ) assertEquals( "org.iso.18013.5.1.mDL", diff --git a/identity-mdoc/src/jvmMain/kotlin/com/android/identity/mdoc/vical/SignedVicalJvm.kt b/identity-mdoc/src/jvmMain/kotlin/com/android/identity/mdoc/vical/SignedVicalJvm.kt deleted file mode 100644 index c2bbe49ea..000000000 --- a/identity-mdoc/src/jvmMain/kotlin/com/android/identity/mdoc/vical/SignedVicalJvm.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.android.identity.mdoc.vical - -import com.android.identity.cbor.Bstr -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborArray -import com.android.identity.cbor.CborMap -import com.android.identity.cbor.Tagged -import com.android.identity.cbor.toDataItem -import com.android.identity.cbor.toDataItemDateTimeString -import com.android.identity.cose.Cose -import com.android.identity.cose.CoseNumberLabel -import com.android.identity.crypto.Algorithm -import com.android.identity.crypto.EcPrivateKey -import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.javaX509Certificate -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.SubjectKeyIdentifier -import java.security.cert.X509Certificate - -// This is currently Java-only because we need Java-only functionality in X509Certificate - -/** - * Generates a VICAL - * - * @param signingKey the key used to sign the VICAL. This must match the public key in the leaf - * certificate in `vicalProviderCertificateChain`. - * @param signingAlgorithm the algorithm used to make the signature - * @return the bytes of the CBOR encoded COSE_Sign1 with the VICAL. - */ -fun SignedVical.generate( - signingKey: EcPrivateKey, - signingAlgorithm: Algorithm -): ByteArray { - val certInfosBuilder = CborArray.builder() - for (certInfo in vical.certificateInfos) { - val javaCert = X509Cert(certInfo.certificate).javaX509Certificate - - val docTypesBuilder = CborArray.builder() - certInfo.docType.forEach { docTypesBuilder.add(it) } - - certInfosBuilder.addMap() - .put("certificate", certInfo.certificate) - .put("serialNumber", Tagged(2, Bstr(javaCert.serialNumber.toByteArray()))) - .put("ski", javaCert.subjectKeyIdentifier) - .put("docType", docTypesBuilder.end().build()) - .end() - } - - val vicalBuilder = CborMap.builder() - .put("version", vical.version) - .put("vicalProvider", vical.vicalProvider) - .put("date", vical.date.toDataItemDateTimeString()) - vical.nextUpdate?.let { vicalBuilder.put("nextUpdate", it.toDataItemDateTimeString())} - vical.vicalIssueID?.let { vicalBuilder.put("vicalIssueID", it.toDataItem()) } - vicalBuilder.put("certificateInfos", certInfosBuilder.end().build()) - - val encodedVical = Cbor.encode(vicalBuilder.end().build()) - - val signature = Cose.coseSign1Sign( - key = signingKey, - dataToSign = encodedVical, - includeDataInPayload = true, - signatureAlgorithm = signingAlgorithm, - protectedHeaders = mapOf( - Pair(CoseNumberLabel(Cose.COSE_LABEL_ALG), signingAlgorithm.coseAlgorithmIdentifier.toDataItem()) - ), - unprotectedHeaders = mapOf( - Pair(CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN), vicalProviderCertificateChain.toDataItem()) - ) - ) - - return Cbor.encode(signature.toDataItem()) -} - -/** - * Get the Subject Key Identifier Extension from the X509 certificate. - */ -private val X509Certificate.subjectKeyIdentifier: ByteArray - get() { - val extensionValue = this.getExtensionValue(Extension.subjectKeyIdentifier.id) - ?: throw IllegalArgumentException("No SubjectKeyIdentifier extension") - val octets = DEROctetString.getInstance(extensionValue).octets - val subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets) - return subjectKeyIdentifier.keyIdentifier - } diff --git a/identity-sdjwt/src/test/java/com/android/identity/sdjwt/SdJwtVcTest.kt b/identity-sdjwt/src/test/java/com/android/identity/sdjwt/SdJwtVcTest.kt index d49c4e518..09bf25bb5 100644 --- a/identity-sdjwt/src/test/java/com/android/identity/sdjwt/SdJwtVcTest.kt +++ b/identity-sdjwt/src/test/java/com/android/identity/sdjwt/SdJwtVcTest.kt @@ -1,11 +1,13 @@ package com.android.identity.sdjwt +import com.android.identity.asn1.ASN1Integer import com.android.identity.credential.CredentialFactory import com.android.identity.crypto.Algorithm import com.android.identity.crypto.X509Cert import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve -import com.android.identity.crypto.create +import com.android.identity.crypto.X500Name +import com.android.identity.crypto.X509KeyUsage import com.android.identity.document.Document import com.android.identity.document.DocumentStore import com.android.identity.sdjwt.SdJwtVerifiableCredential.AttributeNotDisclosedException @@ -109,21 +111,20 @@ class SdJwtVcTest { val issuerKey = Crypto.createEcPrivateKey(EcCurve.P256) val validFrom = Clock.System.now() - val validUntil = Instant.fromEpochMilliseconds( - validFrom.toEpochMilliseconds() + 5L * 365 * 24 * 60 * 60 * 1000 + val validUntil = validFrom + 5.days + issuerCert = X509Cert.Builder( + publicKey = issuerKey.publicKey, + signingKey = issuerKey, + signatureAlgorithm = issuerKey.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=State of Utopia"), + issuer = X500Name.fromName("CN=State of Utopia"), + validFrom = validFrom, + validUntil = validUntil ) - issuerCert = X509Cert.create( - issuerKey.publicKey, - issuerKey, - null, - Algorithm.ES256, - "1", - "CN=State Of Utopia", - "CN=State Of Utopia", - validFrom, - validUntil, setOf(), listOf() - ) - + .includeSubjectKeyIdentifier() + .setKeyUsage(setOf(X509KeyUsage.DIGITAL_SIGNATURE)) + .build() // Issuer knows that it will use ECDSA with SHA-256. // Public keys and cert chains will be at https://example-issuer.com/... diff --git a/identity/SwiftBridge/SwiftBridge/SwiftBridge.swift b/identity/SwiftBridge/SwiftBridge/SwiftBridge.swift index f0056f19d..2f908da7e 100644 --- a/identity/SwiftBridge/SwiftBridge/SwiftBridge.swift +++ b/identity/SwiftBridge/SwiftBridge/SwiftBridge.swift @@ -5,6 +5,11 @@ import LocalAuthentication import DeviceCheck @objc public class SwiftBridge : NSObject { + @objc(sha1:) public class func sha1(data: Data) -> Data { + let hashed = Insecure.SHA1.hash(data: data) + return Data(hashed) + } + @objc(sha256:) public class func sha256(data: Data) -> Data { let hashed = SHA256.hash(data: data) return Data(hashed) diff --git a/identity/native/SwiftCrypto/KotlinWrappers.swift b/identity/native/SwiftCrypto/KotlinWrappers.swift deleted file mode 100644 index d76203096..000000000 --- a/identity/native/SwiftCrypto/KotlinWrappers.swift +++ /dev/null @@ -1,287 +0,0 @@ -import CryptoKit -import Foundation -import Security - -@objc public class SwiftCrypto : NSObject { - @objc(sha256:) public class func sha256(data: Data) -> Data { - let hashed = SHA256.hash(data: data) - return Data(hashed) - } - - @objc(sha384:) public class func sha384(data: Data) -> Data { - let hashed = SHA384.hash(data: data) - return Data(hashed) - } - - @objc(sha512:) public class func sha512(data: Data) -> Data { - let hashed = SHA512.hash(data: data) - return Data(hashed) - } - - @objc(hmacSha256: :) public class func hmacSha256(key: Data, data: Data) -> Data { - let symmetricKey = SymmetricKey(data: key) - let mac = HMAC.authenticationCode(for: data, using: symmetricKey) - return Data(mac) - } - - @objc(hmacSha384: :) public class func hmacSha384(key: Data, data: Data) -> Data { - let symmetricKey = SymmetricKey(data: key) - let mac = HMAC.authenticationCode(for: data, using: symmetricKey) - return Data(mac) - } - - @objc(hmacSha512: :) public class func hmacSha512(key: Data, data: Data) -> Data { - let symmetricKey = SymmetricKey(data: key) - let mac = HMAC.authenticationCode(for: data, using: symmetricKey) - return Data(mac) - } - - @objc(aesGcmEncrypt: : :) public class func aesGcmEncrypt(key: Data, plainText: Data, nonce: Data) -> Data { - let symmetricKey = SymmetricKey(data: key) - let sealedBox = try! AES.GCM.seal(plainText, using: symmetricKey, nonce: AES.GCM.Nonce(data: nonce)) - var ret = sealedBox.ciphertext - ret.append(sealedBox.tag) - return ret - } - - @objc(aesGcmDecrypt: : :) public class func aesGcmDecrypt(key: Data, cipherText: Data, nonce: Data) -> Data? { - let symmetricKey = SymmetricKey(data: key) - var combined = nonce - combined.append(cipherText) - let sealedBox = try! AES.GCM.SealedBox(combined: combined) - do { - return try AES.GCM.open(sealedBox, using: symmetricKey) - } catch { - return nil - } - } - - @objc(hkdf: : : : :) public class func hkdf(hashLen: Int, ikm: Data, salt: Data, info: Data, size: Int) -> Data? { - guard #available(iOS 14.0, *) else { - return nil - } - let inputKeyMaterial = SymmetricKey(data: ikm) - let res: SymmetricKey - switch (hashLen) { - case 32: - res = HKDF.deriveKey( - inputKeyMaterial: inputKeyMaterial, - salt: salt, - info: info, - outputByteCount: size - ) - break - case 48: - res = HKDF.deriveKey( - inputKeyMaterial: inputKeyMaterial, - salt: salt, - info: info, - outputByteCount: size - ) - break - case 64: - res = HKDF.deriveKey( - inputKeyMaterial: inputKeyMaterial, - salt: salt, - info: info, - outputByteCount: size - ) - break - default: - return nil - } - return res.withUnsafeBytes { - return Data(Array($0)) - } - } - - static let CURVE_P256 = 1 - static let CURVE_P384 = 2 - static let CURVE_P521 = 3 - - @objc(createEcPrivateKey:) public class func createEcPrivateKey(curve: Int) -> Array { - switch (curve) { - case CURVE_P256: - let key = P256.Signing.PrivateKey.init(compactRepresentable: false) - return [Data(key.rawRepresentation), Data(key.publicKey.rawRepresentation)] - case CURVE_P384: - let key = P384.Signing.PrivateKey.init(compactRepresentable: false) - return [Data(key.rawRepresentation), Data(key.publicKey.rawRepresentation)] - case CURVE_P521: - let key = P521.Signing.PrivateKey.init(compactRepresentable: false) - return [Data(key.rawRepresentation), Data(key.publicKey.rawRepresentation)] - default: - return [] - } - } - - @objc(ecPublicKeyToPem: :) public class func ecPublicKeyToPem(curve: Int, rawRepresentation: Data) -> String? { - guard #available(iOS 14.0, *) else { - return nil - } - switch (curve) { - case CURVE_P256: - let key = try! P256.Signing.PublicKey(rawRepresentation: rawRepresentation) - return key.pemRepresentation - case CURVE_P384: - let key = try! P384.Signing.PublicKey(rawRepresentation: rawRepresentation) - return key.pemRepresentation - case CURVE_P521: - let key = try! P521.Signing.PublicKey(rawRepresentation: rawRepresentation) - return key.pemRepresentation - default: - return "" - } - } - - @objc(ecPublicKeyFromPem: :) public class func ecPublicKeyFromPem(curve: Int, pemRepresentation: String) -> Data? { - guard #available(iOS 14.0, *) else { - return nil - } - switch (curve) { - case CURVE_P256: - let key = try! P256.Signing.PublicKey(pemRepresentation: pemRepresentation) - return Data(key.rawRepresentation) - case CURVE_P384: - let key = try! P384.Signing.PublicKey(pemRepresentation: pemRepresentation) - return Data(key.rawRepresentation) - case CURVE_P521: - let key = try! P521.Signing.PublicKey(pemRepresentation: pemRepresentation) - return Data(key.rawRepresentation) - default: - return nil - } - } - - @objc(ecPrivateKeyToPem: :) public class func ecPrivateKeyToPem(curve: Int, rawRepresentation: Data) -> String? { - guard #available(iOS 14.0, *) else { - return nil - } - switch (curve) { - case CURVE_P256: - let key = try! P256.Signing.PrivateKey(rawRepresentation: rawRepresentation) - return key.pemRepresentation - case CURVE_P384: - let key = try! P384.Signing.PrivateKey(rawRepresentation: rawRepresentation) - return key.pemRepresentation - case CURVE_P521: - let key = try! P521.Signing.PrivateKey(rawRepresentation: rawRepresentation) - return key.pemRepresentation - default: - return "" - } - } - - @objc(ecPrivateKeyFromPem: :) public class func ecPrivateKeyFromPem(curve: Int, pemRepresentation: String) -> Data? { - guard #available(iOS 14.0, *) else { - return nil - } - switch (curve) { - case CURVE_P256: - let key = try! P256.Signing.PrivateKey(pemRepresentation: pemRepresentation) - return Data(key.rawRepresentation) - case CURVE_P384: - let key = try! P384.Signing.PrivateKey(pemRepresentation: pemRepresentation) - return Data(key.rawRepresentation) - case CURVE_P521: - let key = try! P521.Signing.PrivateKey(pemRepresentation: pemRepresentation) - return Data(key.rawRepresentation) - default: - return nil - } - } - - @objc(ecSign: : :) public class func ecSign(privateKeyCurve: Int, privateKeyRepresentation: Data, dataToSign: Data) -> Data? { - switch (privateKeyCurve) { - case CURVE_P256: - let key = try! P256.Signing.PrivateKey(rawRepresentation: privateKeyRepresentation) - let signature = try! key.signature(for: dataToSign) - return Data(signature.rawRepresentation) - case CURVE_P384: - let key = try! P384.Signing.PrivateKey(rawRepresentation: privateKeyRepresentation) - let signature = try! key.signature(for: dataToSign) - return Data(signature.rawRepresentation) - case CURVE_P521: - let key = try! P521.Signing.PrivateKey(rawRepresentation: privateKeyRepresentation) - let signature = try! key.signature(for: dataToSign) - return Data(signature.rawRepresentation) - default: - return nil - } - } - - @objc(ecVerifySignature: : : :) public class func ecVerifySignature(publicKeyCurve: Int, publicKeyRepresentation: Data, dataThatWasSigned: Data, signature: Data) -> Bool { - switch (publicKeyCurve) { - case CURVE_P256: - let key = try! P256.Signing.PublicKey(rawRepresentation: publicKeyRepresentation) - let ecdsaSignature = try! P256.Signing.ECDSASignature(rawRepresentation: signature) - return key.isValidSignature(ecdsaSignature, for: dataThatWasSigned) - case CURVE_P384: - let key = try! P384.Signing.PublicKey(rawRepresentation: publicKeyRepresentation) - let ecdsaSignature = try! P384.Signing.ECDSASignature(rawRepresentation: signature) - return key.isValidSignature(ecdsaSignature, for: dataThatWasSigned) - case CURVE_P521: - let key = try! P521.Signing.PublicKey(rawRepresentation: publicKeyRepresentation) - let ecdsaSignature = try! P521.Signing.ECDSASignature(rawRepresentation: signature) - return key.isValidSignature(ecdsaSignature, for: dataThatWasSigned) - default: - return false - } - } - - @objc(ecKeyAgreement: : :) public class func ecKeyAgreement(privateKeyCurve: Int, privateKeyRepresentation: Data, otherPublicKeyRepresentation: Data) -> Data? { - switch (privateKeyCurve) { - case CURVE_P256: - let key = try! P256.KeyAgreement.PrivateKey(rawRepresentation: privateKeyRepresentation) - let otherKey = try! P256.KeyAgreement.PublicKey(rawRepresentation: otherPublicKeyRepresentation) - let sharedSecret = try! key.sharedSecretFromKeyAgreement(with: otherKey) - return sharedSecret.withUnsafeBytes { return Data(Array($0)) } - case CURVE_P384: - let key = try! P384.KeyAgreement.PrivateKey(rawRepresentation: privateKeyRepresentation) - let otherKey = try! P384.KeyAgreement.PublicKey(rawRepresentation: otherPublicKeyRepresentation) - let sharedSecret = try! key.sharedSecretFromKeyAgreement(with: otherKey) - return sharedSecret.withUnsafeBytes { return Data(Array($0)) } - case CURVE_P521: - let key = try! P521.KeyAgreement.PrivateKey(rawRepresentation: privateKeyRepresentation) - let otherKey = try! P521.KeyAgreement.PublicKey(rawRepresentation: otherPublicKeyRepresentation) - let sharedSecret = try! key.sharedSecretFromKeyAgreement(with: otherKey) - return sharedSecret.withUnsafeBytes { return Data(Array($0)) } - default: - return nil - } - } - - @objc(hpkeEncrypt: : :) public class func hpkeEncrypt(receiverPublicKeyRepresentation: Data, plainText: Data, aad: Data) -> Array { - guard #available(iOS 17.0, *) else { - return [] - } - let receiverKey = try! P256.KeyAgreement.PublicKey(rawRepresentation: receiverPublicKeyRepresentation) - var sender = try! HPKE.Sender(recipientKey: receiverKey, ciphersuite: HPKE.Ciphersuite.P256_SHA256_AES_GCM_256, info: Data()) - let cipherText = try! sender.seal(plainText, authenticating: aad) - return [sender.encapsulatedKey, cipherText] - } - - @objc(hpkeDecrypt: : : :) public class func hpkeDecrypt(receiverPrivateKeyRepresentation: Data, cipherText: Data, aad: Data, encapsulatedPublicKey: Data) -> Data? { - guard #available(iOS 17.0, *) else { - return nil - } - let receiverKey = try! P256.KeyAgreement.PrivateKey(rawRepresentation: receiverPrivateKeyRepresentation) - var receiver = try! HPKE.Recipient(privateKey: receiverKey, ciphersuite: HPKE.Ciphersuite.P256_SHA256_AES_GCM_256, info: Data(), encapsulatedKey: encapsulatedPublicKey) - let plainText = try! receiver.open(cipherText, authenticating: aad) - return plainText - } - - @objc(x509CertGetKey:) public class func x509CertGetKey(encodedX509Cert: Data) -> Data? { - let certificate = SecCertificateCreateWithData(nil, encodedX509Cert as CFData) - if (certificate == nil) { - return nil - } - let key = SecCertificateCopyKey(certificate!) - if (key == nil) { - return nil - } - let data = SecKeyCopyExternalRepresentation(key!, nil) - return data as Data? - } - -} diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1.kt new file mode 100644 index 000000000..8aa86bc6a --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1.kt @@ -0,0 +1,326 @@ +package com.android.identity.asn1 + +import com.android.identity.util.toHex +import kotlinx.io.bytestring.ByteStringBuilder +import kotlin.math.max + +internal data class IdentifierOctets( + val cls: ASN1TagClass, + val enc: ASN1Encoding, + val tag: Int, +) + +/** + * ASN.1 support routines. + * + * This package contains support for ASN.1 with DER encoding according to + * [ITU-T Recommendation X.690](https://www.itu.int/itu-t/recommendations/rec.aspx?rec=x.690). + */ +object ASN1 { + + internal fun appendIdentifierAndLength( + builder: ByteStringBuilder, + cls: ASN1TagClass, + enc: ASN1Encoding, + tag: Int, + length: Int + ) { + if (tag <= 0x1e) { + builder.append((cls.value or enc.value or tag).toByte()) + } else { + builder.append((cls.value or enc.value or 0x1f).toByte()) + val bitLength = Int.SIZE_BITS - tag.countLeadingZeroBits() + val bytesNeeded = max((bitLength + 6) / 7, 1) + for (n in IntRange(0, bytesNeeded - 1).reversed()) { + var digit = tag.shr(n * 7).and(0x7f) + if (n != 0) { + digit = digit.or(0x80) + } + builder.append(digit.and(0xff).toByte()) + } + } + + if (length < 0x80) { + builder.append(length.toByte()) + } else { + val lengthFieldSize = ((Int.SIZE_BITS - length.countLeadingZeroBits()) + 7)/8 + builder.append((0x80 + lengthFieldSize).toByte()) + val encodedLength = byteArrayOf( + (length shr 24).and(0xff).toByte(), + (length shr 16).and(0xff).toByte(), + (length shr 8).and(0xff).toByte(), + (length shr 0).and(0xff).toByte(), + ) + for (b in IntRange(4 - lengthFieldSize, 3)) { + builder.append(encodedLength[b]) + } + } + } + + internal fun appendUniversalTagEncodingLength( + builder: ByteStringBuilder, + tag: Int, + encoding: ASN1Encoding, + length: Int + ) { + appendIdentifierAndLength(builder, ASN1TagClass.UNIVERSAL, encoding, tag, length) + } + + internal fun decodeIdentifierOctets(derEncoded: ByteArray, offset: Int): Pair { + var o = offset + val idOctet0 = derEncoded[o++] + val cls = ASN1TagClass.parse(idOctet0) + val enc = ASN1Encoding.parse(idOctet0) + val tag0 = idOctet0.toInt().and(0x1f) + + val tag = if (tag0 <= 0x1e) { + tag0 + } else { + var result = 0 + do { + val b = derEncoded[o++].toInt().and(0xff) + result = result shl 7 + result = result or b.and(0x7f) + } while (b.and(0x80) != 0) + result + } + return Pair(o, IdentifierOctets(cls, enc, tag)) + } + + internal fun decodeLength(derEncoded: ByteArray, offset: Int): Pair { + val len0 = derEncoded[offset].toInt().and(0xff) + if (len0.and(0x80) == 0) { + // Short-form + return Pair(offset + 1, len0) + } + val numOctets = len0.and(0x7f) + if (numOctets == 0) { + throw IllegalArgumentException("Indeterminate length not supported") + } + var value = 0 + for (n in IntRange(offset + 1, offset + 1 + numOctets - 1)) { + value = value.shl(8).or(derEncoded[n].toInt().and(0xff)) + } + return Pair(offset + 1 + numOctets, value) + } + + internal fun decode(derEncoded: ByteArray, offset: Int): Pair { + val (lengthOffset, idOctets) = decodeIdentifierOctets(derEncoded, offset) + val (contentOffset, length) = decodeLength(derEncoded, lengthOffset) + val content = derEncoded.sliceArray(IntRange(contentOffset, contentOffset + length - 1)) + val nextOffset = contentOffset + length + val decodedObject = if (idOctets.cls == ASN1TagClass.UNIVERSAL) { + when (idOctets.tag) { + // Note: we currently don't support the following tags and will simply + // return an ASN1RawObject() instance instead + // + // - ObjectDescriptor (tag 0x07) + // - EXTERNAL (tag 0x08) + // - REAL (tag 0x09) + // - EMBEDDED PDV (tag 0x0b) + // - RELATIVE-OID (tag 0x0d) + // - DATE (tag 0x1f) + // - TIME-OF-DAY (tag 0x20) + // - DATE-TIME (tag 0x21) + // - DURATION (tag 0x22) + // + ASN1Boolean.TAG_NUMBER -> ASN1Boolean.parse(content) + ASN1Null.TAG_NUMBER -> ASN1Null.parse(content) + ASN1ObjectIdentifier.TAG_NUMBER -> ASN1ObjectIdentifier.parse(content) + ASN1OctetString.TAG_NUMBER -> ASN1OctetString.parse(content) + ASN1BitString.TAG_NUMBER -> ASN1BitString.parse(content) + ASN1Sequence.TAG_NUMBER -> ASN1Sequence.parse(content) + ASN1Set.TAG_NUMBER -> ASN1Set.parse(content) + else -> { + if (ASN1IntegerTag.entries.find { it.tag == idOctets.tag } != null ) { + ASN1Integer.parse(content, idOctets.tag) + } else if (ASN1StringTag.entries.find { it.tag == idOctets.tag } != null) { + ASN1String.parse(content, idOctets.tag) + } else if (ASN1TimeTag.entries.find { it.tag == idOctets.tag } != null) { + ASN1Time.parse(content, idOctets.tag) + } else { + ASN1RawObject(idOctets.cls, idOctets.enc, idOctets.tag, content) + } + } + } + } else { + ASN1TaggedObject.parse(idOctets.cls, idOctets.enc, idOctets.tag, content) + } + return Pair(nextOffset, decodedObject) + } + + /** + * Decodes a single DER encoded value. + * + * @param derEncoded the encoded bytes. + * @return a [ASN1Object]-derived instance. + * @throws IllegalArgumentException if the given bytes are not valid. + */ + fun decode(derEncoded: ByteArray): ASN1Object? { + val (newOffset, obj) = decode(derEncoded, 0) + if (newOffset != derEncoded.size) { + throw IllegalArgumentException( + "${newOffset - derEncoded.size} bytes leftover after decoding" + ) + } + return obj + } + + /** + * Decodes multiple encoded DER values. + * + * @param derEncoded the encoded bytes. + * @return one or more [ASN1Object]-derived instances. + * @throws IllegalArgumentException if the given bytes are not valid. + */ + fun decodeMultiple(derEncoded: ByteArray): List { + val objects = mutableListOf() + var offset = 0 + do { + val (newOffset, obj) = decode(derEncoded, offset) + if (obj != null) { + objects.add(obj) + } + offset = newOffset + } while (offset < derEncoded.size) + return objects + } + + /** + * Encodes a [ASN1Object] instance. + * + * @param obj a [ASN1Object]-derived instance. + * @return the encoded bytes. + */ + fun encode(obj: ASN1Object): ByteArray { + val builder = ByteStringBuilder() + obj.encode(builder) + return builder.toByteString().toByteArray() + } + + private fun print( + sb: StringBuilder, + indent: Int, + obj: ASN1Object + ) { + for (n in IntRange(1, indent)) { + sb.append(" ") + } + when (obj) { + is ASN1Boolean -> { + sb.append("BOOLEAN ${obj.value}\n") + } + is ASN1Integer -> { + val label = when (obj.tag) { + ASN1IntegerTag.INTEGER.tag -> "INTEGER" + ASN1IntegerTag.ENUMERATED.tag -> "ENUMERATED" + else -> throw IllegalArgumentException() + } + // TODO: always print as integer when we have BigInteger support + try { + sb.append("$label ${obj.toLong()}\n") + } catch (e: IllegalStateException) { + sb.append("$label ${obj.value.toHex()}\n") + } + } + is ASN1Null -> { + sb.append("NULL\n") + } + is ASN1ObjectIdentifier -> { + sb.append("OBJECT IDENTIFIER ${obj.oid}") + OID.lookupByOid(obj.oid)?.let { sb.append(" ${it.description}") } + sb.append("\n") + } + is ASN1OctetString -> { + sb.append("OCTET STRING (${obj.value.size} byte) ${obj.value.toHex()}\n") + } + is ASN1BitString -> { + sb.append("BIT STRING (${obj.value.size*8 - obj.numUnusedBits} bit) ${obj.renderBitString()}\n") + } + is ASN1Time -> { + val label = when (obj.tag) { + ASN1TimeTag.GENERALIZED_TIME.tag -> "GeneralizedTime" + ASN1TimeTag.UTC_TIME.tag -> "UTCTime" + else -> throw IllegalArgumentException() + } + sb.append("$label ${obj.value}\n") + } + is ASN1String -> { + val label = when (obj.tag) { + ASN1StringTag.UTF8_STRING.tag -> "UTF8String" + ASN1StringTag.NUMERIC_STRING.tag -> "NumericString" + ASN1StringTag.PRINTABLE_STRING.tag -> "PrintableString" + ASN1StringTag.TELETEX_STRING.tag -> "TeletexString" + ASN1StringTag.VIDEOTEX_STRING.tag -> "VideotexString" + ASN1StringTag.IA5_STRING.tag -> "IA5String" + ASN1StringTag.GRAPHIC_STRING.tag -> "GraphicString" + ASN1StringTag.VISIBLE_STRING.tag -> "VisibleString" + ASN1StringTag.GENERAL_STRING.tag -> "GeneralString" + ASN1StringTag.UNIVERSAL_STRING.tag -> "UniversalString" + ASN1StringTag.CHARACTER_STRING.tag -> "CharacterString" + ASN1StringTag.BMP_STRING.tag -> "BmpString" + else -> throw IllegalArgumentException() + } + sb.append("$label ${obj.value}\n") + } + is ASN1Sequence -> { + sb.append("SEQUENCE (${obj.elements.size} elem)\n") + for (elem in obj.elements) { + print(sb, indent + 2, elem) + } + } + is ASN1Set -> { + sb.append("SET (${obj.elements.size} elem)\n") + for (elem in obj.elements) { + print(sb, indent + 2, elem) + } + } + is ASN1TaggedObject -> { + when (obj.cls) { + ASN1TagClass.UNIVERSAL -> { + sb.append("[UNIVERSAL ${obj.tag}] (1 elem)\n") + } + ASN1TagClass.APPLICATION -> { + sb.append("[APPLICATION ${obj.tag}] (1 elem)\n") + } + ASN1TagClass.CONTEXT_SPECIFIC -> { + sb.append("[${obj.tag}] (1 elem)\n") + } + ASN1TagClass.PRIVATE -> { + sb.append("[PRIVATE ${obj.tag}] (1 elem)\n") + } + } + // Try and decode the content as ASN.1, and if so, print it. If not, + // just print the content + try { + val decodedObject = ASN1.decode(obj.content) + print(sb, indent + 2, decodedObject!!) + } catch (_: Throwable) { + for (n in IntRange(1, indent + 2)) { + sb.append(" ") + } + sb.append("(${obj.content.size} byte) ${obj.content.toHex()}\n") + } + } + is ASN1RawObject -> { + sb.append("UNSUPPORTED TAG class=${obj.cls} encoding=${obj.enc} ") + sb.append("tag=${obj.tag} value=${obj.content.toHex()}") + } + is ASN1PrimitiveValue -> { + //throw IllegalStateException() + } + } + } + + /** + * Pretty-prints a [ASN1Object] instance. + * + * @param obj a [ASN1Object]-derived instance. + * @return the pretty-printed form. + */ + fun print(obj: ASN1Object): String { + val sb = StringBuilder() + print(sb, 0, obj) + return sb.toString() + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1BitString.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1BitString.kt new file mode 100644 index 000000000..3e5309b32 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1BitString.kt @@ -0,0 +1,92 @@ +package com.android.identity.asn1 + +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1BitString( + val numUnusedBits: Int, + val value: ByteArray +): ASN1PrimitiveValue(tag = TAG_NUMBER) { + + constructor(booleanValues: BooleanArray): + this( + if (booleanValues.size == 0) 0 else (8 - (booleanValues.size % 8)), + encodeBooleans(booleanValues) + ) + + override fun encode(builder: ByteStringBuilder) { + ASN1.appendUniversalTagEncodingLength(builder, tag, enc, value.size + 1) + builder.append(numUnusedBits.toByte()) + builder.append(value) + } + + override fun equals(other: Any?): Boolean = other is ASN1BitString && other.value contentEquals value + + override fun hashCode(): Int = value.hashCode() + + override fun toString(): String { + return "ASN1BitString(${renderBitString()})" + } + + fun asBooleans(): BooleanArray { + val result = mutableListOf() + for (n in IntRange(0, value.size*8 - numUnusedBits - 1)) { + val offset = n/8 + val bitNum = 7 - (n - offset*8) + val boolVal = if (value[offset].toInt().and(1.shl(bitNum)) != 0x00) { + true + } else { + false + } + result.add(boolVal) + } + return result.toBooleanArray() + } + + internal fun renderBitString(): String { + val sb = StringBuilder() + for (n in value.indices) { + val start = if (n == value.size - 1) { + numUnusedBits + } else { + 0 + } + val byteValue = value[n] + for (m in IntRange(start, 7).reversed()) { + val bitSet = byteValue.toInt().and(1.shl(m)) != 0 + sb.append(if (bitSet) "1" else "0") + } + } + return sb.toString() + } + + companion object { + const val TAG_NUMBER = 0x03 + + private fun encodeBooleans(booleanValues: BooleanArray): ByteArray { + if (booleanValues.size == 0) { + return byteArrayOf() + } + val encodedBooleans = mutableListOf() + val numBytes = (booleanValues.size + 7)/8 + for (n in IntRange(0, numBytes - 1)) { + var b: Int = 0 + for (m in IntRange(0, 7)) { + val booleanNum = n*8 + m + if (booleanNum < booleanValues.size) { + if (booleanValues[booleanNum]) { + b = b or (1.shl(7 - m)) + } + } + } + encodedBooleans.add(b.and(0xff).toByte()) + } + return encodedBooleans.toByteArray() + } + + fun parse(content: ByteArray): ASN1BitString { + val numUnusedBits = content[0].toInt() + val encodedBits = content.sliceArray(IntRange(1, content.size - 1)) + return ASN1BitString(numUnusedBits, encodedBits) + } + } +} diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Boolean.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Boolean.kt new file mode 100644 index 000000000..62af42455 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Boolean.kt @@ -0,0 +1,35 @@ +package com.android.identity.asn1 + +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1Boolean(val value: Boolean): ASN1PrimitiveValue(tag = TAG_NUMBER) { + + override fun encode(builder: ByteStringBuilder) { + ASN1.appendUniversalTagEncodingLength(builder, TAG_NUMBER, enc, 1) + builder.append(if (value) 0xff.toByte() else 0x00.toByte()) + } + + override fun equals(other: Any?): Boolean = other is ASN1Boolean && value == other.value + + override fun hashCode(): Int = value.hashCode() + + override fun toString(): String { + return "ASN1Boolean($value)" + } + + companion object { + const val TAG_NUMBER = 0x01 + + fun parse(content: ByteArray): ASN1Boolean { + require(content.size == 1) { "Content size is ${content.size}, expected 1" } + val value = when (content[0]) { + 0x00.toByte() -> false + 0xff.toByte() -> true + else -> { + throw IllegalArgumentException("Content value is ${content[0]}, expected 0x00 or 0xff") + } + } + return ASN1Boolean(value) + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Encoding.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Encoding.kt new file mode 100644 index 000000000..13cfbd110 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Encoding.kt @@ -0,0 +1,15 @@ +package com.android.identity.asn1 + +enum class ASN1Encoding(val value: Int) { + PRIMITIVE(0x00), + CONSTRUCTED(0x20) + + ; + + companion object { + internal fun parse(idOctet0: Byte): ASN1Encoding { + val bits = (idOctet0.toInt().and(0x20)) + return ASN1Encoding.entries.first() { it.value == bits } + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Integer.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Integer.kt new file mode 100644 index 000000000..fc5911e9b --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Integer.kt @@ -0,0 +1,111 @@ +package com.android.identity.asn1 + +import com.android.identity.util.toHex +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1Integer( + val value: ByteArray, + tag: Int = ASN1IntegerTag.INTEGER.tag +): ASN1PrimitiveValue(tag = tag) { + + constructor(longValue: Long, + tag: Int = ASN1IntegerTag.INTEGER.tag) + : this(longValue.derEncodeToByteArray(), tag) + + override fun encode(builder: ByteStringBuilder) { + ASN1.appendUniversalTagEncodingLength(builder, tag, enc, value.size) + builder.append(value) + } + + override fun equals(other: Any?): Boolean = + other is ASN1Integer && tag == other.tag && value contentEquals other.value + + override fun hashCode(): Int = value.contentHashCode() + + override fun toString(): String { + return "ASN1Integer(${tag}, ${value.toHex()})" + } + + /** + * Gets the value as a [Long]. + * + * @throws IllegalStateException if the value doesn't fit in a [Long]. + */ + fun toLong(): Long { + if (value.size > 8) { + throw IllegalStateException("Value doesn't fit in a Long") + } + return value.derDecodeAsLong() + } + + companion object { + fun parse(content: ByteArray, tag: Int): ASN1Integer { + return ASN1Integer(content, tag) + } + } +} + +internal fun Long.derEncodeToByteArray(): ByteArray { + var v = this + val bsb = ByteStringBuilder() + for (n in IntRange(0, 7)) { + bsb.append(v.and(0xffL).toByte()) + v = v.shr(8) + } + var value = bsb.toByteString().toByteArray().reversedArray() + if (this >= 0) { + // Remove leading 0x00 + var numRemove = 0 + for (n in IntRange(0, 6)) { + val digit = value[n].toInt().and(0xff) + val nextDigit = value[n + 1].toInt().and(0xff) + if (digit == 0x00 && (nextDigit.and(0x80) == 0)) { + numRemove++ + } else { + break + } + } + return value.sliceArray(IntRange(numRemove, 7)) + } else { + // Remove leading 0xff + var numRemove = 0 + for (n in IntRange(0, 6)) { + val digit = value[n].toInt().and(0xff) + val nextDigit = value[n + 1].toInt().and(0xff) + if (digit == 0xff && (nextDigit.and(0x80) != 0)) { + numRemove++ + } else { + break + } + } + return value.sliceArray(IntRange(numRemove, 7)) + } +} + +internal fun ByteArray.derDecodeAsLong(): Long { + var signPositive = true + if (this.size > 9) { + throw IllegalArgumentException("Cannot decode Long from ByteArray of size ${this.size}") + } else if (this.size == 9) { + if (this[0].toInt() == 0xff) { + signPositive = false + } else { + throw IllegalArgumentException("Illegal sign value ${this[0]}") + } + } else { + if (this[0].toInt().and(0x80) != 0) { + signPositive = false + } + } + + var result = 0L + if (!signPositive && this.size < 8) { + for (n in IntRange(this.size, 7)) { + result = result or 0xffL.shl((this.size - 1 - n)*8) + } + } + for (n in this.indices) { + result = result or this[n].toLong().and(0xff).shl((this.size - 1 - n)*8) + } + return result +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1IntegerTag.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1IntegerTag.kt new file mode 100644 index 000000000..c9088738f --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1IntegerTag.kt @@ -0,0 +1,6 @@ +package com.android.identity.asn1 + +enum class ASN1IntegerTag(val tag: Int) { + INTEGER(0x02), + ENUMERATED(0x0a), +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Null.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Null.kt new file mode 100644 index 000000000..c78ec08d3 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Null.kt @@ -0,0 +1,27 @@ +package com.android.identity.asn1 + +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1Null(): ASN1PrimitiveValue(tag = TAG_NUMBER) { + + override fun encode(builder: ByteStringBuilder) { + ASN1.appendUniversalTagEncodingLength(builder, TAG_NUMBER, enc, 0) + } + + override fun equals(other: Any?): Boolean = other is ASN1Null + + override fun hashCode(): Int = 0 + + override fun toString(): String { + return "ASN1Null()" + } + + companion object { + const val TAG_NUMBER = 0x05 + + fun parse(content: ByteArray): ASN1Null { + require(content.size == 0) { "Content size is ${content.size}, expected 0" } + return ASN1Null() + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Object.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Object.kt new file mode 100644 index 000000000..979d2b5a4 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Object.kt @@ -0,0 +1,19 @@ +package com.android.identity.asn1 + +import kotlinx.io.bytestring.ByteStringBuilder + +/** + * Abstract base class for ASN.1 values. + * + * @property cls the class of the value. + * @property enc the encoding of the value, either constructed or primitive. + * @property tag the tag number. + */ +sealed class ASN1Object( + open val cls: ASN1TagClass, + open val enc: ASN1Encoding, + open val tag: Int +) { + + internal abstract fun encode(builder: ByteStringBuilder) +} diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1ObjectIdentifier.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1ObjectIdentifier.kt new file mode 100644 index 000000000..853e002aa --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1ObjectIdentifier.kt @@ -0,0 +1,62 @@ +package com.android.identity.asn1 + +import kotlinx.io.bytestring.ByteStringBuilder +import kotlin.math.max + +class ASN1ObjectIdentifier(val oid: String): ASN1PrimitiveValue(tag = TAG_NUMBER) { + + override fun encode(builder: ByteStringBuilder) { + val bsb = ByteStringBuilder() + val components = oid.split(".").map { it.toInt() } + if (components.size < 2) { + throw IllegalStateException("OID must have at least two components") + } + val firstOctet = components[0]*40 + components[1] + // Guaranteed to fit in one byte as per spec + bsb.append(firstOctet.toByte()) + for (component in components.subList(2, components.size)) { + val bitLength = Int.SIZE_BITS - component.countLeadingZeroBits() + val bytesNeeded = max((bitLength + 6) / 7, 1) + for (n in IntRange(0, bytesNeeded - 1).reversed()) { + var digit = component.shr(n * 7).and(0x7f) + if (n > 0) { + digit = digit.or(0x80) + } + bsb.append(digit.toByte()) + } + } + val componentsEncoded = bsb.toByteString().toByteArray() + ASN1.appendUniversalTagEncodingLength(builder, TAG_NUMBER, enc, componentsEncoded.size) + builder.append(componentsEncoded) + } + + override fun equals(other: Any?): Boolean = other is ASN1ObjectIdentifier && oid == other.oid + + override fun hashCode(): Int = oid.hashCode() + + override fun toString(): String { + return "ASN1ObjectIdentifier($oid)" + } + + companion object { + const val TAG_NUMBER = 0x06 + + fun parse(content: ByteArray): ASN1ObjectIdentifier { + if (content.size < 1) { + throw IllegalStateException("Content must be at least a single byte") + } + val sb = StringBuilder() + sb.append("${content[0].toInt()/40}.${content[0].toInt()%40}") + var currentComponent = 0 + for (n in IntRange(1, content.size - 1)) { + val digit = content[n].toInt() + currentComponent = currentComponent.shl(7).or(digit.and(0x7f)) + if (digit.and(0x80) == 0) { + sb.append(".$currentComponent") + currentComponent = 0 + } + } + return ASN1ObjectIdentifier(sb.toString()) + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1OctetString.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1OctetString.kt new file mode 100644 index 000000000..6ee3441a1 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1OctetString.kt @@ -0,0 +1,30 @@ +package com.android.identity.asn1 + +import com.android.identity.util.toHex +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1OctetString( + val value: ByteArray +): ASN1PrimitiveValue(tag = TAG_NUMBER) { + + override fun encode(builder: ByteStringBuilder) { + ASN1.appendUniversalTagEncodingLength(builder, tag, enc, value.size) + builder.append(value) + } + + override fun equals(other: Any?): Boolean = other is ASN1OctetString && other.value contentEquals value + + override fun hashCode(): Int = value.hashCode() + + override fun toString(): String { + return "ASN1OctetString(${value.toHex()})" + } + + companion object { + const val TAG_NUMBER = 0x04 + + fun parse(content: ByteArray): ASN1OctetString { + return ASN1OctetString(content) + } + } +} diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1PrimitiveValue.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1PrimitiveValue.kt new file mode 100644 index 000000000..87a42c52b --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1PrimitiveValue.kt @@ -0,0 +1,9 @@ +package com.android.identity.asn1 + +abstract class ASN1PrimitiveValue( + override val tag: Int, +): ASN1Object( + cls = ASN1TagClass.UNIVERSAL, + enc = ASN1Encoding.PRIMITIVE, + tag = tag) { +} diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1RawObject.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1RawObject.kt new file mode 100644 index 000000000..8de9953fa --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1RawObject.kt @@ -0,0 +1,29 @@ +package com.android.identity.asn1 + +import com.android.identity.util.toHex +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1RawObject( + cls: ASN1TagClass, + enc: ASN1Encoding, + tag: Int, + val content: ByteArray +): ASN1Object(cls, enc, tag) { + + override fun encode(builder: ByteStringBuilder) { + ASN1.appendUniversalTagEncodingLength(builder, tag, enc, content.size) + builder.append(content) + } + + override fun equals(other: Any?): Boolean = other is ASN1RawObject && + other.cls == cls && + other.enc == enc && + other.tag == tag && + other.content contentEquals content + + override fun hashCode(): Int = content.hashCode() + + override fun toString(): String { + return "ASN1RawObject($cls, $enc, $tag, ${content.toHex()})" + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Sequence.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Sequence.kt new file mode 100644 index 000000000..e3aeee2c3 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Sequence.kt @@ -0,0 +1,46 @@ +package com.android.identity.asn1 + +import kotlinx.io.bytestring.ByteStringBuilder + + +class ASN1Sequence(val elements: List): ASN1Object( + cls = ASN1TagClass.UNIVERSAL, + enc = ASN1Encoding.CONSTRUCTED, + tag = TAG_NUMBER) { + + override fun encode(builder: ByteStringBuilder) { + val bsb = ByteStringBuilder() + for (elem in elements) { + elem.encode(bsb) + } + val encodedElements = bsb.toByteString().toByteArray() + ASN1.appendUniversalTagEncodingLength(builder, TAG_NUMBER, enc, encodedElements.size) + builder.append(encodedElements) + } + + override fun equals(other: Any?): Boolean = other is ASN1Sequence && elements == other.elements + + override fun hashCode(): Int = elements.hashCode() + + override fun toString(): String { + val sb = StringBuilder("ASN1Sequence(") + var first = true + for (elem in elements) { + if (!first) { + sb.append(", ") + } + first = false + sb.append("$elem") + } + sb.append(")") + return sb.toString() + } + + companion object { + const val TAG_NUMBER = 0x10 + + fun parse(content: ByteArray): ASN1Sequence { + return ASN1Sequence(ASN1.decodeMultiple(content)) + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Set.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Set.kt new file mode 100644 index 000000000..1f494a567 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Set.kt @@ -0,0 +1,45 @@ +package com.android.identity.asn1 + +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1Set(val elements: List): ASN1Object( + cls = ASN1TagClass.UNIVERSAL, + enc = ASN1Encoding.CONSTRUCTED, + tag = TAG_NUMBER) { + + override fun encode(builder: ByteStringBuilder) { + val bsb = ByteStringBuilder() + for (elem in elements) { + elem.encode(bsb) + } + val encodedElements = bsb.toByteString().toByteArray() + ASN1.appendUniversalTagEncodingLength(builder, TAG_NUMBER, enc, encodedElements.size) + builder.append(encodedElements) + } + + override fun equals(other: Any?): Boolean = other is ASN1Set && elements == other.elements + + override fun hashCode(): Int = elements.hashCode() + + override fun toString(): String { + val sb = StringBuilder("ASN1Set(") + var first = true + for (elem in elements) { + if (!first) { + sb.append(", ") + } + first = false + sb.append("$elem") + } + sb.append(")") + return sb.toString() + } + + companion object { + const val TAG_NUMBER = 0x11 + + fun parse(content: ByteArray): ASN1Set { + return ASN1Set(ASN1.decodeMultiple(content)) + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1String.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1String.kt new file mode 100644 index 000000000..901dfff32 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1String.kt @@ -0,0 +1,30 @@ +package com.android.identity.asn1 + +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1String( + val value: String, + tag: Int = ASN1StringTag.UTF8_STRING.tag +): ASN1PrimitiveValue(tag) { + + override fun encode(builder: ByteStringBuilder) { + val encoded = value.encodeToByteArray() + ASN1.appendUniversalTagEncodingLength(builder, tag, enc, encoded.size) + builder.append(encoded) + } + + override fun equals(other: Any?): Boolean = + other is ASN1String && other.tag == tag && other.value == value + + override fun hashCode(): Int = value.hashCode() + + override fun toString(): String { + return "ASN1String(${tag}, \"$value\")" + } + + companion object { + fun parse(content: ByteArray, tag: Int): ASN1String { + return ASN1String(content.decodeToString(), tag) + } + } +} diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1StringTag.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1StringTag.kt new file mode 100644 index 000000000..b701c7ccc --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1StringTag.kt @@ -0,0 +1,16 @@ +package com.android.identity.asn1 + +enum class ASN1StringTag(val tag: Int) { + UTF8_STRING(0x0c), + NUMERIC_STRING(0x12), + PRINTABLE_STRING(0x13), + TELETEX_STRING(0x14), + VIDEOTEX_STRING(0x15), + IA5_STRING(0x16), + GRAPHIC_STRING(0x19), + VISIBLE_STRING(0x1a), + GENERAL_STRING(0x1b), + UNIVERSAL_STRING(0x1c), + CHARACTER_STRING(0x1d), + BMP_STRING(0x1e) +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TagClass.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TagClass.kt new file mode 100644 index 000000000..32cca747f --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TagClass.kt @@ -0,0 +1,17 @@ +package com.android.identity.asn1 + +enum class ASN1TagClass(val value: Int) { + UNIVERSAL(0x00), + APPLICATION(0x40), + CONTEXT_SPECIFIC(0x80), + PRIVATE(0xc0) + + ; + + companion object { + internal fun parse(idOctet0: Byte): ASN1TagClass { + val upperBits = (idOctet0.toInt().and(0xc0)) + return ASN1TagClass.entries.first() { it.value == upperBits } + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TaggedObject.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TaggedObject.kt new file mode 100644 index 000000000..ee2e0bdab --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TaggedObject.kt @@ -0,0 +1,44 @@ +package com.android.identity.asn1 + +import com.android.identity.util.toHex +import kotlinx.io.bytestring.ByteStringBuilder + +class ASN1TaggedObject( + cls: ASN1TagClass, + enc: ASN1Encoding, + tag: Int, + val content: ByteArray +): ASN1Object( + cls = cls, + enc = enc, + tag = tag) { + + override fun encode(builder: ByteStringBuilder) { + ASN1.appendIdentifierAndLength(builder, cls, enc, tag, content.size) + builder.append(content) + } + + override fun equals(other: Any?): Boolean = other is ASN1TaggedObject && + cls == other.cls && tag == other.tag && content contentEquals other.content + + override fun hashCode(): Int { + var result = cls.value + result = 31*result + enc.value + result = 31*result + tag + result = 31*result + content.contentHashCode() + return result + } + + override fun toString(): String { + val sb = StringBuilder("ASN1TaggedObject(") + sb.append("cls=${cls}, tag=${tag}, content=${content.toHex()}") + sb.append(")") + return sb.toString() + } + + companion object { + fun parse(cls: ASN1TagClass, enc: ASN1Encoding, tag: Int, content: ByteArray): ASN1TaggedObject { + return ASN1TaggedObject(cls, enc, tag, content) + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Time.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Time.kt new file mode 100644 index 000000000..c01d34931 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1Time.kt @@ -0,0 +1,133 @@ +package com.android.identity.asn1 + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atTime +import kotlinx.datetime.format +import kotlinx.datetime.format.byUnicodePattern +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import kotlinx.io.bytestring.ByteStringBuilder +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds + +class ASN1Time( + val value: Instant, + tag: Int = ASN1TimeTag.GENERALIZED_TIME.tag +): ASN1PrimitiveValue(tag) { + + override fun encode(builder: ByteStringBuilder) { + val ldt = value.toLocalDateTime(TimeZone.UTC) + val str = when (tag) { + ASN1TimeTag.UTC_TIME.tag -> { + // Looks like 'yy' makes LocalDateTime.format() yield '+1950' for years before 2000 + val yearTwoDigits = ldt.year % 100 + val yearTwoDigitsWithPadding = if (yearTwoDigits < 10) { + "0" + yearTwoDigits.toString() + } else { + yearTwoDigits.toString() + } + val dateTimeFormat = LocalDateTime.Format { + byUnicodePattern("MMddHHmmss'Z'") + } + yearTwoDigitsWithPadding + ldt.format(dateTimeFormat) + } + + ASN1TimeTag.GENERALIZED_TIME.tag -> { + if (ldt.nanosecond > 0) { + // From X.690 clause 11.7.3: The fractional-seconds elements, if present, + // shall omit all trailing zeros; if the elements correspond to 0, + // they shall be wholly omitted, and the decimal point element also shall + // be omitted. + // + val nanoAsStr = + (ldt.nanosecond.nanoseconds + 1.seconds).inWholeNanoseconds.toString() + .trim('0').substring(1) + + ldt.format( + LocalDateTime.Format { + byUnicodePattern("yyyyMMddHHmmss") + } + ) + "." + nanoAsStr + "Z" + } else { + ldt.format( + LocalDateTime.Format { + byUnicodePattern("yyyyMMddHHmmss'Z'") + } + ) + } + } + + else -> throw IllegalStateException() + } + val encoded = str.encodeToByteArray() + ASN1.appendUniversalTagEncodingLength(builder, tag, enc, encoded.size) + builder.append(encoded) + } + + override fun equals(other: Any?): Boolean = + other is ASN1Time && other.tag == tag && other.value == value + + override fun hashCode(): Int = value.hashCode() + + override fun toString(): String { + return "ASN1Time(${tag}, $value)" + } + + companion object { + fun parse(content: ByteArray, tag: Int): ASN1Time { + val str = content.decodeToString() + val parsedTime = when (tag) { + ASN1TimeTag.UTC_TIME.tag -> { + if (str.length != 13 || str[12] != 'Z') { + throw IllegalArgumentException("UTCTime string is malformed") + } + val yearTwoDigits = str.slice(IntRange(0, 1)).toInt() + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.1 + val century = if (yearTwoDigits < 50) 2000 else 1900 + val year = yearTwoDigits + century + val month = str.slice(IntRange(2, 3)).toInt() + val day = str.slice(IntRange(4, 5)).toInt() + val hour = str.slice(IntRange(6, 7)).toInt() + val minute = str.slice(IntRange(8, 9)).toInt() + val second = str.slice(IntRange(10, 11)).toInt() + val ld = LocalDate(year, month, day) + val ut = LocalTime(hour, minute, second) + ld.atTime(ut).toInstant(TimeZone.UTC) + } + + ASN1TimeTag.GENERALIZED_TIME.tag -> { + val year = str.slice(IntRange(0, 3)).toInt() + val month = str.slice(IntRange(4, 5)).toInt() + val day = str.slice(IntRange(6, 7)).toInt() + val hour = str.slice(IntRange(8, 9)).toInt() + val minute = str.slice(IntRange(10, 11)).toInt() + val second = str.slice(IntRange(12, 13)).toInt() + val nanoSeconds = if (str[14] == 'Z') { + if (str.length != 15) { + throw IllegalArgumentException("GeneralizedTime string is malformed") + } + 0 + } else if (str[14] == '.'){ + if (str[str.length - 1] != 'Z') { + throw IllegalArgumentException("GeneralizedTime string is malformed") + } + val fractionalStr = str.slice(IntRange(15, str.length - 2)) + LocalTime.parse("00:00:00.$fractionalStr").nanosecond + } else { + throw IllegalArgumentException("GeneralizedTime string is malformed") + } + val ld = LocalDate(year, month, day) + val ut = LocalTime(hour, minute, second, nanoSeconds) + ld.atTime(ut).toInstant(TimeZone.UTC) + } + + else -> throw IllegalArgumentException("Unsupported tag number") + } + return ASN1Time(parsedTime, tag) + } + } +} diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TimeTag.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TimeTag.kt new file mode 100644 index 000000000..cc2b112bb --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/ASN1TimeTag.kt @@ -0,0 +1,6 @@ +package com.android.identity.asn1 + +enum class ASN1TimeTag(val tag: Int) { + UTC_TIME(0x17), + GENERALIZED_TIME(0x18) +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/asn1/OID.kt b/identity/src/commonMain/kotlin/com/android/identity/asn1/OID.kt new file mode 100644 index 000000000..1890339af --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/asn1/OID.kt @@ -0,0 +1,57 @@ +package com.android.identity.asn1 + +/** + * Registry of known OIDs. + * + * @property oid the OID. + * @property description a textual description of the OID. + */ +enum class OID( + val oid: String, + val description: String +) { + EC_PUBLIC_KEY("1.2.840.10045.2.1", "Elliptic curve public key cryptography"), + EC_CURVE_P256("1.2.840.10045.3.1.7", "NIST Curve P-256"), + EC_CURVE_P384("1.3.132.0.34", "EC Curve P-384"), + EC_CURVE_P521("1.3.132.0.35", "EC Curve P-521"), + EC_CURVE_BRAINPOOLP256R1("1.3.36.3.3.2.8.1.1.7", "EC Curve Brainpool P256r1"), + EC_CURVE_BRAINPOOLP320R1("1.3.36.3.3.2.8.1.1.9", "EC Curve Brainpool P320r1"), + EC_CURVE_BRAINPOOLP384R1("1.3.36.3.3.2.8.1.1.11", "EC Curve Brainpool P384r1"), + EC_CURVE_BRAINPOOLP512R1("1.3.36.3.3.2.8.1.1.13", "EC Curve Brainpool P512r1"), + + SIGNATURE_ECDSA_SHA256("1.2.840.10045.4.3.2", "ECDSA coupled with SHA-256"), + SIGNATURE_ECDSA_SHA384("1.2.840.10045.4.3.3", "ECDSA coupled with SHA-384"), + SIGNATURE_ECDSA_SHA512("1.2.840.10045.4.3.4", "ECDSA coupled with SHA-512"), + + X25519("1.3.101.110", "X25519 algorithm used with the Diffie-Hellman operation"), + X448("1.3.101.111", "X448 algorithm used with the Diffie-Hellman operation"), + ED25519("1.3.101.112", "Edwards-curve Digital Signature Algorithm (EdDSA) Ed25519"), + ED448("1.3.101.113", "Edwards-curve Digital Signature Algorithm (EdDSA) Ed448"), + + COMMON_NAME("2.5.4.3", "commonName (X.520 DN component)"), + COUNTRY_NAME("2.5.4.6", "countryName (X.520 DN component)"), + LOCALITY_NAME("2.5.4.7", "localityName (X.520 DN component)"), + STATE_OR_PROVINCE_NAME("2.5.4.8", "stateOrProvinceName (X.520 DN component)"), + ORGANIZATION_NAME("2.5.4.10", "organizationName (X.520 DN component)"), + ORGANIZATIONAL_UNIT_NAME("organizationalUnitName (X.520 DN component)", ""), + + X509_EXTENSION_KEY_USAGE("2.5.29.15", "keyUsage (X.509 extension)"), + X509_EXTENSION_EXTENDED_KEY_USAGE("2.5.29.37", "extKeyUsage (X.509 extension)"), + X509_EXTENSION_BASIC_CONSTRAINTS("2.5.29.19", "basicConstraints (X.509 extension)"), + X509_EXTENSION_SUBJECT_KEY_IDENTIFIER("2.5.29.14", "subjectKeyIdentifier (X.509 extension)"), + X509_EXTENSION_AUTHORITY_KEY_IDENTIFIER("2.5.29.35", "authorityKeyIdentifier (X.509 extension)"), + X509_EXTENSION_ISSUER_ALT_NAME("2.5.29.18", "issuerAltName (X.509 extension)"), + X509_EXTENSION_CRL_DISTRIBUTION_POINTS("2.5.29.31", "cRLDistributionPoints (X.509 extension)"), + + ISO_18013_5_MDL_DS("1.0.18013.5.1.2", "Mobile Driving Licence (mDL) Document Signer (DS)") + + ; + + companion object { + private val stringToOid: Map by lazy { + OID.entries.associateBy({it.oid}, {it}) + } + + fun lookupByOid(oid: String): OID? = stringToOid[oid] + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/crypto/Algorithm.kt b/identity/src/commonMain/kotlin/com/android/identity/crypto/Algorithm.kt index 491d3666b..30b0bb8b0 100644 --- a/identity/src/commonMain/kotlin/com/android/identity/crypto/Algorithm.kt +++ b/identity/src/commonMain/kotlin/com/android/identity/crypto/Algorithm.kt @@ -22,6 +22,9 @@ enum class Algorithm(val coseAlgorithmIdentifier: Int) { /** The algorithm identifier for signatures using EdDSA */ EDDSA(-8), + /** SHA-1 Hash (insecure, shouldn't be used) */ + INSECURE_SHA1(-14), + /** SHA-2 256-bit Hash */ SHA256(-16), diff --git a/identity/src/commonMain/kotlin/com/android/identity/crypto/Crypto.kt b/identity/src/commonMain/kotlin/com/android/identity/crypto/Crypto.kt index 106bd12a3..930748e17 100644 --- a/identity/src/commonMain/kotlin/com/android/identity/crypto/Crypto.kt +++ b/identity/src/commonMain/kotlin/com/android/identity/crypto/Crypto.kt @@ -202,4 +202,7 @@ expect object Crypto { internal fun ecPrivateKeyFromPem(pemEncoding: String, publicKey: EcPublicKey): EcPrivateKey internal fun uuidGetRandom(): UUID + + // TODO: replace with non-platform specific code + internal fun validateCertChain(certChain: X509CertChain): Boolean } \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/crypto/EcSignature.kt b/identity/src/commonMain/kotlin/com/android/identity/crypto/EcSignature.kt index edad8a695..a92219c43 100644 --- a/identity/src/commonMain/kotlin/com/android/identity/crypto/EcSignature.kt +++ b/identity/src/commonMain/kotlin/com/android/identity/crypto/EcSignature.kt @@ -1,7 +1,11 @@ package com.android.identity.crypto +import com.android.identity.asn1.ASN1 +import com.android.identity.asn1.ASN1Integer +import com.android.identity.asn1.ASN1Sequence import com.android.identity.cbor.CborMap import com.android.identity.cbor.DataItem +import com.android.identity.util.toHex /** * An Elliptic Curve Cryptography signature. @@ -40,6 +44,20 @@ data class EcSignature( return result } + fun toDerEncoded(): ByteArray { + // r and s are both encoded without a sign but ASN1Integer uses a sign. So we need + // to insert zeroes as needed... + val rS = stripLeadingZeroes(r) + val sS = stripLeadingZeroes(s) + val rP = if (rS[0].toInt().and(0x80) != 0) { byteArrayOf(0x00) + rS } else { rS } + val sP = if (sS[0].toInt().and(0x80) != 0) { byteArrayOf(0x00) + sS } else { sS } + val derEncoded = ASN1.encode(ASN1Sequence(listOf( + ASN1Integer(rP), + ASN1Integer(sP) + ))) + return derEncoded + } + companion object { fun fromCoseEncoded(coseSignature: ByteArray): EcSignature { val len = coseSignature.size @@ -52,5 +70,33 @@ data class EcSignature( require(dataItem is CborMap) return EcSignature(dataItem["r"].asBstr, dataItem["s"].asBstr) } + + fun fromDerEncoded( + keySizeBits: Int, + derEncodedSignature: ByteArray + ): EcSignature { + val derSignature = ASN1.decode(derEncodedSignature) as ASN1Sequence + val r = (derSignature.elements[0] as ASN1Integer).value + val s = (derSignature.elements[1] as ASN1Integer).value + // Need to make sure that each component is exactly as big as the key size. + val rS = stripLeadingZeroes(r) + val sS = stripLeadingZeroes(s) + val keySize = (keySizeBits + 7)/8 + val rPadded = ByteArray(keySize) + val sPadded = ByteArray(keySize) + rS.copyInto(rPadded, keySize - rS.size) + sS.copyInto(sPadded, keySize - sS.size) + check(rPadded.size == keySize) + check(sPadded.size == keySize) + val sig = EcSignature(rPadded, sPadded) + return sig + } } } + +private fun stripLeadingZeroes(array: ByteArray): ByteArray { + val idx = array.indexOfFirst { it != 0.toByte() } + if (idx == -1) + return array + return array.copyOfRange(idx, array.size) +} diff --git a/identity/src/commonMain/kotlin/com/android/identity/crypto/X500Name.kt b/identity/src/commonMain/kotlin/com/android/identity/crypto/X500Name.kt new file mode 100644 index 000000000..7151b42c5 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/crypto/X500Name.kt @@ -0,0 +1,106 @@ +package com.android.identity.crypto + +import com.android.identity.asn1.ASN1 +import com.android.identity.asn1.ASN1String +import com.android.identity.asn1.ASN1StringTag +import com.android.identity.asn1.OID +import com.android.identity.util.toHex + +/** + * This represents a X.501 Name as used for X.509 certificates. + * + * @property components a map from OID to the value. + */ +class X500Name(val components: Map) { + + override fun equals(other: Any?): Boolean = other is X500Name && name == other.name + + override fun hashCode(): Int = name.hashCode() + + /** + * The X.501 Name encoded according to + * [RFC 2253](https://datatracker.ietf.org/doc/html/rfc2253). + */ + val name: String + get() { + val sb = StringBuilder() + for (oid in components.keys.reversed()) { + if (!sb.isEmpty()) { + sb.append(',') + } + val value = components[oid]!! + val oidName = knownOids.get(oid) + if (oidName != null) { + sb.append("$oidName=${value.value}") + } else { + // From https://datatracker.ietf.org/doc/html/rfc2253#section-2.4 + // + // If the AttributeValue is of a type which does not have a string + // representation defined for it, then it is simply encoded as an + // octothorpe character ('#' ASCII 35) followed by the hexadecimal + // representation of each of the bytes of the BER encoding of the X.500 + // AttributeValue. This form SHOULD be used if the AttributeType is of + // the dotted-decimal form. + // + sb.append("$oid=#${ASN1.encode(value).toHex()}") + } + } + return sb.toString() + } + + companion object { + // From RFC 5280 Annex A, could add more as needed. + // + private val knownOids = mapOf( + OID.COMMON_NAME.oid to "CN", + OID.COUNTRY_NAME.oid to "C", + OID.LOCALITY_NAME.oid to "L", + OID.STATE_OR_PROVINCE_NAME.oid to "ST", + OID.ORGANIZATION_NAME.oid to "O", + OID.ORGANIZATIONAL_UNIT_NAME.oid to "OU", + ) + + private val knownNames: Map by lazy { + knownOids.entries.associateBy({it.value}, {it.key}) + } + + /** + * Builds a [X500Name] from the encoded form. + * + * For example, if passing the string `CN=David,ST=US-MA,O=Google,OU=Android,C=US` a + * [X500Name] instance with the the [X500Name.components] property containing the + * following entries + * + * ``` + * val components = mapOf( + * "2.5.4.6" to ASN1String("US"), + * "2.5.4.11" to ASN1String("Android"), + * "2.5.4.10" to ASN1String("Google"), + * "2.5.4.8" to ASN1String("US-MA"), + * "2.5.4.3" to ASN1String("David"), + * ) + * ``` + * + * @param name an encoded form of a X.501 Name according to + * [RFC 2253](https://datatracker.ietf.org/doc/html/rfc2253). + * @throws IllegalArgumentException if one of the keys isn't known. + */ + fun fromName(name: String): X500Name { + val components = mutableMapOf() + for (part in name.split(",").reversed()) { + val pos = part.indexOf('=') + if (pos < 0) { + throw IllegalArgumentException("No equal sign found in component") + } + val key = part.substring(0, pos) + val value = part.substring(pos + 1) + val oidForKey = knownNames[key] + if (oidForKey == null) { + throw IllegalArgumentException("Unknown OID for $key") + } + components.put(oidForKey, ASN1String(value)) + } + return X500Name(components) + } + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/crypto/X509Cert.kt b/identity/src/commonMain/kotlin/com/android/identity/crypto/X509Cert.kt index 5053be792..5ef39d1ec 100644 --- a/identity/src/commonMain/kotlin/com/android/identity/crypto/X509Cert.kt +++ b/identity/src/commonMain/kotlin/com/android/identity/crypto/X509Cert.kt @@ -1,31 +1,168 @@ package com.android.identity.crypto +import com.android.identity.asn1.ASN1 +import com.android.identity.asn1.ASN1BitString +import com.android.identity.asn1.ASN1Boolean +import com.android.identity.asn1.ASN1Encoding +import com.android.identity.asn1.ASN1Integer +import com.android.identity.asn1.ASN1Object +import com.android.identity.asn1.ASN1ObjectIdentifier +import com.android.identity.asn1.ASN1OctetString +import com.android.identity.asn1.ASN1Sequence +import com.android.identity.asn1.ASN1Set +import com.android.identity.asn1.ASN1String +import com.android.identity.asn1.ASN1TagClass +import com.android.identity.asn1.ASN1TaggedObject +import com.android.identity.asn1.ASN1Time +import com.android.identity.asn1.OID +import com.android.identity.cbor.Bstr import com.android.identity.cbor.DataItem +import com.android.identity.util.Logger +import kotlinx.datetime.Instant +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi /** * A data type for a X509 certificate. * * @param encodedCertificate the bytes of the X.509 certificate. */ -expect class X509Cert( - encodedCertificate: ByteArray -) { - /** - * The encoded certificate. - */ +class X509Cert( val encodedCertificate: ByteArray +) { + override fun equals(other: Any?): Boolean = other is X509Cert && + encodedCertificate contentEquals other.encodedCertificate + + override fun hashCode(): Int = encodedCertificate.contentHashCode() /** * Gets an [DataItem] with the encoded X.509 certificate. */ - fun toDataItem(): DataItem + fun toDataItem(): DataItem = Bstr(encodedCertificate) /** * Encode this certificate in PEM format * * @return a PEM encoded string. */ - fun toPem(): String + @OptIn(ExperimentalEncodingApi::class) + fun toPem(): String { + val sb = StringBuilder() + sb.append("-----BEGIN CERTIFICATE-----\n") + sb.append(Base64.Mime.encode(encodedCertificate)) + sb.append("\n-----END CERTIFICATE-----\n") + return sb.toString() + } + + /** + * Checks if the certificate was signed with a given key. + * + * @param publicKey the key to check the signature with. + * @return `true` if the certificate was signed with the given key, `false` otherwise. + */ + fun verify(publicKey: EcPublicKey): Boolean { + val ecSignature = when (signatureAlgorithm) { + Algorithm.ES256, + Algorithm.ES384, + Algorithm.ES512 -> { + EcSignature.fromDerEncoded(publicKey.curve.bitSize, signature) + } + Algorithm.EDDSA -> { + val len = signature.size + val r = signature.sliceArray(IntRange(0, len/2 - 1)) + val s = signature.sliceArray(IntRange(len/2, len - 1)) + EcSignature(r, s) + } + else -> throw IllegalArgumentException("Unsupported algorithm $signatureAlgorithm") + } + return Crypto.checkSignature( + publicKey, + tbsCertificate, + signatureAlgorithm, + ecSignature + ) + } + + private val parsedCert: ASN1Sequence by lazy { + ASN1.decode(encodedCertificate)!! as ASN1Sequence + } + + private val tbsCert: ASN1Sequence by lazy { + parsedCert.elements[0] as ASN1Sequence + } + + /** + * The certificate version. + * + * This returns the encoded value and for X.509 Version 3 Certificate this value is 2. + */ + val version: Int + get() { + val child = ASN1.decode((tbsCert.elements[0] as ASN1TaggedObject).content) + val versionCode = (child as ASN1Integer).toLong().toInt() + return versionCode + } + + /** + * The certificate serial number. + */ + val serialNumber: ASN1Integer + get() = (tbsCert.elements[1] as ASN1Integer) + + /** + * The subject of the certificate. + */ + val subject: X500Name + get() = parseName(tbsCert.elements[5] as ASN1Sequence) + + /** + * The issuer of the certificate. + */ + val issuer: X500Name + get() = parseName(tbsCert.elements[3] as ASN1Sequence) + + /** + * The point in time where the certificate is valid from. + */ + val validityNotBefore: Instant + get() = ((tbsCert.elements[4] as ASN1Sequence).elements[0] as ASN1Time).value + + /** + * The point in time where the certificate is valid until. + */ + val validityNotAfter: Instant + get() = ((tbsCert.elements[4] as ASN1Sequence).elements[1] as ASN1Time).value + + /** + * The bytes of TBSCertificate. + */ + val tbsCertificate: ByteArray + get() = ASN1.encode(tbsCert) + + /** + * The certificate signature. + */ + val signature: ByteArray + get() = (parsedCert.elements[2] as ASN1BitString).value + + /** + * The signature algorithm for the certificate. + * + * @throws IllegalArgumentException if the OID for the algorithm doesn't correspond with a signature algorithm + * value in the [Algorithm] enumeration. + */ + val signatureAlgorithm: Algorithm + get() { + val algorithmIdentifier = parsedCert.elements[1] as ASN1Sequence + val algorithmOid = (algorithmIdentifier.elements[0] as ASN1ObjectIdentifier).oid + return when (algorithmOid) { + "1.2.840.10045.4.3.2" -> Algorithm.ES256 + "1.2.840.10045.4.3.3" -> Algorithm.ES384 + "1.2.840.10045.4.3.4" -> Algorithm.ES512 + "1.3.101.112", "1.3.101.113" -> Algorithm.EDDSA // ED25519, ED448 + else -> throw IllegalArgumentException("Unexpected algorithm OID $algorithmOid") + } + } /** * The public key in the certificate, as an Elliptic Curve key. @@ -33,18 +170,169 @@ expect class X509Cert( * Note that this is only supported for curves in [Crypto.supportedCurves]. * * @throws IllegalStateException if the public key for the certificate isn't an EC key or - * supported by the platform. + * its EC curve isn't supported by the platform. */ val ecPublicKey: EcPublicKey + get() { + val subjectPublicKeyInfo = tbsCert.elements[6] as ASN1Sequence + val algorithmIdentifier = subjectPublicKeyInfo.elements[0] as ASN1Sequence + val algorithmOid = (algorithmIdentifier.elements[0] as ASN1ObjectIdentifier).oid + val curve = when (algorithmOid) { + // https://datatracker.ietf.org/doc/html/rfc5480#section-2.1.1 + OID.EC_PUBLIC_KEY.oid -> { + val ecCurveString = (algorithmIdentifier.elements[1] as ASN1ObjectIdentifier).oid + when (ecCurveString) { + "1.2.840.10045.3.1.7" -> EcCurve.P256 + "1.3.132.0.34" -> EcCurve.P384 + "1.3.132.0.35" -> EcCurve.P521 + "1.3.36.3.3.2.8.1.1.7" -> EcCurve.BRAINPOOLP256R1 + "1.3.36.3.3.2.8.1.1.9" -> EcCurve.BRAINPOOLP320R1 + "1.3.36.3.3.2.8.1.1.11" -> EcCurve.BRAINPOOLP384R1 + "1.3.36.3.3.2.8.1.1.13" -> EcCurve.BRAINPOOLP512R1 + else -> throw IllegalStateException("Unexpected curve OID $ecCurveString") + } + } + "1.3.101.110" -> EcCurve.X25519 + "1.3.101.111" -> EcCurve.X448 + "1.3.101.112" -> EcCurve.ED25519 + "1.3.101.113" -> EcCurve.ED448 + else -> throw IllegalStateException("Unexpected OID $algorithmOid") + } + val keyMaterial = (subjectPublicKeyInfo.elements[1] as ASN1BitString).value + return when (curve) { + EcCurve.P256, + EcCurve.P384, + EcCurve.P521, + EcCurve.BRAINPOOLP256R1, + EcCurve.BRAINPOOLP320R1, + EcCurve.BRAINPOOLP384R1, + EcCurve.BRAINPOOLP512R1 -> { + EcPublicKeyDoubleCoordinate.fromUncompressedPointEncoding(curve, keyMaterial) + } + EcCurve.ED25519, + EcCurve.X25519, + EcCurve.ED448, + EcCurve.X448 -> { + EcPublicKeyOkp(curve, keyMaterial) + } + } + } + + /** + * The OIDs for X.509 extensions which are marked as critical. + */ + val criticalExtensionOIDs: Set + get() = getExtensionOIDs(true) + + /** + * The OIDs for X.509 extensions which are not marked as critical. + */ + val nonCriticalExtensionOIDs: Set + get() = getExtensionOIDs(false) + + private fun getExtensionsSeq(): ASN1Sequence? { + for (elem in tbsCert.elements) { + if (elem is ASN1TaggedObject && + elem.cls == ASN1TagClass.CONTEXT_SPECIFIC && + elem.enc == ASN1Encoding.CONSTRUCTED && + elem.tag == 0x03) { + return ASN1.decode(elem.content) as ASN1Sequence + } + } + return null + } + + private fun getExtensionOIDs(getCritical: Boolean): Set { + val extSeq = getExtensionsSeq() ?: return emptySet() + val ret = mutableSetOf() + for (ext in extSeq.elements) { + ext as ASN1Sequence + val isCritical = if (ext.elements.size == 3) { + (ext.elements[1] as ASN1Boolean).value + } else { + false + } + if ((isCritical && getCritical) || (!isCritical && !getCritical)) { + ret.add((ext.elements[0] as ASN1ObjectIdentifier).oid) + } + } + return ret + } + + /** + * Gets the bytes of a X.509 extension. + * + * @param oid the OID to get the extension from + * @return the bytes of the extension or `null` if no such extension exist. + */ + fun getExtensionValue(oid: String): ByteArray? { + val extSeq = getExtensionsSeq() ?: return null + for (ext in extSeq.elements) { + ext as ASN1Sequence + if ((ext.elements[0] as ASN1ObjectIdentifier).oid == oid) { + if (ext.elements.size == 3) { + return (ext.elements[2] as ASN1OctetString).value + } else { + return (ext.elements[1] as ASN1OctetString).value + } + } + } + return null + } + + /** + * The subject key identifier (OID 2.5.29.14), or `null` if not present in the certificate. + */ + val subjectKeyIdentifier: ByteArray? + get() { + val extVal = getExtensionValue(OID.X509_EXTENSION_SUBJECT_KEY_IDENTIFIER.oid) ?: return null + return (ASN1.decode(extVal) as ASN1OctetString).value + } + + /** + * The authority key identifier (OID 2.5.29.35), or `null` if not present in the certificate. + */ + val authorityKeyIdentifier: ByteArray? + get() { + val extVal = getExtensionValue(OID.X509_EXTENSION_AUTHORITY_KEY_IDENTIFIER.oid) ?: return null + val seq = ASN1.decode(extVal) as ASN1Sequence + val taggedObject = seq.elements[0] as ASN1TaggedObject + check(taggedObject.cls == ASN1TagClass.CONTEXT_SPECIFIC) { "Expected context-specific tag" } + check(taggedObject.enc == ASN1Encoding.PRIMITIVE) + check(taggedObject.tag == 0) { "Expected tag 0" } + // Note: tags in AuthorityKeyIdentifier are IMPLICIT b/c its definition appear in + // the implicitly tagged ASN.1 module, see RFC 5280 Appendix A.2. + // + return taggedObject.content + } + + /** + * The key usage (OID 2.5.29.15) or the empty set if not present. + */ + val keyUsage: Set + get() { + val extVal = getExtensionValue(OID.X509_EXTENSION_KEY_USAGE.oid) ?: return emptySet() + check(criticalExtensionOIDs.contains(OID.X509_EXTENSION_KEY_USAGE.oid)) + return X509KeyUsage.decodeSet(ASN1.decode(extVal) as ASN1BitString) + } companion object { + private const val TAG = "X509Cert" + /** * Creates a [X509Cert] from a PEM encoded string. * * @param pemEncoding the PEM encoded string. * @return a new [X509Cert]. */ - fun fromPem(pemEncoding: String): X509Cert + @OptIn(ExperimentalEncodingApi::class) + fun fromPem(pemEncoding: String): X509Cert { + val encoded = Base64.Mime.decode(pemEncoding + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .trim()) + return X509Cert(encoded) + } /** * Gets a [X509Cert] from a [DataItem]. @@ -52,6 +340,308 @@ expect class X509Cert( * @param dataItem the data item, must have been encoded with [toDataItem]. * @return the certificate. */ - fun fromDataItem(dataItem: DataItem): X509Cert + fun fromDataItem(dataItem: DataItem): X509Cert { + return X509Cert(dataItem.asBstr) + } + } + + /** + * Builder for X.509 certificate. + */ + class Builder( + private val publicKey: EcPublicKey, + private val signingKey: EcPrivateKey, + private val signatureAlgorithm: Algorithm, + private val serialNumber: ASN1Integer, + private val subject: X500Name, + private val issuer: X500Name, + private val validFrom: Instant, + private val validUntil: Instant, + ) { + private data class Extension( + val critical: Boolean, + val value: ByteArray + ) + private val extensions = mutableMapOf() + + private var includeSubjectKeyIdentifierFlag: Boolean = false + private var includeAuthorityKeyIdentifierAsSubjectKeyIdentifierFlag: Boolean = false + + fun addExtension(oid: String, critical: Boolean, value: ByteArray): Builder { + extensions.put(oid, Extension(critical, value)) + return this + } + + /** + * Generate and include the Subject Key Identifier extension . + * + * The extension will be marked as non-critical. + * + * @param `true` to include the Subject Key Identifier, `false to not. + */ + fun includeSubjectKeyIdentifier(value: Boolean = true): Builder { + includeSubjectKeyIdentifierFlag = value + return this + } + + /** + * Set the Authority Key Identifier with keyIdentifier set to the same value as the + * Subject Key Identifier. + * + * This is only meaningful when creating a self-signed certificate. + * + * The extension will be marked as non-critical. + * + * @param `true` to include the Authority Key Identifier, `false to not. + */ + fun includeAuthorityKeyIdentifierAsSubjectKeyIdentifier(value: Boolean = true): Builder { + includeAuthorityKeyIdentifierAsSubjectKeyIdentifierFlag = value + return this + } + + /** + * Sets Authority Key Identifier extension to the given value. + * + * The extension will be marked as non-critical. + */ + fun setAuthorityKeyIdentifierToCertificate(certificate: X509Cert): Builder { + addExtension( + OID.X509_EXTENSION_AUTHORITY_KEY_IDENTIFIER.oid, + false, + // Note: AuthorityKeyIdentifier uses IMPLICIT tags + ASN1.encode( + ASN1Sequence(listOf( + ASN1TaggedObject( + ASN1TagClass.CONTEXT_SPECIFIC, + ASN1Encoding.PRIMITIVE, + 0, + certificate.subjectKeyIdentifier!! + ) + )) + ) + ) + return this + } + + fun setKeyUsage(keyUsage: Set): Builder { + addExtension( + OID.X509_EXTENSION_KEY_USAGE.oid, + true, + ASN1.encode(X509KeyUsage.encodeSet(keyUsage)) + ) + return this + } + + fun setBasicConstraints( + ca: Boolean, + pathLenConstraint: Int?, + ): Builder { + val seq = mutableListOf( + ASN1Boolean(ca) + ) + if (pathLenConstraint != null) { + seq.add(ASN1Integer(pathLenConstraint.toLong())) + } + addExtension( + OID.X509_EXTENSION_BASIC_CONSTRAINTS.oid, + true, + ASN1.encode(ASN1Sequence(seq)) + ) + return this + } + + fun build(): X509Cert { + val signatureAlgorithmSeq = signatureAlgorithm.getSignatureAlgorithmSeq(signingKey.curve) + + val subjectPublicKey = when (publicKey) { + is EcPublicKeyDoubleCoordinate -> { + publicKey.asUncompressedPointEncoding + } + is EcPublicKeyOkp -> { + publicKey.x + } + } + val subjectPublicKeyInfoSeq = ASN1Sequence(listOf( + publicKey.curve.getCurveAlgorithmSeq(), + ASN1BitString(0, subjectPublicKey) + )) + + if (validFrom.nanosecondsOfSecond != 0) { + Logger.w(TAG, "Truncating fractional seconds of validFrom") + } + if (validUntil.nanosecondsOfSecond != 0) { + Logger.w(TAG, "Truncating fractional seconds of validUntil") + } + val validFromTruncated = Instant.fromEpochSeconds(validFrom.epochSeconds) + val validUntilTruncated = Instant.fromEpochSeconds(validUntil.epochSeconds) + val tbsCertObjs = mutableListOf( + ASN1TaggedObject( + ASN1TagClass.CONTEXT_SPECIFIC, + ASN1Encoding.CONSTRUCTED, + 0, + ASN1.encode(ASN1Integer(2L)) + ), + serialNumber, + signatureAlgorithmSeq, + generateName(issuer), + ASN1Sequence(listOf( + ASN1Time(validFromTruncated), + ASN1Time(validUntilTruncated) + )), + generateName(subject), + subjectPublicKeyInfoSeq, + ) + + if (includeSubjectKeyIdentifierFlag) { + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 + addExtension( + OID.X509_EXTENSION_SUBJECT_KEY_IDENTIFIER.oid, + false, + ASN1.encode(ASN1OctetString(Crypto.digest(Algorithm.INSECURE_SHA1, subjectPublicKey))) + ) + } + + if (includeAuthorityKeyIdentifierAsSubjectKeyIdentifierFlag) { + addExtension( + OID.X509_EXTENSION_AUTHORITY_KEY_IDENTIFIER.oid, + false, + // Note: AuthorityKeyIdentifier uses IMPLICIT tags + ASN1.encode( + ASN1Sequence(listOf( + ASN1TaggedObject( + ASN1TagClass.CONTEXT_SPECIFIC, + ASN1Encoding.PRIMITIVE, + 0, + Crypto.digest(Algorithm.INSECURE_SHA1, subjectPublicKey) + ) + )) + ) + ) + } + + if (extensions.size > 0) { + val extensionObjs = mutableListOf() + for ((oid, ext) in extensions) { + extensionObjs.add( + if (ext.critical) { + ASN1Sequence( + listOf( + ASN1ObjectIdentifier(oid), + ASN1Boolean(true), + ASN1OctetString(ext.value) + ) + ) + } else { + ASN1Sequence( + listOf( + ASN1ObjectIdentifier(oid), + ASN1OctetString(ext.value) + ) + ) + } + ) + } + tbsCertObjs.add(ASN1TaggedObject( + ASN1TagClass.CONTEXT_SPECIFIC, + ASN1Encoding.CONSTRUCTED, + 3, + ASN1.encode(ASN1Sequence(extensionObjs)) + )) + } + + val tbsCert = ASN1Sequence(tbsCertObjs) + + val encodedTbsCert = ASN1.encode(tbsCert) + val signature = Crypto.sign( + signingKey, + signatureAlgorithm, + encodedTbsCert + ) + val encodedSignature = when (signatureAlgorithm) { + Algorithm.ES256, + Algorithm.ES384, + Algorithm.ES512 -> signature.toDerEncoded() + Algorithm.EDDSA -> signature.r + signature.s + else -> throw IllegalArgumentException("Unsupported signature algorithm $signatureAlgorithm") + } + val cert = ASN1Sequence(listOf( + tbsCert, + signatureAlgorithmSeq, + ASN1BitString(0, encodedSignature), + )) + return X509Cert(ASN1.encode(cert)) + } + } +} + +private fun Algorithm.getSignatureAlgorithmSeq(signingKeyCurve: EcCurve): ASN1Sequence { + val signatureAlgorithmOid = when (this) { + Algorithm.ES256 -> "1.2.840.10045.4.3.2" + Algorithm.ES384 -> "1.2.840.10045.4.3.3" + Algorithm.ES512 -> "1.2.840.10045.4.3.4" + Algorithm.EDDSA -> { + when (signingKeyCurve) { + EcCurve.ED25519 -> "1.3.101.112" + EcCurve.ED448 -> "1.3.101.113" + else -> throw IllegalArgumentException( + "Unsupported curve ${signingKeyCurve} for $this") + } + } + else -> { + throw IllegalArgumentException("Unsupported signature algorithm $this") + } + } + return ASN1Sequence(listOf(ASN1ObjectIdentifier(signatureAlgorithmOid))) +} + +private fun EcCurve.getCurveAlgorithmSeq(): ASN1Sequence { + val (algOid, paramOid) = when (this) { + EcCurve.P256 -> Pair(OID.EC_PUBLIC_KEY.oid, OID.EC_CURVE_P256.oid) + EcCurve.P384 -> Pair(OID.EC_PUBLIC_KEY.oid, OID.EC_CURVE_P384.oid) + EcCurve.P521 -> Pair(OID.EC_PUBLIC_KEY.oid, OID.EC_CURVE_P521.oid) + EcCurve.BRAINPOOLP256R1 -> Pair(OID.EC_PUBLIC_KEY.oid, OID.EC_CURVE_BRAINPOOLP256R1.oid) + EcCurve.BRAINPOOLP320R1 -> Pair(OID.EC_PUBLIC_KEY.oid, OID.EC_CURVE_BRAINPOOLP320R1.oid) + EcCurve.BRAINPOOLP384R1 -> Pair(OID.EC_PUBLIC_KEY.oid, OID.EC_CURVE_BRAINPOOLP384R1.oid) + EcCurve.BRAINPOOLP512R1 -> Pair(OID.EC_PUBLIC_KEY.oid, OID.EC_CURVE_BRAINPOOLP512R1.oid) + EcCurve.X25519 -> Pair(OID.X25519.oid, null) + EcCurve.X448 -> Pair(OID.X448.oid, null) + EcCurve.ED25519 -> Pair(OID.ED25519.oid, null) + EcCurve.ED448 -> Pair(OID.ED448.oid, null) + } + if (paramOid != null) { + return ASN1Sequence(listOf( + ASN1ObjectIdentifier(algOid), + ASN1ObjectIdentifier(paramOid) + )) + } + return ASN1Sequence(listOf( + ASN1ObjectIdentifier(algOid), + )) +} + +private fun parseName(obj: ASN1Sequence): X500Name { + val components = mutableMapOf() + for (elem in obj.elements) { + val dnSet = elem as ASN1Set + val typeAndValue = dnSet.elements[0] as ASN1Sequence + val oidObject = typeAndValue.elements[0] as ASN1ObjectIdentifier + val nameObject = typeAndValue.elements[1] as ASN1String + components.put(oidObject.oid, nameObject) + } + return X500Name(components) +} + +private fun generateName(name: X500Name): ASN1Sequence { + val objs = mutableListOf() + for ((oid, value) in name.components) { + objs.add( + ASN1Set(listOf( + ASN1Sequence(listOf( + ASN1ObjectIdentifier(oid), + value + )) + )) + ) } + return ASN1Sequence(objs) } diff --git a/identity/src/commonMain/kotlin/com/android/identity/crypto/X509CertChain.kt b/identity/src/commonMain/kotlin/com/android/identity/crypto/X509CertChain.kt index ae9cbd5c3..94227ac25 100644 --- a/identity/src/commonMain/kotlin/com/android/identity/crypto/X509CertChain.kt +++ b/identity/src/commonMain/kotlin/com/android/identity/crypto/X509CertChain.kt @@ -32,6 +32,13 @@ data class X509CertChain( } } + /** + * Validates that every certificate in the chain is signed by the next one. + * + * @return true if every certificate in the chain is signed by the next one, false otherwise. + */ + fun validate(): Boolean = Crypto.validateCertChain(this) + companion object { /** * Decodes a certificate chain from CBOR. diff --git a/identity/src/commonMain/kotlin/com/android/identity/crypto/X509KeyUsage.kt b/identity/src/commonMain/kotlin/com/android/identity/crypto/X509KeyUsage.kt new file mode 100644 index 000000000..8b1b147d8 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/crypto/X509KeyUsage.kt @@ -0,0 +1,61 @@ +package com.android.identity.crypto + +import com.android.identity.asn1.ASN1BitString + +enum class X509KeyUsage(val bitNumber: Int) { + DIGITAL_SIGNATURE(0), + NON_REPUDIATION(1), + KEY_ENCIPHERMENT(2), + DATA_ENCIPHERMENT(3), + KEY_AGREEMENT(4), + KEY_CERT_SIGN(5), + CRL_SIGN(6), + ENCIPHER_ONLY(7), + DECIPHER_ONLY(8) + + ; + + companion object { + + fun encodeSet(usages: Set): ASN1BitString { + // Because the definitions is + // + // KeyUsage ::= BIT STRING { + // digitalSignature (0), + // nonRepudiation (1), -- recent editions of X.509 have + // -- renamed this bit to contentCommitment + // keyEncipherment (2), + // dataEncipherment (3), + // keyAgreement (4), + // keyCertSign (5), + // cRLSign (6), + // encipherOnly (7), + // decipherOnly (8) } + // + // we need to drop trailing zero-bits. + // + val booleans = (entries.map { usages.contains(it) }) + val idx = booleans.indexOfLast( { it == true } ) + val booleansReduced = if (idx < 0) { + emptyList() + } else { + booleans.slice(IntRange(0, idx)) + } + return ASN1BitString(booleansReduced.toBooleanArray()) + } + + fun decodeSet(encodedValue: ASN1BitString): Set { + val booleans = encodedValue.asBooleans() + val result = mutableSetOf() + for (usage in entries) { + if (usage.bitNumber < booleans.size) { + if (booleans[usage.bitNumber]) { + result.add(usage) + } + } + } + return result + } + + } +} diff --git a/identity/src/javaSharedMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt b/identity/src/commonMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt similarity index 62% rename from identity/src/javaSharedMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt rename to identity/src/commonMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt index 273bcfd04..fe55318c4 100644 --- a/identity/src/javaSharedMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt +++ b/identity/src/commonMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt @@ -1,32 +1,17 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * 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 com.android.identity.trustmanagement -import com.android.identity.crypto.javaX509Certificate -import java.security.cert.CertificateException -import java.security.cert.PKIXCertPathChecker -import java.security.cert.X509Certificate +import com.android.identity.crypto.X509Cert +import com.android.identity.util.toHex +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant /** * This class is used for the verification of a certificate chain. * * The user of the class can add trust roots using method [addTrustPoint]. - * At this moment certificates of type [X509Certificate] are supported. + * At this moment certificates of type [X509Cert] are supported. * - * The Subject Key Identifier (extension 2.5.29.14 in the [X509Certificate]) + * The Subject Key Identifier (extension 2.5.29.14 in the [X509Cert]) * is used as the primary key / unique identifier of the root CA certificate. * In the verification of the chain this will be matched with the Authority * Key Identifier (extension 2.5.29.35) of the certificate issued by this @@ -34,6 +19,7 @@ import java.security.cert.X509Certificate */ class TrustManager { + // Maps from the hex-encoding of SubjectKeyIdentifier private val certificates = mutableMapOf() /** @@ -42,7 +28,7 @@ class TrustManager { */ class TrustResult( var isTrusted: Boolean, - var trustChain: List = listOf(), + var trustChain: List = listOf(), var trustPoints: List = listOf(), var error: Throwable? = null ) @@ -50,12 +36,10 @@ class TrustManager { /** * Add a [TrustPoint] to the [TrustManager]. */ - fun addTrustPoint(trustPoint: TrustPoint) = - TrustManagerUtil.getSubjectKeyIdentifier(trustPoint.certificate.javaX509Certificate).also { key -> - if (key.isNotEmpty()) { - certificates[key] = trustPoint - } - } + fun addTrustPoint(trustPoint: TrustPoint) { + check(trustPoint.certificate.subjectKeyIdentifier != null) + certificates[trustPoint.certificate.subjectKeyIdentifier!!.toHex()] = trustPoint + } /** @@ -66,20 +50,17 @@ class TrustManager { /** * Remove a [TrustPoint] from the [TrustManager]. */ - fun removeTrustPoint(trustPoint: TrustPoint) = - TrustManagerUtil.getSubjectKeyIdentifier(trustPoint.certificate.javaX509Certificate).also { key -> - certificates.remove(key) - } + fun removeTrustPoint(trustPoint: TrustPoint) = { + check(trustPoint.certificate.subjectKeyIdentifier != null) + certificates.remove(trustPoint.certificate.subjectKeyIdentifier!!.toHex()) + } /** - * Verify a certificate chain (without the self-signed root certificate) - * with the possibility of custom validations on the certificates - * ([customValidators]), for instance the country code in certificate chain - * of the mDL, like implemented in the CountryValidator in the reader app. + * Verify a certificate chain (without the self-signed root certificate). * * @param [chain] the certificate chain without the self-signed root * certificate - * @param [customValidators] optional parameter with custom validators + * @param [atTime] the point in time to check validity for. * @return [TrustResult] a class that returns a verdict * [TrustResult.isTrusted], optionally [TrustResult.trustPoints] the found * trusted root certificates with their display names and icons, optionally @@ -88,14 +69,15 @@ class TrustManager { * an error message when the certificate chain is not trusted. */ fun verify( - chain: List, - customValidators: List = emptyList() + chain: List, + atTime: Instant = Clock.System.now(), ): TrustResult { + // TODO: add support for customValidators similar to PKIXCertPathChecker try { val trustPoints = getAllTrustPoints(chain) - val completeChain = chain.plus(trustPoints.map { it.certificate.javaX509Certificate }) + val completeChain = chain.plus(trustPoints.map { it.certificate }) try { - validateCertificationTrustPath(completeChain, customValidators) + validateCertificationTrustPath(completeChain, atTime) return TrustResult( isTrusted = true, trustPoints = trustPoints, @@ -119,7 +101,7 @@ class TrustManager { // just submits a certificate for the key that their reader will be using. // if (chain.size == 1) { - val trustPoint = certificates[TrustManagerUtil.getSubjectKeyIdentifier(chain[0])] + val trustPoint = certificates[chain[0].subjectKeyIdentifier!!.toHex()] if (trustPoint != null) { return TrustResult( isTrusted = true, @@ -137,15 +119,15 @@ class TrustManager { } } - private fun getAllTrustPoints(chain: List): List { + private fun getAllTrustPoints(chain: List): List { val result = mutableListOf() // only an exception if not a single CA certificate is found var caCertificate: TrustPoint? = findCaCertificate(chain) - ?: throw CertificateException("No trusted root certificate could not be found") + ?: throw IllegalStateException("No trusted root certificate could not be found") result.add(caCertificate!!) - while (caCertificate != null && !TrustManagerUtil.isSelfSigned(caCertificate.certificate.javaX509Certificate)) { - caCertificate = findCaCertificate(listOf(caCertificate.certificate.javaX509Certificate)) + while (caCertificate != null && !TrustManagerUtil.isSelfSigned(caCertificate.certificate)) { + caCertificate = findCaCertificate(listOf(caCertificate.certificate)) if (caCertificate != null) { result.add(caCertificate) } @@ -156,12 +138,11 @@ class TrustManager { /** * Find a CA Certificate for a certificate chain. */ - private fun findCaCertificate(chain: List): TrustPoint? { + private fun findCaCertificate(chain: List): TrustPoint? { chain.forEach { cert -> - TrustManagerUtil.getAuthorityKeyIdentifier(cert).also { key -> - // only certificates with an Authority Key Identifier extension will be matched - if (key.isNotEmpty() && certificates.containsKey(key)) { - return certificates[key] + cert.authorityKeyIdentifier?.toHex().let { + if (certificates.containsKey(it)) { + return certificates[it] } } } @@ -172,23 +153,21 @@ class TrustManager { * Validate the certificate trust path. */ private fun validateCertificationTrustPath( - certificationTrustPath: List, - customValidators: List + certificationTrustPath: List, + atTime: Instant ) { val certIterator = certificationTrustPath.iterator() val leafCertificate = certIterator.next() TrustManagerUtil.checkKeyUsageDocumentSigner(leafCertificate) - TrustManagerUtil.checkValidity(leafCertificate) - TrustManagerUtil.executeCustomValidations(leafCertificate, customValidators) + TrustManagerUtil.checkValidity(leafCertificate, atTime) var previousCertificate = leafCertificate - var caCertificate: X509Certificate? = null + var caCertificate: X509Cert? = null while (certIterator.hasNext()) { caCertificate = certIterator.next() TrustManagerUtil.checkKeyUsageCaCertificate(caCertificate) TrustManagerUtil.checkCaIsIssuer(previousCertificate, caCertificate) TrustManagerUtil.verifySignature(previousCertificate, caCertificate) - TrustManagerUtil.executeCustomValidations(caCertificate, customValidators) previousCertificate = caCertificate } if (caCertificate != null && TrustManagerUtil.isSelfSigned(caCertificate)) { @@ -196,4 +175,4 @@ class TrustManager { TrustManagerUtil.verifySignature(caCertificate, caCertificate) } } -} \ No newline at end of file +} diff --git a/identity/src/commonMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt b/identity/src/commonMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt new file mode 100644 index 000000000..15e35eb47 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * 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 com.android.identity.trustmanagement + +import com.android.identity.crypto.X509Cert +import com.android.identity.crypto.X509KeyUsage +import kotlinx.datetime.Instant + +/** + * Object with utility functions for the TrustManager. + */ +internal object TrustManagerUtil { + /** + * Check whether a certificate is self-signed + */ + fun isSelfSigned(certificate: X509Cert): Boolean = + certificate.issuer == certificate.subject + + /** + * Check that the key usage is the creation of digital signatures. + */ + fun checkKeyUsageDocumentSigner(certificate: X509Cert) { + check(certificate.keyUsage.contains(X509KeyUsage.DIGITAL_SIGNATURE)) { + "Document Signer certificate is not a signing certificate" + } + } + + /** + * Check the validity period of a certificate (based on the system date). + */ + fun checkValidity( + certificate: X509Cert, + atTime: Instant + ) { + // check if the certificate is currently valid + // NOTE does not check if it is valid within the validity period of + // the issuing CA + check(atTime >= certificate.validityNotBefore) { + "Certificate is not yet valid" + } + check(atTime <= certificate.validityNotAfter) { + "Certificate is no longer valid" + } + } + + /** + * Check that the key usage is to sign certificates. + */ + fun checkKeyUsageCaCertificate(caCertificate: X509Cert) { + check(caCertificate.keyUsage.contains(X509KeyUsage.KEY_CERT_SIGN)) { + "CA certificate doesn't have the key usage to sign certificates" + } + } + + /** + * Check that the issuer in [certificate] is equal to the subject in + * [caCertificate]. + */ + fun checkCaIsIssuer(certificate: X509Cert, caCertificate: X509Cert) { + val issuerName = certificate.issuer.name + val nameCA = caCertificate.subject.name + if (issuerName != nameCA) { + throw IllegalStateException("CA certificate '$nameCA' isn't the issuer of the certificate before it. It should be '$issuerName'") + } + } + + /** + * Verify the signature of the [certificate] with the public key of the + * [caCertificate]. + */ + fun verifySignature(certificate: X509Cert, caCertificate: X509Cert) = + try { + certificate.verify(caCertificate.ecPublicKey) + } catch (e: Throwable) { + throw IllegalStateException( + "Certificate '${certificate.subject}' could not be verified with the public key of CA certificate '${caCertificate.subject}'" + ) + } +} \ No newline at end of file diff --git a/identity/src/commonTest/kotlin/com/android/identity/asn1/ASN1Tests.kt b/identity/src/commonTest/kotlin/com/android/identity/asn1/ASN1Tests.kt new file mode 100644 index 000000000..0ed1f0b0e --- /dev/null +++ b/identity/src/commonTest/kotlin/com/android/identity/asn1/ASN1Tests.kt @@ -0,0 +1,661 @@ +package com.android.identity.asn1 + +import com.android.identity.crypto.X509Cert +import com.android.identity.util.fromHex +import com.android.identity.util.toHex +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.io.bytestring.ByteStringBuilder +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.time.Duration.Companion.seconds + +class ASN1Tests { + + private fun encodeTLV( + cls: ASN1TagClass, + enc: ASN1Encoding, + tag: Int, + length: Int + ): ByteArray { + val bsb = ByteStringBuilder() + ASN1.appendIdentifierAndLength(bsb, cls, enc, tag, length) + return bsb.toByteString().toByteArray() + } + + private fun decodeTagFromTLV( + encoded: ByteArray + ): Int { + val (offset, identifierOctects) = ASN1.decodeIdentifierOctets(encoded, 0) + return identifierOctects.tag + } + + @Test + fun testTLVEncoding() { + assertEquals("1e00", encodeTLV(ASN1TagClass.UNIVERSAL, ASN1Encoding.PRIMITIVE, 0x1e, 0).toHex()) + assertEquals("1f1f00", encodeTLV(ASN1TagClass.UNIVERSAL, ASN1Encoding.PRIMITIVE, 0x1f, 0).toHex()) + assertEquals("1f7f00", encodeTLV(ASN1TagClass.UNIVERSAL, ASN1Encoding.PRIMITIVE, 0x7f, 0).toHex()) + assertEquals("1f810000", encodeTLV(ASN1TagClass.UNIVERSAL, ASN1Encoding.PRIMITIVE, 0x80, 0).toHex()) + assertEquals("1f820000", encodeTLV(ASN1TagClass.UNIVERSAL, ASN1Encoding.PRIMITIVE, 0x100, 0).toHex()) + assertEquals("1fbf7f00", encodeTLV(ASN1TagClass.UNIVERSAL, ASN1Encoding.PRIMITIVE, 0x1fff, 0).toHex()) + + assertEquals(0x1e, decodeTagFromTLV("1e00".fromHex())) + assertEquals(0x1f, decodeTagFromTLV("1f1f00".fromHex())) + assertEquals(0x7f, decodeTagFromTLV("1f7f00".fromHex())) + assertEquals(0x80, decodeTagFromTLV("1f810000".fromHex())) + assertEquals(0x100, decodeTagFromTLV("1f820000".fromHex())) + assertEquals(0x1fff, decodeTagFromTLV("1fbf7f00".fromHex())) + } + + @Test + fun testBoolean() { + assertContentEquals("010100".fromHex(), ASN1.encode(ASN1Boolean(false))) + assertContentEquals("0101ff".fromHex(), ASN1.encode(ASN1Boolean(true))) + + assertEquals(ASN1Boolean(false), ASN1.decode("010100".fromHex())) + assertEquals(ASN1Boolean(true), ASN1.decode("0101ff".fromHex())) + assertFailsWith(IllegalArgumentException::class) { + assertEquals(ASN1Boolean(false), ASN1.decode("010101".fromHex())) + } + + assertEquals( + """ + SEQUENCE (2 elem) + BOOLEAN true + BOOLEAN false + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1Boolean(true), + ASN1Boolean(false), + ))).trim() + ) + } + + @Test + fun testInteger() { + // See also ASN1TestsJvm.kt which tests that Long.derEncodeToByteArray() and + // ByteArray.derDecodeAsLong() works as expected. + // + assertEquals("020100", ASN1.encode(ASN1Integer(0)).toHex()) + assertEquals("020101", ASN1.encode(ASN1Integer(1)).toHex()) + assertEquals("02020080", ASN1.encode(ASN1Integer(128L)).toHex()) + assertEquals("0201ff", ASN1.encode(ASN1Integer(-1L)).toHex()) + assertEquals("0202ff01", ASN1.encode(ASN1Integer(-255L)).toHex()) + assertEquals("0202ff00", ASN1.encode(ASN1Integer(-256L)).toHex()) + + assertEquals(ASN1Integer(0L), ASN1.decode("020100".fromHex())) + assertEquals(ASN1Integer(1L), ASN1.decode("020101".fromHex())) + assertEquals(ASN1Integer(128L), ASN1.decode("02020080".fromHex())) + assertEquals(ASN1Integer(-1L), ASN1.decode("0201ff".fromHex())) + assertEquals(ASN1Integer(-255L), ASN1.decode("0202ff01".fromHex())) + assertEquals(ASN1Integer(-256L), ASN1.decode("0202ff00".fromHex())) + + assertEquals( + """ + SEQUENCE (6 elem) + INTEGER 0 + INTEGER 1 + INTEGER 128 + INTEGER -1 + INTEGER -255 + INTEGER -256 + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1Integer(0L), + ASN1Integer(1L), + ASN1Integer(128L), + ASN1Integer(-1L), + ASN1Integer(-255L), + ASN1Integer(-256L), + ))).trim() + ) + } + + @Test + fun testEnumerated() { + assertEquals("0a0100", ASN1.encode(ASN1Integer(0, ASN1IntegerTag.ENUMERATED.tag)).toHex()) + assertEquals("0a0101", ASN1.encode(ASN1Integer(1, ASN1IntegerTag.ENUMERATED.tag)).toHex()) + assertEquals("0a020080", ASN1.encode(ASN1Integer(128L, ASN1IntegerTag.ENUMERATED.tag)).toHex()) + assertEquals("0a01ff", ASN1.encode(ASN1Integer(-1L, ASN1IntegerTag.ENUMERATED.tag)).toHex()) + assertEquals("0a02ff01", ASN1.encode(ASN1Integer(-255L, ASN1IntegerTag.ENUMERATED.tag)).toHex()) + assertEquals("0a02ff00", ASN1.encode(ASN1Integer(-256L, ASN1IntegerTag.ENUMERATED.tag)).toHex()) + + assertEquals(ASN1Integer(0L, ASN1IntegerTag.ENUMERATED.tag), ASN1.decode("0a0100".fromHex())) + assertEquals(ASN1Integer(1L, ASN1IntegerTag.ENUMERATED.tag), ASN1.decode("0a0101".fromHex())) + assertEquals(ASN1Integer(128L, ASN1IntegerTag.ENUMERATED.tag), ASN1.decode("0a020080".fromHex())) + assertEquals(ASN1Integer(-1L, ASN1IntegerTag.ENUMERATED.tag), ASN1.decode("0a01ff".fromHex())) + assertEquals(ASN1Integer(-255L, ASN1IntegerTag.ENUMERATED.tag), ASN1.decode("0a02ff01".fromHex())) + assertEquals(ASN1Integer(-256L, ASN1IntegerTag.ENUMERATED.tag), ASN1.decode("0a02ff00".fromHex())) + + assertEquals( + """ + SEQUENCE (6 elem) + ENUMERATED 0 + ENUMERATED 1 + ENUMERATED 128 + ENUMERATED -1 + ENUMERATED -255 + ENUMERATED -256 + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1Integer(0L, ASN1IntegerTag.ENUMERATED.tag), + ASN1Integer(1L, ASN1IntegerTag.ENUMERATED.tag), + ASN1Integer(128L, ASN1IntegerTag.ENUMERATED.tag), + ASN1Integer(-1L, ASN1IntegerTag.ENUMERATED.tag), + ASN1Integer(-255L, ASN1IntegerTag.ENUMERATED.tag), + ASN1Integer(-256L, ASN1IntegerTag.ENUMERATED.tag), + ))).trim() + ) + } + + @Test + fun testNull() { + assertContentEquals("0500".fromHex(), ASN1.encode(ASN1Null())) + + assertEquals(ASN1Null(), ASN1.decode("0500".fromHex())) + + assertEquals( + """ + SEQUENCE (2 elem) + NULL + NULL + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1Null(), + ASN1Null(), + ))).trim() + ) + } + + @Test + fun testSequence() { + assertEquals( + "3009020100020101020102", + ASN1.encode( + ASN1Sequence( + listOf( + ASN1Integer(0), + ASN1Integer(1), + ASN1Integer(2), + ) + ) + ).toHex() + ) + + assertEquals( + ASN1Sequence( + listOf( + ASN1Integer(0), + ASN1Integer(1), + ASN1Integer(2), + ) + ), + ASN1.decode("3009020100020101020102".fromHex()) + ) + + assertEquals( + """ + SEQUENCE (3 elem) + INTEGER 0 + INTEGER 1 + INTEGER 2 + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1Integer(0), + ASN1Integer(1), + ASN1Integer(2), + ))).trim() + ) + } + + @Test + fun testSet() { + assertEquals( + "3109020100020101020102", + ASN1.encode( + ASN1Set( + listOf( + ASN1Integer(0), + ASN1Integer(1), + ASN1Integer(2), + ) + ) + ).toHex() + ) + + assertEquals( + ASN1Set( + listOf( + ASN1Integer(0), + ASN1Integer(1), + ASN1Integer(2), + ) + ), + ASN1.decode("3109020100020101020102".fromHex()) + ) + + assertEquals( + """ + SET (3 elem) + INTEGER 0 + INTEGER 1 + INTEGER 2 + """.trimIndent(), + ASN1.print(ASN1Set(listOf( + ASN1Integer(0), + ASN1Integer(1), + ASN1Integer(2), + ))).trim() + ) + } + + @Test + fun testObjectIdentifier() { + assertEquals( + "06082a8648ce3d040303", + ASN1.encode(ASN1ObjectIdentifier("1.2.840.10045.4.3.3")).toHex() + ) + assertEquals( + "0603551d0e", + ASN1.encode(ASN1ObjectIdentifier("2.5.29.14")).toHex() + ) + assertEquals( + "06052b81040022", + ASN1.encode(ASN1ObjectIdentifier("1.3.132.0.34")).toHex() + ) + + assertEquals( + ASN1ObjectIdentifier("1.2.840.10045.4.3.3"), + ASN1.decode("06082a8648ce3d040303".fromHex()) + ) + assertEquals( + ASN1ObjectIdentifier("2.5.29.14"), + ASN1.decode("0603551d0e".fromHex()) + ) + assertEquals( + ASN1ObjectIdentifier("1.3.132.0.34"), + ASN1.decode("06052b81040022".fromHex()) + ) + + assertEquals( + """ + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.29.14 subjectKeyIdentifier (X.509 extension) + OBJECT IDENTIFIER 1.3.132.0.34 EC Curve P-384 + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1ObjectIdentifier(OID.X509_EXTENSION_SUBJECT_KEY_IDENTIFIER.oid), + ASN1ObjectIdentifier(OID.EC_CURVE_P384.oid) + ))).trim() + ) + } + + @Test + fun testTime() { + val pairs = mapOf( + ASN1Time( + LocalDateTime(1950, 2, 1, 0, 0, 0).toInstant(TimeZone.UTC), + ASN1TimeTag.UTC_TIME.tag + ) to "500201000000Z".encodeToByteArray().prependPrimitiveTLV(0x17), + ASN1Time( + LocalDateTime(1999, 12, 31, 10, 20, 30).toInstant(TimeZone.UTC), + ASN1TimeTag.UTC_TIME.tag + ) to "991231102030Z".encodeToByteArray().prependPrimitiveTLV(0x17), + ASN1Time( + LocalDateTime(2000, 1, 1, 0, 0, 0).toInstant(TimeZone.UTC), + ASN1TimeTag.UTC_TIME.tag + ) to "000101000000Z".encodeToByteArray().prependPrimitiveTLV(0x17), + ASN1Time( + LocalDateTime(2024, 12, 6, 9, 57, 10).toInstant(TimeZone.UTC), + ASN1TimeTag.UTC_TIME.tag + ) to "241206095710Z".encodeToByteArray().prependPrimitiveTLV(0x17), + + ASN1Time( + LocalDateTime(2024, 1, 1, 0, 0, 0).toInstant(TimeZone.UTC), + ASN1TimeTag.GENERALIZED_TIME.tag + ) to "20240101000000Z".encodeToByteArray().prependPrimitiveTLV(0x18), + ASN1Time( + LocalDateTime(2024, 1, 1, 0, 0, 0, + 0.5.seconds.inWholeNanoseconds.toInt() + ).toInstant(TimeZone.UTC), + ASN1TimeTag.GENERALIZED_TIME.tag + ) to "20240101000000.5Z".encodeToByteArray().prependPrimitiveTLV(0x18), + ASN1Time( + LocalDateTime(2024, 1, 1, 0, 0, 0, + 0.54321.seconds.inWholeNanoseconds.toInt() + ).toInstant(TimeZone.UTC), + ASN1TimeTag.GENERALIZED_TIME.tag + ) to "20240101000000.54321Z".encodeToByteArray().prependPrimitiveTLV(0x18), + ) + for ((obj, encoded) in pairs) { + assertEquals(obj, ASN1.decode(encoded), "error decoding ${encoded.toHex()}") + assertEquals(encoded.toHex(), ASN1.encode(obj).toHex(), "error encoding $obj (should be ${encoded.toHex()})") + } + + assertEquals( + """ + SEQUENCE (2 elem) + GeneralizedTime 2024-01-01T00:00:00.500Z + UTCTime 1999-12-31T10:20:30Z + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1Time( + LocalDateTime(2024, 1, 1, 0, 0, 0, + 0.5.seconds.inWholeNanoseconds.toInt() + ).toInstant(TimeZone.UTC), + ), + ASN1Time( + LocalDateTime(1999, 12, 31, 10, 20, 30).toInstant(TimeZone.UTC), + ASN1TimeTag.UTC_TIME.tag + ) + ))).trim() + ) + } + + @Test + fun testStrings() { + val pairs = mutableMapOf( + ASN1String("foobar") to "0c06".fromHex() + "foobar".encodeToByteArray() + ) + // Tests for all string tags + for (tag in ASN1StringTag.entries) { + pairs.put( + ASN1String(value = "foo", tag = tag.tag), + byteArrayOf(tag.tag.toByte(), 3) + "foo".encodeToByteArray() + ) + } + for ((obj, encoded) in pairs) { + assertEquals(obj, ASN1.decode(encoded)) + assertEquals(encoded.toHex(), ASN1.encode(obj).toHex()) + } + + assertEquals( + """ + SEQUENCE (12 elem) + UTF8String String with tag value 12 + NumericString String with tag value 18 + PrintableString String with tag value 19 + TeletexString String with tag value 20 + VideotexString String with tag value 21 + IA5String String with tag value 22 + GraphicString String with tag value 25 + VisibleString String with tag value 26 + GeneralString String with tag value 27 + UniversalString String with tag value 28 + CharacterString String with tag value 29 + BmpString String with tag value 30 + """.trimIndent(), + ASN1.print(ASN1Sequence( + ASN1StringTag.entries.map { ASN1String("String with tag value ${it.tag}", it.tag) } + )).trim() + ) + } + + @Test + fun testBitString() { + val pairs = mapOf>( + ASN1BitString(numUnusedBits = 0, value = "4401".fromHex()) to + Pair("0303004401".fromHex(), "0100010000000001"), + + ASN1BitString(numUnusedBits = 2, value = "44f8".fromHex()) to + Pair("03030244f8".fromHex(), "01000100111110") + ) + for ((obj, encodedAndTextual) in pairs) { + assertEquals(obj, ASN1.decode(encodedAndTextual.first)) + assertEquals(encodedAndTextual.first.toHex(), ASN1.encode(obj).toHex()) + assertEquals(encodedAndTextual.second, (obj as ASN1BitString).renderBitString()) + } + + assertEquals( + """ + SEQUENCE (6 elem) + BIT STRING (3 bit) 111 + BIT STRING (5 bit) 11000 + BIT STRING (5 bit) 11111 + BIT STRING (8 bit) 11111111 + BIT STRING (16 bit) 1111111100000000 + BIT STRING (17 bit) 11111111000000001 + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1BitString(5, "e0".fromHex()), + ASN1BitString(3, "c0".fromHex()), + ASN1BitString(3, "f8".fromHex()), + ASN1BitString(0, "ff".fromHex()), + ASN1BitString(0, "ff00".fromHex()), + ASN1BitString(7, "ff0080".fromHex()), + ))).trim() + ) + + // to/from boolean arrays + // + assertEquals( + "BIT STRING (1 bit) 0", + ASN1.print(ASN1BitString(listOf(false).toBooleanArray())).trim() + ) + assertEquals( + "BIT STRING (1 bit) 1", + ASN1.print(ASN1BitString(listOf(true).toBooleanArray())).trim() + ) + assertEquals( + "BIT STRING (2 bit) 01", + ASN1.print(ASN1BitString(listOf(false, true).toBooleanArray())).trim() + ) + assertEquals( + "BIT STRING (7 bit) 1011010", + ASN1.print(ASN1BitString(listOf(true, false, true, true, false, true, false).toBooleanArray())).trim() + ) + + assertContentEquals( + listOf(false).toBooleanArray(), + ASN1BitString(listOf(false).toBooleanArray()).asBooleans() + ) + assertContentEquals( + listOf(true).toBooleanArray(), + ASN1BitString(listOf(true).toBooleanArray()).asBooleans() + ) + assertContentEquals( + listOf(false, true).toBooleanArray(), + ASN1BitString(listOf(false, true).toBooleanArray()).asBooleans() + ) + assertContentEquals( + listOf(true, false, true, true, false, true, false, true, true).toBooleanArray(), + ASN1BitString(listOf(true, false, true, true, false, true, false, true, true).toBooleanArray()).asBooleans() + ) + } + + @Test + fun testOctetString() { + val pairs = mapOf( + ASN1OctetString(value = "fffe".fromHex()) to "0402fffe".fromHex(), + ASN1OctetString(value = "".fromHex()) to "0400".fromHex(), + ) + for ((obj, encoded) in pairs) { + assertEquals(obj, ASN1.decode(encoded)) + assertEquals(encoded.toHex(), ASN1.encode(obj).toHex()) + } + + assertEquals( + """ + SEQUENCE (6 elem) + OCTET STRING (1 byte) e0 + OCTET STRING (1 byte) c0 + OCTET STRING (1 byte) f8 + OCTET STRING (1 byte) ff + OCTET STRING (2 byte) ff00 + OCTET STRING (3 byte) ff0080 + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1OctetString("e0".fromHex()), + ASN1OctetString("c0".fromHex()), + ASN1OctetString("f8".fromHex()), + ASN1OctetString("ff".fromHex()), + ASN1OctetString("ff00".fromHex()), + ASN1OctetString("ff0080".fromHex()), + ))).trim() + ) + } + + @Test + fun testUnsupportedTag() { + // Checks that an unsupported tag appears as ASN1RawObject + // + val obj = ASN1Sequence( + listOf( + ASN1Integer(0), + ASN1Integer(1), + ASN1RawObject(ASN1TagClass.UNIVERSAL, ASN1Encoding.PRIMITIVE, 0x09, byteArrayOf(16, 17)), + ASN1Integer(2), + ) + ) + val encoded = "300d02010002010109021011020102".fromHex() + + assertEquals(obj, ASN1.decode(encoded)) + assertEquals(encoded.toHex(), ASN1.encode(obj).toHex()) + + assertEquals( + """ + SEQUENCE (1 elem) + UNSUPPORTED TAG class=UNIVERSAL encoding=PRIMITIVE tag=9 value=1011 + """.trimIndent(), + ASN1.print(ASN1Sequence(listOf( + ASN1RawObject(ASN1TagClass.UNIVERSAL, ASN1Encoding.PRIMITIVE, 0x09, byteArrayOf(16, 17)) + ))).trim() + ) + } + + + // This is Maryland's IACA certificate (IACA_Root_2024.cer) downloaded from + // + // https://mva.maryland.gov/Pages/MDMobileID_Googlewallet.aspx + // + private val exampleX509Cert = X509Cert.fromPem( + """ +-----BEGIN CERTIFICATE----- +MIICxjCCAmygAwIBAgITJkV7El8K11IXqY7mz96n/EhiITAKBggqhkjOPQQDAjBq +MQ4wDAYDVQQIEwVVUy1NRDELMAkGA1UEBhMCVVMxFDASBgNVBAcTC0dsZW4gQnVy +bmllMRUwEwYDVQQKEwxNYXJ5bGFuZCBNVkExHjAcBgNVBAMTFUZhc3QgRW50ZXJw +cmlzZXMgUm9vdDAeFw0yNDAxMDUwNTAwMDBaFw0yOTAxMDQwNTAwMDBaMGoxDjAM +BgNVBAgTBVVTLU1EMQswCQYDVQQGEwJVUzEUMBIGA1UEBxMLR2xlbiBCdXJuaWUx +FTATBgNVBAoTDE1hcnlsYW5kIE1WQTEeMBwGA1UEAxMVRmFzdCBFbnRlcnByaXNl +cyBSb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaWcKIqlAWboV93RAa5ad +0LJBn8W0/yYwtOyUlxuTxoo4SPkorKmOz3EhThC+U4WRrt13aSnCsJtK+waBFghX +u6OB8DCB7TAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV +HQ4EFgQUTprRzaFBJ1SLjJsO01tlLCQ4YF0wPAYDVR0SBDUwM4EWbXZhY3NAbWRv +dC5zdGF0ZS5tZC51c4YZaHR0cHM6Ly9tdmEubWFyeWxhbmQuZ292LzBYBgNVHR8E +UTBPME2gS6BJhkdodHRwczovL215bXZhLm1hcnlsYW5kLmdvdjo1NDQzL01EUC9X +ZWJTZXJ2aWNlcy9DUkwvbURML3Jldm9jYXRpb25zLmNybDAQBgkrBgEEAYPFIQEE +A01EUDAKBggqhkjOPQQDAgNIADBFAiEAnX3+E4E5dQ+5G1rmStJTW79ZAiDTabyL +8lJuYL/nDxMCIHHkAyIJcQlQmKDUVkBr3heUd5N9Y8GWdbWnbHuwe7Om +-----END CERTIFICATE----- + """.trimIndent()) + + @Test + fun testCertificate() { + // Check that we encode to exactly the same bits as we decoded... + val certificate = ASN1.decode(exampleX509Cert.encodedCertificate) + val reencoded = ASN1.encode(certificate!!) + assertEquals(exampleX509Cert.encodedCertificate.toHex(), reencoded.toHex()) + } + + @Test + fun testPrettyPrint() { + val certificate = ASN1.decode(exampleX509Cert.encodedCertificate) + assertEquals(""" + SEQUENCE (3 elem) + SEQUENCE (8 elem) + [0] (1 elem) + INTEGER 2 + INTEGER 26457b125f0ad75217a98ee6cfdea7fc486221 + SEQUENCE (1 elem) + OBJECT IDENTIFIER 1.2.840.10045.4.3.2 ECDSA coupled with SHA-256 + SEQUENCE (5 elem) + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.8 stateOrProvinceName (X.520 DN component) + PrintableString US-MD + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.6 countryName (X.520 DN component) + PrintableString US + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.7 localityName (X.520 DN component) + PrintableString Glen Burnie + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.10 organizationName (X.520 DN component) + PrintableString Maryland MVA + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.3 commonName (X.520 DN component) + PrintableString Fast Enterprises Root + SEQUENCE (2 elem) + UTCTime 2024-01-05T05:00:00Z + UTCTime 2029-01-04T05:00:00Z + SEQUENCE (5 elem) + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.8 stateOrProvinceName (X.520 DN component) + PrintableString US-MD + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.6 countryName (X.520 DN component) + PrintableString US + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.7 localityName (X.520 DN component) + PrintableString Glen Burnie + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.10 organizationName (X.520 DN component) + PrintableString Maryland MVA + SET (1 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.4.3 commonName (X.520 DN component) + PrintableString Fast Enterprises Root + SEQUENCE (2 elem) + SEQUENCE (2 elem) + OBJECT IDENTIFIER 1.2.840.10045.2.1 Elliptic curve public key cryptography + OBJECT IDENTIFIER 1.2.840.10045.3.1.7 NIST Curve P-256 + BIT STRING (520 bit) 0000010001101001011001110000101000100010101010010100000001011001101110100001010111110111011101000100000001101011100101101001110111010000101100100100000110011111110001011011010011111111001001100011000010110100111011001001010010010111000110111001001111000110100010100011100001001000111110010010100010101100101010011000111011001111011100010010000101001110000100001011111001010011100001011001000110101110110111010111011101101001001010011100001010110000100110110100101011111011000001101000000100010110000010000101011110111011 + [3] (1 elem) + SEQUENCE (6 elem) + SEQUENCE (3 elem) + OBJECT IDENTIFIER 2.5.29.15 keyUsage (X.509 extension) + BOOLEAN true + OCTET STRING (4 byte) 03020106 + SEQUENCE (3 elem) + OBJECT IDENTIFIER 2.5.29.19 basicConstraints (X.509 extension) + BOOLEAN true + OCTET STRING (8 byte) 30060101ff020100 + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.29.14 subjectKeyIdentifier (X.509 extension) + OCTET STRING (22 byte) 04144e9ad1cda14127548b8c9b0ed35b652c2438605d + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.29.18 issuerAltName (X.509 extension) + OCTET STRING (53 byte) 303381166d76616373406d646f742e73746174652e6d642e7573861968747470733a2f2f6d76612e6d6172796c616e642e676f762f + SEQUENCE (2 elem) + OBJECT IDENTIFIER 2.5.29.31 cRLDistributionPoints (X.509 extension) + OCTET STRING (81 byte) 304f304da04ba049864768747470733a2f2f6d796d76612e6d6172796c616e642e676f763a353434332f4d44502f57656253657276696365732f43524c2f6d444c2f7265766f636174696f6e732e63726c + SEQUENCE (2 elem) + OBJECT IDENTIFIER 1.3.6.1.4.1.58017.1 + OCTET STRING (3 byte) 4d4450 + SEQUENCE (1 elem) + OBJECT IDENTIFIER 1.2.840.10045.4.3.2 ECDSA coupled with SHA-256 + BIT STRING (568 bit) 0011000001000101000000100010000100000000100111010111110111111110000100111000000100111001011101010000111110111001000110110101101011100110010010101101001001010011010110111011111101011001000000100010000011010011011010011011110010001011111100100101001001101110011000001011111111100111000011110001001100000010001000000111000111100100000000110010001000001001011100010000100101010000100110001010000011010100010101100100000001101011110111100001011110010100011101111001001101111101011000111100000110010110011101011011010110100111011011000111101110110000011110111011001110100110 + """.trimIndent(), + ASN1.print(certificate!!).trim() + ) + } +} + +private fun ByteArray.prependPrimitiveTLV(tagNum: Int): ByteArray { + val bsb = ByteStringBuilder() + ASN1.appendUniversalTagEncodingLength(bsb, tagNum, ASN1Encoding.PRIMITIVE, this.size) + bsb.append(this) + return bsb.toByteString().toByteArray() +} + diff --git a/identity/src/commonTest/kotlin/com/android/identity/crypto/X500NameTests.kt b/identity/src/commonTest/kotlin/com/android/identity/crypto/X500NameTests.kt new file mode 100644 index 000000000..2b6d69ab6 --- /dev/null +++ b/identity/src/commonTest/kotlin/com/android/identity/crypto/X500NameTests.kt @@ -0,0 +1,64 @@ +package com.android.identity.crypto + +import com.android.identity.asn1.ASN1String +import com.android.identity.asn1.OID +import kotlin.test.Test +import kotlin.test.assertEquals + +class X500NameTests { + @Test + fun testX500NameSimple() { + assertEquals( + "CN=Foo", + X500Name.fromName("CN=Foo").name + ) + assertEquals( + mapOf(OID.COMMON_NAME.oid to ASN1String("Foo")), + X500Name.fromName("CN=Foo").components + ) + } + + @Test + fun testX500NameComplicated() { + val str = "CN=David,ST=US-MA,O=Google,OU=Android,C=US" + assertEquals(str, X500Name.fromName(str).name) + assertEquals( + mapOf( + OID.COUNTRY_NAME.oid to ASN1String("US"), + OID.ORGANIZATIONAL_UNIT_NAME.oid to ASN1String("Android"), + OID.ORGANIZATION_NAME.oid to ASN1String("Google"), + OID.STATE_OR_PROVINCE_NAME.oid to ASN1String("US-MA"), + OID.COMMON_NAME.oid to ASN1String("David"), + ), + X500Name.fromName(str).components + ) + // assertEquals() on a map doesn't check the order... make sure that the parsed keys + // are the reverse order of their appearance in the string as per RFC 2293 section 2.1 + assertEquals( + listOf( + OID.COUNTRY_NAME.oid, + OID.ORGANIZATIONAL_UNIT_NAME.oid, + OID.ORGANIZATION_NAME.oid, + OID.STATE_OR_PROVINCE_NAME.oid, + OID.COMMON_NAME.oid, + ), + X500Name.fromName(str).components.keys.toList() + ) + } + + @Test + fun testX500NameFromOID() { + assertEquals( + "CN=Foo", + X500Name(mapOf(OID.COMMON_NAME.oid to ASN1String("Foo"))).name + ) + } + + @Test + fun testX500NameFromUnknownOID() { + assertEquals( + "2.5.4.999=#0c03466f6f", + X500Name(mapOf("2.5.4.999" to ASN1String("Foo"))).name + ) + } +} \ No newline at end of file diff --git a/identity/src/commonTest/kotlin/com/android/identity/crypto/X509CertTests.kt b/identity/src/commonTest/kotlin/com/android/identity/crypto/X509CertTests.kt index 85a1d54f7..4860d712a 100644 --- a/identity/src/commonTest/kotlin/com/android/identity/crypto/X509CertTests.kt +++ b/identity/src/commonTest/kotlin/com/android/identity/crypto/X509CertTests.kt @@ -1,16 +1,24 @@ package com.android.identity.crypto +import com.android.identity.asn1.ASN1 +import com.android.identity.asn1.ASN1Integer +import com.android.identity.asn1.OID import com.android.identity.util.fromHex import com.android.identity.util.toHex +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.hours class X509CertTests { // This is a key attestation recorded from an Android device and traces up to a Google CA. // It contains both EC and RSA keys of various sizes making it an useful test vector for - // X509Certificate implementations. + // X509Cert implementations. // private val androidKeyCertChain = X509CertChain( listOf( @@ -107,4 +115,118 @@ class X509CertTests { } } + // Checks that X509Cert.verify() works with certificates created by X509Cert.Builder + private fun testCertSignedWithCurve(curve: EcCurve) { + if (!Crypto.supportedCurves.contains(curve)) { + println("Skipping testCertSignedWithCurve($curve) since platform does not support the curve.") + return + } + + val key = Crypto.createEcPrivateKey(curve) + val now = Instant.fromEpochSeconds(Clock.System.now().epochSeconds) + val serialNumber = ASN1Integer(1) + val subject = X500Name.fromName("CN=Foobar1") + val issuer = X500Name.fromName("CN=Foobar2") + val cert = X509Cert.Builder( + publicKey = key.publicKey, + signingKey = key, + signatureAlgorithm = key.curve.defaultSigningAlgorithm, + serialNumber = serialNumber, + subject = subject, + issuer = issuer, + validFrom = now - 1.hours, + validUntil = now + 1.hours + ) + .includeSubjectKeyIdentifier() + .includeAuthorityKeyIdentifierAsSubjectKeyIdentifier() + .build() + + assertTrue(cert.verify(key.publicKey)) + + // Also check that the fields are as expected. + assertEquals(curve.defaultSigningAlgorithm, cert.signatureAlgorithm) + assertEquals(2, cert.version) + assertEquals(cert.serialNumber, serialNumber) + assertEquals(cert.issuer, issuer) + assertEquals(cert.validityNotBefore, now - 1.hours) + assertEquals(cert.validityNotAfter, now + 1.hours) + assertEquals(cert.subject, subject) + assertEquals(cert.ecPublicKey, key.publicKey) + assertEquals( + setOf( + OID.X509_EXTENSION_SUBJECT_KEY_IDENTIFIER.oid, + OID.X509_EXTENSION_AUTHORITY_KEY_IDENTIFIER.oid, + ), + cert.nonCriticalExtensionOIDs + ) + assertTrue(cert.criticalExtensionOIDs.isEmpty()) + + val subjectPublicKey = when (key.publicKey) { + is EcPublicKeyDoubleCoordinate -> { + (key.publicKey as EcPublicKeyDoubleCoordinate).asUncompressedPointEncoding + } + is EcPublicKeyOkp -> { + (key.publicKey as EcPublicKeyOkp).x + } + } + + // Check the subjectKeyIdentifier and authorityKeyIdentifier extensions are correct and + // also correctly encoded. + val expectedSubjectKeyIdentifier = Crypto.digest(Algorithm.INSECURE_SHA1, subjectPublicKey) + assertEquals(expectedSubjectKeyIdentifier.toHex(), cert.subjectKeyIdentifier!!.toHex()) + assertEquals(expectedSubjectKeyIdentifier.toHex(), cert.authorityKeyIdentifier!!.toHex()) + assertEquals( + "OCTET STRING (20 byte) ${expectedSubjectKeyIdentifier.toHex()}", + ASN1.print(ASN1.decode(cert.getExtensionValue(OID.X509_EXTENSION_SUBJECT_KEY_IDENTIFIER.oid)!!)!!).trim() + ) + assertEquals( + """ + SEQUENCE (1 elem) + [0] (1 elem) + (20 byte) ${expectedSubjectKeyIdentifier.toHex()} + """.trimIndent(), + ASN1.print(ASN1.decode(cert.getExtensionValue(OID.X509_EXTENSION_AUTHORITY_KEY_IDENTIFIER.oid)!!)!!).trim() + ) + } + + @Test fun testCertSignedWithCurve_P256() = testCertSignedWithCurve(EcCurve.P256) + @Test fun testCertSignedWithCurve_P384() = testCertSignedWithCurve(EcCurve.P384) + @Test fun testCertSignedWithCurve_P521() = testCertSignedWithCurve(EcCurve.P521) + @Test fun testCertSignedWithCurve_BRAINPOOLP256R1() = testCertSignedWithCurve(EcCurve.BRAINPOOLP256R1) + @Test fun testCertSignedWithCurve_BRAINPOOLP320R1() = testCertSignedWithCurve(EcCurve.BRAINPOOLP320R1) + @Test fun testCertSignedWithCurve_BRAINPOOLP384R1() = testCertSignedWithCurve(EcCurve.BRAINPOOLP384R1) + @Test fun testCertSignedWithCurve_BRAINPOOLP512R1() = testCertSignedWithCurve(EcCurve.BRAINPOOLP512R1) + @Test fun testCertSignedWithCurve_ED25519() = testCertSignedWithCurve(EcCurve.ED25519) + @Test fun testCertSignedWithCurve_ED448() = testCertSignedWithCurve(EcCurve.ED448) + + @Test + fun testKeyUsageEncoding() { + assertEquals( + "BIT STRING (9 bit) 000000001", + ASN1.print(X509KeyUsage.encodeSet(setOf( + X509KeyUsage.DECIPHER_ONLY + ))).trim() + ) + assertEquals( + X509KeyUsage.encodeSet(setOf( + X509KeyUsage.DECIPHER_ONLY + )), + ASN1.decode("0303070080".fromHex()) + ) + + // Check that we handle trailing zero bits correctly + assertEquals( + "BIT STRING (7 bit) 0000011", + ASN1.print(X509KeyUsage.encodeSet(setOf( + X509KeyUsage.KEY_CERT_SIGN, X509KeyUsage.CRL_SIGN + ))).trim() + ) + assertEquals( + X509KeyUsage.encodeSet(setOf( + X509KeyUsage.KEY_CERT_SIGN, X509KeyUsage.CRL_SIGN + )), + ASN1.decode("03020106".fromHex()) + ) + } + } \ No newline at end of file diff --git a/identity/src/commonTest/kotlin/com/android/identity/trustmanagement/TrustManagerTest.kt b/identity/src/commonTest/kotlin/com/android/identity/trustmanagement/TrustManagerTest.kt new file mode 100644 index 000000000..5e8d30e55 --- /dev/null +++ b/identity/src/commonTest/kotlin/com/android/identity/trustmanagement/TrustManagerTest.kt @@ -0,0 +1,265 @@ +package com.android.identity.trustmanagement + +import com.android.identity.asn1.ASN1Integer +import com.android.identity.crypto.Crypto +import com.android.identity.crypto.EcCurve +import com.android.identity.crypto.X500Name +import com.android.identity.crypto.X509Cert +import com.android.identity.crypto.X509KeyUsage +import kotlinx.datetime.Clock +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.hours + + +class TrustManagerTest { + + val caCertificate: X509Cert + val intermediateCertificate: X509Cert + val dsCertificate: X509Cert + + val dsValidInThePastCertificate: X509Cert + val dsValidInTheFutureCertificate: X509Cert + + val ca2Certificate: X509Cert + val ds2Certificate: X509Cert + + init { + val now = Clock.System.now() + + val caKey = Crypto.createEcPrivateKey(EcCurve.P384) + caCertificate = + X509Cert.Builder( + publicKey = caKey.publicKey, + signingKey = caKey, + signatureAlgorithm = caKey.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Test TrustManager CA"), + issuer = X500Name.fromName("CN=Test TrustManager CA"), + validFrom = now - 1.hours, + validUntil = now + 1.hours + ) + .includeSubjectKeyIdentifier() + .setKeyUsage(setOf(X509KeyUsage.KEY_CERT_SIGN)) + .build() + + val intermediateKey = Crypto.createEcPrivateKey(EcCurve.P384) + intermediateCertificate = X509Cert.Builder( + publicKey = intermediateKey.publicKey, + signingKey = caKey, + signatureAlgorithm = caKey.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Test TrustManager Intermediate CA"), + issuer = caCertificate.subject, + validFrom = now - 1.hours, + validUntil = now + 1.hours + ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(caCertificate) + .setKeyUsage(setOf(X509KeyUsage.KEY_CERT_SIGN)) + .build() + + val dsKey = Crypto.createEcPrivateKey(EcCurve.P384) + dsCertificate = X509Cert.Builder( + publicKey = dsKey.publicKey, + signingKey = intermediateKey, + signatureAlgorithm = intermediateKey.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Test TrustManager DS"), + issuer = intermediateCertificate.subject, + validFrom = now - 1.hours, + validUntil = now + 1.hours + ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(intermediateCertificate) + .setKeyUsage(setOf(X509KeyUsage.DIGITAL_SIGNATURE)) + .build() + + val dsValidInThePastKey = Crypto.createEcPrivateKey(EcCurve.P384) + dsValidInThePastCertificate = X509Cert.Builder( + publicKey = dsValidInThePastKey.publicKey, + signingKey = intermediateKey, + signatureAlgorithm = intermediateKey.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Test TrustManager DS Valid In The Past"), + issuer = intermediateCertificate.subject, + validFrom = now - 3.hours, + validUntil = now - 1.hours + ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(intermediateCertificate) + .setKeyUsage(setOf(X509KeyUsage.DIGITAL_SIGNATURE)) + .build() + + val dsValidInTheFutureKey = Crypto.createEcPrivateKey(EcCurve.P384) + dsValidInTheFutureCertificate = X509Cert.Builder( + publicKey = dsValidInTheFutureKey.publicKey, + signingKey = intermediateKey, + signatureAlgorithm = intermediateKey.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Test TrustManager Valid In The Future"), + issuer = intermediateCertificate.subject, + validFrom = now + 1.hours, + validUntil = now + 3.hours + ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(intermediateCertificate) + .setKeyUsage(setOf(X509KeyUsage.DIGITAL_SIGNATURE)) + .build() + + val ca2Key = Crypto.createEcPrivateKey(EcCurve.P384) + ca2Certificate = + X509Cert.Builder( + publicKey = ca2Key.publicKey, + signingKey = ca2Key, + signatureAlgorithm = ca2Key.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Test TrustManager CA2"), + issuer = X500Name.fromName("CN=Test TrustManager CA2"), + validFrom = now - 1.hours, + validUntil = now + 1.hours + ) + .includeSubjectKeyIdentifier() + .setKeyUsage(setOf(X509KeyUsage.KEY_CERT_SIGN)) + .build() + + val ds2Key = Crypto.createEcPrivateKey(EcCurve.P384) + ds2Certificate = X509Cert.Builder( + publicKey = ds2Key.publicKey, + signingKey = ca2Key, + signatureAlgorithm = ca2Key.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Test TrustManager DS2"), + issuer = ca2Certificate.subject, + validFrom = now - 1.hours, + validUntil = now + 1.hours + ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(ca2Certificate) + .setKeyUsage(setOf(X509KeyUsage.DIGITAL_SIGNATURE)) + .build() + } + + @Test + fun testTrustManagerHappyFlow() { + val trustManager = TrustManager() + + trustManager.addTrustPoint(TrustPoint(intermediateCertificate)) + trustManager.addTrustPoint(TrustPoint(caCertificate)) + + trustManager.verify(listOf(dsCertificate)).let { + assertEquals(null, it.error) + assertTrue(it.isTrusted) + assertEquals(3, it.trustChain.size) + assertEquals(caCertificate, it.trustChain.last()) + } + } + + @Test + fun testTrustManagerValidInThePast() { + val trustManager = TrustManager() + + trustManager.addTrustPoint(TrustPoint(intermediateCertificate)) + trustManager.addTrustPoint(TrustPoint(caCertificate)) + + trustManager.verify(listOf(dsValidInThePastCertificate)).let { + assertEquals("Certificate is no longer valid", it.error?.message) + assertFalse(it.isTrusted) + assertEquals(3, it.trustChain.size) + assertEquals(caCertificate, it.trustChain.last()) + } + } + + @Test + fun testTrustManagerValidInTheFuture() { + val trustManager = TrustManager() + + trustManager.addTrustPoint(TrustPoint(intermediateCertificate)) + trustManager.addTrustPoint(TrustPoint(caCertificate)) + + trustManager.verify(listOf(dsValidInTheFutureCertificate)).let { + assertEquals("Certificate is not yet valid", it.error?.message) + assertFalse(it.isTrusted) + assertEquals(3, it.trustChain.size) + assertEquals(caCertificate, it.trustChain.last()) + } + } + + @Test + fun testTrustManagerHappyFlowWithOnlyIntermediateCertifcate() { + val trustManager = TrustManager() + + trustManager.addTrustPoint(TrustPoint(intermediateCertificate)) + + trustManager.verify(listOf(dsCertificate)).let { + assertEquals(null, it.error) + assertTrue(it.isTrusted) + assertEquals(2, it.trustChain.size) + assertEquals(intermediateCertificate, it.trustChain.last()) + } + } + + @Test + fun testTrustManagerHappyFlowWithChainOfTwo() { + val trustManager = TrustManager() + + trustManager.addTrustPoint(TrustPoint(caCertificate)) + + trustManager.verify(listOf(dsCertificate, intermediateCertificate)).let { + assertEquals(null, it.error) + assertTrue(it.isTrusted) + assertEquals(3, it.trustChain.size) + assertEquals(caCertificate, it.trustChain.last()) + } + } + + @Test + fun testTrustManagerTrustPointNotCaCert() { + val trustManager = TrustManager() + + trustManager.addTrustPoint(TrustPoint(dsCertificate)) + + trustManager.verify(listOf(dsCertificate)).let { + assertEquals(null, it.error) + assertTrue(it.isTrusted) + assertEquals(1, it.trustChain.size) + assertEquals(dsCertificate, it.trustChain.last()) + } + } + + @Test + fun testTrustManagerHappyFlowMultipleCerts() { + val trustManager = TrustManager() + + trustManager.addTrustPoint(TrustPoint(intermediateCertificate)) + trustManager.addTrustPoint(TrustPoint(caCertificate)) + trustManager.addTrustPoint(TrustPoint(ca2Certificate)) + + trustManager.verify(listOf(dsCertificate)).let { + assertEquals(null, it.error) + assertTrue(it.isTrusted) + assertEquals(3, it.trustChain.size) + assertEquals(caCertificate, it.trustChain.last()) + } + + trustManager.verify(listOf(ds2Certificate)).let { + assertEquals(null, it.error) + assertTrue(it.isTrusted) + assertEquals(2, it.trustChain.size) + assertEquals(ca2Certificate, it.trustChain.last()) + } + } + + @Test + fun testTrustManagerNoTrustPoints() { + val trustManager = TrustManager() + + trustManager.verify(listOf(dsCertificate)).let { + assertEquals("No trusted root certificate could not be found", it.error?.message) + assertFalse(it.isTrusted) + assertEquals(0, it.trustChain.size) + } + } +} \ No newline at end of file diff --git a/identity/src/iosMain/kotlin/com/android/identity/crypto/CryptoIos.kt b/identity/src/iosMain/kotlin/com/android/identity/crypto/CryptoIos.kt index 4b33991ea..14de9c8b1 100644 --- a/identity/src/iosMain/kotlin/com/android/identity/crypto/CryptoIos.kt +++ b/identity/src/iosMain/kotlin/com/android/identity/crypto/CryptoIos.kt @@ -36,6 +36,7 @@ actual object Crypto { message: ByteArray ): ByteArray { return when (algorithm) { + Algorithm.INSECURE_SHA1 -> SwiftBridge.sha1(message.toNSData()).toByteArray() Algorithm.SHA256 -> SwiftBridge.sha256(message.toNSData()).toByteArray() Algorithm.SHA384 -> SwiftBridge.sha384(message.toNSData()).toByteArray() Algorithm.SHA512 -> SwiftBridge.sha512(message.toNSData()).toByteArray() @@ -321,4 +322,6 @@ actual object Crypto { keyUnlockData?.authenticationContext as objcnames.classes.LAContext? )?.toByteArray() ?: throw KeyLockedException("Unable to unlock key") } + + internal actual fun validateCertChain(certChain: X509CertChain): Boolean = TODO() } diff --git a/identity/src/iosMain/kotlin/com/android/identity/crypto/X509CertIos.kt b/identity/src/iosMain/kotlin/com/android/identity/crypto/X509CertIos.kt deleted file mode 100644 index dd2d54bc0..000000000 --- a/identity/src/iosMain/kotlin/com/android/identity/crypto/X509CertIos.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.android.identity.crypto - -import com.android.identity.cbor.Bstr -import com.android.identity.cbor.DataItem -import com.android.identity.SwiftBridge -import com.android.identity.util.toByteArray -import com.android.identity.util.toNSData -import kotlinx.cinterop.ExperimentalForeignApi -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - -@OptIn(ExperimentalForeignApi::class) -actual class X509Cert actual constructor(actual val encodedCertificate: ByteArray) { - - actual fun toDataItem(): DataItem = Bstr(encodedCertificate) - - @OptIn(ExperimentalEncodingApi::class) - actual fun toPem(): String { - val sb = StringBuilder() - sb.append("-----BEGIN CERTIFICATE-----\n") - sb.append(Base64.Mime.encode(encodedCertificate)) - sb.append("\n-----END CERTIFICATE-----\n") - return sb.toString() - } - - actual val ecPublicKey: EcPublicKey - get() { - val keyData = SwiftBridge.x509CertGetKey(encodedCertificate.toNSData()) - if (keyData == null) { - throw IllegalStateException("Error getting key") - } - /* From docs for SecKeyCopyExternalRepresentation() - * - * The method returns data in the PKCS #1 format for an RSA key. For an elliptic curve - * public key, the format follows the ANSI X9.63 standard using a byte string of 04 || X - * || Y. For an elliptic curve private key, the output is formatted as the public key - * concatenated with the big endian encoding of the secret scalar, or 04 || X || Y || K. - * All of these representations use constant size integers, including leading zeros - * as needed. - */ - val data = keyData.toByteArray() - val componentSize = (data.size - 1)/2 - val x = data.sliceArray(IntRange(1, componentSize)) - val y = data.sliceArray(IntRange(componentSize + 1, data.size - 1)) - - val ecCurve = when (componentSize) { - 32 -> EcCurve.P256 - 48 -> EcCurve.P384 - 65 -> EcCurve.P521 - else -> throw IllegalStateException("Unsupported component size ${componentSize}") - } - return EcPublicKeyDoubleCoordinate(ecCurve, x, y) - } - - actual companion object { - @OptIn(ExperimentalEncodingApi::class) - actual fun fromPem(pemEncoding: String): X509Cert { - val encoded = Base64.Mime.decode(pemEncoding - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .trim()) - return X509Cert(encoded) - } - - actual fun fromDataItem(dataItem: DataItem): X509Cert { - return X509Cert(dataItem.asBstr) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is X509Cert) return false - - return encodedCertificate.contentEquals(other.encodedCertificate) - } - - override fun hashCode(): Int { - return encodedCertificate.contentHashCode() - } -} diff --git a/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/CryptoJvm.kt b/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/CryptoJvm.kt index f5b094e1d..8caa8d900 100644 --- a/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/CryptoJvm.kt +++ b/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/CryptoJvm.kt @@ -22,10 +22,6 @@ import com.google.crypto.tink.proto.OutputPrefixType import com.google.crypto.tink.shaded.protobuf.ByteString import com.google.crypto.tink.subtle.EllipticCurves import kotlinx.io.bytestring.ByteStringBuilder -import org.bouncycastle.asn1.ASN1InputStream -import org.bouncycastle.asn1.ASN1Integer -import org.bouncycastle.asn1.ASN1Sequence -import org.bouncycastle.asn1.DERSequenceGenerator import org.bouncycastle.crypto.agreement.X25519Agreement import org.bouncycastle.crypto.agreement.X448Agreement import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator @@ -51,10 +47,6 @@ import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.spec.ECPrivateKeySpec import org.bouncycastle.util.BigIntegers -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.math.BigInteger import java.security.KeyFactory import java.security.KeyPairGenerator import java.security.MessageDigest @@ -107,7 +99,7 @@ actual object Crypto { /** * Message digest function. * - * @param algorithm must one of [Algorithm.SHA256], [Algorithm.SHA384], [Algorithm.SHA512]. + * @param algorithm must one of [Algorithm.INSECURE_SHA1], [Algorithm.SHA256], [Algorithm.SHA384], [Algorithm.SHA512]. * @param message the message to get a digest of. * @return the digest. * @throws IllegalArgumentException if the given algorithm is not supported. @@ -117,6 +109,7 @@ actual object Crypto { message: ByteArray ): ByteArray { val algName = when (algorithm) { + Algorithm.INSECURE_SHA1 -> "SHA-1" Algorithm.SHA256 -> "SHA-256" Algorithm.SHA384 -> "SHA-384" Algorithm.SHA512 -> "SHA-512" @@ -297,9 +290,6 @@ actual object Crypto { /** * Checks signature validity. * - * The signature must be DER encoded except for curve Ed25519 and Ed448 where it - * should just be the raw R and S values. - * * @param publicKey the public key the signature was made with. * @param message the data that was signed. * @param algorithm the signature algorithm to use. @@ -336,7 +326,7 @@ actual object Crypto { signature.r + signature.s } else -> { - signature.toDer() + signature.toDerEncoded() } } Signature.getInstance(signatureAlgorithm).run { @@ -463,7 +453,7 @@ actual object Crypto { update(message) sign() } - EcSignature.fromDer(key.curve, derEncodedSignature) + EcSignature.fromDerEncoded(key.curve.bitSize, derEncodedSignature) } catch (e: Exception) { throw IllegalStateException("Unexpected Exception", e) } @@ -794,4 +784,21 @@ actual object Crypto { internal actual fun uuidGetRandom(): UUID { return UUID.fromJavaUuid(java.util.UUID.randomUUID()) } + + internal actual fun validateCertChain(certChain: X509CertChain): Boolean { + val javaCerts = certChain.javaX509Certificates + for (n in javaCerts.indices) { + if (n < javaCerts.size - 1) { + val cert = javaCerts[n] + val certSignedBy = javaCerts[n + 1] + try { + cert.verify(certSignedBy.publicKey) + } catch (_: Throwable) { + return false + } + } + } + return true + } + } diff --git a/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/EcSignatureJvm.kt b/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/EcSignatureJvm.kt deleted file mode 100644 index ab3712d04..000000000 --- a/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/EcSignatureJvm.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.android.identity.crypto - -import org.bouncycastle.asn1.ASN1InputStream -import org.bouncycastle.asn1.ASN1Integer -import org.bouncycastle.asn1.ASN1Sequence -import org.bouncycastle.asn1.DERSequenceGenerator -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.math.BigInteger - -fun EcSignature.toDer(): ByteArray { - // r and s are always positive and may use all bits so use the constructor which - // parses them as unsigned. - val rBigInt = BigInteger(1, r) - val sBigInt = BigInteger(1, s) - val baos = ByteArrayOutputStream() - try { - DERSequenceGenerator(baos).apply { - addObject(ASN1Integer(rBigInt.toByteArray())) - addObject(ASN1Integer(sBigInt.toByteArray())) - close() - } - } catch (e: IOException) { - throw IllegalStateException("Error generating DER signature", e) - } - return baos.toByteArray() -} - -fun EcSignature.Companion.fromDer(curve: EcCurve, derEncodedSignature: ByteArray): EcSignature { - val asn1 = try { - ASN1InputStream(ByteArrayInputStream(derEncodedSignature)).readObject() - } catch (e: IOException) { - throw IllegalArgumentException("Error decoding DER signature", e) - } - val asn1Encodables = (asn1 as ASN1Sequence).toArray() - require(asn1Encodables.size == 2) { "Expected two items in sequence" } - val r = stripLeadingZeroes(((asn1Encodables[0].toASN1Primitive() as ASN1Integer).value).toByteArray()) - val s = stripLeadingZeroes(((asn1Encodables[1].toASN1Primitive() as ASN1Integer).value).toByteArray()) - - val keySize = (curve.bitSize + 7)/8 - check(r.size <= keySize) - check(s.size <= keySize) - - val rPadded = ByteArray(keySize) - val sPadded = ByteArray(keySize) - r.copyInto(rPadded, keySize - r.size) - s.copyInto(sPadded, keySize - s.size) - - check(rPadded.size == keySize) - check(sPadded.size == keySize) - - return EcSignature(rPadded, sPadded) -} - -private fun stripLeadingZeroes(array: ByteArray): ByteArray { - val idx = array.indexOfFirst { it != 0.toByte() } - if (idx == -1) - return array - return array.copyOfRange(idx, array.size) -} - diff --git a/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/X509CertJvm.kt b/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/X509CertJvm.kt index dbf839d20..fcbf94c58 100644 --- a/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/X509CertJvm.kt +++ b/identity/src/javaSharedMain/kotlin/com/android/identity/crypto/X509CertJvm.kt @@ -1,144 +1,9 @@ package com.android.identity.crypto -import com.android.identity.cbor.Bstr -import com.android.identity.cbor.DataItem -import kotlinx.datetime.Instant -import org.bouncycastle.asn1.ASN1InputStream -import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.ASN1OctetString -import org.bouncycastle.asn1.ASN1Sequence -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.SubjectKeyIdentifier -import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import java.io.ByteArrayInputStream -import java.math.BigInteger import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate -import java.util.Date -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - -actual class X509Cert actual constructor(actual val encodedCertificate: ByteArray) { - - actual fun toDataItem(): DataItem = Bstr(encodedCertificate) - - @OptIn(ExperimentalEncodingApi::class) - actual fun toPem(): String { - val sb = StringBuilder() - sb.append("-----BEGIN CERTIFICATE-----\n") - sb.append(Base64.Mime.encode(encodedCertificate)) - sb.append("\n-----END CERTIFICATE-----\n") - return sb.toString() } - - fun verify(signingCertificate: X509Cert): Boolean = - try { - this.javaX509Certificate.verify(signingCertificate.javaX509Certificate.publicKey) - true - } catch (e: Throwable) { - false - } - - private fun getCurve(tbsCertificate: ByteArray): EcCurve { - // We need to look in SubjectPublicKeyInfo for the curve... - // - // TBSCertificate ::= SEQUENCE { - // version [0] EXPLICIT Version DEFAULT v1, - // serialNumber CertificateSerialNumber, - // signature AlgorithmIdentifier, - // issuer Name, - // validity Validity, - // subject Name, - // subjectPublicKeyInfo SubjectPublicKeyInfo, - // issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, - // -- If present, version MUST be v2 or v3 - // subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, - // -- If present, version MUST be v2 or v3 - // extensions [3] EXPLICIT Extensions OPTIONAL - // -- If present, version MUST be v3 - // } - // - // where - // - // SubjectPublicKeyInfo ::= SEQUENCE { - // algorithm AlgorithmIdentifier, - // subjectPublicKey BIT STRING } - // - // AlgorithmIdentifier ::= SEQUENCE { - // algorithm OBJECT IDENTIFIER, - // parameters ANY DEFINED BY algorithm OPTIONAL } - // - // This is all from https://datatracker.ietf.org/doc/html/rfc5280 - // - - val input = ASN1InputStream(tbsCertificate) - val seq = ASN1Sequence.getInstance(input.readObject()) - val subjectPublicKeyInfo = seq.getObjectAt(6) as ASN1Sequence - val algorithmIdentifier = subjectPublicKeyInfo.getObjectAt(0) as ASN1Sequence - val algorithmOidString = (algorithmIdentifier.getObjectAt(0) as ASN1ObjectIdentifier).id - val curve = - when (algorithmOidString) { - // https://datatracker.ietf.org/doc/html/rfc5480#section-2.1.1 - "1.2.840.10045.2.1" -> { - val ecCurveString = (algorithmIdentifier.getObjectAt(1) as - ASN1ObjectIdentifier).id - when (ecCurveString) { - "1.2.840.10045.3.1.7" -> EcCurve.P256 - "1.3.132.0.34" -> EcCurve.P384 - "1.3.132.0.35" -> EcCurve.P521 - "1.3.36.3.3.2.8.1.1.7" -> EcCurve.BRAINPOOLP256R1 - "1.3.36.3.3.2.8.1.1.9" -> EcCurve.BRAINPOOLP320R1 - "1.3.36.3.3.2.8.1.1.11" -> EcCurve.BRAINPOOLP384R1 - "1.3.36.3.3.2.8.1.1.13" -> EcCurve.BRAINPOOLP512R1 - else -> throw IllegalStateException("Unexpected curve OID $ecCurveString") - } - } - - "1.3.101.110" -> EcCurve.X25519 - "1.3.101.111" -> EcCurve.X448 - "1.3.101.112" -> EcCurve.ED25519 - "1.3.101.113" -> EcCurve.ED448 - else -> throw IllegalStateException("Unexpected OID $algorithmOidString") - } - return curve - } - - actual val ecPublicKey: EcPublicKey - get() { - val curve = getCurve(javaX509Certificate.tbsCertificate) - return javaX509Certificate.publicKey.toEcPublicKey(curve) - } - - actual companion object { - @OptIn(ExperimentalEncodingApi::class) - actual fun fromPem(pemEncoding: String): X509Cert { - val encoded = Base64.Mime.decode(pemEncoding - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .trim()) - return X509Cert(encoded) - } - - actual fun fromDataItem(dataItem: DataItem): X509Cert { - return X509Cert(dataItem.asBstr) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is X509Cert) return false - - return encodedCertificate.contentEquals(other.encodedCertificate) - } - - override fun hashCode(): Int { - return encodedCertificate.contentHashCode() - } -} /** * The Java X509 certificate from the encoded certificate data. @@ -153,138 +18,3 @@ val X509Cert.javaX509Certificate: X509Certificate throw IllegalStateException("Error decoding certificate blob", e) } } - -/** - * Options for [Crypto.createX509v3Certificate] function. - */ -enum class X509CertificateCreateOption { - /** - * Include the Subject Key Identifier extension as per RFC 5280 section 4.2.1.2. - * - * The extension will be marked as non-critical. - */ - INCLUDE_SUBJECT_KEY_IDENTIFIER, - - /** - * Set the Authority Key Identifier with keyIdentifier set to the same value as the - * Subject Key Identifier. - * - * This option is only meaningful when creating a self-signed certificate. - * - * The extension will be marked as non-critical. - */ - INCLUDE_AUTHORITY_KEY_IDENTIFIER_AS_SUBJECT_KEY_IDENTIFIER, - - /** - * Set the Authority Key Identifier with keyIdentifier set to the same value as the - * Subject Key Identifier in the given `sigingKeyCertificate`. - */ - INCLUDE_AUTHORITY_KEY_IDENTIFIER_FROM_SIGNING_KEY_CERTIFICATE, -} - -/** - * Data for a X509 extension - * - * @param oid the OID of the extension. - * @param isCritical criticality. - * @param payload the payload of the extension. - */ -data class X509CertificateExtension( - val oid: String, - val isCritical: Boolean, - val payload: ByteArray -) - -/** - * Creates a certificate for a public key. - * - * @param publicKey the key to create a certificate for. - * @param signingKey the key to sign the certificate. - * @param signingKeyCertificate the certificate for the signing key, if available. - * @param signatureAlgorithm the signature algorithm to use. - * @param serial the serial, must be a number. - * @param subject the subject string. - * @param issuer the issuer string. - * @param validFrom when the certificate should be valid from. - * @param validUntil when the certificate should be valid until. - * @param options one or more options from the [CreateCertificateOption] enumeration. - * @param additionalExtensions additional extensions to put into the certificate. - */ -fun X509Cert.Companion.create(publicKey: EcPublicKey, - signingKey: EcPrivateKey, - signingKeyCertificate: X509Cert?, - signatureAlgorithm: Algorithm, - serial: String, - subject: String, - issuer: String, - validFrom: Instant, - validUntil: Instant, - options: Set, - additionalExtensions: List): X509Cert { - val signatureAlgorithmString = when (signatureAlgorithm) { - Algorithm.ES256 -> "SHA256withECDSA" - Algorithm.ES384 -> "SHA384withECDSA" - Algorithm.ES512 -> "SHA512withECDSA" - Algorithm.EDDSA -> { - when (signingKey.curve) { - EcCurve.ED25519 -> "Ed25519" - EcCurve.ED448 -> "Ed448" - else -> throw IllegalArgumentException( - "ALGORITHM_EDDSA can only be used with Ed25519 and Ed448" - ) - } - } - - else -> throw IllegalArgumentException("Algorithm cannot be used for signing") - } - val certSigningKeyJava = signingKey.javaPrivateKey - - val publicKeyJava = publicKey.javaPublicKey - val certBuilder = JcaX509v3CertificateBuilder( - X500Name(issuer), - BigInteger(serial), - Date(validFrom.toEpochMilliseconds()), - Date(validUntil.toEpochMilliseconds()), - X500Name(subject), - publicKeyJava - ) - if (options.contains(X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER)) { - certBuilder.addExtension( - Extension.subjectKeyIdentifier, - false, - JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKeyJava) - ) - } - if (options.contains(X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_AS_SUBJECT_KEY_IDENTIFIER)) { - certBuilder.addExtension( - Extension.authorityKeyIdentifier, - false, - JcaX509ExtensionUtils().createAuthorityKeyIdentifier(publicKeyJava) - ) - } - if (options.contains( - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_FROM_SIGNING_KEY_CERTIFICATE)) { - check(signingKeyCertificate != null) - val signerCert = signingKeyCertificate.javaX509Certificate - val encoded = signerCert.getExtensionValue(Extension.subjectKeyIdentifier.toString()) - val subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(ASN1OctetString.getInstance(encoded).octets) - certBuilder.addExtension( - Extension.authorityKeyIdentifier, - false, - AuthorityKeyIdentifier(subjectKeyIdentifier.keyIdentifier) - ) - } - additionalExtensions.forEach { extension -> - certBuilder.addExtension( - ASN1ObjectIdentifier(extension.oid), - extension.isCritical, - extension.payload - ) - } - val signer = JcaContentSignerBuilder(signatureAlgorithmString).build(certSigningKeyJava) - val encodedCert: ByteArray = certBuilder.build(signer).getEncoded() - val cf = CertificateFactory.getInstance("X.509") - val bais = ByteArrayInputStream(encodedCert) - val x509cert = cf.generateCertificate(bais) as X509Certificate - return X509Cert(x509cert.encoded) -} \ No newline at end of file diff --git a/identity/src/javaSharedMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt b/identity/src/javaSharedMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt deleted file mode 100644 index 3585f64c2..000000000 --- a/identity/src/javaSharedMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * 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 com.android.identity.trustmanagement - -import com.android.identity.util.toHex -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.SubjectKeyIdentifier -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.io.ByteArrayInputStream -import java.security.InvalidKeyException -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.PKIXCertPathChecker -import java.security.cert.X509Certificate - -/** - * Object with utility functions for the TrustManager. - */ -internal object TrustManagerUtil { - - private const val DIGITAL_SIGNATURE = 0 - private const val KEY_CERT_SIGN = 5 - - /** - * Get the Subject Key Identifier Extension from the X509 certificate - * in hexadecimal format. - */ - fun getSubjectKeyIdentifier(certificate: X509Certificate): String { - val extensionValue = certificate.getExtensionValue(Extension.subjectKeyIdentifier.id) - ?: return "" - val octets = DEROctetString.getInstance(extensionValue).octets - val subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets) - return subjectKeyIdentifier.keyIdentifier.toHex() - } - - /** - * Get the Authority Key Identifier Extension from the X509 certificate - * in hexadecimal format. - */ - fun getAuthorityKeyIdentifier(certificate: X509Certificate): String { - val extensionValue = certificate.getExtensionValue(Extension.authorityKeyIdentifier.id) - ?: return "" - val octets = DEROctetString.getInstance(extensionValue).octets - val authorityKeyIdentifier = AuthorityKeyIdentifier.getInstance(octets) - return authorityKeyIdentifier.keyIdentifier.toHex() - } - - /** - * Check whether a certificate is self-signed - */ - fun isSelfSigned(certificate: X509Certificate): Boolean = - certificate.issuerX500Principal.name == certificate.subjectX500Principal.name - - /** - * Check that the key usage is the creation of digital signatures. - */ - fun checkKeyUsageDocumentSigner(certificate: X509Certificate) { - if (!hasKeyUsage(certificate, DIGITAL_SIGNATURE)) { - throw CertificateException("Document Signer certificate is not a signing certificate") - } - } - - /** - * Check the validity period of a certificate (based on the system date). - */ - fun checkValidity(certificate: X509Certificate) { - // check if the certificate is currently valid - // NOTE does not check if it is valid within the validity period of - // the issuing CA - certificate.checkValidity() - // NOTE throws multiple exceptions derived from CertificateException - } - - /** - * Execute custom validations on a certificate. - */ - fun executeCustomValidations( - certificate: X509Certificate, - customValidations: List - ) = customValidations.map { checker -> checker.check(certificate) } - - - /** - * Check that the key usage is to sign certificates. - */ - fun checkKeyUsageCaCertificate(caCertificate: X509Certificate) { - if (!hasKeyUsage(caCertificate, KEY_CERT_SIGN)) { - throw CertificateException("CA certificate doesn't have the key usage to sign certificates") - } - } - - /** - * Check that the issuer in [certificate] is equal to the subject in - * [caCertificate]. - */ - fun checkCaIsIssuer(certificate: X509Certificate, caCertificate: X509Certificate) { - val issuerName = X500Name(certificate.issuerX500Principal.name) - val nameCA = X500Name(caCertificate.subjectX500Principal.name) - if (issuerName != nameCA) { - throw CertificateException("CA certificate '$nameCA' isn't the issuer of the certificate before it. It should be '$issuerName'") - } - } - - /** - * Verify the signature of the [certificate] with the public key of the - * [caCertificate]. - */ - fun verifySignature(certificate: X509Certificate, caCertificate: X509Certificate) = - try { - try { - certificate.verify(caCertificate.publicKey) - } catch (e: InvalidKeyException) { - verifySignatureBouncyCastle(certificate, caCertificate) - } - } catch (e: Exception) { - throw CertificateException( - "Certificate '${ - certificate.subjectX500Principal.name - }' could not be verified with the public key of CA certificate '${caCertificate.subjectX500Principal.name}'" - ) - } - - /** - * If it is technically not possible to verify the signature, - * try BouncyCastle... - */ - private fun verifySignatureBouncyCastle( - certificate: X509Certificate, - caCertificate: X509Certificate - ) { - // Try to decode certificate using BouncyCastleProvider. - val factory = CertificateFactory.getInstance("X509", BouncyCastleProvider()) - val certificateBouncyCastle = factory.generateCertificate( - ByteArrayInputStream(certificate.encoded) - ) as X509Certificate - val caCertificateBouncyCastle = factory.generateCertificate( - ByteArrayInputStream(caCertificate.encoded) - ) as X509Certificate - certificateBouncyCastle.verify(caCertificateBouncyCastle.publicKey) - } - - /** - * Determine whether the certificate has certain key usage. - */ - private fun hasKeyUsage(certificate: X509Certificate, keyUsage: Int): Boolean = - certificate.keyUsage?.let { it[keyUsage] } ?: false -} \ No newline at end of file diff --git a/identity/src/jvmTest/kotlin/com/android/identity/asn1/ASN1TestsJvm.kt b/identity/src/jvmTest/kotlin/com/android/identity/asn1/ASN1TestsJvm.kt new file mode 100644 index 000000000..1997058ba --- /dev/null +++ b/identity/src/jvmTest/kotlin/com/android/identity/asn1/ASN1TestsJvm.kt @@ -0,0 +1,24 @@ +package com.android.identity.asn1 + +import com.android.identity.util.toHex +import java.math.BigInteger +import kotlin.test.Test +import kotlin.test.assertEquals + +class ASN1TestsJvm { + + // Checks that DER encoding routines for Longs behave as expected, that is, that they + // are encoded the same way as Java's BigInteger. + // + @Test + fun testLongEncodeDecode() { + for (n in listOf(0, 1, 2, 0xff, 0x100, 0x101, 0xffff, 0x10000, 0x10001) + + listOf(-1, -2, -0xff, -0x100, -0x101, -0xffff, -0x10000, -0x10001)) { + val bi = BigInteger.valueOf(n.toLong()) + val encoded = n.toLong().derEncodeToByteArray() + assertEquals(bi.toByteArray().toHex(), encoded.toHex()) + val decodedN = encoded.derDecodeAsLong() + assertEquals(n.toLong(), decodedN) + } + } +} \ No newline at end of file diff --git a/identity/src/jvmTest/kotlin/com/android/identity/crypto/X509CertTestsJvm.kt b/identity/src/jvmTest/kotlin/com/android/identity/crypto/X509CertTestsJvm.kt index 73d3600ba..bfdd32eab 100644 --- a/identity/src/jvmTest/kotlin/com/android/identity/crypto/X509CertTestsJvm.kt +++ b/identity/src/jvmTest/kotlin/com/android/identity/crypto/X509CertTestsJvm.kt @@ -1,8 +1,13 @@ package com.android.identity.crypto -import com.android.identity.util.fromHex +import com.android.identity.asn1.ASN1 +import com.android.identity.asn1.ASN1Integer +import com.android.identity.asn1.ASN1OctetString import com.android.identity.util.toHex -import org.bouncycastle.asn1.x500.X500Name +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.toKotlinInstant +import org.bouncycastle.asn1.x500.X500Name as bcX500Name import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.jcajce.spec.XDHParameterSpec import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -16,10 +21,12 @@ import java.util.Date import java.util.concurrent.TimeUnit import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals -import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.hours -// TODO: move to commonTest once iOS implementations are ready. class X509CertTestsJvm { @BeforeTest @@ -27,104 +34,93 @@ class X509CertTestsJvm { Security.insertProviderAt(BouncyCastleProvider(), 1) } - private fun createKeyPair(curve: EcCurve): KeyPair { - val stdName = when (curve) { - EcCurve.P256 -> "secp256r1" - EcCurve.P384 -> "secp384r1" - EcCurve.P521 -> "secp521r1" - EcCurve.BRAINPOOLP256R1 -> "brainpoolP256r1" - EcCurve.BRAINPOOLP320R1 -> "brainpoolP320r1" - EcCurve.BRAINPOOLP384R1 -> "brainpoolP384r1" - EcCurve.BRAINPOOLP512R1 -> "brainpoolP512r1" - EcCurve.X25519 -> "X25519" - EcCurve.ED25519 -> "Ed25519" - EcCurve.X448 -> "X448" - EcCurve.ED448 -> "Ed448" - } - return try { - val kpg: KeyPairGenerator - if (stdName == "X25519") { - kpg = KeyPairGenerator.getInstance( - "X25519", - BouncyCastleProvider.PROVIDER_NAME - ) - kpg.initialize(XDHParameterSpec(XDHParameterSpec.X25519)) - } else if (stdName == "Ed25519") { - kpg = KeyPairGenerator.getInstance( - "Ed25519", - BouncyCastleProvider.PROVIDER_NAME - ) - } else if (stdName == "X448") { - kpg = KeyPairGenerator.getInstance( - "X448", - BouncyCastleProvider.PROVIDER_NAME - ) - kpg.initialize(XDHParameterSpec(XDHParameterSpec.X448)) - } else if (stdName == "Ed448") { - kpg = KeyPairGenerator.getInstance( - "Ed448", - BouncyCastleProvider.PROVIDER_NAME - ) - } else { - kpg = KeyPairGenerator.getInstance( - "EC", - BouncyCastleProvider.PROVIDER_NAME - ) - kpg.initialize(ECGenParameterSpec(stdName)) - } - kpg.generateKeyPair() - } catch (e: Exception) { - throw IllegalStateException("Error generating ephemeral key-pair", e) - } - } + // This is Maryland's IACA certificate (IACA_Root_2024.cer) downloaded from + // + // https://mva.maryland.gov/Pages/MDMobileID_Googlewallet.aspx + // + private val exampleX509Cert = X509Cert.fromPem( + """ +-----BEGIN CERTIFICATE----- +MIICxjCCAmygAwIBAgITJkV7El8K11IXqY7mz96n/EhiITAKBggqhkjOPQQDAjBq +MQ4wDAYDVQQIEwVVUy1NRDELMAkGA1UEBhMCVVMxFDASBgNVBAcTC0dsZW4gQnVy +bmllMRUwEwYDVQQKEwxNYXJ5bGFuZCBNVkExHjAcBgNVBAMTFUZhc3QgRW50ZXJw +cmlzZXMgUm9vdDAeFw0yNDAxMDUwNTAwMDBaFw0yOTAxMDQwNTAwMDBaMGoxDjAM +BgNVBAgTBVVTLU1EMQswCQYDVQQGEwJVUzEUMBIGA1UEBxMLR2xlbiBCdXJuaWUx +FTATBgNVBAoTDE1hcnlsYW5kIE1WQTEeMBwGA1UEAxMVRmFzdCBFbnRlcnByaXNl +cyBSb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaWcKIqlAWboV93RAa5ad +0LJBn8W0/yYwtOyUlxuTxoo4SPkorKmOz3EhThC+U4WRrt13aSnCsJtK+waBFghX +u6OB8DCB7TAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV +HQ4EFgQUTprRzaFBJ1SLjJsO01tlLCQ4YF0wPAYDVR0SBDUwM4EWbXZhY3NAbWRv +dC5zdGF0ZS5tZC51c4YZaHR0cHM6Ly9tdmEubWFyeWxhbmQuZ292LzBYBgNVHR8E +UTBPME2gS6BJhkdodHRwczovL215bXZhLm1hcnlsYW5kLmdvdjo1NDQzL01EUC9X +ZWJTZXJ2aWNlcy9DUkwvbURML3Jldm9jYXRpb25zLmNybDAQBgkrBgEEAYPFIQEE +A01EUDAKBggqhkjOPQQDAgNIADBFAiEAnX3+E4E5dQ+5G1rmStJTW79ZAiDTabyL +8lJuYL/nDxMCIHHkAyIJcQlQmKDUVkBr3heUd5N9Y8GWdbWnbHuwe7Om +-----END CERTIFICATE----- + """.trimIndent()) + + @Test + fun testX509Parsing() { + val cert = exampleX509Cert + val javaCert = exampleX509Cert.javaX509Certificate - private fun testCurve(curve: EcCurve) { - // Generate an X.509 certificate for all supported curves and check we correctly - // retrieve the curve information from the resulting certificate. - val attestationKey = createKeyPair(EcCurve.P384) - val attestationKeySignatureAlgorithm = "SHA384withECDSA" - val keyPair = createKeyPair(curve) + // This checks that out own ASN.1 / X.509 routines agree with the ones in Java. + // - var issuer = X500Name("CN=testIssuer") - var subject = X500Name("CN=testSubject") - var validFrom = Date() - var validUntil = Date(Date().time + TimeUnit.MILLISECONDS.convert(365, TimeUnit.DAYS)) - val serial = BigInteger.ONE - val certBuilder = JcaX509v3CertificateBuilder( - issuer, - serial, - validFrom, - validUntil, - subject, - keyPair.public - ) - val signer = JcaContentSignerBuilder(attestationKeySignatureAlgorithm) - .build(attestationKey.private) - val encodedX509Cert = certBuilder.build(signer).encoded + assertEquals(cert.subject.name, javaCert.subjectX500Principal.name) + assertEquals(cert.issuer.name, javaCert.issuerX500Principal.name) + assertEquals(cert.version, javaCert.version - 1) + assertEquals(cert.serialNumber, ASN1Integer(javaCert.serialNumber.toByteArray())) + assertEquals(cert.validityNotBefore, javaCert.notBefore.toInstant().toKotlinInstant()) + assertEquals(cert.validityNotAfter, javaCert.notAfter.toInstant().toKotlinInstant()) - // Checks the decoded curve is correct. - val cert = X509Cert(encodedX509Cert) - val publicKey = cert.ecPublicKey - assertEquals(curve, publicKey.curve) + assertContentEquals(cert.tbsCertificate, javaCert.tbsCertificate) + assertContentEquals(cert.signature, javaCert.signature) + assertEquals(cert.signatureAlgorithm, Algorithm.ES256) - // Checks the key material is correct. - assertEquals(publicKey.javaPublicKey, keyPair.public) + assertEquals(cert.criticalExtensionOIDs, javaCert.criticalExtensionOIDs) + assertEquals(cert.nonCriticalExtensionOIDs, javaCert.nonCriticalExtensionOIDs) - val pemEncoded = cert.toPem() - val cert2 = X509Cert.fromPem(pemEncoded) - assertEquals(cert2, cert) - assertEquals(cert2.javaX509Certificate, cert.javaX509Certificate) + for (oid in cert.criticalExtensionOIDs + cert.nonCriticalExtensionOIDs) { + // Note: Java API always return the value wrapped in an OCTET STRING. So we need to unwrap it. + val ourValue = cert.getExtensionValue(oid) + val javaValue = javaCert.getExtensionValue(oid) + val javaValueUnwrapped = (ASN1.decode(javaValue) as ASN1OctetString).value + assertContentEquals(ourValue, javaValueUnwrapped) + } + // Check non-existent extensions (1.2.3.4.5 as an example) return null + assertNull(cert.getExtensionValue("1.2.3.4.5")) + assertNull(javaCert.getExtensionValue("1.2.3.4.5")) + assertEquals("4e9ad1cda14127548b8c9b0ed35b652c2438605d", cert.subjectKeyIdentifier!!.toHex()) + } + + // Checks that the Java X509Certificate.verify() works with certificates created by X509Cert.Builder + private fun testJavaCertSignedWithCurve(curve: EcCurve) { + val key = Crypto.createEcPrivateKey(curve) + val now = Instant.fromEpochSeconds(Clock.System.now().epochSeconds) + val cert = X509Cert.Builder( + publicKey = key.publicKey, + signingKey = key, + signatureAlgorithm = key.curve.defaultSigningAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName("CN=Foobar"), + issuer = X500Name.fromName("CN=Foobar"), + validFrom = now - 1.hours, + validUntil = now + 1.hours + ).build() + println("blah_cert\n:${cert.toPem()}") + val javaCert = cert.javaX509Certificate + javaCert.verify(key.publicKey.javaPublicKey) } - @Test fun testCurve_P256() = testCurve(EcCurve.P256) - @Test fun testCurve_P384() = testCurve(EcCurve.P384) - @Test fun testCurve_P521() = testCurve(EcCurve.P521) - @Test fun testCurve_BRAINPOOLP256R1() = testCurve(EcCurve.BRAINPOOLP256R1) - @Test fun testCurve_BRAINPOOLP320R1() = testCurve(EcCurve.BRAINPOOLP320R1) - @Test fun testCurve_BRAINPOOLP384R1() = testCurve(EcCurve.BRAINPOOLP384R1) - @Test fun testCurve_BRAINPOOLP512R1() = testCurve(EcCurve.BRAINPOOLP512R1) - @Test fun testCurve_ED25519() = testCurve(EcCurve.ED25519) - @Test fun testCurve_X25519() = testCurve(EcCurve.X25519) - @Test fun testCurve_ED448() = testCurve(EcCurve.ED448) - @Test fun testCurve_X448() = testCurve(EcCurve.X448) + @Test fun testCertSignedWithCurve_P256() = testJavaCertSignedWithCurve(EcCurve.P256) + @Test fun testCertSignedWithCurve_P384() = testJavaCertSignedWithCurve(EcCurve.P384) + @Test fun testCertSignedWithCurve_P521() = testJavaCertSignedWithCurve(EcCurve.P521) + @Test fun testCertSignedWithCurve_BRAINPOOLP256R1() = testJavaCertSignedWithCurve(EcCurve.BRAINPOOLP256R1) + @Test fun testCertSignedWithCurve_BRAINPOOLP320R1() = testJavaCertSignedWithCurve(EcCurve.BRAINPOOLP320R1) + @Test fun testCertSignedWithCurve_BRAINPOOLP384R1() = testJavaCertSignedWithCurve(EcCurve.BRAINPOOLP384R1) + @Test fun testCertSignedWithCurve_BRAINPOOLP512R1() = testJavaCertSignedWithCurve(EcCurve.BRAINPOOLP512R1) + @Test fun testCertSignedWithCurve_ED25519() = testJavaCertSignedWithCurve(EcCurve.ED25519) + @Test fun testCertSignedWithCurve_ED448() = testJavaCertSignedWithCurve(EcCurve.ED448) + } \ No newline at end of file diff --git a/identity/src/jvmTest/kotlin/com/android/identity/trustmanagement/TrustManagerTest.kt b/identity/src/jvmTest/kotlin/com/android/identity/trustmanagement/TrustManagerTest.kt deleted file mode 100644 index 1130469be..000000000 --- a/identity/src/jvmTest/kotlin/com/android/identity/trustmanagement/TrustManagerTest.kt +++ /dev/null @@ -1,246 +0,0 @@ -package com.android.identity.trustmanagement - -import com.android.identity.crypto.X509Cert -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.BasicConstraints -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.KeyUsage -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo -import org.bouncycastle.cert.X509ExtensionUtils -import org.bouncycastle.cert.bc.BcX509ExtensionUtils -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder -import org.bouncycastle.crypto.util.PublicKeyFactory -import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import org.junit.Assert -import org.junit.Test -import java.io.ByteArrayInputStream -import java.math.BigInteger -import java.security.KeyPairGenerator -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.security.spec.ECGenParameterSpec -import java.util.Date - - -class TrustManagerTest { - // Generated by first generating a new mdlCaCertificatePem (see below), then: - // $ ./gradlew --quiet runIdentityCtl --args \ - // "generateDs --iaca_certificate iaca_certificate.pem \ - // --iaca_private_key iaca_private_key.pem --validity_in_years 10" - // Then copy the contents of the identityctl/ds_certificate.pem file here. - val mdlDsCertificatePem = """ - -----BEGIN CERTIFICATE----- - MIICojCCAiegAwIBAgIQFHtQEncyjom+wHkUPyfqLDAKBggqhkjOPQQDAjA5MQswCQYDVQQGEwJa - WjEqMCgGA1UEAwwhT1dGIElkZW50aXR5IENyZWRlbnRpYWwgVEVTVCBJQUNBMB4XDTI0MTAyOTIx - MDg0MloXDTM0MTAyOTIxMDg0MlowNzEoMCYGA1UEAwwfT1dGIElkZW50aXR5IENyZWRlbnRpYWwg - VEVTVCBEUzELMAkGA1UEBhMCWlowWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASaYTJ+iguH3Pp1 - wY8bATG5J+7C1I8U6AqN0rKgBeT7RpY28EaC2HrmTV4Boidefc+C5J2hkDTjch9/pqZuRP+uo4IB - ETCCAQ0wHQYDVR0OBBYEFKF8D5k9cD+GezyhAik3HOCTlyXoMB8GA1UdIwQYMBaAFEJo12Nqz57b - TrgLa1Iw8YI7gGPjMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMFQGA1Ud - HwRNBEswSTBHoEWgQ4ZBaHR0cHM6Ly9naXRodWIuY29tL29wZW53YWxsZXQtZm91bmRhdGlvbi1s - YWJzL2lkZW50aXR5LWNyZWRlbnRpYWwwTgYDVR0SBEcERTBDhkFodHRwczovL2dpdGh1Yi5jb20v - b3BlbndhbGxldC1mb3VuZGF0aW9uLWxhYnMvaWRlbnRpdHktY3JlZGVudGlhbDAKBggqhkjOPQQD - AgNpADBmAjEAlqMn2N2Pd6QnfKOcpBNYUtWlgIwVyFTafzL7BzpYXxRD4PRXBOdlX4aTqk6QUZPk - AjEAiUooCB4aDQEoKE0DjpY3m/GNdjfThumDfxuryr8G1bPO8MVlJfsxPo63BxnmqdA4 - -----END CERTIFICATE----- - """.trimIndent() - - // Generated by: - // $ ./gradlew --quiet runIdentityCtl --args "generateIaca --validity_in_years 10" - // Then copy the contents of the identityctl/iaca_certificate.pem file here. - val mdlCaCertificatePem = """ - -----BEGIN CERTIFICATE----- - MIICuTCCAkCgAwIBAgIRAIxlo7ajVrEgr3Cwcn6tKqwwCgYIKoZIzj0EAwMwOTEqMCgGA1UEAwwh - T1dGIElkZW50aXR5IENyZWRlbnRpYWwgVEVTVCBJQUNBMQswCQYDVQQGEwJaWjAeFw0yNDEwMjky - MTA4MTFaFw0zNDEwMjkyMTA4MTFaMDkxKjAoBgNVBAMMIU9XRiBJZGVudGl0eSBDcmVkZW50aWFs - IFRFU1QgSUFDQTELMAkGA1UEBhMCWlowdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASZiCZJrPJtWPfq - 4FhcVZURmxlNPcmlMIIVfjt5dxMKlIco65kf+uyDDnAdrt5f1wEOdWuKAr+iBoO/50xUtjqchOX6 - ETfHmke43lY1H19UCkuGi9SdXJWdpBeJO20QHnGjggEKMIIBBjAdBgNVHQ4EFgQUQmjXY2rPnttO - uAtrUjDxgjuAY+MwHwYDVR0jBBgwFoAUQmjXY2rPnttOuAtrUjDxgjuAY+MwDgYDVR0PAQH/BAQD - AgEGMEwGA1UdEgRFMEOGQWh0dHBzOi8vZ2l0aHViLmNvbS9vcGVud2FsbGV0LWZvdW5kYXRpb24t - bGFicy9pZGVudGl0eS1jcmVkZW50aWFsMBIGA1UdEwEB/wQIMAYBAf8CAQAwUgYDVR0fBEswSTBH - oEWgQ4ZBaHR0cHM6Ly9naXRodWIuY29tL29wZW53YWxsZXQtZm91bmRhdGlvbi1sYWJzL2lkZW50 - aXR5LWNyZWRlbnRpYWwwCgYIKoZIzj0EAwMDZwAwZAIwOFyRmLlAH8x2rVDD8j4uuhMNuldOqfLF - uVcZgsOVIGOsYfSbWq8PJZpH3l88niUSAjBI1cdPojUMa9CFF0C6kqOE6jchM7W4OvDPh88pApg0 - uT32u4G3/NXr8L2JZTBcLOc= - -----END CERTIFICATE----- - """.trimIndent() - - val mdlDsCertificate: X509Certificate - val mdlCaCertificate: X509Certificate - - val caCertificate: X509Certificate - val intermediateCertificate: X509Certificate - val dsCertificate: X509Certificate - - init { - mdlDsCertificate = - parseCertificate(mdlDsCertificatePem.byteInputStream(Charsets.US_ASCII).readBytes()) - mdlCaCertificate = - parseCertificate(mdlCaCertificatePem.byteInputStream(Charsets.US_ASCII).readBytes()) - - java.security.Security.insertProviderAt(BouncyCastleProvider(), 1) - val extensionUtils: X509ExtensionUtils = BcX509ExtensionUtils() - val kpg = KeyPairGenerator.getInstance("EC", BouncyCastleProvider()) - kpg.initialize(ECGenParameterSpec("secp256r1")) - - // generate CA certificate - val keyPairCA = kpg.generateKeyPair() - val caPublicKeyInfo: SubjectPublicKeyInfo = - SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo( - PublicKeyFactory.createKey(keyPairCA.public.encoded) - ) - val nowMillis = System.currentTimeMillis() - val certBuilderCA = JcaX509v3CertificateBuilder( - X500Name("CN=Test TrustManager CA"), - BigInteger.ONE, - Date(nowMillis), - Date(nowMillis + 24 * 3600 * 1000), - X500Name("CN=Test TrustManager CA"), - keyPairCA.public - ).addExtension(Extension.basicConstraints, true, BasicConstraints(0)) - .addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign)) - .addExtension( - Extension.subjectKeyIdentifier, - false, - extensionUtils.createSubjectKeyIdentifier(caPublicKeyInfo) - ) - val signerCA = JcaContentSignerBuilder("SHA256withECDSA").build(keyPairCA.private) - val caHolder = certBuilderCA.build(signerCA) - val cf = CertificateFactory.getInstance("X.509") - val caStream = ByteArrayInputStream(caHolder.encoded) - caCertificate = cf.generateCertificate(caStream) as X509Certificate - - // generate intermediate certificate - val keyPairIntermediate = kpg.generateKeyPair() - val intermediatePublicKeyInfo: SubjectPublicKeyInfo = - SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo( - PublicKeyFactory.createKey(keyPairIntermediate.public.encoded) - ) - val certBuilderIntermediate = JcaX509v3CertificateBuilder( - X500Name("CN=Test TrustManager CA"), - BigInteger.TWO, - Date(nowMillis), - Date(nowMillis + 24 * 3600 * 1000), - X500Name("CN=Test TrustManager Intermediate"), - keyPairIntermediate.public - ).addExtension(Extension.basicConstraints, true, BasicConstraints(0)) - .addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign)) - .addExtension( - Extension.authorityKeyIdentifier, - false, - extensionUtils.createAuthorityKeyIdentifier(caHolder) - ) - .addExtension( - Extension.subjectKeyIdentifier, - false, - extensionUtils.createSubjectKeyIdentifier(intermediatePublicKeyInfo) - ) - - val intermediateHolder = certBuilderIntermediate.build(signerCA) - val intermediateStream = ByteArrayInputStream(intermediateHolder.encoded) - intermediateCertificate = cf.generateCertificate(intermediateStream) as X509Certificate - - // generate DS certificate - val keyPairDS = kpg.generateKeyPair() - val dsPublicKeyInfo: SubjectPublicKeyInfo = - SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo( - PublicKeyFactory.createKey(keyPairDS.public.encoded) - ) - val certBuilderDS = JcaX509v3CertificateBuilder( - X500Name("CN=Test TrustManager Intermediate"), - BigInteger.ONE.add(BigInteger.TWO), - Date(nowMillis), - Date(nowMillis + 24 * 3600 * 1000), - X500Name("CN=Test TrustManager DS"), - keyPairDS.public - ).addExtension(Extension.basicConstraints, true, BasicConstraints(0)) - .addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.digitalSignature)) - .addExtension( - Extension.authorityKeyIdentifier, - false, - extensionUtils.createAuthorityKeyIdentifier(intermediateHolder) - ) - .addExtension( - Extension.subjectKeyIdentifier, - false, - extensionUtils.createSubjectKeyIdentifier(dsPublicKeyInfo) - ) - val signerIntermediate = - JcaContentSignerBuilder("SHA256withECDSA").build(keyPairIntermediate.private) - val dsHolder = certBuilderDS.build(signerIntermediate) - val dsStream = ByteArrayInputStream(dsHolder.encoded) - dsCertificate = cf.generateCertificate(dsStream) as X509Certificate - } - - @Test - fun testTrustManagerHappyFlow() { - // arrange (start with a TrustManager without certificates) - val trustManager = TrustManager() - - // act (add certificate and verify chain) - trustManager.addTrustPoint(TrustPoint(X509Cert(mdlCaCertificate.encoded))) - val result = trustManager.verify(listOf(mdlDsCertificate)) - - // assert - Assert.assertTrue("DS Certificate is trusted", result.isTrusted) - Assert.assertEquals("Trust chain contains 2 certificates", 2, result.trustChain.size) - Assert.assertEquals("Error is empty", result.error, null) - } - - @Test - fun testTrustManagerHappyFlowWithIntermediateAndCaCertifcate() { - // arrange (start with a TrustManager without certificates) - val trustManager = TrustManager() - - // act (add intermediate and CA certificate and verify chain) - trustManager.addTrustPoint(TrustPoint(X509Cert(intermediateCertificate.encoded))) - trustManager.addTrustPoint(TrustPoint(X509Cert(caCertificate.encoded))) - val result = trustManager.verify(listOf(dsCertificate)) - - // assert - Assert.assertTrue("DS Certificate is trusted", result.isTrusted) - Assert.assertEquals("Trust chain contains 3 certificates", 3, result.trustChain.size) - Assert.assertEquals("Error is empty", result.error, null) - } - - @Test - fun testTrustManagerHappyFlowWithIntermediateCertifcate() { - // arrange (start with a TrustManager without certificates) - val trustManager = TrustManager() - - // act (add intermediate certificate (without CA) and verify chain) - trustManager.addTrustPoint(TrustPoint(X509Cert(intermediateCertificate.encoded))) - val result = trustManager.verify(listOf(dsCertificate)) - - // assert - Assert.assertTrue("DS Certificate is trusted", result.isTrusted) - Assert.assertEquals("Trust chain contains 2 certificates", 2, result.trustChain.size) - Assert.assertEquals("Error is empty", result.error, null) - } - - @Test - fun testTrustManagerCaCertificateMissing() { - // arrange (start with a TrustManager without certificates) - val trustManager = TrustManager() - - // act (verify chain) - val result = trustManager.verify(listOf(mdlDsCertificate)) - - // assert - Assert.assertFalse("DS Certificate is not trusted", result.isTrusted) - Assert.assertEquals("Trust chain is empty", 0, result.trustChain.size) - Assert.assertEquals( - "Trustmanager complains about missing CA Certificate", - "No trusted root certificate could not be found", - result.error?.message - ) - } - - private fun parseCertificate(certificateBytes: ByteArray): X509Certificate { - return CertificateFactory.getInstance("X509") - .generateCertificate(ByteArrayInputStream(certificateBytes)) as X509Certificate - } -} \ No newline at end of file diff --git a/identityctl/src/main/java/com/android/identity/identityctl/IdentityCtl.kt b/identityctl/src/main/java/com/android/identity/identityctl/IdentityCtl.kt index b0186176d..2485224b3 100644 --- a/identityctl/src/main/java/com/android/identity/identityctl/IdentityCtl.kt +++ b/identityctl/src/main/java/com/android/identity/identityctl/IdentityCtl.kt @@ -1,31 +1,18 @@ package com.android.identity.identityctl -import com.android.identity.crypto.Algorithm +import com.android.identity.asn1.ASN1Integer import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey -import com.android.identity.crypto.EcPublicKey +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.X509CertificateCreateOption -import com.android.identity.crypto.X509CertificateExtension -import com.android.identity.crypto.create -import com.android.identity.crypto.javaX509Certificate +import com.android.identity.crypto.X509KeyUsage +import com.android.identity.mdoc.util.MdocUtil import kotlinx.datetime.Clock import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.plus -import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.x509.BasicConstraints -import org.bouncycastle.asn1.x509.CRLDistPoint -import org.bouncycastle.asn1.x509.DistributionPoint -import org.bouncycastle.asn1.x509.DistributionPointName -import org.bouncycastle.asn1.x509.ExtendedKeyUsage -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.GeneralName -import org.bouncycastle.asn1.x509.GeneralNames -import org.bouncycastle.asn1.x509.KeyPurposeId -import org.bouncycastle.asn1.x509.KeyUsage import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.util.BigIntegers import java.io.File @@ -37,202 +24,6 @@ import kotlin.random.Random @OptIn(ExperimentalEncodingApi::class) object IdentityCtl { - - /** - * Generates a self-signed IACA certificate according to ISO/IEC 18013-5:2021 Annex B.1.2. - * - * @param iacaKey the private key. - * @param subject the value to use for subject and issuer, e.g. "CN=Test IACA,C=ZZ". - * @param validFrom the point in time the certificate should be valid from. - * @param validUntil the point in time the certificate should be valid until. - * @param issuerAltNameUrl the issuer alternative name (see RFC 5280 section 4.2.1.7), - * e.g. "http://issuer.example.com/informative/web/page". - * @param crlUrl the URL for revocation (see RFC 5280 section 4.2.1.13). - * @return a [X509Cert] with all the required extensions. - */ - @JvmStatic - fun generateIacaCertificate( - iacaKey: EcPrivateKey, - subject: String, - validFrom: Instant, - validUntil: Instant, - issuerAltNameUrl: String, - crlUrl: String - ): X509Cert { - // Requirements for the IACA certificate is defined in ISO/IEC 18013-5:2021 Annex B - - // From 18013-5 table B.1: countryName is mandatory - // stateOrProvinceName is optional. - // organizationName is optional. - // commonName shall be present. - // serialNumber is optional. - // - - // From 18013-5 Annex B: 3-5 years is recommended - // Maximum of 20 years after “Not before” date - - val curve = iacaKey.curve - - // From 18013-5 table B.1: Non-sequential positive, non-zero integer, shall contain - // at least 63 bits of output from a CSPRNG, should contain at - // least 71 bits of output from a CSPRNG, maximum 20 octets. - val serial = BigIntegers.fromUnsignedByteArray(Random.Default.nextBytes(16)).toString() - - val extensions = mutableListOf() - - // From 18013-5 table B.1: critical: Key certificate signature + CRL signature bits set - extensions.add( - X509CertificateExtension( - Extension.keyUsage.toString(), - true, - KeyUsage(KeyUsage.cRLSign + KeyUsage.keyCertSign).encoded - ) - ) - - // From 18013-5 table B.1: non-critical, Email or URL - extensions.add( - X509CertificateExtension( - Extension.issuerAlternativeName.toString(), - false, - GeneralNames( - GeneralName(GeneralName.uniformResourceIdentifier, issuerAltNameUrl) - ).encoded - ) - ) - - // From 18013-5 table B.1: critical, CA=true, pathLenConstraint=0 - extensions.add( - X509CertificateExtension( - Extension.basicConstraints.toString(), - true, - BasicConstraints(0).encoded - ) - ) - - // From 18013-5 table B.1: non-critical, The ‘reasons’ and ‘cRL Issuer’ - // fields shall not be used. - val distributionPoint = - DistributionPoint( - DistributionPointName( - GeneralNames( - GeneralName(GeneralName.uniformResourceIdentifier, crlUrl) - ) - ), - null, - null - ) - extensions.add( - X509CertificateExtension( - Extension.cRLDistributionPoints.toString(), - false, - CRLDistPoint(listOf(distributionPoint).toTypedArray()).encoded - ) - ) - - return X509Cert.create( - iacaKey.publicKey, - iacaKey, - null, - curve.defaultSigningAlgorithm, - serial, - subject, - subject, - validFrom, - validUntil, - // 18013-5 Annex C requires both of these to be present - setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_AS_SUBJECT_KEY_IDENTIFIER - ), - extensions - ) - } - - /** - * Generates a Document Signing certificate according to ISO/IEC 18013-5:2021 Annex B.1.4. - * - * @param iacaCert the IACA certificate the DS certificate. - * @param iacaKey the private key for the IACA certificate. - * @param dsKey the public part of the DS key. - * @param subject the value to use for subject, e.g. "CN=Test DS,C=ZZ". - * @param validFrom the point in time the certificate should be valid from. - * @param validUntil the point in time the certificate should be valid until. - * @return a [X509Cert] with all the required extensions. - */ - @JvmStatic - fun generateDsCertificate( - iacaCert: X509Cert, - iacaKey: EcPrivateKey, - dsKey: EcPublicKey, - subject: String, - validFrom: Instant, - validUntil: Instant, - ): X509Cert { - - val iacaCertJava = iacaCert.javaX509Certificate - - // Must be same exact binary value as the subject of IACA certificate. - val issuer = iacaCertJava.subjectX500Principal.toString() - - val serial = BigIntegers.fromUnsignedByteArray(Random.Default.nextBytes(16)).toString() - - val extensions = mutableListOf() - - extensions.add( - X509CertificateExtension( - Extension.keyUsage.toString(), - true, - KeyUsage(KeyUsage.digitalSignature).encoded - ) - ) - - extensions.add( - X509CertificateExtension( - Extension.extendedKeyUsage.toString(), - true, - ExtendedKeyUsage( - KeyPurposeId.getInstance(ASN1ObjectIdentifier("1.0.18013.5.1.2")) - ).encoded - ) - ) - - // Copy cRLDistributionPoints and issuerAlternativeName from IACA cert - extensions.add( - X509CertificateExtension( - Extension.cRLDistributionPoints.toString(), - false, - iacaCertJava.getExtensionValue(Extension.cRLDistributionPoints.toString()) - ) - ) - extensions.add( - X509CertificateExtension( - Extension.issuerAlternativeName.toString(), - false, - iacaCertJava.getExtensionValue(Extension.issuerAlternativeName.toString()) - ) - ) - - val documentSigningKeyCert = X509Cert.create( - dsKey, - iacaKey, - iacaCert, - Algorithm.ES256, - serial, - subject, - issuer, - validFrom, - validUntil, - setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_FROM_SIGNING_KEY_CERTIFICATE - ), - extensions - ) - - return documentSigningKeyCert - } - - fun getArg( args: Array, argName: String, @@ -252,9 +43,9 @@ object IdentityCtl { fun generateIaca(args: Array) { val certificateOutputFilename = - getArg(args,"out_certificate","iaca_certificate.pem") + getArg(args,"out_certificate", "iaca_certificate.pem") val privateKeyOutputFilename = - getArg(args,"out_private_key","iaca_private_key.pem") + getArg(args,"out_private_key", "iaca_private_key.pem") // Requirements for the IACA certificate is defined in ISO/IEC 18013-5:2021 Annex B @@ -264,18 +55,18 @@ object IdentityCtl { // commonName shall be present. // serialNumber is optional. // - val subjectAndIssuer = - getArg(args,"subject_and_issuer", - "CN=OWF Identity Credential TEST IACA,C=ZZ") + val subjectAndIssuer = X500Name.fromName( + getArg(args, "subject_and_issuer", "CN=OWF Identity Credential TEST IACA,C=ZZ") + ) // From 18013-5 Annex B: 3-5 years is recommended // Maximum of 20 years after “Not before” date - val validityInYears = getArg(args,"validity_in_years","5").toInt() - val now = Clock.System.now() + val validityInYears = getArg(args, "validity_in_years", "5").toInt() + val now = Instant.fromEpochSeconds(Clock.System.now().epochSeconds) val validFrom = now val validUntil = now.plus(DateTimePeriod(years = validityInYears), TimeZone.currentSystemDefault()) - val curveName = getArg(args,"curve","P384") + val curveName = getArg(args, "curve", "P384") val curve = EcCurve.values().find { curve -> curve.name == curveName } ?: throw IllegalArgumentException("No curve with name $curveName") @@ -292,9 +83,12 @@ object IdentityCtl { "https://github.com/openwallet-foundation-labs/identity-credential" ) - val iacaCertificate = generateIacaCertificate( + val serial = ASN1Integer(BigIntegers.fromUnsignedByteArray(Random.Default.nextBytes(16)).toByteArray()) + + val iacaCertificate = MdocUtil.generateIacaCertificate( iacaKey, subjectAndIssuer, + serial, validFrom, validUntil, issuerAltNameUrl, @@ -318,9 +112,9 @@ object IdentityCtl { fun generateDs(args: Array) { val iacaCertificateFilename = - getArg(args,"iaca_certificate","iaca_certificate.pem") + getArg(args, "iaca_certificate", "iaca_certificate.pem") val iacaPrivateKeyFilename = - getArg(args,"iaca_private_key","iaca_private_key.pem") + getArg(args, "iaca_private_key", "iaca_private_key.pem") val iacaCert = X509Cert.fromPem( String(File(iacaCertificateFilename).readBytes(), StandardCharsets.US_ASCII)) @@ -330,36 +124,43 @@ object IdentityCtl { iacaCert.ecPublicKey) val certificateOutputFilename = - getArg(args,"out_certificate","ds_certificate.pem") + getArg(args, "out_certificate", "ds_certificate.pem") val privateKeyOutputFilename = - getArg(args,"out_private_key","ds_private_key.pem") + getArg(args, "out_private_key", "ds_private_key.pem") // Requirements for the IACA certificate is defined in ISO/IEC 18013-5:2021 Annex B - val subject = getArg(args,"subject", "CN=OWF Identity Credential TEST DS,C=ZZ") + val subject = X500Name.fromName( + getArg(args, "subject", "CN=OWF Identity Credential TEST DS,C=ZZ") + ) - val validityInYears = getArg(args,"validity_in_years","1").toInt() - val now = Clock.System.now() + val validityInYears = getArg(args, "validity_in_years", "1").toInt() + val now = Instant.fromEpochSeconds(Clock.System.now().epochSeconds) val validFrom = now val validUntil = now.plus(DateTimePeriod(years = validityInYears), TimeZone.currentSystemDefault()) - val curveName = getArg(args,"curve","P256") + val curveName = getArg(args, "curve", "P256") val curve = EcCurve.values().find { curve -> curve.name == curveName } ?: throw IllegalArgumentException("No curve with name $curveName") val dsKey = Crypto.createEcPrivateKey(curve) - val dsCertificate = generateDsCertificate( + val serial = ASN1Integer(BigIntegers.fromUnsignedByteArray(Random.Default.nextBytes(16)).toByteArray()) + + val dsCertificate = MdocUtil.generateDsCertificate( iacaCert, iacaPrivateKey, dsKey.publicKey, subject, + serial, validFrom, validUntil ) println("Generated DS certificate and private key.") + println("- Loaded IACA cert from $iacaCertificateFilename") + File(privateKeyOutputFilename).outputStream().bufferedWriter().let { it.write(dsKey.toPem()) it.close() @@ -375,55 +176,43 @@ object IdentityCtl { fun generateReaderRoot(args: Array) { val certificateOutputFilename = - getArg(args,"out_certificate","reader_root_certificate.pem") + getArg(args, "out_certificate", "reader_root_certificate.pem") val privateKeyOutputFilename = - getArg(args,"out_private_key","reader_root_private_key.pem") + getArg(args, "out_private_key", "reader_root_private_key.pem") - val subjectAndIssuer = - getArg(args,"subject_and_issuer", - "CN=OWF Identity Credential TEST Reader CA,C=ZZ") + val subjectAndIssuer = X500Name.fromName( + getArg(args, "subject_and_issuer", "CN=OWF Identity Credential TEST Reader CA,C=ZZ") + ) // From 18013-5 Annex B: 3-5 years is recommended // Maximum of 20 years after “Not before” date - val validityInYears = getArg(args,"validity_in_years","5").toInt() - val now = Clock.System.now() + val validityInYears = getArg(args, "validity_in_years", "5").toInt() + val now = Instant.fromEpochSeconds(Clock.System.now().epochSeconds) val validFrom = now val validUntil = now.plus(DateTimePeriod(years = validityInYears), TimeZone.currentSystemDefault()) - val curveName = getArg(args,"curve","P384") + val curveName = getArg(args, "curve", "P384") val curve = EcCurve.values().find { curve -> curve.name == curveName } ?: throw IllegalArgumentException("No curve with name $curveName") - val serial = BigIntegers.fromUnsignedByteArray(Random.Default.nextBytes(16)).toString() + val serial = ASN1Integer(BigIntegers.fromUnsignedByteArray(Random.Default.nextBytes(16)).toByteArray()) val readerRootKey = Crypto.createEcPrivateKey(curve) - val extensions = mutableListOf() - - extensions.add( - X509CertificateExtension( - Extension.keyUsage.toString(), - true, - KeyUsage(KeyUsage.cRLSign + KeyUsage.keyCertSign).encoded - ) - ) - - val readerRootCertificate = X509Cert.create( - readerRootKey.publicKey, - readerRootKey, - null, - curve.defaultSigningAlgorithm, - serial, - subjectAndIssuer, - subjectAndIssuer, - validFrom, - validUntil, - setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_AS_SUBJECT_KEY_IDENTIFIER - ), - extensions + val readerRootCertificate = X509Cert.Builder( + publicKey = readerRootKey.publicKey, + signingKey = readerRootKey, + signatureAlgorithm = readerRootKey.curve.defaultSigningAlgorithm, + serialNumber = serial, + subject = subjectAndIssuer, + issuer = subjectAndIssuer, + validFrom = validFrom, + validUntil = validUntil ) + .includeSubjectKeyIdentifier() + .setKeyUsage(setOf(X509KeyUsage.CRL_SIGN, X509KeyUsage.KEY_CERT_SIGN)) + .setBasicConstraints(true, 0) + .build() println("Generated self-signed reader root certificate and private key.") @@ -449,7 +238,7 @@ Generate an IACA certificate and corresponding private key: identityctl generateIaca [--out_certificate iaca_certificate.pem] [--out_private_key iaca_private_key.pem] - [--subject_and_issuer 'CN=Utopia TEST IACA,C=ZZ'] + [--subject_and_issuer 'CN=OWF Identity Credential TEST IACA,C=ZZ'] [--validity_in_years 5] [--curve P384] [--issuer_alt_name_url https://issuer.example.com/website] @@ -462,7 +251,7 @@ Generate an DS certificate and corresponding private key: --iaca_private_key iaca_private_key.pem [--out_certificate ds_certificate.pem] [--out_private_key ds_private_key.pem] - [--subject 'CN=Utopia TEST DS,C=ZZ'] + [--subject 'CN=OWF Identity Credential TEST DS,C=ZZ'] [--validity_in_years 1] [--curve P256] @@ -471,7 +260,7 @@ Generate an reader root and corresponding private key: identityctl generateReaderRoot [--out_certificate reader_root_certificate.pem] [--out_private_key reader_root_private_key.pem] - [--subject_and_issuer 'CN=Utopia TEST Reader CA,C=ZZ'] + [--subject_and_issuer 'CN=OWF Identity Credential TEST Reader CA,C=ZZ'] [--validity_in_years 3] [--curve P384] @@ -504,5 +293,4 @@ Generate an reader root and corresponding private key: } } } - } diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/TestAppUtils.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/TestAppUtils.kt index 3cb553a7b..350382bdf 100644 --- a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/TestAppUtils.kt +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/TestAppUtils.kt @@ -1,6 +1,7 @@ package com.android.identity.testapp import com.android.identity.appsupport.ui.consent.MdocConsentField +import com.android.identity.asn1.ASN1Integer import com.android.identity.cbor.Bstr import com.android.identity.cbor.Cbor import com.android.identity.cbor.CborArray @@ -13,11 +14,14 @@ import com.android.identity.cose.CoseLabel import com.android.identity.cose.CoseNumberLabel import com.android.identity.credential.CredentialFactory import com.android.identity.crypto.Algorithm +import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey import com.android.identity.crypto.EcPublicKey +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain +import com.android.identity.crypto.X509KeyUsage import com.android.identity.document.DocumentStore import com.android.identity.document.NameSpacedData import com.android.identity.documenttype.DocumentTypeRepository @@ -39,9 +43,17 @@ import com.android.identity.securearea.software.SoftwareCreateKeySettings import com.android.identity.securearea.software.SoftwareSecureArea import com.android.identity.storage.EphemeralStorageEngine import com.android.identity.storage.StorageEngine +import com.android.identity.trustmanagement.TrustManager +import com.android.identity.trustmanagement.TrustPoint import com.android.identity.util.Constants import com.android.identity.util.Logger +import identitycredential.samples.testapp.generated.resources.Res +import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import org.jetbrains.compose.resources.ExperimentalResourceApi import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.iterator @@ -69,9 +81,9 @@ object TestAppUtils { docType = mdocRequest.docType, itemsToRequest = itemsToRequest, requestInfo = null, - readerKey = null, - signatureAlgorithm = Algorithm.UNSET, - readerKeyCertificateChain = null, + readerKey = readerKey, + signatureAlgorithm = readerKey.curve.defaultSigningAlgorithm, + readerKeyCertificateChain = X509CertChain(listOf(readerCert, readerRootCert)), ) return deviceRequestGenerator.generate() } @@ -141,7 +153,28 @@ object TestAppUtils { private lateinit var secureAreaRepository: SecureAreaRepository private lateinit var credentialFactory: CredentialFactory lateinit var documentTypeRepository: DocumentTypeRepository + + private val certsValidFrom = LocalDate.parse("2024-12-01").atStartOfDayIn(TimeZone.UTC) + private val certsValidUntil = LocalDate.parse("2034-12-01").atStartOfDayIn(TimeZone.UTC) + + private lateinit var dsKey: EcPrivateKey lateinit var dsKeyPub: EcPublicKey + lateinit var dsCert: X509Cert + + private lateinit var iacaKey: EcPrivateKey + lateinit var iacaKeyPub: EcPublicKey + lateinit var iacaCert: X509Cert + + private lateinit var readerKey: EcPrivateKey + lateinit var readerKeyPub: EcPublicKey + lateinit var readerCert: X509Cert + + private lateinit var readerRootKey: EcPrivateKey + lateinit var readerRootKeyPub: EcPublicKey + lateinit var readerRootCert: X509Cert + + lateinit var issuerTrustManager: TrustManager + lateinit var readerTrustManager: TrustManager init { storageEngine = EphemeralStorageEngine() @@ -152,11 +185,116 @@ object TestAppUtils { credentialFactory.addCredentialImplementation(MdocCredential::class) { document, dataItem -> MdocCredential(document, dataItem) } + generateKeysAndCerts() + generateTrustManagers() provisionDocument() documentTypeRepository = DocumentTypeRepository() documentTypeRepository.addDocumentType(DrivingLicense.getDocumentType()) } + private fun generateKeysAndCerts() { + iacaKeyPub = EcPublicKey.fromPem( + """ + -----BEGIN PUBLIC KEY----- + MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAElQdbKvX5mU29gS+xH/XLa5hSRRzMGpdN + 5PCLJHKIQYUWdnRRH6A0oLiCt0I/gX90D5NZN27LY2VGaiDkgHI9J3CW99YHZ/5N + /4x1uBkz7X66R5oKAOFP9nCAKhM2C+PI + -----END PUBLIC KEY----- + """.trimIndent().trim(), + EcCurve.P384 + ) + iacaKey = EcPrivateKey.fromPem( + """ + -----BEGIN PRIVATE KEY----- + MFcCAQAwEAYHKoZIzj0CAQYFK4EEACIEQDA+AgEBBDBLCuy17r0A6FtCd552BGW12sQKD095yEaG + nZxSDva2gKvmaKex2dylcZg5cR39M0SgBwYFK4EEACI= + -----END PRIVATE KEY----- + """.trimIndent().trim(), + iacaKeyPub + ) + iacaCert = MdocUtil.generateIacaCertificate( + iacaKey = iacaKey, + subject = X500Name.fromName("C=ZZ,CN=OWF Identity Credential TEST IACA"), + serial = ASN1Integer(1L), + validFrom = certsValidFrom, + validUntil = certsValidUntil, + issuerAltNameUrl = "https://github.com/openwallet-foundation-labs/identity-credential", + crlUrl = "https://github.com/openwallet-foundation-labs/identity-credential" + ) + + dsKey = Crypto.createEcPrivateKey(EcCurve.P256) + dsKeyPub = dsKey.publicKey + dsCert = MdocUtil.generateDsCertificate( + iacaCert = iacaCert, + iacaKey = iacaKey, + dsKey = dsKeyPub, + subject = X500Name.fromName("C=ZZ,CN=OWF Identity Credential TEST DS"), + serial = ASN1Integer(1L), + validFrom = certsValidFrom, + validUntil = certsValidUntil, + ) + + readerRootKeyPub = EcPublicKey.fromPem( + """ + -----BEGIN PUBLIC KEY----- + MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzNWVQ0OxOAProyZdCezqfKr8vwUoIP1A + k8Plq8GCN9v41faZPELuQUk21A2RNj0IXqsuLN4/MtYDLvkOaXpoWJ/3ODjGF2WP + Lg/reFqPVBaEg5BW75bpmf3LjuU0wCO+ + -----END PUBLIC KEY----- + """.trimIndent().trim(), + EcCurve.P384 + ) + readerRootKey = EcPrivateKey.fromPem( + """ + -----BEGIN PRIVATE KEY----- + MFcCAQAwEAYHKoZIzj0CAQYFK4EEACIEQDA+AgEBBDDxgrZBXnoO54/hZM2DAGrByoWRatjH9hGs + lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI= + -----END PRIVATE KEY----- + """.trimIndent().trim(), + readerRootKeyPub + ) + readerRootCert = MdocUtil.generateReaderRootCertificate( + readerRootKey = iacaKey, + subject = X500Name.fromName("CN=OWF IC TestApp Reader Root"), + serial = ASN1Integer(1L), + validFrom = certsValidFrom, + validUntil = certsValidUntil, + ) + + readerKey = Crypto.createEcPrivateKey(EcCurve.P256) + readerKeyPub = readerKey.publicKey + readerCert = MdocUtil.generateReaderCertificate( + readerRootCert = readerRootCert, + readerRootKey = readerRootKey, + readerKey = readerKeyPub, + subject = X500Name.fromName("CN=OWF IC TestApp Reader Cert"), + serial = ASN1Integer(1L), + validFrom = certsValidFrom, + validUntil = certsValidUntil, + ) + } + + @OptIn(ExperimentalResourceApi::class) + private fun generateTrustManagers() { + issuerTrustManager = TrustManager() + issuerTrustManager.addTrustPoint( + TrustPoint( + certificate = iacaCert, + displayName = "OWF IC TestApp Issuer", + displayIcon = null + ) + ) + + readerTrustManager = TrustManager() + readerTrustManager.addTrustPoint( + TrustPoint( + certificate = readerRootCert, + displayName = "OWF IC TestApp", + displayIcon = runBlocking { Res.readBytes("files/utopia-brewery.png") } + ) + ) + } + private fun provisionDocument() { val documentStore = DocumentStore( storageEngine, @@ -227,35 +365,6 @@ object TestAppUtils { val validFrom = Clock.System.now() - 1.hours val validUntil = validFrom + 24.hours - val documentSignerKeyPub = EcPublicKey.fromPem( -"""-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnmiWAMGIeo2E3usWRLL/EPfh1Bw5 -JHgq8RYzJvraMj5QZSh94CL/nlEi3vikGxDP34HjxZcjzGEimGg03sB6Ng== ------END PUBLIC KEY-----""", - EcCurve.P256 - ) - dsKeyPub = documentSignerKeyPub - - val documentSignerKey = EcPrivateKey.fromPem( - """-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/ANvinTxJAdR8nQ0 -NoUdBMcRJz+xLsb0kmhyMk+lkkGhRANCAASeaJYAwYh6jYTe6xZEsv8Q9+HUHDkk -eCrxFjMm+toyPlBlKH3gIv+eUSLe+KQbEM/fgePFlyPMYSKYaDTewHo2 ------END PRIVATE KEY-----""", - documentSignerKeyPub - ) - - val documentSignerCert = X509Cert.fromPem( -"""-----BEGIN CERTIFICATE----- -MIIBITCBx6ADAgECAgEBMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMMD1N0YXRlIE9mIFV0b3BpYTAe -Fw0yNDExMDcyMTUzMDdaFw0zNDExMDUyMTUzMDdaMBoxGDAWBgNVBAMMD1N0YXRlIE9mIFV0b3Bp -YTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ5olgDBiHqNhN7rFkSy/xD34dQcOSR4KvEWMyb6 -2jI+UGUofeAi/55RIt74pBsQz9+B48WXI8xhIphoNN7AejYwCgYIKoZIzj0EAwIDSQAwRgIhALkq -UIVeaSW0xhLuMdwHyjiwTV8USD4zq68369ZW6jBvAiEAj2smZAXJB04x/s3exzjnI5BQprUOSfYE -uku1Jv7gA+A= ------END CERTIFICATE-----"""" - ) - val mso = msoGenerator.generate() val taggedEncodedMso = Cbor.encode(Tagged(24, Bstr(mso))) @@ -272,12 +381,12 @@ uku1Jv7gA+A= val unprotectedHeaders = mapOf( Pair( CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN), - X509CertChain(listOf(documentSignerCert)).toDataItem() + X509CertChain(listOf(dsCert, iacaCert)).toDataItem() ) ) val encodedIssuerAuth = Cbor.encode( Cose.coseSign1Sign( - documentSignerKey, + dsKey, taggedEncodedMso, true, Algorithm.ES256, @@ -298,5 +407,4 @@ uku1Jv7gA+A= ) } - } \ No newline at end of file diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/IsoMdocProximityReadingScreen.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/IsoMdocProximityReadingScreen.kt index fb2c5ec40..cd67a3c0c 100644 --- a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/IsoMdocProximityReadingScreen.kt +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/IsoMdocProximityReadingScreen.kt @@ -65,6 +65,7 @@ import com.android.identity.mdoc.transport.MdocTransportClosedException import com.android.identity.mdoc.transport.MdocTransportFactory import com.android.identity.mdoc.transport.MdocTransportOptions import com.android.identity.testapp.TestAppUtils +import com.android.identity.trustmanagement.TrustManager import com.android.identity.util.Constants import com.android.identity.util.Logger import com.android.identity.util.fromBase64Url @@ -495,7 +496,8 @@ private fun ShowReaderResults( // TODO: show multiple documents val documentData = DocumentData.fromMdocDeviceResponseDocument( deviceResponse.documents[0], - TestAppUtils.documentTypeRepository + TestAppUtils.documentTypeRepository, + TestAppUtils.issuerTrustManager ) ShowDocumentData(documentData, 0, deviceResponse.documents.size) } @@ -639,15 +641,16 @@ private data class DocumentData( fun fromMdocDeviceResponseDocument( document: DeviceResponseParser.Document, documentTypeRepository: DocumentTypeRepository, + issuerTrustManager: TrustManager ): DocumentData { val infos = mutableListOf() val warnings = mutableListOf() val kvPairs = mutableListOf() if (document.issuerSignedAuthenticated) { - // TODO: Take a [TrustManager] instance and use that to determine trust relationship - if (document.issuerCertificateChain.certificates.last().ecPublicKey == TestAppUtils.dsKeyPub) { - infos.add("Issuer is in a trust list") + val trustResult = issuerTrustManager.verify(document.issuerCertificateChain.certificates) + if (trustResult.isTrusted) { + infos.add("Issuer '${trustResult.trustPoints[0].displayName}' is in a trust list") } else { warnings.add("Issuer is not in trust list") } diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/IsoMdocProximitySharingScreen.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/IsoMdocProximitySharingScreen.kt index b5cf506ba..152961d4a 100644 --- a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/IsoMdocProximitySharingScreen.kt +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/IsoMdocProximitySharingScreen.kt @@ -455,6 +455,19 @@ private suspend fun showConsentPrompt( encodedSessionTranscript, ).parse() for (docRequest in deviceRequest.docRequests) { + Logger.i(TAG, "docRequest.readerAuthenticated=${docRequest.readerAuthenticated}") + val trustPoint = if (docRequest.readerAuthenticated) { + val trustResult = TestAppUtils.readerTrustManager.verify(docRequest.readerCertificateChain!!.certificates) + Logger.i(TAG, "trustResult.isTrusted=${trustResult.isTrusted}") + Logger.i(TAG, "trustResult.error=${trustResult.error}") + if (trustResult.isTrusted) { + trustResult.trustPoints[0] + } else { + null + } + } else { + null + } if (docRequest.docType == DrivingLicense.MDL_DOCTYPE) { val cardArt = getDrawableResourceBytes( getSystemResourceEnvironment(), @@ -476,7 +489,7 @@ private suspend fun showConsentPrompt( ), consentFields = consentFields, relyingParty = ConsentRelyingParty( - trustPoint = null, + trustPoint = trustPoint, websiteOrigin = null, ) ) diff --git a/server-openid4vci/src/main/java/com/android/identity/server/openid4vci/openid4vp.kt b/server-openid4vci/src/main/java/com/android/identity/server/openid4vci/openid4vp.kt index 998fabb1e..e3e5b81fb 100644 --- a/server-openid4vci/src/main/java/com/android/identity/server/openid4vci/openid4vp.kt +++ b/server-openid4vci/src/main/java/com/android/identity/server/openid4vci/openid4vp.kt @@ -1,19 +1,19 @@ package com.android.identity.server.openid4vci +import com.android.identity.asn1.ASN1Integer import com.android.identity.crypto.Algorithm import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey import com.android.identity.crypto.EcPublicKey import com.android.identity.crypto.EcPublicKeyDoubleCoordinate +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.X509CertificateCreateOption -import com.android.identity.crypto.X509CertificateExtension -import com.android.identity.crypto.create import com.android.identity.crypto.javaX509Certificate import com.android.identity.documenttype.DocumentWellKnownRequest import com.android.identity.documenttype.knowntypes.EUPersonalID +import com.android.identity.mdoc.util.MdocUtil import com.android.identity.sdjwt.util.JsonWebKey import com.android.identity.util.toBase64Url import kotlinx.datetime.Clock @@ -132,24 +132,6 @@ private fun createSingleUseReaderKey(): Pair { val validFrom = now.plus(DateTimePeriod(minutes = -10), TimeZone.currentSystemDefault()) val validUntil = now.plus(DateTimePeriod(minutes = 10), TimeZone.currentSystemDefault()) val readerKey = Crypto.createEcPrivateKey(EcCurve.P256) - - val extensions = mutableListOf() - extensions.add( - X509CertificateExtension( - Extension.keyUsage.toString(), - true, - KeyUsage(KeyUsage.digitalSignature).encoded - ) - ) - extensions.add( - X509CertificateExtension( - Extension.extendedKeyUsage.toString(), - true, - ExtendedKeyUsage( - KeyPurposeId.getInstance(ASN1ObjectIdentifier("1.0.18013.5.1.2")) - ).encoded - ) - ) val readerKeySubject = "CN=OWF IC Online Verifier Single-Use Reader Key" // TODO: for now, instead of using the per-site Reader Root generated at first run, use the @@ -176,24 +158,14 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI= -----END PRIVATE KEY----- """.trimIndent(), owfIcReaderCert.ecPublicKey) - val owfIcReaderRootSignatureAlgorithm = Algorithm.ES384 - val owfIcReaderRootIssuer = owfIcReaderCert.javaX509Certificate.issuerX500Principal.name - - val readerKeyCertificate = X509Cert.create( - readerKey.publicKey, - owfIcReaderRoot, - owfIcReaderCert, - owfIcReaderRootSignatureAlgorithm, - "1", - readerKeySubject, - owfIcReaderRootIssuer, - validFrom, - validUntil, - setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_FROM_SIGNING_KEY_CERTIFICATE - ), - extensions + val readerKeyCertificate = MdocUtil.generateReaderCertificate( + readerRootCert = owfIcReaderCert, + readerRootKey = owfIcReaderRoot, + readerKey = readerKey.publicKey, + subject = X500Name.fromName(readerKeySubject), + serial = ASN1Integer(1L), + validFrom = validFrom, + validUntil = validUntil ) return Pair( readerKey, diff --git a/server/src/main/java/com/android/identity/wallet/server/CloudSecureAreaServlet.kt b/server/src/main/java/com/android/identity/wallet/server/CloudSecureAreaServlet.kt index 171bd9d22..827385b50 100644 --- a/server/src/main/java/com/android/identity/wallet/server/CloudSecureAreaServlet.kt +++ b/server/src/main/java/com/android/identity/wallet/server/CloudSecureAreaServlet.kt @@ -1,5 +1,7 @@ package com.android.identity.wallet.server +import com.android.identity.asn1.ASN1 +import com.android.identity.asn1.ASN1Integer import com.android.identity.cbor.Cbor import com.android.identity.cbor.CborArray import com.android.identity.crypto.Algorithm @@ -7,9 +9,8 @@ import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.X509CertChain import com.android.identity.crypto.EcPrivateKey +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.create -import com.android.identity.crypto.javaX509Certificate import com.android.identity.flow.handler.FlowNotifications import com.android.identity.flow.server.Configuration import com.android.identity.flow.server.FlowEnvironment @@ -105,37 +106,37 @@ class CloudSecureAreaServlet : BaseHttpServlet() { val attestationKey = Crypto.createEcPrivateKey(EcCurve.P256) val attestationKeySignatureAlgorithm = Algorithm.ES256 val attestationKeySubject = "CN=Cloud Secure Area Attestation Root" - val attestationKeyCertificate = X509Cert.create( - attestationKey.publicKey, - rootPrivateKey, - null, - attestationKeySignatureAlgorithm, - "1", - attestationKeySubject, - rootCertificate.javaX509Certificate.issuerX500Principal.name, - validFrom, - validUntil, - setOf(), - listOf() + val attestationKeyCertificate = X509Cert.Builder( + publicKey = attestationKey.publicKey, + signingKey = rootPrivateKey, + signatureAlgorithm = attestationKeySignatureAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName(attestationKeySubject), + issuer = rootCertificate.subject, + validFrom = validFrom, + validUntil = validUntil ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(rootCertificate) + .build() // Create Cloud Binding Key Attestation Root w/ self-signed certificate. val cloudBindingKeyAttestationKey = Crypto.createEcPrivateKey(EcCurve.P256) val cloudBindingKeySignatureAlgorithm = Algorithm.ES256 val cloudBindingKeySubject = "CN=Cloud Secure Area Cloud Binding Key Attestation Root" - val cloudBindingKeyAttestationCertificate = X509Cert.create( - cloudBindingKeyAttestationKey.publicKey, - cloudBindingKeyAttestationKey, - null, - cloudBindingKeySignatureAlgorithm, - "1", - cloudBindingKeySubject, - cloudBindingKeySubject, - validFrom, - validUntil, - setOf(), - listOf() + val cloudBindingKeyAttestationCertificate = X509Cert.Builder( + publicKey = cloudBindingKeyAttestationKey.publicKey, + signingKey = cloudBindingKeyAttestationKey, + signatureAlgorithm = cloudBindingKeySignatureAlgorithm, + serialNumber = ASN1Integer(1L), + subject = X500Name.fromName(cloudBindingKeySubject), + issuer = X500Name.fromName(cloudBindingKeySubject), + validFrom = validFrom, + validUntil = validUntil ) + .includeSubjectKeyIdentifier() + .setAuthorityKeyIdentifierToCertificate(rootCertificate) + .build() return KeyMaterial( serverSecureAreaBoundKey, @@ -244,14 +245,14 @@ class CloudSecureAreaServlet : BaseHttpServlet() { for (certificate in keyMaterial.attestationKeyCertificates.certificates) { sb.append("

Certificate

") sb.append("
")
-            sb.append(certificate.javaX509Certificate)
+            sb.append(ASN1.print(ASN1.decode(certificate.tbsCertificate)!!))
             sb.append("
") } sb.append("

Cloud Binding Key Attestation Root

") for (certificate in keyMaterial.cloudBindingKeyCertificates.certificates) { sb.append("

Certificate

") sb.append("
")
-            sb.append(certificate.javaX509Certificate)
+            sb.append(ASN1.print(ASN1.decode(certificate.tbsCertificate)!!))
             sb.append("
") } sb.append( diff --git a/server/src/main/java/com/android/identity/wallet/server/VerifierServlet.kt b/server/src/main/java/com/android/identity/wallet/server/VerifierServlet.kt index d2c21b3df..27358ad21 100644 --- a/server/src/main/java/com/android/identity/wallet/server/VerifierServlet.kt +++ b/server/src/main/java/com/android/identity/wallet/server/VerifierServlet.kt @@ -1,5 +1,6 @@ package com.android.identity.wallet.server +import com.android.identity.asn1.ASN1Integer import com.android.identity.cbor.Cbor import com.android.identity.cbor.CborArray import com.android.identity.cbor.CborMap @@ -13,14 +14,11 @@ import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey import com.android.identity.crypto.EcPublicKey import com.android.identity.crypto.EcPublicKeyDoubleCoordinate +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.X509CertificateCreateOption -import com.android.identity.crypto.X509CertificateExtension -import com.android.identity.crypto.create import com.android.identity.crypto.javaPrivateKey import com.android.identity.crypto.javaPublicKey -import com.android.identity.crypto.javaX509Certificate import com.android.identity.documenttype.DocumentTypeRepository import com.android.identity.documenttype.DocumentWellKnownRequest import com.android.identity.documenttype.knowntypes.DrivingLicense @@ -35,6 +33,7 @@ import com.android.identity.flow.server.FlowEnvironment import com.android.identity.flow.server.Storage import com.android.identity.mdoc.request.DeviceRequestGenerator import com.android.identity.mdoc.response.DeviceResponseParser +import com.android.identity.mdoc.util.MdocUtil import com.android.identity.sdjwt.presentation.SdJwtVerifiablePresentation import com.android.identity.sdjwt.vc.JwtBody import com.android.identity.server.BaseHttpServlet @@ -239,15 +238,6 @@ class VerifierServlet : BaseHttpServlet() { val validFrom = now val validUntil = now.plus(DateTimePeriod(years = 10), TimeZone.currentSystemDefault()) - val extensions = mutableListOf() - extensions.add( - X509CertificateExtension( - Extension.keyUsage.toString(), - true, - KeyUsage(KeyUsage.cRLSign + KeyUsage.keyCertSign).encoded - ) - ) - // Create Reader Root w/ self-signed certificate. // // TODO: Migrate to Curve P-384 once we migrate off com.nimbusds.* which @@ -256,21 +246,12 @@ class VerifierServlet : BaseHttpServlet() { val readerRootKey = Crypto.createEcPrivateKey(EcCurve.P256) val readerRootKeySignatureAlgorithm = Algorithm.ES256 val readerRootKeySubject = "CN=OWF IC Online Verifier Reader Root Key" - val readerRootKeyCertificate = X509Cert.create( - readerRootKey.publicKey, - readerRootKey, - null, - readerRootKeySignatureAlgorithm, - "1", - readerRootKeySubject, - readerRootKeySubject, - validFrom, - validUntil, - setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_AS_SUBJECT_KEY_IDENTIFIER - ), - extensions + val readerRootKeyCertificate = MdocUtil.generateReaderRootCertificate( + readerRootKey = readerRootKey, + subject = X500Name.fromName(readerRootKeySubject), + serial = ASN1Integer(1L), + validFrom = validFrom, + validUntil = validUntil ) return KeyMaterial( @@ -383,24 +364,6 @@ class VerifierServlet : BaseHttpServlet() { val validFrom = now.plus(DateTimePeriod(minutes = -10), TimeZone.currentSystemDefault()) val validUntil = now.plus(DateTimePeriod(minutes = 10), TimeZone.currentSystemDefault()) val readerKey = Crypto.createEcPrivateKey(EcCurve.P256) - - val extensions = mutableListOf() - extensions.add( - X509CertificateExtension( - Extension.keyUsage.toString(), - true, - KeyUsage(KeyUsage.digitalSignature).encoded - ) - ) - extensions.add( - X509CertificateExtension( - Extension.extendedKeyUsage.toString(), - true, - ExtendedKeyUsage( - KeyPurposeId.getInstance(ASN1ObjectIdentifier("1.0.18013.5.1.2")) - ).encoded - ) - ) val readerKeySubject = "CN=OWF IC Online Verifier Single-Use Reader Key" // TODO: for now, instead of using the per-site Reader Root generated at first run, use the @@ -427,53 +390,19 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI= -----END PRIVATE KEY----- """.trimIndent(), owfIcReaderCert.ecPublicKey) - val owfIcReaderRootSignatureAlgorithm = Algorithm.ES384 - val owfIcReaderRootIssuer = owfIcReaderCert.javaX509Certificate.issuerX500Principal.name - - val readerKeyCertificate = X509Cert.create( - readerKey.publicKey, - owfIcReaderRoot, - owfIcReaderCert, - owfIcReaderRootSignatureAlgorithm, - "1", - readerKeySubject, - owfIcReaderRootIssuer, - validFrom, - validUntil, - setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_FROM_SIGNING_KEY_CERTIFICATE - ), - extensions + val readerKeyCertificate = MdocUtil.generateReaderCertificate( + readerRootCert = owfIcReaderCert, + readerRootKey = owfIcReaderRoot, + readerKey = readerKey.publicKey, + subject = X500Name.fromName(readerKeySubject), + serial = ASN1Integer(1L), + validFrom = validFrom, + validUntil = validUntil ) return Pair( readerKey, X509CertChain(listOf(readerKeyCertificate) + owfIcReaderCert) ) - - /* - val readerKeyCertificate = X509Cert.create( - readerKey.publicKey, - keyMaterial.readerRootKey, - keyMaterial.readerRootKeyCertificates.certificates[0], - keyMaterial.readerRootKeySignatureAlgorithm, - "1", - readerKeySubject, - keyMaterial.readerRootKeyIssuer, - validFrom, - validUntil, - setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_FROM_SIGNING_KEY_CERTIFICATE - ), - extensions - ) - return Pair( - readerKey, - X509CertChain(listOf(readerKeyCertificate) + keyMaterial.readerRootKeyCertificates.certificates) - ) - - */ } override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/PresentationActivity.kt b/wallet/src/main/java/com/android/identity_credential/wallet/PresentationActivity.kt index 491986271..e33a9aaa1 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/PresentationActivity.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/PresentationActivity.kt @@ -361,8 +361,7 @@ class PresentationActivity : FragmentActivity() { var trustPoint: TrustPoint? = null if (docRequest.readerAuthenticated) { val result = walletApp.readerTrustManager.verify( - docRequest.readerCertificateChain!!.javaX509Certificates, - customValidators = emptyList() // not needed for reader auth + docRequest.readerCertificateChain!!.certificates, ) if (result.isTrusted && result.trustPoints.isNotEmpty()) { trustPoint = result.trustPoints.first() diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/ReaderModel.kt b/wallet/src/main/java/com/android/identity_credential/wallet/ReaderModel.kt index 34b2eb966..6cbfa02ce 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/ReaderModel.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/ReaderModel.kt @@ -249,10 +249,8 @@ class ReaderModel( val warningTexts = mutableListOf() if (document.issuerSignedAuthenticated) { - val readerAuthChain = document.issuerCertificateChain.javaX509Certificates val trustResult = trustManager.verify( - readerAuthChain, - emptyList() // TODO: use mDL specific validators, if applicable + document.issuerCertificateChain.certificates, ) if (trustResult.isTrusted) { val trustPoint = trustResult.trustPoints[0] @@ -260,8 +258,8 @@ class ReaderModel( ?: trustPoint.certificate.javaX509Certificate.subjectX500Principal.name infoTexts.add(res.getString(R.string.reader_model_info_in_trust_list, displayName)) } else { - val dsCert = readerAuthChain[0] - val displayName = dsCert.issuerX500Principal.name + val dsCert = document.issuerCertificateChain.certificates[0] + val displayName = dsCert.issuer.name warningTexts.add(res.getString(R.string.reader_model_warning_not_in_trust_list, displayName)) } } diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/WalletApplication.kt b/wallet/src/main/java/com/android/identity_credential/wallet/WalletApplication.kt index ca5dc35df..9455291f2 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/WalletApplication.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/WalletApplication.kt @@ -231,7 +231,10 @@ class WalletApplication : Application() { R.raw.austroad_test_event_reader_thales_root, R.raw.austroad_test_event_reader_zetes, )) { - val cert = X509Cert(resources.openRawResource(certResourceId).readBytes()) + val pemEncodedCert = resources.openRawResource(certResourceId).readBytes().decodeToString() + Logger.i(TAG, "PEMEncoded\n$pemEncodedCert") + val cert = X509Cert.fromPem(pemEncodedCert) + Logger.iHex(TAG, "x509cert", cert.encodedCertificate) readerTrustManager.addTrustPoint( TrustPoint( cert, diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/credman/CredmanPresentationActivity.kt b/wallet/src/main/java/com/android/identity_credential/wallet/credman/CredmanPresentationActivity.kt index 99c3e361c..89dea6a0d 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/credman/CredmanPresentationActivity.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/credman/CredmanPresentationActivity.kt @@ -233,8 +233,7 @@ class CredmanPresentationActivity : FragmentActivity() { var trustPoint: TrustPoint? = null if (docRequest.readerAuthenticated) { val result = walletApp.readerTrustManager.verify( - docRequest.readerCertificateChain!!.javaX509Certificates, - customValidators = emptyList() // not needed for reader auth + docRequest.readerCertificateChain!!.certificates, ) if (result.isTrusted && result.trustPoints.isNotEmpty()) { trustPoint = result.trustPoints.first() diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/presentation/OpenID4VPPresentationActivity.kt b/wallet/src/main/java/com/android/identity_credential/wallet/presentation/OpenID4VPPresentationActivity.kt index 856c9ad60..16550151a 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/presentation/OpenID4VPPresentationActivity.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/presentation/OpenID4VPPresentationActivity.kt @@ -59,6 +59,7 @@ import com.android.identity.appsupport.ui.consent.ConsentField import com.android.identity.appsupport.ui.consent.ConsentRelyingParty import com.android.identity.appsupport.ui.consent.MdocConsentField import com.android.identity.appsupport.ui.consent.VcConsentField +import com.android.identity.crypto.javaX509Certificate import com.android.identity.sdjwt.credential.SdJwtVcCredential import com.android.identity_credential.wallet.ui.theme.IdentityCredentialTheme // TODO: replace the nimbusds library usage with non-java-based alternative @@ -148,7 +149,7 @@ internal data class AuthorizationRequest ( var responseUri: String, var state: String?, var clientMetadata: JsonObject, - var certificateChain: List? + var certificateChain: List? ) class NoMatchingDocumentException(message: String): Exception(message) {} @@ -534,15 +535,14 @@ class OpenID4VPPresentationActivity : FragmentActivity() { var trustPoint: TrustPoint? = null if (authorizationRequest.certificateChain != null) { - val trustResult = walletApp.readerTrustManager.verify(authorizationRequest.certificateChain!!) + val trustResult = walletApp.readerTrustManager.verify( + authorizationRequest.certificateChain!! + ) if (!trustResult.isTrusted) { Logger.w(TAG, "Reader root not trusted") if (trustResult.error != null) { Logger.w(TAG, "trustResult.error", trustResult.error!!) } - for (cert in authorizationRequest.certificateChain!!) { - Logger.i(TAG, "${X509Cert(cert.encoded).toPem()}") - } } if (trustResult.isTrusted && trustResult.trustPoints.size > 0) { trustPoint = trustResult.trustPoints[0] @@ -798,7 +798,7 @@ internal fun getAuthRequestFromJwt(signedJWT: SignedJWT, clientId: String?): Aut } Logger.i(TAG, "signedJWT client_id: ${signedJWT.jwtClaimsSet.getStringClaim("client_id")}") val x5c = signedJWT.header?.x509CertChain ?: throw IllegalArgumentException("Error retrieving cert chain") - val pubCertChain = x5c.mapNotNull { runCatching { X509CertUtils.parse(it.decode()) }.getOrNull() } + val pubCertChain = x5c.mapNotNull { runCatching { X509Cert(it.decode()) }.getOrNull() } if (pubCertChain.isEmpty()) { throw IllegalArgumentException("Invalid x5c") } @@ -814,7 +814,7 @@ internal fun getAuthRequestFromJwt(signedJWT: SignedJWT, clientId: String?): Aut JOSEObjectType(""), null, ) - jwsKeySelector = JWSKeySelector { _, _ -> listOf(pubCertChain[0].publicKey) } + jwsKeySelector = JWSKeySelector { _, _ -> listOf(pubCertChain[0].javaX509Certificate.publicKey) } jwtClaimsSetVerifier = TimeChecks() } jwtProcessor.process(signedJWT, null) diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/presentation/TransferHelper.kt b/wallet/src/main/java/com/android/identity_credential/wallet/presentation/TransferHelper.kt index 62f3e79df..342e3a8a0 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/presentation/TransferHelper.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/presentation/TransferHelper.kt @@ -111,8 +111,7 @@ class TransferHelper( var trustPoint: TrustPoint? = null if (docRequest.readerAuthenticated) { val result = trustManager.verify( - docRequest.readerCertificateChain!!.javaX509Certificates, - customValidators = emptyList() // not neeeded for reader auth + docRequest.readerCertificateChain!!.certificates, ) if (result.isTrusted && !result.trustPoints.isEmpty()) { trustPoint = result.trustPoints.first() diff --git a/wallet/src/test/java/com/android/identity_credential/wallet/OpenID4VPTest.kt b/wallet/src/test/java/com/android/identity_credential/wallet/OpenID4VPTest.kt index 492b036bc..174232ae0 100644 --- a/wallet/src/test/java/com/android/identity_credential/wallet/OpenID4VPTest.kt +++ b/wallet/src/test/java/com/android/identity_credential/wallet/OpenID4VPTest.kt @@ -1,19 +1,17 @@ package com.android.identity_credential.wallet -import com.android.identity.crypto.Algorithm +import com.android.identity.asn1.ASN1Integer import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPrivateKey +import com.android.identity.crypto.X500Name import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.X509CertificateCreateOption -import com.android.identity.crypto.X509CertificateExtension -import com.android.identity.crypto.create import com.android.identity.crypto.javaPrivateKey import com.android.identity.crypto.javaPublicKey -import com.android.identity.crypto.javaX509Certificate import com.android.identity.document.DocumentRequest import com.android.identity.issuance.CredentialFormat +import com.android.identity.mdoc.util.MdocUtil import com.android.identity.util.toBase64Url import com.android.identity_credential.wallet.presentation.DescriptorMap import com.android.identity_credential.wallet.presentation.createPresentationSubmission @@ -38,11 +36,6 @@ import kotlinx.datetime.plus import kotlinx.serialization.json.Json.Default.parseToJsonElement import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject -import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.x509.ExtendedKeyUsage -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.KeyPurposeId -import org.bouncycastle.asn1.x509.KeyUsage import org.junit.Assert import org.junit.Test import java.security.interfaces.ECPrivateKey @@ -86,24 +79,6 @@ class OpenID4VPTest { val validFrom = now.plus(DateTimePeriod(minutes = -10), TimeZone.currentSystemDefault()) val validUntil = now.plus(DateTimePeriod(minutes = 10), TimeZone.currentSystemDefault()) val readerKey = Crypto.createEcPrivateKey(EcCurve.P256) - - val extensions = mutableListOf() - extensions.add( - X509CertificateExtension( - Extension.keyUsage.toString(), - true, - KeyUsage(KeyUsage.digitalSignature).encoded - ) - ) - extensions.add( - X509CertificateExtension( - Extension.extendedKeyUsage.toString(), - true, - ExtendedKeyUsage( - KeyPurposeId.getInstance(ASN1ObjectIdentifier("1.0.18013.5.1.2")) - ).encoded - ) - ) val readerKeySubject = "CN=OWF IC Online Verifier Single-Use Reader Key" // TODO: for now, instead of using the per-site Reader Root generated at first run, use the @@ -130,24 +105,14 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI= -----END PRIVATE KEY----- """.trimIndent(), owfIcReaderCert.ecPublicKey) - val owfIcReaderRootSignatureAlgorithm = Algorithm.ES384 - val owfIcReaderRootIssuer = owfIcReaderCert.javaX509Certificate.issuerX500Principal.name - - val readerKeyCertificate = X509Cert.create( - readerKey.publicKey, - owfIcReaderRoot, - owfIcReaderCert, - owfIcReaderRootSignatureAlgorithm, - "1", - readerKeySubject, - owfIcReaderRootIssuer, - validFrom, - validUntil, - setOf( - X509CertificateCreateOption.INCLUDE_SUBJECT_KEY_IDENTIFIER, - X509CertificateCreateOption.INCLUDE_AUTHORITY_KEY_IDENTIFIER_FROM_SIGNING_KEY_CERTIFICATE - ), - extensions + val readerKeyCertificate = MdocUtil.generateReaderCertificate( + readerRootCert = owfIcReaderCert, + readerRootKey = owfIcReaderRoot, + readerKey = readerKey.publicKey, + subject = X500Name.fromName(readerKeySubject), + serial = ASN1Integer(1L), + validFrom = validFrom, + validUntil = validUntil ) return Pair( readerKey,