Skip to content

Commit 27dcd9f

Browse files
sethmoocopybara-github
authored andcommitted
Add constraint enforcing SecurityLevel matching
It's possible that a vulnerable keymint implementation could lie about the security level. e.g. a vulnerable trusted OS could be coerced into claiming it is actually StrongBox. All certificates issued to StrongBox implementations have a StrongBox claim in their certificate (carried within the subject). Add a constraint that checks for this claim and matches it against the Google-signed certificate(s). PiperOrigin-RevId: 948421025
1 parent 18abe6c commit 27dcd9f

7 files changed

Lines changed: 181 additions & 43 deletions

File tree

src/main/kotlin/ConstraintConfig.kt

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.android.keyattestation.verifier
1818

1919
import androidx.annotation.RequiresApi
20+
import com.android.keyattestation.verifier.provider.KeyAttestationCertPath
2021
import com.google.common.collect.ImmutableList
2122
import com.google.errorprone.annotations.Immutable
2223
import com.google.errorprone.annotations.ThreadSafe
@@ -36,7 +37,7 @@ sealed interface Constraint {
3637
val label: String
3738

3839
/** Verifies that [description] satisfies this [Constraint]. */
39-
fun check(description: KeyDescription): Result
40+
fun check(description: KeyDescription, certPath: KeyAttestationCertPath): Result
4041
}
4142

