fix(certgen): self-signed certs for no-challenge keys per AOSP spec
AOSP ta/src/keys.rs:451-478 requires self-signed leaf (depth 1) when no attestation challenge is provided. Both Kotlin and Rust paths now return subject==issuer, signed by generated key, no attestation extension. Adds cert chain trace logging in debug builds.
This commit is contained in:
+11
-1
@@ -314,6 +314,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
|
|
||||||
if (userUpdatedKeys.remove(keyId)) {
|
if (userUpdatedKeys.remove(keyId)) {
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
|
||||||
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
@@ -324,12 +325,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" }
|
||||||
|
|
||||||
if (parsedParameters.isImportKey()) {
|
if (parsedParameters.isImportKey()) {
|
||||||
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||||
if (retainedChain == null) {
|
if (retainedChain == null) {
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
|
||||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
|
||||||
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
||||||
response.metadata.authorizations =
|
response.metadata.authorizations =
|
||||||
@@ -341,6 +346,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
|
||||||
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
|
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
@@ -464,7 +470,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (generatedKeyInfo == null) {
|
if (generatedKeyInfo == null) {
|
||||||
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) }
|
descriptor.alias?.let {
|
||||||
|
val kid = KeyIdentifier(callingUid, it)
|
||||||
|
userUpdatedKeys.add(kid)
|
||||||
|
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
|
||||||
|
}
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
@@ -142,12 +142,14 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
}
|
}
|
||||||
attestationKeys.remove(keyId)
|
attestationKeys.remove(keyId)
|
||||||
importedKeys.add(keyId)
|
importedKeys.add(keyId)
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: added to importedKeys, skipUid=${ConfigurationManager.shouldSkipUid(callingUid)}" }
|
||||||
|
|
||||||
if (!ConfigurationManager.shouldSkipUid(callingUid)) {
|
if (!ConfigurationManager.shouldSkipUid(callingUid)) {
|
||||||
val metadata: KeyMetadata =
|
val metadata: KeyMetadata =
|
||||||
reply.readTypedObject(KeyMetadata.CREATOR)
|
reply.readTypedObject(KeyMetadata.CREATOR)
|
||||||
?: return TransactionResult.SkipTransaction
|
?: return TransactionResult.SkipTransaction
|
||||||
val originalChain = CertificateHelper.getCertificateChain(metadata)
|
val originalChain = CertificateHelper.getCertificateChain(metadata)
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: chainSize=${originalChain?.size ?: 0}" }
|
||||||
if (originalChain != null && originalChain.size > 1) {
|
if (originalChain != null && originalChain.size > 1) {
|
||||||
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||||
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
|
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
|
||||||
@@ -158,6 +160,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
this.metadata = metadata
|
this.metadata = metadata
|
||||||
iSecurityLevel = original
|
iSecurityLevel = original
|
||||||
}
|
}
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: PATCHED chain (chainSize=${newChain.size})" }
|
||||||
SystemLogger.debug("Cached patched certificate chain for imported key $keyId.")
|
SystemLogger.debug("Cached patched certificate chain for imported key $keyId.")
|
||||||
return InterceptorUtils.createTypedObjectReply(metadata)
|
return InterceptorUtils.createTypedObjectReply(metadata)
|
||||||
}
|
}
|
||||||
@@ -420,6 +423,11 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||||
val parsedParams = KeyMintAttestation(params)
|
val parsedParams = KeyMintAttestation(params)
|
||||||
|
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] generateKey alias=${keyDescriptor.alias} algo=${parsedParams.algorithm} challenge=${parsedParams.attestationChallenge?.size ?: "null"} serial=${parsedParams.serial != null} imei=${parsedParams.imei != null} noAuth=${parsedParams.noAuthRequired} purposes=${parsedParams.purpose}" }
|
||||||
|
if (SystemLogger.isDebugBuild) params.forEach { p ->
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] tag=${p.tag} value=${p.value}" }
|
||||||
|
}
|
||||||
|
|
||||||
val challenge = parsedParams.attestationChallenge
|
val challenge = parsedParams.attestationChallenge
|
||||||
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) {
|
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) {
|
||||||
SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})")
|
SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})")
|
||||||
@@ -487,6 +495,8 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
|
|
||||||
if (isAuto) SystemLogger.debug("AUTO dispatch: teePathDecision=${teePathDecision.get()} for ${keyDescriptor.alias}")
|
if (isAuto) SystemLogger.debug("AUTO dispatch: teePathDecision=${teePathDecision.get()} for ${keyDescriptor.alias}")
|
||||||
|
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] dispatch: forceGen=$forceGenerate isAuto=$isAuto teePath=${teePathDecision.get()} hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" }
|
||||||
|
|
||||||
when {
|
when {
|
||||||
forceGenerate -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
forceGenerate -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
||||||
isAuto && teePathDecision.get() == null -> raceTeePatch(callingUid, keyDescriptor, attestationKey, params, parsedParams, keyId, isAttestKeyRequest)
|
isAuto && teePathDecision.get() == null -> raceTeePatch(callingUid, keyDescriptor, attestationKey, params, parsedParams, keyId, isAttestKeyRequest)
|
||||||
@@ -578,6 +588,17 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams)
|
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams)
|
||||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
||||||
|
|
||||||
|
if (SystemLogger.isDebugBuild) {
|
||||||
|
val chain = keyData.second
|
||||||
|
val leaf = chain.firstOrNull() as? java.security.cert.X509Certificate
|
||||||
|
SystemLogger.trace {
|
||||||
|
"[certchain] ${keyDescriptor.alias}: depth=${chain.size} " +
|
||||||
|
"issuer=${leaf?.issuerX500Principal?.name} " +
|
||||||
|
"subject=${leaf?.subjectX500Principal?.name} " +
|
||||||
|
"hasAttest=${leaf?.getExtensionValue("1.3.6.1.4.1.11129.2.1.17") != null}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val certChainCopy = keyData.second.toList()
|
val certChainCopy = keyData.second.toList()
|
||||||
persistExecutor.execute {
|
persistExecutor.execute {
|
||||||
GeneratedKeyPersistence.save(
|
GeneratedKeyPersistence.save(
|
||||||
|
|||||||
@@ -117,4 +117,9 @@ object SystemLogger {
|
|||||||
if (!acquireLogPermit()) return
|
if (!acquireLogPermit()) return
|
||||||
Log.v(TAG, message())
|
Log.v(TAG, message())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun trace(message: () -> String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
|
Log.w(TAG, message())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ object CertificateGenerator {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
|
||||||
|
if (challenge == null && attestKeyAlias == null) {
|
||||||
|
SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" }
|
||||||
|
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
|
||||||
|
}
|
||||||
|
|
||||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||||
|
|
||||||
val (signingKey, issuer) =
|
val (signingKey, issuer) =
|
||||||
@@ -257,4 +263,39 @@ object CertificateGenerator {
|
|||||||
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AOSP ta/src/keys.rs:452-478, ta/src/cert.rs:111-114
|
||||||
|
private fun buildSelfSignedCertificate(
|
||||||
|
keyPair: KeyPair,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
): Certificate {
|
||||||
|
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||||
|
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||||
|
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||||
|
|
||||||
|
val builder = JcaX509v3CertificateBuilder(
|
||||||
|
subject,
|
||||||
|
params.certificateSerial ?: BigInteger.ONE,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
subject,
|
||||||
|
keyPair.public,
|
||||||
|
)
|
||||||
|
|
||||||
|
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
|
||||||
|
if (keyUsageBits != 0) {
|
||||||
|
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||||
|
}
|
||||||
|
|
||||||
|
val signerAlgorithm = when (keyPair.private.algorithm) {
|
||||||
|
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||||
|
"RSA" -> "SHA256withRSA"
|
||||||
|
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
|
||||||
|
}
|
||||||
|
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(keyPair.private)
|
||||||
|
|
||||||
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,66 @@ const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
|
|||||||
// Extension OIDs
|
// Extension OIDs
|
||||||
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
|
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
|
||||||
|
|
||||||
|
// AOSP ta/src/keys.rs:451-478: no challenge = self-signed leaf, chain depth 1
|
||||||
|
pub fn build_self_signed_cert(
|
||||||
|
key_pair: &GeneratedKeyPair,
|
||||||
|
params: &CertGenParams,
|
||||||
|
) -> Result<Vec<Vec<u8>>> {
|
||||||
|
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
|
||||||
|
let sig_alg_der = signature_algorithm_for_signing_key(&key_pair.private_key_pkcs8, params.algorithm)?;
|
||||||
|
|
||||||
|
let serial_bytes = if let Some(ref serial) = params.cert_serial {
|
||||||
|
serial.clone()
|
||||||
|
} else {
|
||||||
|
vec![1u8]
|
||||||
|
};
|
||||||
|
|
||||||
|
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
||||||
|
subject.clone()
|
||||||
|
} else {
|
||||||
|
encode_simple_cn_dn("Android Keystore Key")
|
||||||
|
};
|
||||||
|
|
||||||
|
let not_before = timestamp_to_datetime(params.cert_not_before)?;
|
||||||
|
let not_after = if params.cert_not_after == -1 {
|
||||||
|
// No keybox fallback available; use far-future (year 9999)
|
||||||
|
OffsetDateTime::from_unix_timestamp(253402300799)
|
||||||
|
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365 * 30))
|
||||||
|
} else {
|
||||||
|
timestamp_to_datetime(params.cert_not_after)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let extensions_der = build_extensions(None, ¶ms.purposes)?;
|
||||||
|
|
||||||
|
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
|
||||||
|
let serial_der = encode_der_integer(&serial_bytes);
|
||||||
|
let validity_der = encode_validity(¬_before, ¬_after);
|
||||||
|
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
|
||||||
|
|
||||||
|
// issuer == subject (self-signed, per AOSP ta/src/cert.rs:111-114)
|
||||||
|
let tbs_der = encode_der_sequence(&[
|
||||||
|
&version_der,
|
||||||
|
&serial_der,
|
||||||
|
&sig_alg_der,
|
||||||
|
&subject_dn_der,
|
||||||
|
&validity_der,
|
||||||
|
&subject_dn_der,
|
||||||
|
&spki_der,
|
||||||
|
&extensions_tagged,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let signature_bytes = sign_tbs(&tbs_der, &key_pair.private_key_pkcs8, params.algorithm)?;
|
||||||
|
let signature_bit_string = encode_der_bit_string(&signature_bytes);
|
||||||
|
|
||||||
|
let cert_der = encode_der_sequence(&[
|
||||||
|
&tbs_der,
|
||||||
|
&sig_alg_der,
|
||||||
|
&signature_bit_string,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Ok(vec![cert_der])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_certificate_chain(
|
pub fn build_certificate_chain(
|
||||||
key_pair: &GeneratedKeyPair,
|
key_pair: &GeneratedKeyPair,
|
||||||
attestation_ext_der: Option<&[u8]>,
|
attestation_ext_der: Option<&[u8]>,
|
||||||
|
|||||||
@@ -62,18 +62,14 @@ fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteAr
|
|||||||
|
|
||||||
let keybox = keybox::parse_keybox(¶ms.keybox_cert_chain, ¶ms.keybox_private_key)?;
|
let keybox = keybox::parse_keybox(¶ms.keybox_cert_chain, ¶ms.keybox_private_key)?;
|
||||||
|
|
||||||
let attest_ext = match params.attestation_challenge {
|
let cert_chain = if params.attestation_challenge.is_some() {
|
||||||
Some(_) => Some(attestation::build_attestation_extension(¶ms)?),
|
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||||
None => None,
|
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, ¶ms)?
|
||||||
|
} else {
|
||||||
|
tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
|
||||||
|
certbuilder::build_self_signed_cert(&key_pair, ¶ms)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let cert_chain = certbuilder::build_certificate_chain(
|
|
||||||
&key_pair,
|
|
||||||
attest_ext.as_deref(),
|
|
||||||
&keybox,
|
|
||||||
¶ms,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||||
|
|
||||||
let out = env.byte_array_from_slice(&blob)?;
|
let out = env.byte_array_from_slice(&blob)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user