diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/attestation/verifications/verifyPacked.ts | 225 | ||||
-rw-r--r-- | src/attestation/verifyAttestationResponse.ts | 9 | ||||
-rw-r--r-- | src/helpers/constants.ts | 50 |
3 files changed, 230 insertions, 54 deletions
diff --git a/src/attestation/verifications/verifyPacked.ts b/src/attestation/verifications/verifyPacked.ts new file mode 100644 index 0000000..c5bc1b3 --- /dev/null +++ b/src/attestation/verifications/verifyPacked.ts @@ -0,0 +1,225 @@ +import base64url from 'base64url'; +import cbor from 'cbor'; +import elliptic from 'elliptic'; +import NodeRSA, { SigningSchemeHash } from 'node-rsa'; + +import { AttestationObject, VerifiedAttestation } from "@types"; +import parseAttestationAuthData from "@helpers/parseAttestationAuthData"; +import convertCOSEECDHAtoPKCS from "@helpers/convertCOSEECDHAtoPKCS"; +import toHash from "@helpers/toHash"; +import convertASN1toPEM from '@helpers/convertASN1toPEM'; +import getCertificateInfo from '@helpers/getCertificateInfo'; +import verifySignature from '@helpers/verifySignature'; + + +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 = convertCOSEECDHAtoPKCS(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 publicKeyCOSE: Map<COSEAlgorithmIdentifier, number | Buffer> = cbor.decodeAllSync(COSEPublicKey)[0]; + + const kty = publicKeyCOSE.get(COSEKEYS.kty); + const alg = publicKeyCOSE.get(COSEKEYS.alg); + const x = publicKeyCOSE.get(COSEKEYS.x); + const y = publicKeyCOSE.get(COSEKEYS.y); + + if (!alg) { + throw new Error('COSE public key was missing alg'); + } + + if (!kty) { + throw new Error('COSE public key was missing kty'); + } + + if (!x) { + throw new Error('COSE public key was missing x'); + } + + if (!y) { + throw new Error('COSE public key was missing y'); + } + + const hashAlg: string = COSEALGHASH[(alg as number)]; + + if (kty === COSEKTY.EC2) { + console.log('EC2'); + + const crv = publicKeyCOSE.get(COSEKEYS.crv); + + if (!crv) { + throw new Error('COSE public key was missing kty crv'); + } + + const ansiKey = Buffer.concat([ + Buffer.from([0x04]), + (x as Buffer), + (y as Buffer), + ]); + + const signatureBaseHash = toHash(signatureBase, hashAlg); + + const ec = new elliptic.ec(COSECRV[(crv as number)]); + const key = ec.keyFromPublic(ansiKey); + + toReturn.verified = key.verify(signatureBaseHash, sig); + } else if (kty === COSEKTY.RSA) { + console.log('RSA'); + + const n = publicKeyCOSE.get(COSEKEYS.n); + + if (!n) { + throw new Error('COSE public key was missing kty 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 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 COSEKEYS { + kty = 1, + alg = 3, + crv = -1, + x = -2, + y = -3, + n = -1, + e = -2, +} + +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.ts b/src/attestation/verifyAttestationResponse.ts index 3eba00f..8605dfb 100644 --- a/src/attestation/verifyAttestationResponse.ts +++ b/src/attestation/verifyAttestationResponse.ts @@ -3,6 +3,7 @@ import decodeClientDataJSON from '@helpers/decodeClientDataJSON'; import { ATTESTATION_FORMATS, EncodedAuthenticatorAttestationResponse, VerifiedAttestation } from '@types'; import verifyFIDOU2F from './verifications/verifyFIDOU2F'; +import verifyPacked from './verifications/verifyPacked'; /** * Verify that the user has legitimately completed the registration process @@ -47,10 +48,10 @@ export default function verifyAttestationResponse( return verifyFIDOU2F(attestationObject, base64ClientDataJSON); } - // if (fmt === ATTESTATION_FORMATS.PACKED) { - // console.log('Decoding Packed attestation'); - // return WebauthnService.verifyAttestationPacked(decodedAttestation, clientDataJSON); - // } + 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'); diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index 5168671..ab2efa9 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -1,54 +1,4 @@ /** - * U2F Presence constant - */ -export const U2F_USER_PRESENTED = 0x01; - -export const COSEKEYS = { - 'kty' : 1, - 'alg' : 3, - 'crv' : -1, - 'x' : -2, - 'y' : -3, - 'n' : -1, - 'e' : -2 -} - -export const COSEKTY = { - 'OKP': 1, - 'EC2': 2, - 'RSA': 3 -} - -export const COSERSASCHEME = { - '-3': 'pss-sha256', - '-39': 'pss-sha512', - '-38': 'pss-sha384', - '-65535': 'pkcs1-sha1', - '-257': 'pkcs1-sha256', - '-258': 'pkcs1-sha384', - '-259': 'pkcs1-sha512' -} - -export const COSEALGHASH = { - '-257': 'sha256', - '-258': 'sha384', - '-259': 'sha512', - '-65535': 'sha1', - '-39': 'sha512', - '-38': 'sha384', - '-37': 'sha256', - '-7': 'sha256', - '-8': 'sha512', - '-36': 'sha512' -} - -export const COSECRV = { - 1: 'p256', - 2: 'p384', - 3: 'p521', -}; - -/** * 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. * |