From ef4e3ee41b32a2ab938b70118c66d6f8388906e8 Mon Sep 17 00:00:00 2001 From: microshine Date: Wed, 18 Oct 2023 01:03:15 +0200 Subject: [PATCH] fix: support wrong order fields for KeyDescription --- packages/android/README.md | 68 +++++++++++++++- packages/android/src/index.ts | 4 +- packages/android/src/key_description.ts | 49 +++++++++-- packages/android/src/nonstandard.ts | 104 ++++++++++++++++++++++++ packages/android/test/test.ts | 94 +++++++++++++++++++-- 5 files changed, 304 insertions(+), 15 deletions(-) create mode 100644 packages/android/src/nonstandard.ts diff --git a/packages/android/README.md b/packages/android/README.md index d9f4b18..a0ecce1 100644 --- a/packages/android/README.md +++ b/packages/android/README.md @@ -5,4 +5,70 @@ [![NPM](https://nodei.co/npm/@peculiar/asn1-android.png)](https://nodei.co/npm/@peculiar/asn1-android/) -[Android key attestation schema](https://source.android.com/security/keystore/attestation#schema) \ No newline at end of file +- [Android key attestation schema](https://source.android.com/security/keystore/attestation#schema) +- [Key attestation extension data schema](https://developer.android.com/privacy-and-security/security-key-attestation#key_attestation_ext_schema) +- [AttestationApplicationId](https://developer.android.com/privacy-and-security/security-key-attestation#key_attestation_ext_schema_attestationid) + +## KeyDescription and NonStandardKeyDescription + +The `KeyDescription` class in this library represents the ASN.1 schema for the Android Keystore Key Description structure. However, in practice, there are cases where the `AuthorizationList` fields in the `softwareEnforced` and `teeEnforced` fields are not strictly ordered, which can lead to ASN.1 structure reading errors. + +To address this issue, this library provides a `NonStandardKeyDescription` class that can read such structures. However, when creating extensions, it is recommended to use `KeyDescription`, as it guarantees the order of object fields according to the specification. + +Here are simplified TypeScript examples: + +Example of creating a `KeyDescription` object in TypeScript for the Android Keystore system + +```typescript +const attestation = new android.AttestationApplicationId({ + packageInfos: [ + new android.AttestationPackageInfo({ + packageName: new OctetString(Buffer.from("123", "utf8")), + version: 1, + }), + ], + signatureDigests: [ + new OctetString(Buffer.from("123", "utf8")), + ], +}); + +const keyDescription = new KeyDescription({ + attestationVersion: android.Version.v200, + attestationSecurityLevel: android.SecurityLevel.software, + keymasterVersion: 1, + keymasterSecurityLevel: android.SecurityLevel.software, + attestationChallenge: new OctetString(Buffer.from("123", "utf8")), + uniqueId: new OctetString(Buffer.from("123", "utf8")), + softwareEnforced: new android.AuthorizationList({ + creationDateTime: 1506793476000, + attestationApplicationId: new OctetString(AsnConvert.serialize(attestation)), + }), + teeEnforced: new android.AuthorizationList({ + purpose: new android.IntegerSet([1]), + algorithm: 1, + keySize: 1, + digest: new android.IntegerSet([1]), + ecCurve: 1, + userAuthType: 1, + origin: 1, + rollbackResistant: null, + }), +}); + +const raw = AsnConvert.serialize(keyDescription); +``` + +Example of reading a `NonStandardKeyDescription` object in TypeScript + +```typescript +const keyDescription = AsnConvert.parse(raw, NonStandardKeyDescription); + +console.log(keyDescription.attestationVersion); // 100 +console.log(keyDescription.attestationSecurityLevel); // 1 +console.log(keyDescription.keymasterVersion); // 100 +console.log(keyDescription.keymasterSecurityLevel); // 1 +console.log(keyDescription.attestationChallenge.byteLength); // 32 +console.log(keyDescription.uniqueId.byteLength); // 0 +console.log(keyDescription.softwareEnforced.findProperty("attestationApplicationId")?.byteLength); // 81 +console.log(keyDescription.teeEnforced.findProperty("attestationIdBrand")?.byteLength); // 8 +``` \ No newline at end of file diff --git a/packages/android/src/index.ts b/packages/android/src/index.ts index c3c9948..f3d27a1 100644 --- a/packages/android/src/index.ts +++ b/packages/android/src/index.ts @@ -1 +1,3 @@ -export * from "./key_description"; \ No newline at end of file +export * from "./key_description"; +export * from "./nonstandard"; +export * from "./attestation"; diff --git a/packages/android/src/key_description.ts b/packages/android/src/key_description.ts index 0016a8a..1c8b91f 100644 --- a/packages/android/src/key_description.ts +++ b/packages/android/src/key_description.ts @@ -1,9 +1,18 @@ import { AsnProp, AsnPropTypes, AsnArray, AsnType, AsnTypeTypes, OctetString } from "@peculiar/asn1-schema"; +/** + * Extension OID for key description. + * + * ```asn + * id-ce-keyDescription OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 11129 2 1 17 } + * ``` + */ export const id_ce_keyDescription = "1.3.6.1.4.1.11129.2.1.17"; /** - * ``` + * Implements ASN.1 structure for attestation package info. + * + * ```asn * VerifiedBootState ::= ENUMERATED { * Verified (0), * SelfSigned (1), @@ -20,7 +29,9 @@ export enum VerifiedBootState { } /** - * ``` + * Implements ASN.1 structure for root of trust. + * + * ```asn * RootOfTrust ::= SEQUENCE { * verifiedBootKey OCTET_STRING, * deviceLocked BOOLEAN, @@ -51,8 +62,15 @@ export class RootOfTrust { } } +/** + * Implements ASN.1 structure for set of integers. + * + * ```asn + * IntegerSet ::= SET OF INTEGER + * ``` + */ @AsnType({ type: AsnTypeTypes.Set, itemType: AsnPropTypes.Integer }) -export class IntegerSet extends AsnArray { +export class IntegerSet extends AsnArray { constructor(items?: number[]) { super(items); @@ -64,7 +82,9 @@ export class IntegerSet extends AsnArray { } /** - * ``` + * Implements ASN.1 structure for authorization list. + * + * ```asn * AuthorizationList ::= SEQUENCE { * purpose [1] EXPLICIT SET OF INTEGER OPTIONAL, * algorithm [2] EXPLICIT INTEGER OPTIONAL, @@ -74,6 +94,7 @@ export class IntegerSet extends AsnArray { * ecCurve [10] EXPLICIT INTEGER OPTIONAL, * rsaPublicExponent [200] EXPLICIT INTEGER OPTIONAL, * rollbackResistance [303] EXPLICIT NULL OPTIONAL, # KM4 + * earlyBootOnly [305] EXPLICIT NULL OPTIONAL, # version 4 * activeDateTime [400] EXPLICIT INTEGER OPTIONAL * originationExpireDateTime [401] EXPLICIT INTEGER OPTIONAL * usageExpireDateTime [402] EXPLICIT INTEGER OPTIONAL @@ -103,6 +124,7 @@ export class IntegerSet extends AsnArray { * attestationIdModel [717] EXPLICIT OCTET_STRING OPTIONAL, # KM3 * vendorPatchLevel [718] EXPLICIT INTEGER OPTIONAL, # KM4 * bootPatchLevel [719] EXPLICIT INTEGER OPTIONAL, # KM4 + * deviceUniqueAttestation [720] EXPLICIT NULL OPTIONAL, # version 4 * } * ``` */ @@ -131,6 +153,9 @@ export class AuthorizationList { @AsnProp({ context: 303, type: AsnPropTypes.Null, optional: true }) public rollbackResistance?: null; + @AsnProp({ context: 305, type: AsnPropTypes.Null, optional: true }) + public earlyBootOnly?: null; + @AsnProp({ context: 400, type: AsnPropTypes.Integer, optional: true }) public activeDateTime?: number; @@ -218,13 +243,18 @@ export class AuthorizationList { @AsnProp({ context: 719, type: AsnPropTypes.Integer, optional: true }) public bootPatchLevel?: number; + @AsnProp({ context: 720, type: AsnPropTypes.Null, optional: true }) + public deviceUniqueAttestation?: null; + public constructor(params: Partial = {}) { Object.assign(this, params); } } /** - * ``` + * Implements ASN.1 structure for security level. + * + * ```asn * SecurityLevel ::= ENUMERATED { * Software (0), * TrustedEnvironment (1), @@ -242,12 +272,17 @@ export enum Version { KM2 = 1, KM3 = 2, KM4 = 3, + v4 = 4, + v100 = 100, + v200 = 200, } /** - * ``` + * Implements ASN.1 structure for key description. + * + * ```asn * KeyDescription ::= SEQUENCE { - * attestationVersion INTEGER, # KM2 value is 1. KM3 value is 2. KM4 value is 3. + * attestationVersion INTEGER, # versions 1, 2, 3, 4, 100, and 200 * attestationSecurityLevel SecurityLevel, * keymasterVersion INTEGER, * keymasterSecurityLevel SecurityLevel, diff --git a/packages/android/src/nonstandard.ts b/packages/android/src/nonstandard.ts new file mode 100644 index 0000000..119b178 --- /dev/null +++ b/packages/android/src/nonstandard.ts @@ -0,0 +1,104 @@ +import { AsnProp, AsnPropTypes, AsnArray, AsnType, AsnTypeTypes, OctetString } from "@peculiar/asn1-schema"; +import { AuthorizationList, SecurityLevel, Version } from "./key_description"; + +/** + * This file contains classes to handle non-standard key descriptions and authorizations. + * + * Due to an issue with the asn1-schema library, referenced at https://github.com/PeculiarVentures/asn1-schema/issues/98#issuecomment-1764345351, + * the standard key description does not allow for a non-strict order of fields in the `softwareEnforced` and `teeEnforced` attributes. + * + * To address this and provide greater flexibility, the `NonStandardKeyDescription` and + * `NonStandardAuthorizationList` classes were created, allowing for the use of non-standard authorizations and a flexible field order. + * + * The purpose of these modifications is to ensure compatibility with specific requirements and standards, as well as to offer + * more convenient tools for working with key descriptions and authorizations. + * + * Please refer to the documentation and class comments before using or modifying them. + */ + +/** + * Represents a non-standard authorization for NonStandardAuthorizationList. It uses the same + * structure as AuthorizationList, but it is a CHOICE instead of a SEQUENCE, that allows for + * non-strict ordering of fields. + */ +@AsnType({ type: AsnTypeTypes.Choice }) +export class NonStandardAuthorization extends AuthorizationList { } + +/** + * Represents a list of non-standard authorizations. + * ```asn + * NonStandardAuthorizationList ::= SEQUENCE OF NonStandardAuthorization + * ``` + */ +@AsnType({ type: AsnTypeTypes.Sequence, itemType: NonStandardAuthorization }) +export class NonStandardAuthorizationList extends AsnArray { + constructor(items?: NonStandardAuthorization[]) { + super(items); + + // Set the prototype explicitly. + Object.setPrototypeOf(this, NonStandardAuthorizationList.prototype); + } + + /** + * Finds the first authorization that contains the specified key. + * @param key The key to search for. + * @returns The first authorization that contains the specified key, or `undefined` if not found. + */ + findProperty(key: K): AuthorizationList[K] | undefined { + const prop = this.find((o => key in o)); + if (prop) { + return prop[key]; + } + return undefined; + } +} + +/** + * The AuthorizationList class allows for non-strict ordering of fields in the + * softwareEnforced and teeEnforced fields. + * + * This behavior is due to an issue with the asn1-schema library, which is + * documented here: https://github.com/PeculiarVentures/asn1-schema/issues/98#issuecomment-1764345351 + * + * ```asn + * KeyDescription ::= SEQUENCE { + * attestationVersion INTEGER, # versions 1, 2, 3, 4, 100, and 200 + * attestationSecurityLevel SecurityLevel, + * keymasterVersion INTEGER, + * keymasterSecurityLevel SecurityLevel, + * attestationChallenge OCTET_STRING, + * uniqueId OCTET_STRING, + * softwareEnforced NonStandardAuthorizationList, + * teeEnforced NonStandardAuthorizationList, + * } + * ``` + */ +export class NonStandardKeyDescription { + @AsnProp({ type: AsnPropTypes.Integer }) + public attestationVersion: Version = Version.KM4; + + @AsnProp({ type: AsnPropTypes.Enumerated }) + public attestationSecurityLevel: SecurityLevel = SecurityLevel.software; + + @AsnProp({ type: AsnPropTypes.Integer }) + public keymasterVersion = 0; + + @AsnProp({ type: AsnPropTypes.Enumerated }) + public keymasterSecurityLevel: SecurityLevel = SecurityLevel.software; + + @AsnProp({ type: OctetString }) + public attestationChallenge = new OctetString(); + + @AsnProp({ type: OctetString }) + public uniqueId = new OctetString(); + + @AsnProp({ type: NonStandardAuthorizationList }) + public softwareEnforced = new NonStandardAuthorizationList(); + + @AsnProp({ type: NonStandardAuthorizationList }) + public teeEnforced = new NonStandardAuthorizationList(); + + public constructor(params: Partial = {}) { + Object.assign(this, params); + } +} \ No newline at end of file diff --git a/packages/android/test/test.ts b/packages/android/test/test.ts index d83dcf2..3bb7999 100644 --- a/packages/android/test/test.ts +++ b/packages/android/test/test.ts @@ -1,6 +1,6 @@ import * as assert from "assert"; -import { AsnConvert } from "@peculiar/asn1-schema"; -import { KeyDescription, Version, SecurityLevel } from "../src/key_description"; +import { AsnConvert, OctetString } from "@peculiar/asn1-schema"; +import * as android from "@peculiar/asn1-android"; context("Android", () => { @@ -10,12 +10,12 @@ context("Android", () => { // https://github.com/PeculiarVentures/asn1-schema/issues/23#issuecomment-656408096 const hex = "3081cf0201020a01000201010a010004209f54497cde948349eae4f48de970808d4ddcdce4ddeee23b76d5c5ddcc1b898e04003069bf853d080206015ed3e3cfa0bf85455904573055312f302d0428636f6d2e616e64726f69642e6b657973746f72652e616e64726f69646b657973746f726564656d6f0201013122042074cfcb507488f529108591c7a505919f327732fbc1d803526aea980006d2d8983032a1053103020102a203020103a30402020100a5053103020104aa03020101bf837803020102bf853e03020100bf853f020500"; - const kd = AsnConvert.parse(Buffer.from(hex, "hex"), KeyDescription); + const kd = AsnConvert.parse(Buffer.from(hex, "hex"), android.KeyDescription); - assert.strictEqual(kd.attestationVersion, Version.KM3); - assert.strictEqual(kd.attestationSecurityLevel, SecurityLevel.software); + assert.strictEqual(kd.attestationVersion, android.Version.KM3); + assert.strictEqual(kd.attestationSecurityLevel, android.SecurityLevel.software); assert.strictEqual(kd.keymasterVersion, 1); - assert.strictEqual(kd.keymasterSecurityLevel, SecurityLevel.software); + assert.strictEqual(kd.keymasterSecurityLevel, android.SecurityLevel.software); assert.strictEqual(kd.attestationChallenge.byteLength, 32); assert.strictEqual(kd.uniqueId.byteLength, 0); assert.strictEqual(kd.softwareEnforced.creationDateTime, "1506793476000"); @@ -37,4 +37,86 @@ context("Android", () => { }); + context("NonStandardKeyDescription", () => { + it("parse", () => { + const hex = "308201820201640A01010201640A01010420316231616461333561633432366562316638343563383765653239663065633304003057BF8545530451304F312930270421636F6D2E7469636B7069636B6C6C632E63656F627269656E2E7469636B7069636B0202012931220420CE016851B704DA76FDEDDE34AB314A155CA5A5DB31266D2685FCBF281AB510283081F6A1083106020103020102A203020103A30402020100A5053103020104AA03020101BF8377020500BF853E03020100BF85404C304A04209FB52F0954613F221AF4F4070C31415ED44C1A81D51889DB0946632599B3E9460101FF0A01000420FFAEEC3477824DD82E09B6400602DCB274EB4E89DCB6093AD1F6EDE964ED73C3BF854105020301D4C0BF8542050203031644BF854D1604146D6F746F726F6C61206564676520283230323229BF854C0A04086D6F746F726F6C61BF85480D040B7465736C615F675F737973BF85470704057465736C61BF85460A04086D6F746F726F6C61BF854E0602040134B291BF854F0602040134B291"; + + const kd = AsnConvert.parse(Buffer.from(hex, "hex"), android.NonStandardKeyDescription); + assert.strictEqual(kd.attestationVersion, 100); + assert.strictEqual(kd.attestationSecurityLevel, android.SecurityLevel.trustedEnvironment); + assert.strictEqual(kd.keymasterVersion, 100); + assert.strictEqual(kd.keymasterSecurityLevel, android.SecurityLevel.trustedEnvironment); + assert.strictEqual(kd.attestationChallenge.byteLength, 32); + assert.strictEqual(kd.uniqueId.byteLength, 0); + assert.strictEqual(kd.softwareEnforced.findProperty("attestationApplicationId")?.byteLength, 81); + assert.strictEqual(kd.teeEnforced.findProperty("attestationIdBrand")?.byteLength, 8); + }); + + it("should create standard KeyDescription and parse using NonStandardKeyDescription", () => { + const attestation = new android.AttestationApplicationId({ + packageInfos: [ + new android.AttestationPackageInfo({ + packageName: new OctetString(Buffer.from("123", "utf8")), + version: 1, + }), + ], + signatureDigests: [ + new OctetString(Buffer.from("123", "utf8")), + ], + }); + const kd = new android.KeyDescription({ + attestationVersion: android.Version.v200, + attestationSecurityLevel: android.SecurityLevel.software, + keymasterVersion: 1, + keymasterSecurityLevel: android.SecurityLevel.software, + attestationChallenge: new OctetString(Buffer.from("123", "utf8")), + uniqueId: new OctetString(Buffer.from("123", "utf8")), + softwareEnforced: new android.AuthorizationList({ + creationDateTime: 1506793476000, + attestationApplicationId: new OctetString(AsnConvert.serialize(attestation)), + }), + teeEnforced: new android.AuthorizationList({ + purpose: new android.IntegerSet([1]), + algorithm: 1, + keySize: 1, + digest: new android.IntegerSet([1]), + ecCurve: 1, + userAuthType: 1, + origin: 1, + rollbackResistant: null, + }), + }); + + const raw = AsnConvert.serialize(kd); + const kd2 = AsnConvert.parse(raw, android.NonStandardKeyDescription); + + assert.strictEqual(kd2.attestationVersion, 200); + }); + }); + + context("AttestationApplicationId", () => { + it("serialize/parse", () => { + const attestation = new android.AttestationApplicationId({ + packageInfos: [ + new android.AttestationPackageInfo({ + packageName: new OctetString(Buffer.from("123", "utf8")), + version: 1, + }), + ], + signatureDigests: [ + new OctetString(Buffer.from("123", "utf8")), + ], + }); + const raw = AsnConvert.serialize(attestation); + console.log(Buffer.from(raw).toString("hex")); + + const attestation2 = AsnConvert.parse(raw, android.AttestationApplicationId); + assert.strictEqual(attestation2.packageInfos.length, 1); + assert.strictEqual(attestation2.packageInfos[0].packageName.byteLength, 3); + assert.strictEqual(attestation2.packageInfos[0].version, 1); + assert.strictEqual(attestation2.signatureDigests.length, 1); + assert.strictEqual(attestation2.signatureDigests[0].byteLength, 3); + }); + }); + });