Skip to content

Commit

Permalink
fix: support wrong order fields for KeyDescription
Browse files Browse the repository at this point in the history
  • Loading branch information
microshine committed Oct 17, 2023
1 parent 5145f19 commit ef4e3ee
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 15 deletions.
68 changes: 67 additions & 1 deletion packages/android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- [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
```
4 changes: 3 additions & 1 deletion packages/android/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "./key_description";
export * from "./key_description";
export * from "./nonstandard";
export * from "./attestation";
49 changes: 42 additions & 7 deletions packages/android/src/key_description.ts
Original file line number Diff line number Diff line change
@@ -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),
Expand All @@ -20,7 +29,9 @@ export enum VerifiedBootState {
}

/**
* ```
* Implements ASN.1 structure for root of trust.
*
* ```asn
* RootOfTrust ::= SEQUENCE {
* verifiedBootKey OCTET_STRING,
* deviceLocked BOOLEAN,
Expand Down Expand Up @@ -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<number> {
export class IntegerSet extends AsnArray<number> {

constructor(items?: number[]) {
super(items);
Expand All @@ -64,7 +82,9 @@ export class IntegerSet extends AsnArray<number> {
}

/**
* ```
* Implements ASN.1 structure for authorization list.
*
* ```asn
* AuthorizationList ::= SEQUENCE {
* purpose [1] EXPLICIT SET OF INTEGER OPTIONAL,
* algorithm [2] EXPLICIT INTEGER OPTIONAL,
Expand All @@ -74,6 +94,7 @@ export class IntegerSet extends AsnArray<number> {
* 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
Expand Down Expand Up @@ -103,6 +124,7 @@ export class IntegerSet extends AsnArray<number> {
* 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
* }
* ```
*/
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<AuthorizationList> = {}) {
Object.assign(this, params);
}
}

/**
* ```
* Implements ASN.1 structure for security level.
*
* ```asn
* SecurityLevel ::= ENUMERATED {
* Software (0),
* TrustedEnvironment (1),
Expand All @@ -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,
Expand Down
104 changes: 104 additions & 0 deletions packages/android/src/nonstandard.ts
Original file line number Diff line number Diff line change
@@ -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<NonStandardAuthorization> {
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<K extends keyof AuthorizationList>(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<NonStandardKeyDescription> = {}) {
Object.assign(this, params);
}
}
Loading

0 comments on commit ef4e3ee

Please sign in to comment.