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,