1616
1717package com.android.keyattestation.verifier
1818
19+ import androidx.annotation.RequiresApi
1920import co.nstant.`in`.cbor.CborDecoder
2021import co.nstant.`in`.cbor.CborEncoder
2122import co.nstant.`in`.cbor.CborException
@@ -57,6 +58,11 @@ import org.bouncycastle.asn1.DERSet
5758import org.bouncycastle.asn1.DERTaggedObject
5859import 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
6167data class ProvisioningInfoMap (
6268 val certificatesIssued : Int ,
@@ -96,15 +102,16 @@ data class ProvisioningInfoMap(
96102 }
97103}
98104
105+ @Immutable
99106data 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? ,
107+ val brand : String? = null ,
108+ val device : String? = null ,
109+ val product : String? = null ,
110+ val serialNumber : String? = null ,
111+ @SuppressWarnings( " Immutable " ) val imeis : Set <String > = emptySet() ,
112+ val meid : String? = null ,
113+ val manufacturer : String? = null ,
114+ val model : String? = null ,
108115) {
109116 companion object {
110117 @JvmStatic
@@ -126,6 +133,7 @@ data class DeviceIdentity(
126133}
127134
128135@Immutable
136+ @RequiresApi(24 )
129137data class KeyDescription (
130138 val attestationVersion : BigInteger ,
131139 val attestationSecurityLevel : SecurityLevel ,
@@ -160,23 +168,25 @@ data class KeyDescription(
160168 @JvmField val OID = ASN1ObjectIdentifier (" 1.3.6.1.4.1.11129.2.1.17" )
161169
162170 @JvmStatic
163- fun parseFrom (cert : X509Certificate ) =
171+ @JvmOverloads
172+ fun parseFrom (cert : X509Certificate , logFn : (String ) -> Unit = {}) =
164173 cert
165174 .getExtensionValue(OID .id)
166175 .let { ASN1OctetString .getInstance(it).octets }
167- .let { parseFrom(it) }
176+ .let { parseFrom(it, logFn ) }
168177
169178 @JvmStatic
170- fun parseFrom (bytes : ByteArray ) =
179+ @JvmOverloads
180+ fun parseFrom (bytes : ByteArray , logFn : (String ) -> Unit = {}) =
171181 try {
172- from(ASN1Sequence .getInstance(bytes))
182+ from(ASN1Sequence .getInstance(bytes), logFn )
173183 } catch (e: NullPointerException ) {
174184 // Workaround for a NPE in BouncyCastle.
175185 // https://github.com/bcgit/bc-java/blob/228211ecb973fe87fdd0fc4ab16ba0446ec1a29c/core/src/main/java/org/bouncycastle/asn1/ASN1UniversalType.java#L24
176186 throw IllegalArgumentException (e)
177187 }
178188
179- private fun from (seq : ASN1Sequence ): KeyDescription {
189+ private fun from (seq : ASN1Sequence , logFn : ( String ) -> Unit = {} ): KeyDescription {
180190 require(seq.size() == 8 )
181191 return KeyDescription (
182192 attestationVersion = seq.getObjectAt(0 ).toInt(),
@@ -185,8 +195,8 @@ data class KeyDescription(
185195 keyMintSecurityLevel = seq.getObjectAt(3 ).toSecurityLevel(),
186196 attestationChallenge = seq.getObjectAt(4 ).toByteString(),
187197 uniqueId = seq.getObjectAt(5 ).toByteString(),
188- softwareEnforced = seq.getObjectAt(6 ).toAuthorizationList(),
189- hardwareEnforced = seq.getObjectAt(7 ).toAuthorizationList(),
198+ softwareEnforced = seq.getObjectAt(6 ).toAuthorizationList(logFn ),
199+ hardwareEnforced = seq.getObjectAt(7 ).toAuthorizationList(logFn ),
190200 )
191201 }
192202 }
@@ -218,7 +228,7 @@ enum class Origin(val value: Long) {
218228 RESERVED (3 ),
219229 SECURELY_IMPORTED (4 );
220230
221- internal fun toAsn1 () = ASN1Integer (value)
231+ fun toAsn1 () = ASN1Integer (value)
222232}
223233
224234/* *
@@ -227,6 +237,7 @@ enum class Origin(val value: Long) {
227237 * @see
228238 * https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/Tag.aidl
229239 */
240+ @RequiresApi(24 )
230241enum class KeyMintTag (val value : Int ) {
231242 PURPOSE (1 ),
232243 ALGORITHM (2 ),
@@ -269,7 +280,10 @@ enum class KeyMintTag(val value: Int) {
269280 companion object {
270281 fun from (value : Int ) =
271282 values().firstOrNull { it.value == value }
272- ? : throw IllegalArgumentException (" unknown tag number: $value " )
283+ ? : throw ExtensionParsingException (
284+ " unknown tag number: $value " ,
285+ KeyAttestationReason .UNKNOWN_TAG_NUMBER ,
286+ )
273287 }
274288}
275289
@@ -280,6 +294,7 @@ enum class KeyMintTag(val value: Int) {
280294 * https://source.android.com/docs/security/features/keystore/attestation#authorizationlist-fields
281295 */
282296@Immutable
297+ @RequiresApi(24 )
283298data class AuthorizationList (
284299 @SuppressWarnings(" Immutable" ) val purposes : Set <BigInteger >? = null ,
285300 val algorithms : BigInteger ? = null ,
@@ -397,7 +412,7 @@ data class AuthorizationList(
397412 .let { DERSequence (it.toTypedArray()) }
398413
399414 internal companion object {
400- fun from (seq : ASN1Sequence , validateTagOrder : Boolean = false ): AuthorizationList {
415+ fun from (seq : ASN1Sequence , logFn : ( String ) -> Unit = { _ -> } ): AuthorizationList {
401416 val objects =
402417 seq.associate {
403418 require(it is ASN1TaggedObject ) {
@@ -417,9 +432,8 @@ data class AuthorizationList(
417432 * 2. within each class of tags, the elements or alternatives shall appear in ascending order
418433 * of their tag numbers.
419434 */
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" )
435+ if (! objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs }) {
436+ logFn(" AuthorizationList tags should appear in ascending order" )
423437 }
424438
425439 return AuthorizationList (
@@ -449,7 +463,7 @@ data class AuthorizationList(
449463 rollbackResistant = if (objects.containsKey(KeyMintTag .ROLLBACK_RESISTANT )) true else null ,
450464 rootOfTrust = objects[KeyMintTag .ROOT_OF_TRUST ]?.toRootOfTrust(),
451465 osVersion = objects[KeyMintTag .OS_VERSION ]?.toInt(),
452- osPatchLevel = objects[KeyMintTag .OS_PATCH_LEVEL ]?.toPatchLevel(),
466+ osPatchLevel = objects[KeyMintTag .OS_PATCH_LEVEL ]?.toPatchLevel(" OS " , logFn ),
453467 attestationApplicationId =
454468 objects[KeyMintTag .ATTESTATION_APPLICATION_ID ]?.toAttestationApplicationId(),
455469 attestationIdBrand = objects[KeyMintTag .ATTESTATION_ID_BRAND ]?.toStr(),
@@ -460,8 +474,8 @@ data class AuthorizationList(
460474 attestationIdMeid = objects[KeyMintTag .ATTESTATION_ID_MEID ]?.toStr(),
461475 attestationIdManufacturer = objects[KeyMintTag .ATTESTATION_ID_MANUFACTURER ]?.toStr(),
462476 attestationIdModel = objects[KeyMintTag .ATTESTATION_ID_MODEL ]?.toStr(),
463- vendorPatchLevel = objects[KeyMintTag .VENDOR_PATCH_LEVEL ]?.toPatchLevel(),
464- bootPatchLevel = objects[KeyMintTag .BOOT_PATCH_LEVEL ]?.toPatchLevel(),
477+ vendorPatchLevel = objects[KeyMintTag .VENDOR_PATCH_LEVEL ]?.toPatchLevel(" vendor " , logFn ),
478+ bootPatchLevel = objects[KeyMintTag .BOOT_PATCH_LEVEL ]?.toPatchLevel(" boot " , logFn ),
465479 attestationIdSecondImei = objects[KeyMintTag .ATTESTATION_ID_SECOND_IMEI ]?.toStr(),
466480 moduleHash = objects[KeyMintTag .MODULE_HASH ]?.toByteString(),
467481 )
@@ -480,14 +494,24 @@ data class PatchLevel(val yearMonth: YearMonth, val version: Int? = null) {
480494 }
481495
482496 companion object {
483- fun from (patchLevel : ASN1Encodable ): PatchLevel ? {
497+ fun from (
498+ patchLevel : ASN1Encodable ,
499+ partitionName : String = "",
500+ logFn : (String ) -> Unit = { _ -> },
501+ ): PatchLevel ? {
484502 check(patchLevel is ASN1Integer ) { " Must be an ASN1Integer, was ${this ::class .simpleName} " }
485- return from(patchLevel.value.toString())
503+ return from(patchLevel.value.toString(), partitionName, logFn )
486504 }
487505
488506 @JvmStatic
489- fun from (patchLevel : String ): PatchLevel ? {
507+ @JvmOverloads
508+ fun from (
509+ patchLevel : String ,
510+ partitionName : String = "",
511+ logFn : (String ) -> Unit = { _ -> },
512+ ): PatchLevel ? {
490513 if (patchLevel.length != 6 && patchLevel.length != 8 ) {
514+ logFn(" Invalid $partitionName patch level: $patchLevel " )
491515 return null
492516 }
493517 try {
@@ -496,6 +520,7 @@ data class PatchLevel(val yearMonth: YearMonth, val version: Int? = null) {
496520 val version = if (patchLevel.length == 8 ) patchLevel.substring(6 ).toInt() else null
497521 return PatchLevel (yearMonth, version)
498522 } catch (e: DateTimeParseException ) {
523+ logFn(" Invalid $partitionName patch level: $patchLevel " )
499524 return null
500525 }
501526 }
@@ -625,12 +650,10 @@ private fun ASN1Encodable.toAttestationApplicationId(): AttestationApplicationId
625650 return AttestationApplicationId .from(ASN1Sequence .getInstance(this .octets))
626651}
627652
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 {
653+ @RequiresApi(24 )
654+ private fun ASN1Encodable.toAuthorizationList (logFn : (String ) -> Unit ): AuthorizationList {
632655 check(this is ASN1Sequence ) { " Object must be an ASN1Sequence, was ${this ::class .simpleName} " }
633- return AuthorizationList .from(this , validateTagOrder )
656+ return AuthorizationList .from(this , logFn )
634657}
635658
636659private fun ASN1Encodable.toBoolean (): Boolean {
@@ -657,7 +680,10 @@ private fun ASN1Encodable.toInt(): BigInteger {
657680 return this .value
658681}
659682
660- private fun ASN1Encodable.toPatchLevel (): PatchLevel ? = PatchLevel .from(this )
683+ private fun ASN1Encodable.toPatchLevel (
684+ partitionName : String = "",
685+ logFn : (String ) -> Unit = { _ -> },
686+ ): PatchLevel ? = PatchLevel .from(this , partitionName, logFn)
661687
662688private fun ASN1Encodable.toRootOfTrust (): RootOfTrust {
663689 check(this is ASN1Sequence ) { " Object must be an ASN1Sequence, was ${this ::class .simpleName} " }
0 commit comments