diff options
author | Matthew Miller <matthew@millerti.me> | 2020-05-20 08:44:12 -0700 |
---|---|---|
committer | Matthew Miller <matthew@millerti.me> | 2020-05-20 08:44:12 -0700 |
commit | 2c51287bec3592ebf7d40d886c41da8fb51cbc21 (patch) | |
tree | 603ec65541ea461cbe78c198c34574238eebd10a /src | |
parent | d9074ec54935aa2155151d2dd9dea0974f33da29 (diff) |
Initialize lerna project and move code to `server`
Diffstat (limited to 'src')
26 files changed, 0 insertions, 1545 deletions
diff --git a/src/assertion/generateAssertionCredentials.ts b/src/assertion/generateAssertionCredentials.ts deleted file mode 100644 index 71f9e44..0000000 --- a/src/assertion/generateAssertionCredentials.ts +++ /dev/null @@ -1,29 +0,0 @@ -import base64url from 'base64url'; - -import { AssertionCredentials } from '@libTypes'; - -/** - * Prepare credentials for user registration via navigator.credentials.get(...) - * - * @param challenge Random string the authenticator needs to sign and pass back - * @param base64CredentialIDs Array of base64-encoded authenticator IDs registered by the user for - * assertion - * @param timeout How long (in ms) the user can take to complete attestation - */ -export default function generateAssertionCredentials( - challenge: string, - base64CredentialIDs: string[], - timeout: number = 60000, -): AssertionCredentials { - return { - publicKey: { - challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)), - allowCredentials: base64CredentialIDs.map(id => ({ - id: base64url.toBuffer(id), - type: 'public-key', - transports: ['usb', 'ble', 'nfc'], - })), - timeout, - }, - }; -} diff --git a/src/assertion/parseAssertionAuthData.ts b/src/assertion/parseAssertionAuthData.ts deleted file mode 100644 index e6aa011..0000000 --- a/src/assertion/parseAssertionAuthData.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ParsedAssertionAuthData } from "@libTypes"; - -/** - * Make sense of the authData buffer contained in an Assertion - */ -export default function parseAssertionAuthData(authData: Buffer): ParsedAssertionAuthData { - let intBuffer = authData; - - const rpIdHash = intBuffer.slice(0, 32); - intBuffer = intBuffer.slice(32); - - const flagsBuf = intBuffer.slice(0, 1); - intBuffer = intBuffer.slice(1); - - const flags = flagsBuf[0]; - const counterBuf = intBuffer.slice(0, 4); - intBuffer = intBuffer.slice(4); - - const counter = counterBuf.readUInt32BE(0); - - return { - rpIdHash, - flagsBuf, - flags, - counter, - counterBuf, - }; -} diff --git a/src/assertion/verifyAssertionResponse.test.ts b/src/assertion/verifyAssertionResponse.test.ts deleted file mode 100644 index ba76943..0000000 --- a/src/assertion/verifyAssertionResponse.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import verifyAssertionResponse from './verifyAssertionResponse'; - -test('', () => { - const verification = verifyAssertionResponse( - { - base64AuthenticatorData: 'PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAhw', - base64ClientDataJSON: 'eyJjaGFsbGVuZ2UiOiJXRzVRU21RM1oyOTROR2gyTVROUk56WnViVmhMTlZZMWMwOHRP' + - 'V3BLVG5JIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoi' + - 'aHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3IiwidHlwZSI6IndlYmF1dGhuLmdldCJ9', - base64Signature: 'MEQCIHZYFY3LsKzI0T9XRwEACl7YsYZysZ2HUw3q9f7tlq3wAiBNbyBbQMNM56P6Z00tBEZ6v' + - 'II4f9Al-p4pZw7OBpSaog', - userHandle: null, - }, - 'https://dev.dontneeda.pw', - { - base64PublicKey: 'BBMQEnZRfg4ASys9kfGUj99Xlsa028wqYJZw8xuGahPQJWN3K9D9DajLxzKlY7uf_ulA5D6gh' + - 'UJ9hrouDX84S_I', - base64CredentialID: 'wJZRtQbYjKlpiRnzet7yyVizdsj_oUhi11kFbKyO0hc5gIg-4xeaTC9YC9y9sfow6gO3jE' + - 'MoONBKNX4SmSclmQ', - counter: 134, - }, - ); - - expect(verification.verified).toEqual(true); -}); diff --git a/src/assertion/verifyAssertionResponse.ts b/src/assertion/verifyAssertionResponse.ts deleted file mode 100644 index 49cc905..0000000 --- a/src/assertion/verifyAssertionResponse.ts +++ /dev/null @@ -1,90 +0,0 @@ -import base64url from 'base64url'; - -import { - EncodedAuthenticatorAssertionResponse, - U2F_USER_PRESENTED, - AuthenticatorDevice, - VerifiedAssertion, -} from "@libTypes"; -import decodeClientDataJSON from "@helpers/decodeClientDataJSON"; - -import parseAssertionAuthData from './parseAssertionAuthData'; -import toHash from '@helpers/toHash'; -import convertASN1toPEM from '@helpers/convertASN1toPEM'; -import verifySignature from '@helpers/verifySignature'; - -/** - * Verify that the user has legitimately completed the login process - * - * @param response Authenticator attestation response with base64-encoded values - * @param expectedOrigin Expected URL of website attestation should have occurred on - */ -export default function verifyAssertionResponse( - response: EncodedAuthenticatorAssertionResponse, - expectedOrigin: string, - authenticator: AuthenticatorDevice, -): VerifiedAssertion { - const { base64AuthenticatorData, base64ClientDataJSON, base64Signature } = response; - const clientDataJSON = decodeClientDataJSON(base64ClientDataJSON); - - console.debug('decodedClientDataJSON:', clientDataJSON); - - const { type, origin } = clientDataJSON; - - // Check that the origin is our site - if (origin !== expectedOrigin) { - console.error('client origin did not equal our origin'); - console.debug('expectedOrigin:', expectedOrigin); - console.debug('assertion\'s origin:', origin); - throw new Error('Assertion origin was an unexpected value'); - } - - // Make sure we're handling an assertion - if (type !== 'webauthn.get') { - console.error('type did not equal "webauthn.get"'); - console.debug('attestation\'s type:', type); - throw new Error('Attestation type was an unexpected value'); - } - - const authDataBuffer = base64url.toBuffer(base64AuthenticatorData); - const authData = parseAssertionAuthData(authDataBuffer); - console.log('parsed authData:', authData); - - if (!(authData.flags & U2F_USER_PRESENTED)) { - throw new Error('User was NOT present during authentication!'); - } - - const { - rpIdHash, - flagsBuf, - counterBuf, - counter, - } = authData; - - const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON)); - const signatureBase = Buffer.concat([ - rpIdHash, - flagsBuf, - counterBuf, - clientDataHash, - ]); - - const publicKey = convertASN1toPEM(base64url.toBuffer(authenticator.base64PublicKey)); - const signature = base64url.toBuffer(base64Signature); - - const toReturn = { - verified: verifySignature(signature, signatureBase, publicKey), - }; - - if (toReturn.verified) { - if (counter <= authenticator.counter) { - // Error out when the counter in the DB is greater than or equal to the counter in the - // dataStruct. It's related to how the authenticator maintains the number of times its been - // used for this client. If this happens, then someone's somehow increased the counter - // on the device without going through this site - throw new Error(`Device's counter ${counter} isn't greater than ${authenticator.counter}!`); - } - } - - return toReturn; -} diff --git a/src/attestation/generateAttestationCredentials.ts b/src/attestation/generateAttestationCredentials.ts deleted file mode 100644 index a45c21d..0000000 --- a/src/attestation/generateAttestationCredentials.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AttestationCredentials } from '@libTypes'; - -/** - * Prepare credentials for user registration via navigator.credentials.create(...) - * - * @param serviceName Friendly user-visible website name - * @param rpID Valid domain name (after `https://`) - * @param challenge Random string the authenticator needs to sign and pass back - * @param userID User's website-specific unique ID - * @param username User's website-specific username - * @param timeout How long (in ms) the user can take to complete attestation - * @param attestationType Request a full ("direct") or anonymized ("indirect") attestation statement - */ -export default function generateAttestationCredentials( - serviceName: string, - rpID: string, - challenge: string, - userID: string, - username: string, - timeout: number = 60000, - attestationType: 'direct' | 'indirect' = 'direct', -): AttestationCredentials { - return { - publicKey: { - // Cryptographically random bytes to prevent replay attacks - challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)), - // The organization registering and authenticating the user - rp: { - name: serviceName, - id: rpID, - }, - user: { - id: Uint8Array.from(userID, c => c.charCodeAt(0)), - name: username, - displayName: username, - }, - pubKeyCredParams: [{ - alg: -7, - type: 'public-key', - }], - timeout, - attestation: attestationType, - }, - }; -} diff --git a/src/attestation/parseAttestationAuthData.ts b/src/attestation/parseAttestationAuthData.ts deleted file mode 100644 index 996967d..0000000 --- a/src/attestation/parseAttestationAuthData.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ParsedAttestationAuthData } from "@libTypes"; - -/** - * Make sense of the authData buffer contained in an Attestation - */ -export default function parseAttestationAuthData(authData: Buffer): ParsedAttestationAuthData { - console.log('parsing attestation auth data'); - - let intBuffer = authData; - - const rpIdHash = intBuffer.slice(0, 32); - intBuffer = intBuffer.slice(32); - - const flagsBuf = intBuffer.slice(0, 1); - intBuffer = intBuffer.slice(1); - - const flagsInt = flagsBuf[0]; - - const flags = { - up: !!(flagsInt & 0x01), - uv: !!(flagsInt & 0x04), - at: !!(flagsInt & 0x40), - ed: !!(flagsInt & 0x80), - flagsInt, - }; - - console.debug('flags:', flags); - - const counterBuf = intBuffer.slice(0, 4); - intBuffer = intBuffer.slice(4); - - const counter = counterBuf.readUInt32BE(0); - - let aaguid: Buffer | undefined = undefined; - let credentialID: Buffer | undefined = undefined; - let COSEPublicKey: Buffer | undefined = undefined; - - if (flags.at) { - aaguid = intBuffer.slice(0, 16); - intBuffer = intBuffer.slice(16); - - const credIDLenBuf = intBuffer.slice(0, 2); - intBuffer = intBuffer.slice(2); - - const credIDLen = credIDLenBuf.readUInt16BE(0); - - credentialID = intBuffer.slice(0, credIDLen); - intBuffer = intBuffer.slice(credIDLen); - - COSEPublicKey = intBuffer; - } - - return { - rpIdHash, - flagsBuf, - flags, - counter, - counterBuf, - aaguid, - credentialID, - COSEPublicKey, - }; -} diff --git a/src/attestation/verifications/verifyAndroidSafetyNet.ts b/src/attestation/verifications/verifyAndroidSafetyNet.ts deleted file mode 100644 index 874c388..0000000 --- a/src/attestation/verifications/verifyAndroidSafetyNet.ts +++ /dev/null @@ -1,166 +0,0 @@ -import base64url from 'base64url'; - -import { - AttestationObject, - VerifiedAttestation, - SafetyNetJWTHeader, - SafetyNetJWTPayload, - SafetyNetJWTSignature, -} from "@libTypes"; -import toHash from "@helpers/toHash"; -import verifySignature from '@helpers/verifySignature'; -import convertCOSEtoPKCS from '@helpers/convertCOSEtoPKCS'; -import getCertificateInfo from '@helpers/getCertificateInfo'; - -import parseAttestationAuthData from '../parseAttestationAuthData'; - - -/** - * Verify an attestation response with fmt 'android-safetynet' - */ -export default function verifyAttestationAndroidSafetyNet( - attestationObject: AttestationObject, - base64ClientDataJSON: string, -): VerifiedAttestation { - const { attStmt, authData, fmt } = attestationObject; - - if (!attStmt.response) { - throw new Error('No response was included in attStmt by authenticator'); - } - - // Prepare to verify a JWT - const jwt = attStmt.response.toString('utf8'); - const jwtParts = jwt.split('.'); - - const HEADER: SafetyNetJWTHeader = JSON.parse(base64url.decode(jwtParts[0])); - const PAYLOAD: SafetyNetJWTPayload = JSON.parse(base64url.decode(jwtParts[1])); - const SIGNATURE: SafetyNetJWTSignature = jwtParts[2]; - - console.debug('HEADER:', HEADER); - console.debug('PAYLOAD:', PAYLOAD); - console.debug('SIGNATURE:', SIGNATURE); - - /** - * START Verify PAYLOAD - */ - const { nonce, ctsProfileMatch } = PAYLOAD; - const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON)); - - const nonceBase = Buffer.concat([ - authData, - clientDataHash, - ]); - const nonceBuffer = toHash(nonceBase); - const expectedNonce = nonceBuffer.toString('base64'); - - if (nonce !== expectedNonce) { - console.error('Payload nonce was not the expected value!'); - console.debug('payload nonce:', PAYLOAD.nonce); - console.debug('expected nonce:', expectedNonce); - throw new Error('Could not verify response payload nonce'); - } - - if (!ctsProfileMatch) { - console.error('ctsProfileMatch was false!'); - console.debug('ctsProfileMatch:', ctsProfileMatch); - throw new Error('Could not verify response payload profile'); - } - /** - * END Verify PAYLOAD - */ - - /** - * START Verify Header - */ - // Generate an array of certs constituting a full certificate chain - const fullpathCert = HEADER.x5c.concat([GlobalSignRootCAR2]).map((cert) => { - let pem = ''; - // Take a string of characters and chop them up into 64-char lines (just like a PEM cert) - for (let i = 0; i < cert.length; i += 64) { - pem += `${cert.slice(i, i + 64)}\n`; - } - - return `-----BEGIN CERTIFICATE-----\n${pem}-----END CERTIFICATE-----`; - }); - - console.debug('fullpathCert:', fullpathCert); - - const certificate = fullpathCert[0]; - - const commonCertInfo = getCertificateInfo(certificate); - console.debug('commonCertInfo:', commonCertInfo); - - const { subject } = commonCertInfo; - - // TODO: Find out where this CN string is specified and if it might change - if (subject.CN !== 'attest.android.com') { - console.error('common name was not "attest.android.com"'); - throw new Error('Could not verify certificate common name'); - } - - // TODO: Re-investigate this if we decide to "use MDS or Metadata Statements" - // validateCertificatePath(fullpathCert); - /** - * END Verify Header - */ - - /** - * START Verify Signature - */ - const signatureBaseBuffer = Buffer.from(`${jwtParts[0]}.${jwtParts[1]}`); - const signatureBuffer = base64url.toBuffer(SIGNATURE); - - const toReturn: VerifiedAttestation = { - verified: verifySignature(signatureBuffer, signatureBaseBuffer, certificate), - }; - /** - * END Verify Signature - */ - - - if (toReturn.verified) { - const authDataStruct = parseAttestationAuthData(authData); - console.debug('authDataStruct:', authDataStruct); - const { counter, credentialID, COSEPublicKey } = authDataStruct; - - if (!COSEPublicKey) { - throw new Error('No public key was provided by authenticator'); - } - - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator'); - } - - const publicKey = convertCOSEtoPKCS(COSEPublicKey); - - toReturn.authenticatorInfo = { - fmt, - counter, - base64PublicKey: base64url.encode(publicKey), - base64CredentialID: base64url.encode(credentialID), - }; - } - - return toReturn; -} - -/** - * This "GS Root R2" root certificate was downloaded from https://pki.goog/gsr2/GSR2.crt - * on 08/10/2019 and then run through `base64url.encode()` to get this representation. - * - * The certificate is valid until Dec 15, 2021 - */ -const GlobalSignRootCAR2 = 'MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UEC' + - 'xMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhc' + - 'NMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA' + - '1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKb' + - 'PJA6-Lm8omUVCxKs-IVSbC9N_hHD6ErPLv4dfxn-G07IwXNb9rfF73OX4YJYJkhD10FPe-3t-c4isUoh7SqbKSaZeqKeMW' + - 'hG8eoLrvozps6yWJQeXSpkqBy-0Hne_ig-1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjNS7SgfQx' + - '5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ_gk' + - 'wpRl4pazq-r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQY' + - 'wDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUm-IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0c' + - 'DovL2NybC5nbG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjANBgk' + - 'qhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0_WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk' + - '7mpM0sYmsL4h4hO291xNBrBVNpGP-DTKqttVCL1OmLNIG-6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavSot-3i9DAgBkcRcA' + - 'tjOj4LaR0VknFBbVPFd5uRHg5h6h-u_N5GJG79G-dwfCMNYxdAfvDbbnvRG15RjF-Cv6pgsH_76tuIMRQyV-dTZsXjAzlA' + - 'cmgQWpzU_qlULRuJQ_7TBj0_VLZjmmx6BEP3ojY-x1J96relc8geMJgEtslQIxq_H5COEBkEveegeGTLg'; diff --git a/src/attestation/verifications/verifyFIDOU2F.ts b/src/attestation/verifications/verifyFIDOU2F.ts deleted file mode 100644 index 9464053..0000000 --- a/src/attestation/verifications/verifyFIDOU2F.ts +++ /dev/null @@ -1,80 +0,0 @@ -import base64url from 'base64url'; - -import { AttestationObject, VerifiedAttestation, U2F_USER_PRESENTED } from '@libTypes'; -import toHash from '@helpers/toHash'; -import convertCOSEtoPKCS from '@helpers/convertCOSEtoPKCS'; -import convertASN1toPEM from '@helpers/convertASN1toPEM'; -import verifySignature from '@helpers/verifySignature'; - -import parseAttestationAuthData from '../parseAttestationAuthData'; - - -/** - * Verify an attestation response with fmt 'fido-u2f' - */ -export default function verifyAttestationFIDOU2F( - attestationObject: AttestationObject, - base64ClientDataJSON: string, -): VerifiedAttestation { - const { fmt, authData, attStmt } = attestationObject; - - const authDataStruct = parseAttestationAuthData(authData); - const { - flags, - COSEPublicKey, - rpIdHash, - credentialID, - counter, - } = authDataStruct; - - if (!(flags.flagsInt & U2F_USER_PRESENTED)) { - throw new Error('User was NOT present during authentication'); - } - - if (!COSEPublicKey) { - throw new Error('No public key was provided by authenticator'); - } - - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator'); - } - - const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON)); - const reservedByte = Buffer.from([0x00]); - const publicKey = convertCOSEtoPKCS(COSEPublicKey); - - const signatureBase = Buffer.concat([ - reservedByte, - rpIdHash, - clientDataHash, - credentialID, - publicKey, - ]); - - const { sig, x5c } = attStmt; - - if (!x5c) { - throw new Error('No attestation certificate provided in attestation statement'); - } - - if (!sig) { - throw new Error('No attestation signature provided in attestation statement'); - } - - const publicKeyCertPEM = convertASN1toPEM(x5c[0]); - - const toReturn: VerifiedAttestation = { - verified: verifySignature(sig, signatureBase, publicKeyCertPEM), - }; - - if (toReturn.verified) { - toReturn.authenticatorInfo = { - fmt, - counter, - base64PublicKey: base64url.encode(publicKey), - base64CredentialID: base64url.encode(credentialID), - }; - } - - return toReturn; -} diff --git a/src/attestation/verifications/verifyNone.ts b/src/attestation/verifications/verifyNone.ts deleted file mode 100644 index 18e9417..0000000 --- a/src/attestation/verifications/verifyNone.ts +++ /dev/null @@ -1,61 +0,0 @@ -import base64url from 'base64url'; - -import { AttestationObject, VerifiedAttestation } from "@libTypes"; -import convertCOSEtoPKCS from "@helpers/convertCOSEtoPKCS"; - -import parseAttestationAuthData from '../parseAttestationAuthData'; - - -/** - * Verify an attestation response with fmt 'none' - * - * This is the weaker of the assertions, so there are only so many checks we can perform - */ -export default function verifyAttestationNone( - attestationObject: AttestationObject, -): VerifiedAttestation { - const { fmt, authData } = attestationObject; - const authDataStruct = parseAttestationAuthData(authData); - - console.log('authDataStruct:', authDataStruct); - - const { - credentialID, - COSEPublicKey, - counter, - flags, - } = authDataStruct; - - if (!COSEPublicKey) { - throw new Error('No public key was provided by authenticator'); - } - - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator'); - } - - // Make sure the (U)ser (P)resent for the attestation - if (!flags.up) { - console.error('User was not Present for attestation'); - console.debug('attestation\'s flags:', flags); - throw new Error('User presence could not be verified'); - } - - if (!flags.uv) { - console.warn('The authenticator could not uniquely Verify the user'); - } - - const publicKey = convertCOSEtoPKCS(COSEPublicKey); - - const toReturn: VerifiedAttestation = { - verified: true, - authenticatorInfo: { - fmt, - counter, - base64PublicKey: base64url.encode(publicKey), - base64CredentialID: base64url.encode(credentialID), - }, - }; - - return toReturn; -} diff --git a/src/attestation/verifications/verifyPacked.ts b/src/attestation/verifications/verifyPacked.ts deleted file mode 100644 index b63fef0..0000000 --- a/src/attestation/verifications/verifyPacked.ts +++ /dev/null @@ -1,210 +0,0 @@ -import base64url from 'base64url'; -import cbor from 'cbor'; -import elliptic from 'elliptic'; -import NodeRSA, { SigningSchemeHash } from 'node-rsa'; - -import { AttestationObject, VerifiedAttestation, COSEKEYS, COSEPublicKey } from "@libTypes"; -import convertCOSEtoPKCS from "@helpers/convertCOSEtoPKCS"; -import toHash from "@helpers/toHash"; -import convertASN1toPEM from '@helpers/convertASN1toPEM'; -import getCertificateInfo from '@helpers/getCertificateInfo'; -import verifySignature from '@helpers/verifySignature'; - -import parseAttestationAuthData from '../parseAttestationAuthData'; - - -/** - * Verify an attestation response with fmt 'packed' - */ -export default function verifyAttestationPacked(attestationObject: AttestationObject, - base64ClientDataJSON: string, -): VerifiedAttestation { - const { fmt, authData, attStmt } = attestationObject; - const { sig, x5c, ecdaaKeyId } = attStmt; - - const authDataStruct = parseAttestationAuthData(authData); - - const { COSEPublicKey, counter, credentialID } = authDataStruct; - - if (!COSEPublicKey) { - throw new Error('No public key was provided by authenticator'); - } - - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator'); - } - - if (!sig) { - throw new Error('No attestation signature provided in attestation statement'); - } - - const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON)); - - const signatureBase = Buffer.concat([ - authData, - clientDataHash, - ]); - - const toReturn: VerifiedAttestation = { verified: false }; - const publicKey = convertCOSEtoPKCS(COSEPublicKey); - - if (x5c) { - console.log('FULL Attestation'); - - const leafCert = convertASN1toPEM(x5c[0]); - const leafCertInfo = getCertificateInfo(leafCert); - - const { subject, basicConstraintsCA, version } = leafCertInfo; - const { - OU, - CN, - O, - C, - } = subject; - - if (OU !== 'Authenticator Attestation') { - throw new Error('Batch certificate OU MUST be set strictly to "Authenticator Attestation"!'); - } - - if (!CN) { - throw new Error('Batch certificate CN MUST no be empty!'); - } - - if (!O) { - throw new Error('Batch certificate CN MUST no be empty!'); - } - - if (!C || C.length !== 2) { - throw new Error('Batch certificate C MUST be set to two character ISO 3166 code!'); - } - - if (basicConstraintsCA) { - throw new Error('Batch certificate basic constraints CA MUST be false!'); - } - - if (version !== 3) { - throw new Error('Batch certificate version MUST be 3(ASN1 2)!'); - } - - toReturn.verified = verifySignature(sig, signatureBase, leafCert); - } else if (ecdaaKeyId) { - throw new Error('ECDAA not supported yet'); - } else { - console.log('SELF Attestation'); - - const cosePublicKey: COSEPublicKey = cbor.decodeAllSync(COSEPublicKey)[0]; - - const kty = cosePublicKey.get(COSEKEYS.kty); - const alg = cosePublicKey.get(COSEKEYS.alg); - - if (!alg) { - throw new Error('COSE public key was missing alg'); - } - - if (!kty) { - throw new Error('COSE public key was missing kty'); - } - - const hashAlg: string = COSEALGHASH[(alg as number)]; - - if (kty === COSEKTY.EC2) { - console.log('EC2'); - - const crv = cosePublicKey.get(COSEKEYS.crv); - - if (!crv) { - throw new Error('COSE public key was missing kty crv'); - } - - const pkcsPublicKey = convertCOSEtoPKCS(cosePublicKey); - const signatureBaseHash = toHash(signatureBase, hashAlg); - - const ec = new elliptic.ec(COSECRV[(crv as number)]); - const key = ec.keyFromPublic(pkcsPublicKey); - - toReturn.verified = key.verify(signatureBaseHash, sig); - } else if (kty === COSEKTY.RSA) { - console.log('RSA'); - - const n = cosePublicKey.get(COSEKEYS.n); - - if (!n) { - throw new Error('COSE public key was missing n'); - } - - const signingScheme = COSERSASCHEME[alg as number]; - - // TODO: Verify this works - const key = new NodeRSA(); - key.setOptions({ signingScheme }); - key.importKey({ - n: (n as Buffer), - e: 65537, - }, 'components-public'); - - toReturn.verified = key.verify(signatureBase, sig); - } else if (kty === COSEKTY.OKP) { - console.log('OKP'); - - const x = cosePublicKey.get(COSEKEYS.x); - - if (!x) { - throw new Error('COSE public key was missing x'); - } - - const signatureBaseHash = toHash(signatureBase, hashAlg); - - const key = new elliptic.eddsa('ed25519'); - key.keyFromPublic((x as Buffer)); - - // TODO: is `publicKey` right here? - toReturn.verified = key.verify(signatureBaseHash, sig, publicKey); - } - } - - if (toReturn.verified) { - toReturn.authenticatorInfo = { - fmt, - counter, - base64PublicKey: base64url.encode(publicKey), - base64CredentialID: base64url.encode(credentialID), - }; - } - - return toReturn; -} - -enum COSEKTY { - OKP = 1, - EC2 = 2, - RSA = 3, -} - -const COSERSASCHEME: { [key: string]: SigningSchemeHash } = { - '-3': 'pss-sha256', - '-39': 'pss-sha512', - '-38': 'pss-sha384', - '-65535': 'pkcs1-sha1', - '-257': 'pkcs1-sha256', - '-258': 'pkcs1-sha384', - '-259': 'pkcs1-sha512' -} - -const COSECRV: { [key: number]: string } = { - 1: 'p256', - 2: 'p384', - 3: 'p521', -}; - -const COSEALGHASH: { [key: string]: string } = { - '-257': 'sha256', - '-258': 'sha384', - '-259': 'sha512', - '-65535': 'sha1', - '-39': 'sha512', - '-38': 'sha384', - '-37': 'sha256', - '-7': 'sha256', - '-8': 'sha512', - '-36': 'sha512' -} diff --git a/src/attestation/verifyAttestationResponse.test.ts b/src/attestation/verifyAttestationResponse.test.ts deleted file mode 100644 index 7b31dd0..0000000 --- a/src/attestation/verifyAttestationResponse.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import verifyAttestationResponse from './verifyAttestationResponse'; - -test('should verify FIDO U2F attestation', () => { - const verification = verifyAttestationResponse( - { - base64AttestationObject: 'o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAK40WxA0t7py7AjEXvwGw' + - 'TlmqlvrOks5g9lf+9zXzRiVAiEA3bv60xyXveKDOusYzniD7CDSostCet9PYK7FLdnTdZNjeDVjgVkCwTCCAr0wg' + - 'gGloAMCAQICBCrnYmMwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhb' + - 'CA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDV' + - 'QQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1Ymljb' + - 'yBVMkYgRUUgU2VyaWFsIDcxOTgwNzA3NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCoDhl5gQ9meEf8QqiVUV' + - '4S/Ca+Oax47MhcpIW9VEhqM2RDTmd3HaL3+SnvH49q8YubSRp/1Z1uP+okMynSGnj+jbDBqMCIGCSsGAQQBgsQKA' + - 'gQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgQwMCEGCysGAQQBguUcAQEEBBIEEG1Eu' + - 'pv27C5JuTAMj+kgy3MwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAclfQPNzD4RVphJDW+A75W1MHI' + - '3PZ5kcyYysR3Nx3iuxr1ZJtB+F7nFQweI3jL05HtFh2/4xVIgKb6Th4eVcjMecncBaCinEbOcdP1sEli9Hk2eVm1' + - 'XB5A0faUjXAPw/+QLFCjgXG6ReZ5HVUcWkB7riLsFeJNYitiKrTDXFPLy+sNtVNutcQnFsCerDKuM81TvEAigkIb' + - 'KCGlq8M/NvBg5j83wIxbCYiyV7mIr3RwApHieShzLdJo1S6XydgQjC+/64G5r8C+8AVvNFR3zXXCpio5C3KRIj88' + - 'HEEIYjf6h1fdLfqeIsq+cUUqbq5T+c4nNoZUZCysTB9v5EY4akp+GhhdXRoRGF0YVjEAbElFazplpnc037DORGDZ' + - 'NjDq86cN9vm6+APoAM20wtBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQGFYevaR71ptU5YtXOSnVzPQTsGgK+gLiBKnq' + - 'PWBmZXNRvjISqlLxiwApzlrfkTc3lEMYMatjeACCnsijOkNEGOlAQIDJiABIVggdWLG6UvGyHFw/k/bv6/k6z/LL' + - 'gSO5KXzXw2EcUxkEX8iWCBeaVLz/cbyoKvRIg/q+q7tan0VN+i3WR0BOBCcuNP7yw==', - base64ClientDataJSON: 'eyJjaGFsbGVuZ2UiOiJVMmQ0TjNZME0wOU1jbGRQYjFSNVpFeG5UbG95IiwiY2xpZW50' + - 'RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9jbG92ZXIu' + - 'bWlsbGVydGltZS5kZXY6MzAwMCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ==', - }, - 'https://clover.millertime.dev:3000', - ); - - expect(verification.verified).toEqual(true); - expect(verification.authenticatorInfo?.fmt).toEqual('fido-u2f'); - expect(verification.authenticatorInfo?.counter).toEqual(0); - expect(verification.authenticatorInfo?.base64PublicKey).toEqual( - 'BHVixulLxshxcP5P27-v5Os_yy4EjuSl818NhHFMZBF_XmlS8_3G8qCr0SIP6vqu7Wp9FTfot1kdATgQnLjT-8s', - ); - expect(verification.authenticatorInfo?.base64CredentialID).toEqual( - 'YVh69pHvWm1Tli1c5KdXM9BOwaAr6AuIEqeo9YGZlc1G-MhKqUvGLACnOWt-RNzeUQxgxq2N4AIKeyKM6Q0QYw', - ); -}); - -test('should verify Packed (EC2) attestation', () => { - const verification = verifyAttestationResponse( - { - base64AttestationObject: 'o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIhANvrPZMUFrl_rvlgR' + - 'qz6lCPlF6B4y885FYUCCrhrzAYXAiAb4dQKXbP3IimsTTadkwXQlrRVdxzlbmPXt847-Oh6r2hhdXRoRGF0YVjhP' + - 'dxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFXsOO-a3OAAI1vMYKZIsLJfHwVQMAXQGE4WNXLCDWOCa2x' + - '8hpqk5dZy_xdc4wBd4UgCJ4M_JAHI7oJgDDVb8WUcKqRB_mzRxwCL9vdTl-ZKPXg3_-Zrt1Adgb7EnK9ivqaTOKM' + - 'DqRrKsIObWYJaqpsSJtUKUBAgMmIAEhWCBKMVVaivqCBpqqAxMjuCo5jMeUdh3jDOC0EF4fLBNNTyJYILc7rqDDe' + - 'X1pwCLrl3ZX7IThrtZNwKQVLQyfHiorqP-n', - base64ClientDataJSON: 'eyJjaGFsbGVuZ2UiOiJjelpRU1dKQ2JsQlFibkpIVGxOQ2VFNWtkRVJ5VkRkVmNsWlpT' + - 'a3M1U0UwIiwib3JpZ2luIjoiaHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3IiwidHlwZSI6IndlYmF1dGhuLmNyZWF0' + - 'ZSJ9', - }, - 'https://dev.dontneeda.pw' - ) - - expect(verification.verified).toEqual(true); - expect(verification.authenticatorInfo?.fmt).toEqual('packed'); - expect(verification.authenticatorInfo?.counter).toEqual(1589874425); - expect(verification.authenticatorInfo?.base64PublicKey).toEqual( - 'BEoxVVqK-oIGmqoDEyO4KjmMx5R2HeMM4LQQXh8sE01PtzuuoMN5fWnAIuuXdlfshOGu1k3ApBUtDJ8eKiuo_6c', - ); - expect(verification.authenticatorInfo?.base64CredentialID).toEqual( - 'AYThY1csINY4JrbHyGmqTl1nL_F1zjAF3hSAIngz8kAcjugmAMNVvxZRwqpEH-bNHHAIv291OX5ko9eDf_5mu3U' + - 'B2BvsScr2K-ppM4owOpGsqwg5tZglqqmxIm1Q', - ); -}); - -test('should verify None attestation', () => { - const verification = verifyAttestationResponse( - { - base64AttestationObject: 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3I' + - 'pr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5E' + - 'gIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8x' + - 'xQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow', - base64ClientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYUVWalkxQlhkWHBw' + - 'VURBd1NEQndOV2Q0YURKZmRUVmZVRU0wVG1WWloyUSIsIm9yaWdpbiI6Imh0dHBzOlwvXC9kZXYuZG9udG5lZWRhLn' + - 'B3IiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoib3JnLm1vemlsbGEuZmlyZWZveCJ9' - }, - 'https://dev.dontneeda.pw' - ) - - expect(verification.verified).toEqual(true); - expect(verification.authenticatorInfo?.fmt).toEqual('none'); - expect(verification.authenticatorInfo?.counter).toEqual(0); - expect(verification.authenticatorInfo?.base64PublicKey).toEqual( - 'BD5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow', - ); - expect(verification.authenticatorInfo?.base64CredentialID).toEqual( - 'AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY', - ); -}); - -test('should verify Android SafetyNet attestation', () => { - const verification = verifyAttestationResponse( - { - base64AttestationObject: 'o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDE3MTIyMDM3aHJlc' + - '3BvbnNlWRS9ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxHYTJwRFEwSkljV2RCZDBsQ1FXZEpVV' + - 'kpZY205T01GcFBaRkpyUWtGQlFVRkJRVkIxYm5wQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFSkRUVkZ6ZDBOU' + - 'ldVUldVVkZIUlhkS1ZsVjZSV1ZOUW5kSFFURlZSVU5vVFZaU01qbDJXako0YkVsR1VubGtXRTR3U1VaT2JHTnVXb' + - 'kJaTWxaNlRWSk5kMFZSV1VSV1VWRkVSWGR3U0ZaR1RXZFJNRVZuVFZVNGVFMUNORmhFVkVVMFRWUkJlRTFFUVROT' + - 'lZHc3dUbFp2V0VSVVJUVk5WRUYzVDFSQk0wMVVhekJPVm05M1lrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaEZla' + - '0ZTUW1kT1ZrSkJaMVJEYTA1b1lrZHNiV0l6U25WaFYwVjRSbXBCVlVKblRsWkNRV05VUkZVeGRtUlhOVEJaVjJ4M' + - 'VNVWmFjRnBZWTNoRmVrRlNRbWRPVmtKQmIxUkRhMlIyWWpKa2MxcFRRazFVUlUxNFIzcEJXa0puVGxaQ1FVMVVSV' + - 'zFHTUdSSFZucGtRelZvWW0xU2VXSXliR3RNYlU1MllsUkRRMEZUU1hkRVVWbEtTMjlhU1doMlkwNUJVVVZDUWxGQ' + - 'lJHZG5SVkJCUkVORFFWRnZRMmRuUlVKQlRtcFlhM293WlVzeFUwVTBiU3N2UnpWM1QyOHJXRWRUUlVOeWNXUnVPR' + - 'Gh6UTNCU04yWnpNVFJtU3pCU2FETmFRMWxhVEVaSWNVSnJOa0Z0V2xaM01rczVSa2N3VHpseVVsQmxVVVJKVmxKN' + - 'VJUTXdVWFZ1VXpsMVowaEROR1ZuT1c5MmRrOXRLMUZrV2pKd09UTllhSHAxYmxGRmFGVlhXRU40UVVSSlJVZEtTe' + - 'k5UTW1GQlpucGxPVGxRVEZNeU9XaE1ZMUYxV1ZoSVJHRkROMDlhY1U1dWIzTnBUMGRwWm5NNGRqRnFhVFpJTDNob' + - '2JIUkRXbVV5YkVvck4wZDFkSHBsZUV0d2VIWndSUzkwV2xObVlsazVNRFZ4VTJ4Q2FEbG1jR293TVRWamFtNVJSb' + - 'XRWYzBGVmQyMUxWa0ZWZFdWVmVqUjBTMk5HU3pSd1pYWk9UR0Y0UlVGc0swOXJhV3hOZEVsWlJHRmpSRFZ1Wld3M' + - 'GVFcHBlWE0wTVROb1lXZHhWekJYYUdnMVJsQXpPV2hIYXpsRkwwSjNVVlJxWVhwVGVFZGtkbGd3YlRaNFJsbG9hQ' + - 'zh5VmsxNVdtcFVORXQ2VUVwRlEwRjNSVUZCWVU5RFFXeG5kMmRuU2xWTlFUUkhRVEZWWkVSM1JVSXZkMUZGUVhkS' + - 'lJtOUVRVlJDWjA1V1NGTlZSVVJFUVV0Q1oyZHlRbWRGUmtKUlkwUkJWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRV' + - 'TFDTUVkQk1WVmtSR2RSVjBKQ1VYRkNVWGRIVjI5S1FtRXhiMVJMY1hWd2J6UlhObmhVTm1veVJFRm1RbWRPVmtoV' + - 'FRVVkhSRUZYWjBKVFdUQm1hSFZGVDNaUWJTdDRaMjU0YVZGSE5rUnlabEZ1T1V0NlFtdENaMmR5UW1kRlJrSlJZM' + - 'EpCVVZKWlRVWlpkMHAzV1VsTGQxbENRbEZWU0UxQlIwZEhNbWd3WkVoQk5reDVPWFpaTTA1M1RHNUNjbUZUTlc1a' + - 'U1qbHVUREprTUdONlJuWk5WRUZ5UW1kbmNrSm5SVVpDVVdOM1FXOVpabUZJVWpCalJHOTJURE5DY21GVE5XNWlNa' + - 'mx1VERKa2VtTnFTWFpTTVZKVVRWVTRlRXh0VG5sa1JFRmtRbWRPVmtoU1JVVkdha0ZWWjJoS2FHUklVbXhqTTFGM' + - 'VdWYzFhMk50T1hCYVF6VnFZakl3ZDBsUldVUldVakJuUWtKdmQwZEVRVWxDWjFwdVoxRjNRa0ZuU1hkRVFWbExTM' + - '2RaUWtKQlNGZGxVVWxHUVhwQmRrSm5UbFpJVWpoRlMwUkJiVTFEVTJkSmNVRm5hR2cxYjJSSVVuZFBhVGgyV1ROS' + - '2MweHVRbkpoVXpWdVlqSTVia3d3WkZWVmVrWlFUVk0xYW1OdGQzZG5aMFZGUW1kdmNrSm5SVVZCWkZvMVFXZFJRM' + - 'EpKU0RGQ1NVaDVRVkJCUVdSM1EydDFVVzFSZEVKb1dVWkpaVGRGTmt4TldqTkJTMUJFVjFsQ1VHdGlNemRxYW1RN' + - 'E1FOTVRVE5qUlVGQlFVRlhXbVJFTTFCTVFVRkJSVUYzUWtsTlJWbERTVkZEVTFwRFYyVk1Tblp6YVZaWE5rTm5LM' + - 'mRxTHpsM1dWUktVbnAxTkVocGNXVTBaVmswWXk5dGVYcHFaMGxvUVV4VFlta3ZWR2g2WTNweGRHbHFNMlJyTTNaa' + - 'VRHTkpWek5NYkRKQ01HODNOVWRSWkdoTmFXZGlRbWRCU0ZWQlZtaFJSMjFwTDFoM2RYcFVPV1ZIT1ZKTVNTdDRNR' + - 'm95ZFdKNVdrVldla0UzTlZOWlZtUmhTakJPTUVGQlFVWnRXRkU1ZWpWQlFVRkNRVTFCVW1wQ1JVRnBRbU5EZDBFN' + - 'WFqZE9WRWRZVURJM09IbzBhSEl2ZFVOSWFVRkdUSGx2UTNFeVN6QXJlVXhTZDBwVlltZEpaMlk0WjBocWRuQjNNb' + - 'TFDTVVWVGFuRXlUMll6UVRCQlJVRjNRMnR1UTJGRlMwWlZlVm8zWmk5UmRFbDNSRkZaU2t0dldrbG9kbU5PUVZGR' + - 'lRFSlJRVVJuWjBWQ1FVazVibFJtVWt0SlYyZDBiRmRzTTNkQ1REVTFSVlJXTm10aGVuTndhRmN4ZVVGak5VUjFiV' + - 'FpZVHpReGExcDZkMG8yTVhkS2JXUlNVbFF2VlhORFNYa3hTMFYwTW1Nd1JXcG5iRzVLUTBZeVpXRjNZMFZYYkV4U' + - 'ldUSllVRXg1Um1wclYxRk9ZbE5vUWpGcE5GY3lUbEpIZWxCb2RETnRNV0kwT1doaWMzUjFXRTAyZEZnMVEzbEZTR' + - 'zVVYURoQ2IyMDBMMWRzUm1sb2VtaG5iamd4Ukd4a2IyZDZMMHN5VlhkTk5sTTJRMEl2VTBWNGEybFdabllyZW1KS' + - '01ISnFkbWM1TkVGc1pHcFZabFYzYTBrNVZrNU5ha1ZRTldVNGVXUkNNMjlNYkRabmJIQkRaVVkxWkdkbVUxZzBWV' + - 'Gw0TXpWdmFpOUpTV1F6VlVVdlpGQndZaTl4WjBkMmMydG1aR1Y2ZEcxVmRHVXZTMU50Y21sM1kyZFZWMWRsV0daV' + - 'Vlra3plbk5wYTNkYVltdHdiVkpaUzIxcVVHMW9kalJ5YkdsNlIwTkhkRGhRYmpod2NUaE5Na3RFWmk5UU0ydFdiM' + - '1F6WlRFNFVUMGlMQ0pOU1VsRlUycERRMEY2UzJkQmQwbENRV2RKVGtGbFR6QnRjVWRPYVhGdFFrcFhiRkYxUkVGT' + - '1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFrMU5VMEYzU0dkWlJGWlJVVXhGZUdSSVlrYzVhVmxYZUZSaFYyUjFTV' + - 'VpLZG1JelVXZFJNRVZuVEZOQ1UwMXFSVlJOUWtWSFFURlZSVU5vVFV0U01uaDJXVzFHYzFVeWJHNWlha1ZVVFVKR' + - 'lIwRXhWVVZCZUUxTFVqSjRkbGx0Um5OVk1teHVZbXBCWlVaM01IaE9la0V5VFZSVmQwMUVRWGRPUkVwaFJuY3dlV' + - 'TFVUlhsTlZGVjNUVVJCZDA1RVNtRk5SVWw0UTNwQlNrSm5UbFpDUVZsVVFXeFdWRTFTTkhkSVFWbEVWbEZSUzBWN' + - 'FZraGlNamx1WWtkVloxWklTakZqTTFGblZUSldlV1J0YkdwYVdFMTRSWHBCVWtKblRsWkNRVTFVUTJ0a1ZWVjVRa' + - '1JSVTBGNFZIcEZkMmRuUldsTlFUQkhRMU54UjFOSllqTkVVVVZDUVZGVlFVRTBTVUpFZDBGM1oyZEZTMEZ2U1VKQ' + - 'lVVUlJSMDA1UmpGSmRrNHdOWHByVVU4NUszUk9NWEJKVW5aS2VucDVUMVJJVnpWRWVrVmFhRVF5WlZCRGJuWlZRV' + - 'EJSYXpJNFJtZEpRMlpMY1VNNVJXdHpRelJVTW1aWFFsbHJMMnBEWmtNelVqTldXazFrVXk5a1RqUmFTME5GVUZwU' + - '2NrRjZSSE5wUzFWRWVsSnliVUpDU2pWM2RXUm5lbTVrU1UxWlkweGxMMUpIUjBac05YbFBSRWxMWjJwRmRpOVRTa' + - '2d2VlV3clpFVmhiSFJPTVRGQ2JYTkxLMlZSYlUxR0t5dEJZM2hIVG1oeU5UbHhUUzg1YVd3M01Va3laRTQ0Umtkb' + - 'VkyUmtkM1ZoWldvMFlsaG9jREJNWTFGQ1ltcDRUV05KTjBwUU1HRk5NMVEwU1N0RWMyRjRiVXRHYzJKcWVtRlVUa' + - '001ZFhwd1JteG5UMGxuTjNKU01qVjRiM2x1VlhoMk9IWk9iV3R4TjNwa1VFZElXR3Q0VjFrM2IwYzVhaXRLYTFKN' + - 'VFrRkNhemRZY2twbWIzVmpRbHBGY1VaS1NsTlFhemRZUVRCTVMxY3dXVE42Tlc5Nk1rUXdZekYwU2t0M1NFRm5UV' + - 'UpCUVVkcVoyZEZlazFKU1VKTWVrRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwRlpXWGRJVVZsRVZsSXdiRUpDV1hkR' + - '1FWbEpTM2RaUWtKUlZVaEJkMFZIUTBOelIwRlJWVVpDZDAxRFRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaa' + - 'mhEUVZGQmQwaFJXVVJXVWpCUFFrSlpSVVpLYWxJclJ6UlJOamdyWWpkSFEyWkhTa0ZpYjA5ME9VTm1NSEpOUWpoS' + - 'FFURlZaRWwzVVZsTlFtRkJSa3AyYVVJeFpHNUlRamRCWVdkaVpWZGlVMkZNWkM5alIxbFpkVTFFVlVkRFEzTkhRV' + - 'kZWUmtKM1JVSkNRMnQzU25wQmJFSm5aM0pDWjBWR1FsRmpkMEZaV1ZwaFNGSXdZMFJ2ZGt3eU9XcGpNMEYxWTBkM' + - 'GNFeHRaSFppTW1OMldqTk9lVTFxUVhsQ1owNVdTRkk0UlV0NlFYQk5RMlZuU21GQmFtaHBSbTlrU0ZKM1QyazRkb' + - 'Gt6U25OTWJrSnlZVk0xYm1JeU9XNU1NbVI2WTJwSmRsb3pUbmxOYVRWcVkyMTNkMUIzV1VSV1VqQm5Ra1JuZDA1c' + - 'VFUQkNaMXB1WjFGM1FrRm5TWGRMYWtGdlFtZG5ja0puUlVaQ1VXTkRRVkpaWTJGSVVqQmpTRTAyVEhrNWQyRXlhM' + - '1ZhTWpsMlduazVlVnBZUW5aak1td3dZak5LTlV4NlFVNUNaMnR4YUd0cFJ6bDNNRUpCVVhOR1FVRlBRMEZSUlVGS' + - 'GIwRXJUbTV1TnpoNU5uQlNhbVE1V0d4UlYwNWhOMGhVWjJsYUwzSXpVazVIYTIxVmJWbElVRkZ4TmxOamRHazVVR' + - 'VZoYW5aM1VsUXlhVmRVU0ZGeU1ESm1aWE54VDNGQ1dUSkZWRlYzWjFwUksyeHNkRzlPUm5ab2MwODVkSFpDUTA5S' + - 'llYcHdjM2RYUXpsaFNqbDRhblUwZEZkRVVVZzRUbFpWTmxsYVdpOVlkR1ZFVTBkVk9WbDZTbkZRYWxrNGNUTk5SS' + - 'Gh5ZW0xeFpYQkNRMlkxYnpodGR5OTNTalJoTWtjMmVIcFZjalpHWWpaVU9FMWpSRTh5TWxCTVVrdzJkVE5OTkZSN' + - 'mN6TkJNazB4YWpaaWVXdEtXV2s0ZDFkSlVtUkJka3RNVjFwMUwyRjRRbFppZWxsdGNXMTNhMjAxZWt4VFJGYzFia' + - '2xCU21KRlRFTlJRMXAzVFVnMU5uUXlSSFp4YjJaNGN6WkNRbU5EUmtsYVZWTndlSFUyZURaMFpEQldOMU4yU2tOR' + - 'GIzTnBjbE50U1dGMGFpODVaRk5UVmtSUmFXSmxkRGh4THpkVlN6UjJORnBWVGpnd1lYUnVXbm94ZVdjOVBTSmRmU' + - 'S5leUp1YjI1alpTSTZJbkZyYjB4dE9XSnJUeXNyYzJoMFZITnZheXRqUW1GRmJFcEJXa1pXTUcxRlFqQTVVbWcxV' + - 'TNKWVpGVTlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFOalUwTWpReU5qSTNOek1zSW1Gd2ExQmhZMnRoWjJWT1lXM' + - 'WxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJaXR0Y' + - '0ZKQ016RjRRemRTYUdsaWN5OWxWbUVyTDNWQ05XNTFaMVVyV0UxRFFXa3plSFZKZGpaMGIwMDlJaXdpWTNSelVIS' + - 'nZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0V' + - 'URGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYT' + - 'nBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5yUW5Ib2FZVGgxTEU2VVZwaU1lZWFidDdUeWJ3dzdXZk42RzJ5R01tZ' + - 'kVjbTFabjRWalZkenpoY1BqTS1WR052aWl1RGxyZ2VuWEViZ082V05YNlYzc0hHVjN1VGxGMlBuOUZsY3YxWmItS' + - '2NGVHZUd29iYnY3LUp5VUZzTlhTSnhHZFRTOWxwNU5EdDFnWGJ6OVpORWhzVXI3ajBqbWNyaU9rR29PRzM4MXRSa' + - '0Vqdk5aa0hpMkF1UDF2MWM4RXg3cEpZc09ISzJxaDlmSHFuSlAzcGowUFc3WThpcDBSTVZaNF9xZzFqc0dMMnZ0O' + - 'G12cEJFMjg5dE1fcnROdm94TWU2aEx0Q1ZkdE9ZRjIzMWMtWVFJd2FEbnZWdDcwYW5XLUZYdUx3R1J5dWhfRlpNM' + - '3FCSlhhcXdCNjNITk5uMmh5MFRDdHQ4RDdIMmI4MGltWkZRX1FoYXV0aERhdGFYxT3cRxDpwIiyKduonVYyILs59' + - 'yKa_0ZbCmVrGvuaivigRQAAAAC5P9lh8uZGL7EiggAiR954AEEBDL2BKZVhBca7N3j3asDaoSrA3tJgT_E4KN25T' + - 'hBVqBHCdffSZt9bvku7hPBcd76BzU7Y-ckXslUkD13Imbzde6UBAgMmIAEhWCCT4hId3ByJ_agRyznv1xIazx2nl' + - 'VEGyvN7intoZr7C2CJYIKo3XB-cca9aUOLC-xhp3GfhyfTS0hjws5zL_bT_N1AL', - base64ClientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiWDNaV1VHOUZOREpF' + - 'YUMxM2F6Tmlka2h0WVd0MGFWWjJSVmxETFV4M1FsZyIsIm9yaWdpbiI6Imh0dHBzOlwvXC9kZXYuZG9udG5lZWRh' + - 'LnB3IiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoiY29tLmFuZHJvaWQuY2hyb21lIn0' - }, - 'https://dev.dontneeda.pw' - ) - - expect(verification.verified).toEqual(true); - expect(verification.authenticatorInfo?.fmt).toEqual('android-safetynet'); - expect(verification.authenticatorInfo?.counter).toEqual(0); - expect(verification.authenticatorInfo?.base64PublicKey).toEqual( - 'BJPiEh3cHIn9qBHLOe_XEhrPHaeVUQbK83uKe2hmvsLYqjdcH5xxr1pQ4sL7GGncZ-HJ9NLSGPCznMv9tP83UAs', - ); - expect(verification.authenticatorInfo?.base64CredentialID).toEqual( - 'AQy9gSmVYQXGuzd492rA2qEqwN7SYE_xOCjduU4QVagRwnX30mbfW75Lu4TwXHe-gc1O2PnJF7JVJA9dyJm83Xs', - ); -}); diff --git a/src/attestation/verifyAttestationResponse.ts b/src/attestation/verifyAttestationResponse.ts deleted file mode 100644 index 775a150..0000000 --- a/src/attestation/verifyAttestationResponse.ts +++ /dev/null @@ -1,72 +0,0 @@ -import decodeAttestationObject from '@helpers/decodeAttestationObject'; -import decodeClientDataJSON from '@helpers/decodeClientDataJSON'; -import { ATTESTATION_FORMATS, EncodedAuthenticatorAttestationResponse, VerifiedAttestation } from '@libTypes'; - -import verifyFIDOU2F from './verifications/verifyFIDOU2F'; -import verifyPacked from './verifications/verifyPacked'; -import verifyNone from './verifications/verifyNone'; -import verifyAndroidSafetynet from './verifications/verifyAndroidSafetyNet'; - -/** - * Verify that the user has legitimately completed the registration process - * - * @param response Authenticator attestation response with base64-encoded values - * @param expectedOrigin Expected URL of website attestation should have occurred on - */ -export default function verifyAttestationResponse( - response: EncodedAuthenticatorAttestationResponse, - expectedOrigin: string, -): VerifiedAttestation { - const { base64AttestationObject, base64ClientDataJSON } = response; - const attestationObject = decodeAttestationObject(base64AttestationObject); - const clientDataJSON = decodeClientDataJSON(base64ClientDataJSON); - - console.debug('decoded attestationObject:', attestationObject); - console.debug('decoded clientDataJSON:', clientDataJSON); - - const { type, origin } = clientDataJSON; - - // Check that the origin is our site - if (origin !== expectedOrigin) { - console.error('client origin did not equal our origin'); - console.debug('Expected Origin:', expectedOrigin); - console.debug('attestation\'s origin:', origin); - throw new Error('Attestation origin was an unexpected value'); - } - - // Make sure we're handling an attestation - if (type !== 'webauthn.create') { - console.error('type did not equal "webauthn.create"'); - console.debug('attestation\'s type:', type); - throw new Error('Attestation type was an unexpected value'); - } - - const { fmt } = attestationObject; - - /** - * Verification can only be performed when attestation = 'direct' - */ - if (fmt === ATTESTATION_FORMATS.FIDO_U2F) { - console.log('Decoding FIDO-U2F attestation'); - return verifyFIDOU2F(attestationObject, base64ClientDataJSON); - } - - if (fmt === ATTESTATION_FORMATS.PACKED) { - console.log('Decoding Packed attestation'); - return verifyPacked(attestationObject, base64ClientDataJSON); - } - - if (fmt === ATTESTATION_FORMATS.ANDROID_SAFETYNET) { - console.log('Decoding Android Safetynet attestation'); - return verifyAndroidSafetynet(attestationObject, base64ClientDataJSON); - } - - if (fmt === ATTESTATION_FORMATS.NONE) { - console.log('Decoding None attestation'); - return verifyNone(attestationObject); - } - - const reason = `Unsupported Attestation Format: ${fmt}`; - console.error(reason); - throw new Error(reason); -} diff --git a/src/helpers/asciiToBinary.ts b/src/helpers/asciiToBinary.ts deleted file mode 100644 index b006edd..0000000 --- a/src/helpers/asciiToBinary.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Decode a base64-encoded string to a binary string - * - * @param input Base64-encoded string - */ -export default function asciiToBinary(input: string) { - return Buffer.from(input, 'base64').toString('binary'); -} diff --git a/src/helpers/convertASN1toPEM.ts b/src/helpers/convertASN1toPEM.ts deleted file mode 100644 index c282e15..0000000 --- a/src/helpers/convertASN1toPEM.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Convert binary certificate or public key to an OpenSSL-compatible PEM text format. - * - * @param buffer - Cert or PubKey buffer - * @return PEM - */ -export default function convertASN1toPEM(pkBuffer: Buffer) { - let buffer = pkBuffer; - - let type; - if (buffer.length === 65 && buffer[0] === 0x04) { - /** - * If needed, we encode rawpublic key to ASN structure, adding metadata: - * - * SEQUENCE { - * SEQUENCE { - * OBJECTIDENTIFIER 1.2.840.10045.2.1 (ecPublicKey) - * OBJECTIDENTIFIER 1.2.840.10045.3.1.7 (P-256) - * } - * BITSTRING <raw public key> - * } - * - * Luckily, to do that, we just need to prefix it with constant 26 bytes (metadata is - * constant). - */ - buffer = Buffer.concat([ - Buffer.from('3059301306072a8648ce3d020106082a8648ce3d030107034200', 'hex'), - buffer, - ]); - - type = 'PUBLIC KEY'; - } else { - type = 'CERTIFICATE'; - } - - const b64cert = buffer.toString('base64'); - - let PEMKey = ''; - for (let i = 0; i < Math.ceil(b64cert.length / 64); i += 1) { - const start = 64 * i; - - PEMKey += `${b64cert.substr(start, 64)}\n`; - } - - PEMKey = `-----BEGIN ${type}-----\n${PEMKey}-----END ${type}-----\n`; - - return PEMKey; -} diff --git a/src/helpers/convertCOSEtoPKCS.ts b/src/helpers/convertCOSEtoPKCS.ts deleted file mode 100644 index b7784d4..0000000 --- a/src/helpers/convertCOSEtoPKCS.ts +++ /dev/null @@ -1,49 +0,0 @@ -import cbor from 'cbor'; - -import { COSEKEYS, COSEPublicKey } from '@libTypes'; - -/** - * Takes COSE-encoded public key and converts it to PKCS key - * - * @param cosePublicKey COSE-encoded public key - * @return RAW PKCS encoded public key - */ -export default function convertCOSEtoPKCS(cosePublicKey: Buffer | COSEPublicKey) { - /* - +------+-------+-------+---------+----------------------------------+ - | name | key | label | type | description | - | | type | | | | - +------+-------+-------+---------+----------------------------------+ - | crv | 2 | -1 | int / | EC Curve identifier - Taken from | - | | | | tstr | the COSE Curves registry | - | | | | | | - | x | 2 | -2 | bstr | X Coordinate | - | | | | | | - | y | 2 | -3 | bstr / | Y Coordinate | - | | | | bool | | - | | | | | | - | d | 2 | -4 | bstr | Private key | - +------+-------+-------+---------+----------------------------------+ - */ - let struct: COSEPublicKey; - if (cosePublicKey instanceof Buffer) { - struct = cbor.decodeFirstSync(cosePublicKey); - } else { - struct = cosePublicKey; - } - - const tag = Buffer.from([0x04]); - const x = struct.get(COSEKEYS.x); - const y = struct.get(COSEKEYS.y); - - - if (!x) { - throw new Error('COSE public key was missing x'); - } - - if (!y) { - throw new Error('COSE public key was missing y'); - } - - return Buffer.concat([tag, (x as Buffer), (y as Buffer)]); -} diff --git a/src/helpers/decodeAttestationObject.test.ts b/src/helpers/decodeAttestationObject.test.ts deleted file mode 100644 index d36201e..0000000 --- a/src/helpers/decodeAttestationObject.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import decodeAttestationObject from './decodeAttestationObject'; - -test('should decode base64-encoded indirect attestationObject', () => { - const decoded = decodeAttestationObject( - 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEAbElFazplpnc037DORGDZNjDq86cN9vm6' + - '+APoAM20wtBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQKmPuEwByQJ3e89TccUSrCGDkNWquhevjLLn/' + - 'KNZZaxQQ0steueoG2g12dvnUNbiso8kVJDyLa+6UiA34eniujWlAQIDJiABIVggiUk8wN2j' + - '+3fkKI7KSiLBkKzs3FfhPZxHgHPnGLvOY/YiWCBv7+XyTqArnMVtQ947/8Xk8fnVCdLMRWJGM1VbNevVcQ==' - ); - - expect(decoded.fmt).toEqual('none'); - expect(decoded.attStmt).toEqual({}); - expect(decoded.authData).toBeDefined(); -}); - -test('should decode base64-encoded direct attestationObject', () => { - const decoded = decodeAttestationObject( - 'o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAK40WxA0t7py7AjEXvwGwTlmqlvrOk' + - 's5g9lf+9zXzRiVAiEA3bv60xyXveKDOusYzniD7CDSostCet9PYK7FLdnTdZNjeDVjgVkCwTCCAr0wggGloAMCAQICBCrn' + - 'YmMwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMT' + - 'QwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNV' + - 'BAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDcxOTgwNzA3NT' + - 'BZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCoDhl5gQ9meEf8QqiVUV4S/Ca+Oax47MhcpIW9VEhqM2RDTmd3HaL3+SnvH' + - '49q8YubSRp/1Z1uP+okMynSGnj+jbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBgu' + - 'UcAgEBBAQDAgQwMCEGCysGAQQBguUcAQEEBBIEEG1Eupv27C5JuTAMj+kgy3MwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0B' + - 'AQsFAAOCAQEAclfQPNzD4RVphJDW+A75W1MHI3PZ5kcyYysR3Nx3iuxr1ZJtB+F7nFQweI3jL05HtFh2/4xVIgKb6Th4eV' + - 'cjMecncBaCinEbOcdP1sEli9Hk2eVm1XB5A0faUjXAPw/+QLFCjgXG6ReZ5HVUcWkB7riLsFeJNYitiKrTDXFPLy+sNtVN' + - 'utcQnFsCerDKuM81TvEAigkIbKCGlq8M/NvBg5j83wIxbCYiyV7mIr3RwApHieShzLdJo1S6XydgQjC+/64G5r8C+8AVvN' + - 'FR3zXXCpio5C3KRIj88HEEIYjf6h1fdLfqeIsq+cUUqbq5T+c4nNoZUZCysTB9v5EY4akp+GhhdXRoRGF0YVjEAbElFazp' + - 'lpnc037DORGDZNjDq86cN9vm6+APoAM20wtBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQGFYevaR71ptU5YtXOSnVzPQTsGgK+' + - 'gLiBKnqPWBmZXNRvjISqlLxiwApzlrfkTc3lEMYMatjeACCnsijOkNEGOlAQIDJiABIVggdWLG6UvGyHFw/k/bv6/k6z/L' + - 'LgSO5KXzXw2EcUxkEX8iWCBeaVLz/cbyoKvRIg/q+q7tan0VN+i3WR0BOBCcuNP7yw==' - ); - - expect(decoded.fmt).toEqual('fido-u2f'); - expect(decoded.attStmt.sig).toBeDefined(); - expect(decoded.attStmt.x5c).toBeDefined(); - expect(decoded.authData).toBeDefined(); -}); diff --git a/src/helpers/decodeAttestationObject.ts b/src/helpers/decodeAttestationObject.ts deleted file mode 100644 index 224734e..0000000 --- a/src/helpers/decodeAttestationObject.ts +++ /dev/null @@ -1,17 +0,0 @@ -import base64url from 'base64url'; -import cbor from 'cbor'; - -import { AttestationObject } from '@libTypes'; - -/** - * Convert an AttestationObject from base64 string to a proper object - * - * @param base64AttestationObject Base64-encoded Attestation Object - */ -export default function decodeAttestationObject( - base64AttestationObject: string, -): AttestationObject { - const toBuffer = base64url.toBuffer(base64AttestationObject); - const toCBOR: AttestationObject = cbor.decodeAllSync(toBuffer)[0]; - return toCBOR; -} diff --git a/src/helpers/decodeClientDataJSON.test.ts b/src/helpers/decodeClientDataJSON.test.ts deleted file mode 100644 index b9868f8..0000000 --- a/src/helpers/decodeClientDataJSON.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import decodeClientDataJSON from './decodeClientDataJSON'; - -test('should convert base64-encoded attestation clientDataJSON to JSON', () => { - expect( - decodeClientDataJSON( - 'eyJjaGFsbGVuZ2UiOiJVMmQ0TjNZME0wOU1jbGRQYjFSNVpFeG5UbG95IiwiY2xpZW50RXh0ZW5zaW9ucyI6e30' + - 'sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9jbG92ZXIubWlsbGVydGltZS5kZX' + - 'Y6MzAwMCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ==' - )).toEqual({ - challenge: 'U2d4N3Y0M09McldPb1R5ZExnTloy', - clientExtensions: {}, - hashAlgorithm: 'SHA-256', - origin: 'https://clover.millertime.dev:3000', - type: 'webauthn.create' - }); -}); diff --git a/src/helpers/decodeClientDataJSON.ts b/src/helpers/decodeClientDataJSON.ts deleted file mode 100644 index 7aae023..0000000 --- a/src/helpers/decodeClientDataJSON.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ClientDataJSON } from '@libTypes'; - -import asciiToBinary from './asciiToBinary'; - -/** - * Decode an authenticator's base64-encoded clientDataJSON to JSON - */ -export default function decodeClientDataJSON(data: string): ClientDataJSON { - const toString = asciiToBinary(data); - return JSON.parse(toString); -} diff --git a/src/helpers/getCertificateInfo.ts b/src/helpers/getCertificateInfo.ts deleted file mode 100644 index 1779bb3..0000000 --- a/src/helpers/getCertificateInfo.ts +++ /dev/null @@ -1,31 +0,0 @@ -import jsrsasign from 'jsrsasign'; - -import { CertificateInfo } from '@libTypes'; - -/** - * Extract PEM certificate info - * - * @param pemCertificate Result from call to `convertASN1toPEM(x5c[0])` - */ -export default function getCertificateInfo(pemCertificate: string): CertificateInfo { - const subjectCert = new jsrsasign.X509(); - subjectCert.readCertPEM(pemCertificate); - - const subjectString = subjectCert.getSubjectString(); - const subjectParts = subjectString.slice(1).split('/'); - - const subject: { [key: string]: string } = {}; - subjectParts.forEach((field) => { - const [key, val] = field.split('='); - subject[key] = val; - }); - - const { getVersion } = subjectCert; - const basicConstraintsCA = !!subjectCert.getExtBasicConstraints().cA; - - return { - subject, - version: getVersion(), - basicConstraintsCA, - }; -} diff --git a/src/helpers/toHash.ts b/src/helpers/toHash.ts deleted file mode 100644 index 6e8db1d..0000000 --- a/src/helpers/toHash.ts +++ /dev/null @@ -1,10 +0,0 @@ -import crypto from 'crypto'; - -/** - * Returns hash digest of the given data using the given algorithm. - * @param data Data to hash - * @return The hash - */ -export default function toHash(data: Buffer, algo: string = 'SHA256'): Buffer { - return crypto.createHash(algo).update(data).digest(); -} diff --git a/src/helpers/validateCertificatePath.ts b/src/helpers/validateCertificatePath.ts deleted file mode 100644 index 685ddd8..0000000 --- a/src/helpers/validateCertificatePath.ts +++ /dev/null @@ -1,55 +0,0 @@ -export default function validateCertificatePath(certificates: any[]) { - console.log('certificates', certificates); - return false; - // TODO: Re-investigate this if we decide to "use MDS or Metadata Statements" - // console.debug('validating certificate path'); - - // const uniqueCerts = new Set(certificates); - - // if (uniqueCerts.size !== certificates.length) { - // throw new Error('Certificate path could not be verified due to duplicate certificates'); - // } - - // certificates.forEach((subjectPEM, index) => { - // const subjectCert = new jsrsasign.X509(); - // subjectCert.readCertPEM(subjectPEM); - - // let issuerPEM; - // if (index + 1 >= certificates.length) { - // console.debug('using subjectPEM as issuerPEM'); - // issuerPEM = subjectPEM; - // } else { - // console.debug('using next cert as issuerPEM'); - // issuerPEM = certificates[index + 1]; - // } - - // const issuerCert = new jsrsasign.X509(); - // issuerCert.readCertPEM(issuerPEM); - - // const subjectCertString = subjectCert.getSubjectString(); - // const issuerCertString = issuerCert.getSubjectString(); - // if (subjectCertString !== issuerCertString) { - // console.error('subject strings didn\'t match'); - // console.debug('subjectCertString:', subjectCertString); - // console.debug('issuerCertString:', issuerCertString); - // throw new Error('Certificate issuers didn\'t match'); - // } - - // const subjectCertStruct = jsrsasign.ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]); - // const algorithm = subjectCert.getSignatureAlgorithmField(); - // const signatureHex = subjectCert.getSignatureValueHex(); - - // const Signature = new jsrsasign.crypto.Signature({ alg: algorithm }); - // Signature.init(issuerPEM); - // Signature.updateHex(subjectCertStruct); - - // const sigVerified = Signature.verify(signatureHex); - // if (!sigVerified) { - // console.error('failed to validate certificate path'); - // console.debug('sigVerified:', sigVerified); - // throw new Error('Certificate path could not be validated'); - // } - // }); - - // return true; -} diff --git a/src/helpers/verifySignature.ts b/src/helpers/verifySignature.ts deleted file mode 100644 index c938a23..0000000 --- a/src/helpers/verifySignature.ts +++ /dev/null @@ -1,18 +0,0 @@ -import crypto from 'crypto'; - -/** - * Verify an authenticator's signature - * - * @param signature attStmt.sig - * @param signatureBase Output from Buffer.concat() - * @param publicKey Authenticator's public key as a PEM certificate - */ -export default function verifySignature( - signature: Buffer, - signatureBase: Buffer, - publicKey: string, -): boolean { - return crypto.createVerify('SHA256') - .update(signatureBase) - .verify(publicKey, signature); -} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 356ec2e..0000000 --- a/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import generateAttestationCredentials from './attestation/generateAttestationCredentials'; -import verifyAttestationResponse from './attestation/verifyAttestationResponse'; -import generateAssertionCredentials from './assertion/generateAssertionCredentials'; -import verifyAssertionResponse from './assertion/verifyAssertionResponse'; - -export { - generateAssertionCredentials, - verifyAttestationResponse, - generateAttestationCredentials, - verifyAssertionResponse, -}; - -export { - EncodedAuthenticatorAssertionResponse, - EncodedAuthenticatorAttestationResponse, - VerifiedAttestation, - VerifiedAssertion, - AuthenticatorDevice, -} from './libTypes'; diff --git a/src/libTypes.ts b/src/libTypes.ts deleted file mode 100644 index 58cc90c..0000000 --- a/src/libTypes.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * An object that can be passed into navigator.credentials.create(...) in the browser - */ -export type AttestationCredentials = { - publicKey: PublicKeyCredentialCreationOptions, -}; - -/** - * An object that can be passed into navigator.credentials.get(...) in the browser - */ -export type AssertionCredentials = { - publicKey: PublicKeyCredentialRequestOptions, -}; - -/** - * A slightly-modified AuthenticatorAttestationResponse to simplify working with ArrayBuffers that - * are base64-encoded in the browser so that they can be sent as JSON to the server. - */ -export interface EncodedAuthenticatorAttestationResponse extends Omit< -AuthenticatorAttestationResponse, 'clientDataJSON' | 'attestationObject' -> { - base64ClientDataJSON: string, - base64AttestationObject: string; -} - -/** - * A slightly-modified AuthenticatorAttestationResponse to simplify working with ArrayBuffers that - * are base64-encoded in the browser so that they can be sent as JSON to the server. - */ -export interface EncodedAuthenticatorAssertionResponse extends Omit< -AuthenticatorAssertionResponse, 'clientDataJSON' | 'authenticatorData' | 'signature' -> { - base64AuthenticatorData: string; - base64ClientDataJSON: string; - base64Signature: string; -} - -export enum ATTESTATION_FORMATS { - FIDO_U2F = 'fido-u2f', - PACKED = 'packed', - ANDROID_SAFETYNET = 'android-safetynet', - NONE = 'none', -} - -export type AttestationObject = { - fmt: ATTESTATION_FORMATS, - attStmt: { - sig?: Buffer, - x5c?: Buffer[], - ecdaaKeyId?: Buffer, - response?: Buffer, - }, - authData: Buffer, -}; - -export type ParsedAttestationAuthData = { - rpIdHash: Buffer, - flagsBuf: Buffer, - flags: { - up: boolean, - uv: boolean, - at: boolean, - ed: boolean, - flagsInt: number, - }, - counter: number, - counterBuf: Buffer, - aaguid?: Buffer, - credentialID?: Buffer, - COSEPublicKey?: Buffer, -}; - -export type ClientDataJSON = { - type: string, - challenge: string, - origin: string, -}; - -/** - * Result of attestation verification - */ -export type VerifiedAttestation = { - verified: boolean, - authenticatorInfo?: { - fmt: ATTESTATION_FORMATS, - counter: number, - base64PublicKey: string, - base64CredentialID: string, - }, -}; - -/** - * Result of assertion verification - */ -export type VerifiedAssertion = { - verified: boolean; -}; - -export type CertificateInfo = { - subject: { [key: string]: string }, - version: number, - basicConstraintsCA: boolean, -}; - -export enum COSEKEYS { - kty = 1, - alg = 3, - crv = -1, - x = -2, - y = -3, - n = -1, - e = -2, -} - -export type COSEPublicKey = Map<COSEAlgorithmIdentifier, number | Buffer>; - -export type SafetyNetJWTHeader = { - alg: 'string', - x5c: string[], -}; - -export type SafetyNetJWTPayload = { - nonce: string, - timestampMs: number, - apkPackageName: string, - apkDigestSha256: string, - ctsProfileMatch: boolean, - apkCertificateDigestSha256: string[], - basicIntegrity: boolean, -}; - -export type SafetyNetJWTSignature = string; - -export type ParsedAssertionAuthData = { - rpIdHash: Buffer, - flagsBuf: Buffer, - flags: number, - counter: number, - counterBuf: Buffer, -}; - -/** - * U2F Presence constant - */ -export const U2F_USER_PRESENTED = 0x01; - -/** - * A WebAuthn-compatible device and the information needed to verify assertions by it - */ -export type AuthenticatorDevice = { - base64PublicKey: string, - base64CredentialID: string, - // Number of times this device is expected to have been used - counter: number, -}; diff --git a/src/setupTests.ts b/src/setupTests.ts deleted file mode 100644 index 4cf23af..0000000 --- a/src/setupTests.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Silence some console output -jest.spyOn(console, 'log').mockImplementation(); -jest.spyOn(console, 'debug').mockImplementation(); |