Skip to content

Commit dcd2197

Browse files
carmenyhcopybara-github
authored andcommitted
Add mechanism for adding debug logging.
PiperOrigin-RevId: 809000598
1 parent 6880dd2 commit dcd2197

File tree

8 files changed

+255
-59
lines changed

8 files changed

+255
-59
lines changed

src/main/kotlin/Extension.kt

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,14 @@ data class ProvisioningInfoMap(
9797
}
9898

9999
data class DeviceIdentity(
100-
val brand: String?,
101-
val device: String?,
102-
val product: String?,
103-
val serialNumber: String?,
104-
val imeis: Set<String>,
105-
val meid: String?,
106-
val manufacturer: String?,
107-
val model: String?,
100+
val brand: String? = null,
101+
val device: String? = null,
102+
val product: String? = null,
103+
val serialNumber: String? = null,
104+
val imeis: Set<String> = emptySet(),
105+
val meid: String? = null,
106+
val manufacturer: String? = null,
107+
val model: String? = null,
108108
) {
109109
companion object {
110110
@JvmStatic
@@ -160,23 +160,25 @@ data class KeyDescription(
160160
@JvmField val OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17")
161161

162162
@JvmStatic
163-
fun parseFrom(cert: X509Certificate) =
163+
@JvmOverloads
164+
fun parseFrom(cert: X509Certificate, logFn: (String) -> Unit = {}) =
164165
cert
165166
.getExtensionValue(OID.id)
166167
.let { ASN1OctetString.getInstance(it).octets }
167-
.let { parseFrom(it) }
168+
.let { parseFrom(it, logFn) }
168169

169170
@JvmStatic
170-
fun parseFrom(bytes: ByteArray) =
171+
@JvmOverloads
172+
fun parseFrom(bytes: ByteArray, logFn: (String) -> Unit = {}) =
171173
try {
172-
from(ASN1Sequence.getInstance(bytes))
174+
from(ASN1Sequence.getInstance(bytes), logFn)
173175
} catch (e: NullPointerException) {
174176
// Workaround for a NPE in BouncyCastle.
175177
// https://github.com/bcgit/bc-java/blob/228211ecb973fe87fdd0fc4ab16ba0446ec1a29c/core/src/main/java/org/bouncycastle/asn1/ASN1UniversalType.java#L24
176178
throw IllegalArgumentException(e)
177179
}
178180

179-
private fun from(seq: ASN1Sequence): KeyDescription {
181+
private fun from(seq: ASN1Sequence, logFn: (String) -> Unit = {}): KeyDescription {
180182
require(seq.size() == 8)
181183
return KeyDescription(
182184
attestationVersion = seq.getObjectAt(0).toInt(),
@@ -185,8 +187,8 @@ data class KeyDescription(
185187
keyMintSecurityLevel = seq.getObjectAt(3).toSecurityLevel(),
186188
attestationChallenge = seq.getObjectAt(4).toByteString(),
187189
uniqueId = seq.getObjectAt(5).toByteString(),
188-
softwareEnforced = seq.getObjectAt(6).toAuthorizationList(),
189-
hardwareEnforced = seq.getObjectAt(7).toAuthorizationList(),
190+
softwareEnforced = seq.getObjectAt(6).toAuthorizationList(logFn),
191+
hardwareEnforced = seq.getObjectAt(7).toAuthorizationList(logFn),
190192
)
191193
}
192194
}
@@ -218,7 +220,7 @@ enum class Origin(val value: Long) {
218220
RESERVED(3),
219221
SECURELY_IMPORTED(4);
220222

221-
internal fun toAsn1() = ASN1Integer(value)
223+
fun toAsn1() = ASN1Integer(value)
222224
}
223225

224226
/**
@@ -397,7 +399,7 @@ data class AuthorizationList(
397399
.let { DERSequence(it.toTypedArray()) }
398400

399401
internal companion object {
400-
fun from(seq: ASN1Sequence, validateTagOrder: Boolean = false): AuthorizationList {
402+
fun from(seq: ASN1Sequence, logFn: (String) -> Unit = { _ -> }): AuthorizationList {
401403
val objects =
402404
seq.associate {
403405
require(it is ASN1TaggedObject) {
@@ -417,9 +419,8 @@ data class AuthorizationList(
417419
* 2. within each class of tags, the elements or alternatives shall appear in ascending order
418420
* of their tag numbers.
419421
*/
420-
// TODO: b/356172932 - Add test data once an example certificate is found in the wild.
421-
if (validateTagOrder && !objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs }) {
422-
throw IllegalArgumentException("AuthorizationList tags must appear in ascending order")
422+
if (!objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs }) {
423+
logFn("AuthorizationList tags should appear in ascending order")
423424
}
424425

425426
return AuthorizationList(
@@ -449,7 +450,7 @@ data class AuthorizationList(
449450
rollbackResistant = if (objects.containsKey(KeyMintTag.ROLLBACK_RESISTANT)) true else null,
450451
rootOfTrust = objects[KeyMintTag.ROOT_OF_TRUST]?.toRootOfTrust(),
451452
osVersion = objects[KeyMintTag.OS_VERSION]?.toInt(),
452-
osPatchLevel = objects[KeyMintTag.OS_PATCH_LEVEL]?.toPatchLevel(),
453+
osPatchLevel = objects[KeyMintTag.OS_PATCH_LEVEL]?.toPatchLevel("OS", logFn),
453454
attestationApplicationId =
454455
objects[KeyMintTag.ATTESTATION_APPLICATION_ID]?.toAttestationApplicationId(),
455456
attestationIdBrand = objects[KeyMintTag.ATTESTATION_ID_BRAND]?.toStr(),
@@ -460,8 +461,8 @@ data class AuthorizationList(
460461
attestationIdMeid = objects[KeyMintTag.ATTESTATION_ID_MEID]?.toStr(),
461462
attestationIdManufacturer = objects[KeyMintTag.ATTESTATION_ID_MANUFACTURER]?.toStr(),
462463
attestationIdModel = objects[KeyMintTag.ATTESTATION_ID_MODEL]?.toStr(),
463-
vendorPatchLevel = objects[KeyMintTag.VENDOR_PATCH_LEVEL]?.toPatchLevel(),
464-
bootPatchLevel = objects[KeyMintTag.BOOT_PATCH_LEVEL]?.toPatchLevel(),
464+
vendorPatchLevel = objects[KeyMintTag.VENDOR_PATCH_LEVEL]?.toPatchLevel("vendor", logFn),
465+
bootPatchLevel = objects[KeyMintTag.BOOT_PATCH_LEVEL]?.toPatchLevel("boot", logFn),
465466
attestationIdSecondImei = objects[KeyMintTag.ATTESTATION_ID_SECOND_IMEI]?.toStr(),
466467
moduleHash = objects[KeyMintTag.MODULE_HASH]?.toByteString(),
467468
)
@@ -480,14 +481,24 @@ data class PatchLevel(val yearMonth: YearMonth, val version: Int? = null) {
480481
}
481482

482483
companion object {
483-
fun from(patchLevel: ASN1Encodable): PatchLevel? {
484+
fun from(
485+
patchLevel: ASN1Encodable,
486+
partitionName: String = "",
487+
logFn: (String) -> Unit = { _ -> },
488+
): PatchLevel? {
484489
check(patchLevel is ASN1Integer) { "Must be an ASN1Integer, was ${this::class.simpleName}" }
485-
return from(patchLevel.value.toString())
490+
return from(patchLevel.value.toString(), partitionName, logFn)
486491
}
487492

488493
@JvmStatic
489-
fun from(patchLevel: String): PatchLevel? {
494+
@JvmOverloads
495+
fun from(
496+
patchLevel: String,
497+
partitionName: String = "",
498+
logFn: (String) -> Unit = { _ -> },
499+
): PatchLevel? {
490500
if (patchLevel.length != 6 && patchLevel.length != 8) {
501+
logFn("Invalid $partitionName patch level: $patchLevel")
491502
return null
492503
}
493504
try {
@@ -496,6 +507,7 @@ data class PatchLevel(val yearMonth: YearMonth, val version: Int? = null) {
496507
val version = if (patchLevel.length == 8) patchLevel.substring(6).toInt() else null
497508
return PatchLevel(yearMonth, version)
498509
} catch (e: DateTimeParseException) {
510+
logFn("Invalid $partitionName patch level: $patchLevel")
499511
return null
500512
}
501513
}
@@ -625,12 +637,9 @@ private fun ASN1Encodable.toAttestationApplicationId(): AttestationApplicationId
625637
return AttestationApplicationId.from(ASN1Sequence.getInstance(this.octets))
626638
}
627639

628-
// TODO: b/356172932 - `validateTagOrder` should default to true after making it user configurable.
629-
private fun ASN1Encodable.toAuthorizationList(
630-
validateTagOrder: Boolean = false
631-
): AuthorizationList {
640+
private fun ASN1Encodable.toAuthorizationList(logFn: (String) -> Unit): AuthorizationList {
632641
check(this is ASN1Sequence) { "Object must be an ASN1Sequence, was ${this::class.simpleName}" }
633-
return AuthorizationList.from(this, validateTagOrder)
642+
return AuthorizationList.from(this, logFn)
634643
}
635644

636645
private fun ASN1Encodable.toBoolean(): Boolean {
@@ -657,7 +666,10 @@ private fun ASN1Encodable.toInt(): BigInteger {
657666
return this.value
658667
}
659668

660-
private fun ASN1Encodable.toPatchLevel(): PatchLevel? = PatchLevel.from(this)
669+
private fun ASN1Encodable.toPatchLevel(
670+
partitionName: String = "",
671+
logFn: (String) -> Unit = { _ -> },
672+
): PatchLevel? = PatchLevel.from(this, partitionName, logFn)
661673

662674
private fun ASN1Encodable.toRootOfTrust(): RootOfTrust {
663675
check(this is ASN1Sequence) { "Object must be an ASN1Sequence, was ${this::class.simpleName}" }

src/main/kotlin/Verifier.kt

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.android.keyattestation.verifier.provider.ProvisioningMethod
2222
import com.android.keyattestation.verifier.provider.RevocationChecker
2323
import com.google.errorprone.annotations.ThreadSafe
2424
import com.google.protobuf.ByteString
25+
import com.google.protobuf.kotlin.toByteString
2526
import java.security.PublicKey
2627
import java.security.Security
2728
import java.security.cert.CertPathValidator
@@ -47,21 +48,71 @@ sealed interface VerificationResult {
4748

4849
data class PathValidationFailure(val cause: CertPathValidatorException) : VerificationResult
4950

50-
data object ChainParsingFailure : VerificationResult
51+
data class ChainParsingFailure(val cause: Exception) : VerificationResult
5152

5253
data class ExtensionParsingFailure(val cause: Exception) : VerificationResult
5354

5455
data class ExtensionConstraintViolation(val cause: String) : VerificationResult
5556
}
5657

58+
/** Interface for logging info level key attestation events and information. */
59+
interface LogHook {
60+
61+
/**
62+
* Logs the certificate chain which is being verified. Called for each call to [verify].
63+
*
64+
* @param inputChain The certificate chain which is being verified.
65+
*/
66+
fun logInputChain(inputChain: List<ByteString>)
67+
68+
/**
69+
* Logs the result of the verification. Called for each call to [verify].
70+
*
71+
* @param result The result of the verification.
72+
*/
73+
fun logResult(result: VerificationResult)
74+
75+
/**
76+
* Logs the key description of the leaf certificate. Called if [verify] reaches the point where
77+
* the key description is parsed.
78+
*
79+
* @param keyDescription The key description of the leaf certificate.
80+
*/
81+
fun logKeyDescription(keyDescription: KeyDescription)
82+
83+
/**
84+
* Logs the provisioning info map extension of the attestation certificate. Called if [verify]
85+
* reaches the point where the provisioning info map is parsed, if present in the attestation
86+
* certificate.
87+
*
88+
* @param provisioningInfoMap The provisioning info map extension of the leaf certificate.
89+
*/
90+
fun logProvisioningInfoMap(provisioningInfoMap: ProvisioningInfoMap)
91+
92+
/**
93+
* Logs the serial numbers of the intermediate certificates in the certificate chain. Called if
94+
* [verify] reaches the point where the certificate chain is parsed.
95+
*
96+
* @param certSerialNumbers The serial numbers of the intermediate certificates in the certificate
97+
* chain.
98+
*/
99+
fun logCertSerialNumbers(certSerialNumbers: List<String>)
100+
101+
/**
102+
* Logs an info level message. May be called throughout the verification process.
103+
*
104+
* @param infoMessage The info level message to log.
105+
*/
106+
fun logInfoMessage(infoMessage: String)
107+
}
108+
57109
/**
58110
* Verifier for Android Key Attestation certificate chain.
59111
*
60112
* https://developer.android.com/privacy-and-security/security-key-attestation
61113
*
62114
* @param anchor a [TrustAnchor] to use for certificate path verification.
63115
*/
64-
// TODO: b/356234568 - Verify intermediate certificate revocation status.
65116
@ThreadSafe
66117
open class Verifier(
67118
private val trustAnchorsSource: () -> Set<TrustAnchor>,
@@ -83,14 +134,20 @@ open class Verifier(
83134
fun verify(
84135
chain: List<X509Certificate>,
85136
challengeChecker: ChallengeChecker? = null,
137+
log: LogHook? = null,
86138
): VerificationResult {
87139
val certPath =
88140
try {
89141
KeyAttestationCertPath(chain)
90142
} catch (e: Exception) {
91-
return VerificationResult.ChainParsingFailure
143+
val result = VerificationResult.ChainParsingFailure(e)
144+
log?.logInputChain(chain.map { it.getEncoded().toByteString() })
145+
log?.logResult(result)
146+
return result
92147
}
93-
return verify(certPath, challengeChecker)
148+
val result = internalVerify(certPath, challengeChecker, log)
149+
log?.logResult(result)
150+
return result
94151
}
95152

96153
/**
@@ -99,21 +156,44 @@ open class Verifier(
99156
* @param chain The attestation certificate chain to verify.
100157
* @param challengeChecker The challenge checker to use for additional validation of the challenge
101158
* in the attestation chain.
102-
* @return [VerificationResult]
103-
*
104-
* TODO: b/366058500 - Make the challenge required after Apparat's changes are rollback safe.
159+
* @return [VerificationEvent]
105160
*/
106161
@JvmOverloads
107162
fun verify(
108163
certPath: KeyAttestationCertPath,
109164
challengeChecker: ChallengeChecker? = null,
165+
log: LogHook? = null,
166+
): VerificationResult {
167+
val result = internalVerify(certPath, challengeChecker, log)
168+
log?.logResult(result)
169+
return result
170+
}
171+
172+
internal fun internalVerify(
173+
certPath: KeyAttestationCertPath,
174+
challengeChecker: ChallengeChecker? = null,
175+
log: LogHook? = null,
110176
): VerificationResult {
177+
log?.logInputChain(certPath.certificatesWithAnchor.map { it.getEncoded().toByteString() })
178+
log?.logCertSerialNumbers(
179+
certPath.certificatesWithAnchor.subList(1, certPath.certificatesWithAnchor.size).map {
180+
it.serialNumber.toString(16)
181+
}
182+
)
111183
val certPathValidator = CertPathValidator.getInstance("KeyAttestation")
112184
val certPathParameters =
113185
PKIXParameters(trustAnchorsSource()).apply {
114186
date = Date.from(instantSource.instant())
115187
addCertPathChecker(RevocationChecker(revokedSerialsSource()))
116188
}
189+
190+
val deviceInformation =
191+
if (certPath.provisioningMethod() == ProvisioningMethod.REMOTELY_PROVISIONED) {
192+
certPath.attestationCert().provisioningInfo()
193+
} else {
194+
null
195+
}
196+
deviceInformation?.let { log?.logProvisioningInfoMap(it) }
117197
val pathValidationResult =
118198
try {
119199
certPathValidator.validate(certPath, certPathParameters) as PKIXCertPathValidatorResult
@@ -123,11 +203,15 @@ open class Verifier(
123203

124204
val keyDescription =
125205
try {
126-
checkNotNull(certPath.leafCert().keyDescription()) { "Key attestation extension not found" }
206+
checkNotNull(
207+
KeyDescription.parseFrom(certPath.leafCert(), { msg -> log?.logInfoMessage(msg) })
208+
) {
209+
"Key attestation extension not found"
210+
}
127211
} catch (e: Exception) {
128212
return VerificationResult.ExtensionParsingFailure(e)
129213
}
130-
214+
log?.logKeyDescription(keyDescription)
131215
if (
132216
challengeChecker != null &&
133217
!challengeChecker.checkChallenge(keyDescription.attestationChallenge)
@@ -157,12 +241,6 @@ open class Verifier(
157241
?: return VerificationResult.ExtensionConstraintViolation(
158242
"hardwareEnforced.rootOfTrust is null"
159243
)
160-
val deviceInformation =
161-
if (certPath.provisioningMethod() == ProvisioningMethod.REMOTELY_PROVISIONED) {
162-
certPath.attestationCert().provisioningInfo()
163-
} else {
164-
null
165-
}
166244
return VerificationResult.Success(
167245
pathValidationResult.publicKey,
168246
keyDescription.attestationChallenge,

src/main/kotlin/provider/KeyAttestationCertPath.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ class KeyAttestationCertPath(certs: List<X509Certificate>) : CertPath("X.509") {
4949
when (certs.indexOfLast { it.hasAttestationExtension() }) {
5050
0 -> {} // expected value
5151
-1 -> throw CertificateException("Attestation extension not found")
52-
else -> throw CertificateException("Attestation extension on unexpected certificate")
52+
else ->
53+
if (certs[0].hasAttestationExtension())
54+
throw CertificateException("Additional attestation extension found")
55+
else throw CertificateException("Certificate after target certificate")
5356
}
5457
if (!certs.last().isSelfIssued()) throw CertificateException("Root certificate not found")
5558
this.certificatesWithAnchor = certs

src/main/kotlin/provider/KeyAttestationCertPathValidator.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,9 @@ private class BasicChecker(
253253

254254
private fun verifyValidity(cert: X509Certificate) {
255255
/*
256-
* KAVS does not check the validity of the final certificate in the path. For the purposes of
257-
* migration this path validator is intended to be bug compatible with KAVS, so we do not check
258-
* the validity of the final certificate either.
259-
*
260-
* TODO: b/355190989 - explore if is viable to check the validity of the final certificate.
256+
* Do not check the validity of the final certificate in the path. The
257+
* validity period of the final cert is set on the device so could both be
258+
* subject to tampering and could be impacted by clock skew.
261259
*/
262260
if (remainingCerts == 0) return
263261
try {

src/main/kotlin/provider/RevocationChecker.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class RevocationChecker(private val revokedSerials: Set<String>) : PKIXRevocatio
4545
if (revokedSerials.contains(cert.serialNumber.toString(16))) {
4646
// TODO: b/356234568 - Surface the revocation reason.
4747
throw CertPathValidatorException(
48-
"Certificate has been revoked",
48+
"Certificate has been revoked: ${cert.serialNumber}",
4949
null,
5050
null,
5151
-1,

0 commit comments

Comments
 (0)