Skip to content

Commit b1c9203

Browse files
carmenyhcopybara-github
authored andcommitted
Add KeyAttestationReason to CertPathValidatorException.
This will allow callers to distinguish between different errors in the chain without string matching. This requires explicitly stating the required API for each method which diretly or indirectly uses KeyAttestationReason, but shouldn't be an actual change in requirements since the other reasons also require API 24 when used on Android. This also moves the key attestation chain format checks to be exclusively in the validator instead of some of them being duplicated in the KeyAttestationCertPath constructor. The validator checks could previously never be executed and the CertPathValidatorException allows for more structured information returned on exception. PiperOrigin-RevId: 805800611
1 parent 502036d commit b1c9203

15 files changed

+438
-221
lines changed

build.gradle.kts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ plugins {
2020
id("org.jetbrains.kotlin.jvm") version "2.2.0"
2121
}
2222

23-
repositories { mavenCentral() }
23+
repositories {
24+
mavenCentral()
25+
google()
26+
}
2427

2528
dependencies {
29+
implementation("androidx.annotation:annotation:1.9.1")
2630
implementation("co.nstant.in:cbor:0.9")
2731
implementation("com.google.code.gson:gson:2.11.0")
2832
implementation("com.google.guava:guava:33.3.1-android")
@@ -97,6 +101,4 @@ val generateSources by
97101

98102
sourceSets { main { kotlin.srcDir(generateSources) } }
99103

100-
tasks.named("compileKotlin").configure {
101-
dependsOn("generateSources")
102-
}
104+
tasks.named("compileKotlin").configure { dependsOn("generateSources") }

src/main/kotlin/Extension.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.android.keyattestation.verifier
1818

19+
import androidx.annotation.RequiresApi
1920
import co.nstant.`in`.cbor.CborDecoder
2021
import co.nstant.`in`.cbor.CborEncoder
2122
import co.nstant.`in`.cbor.CborException
@@ -57,6 +58,11 @@ import org.bouncycastle.asn1.DERSet
5758
import org.bouncycastle.asn1.DERTaggedObject
5859
import org.bouncycastle.asn1.x509.Extension
5960

61+
@Immutable
62+
@RequiresApi(24)
63+
data class ExtensionParsingException(val msg: String, val reason: KeyAttestationReason? = null) :
64+
Exception(msg)
65+
6066
@Immutable
6167
data class ProvisioningInfoMap(
6268
val certificatesIssued: Int,
@@ -96,12 +102,13 @@ data class ProvisioningInfoMap(
96102
}
97103
}
98104

105+
@Immutable
99106
data class DeviceIdentity(
100107
val brand: String? = null,
101108
val device: String? = null,
102109
val product: String? = null,
103110
val serialNumber: String? = null,
104-
val imeis: Set<String> = emptySet(),
111+
@SuppressWarnings("Immutable") val imeis: Set<String> = emptySet(),
105112
val meid: String? = null,
106113
val manufacturer: String? = null,
107114
val model: String? = null,
@@ -126,6 +133,7 @@ data class DeviceIdentity(
126133
}
127134

128135
@Immutable
136+
@RequiresApi(24)
129137
data class KeyDescription(
130138
val attestationVersion: BigInteger,
131139
val attestationSecurityLevel: SecurityLevel,
@@ -229,6 +237,7 @@ enum class Origin(val value: Long) {
229237
* @see
230238
* https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/Tag.aidl
231239
*/
240+
@RequiresApi(24)
232241
enum class KeyMintTag(val value: Int) {
233242
PURPOSE(1),
234243
ALGORITHM(2),
@@ -271,7 +280,10 @@ enum class KeyMintTag(val value: Int) {
271280
companion object {
272281
fun from(value: Int) =
273282
values().firstOrNull { it.value == value }
274-
?: throw IllegalArgumentException("unknown tag number: $value")
283+
?: throw ExtensionParsingException(
284+
"unknown tag number: $value",
285+
KeyAttestationReason.UNKNOWN_TAG_NUMBER,
286+
)
275287
}
276288
}
277289

@@ -282,6 +294,7 @@ enum class KeyMintTag(val value: Int) {
282294
* https://source.android.com/docs/security/features/keystore/attestation#authorizationlist-fields
283295
*/
284296
@Immutable
297+
@RequiresApi(24)
285298
data class AuthorizationList(
286299
@SuppressWarnings("Immutable") val purposes: Set<BigInteger>? = null,
287300
val algorithms: BigInteger? = null,
@@ -637,6 +650,7 @@ private fun ASN1Encodable.toAttestationApplicationId(): AttestationApplicationId
637650
return AttestationApplicationId.from(ASN1Sequence.getInstance(this.octets))
638651
}
639652

653+
@RequiresApi(24)
640654
private fun ASN1Encodable.toAuthorizationList(logFn: (String) -> Unit): AuthorizationList {
641655
check(this is ASN1Sequence) { "Object must be an ASN1Sequence, was ${this::class.simpleName}" }
642656
return AuthorizationList.from(this, logFn)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.android.keyattestation.verifier
18+
19+
import androidx.annotation.RequiresApi
20+
import java.security.cert.CertPathValidatorException
21+
22+
/** Reasons why a certificate chain could not be verified which are specific to key attestation. */
23+
@RequiresApi(24)
24+
enum class KeyAttestationReason : CertPathValidatorException.Reason {
25+
CERTIFICATE_AFTER_TARGET,
26+
TARGET_MISSING_ATTESTATION_EXTENSION,
27+
ADDITIONAL_ATTESTATION_EXTENSION,
28+
KEY_ORIGIN_NOT_GENERATED,
29+
MISMATCHED_SECURITY_LEVELS,
30+
ROOT_OF_TRUST_MISSING,
31+
UNKNOWN_TAG_NUMBER,
32+
}

src/main/kotlin/Verifier.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.android.keyattestation.verifier
1818

19+
import androidx.annotation.RequiresApi
1920
import com.android.keyattestation.verifier.provider.KeyAttestationCertPath
2021
import com.android.keyattestation.verifier.provider.KeyAttestationProvider
2122
import com.android.keyattestation.verifier.provider.ProvisioningMethod
@@ -50,9 +51,10 @@ sealed interface VerificationResult {
5051

5152
data class ChainParsingFailure(val cause: Exception) : VerificationResult
5253

53-
data class ExtensionParsingFailure(val cause: Exception) : VerificationResult
54+
data class ExtensionParsingFailure(val cause: ExtensionParsingException) : VerificationResult
5455

55-
data class ExtensionConstraintViolation(val cause: String) : VerificationResult
56+
data class ExtensionConstraintViolation(val cause: String, val reason: KeyAttestationReason) :
57+
VerificationResult
5658
}
5759

5860
/** Interface for logging info level key attestation events and information. */
@@ -114,6 +116,7 @@ interface LogHook {
114116
* @param anchor a [TrustAnchor] to use for certificate path verification.
115117
*/
116118
@ThreadSafe
119+
@RequiresApi(24)
117120
open class Verifier(
118121
private val trustAnchorsSource: () -> Set<TrustAnchor>,
119122
private val revokedSerialsSource: () -> Set<String>,
@@ -211,10 +214,16 @@ open class Verifier(
211214
checkNotNull(
212215
KeyDescription.parseFrom(certPath.leafCert(), { msg -> log?.logInfoMessage(msg) })
213216
) {
217+
// Should never happen since the extension's presence is checked by by validate().
214218
"Key attestation extension not found"
215219
}
216-
} catch (e: Exception) {
220+
} catch (e: ExtensionParsingException) {
217221
return VerificationResult.ExtensionParsingFailure(e)
222+
} catch (e: Exception) {
223+
// TODO(google-internal bug): When experimental contracts aren't experimental,
224+
// update the IllegalArgumentException and IllegalStateException cases to return
225+
// ExtensionParsingException.
226+
return VerificationResult.ExtensionParsingFailure(ExtensionParsingException(e.toString()))
218227
}
219228
log?.logKeyDescription(keyDescription)
220229
if (
@@ -229,7 +238,8 @@ open class Verifier(
229238
keyDescription.hardwareEnforced.origin != Origin.GENERATED
230239
) {
231240
return VerificationResult.ExtensionConstraintViolation(
232-
"origin != GENERATED: ${keyDescription.hardwareEnforced.origin}"
241+
"origin != GENERATED: ${keyDescription.hardwareEnforced.origin}",
242+
KeyAttestationReason.KEY_ORIGIN_NOT_GENERATED,
233243
)
234244
}
235245

@@ -238,13 +248,15 @@ open class Verifier(
238248
keyDescription.attestationSecurityLevel
239249
} else {
240250
return VerificationResult.ExtensionConstraintViolation(
241-
"attestationSecurityLevel != keyMintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}"
251+
"attestationSecurityLevel != keyMintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}",
252+
KeyAttestationReason.MISMATCHED_SECURITY_LEVELS,
242253
)
243254
}
244255
val rootOfTrust =
245256
keyDescription.hardwareEnforced.rootOfTrust
246257
?: return VerificationResult.ExtensionConstraintViolation(
247-
"hardwareEnforced.rootOfTrust is null"
258+
"hardwareEnforced.rootOfTrust is null",
259+
KeyAttestationReason.ROOT_OF_TRUST_MISSING,
248260
)
249261
return VerificationResult.Success(
250262
pathValidationResult.publicKey,

src/main/kotlin/X509CertificateExt.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.android.keyattestation.verifier
1818

19+
import androidx.annotation.RequiresApi
1920
import java.io.InputStream
2021
import java.security.cert.CertificateException
2122
import java.security.cert.CertificateFactory
@@ -36,7 +37,7 @@ fun InputStream.asX509Certificate() =
3637
* @return the DER-encoded OCTET string containing the KeyDescription sequence or null if the
3738
* extension is not present in the certificate.
3839
*/
39-
fun X509Certificate.keyDescription() = KeyDescription.parseFrom(this)
40+
@RequiresApi(24) fun X509Certificate.keyDescription() = KeyDescription.parseFrom(this)
4041

4142
/**
4243
* Returns the Android Key Attestation extension for provisioning info.

src/main/kotlin/provider/KeyAttestationCertPath.kt

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
package com.android.keyattestation.verifier.provider
1818

19+
import com.android.keyattestation.verifier.SecurityLevel
1920
import com.android.keyattestation.verifier.asX509Certificate
2021
import com.google.protobuf.ByteString
2122
import java.security.cert.CertPath
2223
import java.security.cert.CertificateException
2324
import java.security.cert.X509Certificate
25+
import javax.security.auth.x500.X500Principal
2426

2527
/**
2628
* [CertPath] representing an Android key attestation certificate chain.
@@ -29,9 +31,9 @@ import java.security.cert.X509Certificate
2931
* `KeyStore.getCertificateChain()`) in the following order:
3032
* 1. Leaf certificate (containing the extension)
3133
* 2. Attestation certificate (contains the ProvisioningInfo extension if remotely provisioned)
32-
* 3. Intermediate certificate
33-
* 5. [Intermediate certificate] (if remotely provisioned)
34-
* 4. Root certificate
34+
* 3. Intermediate certificate (not present if software-backed attestation)
35+
* 4. [Intermediate certificate] (only present if remotely provisioned)
36+
* 5. Root certificate
3537
*
3638
* The last certificate in the chain is the trust anchor and is not included in the resulting
3739
* [CertPath]: "By convention, the certificates in a CertPath object of type X.509 are ordered
@@ -45,15 +47,8 @@ class KeyAttestationCertPath(certs: List<X509Certificate>) : CertPath("X.509") {
4547
val certificatesWithAnchor: List<X509Certificate>
4648

4749
init {
50+
// < 3 check needed to support parsing software-backed certs.
4851
if (certs.size < 3) throw CertificateException("At least 3 certificates are required")
49-
when (certs.indexOfLast { it.hasAttestationExtension() }) {
50-
0 -> {} // expected value
51-
-1 -> throw CertificateException("Attestation extension not found")
52-
else ->
53-
if (certs[0].hasAttestationExtension())
54-
throw CertificateException("Additional attestation extension found")
55-
else throw CertificateException("Certificate after target certificate")
56-
}
5752
if (!certs.last().isSelfIssued()) throw CertificateException("Root certificate not found")
5853
this.certificatesWithAnchor = certs
5954
}
@@ -68,7 +63,43 @@ class KeyAttestationCertPath(certs: List<X509Certificate>) : CertPath("X.509") {
6863

6964
override fun getCertificates(): List<X509Certificate> = certificatesWithAnchor.dropLast(1)
7065

71-
fun provisioningMethod(): ProvisioningMethod = intermediateCert().provisioningMethod()
66+
fun provisioningMethod() =
67+
when {
68+
isFactoryProvisioned() -> ProvisioningMethod.FACTORY_PROVISIONED
69+
isRemoteProvisioned() -> ProvisioningMethod.REMOTELY_PROVISIONED
70+
else -> ProvisioningMethod.UNKNOWN
71+
}
72+
73+
fun securityLevel() =
74+
when (provisioningMethod()) {
75+
ProvisioningMethod.FACTORY_PROVISIONED ->
76+
when (
77+
parseDN(intermediateCert().subjectX500Principal.getName(X500Principal.RFC1779))[
78+
"OID.2.5.4.12"]
79+
) {
80+
"TEE" -> SecurityLevel.TRUSTED_ENVIRONMENT
81+
"StrongBox" -> SecurityLevel.STRONG_BOX
82+
else -> SecurityLevel.SOFTWARE // Should never happen because isFactoryProvisioned()
83+
}
84+
ProvisioningMethod.REMOTELY_PROVISIONED -> {
85+
try {
86+
when (
87+
parseDN(
88+
certificates[certificates.size - 2]
89+
.subjectX500Principal
90+
.getName(X500Principal.RFC1779)
91+
)["O"]
92+
) {
93+
"TEE" -> SecurityLevel.TRUSTED_ENVIRONMENT
94+
"StrongBox" -> SecurityLevel.STRONG_BOX
95+
else -> SecurityLevel.SOFTWARE
96+
}
97+
} catch (e: Exception) {
98+
SecurityLevel.SOFTWARE
99+
}
100+
}
101+
else -> SecurityLevel.SOFTWARE
102+
}
72103

73104
/**
74105
* Returns the leaf certificate from the certificate chain.
@@ -84,6 +115,17 @@ class KeyAttestationCertPath(certs: List<X509Certificate>) : CertPath("X.509") {
84115

85116
fun intermediateCert(): X509Certificate = certificates.last()
86117

118+
private fun isFactoryProvisioned(): Boolean {
119+
val rdn = parseDN(this.intermediateCert().subjectX500Principal.getName(X500Principal.RFC1779))
120+
return rdn.containsKey("OID.2.5.4.5") && rdn["OID.2.5.4.12"] in setOf("TEE", "StrongBox")
121+
}
122+
123+
// TODO(google-internal bug): Update this to use fields in the RKP root.
124+
private fun isRemoteProvisioned(): Boolean {
125+
val rdn = parseDN(this.intermediateCert().subjectX500Principal.getName(X500Principal.RFC1779))
126+
return rdn["CN"] == "Droid CA2" && rdn["O"] == "Google LLC"
127+
}
128+
87129
companion object {
88130
@JvmStatic
89131
@Throws(CertificateException::class)
@@ -93,3 +135,22 @@ class KeyAttestationCertPath(certs: List<X509Certificate>) : CertPath("X.509") {
93135
private fun X509Certificate.isSelfIssued() = issuerX500Principal == subjectX500Principal
94136
}
95137
}
138+
139+
enum class ProvisioningMethod {
140+
UNKNOWN,
141+
FACTORY_PROVISIONED,
142+
REMOTELY_PROVISIONED,
143+
}
144+
145+
private fun parseDN(dn: String): Map<String, String> {
146+
val attributes = mutableMapOf<String, String>()
147+
val parts = dn.split(",")
148+
149+
for (part in parts) {
150+
val keyValue = part.trim().split("=", limit = 2)
151+
if (keyValue.size == 2) {
152+
attributes[keyValue[0].trim()] = keyValue[1].trim()
153+
}
154+
}
155+
return attributes
156+
}

0 commit comments

Comments
 (0)