Skip to content

Commit 5439d7e

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 81fb212 commit 5439d7e

File tree

9 files changed

+193
-111
lines changed

9 files changed

+193
-111
lines changed

build.gradle.kts

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

22-
repositories { mavenCentral() }
22+
repositories {
23+
mavenCentral()
24+
google()
25+
}
2326

2427
dependencies {
28+
implementation("androidx.annotation:annotation:1.9.1")
2529
implementation("co.nstant.in:cbor:0.9")
2630
implementation("com.google.code.gson:gson:2.11.0")
2731
implementation("com.google.errorprone:error_prone_annotations:2.41.0")
@@ -95,6 +99,4 @@ val generateSources by
9599

96100
sourceSets { main { kotlin.srcDir(generateSources) } }
97101

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

src/main/kotlin/Extension.kt

Lines changed: 17 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,14 @@ 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+
// TODO(google-internal bug): Look into using ImmutableSet.
112+
@SuppressWarnings("Immutable") val imeis: Set<String> = emptySet(),
105113
val meid: String? = null,
106114
val manufacturer: String? = null,
107115
val model: String? = null,
@@ -126,6 +134,7 @@ data class DeviceIdentity(
126134
}
127135

128136
@Immutable
137+
@RequiresApi(24)
129138
data class KeyDescription(
130139
val attestationVersion: BigInteger,
131140
val attestationSecurityLevel: SecurityLevel,
@@ -229,6 +238,7 @@ enum class Origin(val value: Long) {
229238
* @see
230239
* https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/Tag.aidl
231240
*/
241+
@RequiresApi(24)
232242
enum class KeyMintTag(val value: Int) {
233243
PURPOSE(1),
234244
ALGORITHM(2),
@@ -271,7 +281,10 @@ enum class KeyMintTag(val value: Int) {
271281
companion object {
272282
fun from(value: Int) =
273283
values().firstOrNull { it.value == value }
274-
?: throw IllegalArgumentException("unknown tag number: $value")
284+
?: throw ExtensionParsingException(
285+
"unknown tag number: $value",
286+
KeyAttestationReason.UNKNOWN_TAG_NUMBER,
287+
)
275288
}
276289
}
277290

@@ -282,6 +295,7 @@ enum class KeyMintTag(val value: Int) {
282295
* https://source.android.com/docs/security/features/keystore/attestation#authorizationlist-fields
283296
*/
284297
@Immutable
298+
@RequiresApi(24)
285299
data class AuthorizationList(
286300
@SuppressWarnings("Immutable") val purposes: Set<BigInteger>? = null,
287301
val algorithms: BigInteger? = null,
@@ -637,6 +651,7 @@ private fun ASN1Encodable.toAttestationApplicationId(): AttestationApplicationId
637651
return AttestationApplicationId.from(ASN1Sequence.getInstance(this.octets))
638652
}
639653

654+
@RequiresApi(24)
640655
private fun ASN1Encodable.toAuthorizationList(logFn: (String) -> Unit): AuthorizationList {
641656
check(this is ASN1Sequence) { "Object must be an ASN1Sequence, was ${this::class.simpleName}" }
642657
return AuthorizationList.from(this, logFn)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 chain contains a certificate after the target certificate.
26+
// This likely indicates that an attacker is trying to get the verifier to
27+
// accept an attacker-controlled key.
28+
CERTIFICATE_AFTER_TARGET,
29+
// The key description is missing from the expected certificate.
30+
// An Android key attestation chain without a key description is malformed.
31+
TARGET_MISSING_ATTESTATION_EXTENSION,
32+
// Certificate chain contains a certificate other than the target certificate with an attestation
33+
// extension. This likely indicates that an attacker is trying to manipulate the key and
34+
// device properties.
35+
ADDITIONAL_ATTESTATION_EXTENSION,
36+
// The key was not generated. The verifier cannot know that the key has always been in the
37+
// secure environment.
38+
KEY_ORIGIN_NOT_GENERATED,
39+
// The attestation and the KeyMint security levels do not match.
40+
// This likely indicates that the attestation was generated in software and so cannot be trusted.
41+
MISMATCHED_SECURITY_LEVELS,
42+
// The key description is missing the root of trust.
43+
// An Android key attestation chain without a root of trust is malformed.
44+
ROOT_OF_TRUST_MISSING,
45+
// There was an error parsing the key description and an unknown tag number was encountered.
46+
UNKNOWN_TAG_NUMBER,
47+
}

src/main/kotlin/Verifier.kt

Lines changed: 19 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. */
@@ -131,6 +133,7 @@ open class Verifier(
131133
* @return [VerificationResult]
132134
*/
133135
@JvmOverloads
136+
@RequiresApi(24)
134137
fun verify(
135138
chain: List<X509Certificate>,
136139
challengeChecker: ChallengeChecker? = null,
@@ -150,6 +153,7 @@ open class Verifier(
150153
return result
151154
}
152155

156+
@RequiresApi(24)
153157
private fun internalVerify(
154158
certPath: KeyAttestationCertPath,
155159
challengeChecker: ChallengeChecker? = null,
@@ -192,10 +196,16 @@ open class Verifier(
192196
checkNotNull(
193197
KeyDescription.parseFrom(certPath.leafCert(), { msg -> log?.logInfoMessage(msg) })
194198
) {
199+
// Should never happen since the extension's presence is checked by by validate().
195200
"Key attestation extension not found"
196201
}
197-
} catch (e: Exception) {
202+
} catch (e: ExtensionParsingException) {
198203
return VerificationResult.ExtensionParsingFailure(e)
204+
} catch (e: Exception) {
205+
// TODO(google-internal bug): When experimental contracts aren't experimental,
206+
// update the IllegalArgumentException and IllegalStateException cases to return
207+
// ExtensionParsingException.
208+
return VerificationResult.ExtensionParsingFailure(ExtensionParsingException(e.toString()))
199209
}
200210
log?.logKeyDescription(keyDescription)
201211
if (
@@ -210,7 +220,8 @@ open class Verifier(
210220
keyDescription.hardwareEnforced.origin != Origin.GENERATED
211221
) {
212222
return VerificationResult.ExtensionConstraintViolation(
213-
"origin != GENERATED: ${keyDescription.hardwareEnforced.origin}"
223+
"origin != GENERATED: ${keyDescription.hardwareEnforced.origin}",
224+
KeyAttestationReason.KEY_ORIGIN_NOT_GENERATED,
214225
)
215226
}
216227

@@ -219,13 +230,15 @@ open class Verifier(
219230
keyDescription.attestationSecurityLevel
220231
} else {
221232
return VerificationResult.ExtensionConstraintViolation(
222-
"attestationSecurityLevel != keyMintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}"
233+
"attestationSecurityLevel != keyMintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}",
234+
KeyAttestationReason.MISMATCHED_SECURITY_LEVELS,
223235
)
224236
}
225237
val rootOfTrust =
226238
keyDescription.hardwareEnforced.rootOfTrust
227239
?: return VerificationResult.ExtensionConstraintViolation(
228-
"hardwareEnforced.rootOfTrust is null"
240+
"hardwareEnforced.rootOfTrust is null",
241+
KeyAttestationReason.ROOT_OF_TRUST_MISSING,
229242
)
230243
return VerificationResult.Success(
231244
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: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,6 @@ class KeyAttestationCertPath(certs: List<X509Certificate>) : CertPath("X.509") {
4949
init {
5050
// < 3 check needed to support parsing software-backed certs.
5151
if (certs.size < 3) throw CertificateException("At least 3 certificates are required")
52-
when (certs.indexOfLast { it.hasAttestationExtension() }) {
53-
0 -> {} // expected value
54-
-1 -> throw CertificateException("Attestation extension not found")
55-
else ->
56-
if (certs[0].hasAttestationExtension()) {
57-
throw CertificateException("Additional attestation extension found")
58-
} else {
59-
throw CertificateException("Certificate after target certificate")
60-
}
61-
}
6252
if (!certs.last().isSelfIssued()) throw CertificateException("Root certificate not found")
6353
this.certificatesWithAnchor = certs
6454
}

0 commit comments

Comments
 (0)