4243
/**
@@ -113,15 +114,16 @@ fun constraintConfig(init: ConstraintConfigBuilder.() -> Unit): ConstraintConfig
113114
data object IgnoredConstraint : Constraint {
114115
override val label = "Ignored"
115116

116-
override fun check(description: KeyDescription) = Constraint.Satisfied
117+
override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) =
118+
Constraint.Satisfied
117119
}
118120

119121
/** Constraint that checks a single attribute of the [KeyDescription]. */
120122
@Immutable(containerOf = ["T"])
121123
sealed class AttributeConstraint<out T>(override val label: String, val mapper: AttributeMapper?) :
122124
Constraint {
123125
/** Evaluates whether the [description] is satisfied by this [AttributeConstraint]. */
124-
override fun check(description: KeyDescription) =
126+
override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) =
125127
if (isSatisfied(mapper?.invoke(description))) {
126128
Constraint.Satisfied
127129
} else {
@@ -157,21 +159,26 @@ sealed class AttributeConstraint<out T>(override val label: String, val mapper:
157159
*/
158160
@Immutable
159161
@RequiresApi(24)
160-
sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolean) : Constraint {
162+
sealed class SecurityLevelConstraint(
163+
val isSatisfied: (KeyDescription, KeyAttestationCertPath) -> Boolean
164+
) : Constraint {
161165
companion object {
162166
const val LABEL = "Security level"
163167
}
164168

165169
override val label = LABEL
166170

167-
override fun check(description: KeyDescription) =
168-
if (isSatisfied(description)) {
171+
override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) =
172+
if (isSatisfied(description, certPath)) {
169173
Constraint.Satisfied
170174
} else {
171-
Constraint.Violated(getFailureMessage(description))
175+
Constraint.Violated(getFailureMessage(description, certPath))
172176
}
173177

174-
fun getFailureMessage(description: KeyDescription): String =
178+
open fun getFailureMessage(
179+
description: KeyDescription,
180+
certPath: KeyAttestationCertPath,
181+
): String =
175182
"Security level violates constraint: " +
176183
"keyMintSecurityLevel=${description.keyMintSecurityLevel}, " +
177184
"attestationSecurityLevel=${description.attestationSecurityLevel}, " +
@@ -185,8 +192,8 @@ sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolea
185192
*/
186193
@Immutable
187194
data class STRICT(val expectedVal: SecurityLevel) :
188-
SecurityLevelConstraint({
189-
it.keyMintSecurityLevel == expectedVal && it.attestationSecurityLevel == expectedVal
195+
SecurityLevelConstraint({ desc, _ ->
196+
desc.keyMintSecurityLevel == expectedVal && desc.attestationSecurityLevel == expectedVal
190197
})
191198

192199
/**
@@ -195,9 +202,9 @@ sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolea
195202
*/
196203
@Immutable
197204
data object NOT_SOFTWARE :
198-
SecurityLevelConstraint({
199-
it.keyMintSecurityLevel == it.attestationSecurityLevel &&
200-
it.attestationSecurityLevel != SecurityLevel.SOFTWARE
205+
SecurityLevelConstraint({ desc, _ ->
206+
desc.keyMintSecurityLevel == desc.attestationSecurityLevel &&
207+
desc.attestationSecurityLevel != SecurityLevel.SOFTWARE
201208
})
202209

203210
/**
@@ -206,7 +213,39 @@ sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolea
206213
*/
207214
@Immutable
208215
data object CONSISTENT :
209-
SecurityLevelConstraint({ it.attestationSecurityLevel == it.keyMintSecurityLevel })
216+
SecurityLevelConstraint({ desc, _ ->
217+
desc.attestationSecurityLevel == desc.keyMintSecurityLevel
218+
})
219+
220+
/**
221+
* Checks that the keyMintSecurityLevel matches the security level claimed by the certificate.
222+
* this constraint may be used in conjunction with other security level constraints. e.g. it may
223+
* be combined with [STRICT] to verify that the keyMintSecurityLevel is precisely
224+
* [SecurityLevel.STRONG_BOX] and that the security level matches the value claimed by the
225+
* Google-signed certificate.
226+
*/
227+
@Immutable
228+
data object MATCHES_CERTIFICATE :
229+
SecurityLevelConstraint({ desc, certPath ->
230+
when (desc.keyMintSecurityLevel) {
231+
SecurityLevel.SOFTWARE ->
232+
certPath.securityLevel() == null || certPath.securityLevel() == SecurityLevel.SOFTWARE
233+
// Older cert chains do not make a TEE security level claim, so allow null. StrongBox certs
234+
// always explicitly claim the StrongBox security level, so there's no risk of a TEE cert
235+
// chain claiming to be StrongBox.
236+
SecurityLevel.TRUSTED_ENVIRONMENT ->
237+
certPath.securityLevel() == null ||
238+
certPath.securityLevel() == SecurityLevel.TRUSTED_ENVIRONMENT
239+
SecurityLevel.STRONG_BOX -> certPath.securityLevel() == SecurityLevel.STRONG_BOX
240+
}
241+
}) {
242+
override fun getFailureMessage(
243+
description: KeyDescription,
244+
certPath: KeyAttestationCertPath,
245+
): String =
246+
"Security level of KeyMint (${description.keyMintSecurityLevel}) does not match " +
247+
"attestation certificate (${certPath.securityLevel()})"
248+
}
210249
}
211250

212251
/**
@@ -224,7 +263,7 @@ sealed class TagOrderConstraint : Constraint {
224263
*/
225264
@Immutable
226265
data object STRICT : TagOrderConstraint() {
227-
override fun check(description: KeyDescription) =
266+
override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) =
228267
if (
229268
description.softwareEnforced.areTagsOrdered && description.hardwareEnforced.areTagsOrdered
230269
) {

src/main/kotlin/Verifier.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ constructor(
303303
}
304304

305305
for (constraint in constraintConfig.getConstraints()) {
306-
val result = constraint.check(keyDescription)
306+
val result = constraint.check(keyDescription, certPath)
307307
when (result) {
308308
is Constraint.Satisfied -> {}
309309
is Constraint.Violated -> {
@@ -314,6 +314,7 @@ constructor(
314314

315315
val securityLevel =
316316
minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)
317+
317318
val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust
318319
val verifiedBootState = rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED
319320
val deviceLocked = rootOfTrust?.deviceLocked ?: false

src/main/kotlin/provider/KeyAttestationCertPath.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import java.security.cert.X509Certificate
2626
import java.security.interfaces.DSAPublicKey
2727
import java.security.interfaces.ECPublicKey
2828
import java.security.interfaces.RSAPublicKey
29+
import java.util.Locale
2930
import javax.crypto.interfaces.DHPublicKey
3031
import javax.security.auth.x500.X500Principal
3132

@@ -103,14 +104,27 @@ class KeyAttestationCertPath(certs: List<X509Certificate>) : CertPath("X.509") {
103104
*/
104105
fun securityLevel() =
105106
when (provisioningMethod()) {
106-
ProvisioningMethod.FACTORY_PROVISIONED ->
107-
parseDN(intermediateCert().subjectX500Principal.getName(X500Principal.RFC1779))[TITLE_OID]
108-
.toSecurityLevel()
107+
ProvisioningMethod.UNKNOWN -> null
109108
ProvisioningMethod.REMOTELY_PROVISIONED ->
110109
parseDN(attestationCert().subjectX500Principal.getName(X500Principal.RFC1779))["O"]
111110
.toSecurityLevel()
112-
else -> SecurityLevel.SOFTWARE
111+
ProvisioningMethod.FACTORY_PROVISIONED -> securityLevelFromFactoryChain()
112+
}
113+
114+
private fun securityLevelFromFactoryChain(): SecurityLevel? {
115+
check(isFactoryProvisioned()) { "securityLevelFromFactoryChain() called on non-factory chain" }
116+
117+
// Only StrongBox factory chains are guaranteed to encode the security level. The logic here is
118+
// based on the Compatibility Test Suite (CTS) verification logic for StrongBox certs:
119+
// https://cs.android.com/android/platform/superproject/+/android-latest-release:cts/tests/tests/keystore/src/android/keystore/cts/KeyAttestationTest.java;drc=0ef830e0aa7c51200c1e4c7a24169fede350f1ba;l=2430
120+
for (cert in arrayOf(attestationCert(), intermediateCert())) {
121+
val name = cert.subjectX500Principal.getName(X500Principal.RFC1779)
122+
if (name.lowercase(Locale.ROOT).contains("strongbox")) {
123+
return SecurityLevel.STRONG_BOX
124+
}
113125
}
126+
return null
127+
}
114128

115129
/**
116130
* Returns the leaf certificate from the certificate chain.

src/main/kotlin/testing/Certs.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,26 @@ object CertLists {
116116
)
117117
}
118118

119+
/* A chain with TEE in the last intermediate certificate but keymint attributes claiming StrongBox. */
120+
val teeIntermediateWithStrongBoxKeyMintAttributes by lazy {
121+
listOf(
122+
certFactory.generateLeafCert(extension = certFactory.STRONG_BOX_KEY_DESCRIPTION_EXT),
123+
Certs.factoryAttestation,
124+
Certs.factoryIntermediate,
125+
Certs.root,
126+
)
127+
}
128+
129+
/* A chain with StrongBox in the last intermediate certificate but keymint attributes claiming TEE. */
130+
val strongBoxIntermediateWithTeeKeyMintAttributes by lazy {
131+
listOf(
132+
certFactory.generateLeafCert(),
133+
certFactory.generateAttestationCert(issuer = certFactory.strongBoxIntermediate.subject),
134+
certFactory.strongBoxIntermediate,
135+
Certs.root,
136+
)
137+
}
138+
119139
/* A valid remotely provisioned chain for TEE keys. */
120140
val validRemotelyProvisioned by lazy {
121141
val rkpAttestationCert =

src/test/kotlin/ConstraintConfigTest.kt

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,19 @@ class ConstraintConfigTest {
6464
val kd =
6565
keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("foo"))
6666

67-
assertIs<Constraint.Satisfied>(level.check(kd))
68-
assertIs<Constraint.Violated>(level.check(kd.copy(uniqueId = ByteString.copyFromUtf8("bar"))))
67+
assertIs<Constraint.Satisfied>(level.check(kd, testCertPath))
68+
assertIs<Constraint.Violated>(
69+
level.check(kd.copy(uniqueId = ByteString.copyFromUtf8("bar")), testCertPath)
70+
)
6971
}
7072

7173
@Test
7274
fun AttributeConstraintIsSatisfied_notNull_allowsAnyValue() {
7375
val level = AttributeConstraint.NOT_NULL("Root of trust") { it.hardwareEnforced.rootOfTrust }
7476

75-
assertIs<Constraint.Violated>(level.check(keyDescriptionWithSoftwareSecurityLevels))
77+
assertIs<Constraint.Violated>(
78+
level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath)
79+
)
7680

7781
val kdWithRot =
7882
keyDescriptionWithSoftwareSecurityLevels.copy(
@@ -81,48 +85,66 @@ class ConstraintConfigTest {
8185
rootOfTrust = RootOfTrust(ByteString.empty(), false, VerifiedBootState.VERIFIED)
8286
)
8387
)
84-
assertIs<Constraint.Satisfied>(level.check(kdWithRot))
88+
assertIs<Constraint.Satisfied>(level.check(kdWithRot, testCertPath))
8589
}
8690

8791
@Test
8892
fun SecurityLevelConstraintIsSatisfied_strictWithExpectedValue() {
8993
val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX)
9094

91-
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithStrongBoxSecurityLevels))
92-
assertIs<Constraint.Violated>(level.check(keyDescriptionWithTeeSecurityLevels))
93-
assertIs<Constraint.Violated>(level.check(keyDescriptionWithMismatchedSecurityLevels))
95+
assertIs<Constraint.Satisfied>(
96+
level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath)
97+
)
98+
assertIs<Constraint.Violated>(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath))
99+
assertIs<Constraint.Violated>(
100+
level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath)
101+
)
94102
}
95103

96104
@Test
97105
fun SecurityLevelConstraintIsSatisfied_notSoftware_allowsAnyNonSoftwareMatchingLevels() {
98106
val level = SecurityLevelConstraint.NOT_SOFTWARE
99107

100-
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithStrongBoxSecurityLevels))
101-
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithTeeSecurityLevels))
102-
assertIs<Constraint.Violated>(level.check(keyDescriptionWithSoftwareSecurityLevels))
103-
assertIs<Constraint.Violated>(level.check(keyDescriptionWithMismatchedSecurityLevels))
108+
assertIs<Constraint.Satisfied>(
109+
level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath)
110+
)
111+
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath))
112+
assertIs<Constraint.Violated>(
113+
level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath)
114+
)
115+
assertIs<Constraint.Violated>(
116+
level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath)
117+
)
104118
}
105119

