summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/helpers/convertCertBufferToPEM.ts12
-rw-r--r--packages/server/src/metadata/mdsTypes.ts58
-rw-r--r--packages/server/src/metadata/verifyAttestationWithMetadata.test.ts75
-rw-r--r--packages/server/src/metadata/verifyAttestationWithMetadata.ts183
-rw-r--r--packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts7
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts7
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts9
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationPacked.ts7
8 files changed, 243 insertions, 115 deletions
diff --git a/packages/server/src/helpers/convertCertBufferToPEM.ts b/packages/server/src/helpers/convertCertBufferToPEM.ts
index 8de140d..b6949c4 100644
--- a/packages/server/src/helpers/convertCertBufferToPEM.ts
+++ b/packages/server/src/helpers/convertCertBufferToPEM.ts
@@ -5,15 +5,17 @@ import type { Base64URLString } from '@simplewebauthn/typescript-types';
* Convert buffer to an OpenSSL-compatible PEM text format.
*/
export function convertCertBufferToPEM(certBuffer: Buffer | Base64URLString): string {
- let buffer: Buffer;
+ let b64cert: string;
+
+ /**
+ * Get certBuffer to a base64 representation
+ */
if (typeof certBuffer === 'string') {
- buffer = base64url.toBuffer(certBuffer);
+ b64cert = base64url.toBase64(certBuffer);
} else {
- buffer = certBuffer;
+ b64cert = certBuffer.toString('base64');
}
- const b64cert = buffer.toString('base64');
-
let PEMKey = '';
for (let i = 0; i < Math.ceil(b64cert.length / 64); i += 1) {
const start = 64 * i;
diff --git a/packages/server/src/metadata/mdsTypes.ts b/packages/server/src/metadata/mdsTypes.ts
index 22ba564..1bf9f80 100644
--- a/packages/server/src/metadata/mdsTypes.ts
+++ b/packages/server/src/metadata/mdsTypes.ts
@@ -176,7 +176,7 @@ export type MetadataStatement = {
/**
* USER_VERIFY
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#user-verification-methods
+ * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#user-verification-methods
*/
export type UserVerify =
| 'presence_internal'
@@ -195,55 +195,56 @@ export type UserVerify =
/**
* ALG_SIGN
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#authentication-algorithms
+ * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#authentication-algorithms
+ *
+ * Using this helpful TS pattern here so that we can strongly enforce the existence of COSE info
+ * mappings in `algSignToCOSEInfoMap` in verifyAttestationWithMetadata.ts
*/
-export type AlgSign =
- | 'secp256r1_ecdsa_sha256_raw'
- | 'secp256r1_ecdsa_sha256_der'
- | 'rsassa_pss_sha256_raw'
- | 'rsassa_pss_sha256_der'
- | 'secp256k1_ecdsa_sha256_raw'
- | 'secp256k1_ecdsa_sha256_der'
- | 'sm2_sm3_raw'
- | 'rsa_emsa_pkcs1_sha256_raw'
- | 'rsa_emsa_pkcs1_sha256_der'
- | 'rsassa_pss_sha384_raw'
- | 'rsassa_pss_sha256_raw'
- | 'rsassa_pkcsv15_sha256_raw'
- | 'rsassa_pkcsv15_sha384_raw'
- | 'rsassa_pkcsv15_sha512_raw'
- | 'rsassa_pkcsv15_sha1_raw'
- | 'secp384r1_ecdsa_sha384_raw'
- | 'secp512r1_ecdsa_sha256_raw'
- | 'ed25519_eddsa_sha512_raw';
+export type AlgSign = typeof AlgSign[number];
+const AlgSign = [
+ 'secp256r1_ecdsa_sha256_raw',
+ 'secp256r1_ecdsa_sha256_der',
+ 'rsassa_pss_sha256_raw',
+ 'rsassa_pss_sha256_der',
+ 'secp256k1_ecdsa_sha256_raw',
+ 'secp256k1_ecdsa_sha256_der',
+ 'rsassa_pss_sha384_raw',
+ 'rsassa_pkcsv15_sha256_raw',
+ 'rsassa_pkcsv15_sha384_raw',
+ 'rsassa_pkcsv15_sha512_raw',
+ 'rsassa_pkcsv15_sha1_raw',
+ 'secp384r1_ecdsa_sha384_raw',
+ 'secp512r1_ecdsa_sha256_raw',
+ 'ed25519_eddsa_sha512_raw',
+] as const;
/**
* ALG_KEY
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#public-key-representation-formats
+ * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#public-key-representation-formats
*/
export type AlgKey = 'ecc_x962_raw' | 'ecc_x962_der' | 'rsa_2048_raw' | 'rsa_2048_der' | 'cose';
/**
* ATTESTATION
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#authenticator-attestation-types
+ * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#authenticator-attestation-types
*/
-export type Attestation = 'basic_full' | 'basic_surrogate' | 'ecdaa' | 'attca';
+export type Attestation = 'basic_full' | 'basic_surrogate' | 'ecdaa' | 'attca' | 'anonca' | 'none';
/**
* KEY_PROTECTION
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#key-protection-types
+ * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#key-protection-types
*/
export type KeyProtection = 'software' | 'hardware' | 'tee' | 'secure_element' | 'remote_handle';
/**
* MATCHER_PROTECTION
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#matcher-protection-types
+ * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#matcher-protection-types
*/
export type MatcherProtection = 'software' | 'tee' | 'on_chip';
/**
* ATTACHMENT_HINT
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#authenticator-attachment-hints
+ * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#authenticator-attachment-hints
*/
export type AttachmentHint =
| 'internal'
@@ -258,7 +259,7 @@ export type AttachmentHint =
/**
* TRANSACTION_CONFIRMATION_DISPLAY
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#transaction-confirmation-display-types
+ * https://fidoalliance.org/specs/common-specs/fido-registry-v2.2-ps-20220523.html#transaction-confirmation-display-types
*/
export type TransactionConfirmationDisplay =
| 'any'
@@ -291,4 +292,5 @@ export type AuthenticatorGetInfo = {
};
maxMsgSize?: number;
pinProtocols?: number[];
+ algorithms?: { type: 'public-key', alg: number }[];
};
diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts
index 24165d2..b48ef2e 100644
--- a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts
+++ b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts
@@ -47,11 +47,11 @@ test('should verify attestation with metadata (android-safetynet)', async () =>
const credentialPublicKey =
'pQECAyYgASFYIAKH2NrGZT-lUEA3tbBXR9owjW_7OnA1UqoL1UuKY_VCIlggpjeOH0xyBCpGDya55JLXXKrzyOieQN3dvG1pV-Qs-Gs';
- const verified = await verifyAttestationWithMetadata(
- metadataStatementJSONSafetyNet,
- base64url.toBuffer(credentialPublicKey),
+ const verified = await verifyAttestationWithMetadata({
+ statement: metadataStatementJSONSafetyNet,
+ credentialPublicKey: base64url.toBuffer(credentialPublicKey),
x5c,
- );
+ });
expect(verified).toEqual(true);
});
@@ -65,7 +65,7 @@ test('should verify attestation with rsa_emsa_pkcs1_sha256_raw authenticator alg
'protocolFamily': 'fido2',
'schema': 3,
'upv': [{ 'major': 1, 'minor': 0 }],
- 'authenticationAlgorithms': ['rsa_emsa_pkcs1_sha256_raw'],
+ 'authenticationAlgorithms': ['rsassa_pkcsv15_sha256_raw'],
'publicKeyAlgAndEncodings': ['cose'],
'attestationTypes': ['attca'],
'userVerificationDetails': [
@@ -97,11 +97,68 @@ test('should verify attestation with rsa_emsa_pkcs1_sha256_raw authenticator alg
];
const credentialPublicKey = 'pAEDAzkBACBZAQC3X5SKwYUkxFxxyvCnz_37Z57eSdsgQuiBLDaBOd1R6VEZReAV3nVr_7jiRgmWfu1C-S3Aro65eSG5shcDCgIvY3KdEI8K5ENEPlmucjnFILBAE_MZtPmZlkEDmVCDcVspHX2iKqiVWYV6IFzVX1QUf0SAlWijV9NEfKDbij34ddV0qfG2nEMA0_xVpN2OK2BVXonFg6tS3T00XlFh4MdzIauIHTDT63eAdHlkFrMqU53T5IqDvL3VurBmBjYRJ3VDT9mA2sm7fSrJNXhSVLPst-ZsiOioVKrpzFE9sJmyCQvq2nGZ2RhDo8FfAKiw0kvJRkCSSe1ddxryk9_VSCprIUMBAAE';
- const verified = await verifyAttestationWithMetadata(
- metadataStatement,
- base64url.toBuffer(credentialPublicKey),
+ const verified = await verifyAttestationWithMetadata({
+ statement: metadataStatement,
+ credentialPublicKey: base64url.toBuffer(credentialPublicKey),
x5c,
- );
+ });
+
+ expect(verified).toEqual(true);
+});
+
+test('should not validate certificate path when authenticator is self-referencing its attestation statement certificates', async () => {
+ const metadataStatement: MetadataStatement = {
+ "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/",
+ "description": "Virtual Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator with Self Batch Referencing",
+ "aaguid": "5b65dac1-7af4-46e6-8a4f-8701fcc4f3b4",
+ "alternativeDescriptions": {
+ "ru-RU": "Виртуальный Secp256R1 CTAP2 аутентификатор для тестирование серверов на соответсвие спецификации FIDO2 с одинаковыми сертификатами"
+ },
+ "protocolFamily": "fido2",
+ "authenticatorVersion": 2,
+ "upv": [{ "major": 1, "minor": 0 }],
+ "authenticationAlgorithms": ["secp256r1_ecdsa_sha256_raw"],
+ "publicKeyAlgAndEncodings": ["cose"],
+ "attestationTypes": ["basic_full"],
+ "schema": 3,
+ "userVerificationDetails": [
+ [{ "userVerificationMethod": "none" }],
+ [{ "userVerificationMethod": "presence_internal" }],
+ [{ "userVerificationMethod": "passcode_external", "caDesc": { "base": 10, "minLength": 4 } }],
+ [
+ { "userVerificationMethod": "passcode_external", "caDesc": { "base": 10, "minLength": 4 } },
+ { "userVerificationMethod": "presence_internal" }
+ ]
+ ],
+ "keyProtection": ["hardware", "secure_element"],
+ "matcherProtection": ["on_chip"],
+ "cryptoStrength": 128,
+ "attachmentHint": ["external", "wired", "wireless", "nfc"],
+ "tcDisplay": [],
+ "attestationRootCertificates": [
+ "MIIEQTCCAimgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDUyMzE0Mzk0M1oXDTI4MDUyMDE0Mzk0M1owgcIxIzAhBgNVBAMMGkZJRE8yIEJBVENIIEtFWSBwcmltZTI1NnYxMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE86Xl6rbB+8rpf232RJlnYse+9yAEAqdsbyMPZVbxeqmZtZf8S/UIqvjp7wzQE/Wrm9J5FL8IBDeMvMsRuJtUajLDAqMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFZN98D4xlW2oR9sTRnzv0Hi/QF5MA0GCSqGSIb3DQEBCwUAA4ICAQCH3aCf+CCJBdEtQc4JpOnUelwGGw7DxnBMokHHBgrzJxDn9BFcFwxGLxrFV7EfYehQNOD+74OS8fZRgZiNf9EDGAYiHh0+CspfBWd20zCIjlCdDBcyhwq3PLJ65JC/og3CT9AK4kvks4DI+01RYxNv9S8Jx1haO1lgU55hBIr1P/p21ZKnpcCEhPjB/cIFrHJqL5iJGfed+LXni9Suq24OHnp44Mrv4h7OD2elu5yWfdfFb+RGG2TYURFIGYGijsii093w0ZMBOfBS+3Xq/DrHeZbZrrNkY455gJCZ5eV83Nrt9J9/UF0VZHl/hwnSAUC/b3tN/l0ZlC9kPcNzJD04l4ndFBD2KdfQ2HGTX7pybWLZ7yH2BM3ui2OpiacaOzd7OE91rHYB2uZyQ7jdg25yF9M8QI9NHM/itCjdBvAYt4QCT8dX6gmZiIGR2F/YXZAsybtJ16pnUmODVbW80lPbzy+PUQYX79opeD9u6MBorzr9g08Elpb1F3DgSd8VSLlsR2QPllKl4AcJDMIOfZHOQGOzatMV7ipEVRa0L5FnjAWpHHvSNcsjD4Cul562mO3MlI2pCyo+US+nIzG5XZmOeu4Db/Kw/dEPOo2ztHwlU0qKJ7REBsbt63jdQtlwLuiLHwkpiwnrAOZfwbLLu9Yz4tL1eJlQffuwS/Aolsz7HA=="
+ ],
+ "supportedExtensions": [{ "id": "hmac-secret", "fail_if_unknown": false }, { "id": "credProtect", "fail_if_unknown": false }
+ ],
+ "authenticatorGetInfo": {
+ "versions": ["U2F_V2", "FIDO_2_0"],
+ "extensions": ["credProtect", "hmac-secret"],
+ "aaguid": "5b65dac17af446e68a4f8701fcc4f3b4",
+ "options": { "plat": false, "rk": true, "clientPin": true, "up": true, "uv": true },
+ "maxMsgSize": 1200,
+ }
+ };
+
+ const x5c = [
+ 'MIIEQTCCAimgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDUyMzE0Mzk0M1oXDTI4MDUyMDE0Mzk0M1owgcIxIzAhBgNVBAMMGkZJRE8yIEJBVENIIEtFWSBwcmltZTI1NnYxMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE86Xl6rbB-8rpf232RJlnYse-9yAEAqdsbyMPZVbxeqmZtZf8S_UIqvjp7wzQE_Wrm9J5FL8IBDeMvMsRuJtUajLDAqMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFZN98D4xlW2oR9sTRnzv0Hi_QF5MA0GCSqGSIb3DQEBCwUAA4ICAQCH3aCf-CCJBdEtQc4JpOnUelwGGw7DxnBMokHHBgrzJxDn9BFcFwxGLxrFV7EfYehQNOD-74OS8fZRgZiNf9EDGAYiHh0-CspfBWd20zCIjlCdDBcyhwq3PLJ65JC_og3CT9AK4kvks4DI-01RYxNv9S8Jx1haO1lgU55hBIr1P_p21ZKnpcCEhPjB_cIFrHJqL5iJGfed-LXni9Suq24OHnp44Mrv4h7OD2elu5yWfdfFb-RGG2TYURFIGYGijsii093w0ZMBOfBS-3Xq_DrHeZbZrrNkY455gJCZ5eV83Nrt9J9_UF0VZHl_hwnSAUC_b3tN_l0ZlC9kPcNzJD04l4ndFBD2KdfQ2HGTX7pybWLZ7yH2BM3ui2OpiacaOzd7OE91rHYB2uZyQ7jdg25yF9M8QI9NHM_itCjdBvAYt4QCT8dX6gmZiIGR2F_YXZAsybtJ16pnUmODVbW80lPbzy-PUQYX79opeD9u6MBorzr9g08Elpb1F3DgSd8VSLlsR2QPllKl4AcJDMIOfZHOQGOzatMV7ipEVRa0L5FnjAWpHHvSNcsjD4Cul562mO3MlI2pCyo-US-nIzG5XZmOeu4Db_Kw_dEPOo2ztHwlU0qKJ7REBsbt63jdQtlwLuiLHwkpiwnrAOZfwbLLu9Yz4tL1eJlQffuwS_Aolsz7HA'
+ ];
+ const credentialPublicKey = 'pQECAyYgASFYIBdmUVOxrn-OOtkVwGP_vAspH3VkgzcGXVlu3-acb7EZIlggKgDTs0fr2d51sLR6uL3KP2cqR3iIUkKMCjyMJhYOkf4';
+
+ const verified = await verifyAttestationWithMetadata({
+ statement: metadataStatement,
+ credentialPublicKey: base64url.toBuffer(credentialPublicKey),
+ x5c,
+ });
expect(verified).toEqual(true);
});
diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.ts
index 0e7c736..5193135 100644
--- a/packages/server/src/metadata/verifyAttestationWithMetadata.ts
+++ b/packages/server/src/metadata/verifyAttestationWithMetadata.ts
@@ -1,6 +1,6 @@
import { Base64URLString } from '@simplewebauthn/typescript-types';
-import { MetadataStatement, AlgSign } from '../metadata/mdsTypes';
+import type { MetadataStatement, AlgSign } from '../metadata/mdsTypes';
import { convertCertBufferToPEM } from '../helpers/convertCertBufferToPEM';
import { validateCertificatePath } from '../helpers/validateCertificatePath';
import { decodeCredentialPublicKey } from '../helpers/decodeCredentialPublicKey';
@@ -10,17 +10,30 @@ import { COSEKEYS, COSEKTY } from '../helpers/convertCOSEtoPKCS';
* Match properties of the authenticator's attestation statement against expected values as
* registered with the FIDO Alliance Metadata Service
*/
-export async function verifyAttestationWithMetadata(
- statement: MetadataStatement,
- credentialPublicKey: Buffer,
- x5c: Buffer[] | Base64URLString[],
-): Promise<boolean> {
+export async function verifyAttestationWithMetadata({
+ statement,
+ credentialPublicKey,
+ x5c,
+ attestationStatementAlg,
+}: {
+ statement: MetadataStatement;
+ credentialPublicKey: Buffer;
+ x5c: Buffer[] | 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();
- statement.authenticationAlgorithms.forEach(algSign => {
- // Convert algSign string to { kty, alg, crv }
- const algSignCOSEINFO = algSignToCOSEInfo(algSign);
+ 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);
}
@@ -65,35 +78,70 @@ export async function verifyAttestationWithMetadata(
// Make sure the public key is one of the allowed algorithms
if (!foundMatch) {
- const debugMDSAlgs = Array.from(keypairCOSEAlgs);
- // Construct some useful error output about the public key
- const debugPubKeyAlgInfo: COSEInfo = {
- kty: publicKeyCOSEInfo.kty,
- alg: publicKeyCOSEInfo.alg,
- };
- // Don't output a bunch of bytes for `crv` when the public key is an RSA key
- if (publicKeyCOSEInfo.kty !== COSEKTY.RSA) {
- debugPubKeyAlgInfo.crv = publicKeyCOSEInfo.crv;
- }
+ /**
+ * 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, '');
- const strPubKeyAlg = JSON.stringify(debugPubKeyAlgInfo);
- const strMDSAlgs = JSON.stringify(debugMDSAlgs);
+ /**
+ * Construct useful error output about the public key
+ */
+ const strPubKeyAlg = stringifyCOSEInfo(publicKeyCOSEInfo);
throw new Error(
- `Public key algorithm ${strPubKeyAlg} did not match any metadata algorithms ${strMDSAlgs}`,
+ `Public key parameters ${strPubKeyAlg} did not match any of the following metadata algorithms:\n${strMDSAlgs}`,
);
}
- try {
- await validateCertificatePath(
- x5c.map(convertCertBufferToPEM),
- statement.attestationRootCertificates.map(convertCertBufferToPEM),
- );
- } catch (err) {
- const _err = err as Error;
- throw new Error(
- `Could not validate certificate path with any metadata root certificates: ${_err.message}`,
- );
+ /**
+ * 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;
@@ -110,42 +158,41 @@ type COSEInfo = {
*
* Values pulled from `ALG_KEY_COSE` definitions in the FIDO Registry of Predefined Values
*
- * https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#authentication-algorithms
+ * 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 algSignToCOSEInfo(algSign: AlgSign): COSEInfo | undefined {
- switch (algSign) {
- case 'secp256r1_ecdsa_sha256_raw':
- case 'secp256r1_ecdsa_sha256_der':
- return { kty: 2, alg: -7, crv: 1 };
- case 'rsassa_pss_sha256_raw':
- case 'rsassa_pss_sha256_der':
- return { kty: 3, alg: -37 };
- case 'secp256k1_ecdsa_sha256_raw':
- case 'secp256k1_ecdsa_sha256_der':
- return { kty: 2, alg: 7, crv: 8 };
- case 'rsassa_pss_sha384_raw':
- return { kty: 3, alg: -38 };
- case 'rsassa_pkcsv15_sha256_raw':
- return { kty: 3, alg: -257 };
- case 'rsassa_pkcsv15_sha384_raw':
- return { kty: 3, alg: -258 };
- case 'rsassa_pkcsv15_sha512_raw':
- return { kty: 3, alg: -259 };
- case 'rsassa_pkcsv15_sha1_raw':
- return { kty: 3, alg: -65535 };
- case 'secp384r1_ecdsa_sha384_raw':
- return { kty: 2, alg: -35, crv: 2 };
- case 'secp512r1_ecdsa_sha256_raw':
- return { kty: 2, alg: -36, crv: 3 };
- case 'ed25519_eddsa_sha512_raw':
- return { kty: 1, alg: -8, crv: 6 };
- case 'rsa_emsa_pkcs1_sha256_raw':
- case 'rsa_emsa_pkcs1_sha256_der':
- return { kty: 3, alg: -257 };
- // TODO: COSE info wasn't readily available for these, these seem rare...
- // case 'sm2_sm3_raw':
- // return {};
- default:
- return undefined;
+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;
}
diff --git a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
index 2afd0a4..c74a7fe 100644
--- a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
+++ b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
@@ -257,7 +257,12 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
const statement = await MetadataService.getStatement(aaguid);
if (statement) {
try {
- await verifyAttestationWithMetadata(statement, credentialPublicKey, x5c);
+ await verifyAttestationWithMetadata({
+ statement,
+ credentialPublicKey,
+ x5c,
+ attestationStatementAlg: alg,
+ });
} catch (err) {
const _err = err as Error;
throw new Error(`${_err.message} (TPM)`);
diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
index dcc3670..0930eb8 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
@@ -78,7 +78,12 @@ export async function verifyAttestationAndroidKey(
const statement = await MetadataService.getStatement(aaguid);
if (statement) {
try {
- await verifyAttestationWithMetadata(statement, credentialPublicKey, x5c);
+ await verifyAttestationWithMetadata({
+ statement,
+ credentialPublicKey,
+ x5c,
+ attestationStatementAlg: alg,
+ });
} catch (err) {
const _err = err as Error;
throw new Error(`${_err.message} (AndroidKey)`);
diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
index 14be27e..4b8c31f 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
@@ -25,7 +25,7 @@ export async function verifyAttestationAndroidSafetyNet(
verifyTimestampMS = true,
credentialPublicKey,
} = options;
- const { response, ver } = attStmt;
+ const { alg, response, ver } = attStmt;
if (!ver) {
throw new Error('No ver value in attestation (SafetyNet)');
@@ -95,7 +95,12 @@ export async function verifyAttestationAndroidSafetyNet(
const statement = await MetadataService.getStatement(aaguid);
if (statement) {
try {
- await verifyAttestationWithMetadata(statement, credentialPublicKey, HEADER.x5c);
+ await verifyAttestationWithMetadata({
+ statement,
+ credentialPublicKey,
+ x5c: HEADER.x5c,
+ attestationStatementAlg: alg,
+ });
} catch (err) {
const _err = err as Error;
throw new Error(`${_err.message} (SafetyNet)`);
diff --git a/packages/server/src/registration/verifications/verifyAttestationPacked.ts b/packages/server/src/registration/verifications/verifyAttestationPacked.ts
index 844a1ed..415c814 100644
--- a/packages/server/src/registration/verifications/verifyAttestationPacked.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationPacked.ts
@@ -99,7 +99,12 @@ export async function verifyAttestationPacked(
}
try {
- await verifyAttestationWithMetadata(statement, credentialPublicKey, x5c);
+ await verifyAttestationWithMetadata({
+ statement,
+ credentialPublicKey,
+ x5c,
+ attestationStatementAlg: alg,
+ });
} catch (err) {
const _err = err as Error;
throw new Error(`${_err.message} (Packed|Full)`);