diff options
5 files changed, 94 insertions, 192 deletions
diff --git a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts index 31aa53d..9e0c080 100644 --- a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts +++ b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts @@ -1,29 +1,22 @@ import base64url from 'base64url'; -import type { AttestationObject } from '../../helpers/decodeAttestationObject'; -import type { ParsedAuthenticatorData } from '../../helpers/parseAuthenticatorData'; -import type { VerifiedAttestation } from '../verifyAttestationResponse'; +import type { AttestationStatement } from '../../helpers/decodeAttestationObject'; import toHash from '../../helpers/toHash'; import verifySignature from '../../helpers/verifySignature'; -import convertCOSEtoPKCS from '../../helpers/convertCOSEtoPKCS'; import getCertificateInfo from '../../helpers/getCertificateInfo'; +type Options = { + attStmt: AttestationStatement; + clientDataHash: Buffer; + authData: Buffer; +}; + /** * Verify an attestation response with fmt 'android-safetynet' */ -export default function verifyAttestationAndroidSafetyNet( - attestationObject: AttestationObject, - base64ClientDataJSON: string, - parsedAuthData: ParsedAuthenticatorData, - credentialPublicKey: Buffer, -): VerifiedAttestation { - const { attStmt, authData, fmt } = attestationObject; - const { counter, credentialID, flags } = parsedAuthData; - - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator (SafetyNet)'); - } +export default function verifyAttestationAndroidSafetyNet(options: Options): boolean { + const { attStmt, clientDataHash, authData } = options; if (!attStmt.response) { throw new Error('No response was included in attStmt by authenticator (SafetyNet)'); @@ -41,7 +34,6 @@ export default function verifyAttestationAndroidSafetyNet( * START Verify PAYLOAD */ const { nonce, ctsProfileMatch } = PAYLOAD; - const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON)); const nonceBase = Buffer.concat([authData, clientDataHash]); const nonceBuffer = toHash(nonceBase); @@ -95,28 +87,12 @@ export default function verifyAttestationAndroidSafetyNet( const signatureBaseBuffer = Buffer.from(`${jwtParts[0]}.${jwtParts[1]}`); const signatureBuffer = base64url.toBuffer(SIGNATURE); - const toReturn: VerifiedAttestation = { - verified: verifySignature(signatureBuffer, signatureBaseBuffer, certificate), - userVerified: false, - }; + const verified = verifySignature(signatureBuffer, signatureBaseBuffer, certificate); /** * END Verify Signature */ - if (toReturn.verified) { - toReturn.userVerified = flags.uv; - - const publicKey = convertCOSEtoPKCS(credentialPublicKey); - - toReturn.authenticatorInfo = { - fmt, - counter, - base64PublicKey: base64url.encode(publicKey), - base64CredentialID: base64url.encode(credentialID), - }; - } - - return toReturn; + return verified; } /** diff --git a/packages/server/src/attestation/verifications/verifyFIDOU2F.ts b/packages/server/src/attestation/verifications/verifyFIDOU2F.ts index 508f167..0fd2e74 100644 --- a/packages/server/src/attestation/verifications/verifyFIDOU2F.ts +++ b/packages/server/src/attestation/verifications/verifyFIDOU2F.ts @@ -1,34 +1,23 @@ -import base64url from 'base64url'; +import type { AttestationStatement } from '../../helpers/decodeAttestationObject'; -import type { AttestationObject } from '../../helpers/decodeAttestationObject'; -import type { ParsedAuthenticatorData } from '../../helpers/parseAuthenticatorData'; -import type { VerifiedAttestation } from '../verifyAttestationResponse'; - -import toHash from '../../helpers/toHash'; import convertCOSEtoPKCS from '../../helpers/convertCOSEtoPKCS'; import convertASN1toPEM from '../../helpers/convertASN1toPEM'; import verifySignature from '../../helpers/verifySignature'; +type Options = { + attStmt: AttestationStatement; + clientDataHash: Buffer; + rpIdHash: Buffer; + credentialID: Buffer; + credentialPublicKey: Buffer; +}; + /** * Verify an attestation response with fmt 'fido-u2f' */ -export default function verifyAttestationFIDOU2F( - attestationObject: AttestationObject, - base64ClientDataJSON: string, - parsedAuthData: ParsedAuthenticatorData, -): VerifiedAttestation { - const { fmt, attStmt } = attestationObject; - const { flags, credentialPublicKey, rpIdHash, credentialID, counter } = parsedAuthData; +export default function verifyAttestationFIDOU2F(options: Options): boolean { + const { attStmt, clientDataHash, rpIdHash, credentialID, credentialPublicKey } = options; - if (!credentialPublicKey) { - throw new Error('No public key was provided by authenticator (FIDOU2F)'); - } - - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator (FIDOU2F)'); - } - - const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON)); const reservedByte = Buffer.from([0x00]); const publicKey = convertCOSEtoPKCS(credentialPublicKey); @@ -52,19 +41,5 @@ export default function verifyAttestationFIDOU2F( const publicKeyCertPEM = convertASN1toPEM(x5c[0]); - const toReturn: VerifiedAttestation = { - verified: verifySignature(sig, signatureBase, publicKeyCertPEM), - userVerified: flags.uv, - }; - - if (toReturn.verified) { - toReturn.authenticatorInfo = { - fmt, - counter, - base64PublicKey: base64url.encode(publicKey), - base64CredentialID: base64url.encode(credentialID), - }; - } - - return toReturn; + return verifySignature(sig, signatureBase, publicKeyCertPEM); } diff --git a/packages/server/src/attestation/verifications/verifyNone.ts b/packages/server/src/attestation/verifications/verifyNone.ts deleted file mode 100644 index f276a83..0000000 --- a/packages/server/src/attestation/verifications/verifyNone.ts +++ /dev/null @@ -1,43 +0,0 @@ -import base64url from 'base64url'; - -import type { AttestationObject } from '../../helpers/decodeAttestationObject'; -import type { ParsedAuthenticatorData } from '../../helpers/parseAuthenticatorData'; -import type { VerifiedAttestation } from '../verifyAttestationResponse'; - -import convertCOSEtoPKCS from '../../helpers/convertCOSEtoPKCS'; - -/** - * Verify an attestation response with fmt 'none' - * - * This is the weaker of the attestations, so there are only so many checks we can perform - */ -export default function verifyAttestationNone( - attestationObject: AttestationObject, - parsedAuthData: ParsedAuthenticatorData, -): VerifiedAttestation { - const { fmt } = attestationObject; - const { credentialID, credentialPublicKey, counter, flags } = parsedAuthData; - - if (!credentialPublicKey) { - throw new Error('No public key was provided by authenticator (None)'); - } - - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator (None)'); - } - - const publicKey = convertCOSEtoPKCS(credentialPublicKey); - - const toReturn: VerifiedAttestation = { - verified: true, - userVerified: flags.uv, - authenticatorInfo: { - fmt, - counter, - base64PublicKey: base64url.encode(publicKey), - base64CredentialID: base64url.encode(credentialID), - }, - }; - - return toReturn; -} diff --git a/packages/server/src/attestation/verifications/verifyPacked.ts b/packages/server/src/attestation/verifications/verifyPacked.ts index c5f8ec1..45bd57e 100644 --- a/packages/server/src/attestation/verifications/verifyPacked.ts +++ b/packages/server/src/attestation/verifications/verifyPacked.ts @@ -1,53 +1,38 @@ -import base64url from 'base64url'; import elliptic from 'elliptic'; import NodeRSA, { SigningSchemeHash } from 'node-rsa'; -import type { AttestationObject } from '../../helpers/decodeAttestationObject'; -import type { ParsedAuthenticatorData } from '../../helpers/parseAuthenticatorData'; -import type { VerifiedAttestation } from '../verifyAttestationResponse'; +import type { AttestationStatement } from '../../helpers/decodeAttestationObject'; -import convertCOSEtoPKCS, { - COSEKEYS, -} from '../../helpers/convertCOSEtoPKCS'; +import convertCOSEtoPKCS, { COSEKEYS } from '../../helpers/convertCOSEtoPKCS'; import toHash from '../../helpers/toHash'; import convertASN1toPEM from '../../helpers/convertASN1toPEM'; import getCertificateInfo from '../../helpers/getCertificateInfo'; import verifySignature from '../../helpers/verifySignature'; import decodeCredentialPublicKey from '../../helpers/decodeCredentialPublicKey'; +type Options = { + attStmt: AttestationStatement; + clientDataHash: Buffer; + authData: Buffer; + credentialPublicKey: Buffer; +}; + /** * Verify an attestation response with fmt 'packed' */ -export default function verifyAttestationPacked( - attestationObject: AttestationObject, - base64ClientDataJSON: string, - parsedAuthData: ParsedAuthenticatorData, -): VerifiedAttestation { - const { fmt, authData, attStmt } = attestationObject; - const { sig, x5c } = attStmt; - const { credentialPublicKey, counter, credentialID, flags } = parsedAuthData; - - if (!credentialPublicKey) { - throw new Error('No public key was provided by authenticator (Packed)'); - } +export default function verifyAttestationPacked(options: Options): boolean { + const { attStmt, clientDataHash, authData, credentialPublicKey } = options; - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator (Packed)'); - } + const { sig, x5c } = attStmt; if (!sig) { throw new Error('No attestation signature provided in attestation statement (Packed)'); } - const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON)); - const signatureBase = Buffer.concat([authData, clientDataHash]); - const toReturn: VerifiedAttestation = { - verified: false, - userVerified: flags.uv, - }; - const publicKey = convertCOSEtoPKCS(credentialPublicKey); + let verified = false; + const pkcsPublicKey = convertCOSEtoPKCS(credentialPublicKey); if (x5c) { const leafCert = convertASN1toPEM(x5c[0]); @@ -80,7 +65,7 @@ export default function verifyAttestationPacked( throw new Error('Batch certificate version was not `3` (ASN.1 value of 2) (Packed|Full'); } - toReturn.verified = verifySignature(sig, signatureBase, leafCert); + verified = verifySignature(sig, signatureBase, leafCert); } else { const cosePublicKey = decodeCredentialPublicKey(credentialPublicKey); @@ -104,7 +89,6 @@ export default function verifyAttestationPacked( throw new Error('COSE public key was missing kty crv (Packed|EC2)'); } - const pkcsPublicKey = convertCOSEtoPKCS(credentialPublicKey); const signatureBaseHash = toHash(signatureBase, hashAlg); /** @@ -119,7 +103,7 @@ export default function verifyAttestationPacked( const ec = new elliptic.ec(COSECRV[crv as number]); const key = ec.keyFromPublic(pkcsPublicKey); - toReturn.verified = key.verify(signatureBaseHash, sig); + verified = key.verify(signatureBaseHash, sig); } else if (kty === COSEKTY.RSA) { const n = cosePublicKey.get(COSEKEYS.n); @@ -140,7 +124,7 @@ export default function verifyAttestationPacked( 'components-public', ); - toReturn.verified = key.verify(signatureBase, sig); + verified = key.verify(signatureBase, sig); } else if (kty === COSEKTY.OKP) { const x = cosePublicKey.get(COSEKEYS.x); @@ -154,20 +138,11 @@ export default function verifyAttestationPacked( key.keyFromPublic(x as Buffer); // TODO: is `publicKey` right here? - toReturn.verified = key.verify(signatureBaseHash, sig, publicKey); + verified = key.verify(signatureBaseHash, sig, pkcsPublicKey); } } - if (toReturn.verified) { - toReturn.authenticatorInfo = { - fmt, - counter, - base64PublicKey: base64url.encode(publicKey), - base64CredentialID: base64url.encode(credentialID), - }; - } - - return toReturn; + return verified; } enum COSEKTY { diff --git a/packages/server/src/attestation/verifyAttestationResponse.ts b/packages/server/src/attestation/verifyAttestationResponse.ts index 96f659a..309c865 100644 --- a/packages/server/src/attestation/verifyAttestationResponse.ts +++ b/packages/server/src/attestation/verifyAttestationResponse.ts @@ -1,18 +1,16 @@ -import { - AttestationCredentialJSON, -} from '@simplewebauthn/typescript-types'; +import base64url from 'base64url'; +import { AttestationCredentialJSON } from '@simplewebauthn/typescript-types'; import decodeAttestationObject, { ATTESTATION_FORMATS } from '../helpers/decodeAttestationObject'; import decodeClientDataJSON from '../helpers/decodeClientDataJSON'; import parseAuthenticatorData from '../helpers/parseAuthenticatorData'; import toHash from '../helpers/toHash'; import decodeCredentialPublicKey from '../helpers/decodeCredentialPublicKey'; -import { COSEKEYS } from '../helpers/convertCOSEtoPKCS'; +import convertCOSEtoPKCS, { COSEKEYS } from '../helpers/convertCOSEtoPKCS'; import { supportedCOSEAlgorithIdentifiers } from './generateAttestationOptions'; import verifyFIDOU2F from './verifications/verifyFIDOU2F'; import verifyPacked from './verifications/verifyPacked'; -import verifyNone from './verifications/verifyNone'; import verifyAndroidSafetynet from './verifications/verifyAndroidSafetyNet'; /** @@ -52,10 +50,10 @@ export default function verifyAttestationResponse( } const attestationObject = decodeAttestationObject(response.attestationObject); - const { fmt, authData } = attestationObject; + const { fmt, authData, attStmt } = attestationObject; const parsedAuthData = parseAuthenticatorData(authData); - const { rpIdHash, flags, credentialPublicKey } = parsedAuthData; + const { rpIdHash, flags, credentialID, counter, credentialPublicKey } = parsedAuthData; // Make sure the response's RP ID is ours const expectedRPIDHash = toHash(Buffer.from(expectedRPID, 'ascii')); @@ -68,6 +66,10 @@ export default function verifyAttestationResponse( throw new Error('User not present during assertion'); } + if (!credentialID) { + throw new Error('No credential ID was provided by authenticator'); + } + if (!credentialPublicKey) { throw new Error('No public key was provided by authenticator'); } @@ -85,42 +87,59 @@ export default function verifyAttestationResponse( throw new Error(`Unexpected public key alg "${alg}", expected one of "${supported}"`); } + const clientDataHash = toHash(base64url.toBuffer(response.clientDataJSON)); + /** * Verification can only be performed when attestation = 'direct' */ + let verified = false; if (fmt === ATTESTATION_FORMATS.FIDO_U2F) { - return verifyFIDOU2F( - attestationObject, - response.clientDataJSON, - parsedAuthData, - ); + verified = verifyFIDOU2F({ + attStmt, + clientDataHash, + credentialID, + credentialPublicKey, + rpIdHash, + }); + } else if (fmt === ATTESTATION_FORMATS.PACKED) { + verified = verifyPacked({ + attStmt, + authData, + clientDataHash, + credentialPublicKey, + }); + } else if (fmt === ATTESTATION_FORMATS.ANDROID_SAFETYNET) { + verified = verifyAndroidSafetynet({ + attStmt, + authData, + clientDataHash, + }); + } else if (fmt === ATTESTATION_FORMATS.NONE) { + // This is the weaker of the attestations, so there's nothing else to really check + verified = true; + } else { + throw new Error(`Unsupported Attestation Format: ${fmt}`); } - if (fmt === ATTESTATION_FORMATS.PACKED) { - return verifyPacked( - attestationObject, - response.clientDataJSON, - parsedAuthData, - ); - } + const toReturn: VerifiedAttestation = { + verified, + userVerified: flags.uv, + }; - if (fmt === ATTESTATION_FORMATS.ANDROID_SAFETYNET) { - return verifyAndroidSafetynet( - attestationObject, - response.clientDataJSON, - parsedAuthData, - credentialPublicKey, - ); - } + if (toReturn.verified) { + toReturn.userVerified = flags.uv; - if (fmt === ATTESTATION_FORMATS.NONE) { - return verifyNone( - attestationObject, - parsedAuthData, - ); + const publicKey = convertCOSEtoPKCS(credentialPublicKey); + + toReturn.authenticatorInfo = { + fmt, + counter, + base64PublicKey: base64url.encode(publicKey), + base64CredentialID: base64url.encode(credentialID), + }; } - throw new Error(`Unsupported Attestation Format: ${fmt}`); + return toReturn; } /** |