106120
@Test
107121
fun SecurityLevelConstraintIsSatisfied_consistent_allowsAnyMatchingLevels() {
108122
val level = SecurityLevelConstraint.CONSISTENT
109123

110-
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithStrongBoxSecurityLevels))
111-
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithTeeSecurityLevels))
112-
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithSoftwareSecurityLevels))
113-
assertIs<Constraint.Violated>(level.check(keyDescriptionWithMismatchedSecurityLevels))
124+
assertIs<Constraint.Satisfied>(
125+
level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath)
126+
)
127+
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath))
128+
assertIs<Constraint.Satisfied>(
129+
level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath)
130+
)
131+
assertIs<Constraint.Violated>(
132+
level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath)
133+
)
114134
}
115135

116136
@Test
117137
fun AuthorizationListOrderingIsSatisfied_strictWithUnorderedTags_fails() {
118138
val ordering = TagOrderConstraint.STRICT
119139

120-
assertIs<Constraint.Satisfied>(ordering.check(keyDescriptionWithStrongBoxSecurityLevels))
140+
assertIs<Constraint.Satisfied>(
141+
ordering.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath)
142+
)
121143

122144
val kdUnordered =
123145
KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!!
124146

125-
assertIs<Constraint.Violated>(ordering.check(kdUnordered))
147+
assertIs<Constraint.Violated>(ordering.check(kdUnordered, testCertPath))
126148
}
127149

