Skip to content

Commit

Permalink
Key attestation support for cloud secure area keys.
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Sorotokin <[email protected]>
  • Loading branch information
sorotokin committed Nov 13, 2024
1 parent 4c31c5b commit aa6eec9
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package com.android.identity.issuance

import com.android.identity.crypto.X509Cert
import com.android.identity.crypto.X509CertChain
import com.android.identity.crypto.javaX509Certificate
import com.android.identity.crypto.javaX509Certificates
import com.android.identity.issuance.evidence.EvidenceRequestSetupCloudSecureArea
import com.android.identity.securearea.AttestationExtension
import com.android.identity.util.AndroidAttestationExtensionParser
import com.android.identity.util.Logger
import com.android.identity.util.fromHex
import com.android.identity.util.toBase64Url
import com.android.identity.util.toHex
import kotlinx.io.bytestring.ByteString
import kotlinx.io.bytestring.ByteStringBuilder
import kotlinx.io.bytestring.append
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.ASN1OctetString
import org.bouncycastle.asn1.ASN1Sequence
import java.security.cert.X509Certificate

private val salt = byteArrayOf((0xe7).toByte(), 0x7c, (0xf8).toByte(), (0xec).toByte())

Expand Down Expand Up @@ -106,3 +111,48 @@ fun validateKeyAttestation(
private val GOOGLE_ROOT_ATTESTATION_KEY =
"30820222300d06092a864886f70d01010105000382020f003082020a0282020100afb6c7822bb1a701ec2bb42e8bcc541663abef982f32c77f7531030c97524b1b5fe809fbc72aa9451f743cbd9a6f1335744aa55e77f6b6ac3535ee17c25e639517dd9c92e6374a53cbfe258f8ffbb6fd129378a22a4ca99c452d47a59f3201f44197ca1ccd7e762fb2f53151b6feb2fffd2b6fe4fe5bc6bd9ec34bfe08239daafceb8eb5a8ed2b3acd9c5e3a7790e1b51442793159859811ad9eb2a96bbdd7a57c93a91c41fccd27d67fd6f671aa0b815261ad384fa37944864604ddb3d8c4f920a19b1656c2f14ad6d03c56ec060899041c1ed1a5fe6d3440b556bad1d0a152589c53e55d370762f0122eef91861b1b0e6c4c80927499c0e9bec0b83e3bc1f93c72c049604bbd2f1345e62c3f8e26dbec06c94766f3c128239d4f4312fad8123887e06becf567583bf8355a81feeabaf99a83c8df3e2a322afc672bf120b135158b6821ceaf309b6eee77f98833b018daa10e451f06a374d50781f359082966bb778b9308942698e74e0bcd24628a01c2cc03e51f0b3e5b4ac1e4df9eaf9ff6a492a77c1483882885015b422ce67b80b88c9b48e13b607ab545c723ff8c44f8f2d368b9f6520d31145ebf9e862ad71df6a3bfd2450959d653740d97a12f368b13ef66d5d0a54a6e2f5d9a6fef446832bc67844725861f093dd0e6f3405da89643ef0f4d69b6420051fdb93049673e36950580d3cdf4fbd08bc58483952600630203010001"
.fromHex()


fun isCloudKeyAttestation(chain: X509CertChain): Boolean {
return chain.certificates[0].javaX509Certificate
.getExtensionValue(AttestationExtension.ATTESTATION_OID) != null
}

fun validateCloudKeyAttestation(
chain: X509CertChain,
nonce: String,
trustedRootKeys: Set<ByteString>
) {
val x509Certs = chain.certificates
for (n in 0 until x509Certs.size - 1) {
val cert = x509Certs[n]
val nextCert = x509Certs[n + 1]
try {
cert.verify(nextCert)
} catch (e: Exception) {
throw IllegalStateException("Error verifying attestation chain")
}
}

val leafX509Cert = x509Certs.first().javaX509Certificate
val extensionDerEncodedString = leafX509Cert.getExtensionValue(AttestationExtension.ATTESTATION_OID)
?: throw IllegalStateException(
"No attestation extension at OID ${AttestationExtension.ATTESTATION_OID}")

val attestationExtension = try {
val asn1InputStream = ASN1InputStream(extensionDerEncodedString);
(asn1InputStream.readObject() as ASN1OctetString).octets
} catch (e: Exception) {
throw IllegalStateException("Error decoding attestation extension", e)
}

val challengeInAttestation = AttestationExtension.decode(attestationExtension)
if (!challengeInAttestation.contentEquals(nonce.toByteArray())) {
throw IllegalStateException("Challenge in attestation does match expected attestation")
}

val rootPublicKey = ByteString(x509Certs.last().javaX509Certificate.publicKey.encoded)
check(trustedRootKeys.contains(rootPublicKey)) {
"Unexpected attestation root:"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.android.identity.crypto.Crypto
import com.android.identity.crypto.EcPrivateKey
import com.android.identity.crypto.EcPublicKey
import com.android.identity.crypto.X509Cert
import com.android.identity.crypto.javaX509Certificate
import com.android.identity.flow.annotation.FlowMethod
import com.android.identity.flow.annotation.FlowState
import com.android.identity.flow.server.Configuration
Expand All @@ -15,6 +16,8 @@ import com.android.identity.issuance.LandingUrlUnknownException
import com.android.identity.issuance.WalletServerSettings
import com.android.identity.issuance.common.cache
import com.android.identity.issuance.funke.toJson
import com.android.identity.issuance.isCloudKeyAttestation
import com.android.identity.issuance.validateCloudKeyAttestation
import com.android.identity.issuance.validateKeyAttestation
import com.android.identity.securearea.KeyAttestation
import com.android.identity.util.Logger
Expand Down Expand Up @@ -108,13 +111,22 @@ class ApplicationSupportState(
val keyList = keyAttestations.map { attestation ->
// TODO: ensure that keys come from the same device and extract data for key_type
// and user_authentication values
validateKeyAttestation(
attestation.certChain!!,
nonce,
settings.androidRequireGmsAttestation,
settings.androidRequireVerifiedBootGreen,
settings.androidRequireAppSignatureCertificateDigests
)
if (isCloudKeyAttestation(attestation.certChain!!)) {
val trustedRootKeys = getCloudSecureAreaTrustedRootKeys(env)
validateCloudKeyAttestation(
attestation.certChain!!,
nonce,
trustedRootKeys.trustedKeys
)
} else {
validateKeyAttestation(
attestation.certChain!!,
nonce,
settings.androidRequireGmsAttestation,
settings.androidRequireVerifiedBootGreen,
settings.androidRequireAppSignatureCertificateDigests
)
}
attestation.publicKey.toJson(null)
}

Expand Down Expand Up @@ -242,9 +254,26 @@ class ApplicationSupportState(
return "$message.$signature"
}

private suspend fun getCloudSecureAreaTrustedRootKeys(
env: FlowEnvironment
): CloudSecureAreaTrustedRootKeys {
return env.cache(CloudSecureAreaTrustedRootKeys::class) { configuration, resources ->
val certificateName = configuration.getValue("csa.certificate")
?: "cloud_secure_area/certificate.pem"
val certificate = X509Cert.fromPem(resources.getStringResource(certificateName)!!)
CloudSecureAreaTrustedRootKeys(
trustedKeys = setOf(ByteString(certificate.javaX509Certificate.publicKey.encoded))
)
}
}

internal data class AttestationData(
val certificate: X509Cert,
val privateKey: EcPrivateKey,
val clientId: String
)

internal data class CloudSecureAreaTrustedRootKeys(
val trustedKeys: Set<ByteString>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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
import com.android.identity.flow.server.Resources
import com.android.identity.flow.server.Storage
import com.android.identity.issuance.WalletServerSettings
import com.android.identity.securearea.cloud.CloudSecureAreaServer
Expand Down Expand Up @@ -80,25 +81,38 @@ class CloudSecureAreaServlet : BaseHttpServlet() {
)
}

fun createKeyMaterial(): KeyMaterial {
fun createKeyMaterial(serverEnvironment: FlowEnvironment): KeyMaterial {
val serverSecureAreaBoundKey = Random.Default.nextBytes(32)

val now = Clock.System.now()
val validFrom = now
val validUntil = now.plus(DateTimePeriod(years = 10), TimeZone.currentSystemDefault())

// Create Attestation Root w/ self-signed certificate.
// Load attestation root
val configuration = serverEnvironment.getInterface(Configuration::class)!!
val resources = serverEnvironment.getInterface(Resources::class)!!
val certificateName = configuration.getValue("csa.certificate")
?: "cloud_secure_area/certificate.pem"
val rootCertificate = X509Cert.fromPem(resources.getStringResource(certificateName)!!)
val privateKeyName = configuration.getValue("csa.privateKey")
?: "cloud_secure_area/private_key.pem"
val rootPrivateKey = EcPrivateKey.fromPem(
resources.getStringResource(privateKeyName)!!,
rootCertificate.ecPublicKey
)

// Create instance-specific intermediate certificate.
val attestationKey = Crypto.createEcPrivateKey(EcCurve.P256)
val attestationKeySignatureAlgorithm = Algorithm.ES256
val attestationKeySubject = "CN=Cloud Secure Area Attestation Root"
val attestationKeyCertificate = X509Cert.create(
attestationKey.publicKey,
attestationKey,
rootPrivateKey,
null,
attestationKeySignatureAlgorithm,
"1",
attestationKeySubject,
attestationKeySubject,
rootCertificate.javaX509Certificate.issuerX500Principal.name,
validFrom,
validUntil,
setOf(),
Expand Down Expand Up @@ -126,7 +140,7 @@ class CloudSecureAreaServlet : BaseHttpServlet() {
return KeyMaterial(
serverSecureAreaBoundKey,
attestationKey,
X509CertChain(listOf(attestationKeyCertificate)),
X509CertChain(listOf(attestationKeyCertificate, rootCertificate)),
attestationKeySignatureAlgorithm,
attestationKeySubject,
cloudBindingKeyAttestationKey,
Expand All @@ -150,7 +164,7 @@ class CloudSecureAreaServlet : BaseHttpServlet() {
val keyMaterialBlob = runBlocking {
storage.get("RootState", "", "cloudSecureAreaKeyMaterial")?.toByteArray()
?: let {
val blob = KeyMaterial.createKeyMaterial().toCbor()
val blob = KeyMaterial.createKeyMaterial(serverEnvironment).toCbor()
storage.insert(
"RootState",
"",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBUzCB+qADAgECAgkAjXAZuiLhFG8wCgYIKoZIzj0EAwIwFzEVMBMGA1UEAwwM
Y3NhX2Rldl9yb290MB4XDTI0MTExMzAwMDcwM1oXDTM0MTEyMTAwMDcwM1owFzEV
MBMGA1UEAwwMY3NhX2Rldl9yb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
SwFeYEYGbPrayIwwCFeOgNkgfuJZViqnX0GHAyJ2aAtA4Pm88Txy4gKWkIeAw12v
JEfq75Y4WVBTU3mDppPTxKMvMC0wHQYDVR0OBBYEFEqc1iDkhWpfhozT8rxG49A6
ClfbMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgM75KRzV6I4gARk1I
BcCX7n+0r2OEXWfF67N0lHcKNjYCIQDQssA1bu0juNn+GXQNmc0CVhdSAF2JbRc+
71j/NYLI4w==
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PRIVATE KEY-----
MEACAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJjAkAgEBBB9Uhppta7eVZr6gnN4T
92WEy2+QoQQy/JdDT/MUlXL/
-----END PRIVATE KEY-----

0 comments on commit aa6eec9

Please sign in to comment.