Skip to content

Commit 0e5379a

Browse files
carmenyhcopybara-github
authored andcommitted
Add mechanism for adding debug logging.
PiperOrigin-RevId: 769130000
1 parent a1105af commit 0e5379a

File tree

5 files changed

+167
-72
lines changed

5 files changed

+167
-72
lines changed

src/main/kotlin/Extension.kt

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,14 @@ data class ProvisioningInfoMap(
100100

101101
@JsonClass(generateAdapter = true)
102102
data class DeviceIdentity(
103-
val brand: String?,
104-
val device: String?,
105-
val product: String?,
106-
val serialNumber: String?,
107-
val imeis: Set<String>,
108-
val meid: String?,
109-
val manufacturer: String?,
110-
val model: String?,
103+
val brand: String? = null,
104+
val device: String? = null,
105+
val product: String? = null,
106+
val serialNumber: String? = null,
107+
val imeis: Set<String> = emptySet(),
108+
val meid: String? = null,
109+
val manufacturer: String? = null,
110+
val model: String? = null,
111111
) {
112112
companion object {
113113
@JvmStatic
@@ -139,6 +139,7 @@ data class KeyDescription(
139139
val uniqueId: ByteString,
140140
val softwareEnforced: AuthorizationList,
141141
val hardwareEnforced: AuthorizationList,
142+
@SuppressWarnings("Immutable") @Transient val infoMessages: List<String>? = listOf(),
142143
) {
143144
fun asExtension(): Extension {
144145
return Extension(OID, /* critical= */ false, encodeToAsn1())
@@ -182,15 +183,17 @@ data class KeyDescription(
182183

183184
private fun from(seq: ASN1Sequence): KeyDescription {
184185
require(seq.size() == 8)
186+
val infoMessages = mutableListOf<String>()
185187
return KeyDescription(
186188
attestationVersion = seq.getObjectAt(0).toInt(),
187189
attestationSecurityLevel = seq.getObjectAt(1).toSecurityLevel(),
188190
keyMintVersion = seq.getObjectAt(2).toInt(),
189191
keyMintSecurityLevel = seq.getObjectAt(3).toSecurityLevel(),
190192
attestationChallenge = seq.getObjectAt(4).toByteString(),
191193
uniqueId = seq.getObjectAt(5).toByteString(),
192-
softwareEnforced = seq.getObjectAt(6).toAuthorizationList(),
193-
hardwareEnforced = seq.getObjectAt(7).toAuthorizationList(),
194+
softwareEnforced = seq.getObjectAt(6).toAuthorizationList(infoMessages = infoMessages),
195+
hardwareEnforced = seq.getObjectAt(7).toAuthorizationList(infoMessages = infoMessages),
196+
infoMessages = infoMessages.toList(),
194197
)
195198
}
196199
}
@@ -396,7 +399,7 @@ data class AuthorizationList(
396399
.let { DERSequence(it.toTypedArray()) }
397400

398401
internal companion object {
399-
fun from(seq: ASN1Sequence, validateTagOrder: Boolean = false): AuthorizationList {
402+
fun from(seq: ASN1Sequence, infoMessages: MutableList<String>? = null): AuthorizationList {
400403
val objects =
401404
seq.associate {
402405
require(it is ASN1TaggedObject) {
@@ -416,9 +419,8 @@ data class AuthorizationList(
416419
* 2. within each class of tags, the elements or alternatives shall appear in ascending order
417420
* of their tag numbers.
418421
*/
419-
// TODO: b/356172932 - Add test data once an example certificate is found in the wild.
420-
if (validateTagOrder && !objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs }) {
421-
throw IllegalArgumentException("AuthorizationList tags must appear in ascending order")
422+
if (!objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs }) {
423+
infoMessages?.add("AuthorizationList tags must appear in ascending order")
422424
}
423425

424426
return AuthorizationList(
@@ -444,7 +446,15 @@ data class AuthorizationList(
444446
rollbackResistant = if (objects.containsKey(KeyMintTag.ROLLBACK_RESISTANT)) true else null,
445447
rootOfTrust = objects[KeyMintTag.ROOT_OF_TRUST]?.toRootOfTrust(),
446448
osVersion = objects[KeyMintTag.OS_VERSION]?.toInt(),
447-
osPatchLevel = objects[KeyMintTag.OS_PATCH_LEVEL]?.toPatchLevel(),
449+
osPatchLevel =
450+
objects[KeyMintTag.OS_PATCH_LEVEL]?.let {
451+
try {
452+
it.toPatchLevel()
453+
} catch (e: DateTimeParseException) {
454+
infoMessages?.add("Invalid OS patch level: ${e.getParsedString()}")
455+
null
456+
}
457+
},
448458
attestationApplicationId =
449459
objects[KeyMintTag.ATTESTATION_APPLICATION_ID]?.toAttestationApplicationId(),
450460
attestationIdBrand = objects[KeyMintTag.ATTESTATION_ID_BRAND]?.toStr(),
@@ -455,8 +465,24 @@ data class AuthorizationList(
455465
attestationIdMeid = objects[KeyMintTag.ATTESTATION_ID_MEID]?.toStr(),
456466
attestationIdManufacturer = objects[KeyMintTag.ATTESTATION_ID_MANUFACTURER]?.toStr(),
457467
attestationIdModel = objects[KeyMintTag.ATTESTATION_ID_MODEL]?.toStr(),
458-
vendorPatchLevel = objects[KeyMintTag.VENDOR_PATCH_LEVEL]?.toPatchLevel(),
459-
bootPatchLevel = objects[KeyMintTag.BOOT_PATCH_LEVEL]?.toPatchLevel(),
468+
vendorPatchLevel =
469+
objects[KeyMintTag.VENDOR_PATCH_LEVEL]?.let {
470+
try {
471+
it.toPatchLevel()
472+
} catch (e: DateTimeParseException) {
473+
infoMessages?.add("Invalid vendor patch level: ${e.getParsedString()}")
474+
null
475+
}
476+
},
477+
bootPatchLevel =
478+
objects[KeyMintTag.BOOT_PATCH_LEVEL]?.let {
479+
try {
480+
it.toPatchLevel()
481+
} catch (e: DateTimeParseException) {
482+
infoMessages?.add("Invalid boot patch level: ${e.getParsedString()}")
483+
null
484+
}
485+
},
460486
attestationIdSecondImei = objects[KeyMintTag.ATTESTATION_ID_SECOND_IMEI]?.toStr(),
461487
moduleHash = objects[KeyMintTag.MODULE_HASH]?.toByteString(),
462488
)
@@ -475,24 +501,20 @@ data class PatchLevel(val yearMonth: YearMonth, val version: Int? = null) {
475501
}
476502

477503
companion object {
478-
fun from(patchLevel: ASN1Encodable): PatchLevel? {
504+
fun from(patchLevel: ASN1Encodable): PatchLevel {
479505
check(patchLevel is ASN1Integer) { "Must be an ASN1Integer, was ${this::class.simpleName}" }
480506
return from(patchLevel.value.toString())
481507
}
482508

483509
@JvmStatic
484-
fun from(patchLevel: String): PatchLevel? {
510+
fun from(patchLevel: String): PatchLevel {
485511
if (patchLevel.length != 6 && patchLevel.length != 8) {
486-
return null
487-
}
488-
try {
489-
val yearMonth =
490-
DateTimeFormatter.ofPattern("yyyyMM").parse(patchLevel.substring(0, 6), YearMonth::from)
491-
val version = if (patchLevel.length == 8) patchLevel.substring(6).toInt() else null
492-
return PatchLevel(yearMonth, version)
493-
} catch (e: DateTimeParseException) {
494-
return null
512+
throw DateTimeParseException("Invalid patch level length:", patchLevel, 0)
495513
}
514+
val yearMonth =
515+
DateTimeFormatter.ofPattern("yyyyMM").parse(patchLevel.substring(0, 6), YearMonth::from)
516+
val version = if (patchLevel.length == 8) patchLevel.substring(6).toInt() else null
517+
return PatchLevel(yearMonth, version)
496518
}
497519
}
498520
}
@@ -623,12 +645,11 @@ private fun ASN1Encodable.toAttestationApplicationId(): AttestationApplicationId
623645
return AttestationApplicationId.from(ASN1Sequence.getInstance(this.octets))
624646
}
625647

626-
// TODO: b/356172932 - `validateTagOrder` should default to true after making it user configurable.
627648
private fun ASN1Encodable.toAuthorizationList(
628-
validateTagOrder: Boolean = false
649+
infoMessages: MutableList<String>? = null
629650
): AuthorizationList {
630651
check(this is ASN1Sequence) { "Object must be an ASN1Sequence, was ${this::class.simpleName}" }
631-
return AuthorizationList.from(this, validateTagOrder)
652+
return AuthorizationList.from(this, infoMessages)
632653
}
633654

634655
private fun ASN1Encodable.toBoolean(): Boolean {

src/main/kotlin/Verifier.kt

Lines changed: 110 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,36 @@ sealed interface VerificationResult {
4545

4646
data object ChallengeMismatch : VerificationResult
4747

48-
data object PathValidationFailure : VerificationResult
48+
data class PathValidationFailure(val cause: Exception) : VerificationResult
4949

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

5252
data class ExtensionParsingFailure(val cause: Exception) : VerificationResult
5353

5454
data class ExtensionConstraintViolation(val cause: String) : VerificationResult
5555
}
5656

57+
/** Interface for logging info level key attestation events and information. */
58+
interface LogHook {
59+
fun logVerificationEvent(verificationEvent: VerificationEvent)
60+
}
61+
62+
data class VerificationEvent(
63+
val inputChain: List<X509Certificate>,
64+
val result: VerificationResult,
65+
val keyDescription: KeyDescription? = null,
66+
val provisioningInfoMap: ProvisioningInfoMap? = null,
67+
val certSerialNumbers: List<String>? = null,
68+
val infoMessages: List<String>? = null,
69+
)
70+
5771
/**
5872
* Verifier for Android Key Attestation certificate chain.
5973
*
6074
* https://developer.android.com/privacy-and-security/security-key-attestation
6175
*
6276
* @param anchor a [TrustAnchor] to use for certificate path verification.
6377
*/
64-
// TODO: b/356234568 - Verify intermediate certificate revocation status.
6578
@ThreadSafe
6679
open class Verifier(
6780
private val trustAnchorsSource: () -> Set<TrustAnchor>,
@@ -83,14 +96,19 @@ open class Verifier(
8396
fun verify(
8497
chain: List<X509Certificate>,
8598
challengeChecker: ChallengeChecker? = null,
99+
log: LogHook? = null,
86100
): VerificationResult {
87101
val certPath =
88102
try {
89103
KeyAttestationCertPath(chain)
90104
} catch (e: Exception) {
91-
return VerificationResult.ChainParsingFailure
105+
val result = VerificationResult.ChainParsingFailure(e)
106+
log?.logVerificationEvent(VerificationEvent(inputChain = chain, result = result))
107+
return result
92108
}
93-
return verify(certPath, challengeChecker)
109+
val verificationEvent = internalVerify(certPath, challengeChecker)
110+
log?.logVerificationEvent(verificationEvent)
111+
return verificationEvent.result
94112
}
95113

96114
/**
@@ -99,77 +117,140 @@ open class Verifier(
99117
* @param chain The attestation certificate chain to verify.
100118
* @param challengeChecker The challenge checker to use for additional validation of the challenge
101119
* in the attestation chain.
102-
* @return [VerificationResult]
120+
* @return [VerificationEvent]
103121
*
104122
* TODO: b/366058500 - Make the challenge required after Apparat's changes are rollback safe.
105123
*/
106-
@JvmOverloads
107124
fun verify(
108125
certPath: KeyAttestationCertPath,
109126
challengeChecker: ChallengeChecker? = null,
127+
log: LogHook? = null,
110128
): VerificationResult {
129+
val verificationEvent = internalVerify(certPath, challengeChecker)
130+
log?.logVerificationEvent(verificationEvent)
131+
return verificationEvent.result
132+
}
133+
134+
internal fun internalVerify(
135+
certPath: KeyAttestationCertPath,
136+
challengeChecker: ChallengeChecker? = null,
137+
): VerificationEvent {
138+
val serialNumbers =
139+
certPath.certificatesWithAnchor.subList(1, certPath.certificatesWithAnchor.size).map {
140+
it.serialNumber.toString(16)
141+
}
111142
val certPathValidator = CertPathValidator.getInstance("KeyAttestation")
112143
val certPathParameters =
113144
PKIXParameters(trustAnchorsSource()).apply {
114145
date = Date.from(instantSource.instant())
115146
addCertPathChecker(RevocationChecker(revokedSerialsSource()))
116147
}
148+
149+
val deviceInformation =
150+
if (certPath.provisioningMethod() == ProvisioningMethod.REMOTELY_PROVISIONED) {
151+
certPath.attestationCert().provisioningInfo()
152+
} else {
153+
null
154+
}
117155
val pathValidationResult =
118156
try {
119157
certPathValidator.validate(certPath, certPathParameters) as PKIXCertPathValidatorResult
120158
} catch (e: CertPathValidatorException) {
121-
return VerificationResult.PathValidationFailure
159+
return VerificationEvent(
160+
inputChain = certPath.getCertificates(),
161+
result = VerificationResult.PathValidationFailure(e),
162+
certSerialNumbers = serialNumbers,
163+
provisioningInfoMap = deviceInformation,
164+
)
122165
}
123166

124167
val keyDescription =
125168
try {
126169
checkNotNull(certPath.leafCert().keyDescription()) { "Key attestation extension not found" }
127170
} catch (e: Exception) {
128-
return VerificationResult.ExtensionParsingFailure(e)
171+
return VerificationEvent(
172+
inputChain = certPath.getCertificates(),
173+
result = VerificationResult.ExtensionParsingFailure(e),
174+
certSerialNumbers = serialNumbers,
175+
provisioningInfoMap = deviceInformation,
176+
)
129177
}
130178

179+
val infoMessages = keyDescription.infoMessages
180+
131181
if (
132182
challengeChecker != null &&
133183
!challengeChecker.checkChallenge(keyDescription.attestationChallenge)
134184
) {
135-
return VerificationResult.ChallengeMismatch
185+
return VerificationEvent(
186+
inputChain = certPath.getCertificates(),
187+
result = VerificationResult.ChallengeMismatch,
188+
certSerialNumbers = serialNumbers,
189+
keyDescription = keyDescription,
190+
infoMessages = infoMessages,
191+
provisioningInfoMap = deviceInformation,
192+
)
136193
}
137194

138195
if (
139196
keyDescription.hardwareEnforced.origin == null ||
140197
keyDescription.hardwareEnforced.origin != Origin.GENERATED
141198
) {
142-
return VerificationResult.ExtensionConstraintViolation(
143-
"origin != GENERATED: ${keyDescription.hardwareEnforced.origin}"
199+
return VerificationEvent(
200+
result =
201+
VerificationResult.ExtensionConstraintViolation(
202+
"hardwareEnforced.origin is not GENERATED: ${keyDescription.hardwareEnforced.origin}"
203+
),
204+
inputChain = certPath.getCertificates(),
205+
certSerialNumbers = serialNumbers,
206+
keyDescription = keyDescription,
207+
infoMessages = infoMessages,
208+
provisioningInfoMap = deviceInformation,
144209
)
145210
}
146211

147212
val securityLevel =
148213
if (keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel) {
149214
keyDescription.attestationSecurityLevel
150215
} else {
151-
return VerificationResult.ExtensionConstraintViolation(
152-
"attestationSecurityLevel != keyMintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}"
216+
return VerificationEvent(
217+
result =
218+
VerificationResult.ExtensionConstraintViolation(
219+
"attestationSecurityLevel != keymintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}"
220+
),
221+
inputChain = certPath.getCertificates(),
222+
certSerialNumbers = serialNumbers,
223+
keyDescription = keyDescription,
224+
infoMessages = infoMessages,
225+
provisioningInfoMap = deviceInformation,
153226
)
154227
}
155228
val rootOfTrust =
156229
keyDescription.hardwareEnforced.rootOfTrust
157-
?: return VerificationResult.ExtensionConstraintViolation(
158-
"hardwareEnforced.rootOfTrust is null"
230+
?: return VerificationEvent(
231+
result =
232+
VerificationResult.ExtensionConstraintViolation("hardwareEnforced.rootOfTrust is null"),
233+
keyDescription = keyDescription,
234+
inputChain = certPath.getCertificates(),
235+
certSerialNumbers = serialNumbers,
236+
infoMessages = infoMessages,
237+
provisioningInfoMap = deviceInformation,
159238
)
160-
val deviceInformation =
161-
if (certPath.provisioningMethod() == ProvisioningMethod.REMOTELY_PROVISIONED) {
162-
certPath.attestationCert().provisioningInfo()
163-
} else {
164-
null
165-
}
166-
return VerificationResult.Success(
167-
pathValidationResult.publicKey,
168-
keyDescription.attestationChallenge,
169-
securityLevel,
170-
rootOfTrust.verifiedBootState,
171-
deviceInformation,
172-
DeviceIdentity.parseFrom(keyDescription),
239+
return VerificationEvent(
240+
result =
241+
VerificationResult.Success(
242+
pathValidationResult.publicKey,
243+
keyDescription.attestationChallenge,
244+
securityLevel,
245+
rootOfTrust.verifiedBootState,
246+
deviceInformation,
247+
DeviceIdentity.parseFrom(keyDescription),
248+
),
249+
inputChain = certPath.getCertificates(),
250+
certSerialNumbers = serialNumbers,
251+
keyDescription = keyDescription,
252+
infoMessages = infoMessages,
253+
provisioningInfoMap = deviceInformation,
173254
)
174255
}
175256
}

0 commit comments

Comments
 (0)