128150
@Test
@@ -131,7 +153,7 @@ class ConstraintConfigTest {
131153
val kd =
132154
keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("bar"))
133155

134-
val violation = assertIs<Constraint.Violated>(level.check(kd))
156+
val violation = assertIs<Constraint.Violated>(level.check(kd, testCertPath))
135157
assertThat(violation.failureMessage)
136158
.isEqualTo("Unique ID violates constraint: value=bar, config=$level")
137159
}
@@ -140,7 +162,8 @@ class ConstraintConfigTest {
140162
fun securityLevelConstraint_withViolation_returnsCorrectMessage() {
141163
val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX)
142164

143-
val violation = assertIs<Constraint.Violated>(level.check(keyDescriptionWithTeeSecurityLevels))
165+
val violation =
166+
assertIs<Constraint.Violated>(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath))
144167
assertThat(violation.failureMessage)
145168
.isEqualTo(
146169
"Security level violates constraint: keyMintSecurityLevel=TRUSTED_ENVIRONMENT, " +
@@ -154,7 +177,7 @@ class ConstraintConfigTest {
154177
val kdUnordered =
155178
KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!!
156179

157-
val violation = assertIs<Constraint.Violated>(level.check(kdUnordered))
180+
val violation = assertIs<Constraint.Violated>(level.check(kdUnordered, testCertPath))
158181
assertThat(violation.failureMessage)
159182
.isEqualTo("Authorization list tags must be in ascending order")
160183
}

0 commit comments

Comments
 (0)