summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src/attestation/verifications/verifyAndroidKey.ts
blob: e5f68ba56bebf23e9f0be4a26d747994489a4c9d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import type { AttestationStatement } from '../../helpers/decodeAttestationObject';
import convertASN1toPEM from '../../helpers/convertASN1toPEM';
import verifySignature from '../../helpers/verifySignature';
import {
  leafCertToASN1Object,
  findOID,
  asn1ObjectToJSON,
  ASN1Object,
  JASN1,
} from '../../helpers/asn1Utils';
import convertCOSEtoPKCS, { COSEALGHASH } from '../../helpers/convertCOSEtoPKCS';
import MetadataService from '../../metadata/metadataService';
import verifyAttestationWithMetadata from '../../metadata/verifyAttestationWithMetadata';

type Options = {
  authData: Buffer;
  clientDataHash: Buffer;
  attStmt: AttestationStatement;
  credentialPublicKey: Buffer;
  aaguid: Buffer;
};

export default async function verifyAttestationAndroidKey(options: Options): Promise<boolean> {
  const { authData, clientDataHash, attStmt, credentialPublicKey, aaguid } = options;
  const { x5c, sig, alg } = attStmt;

  if (!x5c) {
    throw new Error('No attestation certificate provided in attestation statement (AndroidKey)');
  }

  if (!sig) {
    throw new Error('No attestation signature provided in attestation statement (AndroidKey)');
  }

  if (!alg) {
    throw new Error(`Attestation statement did not contain alg (AndroidKey)`);
  }

  const certASN1 = leafCertToASN1Object(x5c[0]);

  // Check that credentialPublicKey matches the public key in the attestation certificate
  // Find the public cert in the certificate as PKCS
  const certPubKey = getASN1CertificatePublicKey(certASN1);

  if (!certPubKey) {
    throw new Error('Could not retrieve public key from leaf certificate (AndroidKey)');
  }

  // Convert the credentialPublicKey to PKCS
  const reservedByte = Buffer.from([0x00]);
  const credPubKeyPKCS = Buffer.concat([reservedByte, convertCOSEtoPKCS(credentialPublicKey)]);

  if (!credPubKeyPKCS.equals(certPubKey)) {
    throw new Error('Credential public key does not equal leaf cert public key (AndroidKey)');
  }

  // Find Android KeyStore Extension in certificate extensions
  const extKeyStore = getASN1ExtKeyStore(certASN1);

  if (!extKeyStore) {
    throw new Error('Certificate did not contain extKeyStore (AndroidKey)');
  }

  // Verify extKeyStore values
  const { attestationChallenge, teeEnforced, softwareEnforced } = extKeyStore;

  if (!attestationChallenge.equals(clientDataHash)) {
    throw new Error('Attestation challenge was not equal to client data hash (AndroidKey)');
  }

  // Ensure that the key is strictly bound to the caller app identifier (shouldn't contain the
  // following tag)
  const allApplicationsTag = '[600]';

  if (teeEnforced.indexOf(allApplicationsTag) >= 0) {
    throw new Error('teeEnforced contained "[600]" tag (AndroidKey)');
  }

  if (softwareEnforced.indexOf(allApplicationsTag) >= 0) {
    throw new Error('teeEnforced contained "[600]" tag (AndroidKey)');
  }

  // TODO: Confirm that the root certificate is an expected certificate
  // const rootCertPEM = convertASN1toPEM(x5c[x5c.length - 1]);
  // console.log(rootCertPEM);

  // if (rootCertPEM !== expectedRootCert) {
  //   throw new Error('Root certificate was not expected certificate (AndroidKey)');
  // }

  const statement = await MetadataService.getStatement(aaguid);
  if (statement) {
    try {
      verifyAttestationWithMetadata(statement, alg, x5c);
    } catch (err) {
      throw new Error(`${err.message} (AndroidKey)`);
    }
  }

  const signatureBase = Buffer.concat([authData, clientDataHash]);
  const leafCertPEM = convertASN1toPEM(x5c[0]);
  const hashAlg = COSEALGHASH[alg as number];

  return verifySignature(sig, signatureBase, leafCertPEM, hashAlg);
}

type KeyStoreExtensionDescription = {
  attestationVersion: number;
  attestationChallenge: Buffer;
  softwareEnforced: string[];
  teeEnforced: string[];
};

function getASN1ExtKeyStore(certASN1: ASN1Object): KeyStoreExtensionDescription | undefined {
  const oid = '1.3.6.1.4.1.11129.2.1.17';
  const ext = findOID(certASN1, oid);

  if (!ext) {
    return;
  }

  const description = (ext.data as JASN1[])[1];
  const descData = (description.data as JASN1[])[0].data;

  if (!descData) {
    return;
  }

  /**
   * Cast to number according to RFC 5280
   * https://tools.ietf.org/html/rfc5280#section-3.1
   */
  const rawAttestationVersion = (descData[0] as JASN1).data as string;
  let attestationVersion = 1;
  if (rawAttestationVersion === '1') {
    attestationVersion = 2;
  } else if (rawAttestationVersion === '2') {
    attestationVersion = 3;
  }

  const attestationChallenge = (descData[4] as JASN1).data as Buffer;
  const softwareEnforced = ((descData[6] as JASN1).data as JASN1[]).map(data => data.type);
  const teeEnforced = ((descData[7] as JASN1).data as JASN1[]).map(data => data.type);

  return {
    attestationVersion,
    attestationChallenge,
    softwareEnforced,
    teeEnforced,
  };
}

function getASN1CertificatePublicKey(certASN1: ASN1Object): Buffer | undefined {
  const certJSON = asn1ObjectToJSON(certASN1);
  const certTBS = (certJSON.data as JASN1[])[0];
  const certPubKey = (certTBS.data as JASN1[])[6];
  const certPubBuffer = (certPubKey.data as JASN1[])[1].data;

  return certPubBuffer as Buffer;
}

// TODO: Find the most up-to-date expected root cert, the one from Yuriy's article doesn't match
const expectedRootCert = ``;