summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src/metadata/verifyAttestationWithMetadata.ts
blob: 37afab786da46b5977767c21e992ba16a35f9bbb (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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import type { Base64URLString } from '../deps.ts';
import type { AlgSign, MetadataStatement } from '../metadata/mdsTypes.ts';
import { convertCertBufferToPEM } from '../helpers/convertCertBufferToPEM.ts';
import { validateCertificatePath } from '../helpers/validateCertificatePath.ts';
import { decodeCredentialPublicKey } from '../helpers/decodeCredentialPublicKey.ts';
import { COSEALG, COSECRV, COSEKEYS, COSEKTY, isCOSEPublicKeyEC2 } from '../helpers/cose.ts';

/**
 * Match properties of the authenticator's attestation statement against expected values as
 * registered with the FIDO Alliance Metadata Service
 */
export async function verifyAttestationWithMetadata({
  statement,
  credentialPublicKey,
  x5c,
  attestationStatementAlg,
}: {
  statement: MetadataStatement;
  credentialPublicKey: Uint8Array;
  x5c: Uint8Array[] | Base64URLString[];
  attestationStatementAlg?: number;
}): Promise<boolean> {
  const {
    authenticationAlgorithms,
    authenticatorGetInfo,
    attestationRootCertificates,
  } = statement;

  // Make sure the alg in the attestation statement matches one of the ones specified in metadata
  const keypairCOSEAlgs: Set<COSEInfo> = new Set();
  authenticationAlgorithms.forEach((algSign) => {
    // Map algSign string to { kty, alg, crv }
    const algSignCOSEINFO = algSignToCOSEInfoMap[algSign];

    // Keeping this statement here just in case MDS returns something unexpected
    if (algSignCOSEINFO) {
      keypairCOSEAlgs.add(algSignCOSEINFO);
    }
  });

  // Extract the public key's COSE info for comparison
  const decodedPublicKey = decodeCredentialPublicKey(credentialPublicKey);

  const kty = decodedPublicKey.get(COSEKEYS.kty);
  const alg = decodedPublicKey.get(COSEKEYS.alg);

  if (!kty) {
    throw new Error('Credential public key was missing kty');
  }

  if (!alg) {
    throw new Error('Credential public key was missing alg');
  }

  if (!kty) {
    throw new Error('Credential public key was missing kty');
  }

  // Assume everything is a number because these values should be
  const publicKeyCOSEInfo: COSEInfo = { kty, alg };

  if (isCOSEPublicKeyEC2(decodedPublicKey)) {
    const crv = decodedPublicKey.get(COSEKEYS.crv);
    publicKeyCOSEInfo.crv = crv;
  }

  /**
   * Attempt to match the credential public key's algorithm to one specified in the device's
   * metadata
   */
  let foundMatch = false;
  for (const keypairAlg of keypairCOSEAlgs) {
    // Make sure algorithm and key type match
    if (
      keypairAlg.alg === publicKeyCOSEInfo.alg &&
      keypairAlg.kty === publicKeyCOSEInfo.kty
    ) {
      // If not an RSA keypair then make sure curve numbers match too
      if (
        (keypairAlg.kty === COSEKTY.EC2 || keypairAlg.kty === COSEKTY.OKP) &&
        keypairAlg.crv === publicKeyCOSEInfo.crv
      ) {
        foundMatch = true;
      } else {
        // We've matched an RSA public key's properties
        foundMatch = true;
      }
    }

    if (foundMatch) {
      break;
    }
  }

  // Make sure the public key is one of the allowed algorithms
  if (!foundMatch) {
    /**
     * Craft some useful error output from the MDS algorithms
     *
     * Example:
     *
     * ```
     * [
     *   'rsassa_pss_sha256_raw' (COSE info: { kty: 3, alg: -37 }),
     *   'secp256k1_ecdsa_sha256_raw' (COSE info: { kty: 2, alg: -47, crv: 8 })
     * ]
     * ```
     */
    const debugMDSAlgs = authenticationAlgorithms.map(
      (algSign) => `'${algSign}' (COSE info: ${stringifyCOSEInfo(algSignToCOSEInfoMap[algSign])})`,
    );
    const strMDSAlgs = JSON.stringify(debugMDSAlgs, null, 2).replace(/"/g, '');

    /**
     * Construct useful error output about the public key
     */
    const strPubKeyAlg = stringifyCOSEInfo(publicKeyCOSEInfo);

    throw new Error(
      `Public key parameters ${strPubKeyAlg} did not match any of the following metadata algorithms:\n${strMDSAlgs}`,
    );
  }

  /**
   * Confirm the attestation statement's algorithm is one supported according to metadata
   */
  if (
    attestationStatementAlg !== undefined &&
    authenticatorGetInfo?.algorithms !== undefined
  ) {
    const getInfoAlgs = authenticatorGetInfo.algorithms.map((_alg) => _alg.alg);
    if (getInfoAlgs.indexOf(attestationStatementAlg) < 0) {
      throw new Error(
        `Attestation statement alg ${attestationStatementAlg} did not match one of ${getInfoAlgs}`,
      );
    }
  }

  // Prepare to check the certificate chain
  const authenticatorCerts = x5c.map(convertCertBufferToPEM);
  const statementRootCerts = attestationRootCertificates.map(
    convertCertBufferToPEM,
  );

  /**
   * If an authenticator returns exactly one certificate in its x5c, and that cert is found in the
   * metadata statement then the authenticator is "self-referencing". In this case we forego
   * certificate chain validation.
   */
  let authenticatorIsSelfReferencing = false;
  if (
    authenticatorCerts.length === 1 &&
    statementRootCerts.indexOf(authenticatorCerts[0]) >= 0
  ) {
    authenticatorIsSelfReferencing = true;
  }

  if (!authenticatorIsSelfReferencing) {
    try {
      await validateCertificatePath(authenticatorCerts, statementRootCerts);
    } catch (err) {
      const _err = err as Error;
      throw new Error(
        `Could not validate certificate path with any metadata root certificates: ${_err.message}`,
      );
    }
  }

  return true;
}

type COSEInfo = {
  kty: COSEKTY;
  alg: COSEALG;
  crv?: COSECRV;
};

/**
 * Convert ALG_SIGN values to COSE info
 *
 * Values pulled from `ALG_KEY_COSE` definitions in the FIDO Registry of Predefined Values
 *
 * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#authentication-algorithms
 */
export const algSignToCOSEInfoMap: { [key in AlgSign]: COSEInfo } = {
  secp256r1_ecdsa_sha256_raw: { kty: 2, alg: -7, crv: 1 },
  secp256r1_ecdsa_sha256_der: { kty: 2, alg: -7, crv: 1 },
  rsassa_pss_sha256_raw: { kty: 3, alg: -37 },
  rsassa_pss_sha256_der: { kty: 3, alg: -37 },
  secp256k1_ecdsa_sha256_raw: { kty: 2, alg: -47, crv: 8 },
  secp256k1_ecdsa_sha256_der: { kty: 2, alg: -47, crv: 8 },
  rsassa_pss_sha384_raw: { kty: 3, alg: -38 },
  rsassa_pkcsv15_sha256_raw: { kty: 3, alg: -257 },
  rsassa_pkcsv15_sha384_raw: { kty: 3, alg: -258 },
  rsassa_pkcsv15_sha512_raw: { kty: 3, alg: -259 },
  rsassa_pkcsv15_sha1_raw: { kty: 3, alg: -65535 },
  secp384r1_ecdsa_sha384_raw: { kty: 2, alg: -35, crv: 2 },
  secp512r1_ecdsa_sha256_raw: { kty: 2, alg: -36, crv: 3 },
  ed25519_eddsa_sha512_raw: { kty: 1, alg: -8, crv: 6 },
};

/**
 * A helper to format COSEInfo a little nicer than we can achieve with JSON.stringify()
 *
 * Input: `{ "kty": 3, "alg": -257 }`
 *
 * Output: `"{ kty: 3, alg: -257 }"`
 */
function stringifyCOSEInfo(info: COSEInfo): string {
  const { kty, alg, crv } = info;

  let toReturn = '';
  if (kty !== COSEKTY.RSA) {
    toReturn = `{ kty: ${kty}, alg: ${alg}, crv: ${crv} }`;
  } else {
    toReturn = `{ kty: ${kty}, alg: ${alg} }`;
  }

  return toReturn;
}