summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
authorMatthew Miller <matthew@millerti.me>2022-12-18 07:59:52 -0800
committerGitHub <noreply@github.com>2022-12-18 07:59:52 -0800
commitf95c921d2578108ad3c7169d011a295665f8efea (patch)
treee177c21d1c6e830fd2ef5b02b976642486e84a4a /packages/server/src
parent6f363aa53a69cf8c1ea69664924c1e9f8e19dc4e (diff)
parentd4778c4b604abfa1e8a59d3997a4afc26057f980 (diff)
Merge pull request #311 from MasterKale/feat/faster-cert-chain-validation
feat/faster-cert-chain-validation
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/helpers/convertX509PublicKeyToCOSE.ts60
-rw-r--r--packages/server/src/helpers/getCertificateInfo.ts43
-rw-r--r--packages/server/src/helpers/isCertRevoked.ts89
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts2
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts4
-rw-r--r--packages/server/src/helpers/mapX509SignatureAlgToCOSEAlg.ts33
-rw-r--r--packages/server/src/helpers/validateCertificatePath.ts64
-rw-r--r--packages/server/src/helpers/verifySignature.ts16
-rw-r--r--packages/server/src/metadata/verifyAttestationWithMetadata.test.ts80
-rw-r--r--packages/server/src/metadata/verifyAttestationWithMetadata.ts9
-rw-r--r--packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts7
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts4
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts2
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationApple.test.ts4
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts4
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationPacked.ts4
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.test.ts2
17 files changed, 246 insertions, 181 deletions
diff --git a/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts b/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts
index cd76146..5d6b2fe 100644
--- a/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts
+++ b/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts
@@ -1,6 +1,6 @@
import { AsnParser } from '@peculiar/asn1-schema';
import { Certificate } from '@peculiar/asn1-x509';
-import { ECParameters, id_ecPublicKey, id_secp256r1 } from '@peculiar/asn1-ecc';
+import { ECParameters, id_ecPublicKey, id_secp256r1, id_secp384r1 } from '@peculiar/asn1-ecc';
import { RSAPublicKey } from '@peculiar/asn1-rsa';
import {
@@ -10,16 +10,16 @@ import {
COSEKEYS,
COSEPublicKeyEC2,
COSEPublicKeyRSA,
- COSEALG,
} from './cose';
+import { mapX509SignatureAlgToCOSEAlg } from './mapX509SignatureAlgToCOSEAlg';
-export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPublicKey {
+export function convertX509PublicKeyToCOSE(x509Certificate: Uint8Array): COSEPublicKey {
let cosePublicKey: COSEPublicKey = new Map();
/**
- * Time to extract the public key from an X.509 leaf certificate
+ * Time to extract the public key from an X.509 certificate
*/
- const x509 = AsnParser.parse(leafCertificate, Certificate);
+ const x509 = AsnParser.parse(x509Certificate, Certificate);
const { tbsCertificate } = x509;
const { subjectPublicKeyInfo, signature: _tbsSignature } = tbsCertificate;
@@ -32,7 +32,7 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub
* EC2 Public Key
*/
if (!subjectPublicKeyInfo.algorithm.parameters) {
- throw new Error('Leaf cert public key missing parameters (EC2)');
+ throw new Error('Certificate public key was missing parameters (EC2)');
}
const ecParameters = AsnParser.parse(
@@ -41,12 +41,14 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub
);
let crv = -999;
- if (ecParameters.namedCurve === id_secp256r1) {
+ const { namedCurve } = ecParameters;
+
+ if (namedCurve === id_secp256r1) {
crv = COSECRV.P256;
+ } else if (namedCurve === id_secp384r1) {
+ crv = COSECRV.P384;
} else {
- throw new Error(
- `Leaf cert public key contained unexpected namedCurve ${ecParameters.namedCurve} (EC2)`,
- );
+ throw new Error(`Certificate public key contained unexpected namedCurve ${namedCurve} (EC2)`);
}
const subjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey);
@@ -65,7 +67,7 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub
const coseEC2PubKey: COSEPublicKeyEC2 = new Map();
coseEC2PubKey.set(COSEKEYS.kty, COSEKTY.EC2);
- coseEC2PubKey.set(COSEKEYS.alg, signatureAlgorithmToCOSEAlg(signatureAlgorithm));
+ coseEC2PubKey.set(COSEKEYS.alg, mapX509SignatureAlgToCOSEAlg(signatureAlgorithm));
coseEC2PubKey.set(COSEKEYS.crv, crv);
coseEC2PubKey.set(COSEKEYS.x, x);
coseEC2PubKey.set(COSEKEYS.y, y);
@@ -79,46 +81,16 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub
const coseRSAPubKey: COSEPublicKeyRSA = new Map();
coseRSAPubKey.set(COSEKEYS.kty, COSEKTY.RSA);
- coseRSAPubKey.set(COSEKEYS.alg, signatureAlgorithmToCOSEAlg(signatureAlgorithm));
+ coseRSAPubKey.set(COSEKEYS.alg, mapX509SignatureAlgToCOSEAlg(signatureAlgorithm));
coseRSAPubKey.set(COSEKEYS.n, new Uint8Array(rsaPublicKey.modulus));
coseRSAPubKey.set(COSEKEYS.e, new Uint8Array(rsaPublicKey.publicExponent));
cosePublicKey = coseRSAPubKey;
} else {
- throw new Error(`Unexpected leaf cert public key algorithm ${publicKeyAlgorithmID}`);
- }
-
- return cosePublicKey;
-}
-
-/**
- * Map X.509 signature algorithm OIDs to COSE algorithm IDs
- *
- * - EC2 OIDs: https://oidref.com/1.2.840.10045.4.3
- * - RSA OIDs: https://oidref.com/1.2.840.113549.1.1
- */
-function signatureAlgorithmToCOSEAlg(signatureAlgorithm: string): COSEALG {
- let alg: COSEALG;
-
- if (signatureAlgorithm === '1.2.840.10045.4.3.2') {
- alg = COSEALG.ES256;
- } else if (signatureAlgorithm === '1.2.840.10045.4.3.3') {
- alg = COSEALG.ES384;
- } else if (signatureAlgorithm === '1.2.840.10045.4.3.4') {
- alg = COSEALG.ES512;
- } else if (signatureAlgorithm === '1.2.840.113549.1.1.11') {
- alg = COSEALG.RS256;
- } else if (signatureAlgorithm === '1.2.840.113549.1.1.12') {
- alg = COSEALG.RS384;
- } else if (signatureAlgorithm === '1.2.840.113549.1.1.13') {
- alg = COSEALG.RS512;
- } else if (signatureAlgorithm === '1.2.840.113549.1.1.5') {
- alg = COSEALG.RS1;
- } else {
throw new Error(
- `Leaf cert contained unexpected signature algorithm ${signatureAlgorithm} (EC2)`,
+ `Certificate public key contained unexpected algorithm ID ${publicKeyAlgorithmID}`,
);
}
- return alg;
+ return cosePublicKey;
}
diff --git a/packages/server/src/helpers/getCertificateInfo.ts b/packages/server/src/helpers/getCertificateInfo.ts
index e503f70..7ec6eba 100644
--- a/packages/server/src/helpers/getCertificateInfo.ts
+++ b/packages/server/src/helpers/getCertificateInfo.ts
@@ -8,6 +8,7 @@ export type CertificateInfo = {
basicConstraintsCA: boolean;
notBefore: Date;
notAfter: Date;
+ parsedCertificate: Certificate;
};
type Issuer = {
@@ -15,6 +16,7 @@ type Issuer = {
O?: string;
OU?: string;
CN?: string;
+ combined: string;
};
type Subject = {
@@ -22,6 +24,7 @@ type Subject = {
O?: string;
OU?: string;
CN?: string;
+ combined: string;
};
const issuerSubjectIDKey: { [key: string]: 'C' | 'O' | 'OU' | 'CN' } = {
@@ -37,26 +40,28 @@ const issuerSubjectIDKey: { [key: string]: 'C' | 'O' | 'OU' | 'CN' } = {
* @param pemCertificate Result from call to `convertASN1toPEM(x5c[0])`
*/
export function getCertificateInfo(leafCertBuffer: Uint8Array): CertificateInfo {
- const asnx509 = AsnParser.parse(leafCertBuffer, Certificate);
- const parsedCert = asnx509.tbsCertificate;
+ const x509 = AsnParser.parse(leafCertBuffer, Certificate);
+ const parsedCert = x509.tbsCertificate;
// Issuer
- const issuer: Issuer = {};
+ const issuer: Issuer = { combined: '' };
parsedCert.issuer.forEach(([iss]) => {
const key = issuerSubjectIDKey[iss.type];
if (key) {
issuer[key] = iss.value.toString();
}
});
+ issuer.combined = issuerSubjectToString(issuer);
// Subject
- const subject: Subject = {};
+ const subject: Subject = { combined: '' };
parsedCert.subject.forEach(([iss]) => {
const key = issuerSubjectIDKey[iss.type];
if (key) {
subject[key] = iss.value.toString();
}
});
+ subject.combined = issuerSubjectToString(subject);
let basicConstraintsCA = false;
if (parsedCert.extensions) {
@@ -76,5 +81,35 @@ export function getCertificateInfo(leafCertBuffer: Uint8Array): CertificateInfo
basicConstraintsCA,
notBefore: parsedCert.validity.notBefore.getTime(),
notAfter: parsedCert.validity.notAfter.getTime(),
+ parsedCertificate: x509,
};
}
+
+/**
+ * Stringify the parts of Issuer or Subject info for easier comparison of subject issuers with
+ * issuer subjects.
+ *
+ * The order might seem arbitrary, because it is. It should be enough that the two are stringified
+ * in the same order.
+ */
+function issuerSubjectToString(input: Issuer | Subject): string {
+ const parts: string[] = [];
+
+ if (input.C) {
+ parts.push(input.C);
+ }
+
+ if (input.O) {
+ parts.push(input.O);
+ }
+
+ if (input.OU) {
+ parts.push(input.OU);
+ }
+
+ if (input.CN) {
+ parts.push(input.CN);
+ }
+
+ return parts.join(' : ');
+}
diff --git a/packages/server/src/helpers/isCertRevoked.ts b/packages/server/src/helpers/isCertRevoked.ts
index 2d7f5d6..97f2216 100644
--- a/packages/server/src/helpers/isCertRevoked.ts
+++ b/packages/server/src/helpers/isCertRevoked.ts
@@ -1,9 +1,16 @@
-import { X509 } from 'jsrsasign';
import fetch from 'cross-fetch';
import { AsnParser } from '@peculiar/asn1-schema';
-import { CertificateList } from '@peculiar/asn1-x509';
+import {
+ CertificateList,
+ Certificate,
+ AuthorityKeyIdentifier,
+ id_ce_authorityKeyIdentifier,
+ SubjectKeyIdentifier,
+ id_ce_subjectKeyIdentifier,
+ id_ce_cRLDistributionPoints,
+ CRLDistributionPoints,
+} from '@peculiar/asn1-x509';
-import { convertCertBufferToPEM } from './convertCertBufferToPEM';
import { isoUint8Array } from './iso';
/**
@@ -23,31 +30,44 @@ const cacheRevokedCerts: { [certAuthorityKeyID: string]: CAAuthorityInfo } = {};
*
* CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117
*/
-export async function isCertRevoked(cert: X509): Promise<boolean> {
- const certSerialHex = cert.getSerialNumberHex();
+export async function isCertRevoked(cert: Certificate): Promise<boolean> {
+ const { extensions } = cert.tbsCertificate;
- // Check to see if we've got cached info for the cert's CA
- let keyIdentifier: jsrsasign.AuthorityKeyIdentifierResult | jsrsasign.ExtSubjectKeyIdentifier | undefined = undefined;
- try {
- keyIdentifier = cert.getExtAuthorityKeyIdentifier();
- } catch (err) {
- // pass
+ if (!extensions) {
+ return false;
}
- /**
- * We might be dealing with a self-signed root certificate. Check the
- * Subject key Identifier extension next.
- */
- if (!keyIdentifier) {
- try {
- keyIdentifier = cert.getExtSubjectKeyIdentifier();
- } catch (err) {
- // pass
+ let extAuthorityKeyID: AuthorityKeyIdentifier | undefined;
+ let extSubjectKeyID: SubjectKeyIdentifier | undefined;
+ let extCRLDistributionPoints: CRLDistributionPoints | undefined;
+
+ extensions.forEach(ext => {
+ if (ext.extnID === id_ce_authorityKeyIdentifier) {
+ extAuthorityKeyID = AsnParser.parse(ext.extnValue, AuthorityKeyIdentifier);
+ } else if (ext.extnID === id_ce_subjectKeyIdentifier) {
+ extSubjectKeyID = AsnParser.parse(ext.extnValue, SubjectKeyIdentifier);
+ } else if (ext.extnID === id_ce_cRLDistributionPoints) {
+ extCRLDistributionPoints = AsnParser.parse(ext.extnValue, CRLDistributionPoints);
}
+ });
+
+ // Check to see if we've got cached info for the cert's CA
+ let keyIdentifier: string | undefined = undefined;
+
+ if (extAuthorityKeyID && extAuthorityKeyID.keyIdentifier) {
+ keyIdentifier = isoUint8Array.toHex(new Uint8Array(extAuthorityKeyID.keyIdentifier.buffer));
+ } else if (extSubjectKeyID) {
+ /**
+ * We might be dealing with a self-signed root certificate. Check the
+ * Subject key Identifier extension next.
+ */
+ keyIdentifier = isoUint8Array.toHex(new Uint8Array(extSubjectKeyID.buffer));
}
+ const certSerialHex = isoUint8Array.toHex(new Uint8Array(cert.tbsCertificate.serialNumber));
+
if (keyIdentifier) {
- const cached = cacheRevokedCerts[keyIdentifier.kid.hex];
+ const cached = cacheRevokedCerts[keyIdentifier];
if (cached) {
const now = new Date();
// If there's a nextUpdate then make sure we're before it
@@ -57,13 +77,8 @@ export async function isCertRevoked(cert: X509): Promise<boolean> {
}
}
- let crlURL = undefined;
- try {
- crlURL = cert.getExtCRLDistributionPointsURI();
- } catch (err) {
- // Cert probably didn't include any CDP URIs
- return false;
- }
+ const crlURL =
+ extCRLDistributionPoints?.[0].distributionPoint?.fullName?.[0].uniformResourceIdentifier;
// If no URL is provided then we have nothing to check
if (!crlURL) {
@@ -71,17 +86,21 @@ export async function isCertRevoked(cert: X509): Promise<boolean> {
}
// Download and read the CRL
- const crlCert = new X509();
+ let certListBytes: ArrayBuffer;
try {
- const respCRL = await fetch(crlURL[0]);
- const dataCRL = await respCRL.arrayBuffer();
- const dataPEM = convertCertBufferToPEM(new Uint8Array(dataCRL));
- crlCert.readCertPEM(dataPEM);
+ const respCRL = await fetch(crlURL);
+ certListBytes = await respCRL.arrayBuffer();
} catch (err) {
return false;
}
- const data = AsnParser.parse(isoUint8Array.fromHex(crlCert.hex), CertificateList);
+ let data: CertificateList;
+ try {
+ data = AsnParser.parse(certListBytes, CertificateList);
+ } catch (err) {
+ // Something was malformed with the CRL, so pass
+ return false;
+ }
const newCached: CAAuthorityInfo = {
revokedCerts: [],
@@ -104,7 +123,7 @@ export async function isCertRevoked(cert: X509): Promise<boolean> {
// Cache the results
if (keyIdentifier) {
- cacheRevokedCerts[keyIdentifier.kid.hex] = newCached;
+ cacheRevokedCerts[keyIdentifier] = newCached;
}
return newCached.revokedCerts.indexOf(certSerialHex) >= 0;
diff --git a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts
index 3394b90..277bc9e 100644
--- a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts
+++ b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts
@@ -15,5 +15,5 @@ export function mapCoseAlgToWebCryptoAlg(alg: COSEALG): SubtleCryptoAlg {
return 'SHA-512';
}
- throw new Error(`Unexpected COSE alg value of ${alg}`);
+ throw new Error(`Could not map COSE alg value of ${alg} to a WebCrypto alg`);
}
diff --git a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts
index 8be875c..a33c219 100644
--- a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts
+++ b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts
@@ -9,11 +9,11 @@ export function mapCoseAlgToWebCryptoKeyAlgName(alg: COSEALG): SubtleCryptoKeyAl
return 'Ed25519';
} else if ([COSEALG.ES256, COSEALG.ES384, COSEALG.ES512, COSEALG.ES256K].indexOf(alg) >= 0) {
return 'ECDSA';
- } else if ([COSEALG.RS256, COSEALG.RS384, COSEALG.RS512].indexOf(alg) >= 0) {
+ } else if ([COSEALG.RS256, COSEALG.RS384, COSEALG.RS512, COSEALG.RS1].indexOf(alg) >= 0) {
return 'RSASSA-PKCS1-v1_5';
} else if ([COSEALG.PS256, COSEALG.PS384, COSEALG.PS512].indexOf(alg) >= 0) {
return 'RSA-PSS';
}
- throw new Error(`Unexpected COSE alg value of ${alg}`);
+ throw new Error(`Could not map COSE alg value of ${alg} to a WebCrypto key alg name`);
}
diff --git a/packages/server/src/helpers/mapX509SignatureAlgToCOSEAlg.ts b/packages/server/src/helpers/mapX509SignatureAlgToCOSEAlg.ts
new file mode 100644
index 0000000..026b5d0
--- /dev/null
+++ b/packages/server/src/helpers/mapX509SignatureAlgToCOSEAlg.ts
@@ -0,0 +1,33 @@
+import { COSEALG } from './cose';
+
+/**
+ * Map X.509 signature algorithm OIDs to COSE algorithm IDs
+ *
+ * - EC2 OIDs: https://oidref.com/1.2.840.10045.4.3
+ * - RSA OIDs: https://oidref.com/1.2.840.113549.1.1
+ */
+export function mapX509SignatureAlgToCOSEAlg(signatureAlgorithm: string): COSEALG {
+ let alg: COSEALG;
+
+ if (signatureAlgorithm === '1.2.840.10045.4.3.2') {
+ alg = COSEALG.ES256;
+ } else if (signatureAlgorithm === '1.2.840.10045.4.3.3') {
+ alg = COSEALG.ES384;
+ } else if (signatureAlgorithm === '1.2.840.10045.4.3.4') {
+ alg = COSEALG.ES512;
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.11') {
+ alg = COSEALG.RS256;
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.12') {
+ alg = COSEALG.RS384;
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.13') {
+ alg = COSEALG.RS512;
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.5') {
+ alg = COSEALG.RS1;
+ } else {
+ throw new Error(
+ `Unable to map X.509 signature algorithm ${signatureAlgorithm} to a COSE algorithm`,
+ );
+ }
+
+ return alg;
+}
diff --git a/packages/server/src/helpers/validateCertificatePath.ts b/packages/server/src/helpers/validateCertificatePath.ts
index 661c9e7..65e4870 100644
--- a/packages/server/src/helpers/validateCertificatePath.ts
+++ b/packages/server/src/helpers/validateCertificatePath.ts
@@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
-// `ASN1HEX` exists in the lib but not in its typings
-// @ts-ignore 2305
-import { KJUR, X509, ASN1HEX, zulutodate } from 'jsrsasign';
+import { AsnSerializer } from '@peculiar/asn1-schema';
import { isCertRevoked } from './isCertRevoked';
-
-const { crypto } = KJUR;
+import { isoBase64URL } from './iso';
+import { verifySignature } from './verifySignature';
+import { mapX509SignatureAlgToCOSEAlg } from './mapX509SignatureAlgToCOSEAlg';
+import { getCertificateInfo } from './getCertificateInfo';
/**
* Traverse an array of PEM certificates and ensure they form a proper chain
@@ -63,9 +63,6 @@ async function _validatePath(certificates: string[]): Promise<boolean> {
for (let i = 0; i < certificates.length; i += 1) {
const subjectPem = certificates[i];
- const subjectCert = new X509();
- subjectCert.readCertPEM(subjectPem);
-
const isLeafCert = i === 0;
const isRootCert = i + 1 >= certificates.length;
@@ -76,19 +73,20 @@ async function _validatePath(certificates: string[]): Promise<boolean> {
issuerPem = certificates[i + 1];
}
- const issuerCert = new X509();
- issuerCert.readCertPEM(issuerPem);
+ const subjectInfo = getCertificateInfo(pemToBytes(subjectPem));
+ const issuerInfo = getCertificateInfo(pemToBytes(issuerPem));
+
+ const x509Subject = subjectInfo.parsedCertificate;
// Check for certificate revocation
- const subjectCertRevoked = await isCertRevoked(subjectCert);
+ const subjectCertRevoked = await isCertRevoked(x509Subject);
if (subjectCertRevoked) {
throw new Error(`Found revoked certificate in certificate path`);
}
// Check that intermediate certificate is within its valid time window
- const notBefore = zulutodate(issuerCert.getNotBefore());
- const notAfter = zulutodate(issuerCert.getNotAfter());
+ const { notBefore, notAfter } = issuerInfo;
const now = new Date(Date.now());
if (notBefore > now || notAfter < now) {
@@ -107,20 +105,26 @@ async function _validatePath(certificates: string[]): Promise<boolean> {
}
}
- if (subjectCert.getIssuerString() !== issuerCert.getSubjectString()) {
+ if (subjectInfo.issuer.combined !== issuerInfo.subject.combined) {
throw new InvalidSubjectAndIssuer();
}
- const subjectCertStruct = ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]);
- const alg = subjectCert.getSignatureAlgorithmField();
- const signatureHex = subjectCert.getSignatureValueHex();
-
- const Signature = new crypto.Signature({ alg });
- Signature.init(issuerPem);
- // TODO: `updateHex()` takes approximately two seconds per execution, can we improve this?
- Signature.updateHex(subjectCertStruct ?? '');
-
- if (!Signature.verify(signatureHex)) {
+ // Verify the subject certificate's signature with the issuer cert's public key
+ const data = AsnSerializer.serialize(x509Subject.tbsCertificate);
+ const signature = x509Subject.signatureValue;
+ const signatureAlgorithm = mapX509SignatureAlgToCOSEAlg(
+ x509Subject.signatureAlgorithm.algorithm,
+ );
+ const issuerCertBytes = pemToBytes(issuerPem);
+
+ const verified = await verifySignature({
+ data: new Uint8Array(data),
+ signature: new Uint8Array(signature),
+ x509Certificate: issuerCertBytes,
+ hashAlgorithm: signatureAlgorithm,
+ });
+
+ if (!verified) {
throw new Error('Invalid certificate path: invalid signature');
}
}
@@ -143,3 +147,15 @@ class CertificateNotYetValidOrExpired extends Error {
this.name = 'CertificateNotYetValidOrExpired';
}
}
+
+/**
+ * Take a certificate in PEM format and convert it to bytes
+ */
+function pemToBytes(pem: string): Uint8Array {
+ const certBase64 = pem
+ .replace('-----BEGIN CERTIFICATE-----', '')
+ .replace('-----END CERTIFICATE-----', '')
+ .replace(/\n/g, '');
+
+ return isoBase64URL.toBuffer(certBase64, 'base64');
+}
diff --git a/packages/server/src/helpers/verifySignature.ts b/packages/server/src/helpers/verifySignature.ts
index ff4e73b..00ada70 100644
--- a/packages/server/src/helpers/verifySignature.ts
+++ b/packages/server/src/helpers/verifySignature.ts
@@ -10,16 +10,16 @@ export async function verifySignature(opts: {
signature: Uint8Array;
data: Uint8Array;
credentialPublicKey?: Uint8Array;
- leafCertificate?: Uint8Array;
- attestationHashAlgorithm?: COSEALG;
+ x509Certificate?: Uint8Array;
+ hashAlgorithm?: COSEALG;
}): Promise<boolean> {
- const { signature, data, credentialPublicKey, leafCertificate, attestationHashAlgorithm } = opts;
+ const { signature, data, credentialPublicKey, x509Certificate, hashAlgorithm } = opts;
- if (!leafCertificate && !credentialPublicKey) {
+ if (!x509Certificate && !credentialPublicKey) {
throw new Error('Must declare either "leafCert" or "credentialPublicKey"');
}
- if (leafCertificate && credentialPublicKey) {
+ if (x509Certificate && credentialPublicKey) {
throw new Error('Must not declare both "leafCert" and "credentialPublicKey"');
}
@@ -27,14 +27,14 @@ export async function verifySignature(opts: {
if (credentialPublicKey) {
cosePublicKey = decodeCredentialPublicKey(credentialPublicKey);
- } else if (leafCertificate) {
- cosePublicKey = convertX509PublicKeyToCOSE(leafCertificate);
+ } else if (x509Certificate) {
+ cosePublicKey = convertX509PublicKeyToCOSE(x509Certificate);
}
return isoCrypto.verify({
cosePublicKey,
signature,
data,
- shaHashOverride: attestationHashAlgorithm,
+ shaHashOverride: hashAlgorithm,
});
}
diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts
index f0a1bdb..f2d2afd 100644
--- a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts
+++ b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts
@@ -175,51 +175,53 @@ test('should verify idmelon attestation with updated root certificate', async ()
* new root certificate, which is included below and will eventually get rolled out via FIDO MDS.
*/
const metadataStatement: MetadataStatement = {
- "legalHeader": "Submission of this statement and retrieval and use of this statement indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/.",
- "aaguid": "820d89ed-d65a-409e-85cb-f73f0578f82a",
- "description": "Vancosys iOS Authenticator",
- "authenticatorVersion": 2,
- "protocolFamily": "fido2",
- "schema": 3,
- "upv": [{ "major": 1, "minor": 0 }],
- "authenticationAlgorithms": ["secp256r1_ecdsa_sha256_raw"],
- "publicKeyAlgAndEncodings": ["cose"],
- "attestationTypes": ["basic_full"],
- "userVerificationDetails": [
+ legalHeader:
+ 'Submission of this statement and retrieval and use of this statement indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/.',
+ aaguid: '820d89ed-d65a-409e-85cb-f73f0578f82a',
+ description: 'Vancosys iOS Authenticator',
+ authenticatorVersion: 2,
+ protocolFamily: 'fido2',
+ schema: 3,
+ upv: [{ major: 1, minor: 0 }],
+ authenticationAlgorithms: ['secp256r1_ecdsa_sha256_raw'],
+ publicKeyAlgAndEncodings: ['cose'],
+ attestationTypes: ['basic_full'],
+ userVerificationDetails: [
[
- { "userVerificationMethod": "faceprint_internal" },
- { "userVerificationMethod": "voiceprint_internal" },
- { "userVerificationMethod": "passcode_internal" },
- { "userVerificationMethod": "eyeprint_internal" },
- { "userVerificationMethod": "handprint_internal" },
- { "userVerificationMethod": "fingerprint_internal" },
- { "userVerificationMethod": "pattern_internal" },
- { "userVerificationMethod": "location_internal" },
- { "userVerificationMethod": "presence_internal" },
- ]
+ { userVerificationMethod: 'faceprint_internal' },
+ { userVerificationMethod: 'voiceprint_internal' },
+ { userVerificationMethod: 'passcode_internal' },
+ { userVerificationMethod: 'eyeprint_internal' },
+ { userVerificationMethod: 'handprint_internal' },
+ { userVerificationMethod: 'fingerprint_internal' },
+ { userVerificationMethod: 'pattern_internal' },
+ { userVerificationMethod: 'location_internal' },
+ { userVerificationMethod: 'presence_internal' },
+ ],
],
- "keyProtection": ["hardware", "secure_element"],
- "matcherProtection": ["on_chip"],
- "cryptoStrength": 128,
- "attachmentHint": ["external"],
- "tcDisplay": [],
- "attestationRootCertificates": [
- "MIIByzCCAXGgAwIBAgIJANmMNK6jVpuuMAoGCCqGSM49BAMCMEExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEZMBcGA1UEAwwQVmFuY29zeXMgUm9vdCBDQTAgFw0yMjEyMTQxODQxMDlaGA8yMDcyMTIwMTE4NDEwOVowQTEkMCIGA1UECgwbVmFuY29zeXMgRGF0YSBTZWN1cml0eSBJbmMuMRkwFwYDVQQDDBBWYW5jb3N5cyBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEalYgEopnKScAm+d9f1XpGB3zbkZCD3hZEKuxTclpBYlj4ypNRg0gMSa7geBgd6nck50YaVhdy75uIc2wbWX8t6NQME4wHQYDVR0OBBYEFOxyf0cDs8Yl+VnWSZ1uYJAKkFeVMB8GA1UdIwQYMBaAFOxyf0cDs8Yl+VnWSZ1uYJAKkFeVMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAO2XuiRDXxy/UkWhsuZQYNUXeOj08AeTWADAqXvcA30hAiBi2cdGd61PNwHDTYjXPenPcD8S0rFTDncNWfs3E/WDXA=="
+ keyProtection: ['hardware', 'secure_element'],
+ matcherProtection: ['on_chip'],
+ cryptoStrength: 128,
+ attachmentHint: ['external'],
+ tcDisplay: [],
+ attestationRootCertificates: [
+ 'MIIByzCCAXGgAwIBAgIJANmMNK6jVpuuMAoGCCqGSM49BAMCMEExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEZMBcGA1UEAwwQVmFuY29zeXMgUm9vdCBDQTAgFw0yMjEyMTQxODQxMDlaGA8yMDcyMTIwMTE4NDEwOVowQTEkMCIGA1UECgwbVmFuY29zeXMgRGF0YSBTZWN1cml0eSBJbmMuMRkwFwYDVQQDDBBWYW5jb3N5cyBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEalYgEopnKScAm+d9f1XpGB3zbkZCD3hZEKuxTclpBYlj4ypNRg0gMSa7geBgd6nck50YaVhdy75uIc2wbWX8t6NQME4wHQYDVR0OBBYEFOxyf0cDs8Yl+VnWSZ1uYJAKkFeVMB8GA1UdIwQYMBaAFOxyf0cDs8Yl+VnWSZ1uYJAKkFeVMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAO2XuiRDXxy/UkWhsuZQYNUXeOj08AeTWADAqXvcA30hAiBi2cdGd61PNwHDTYjXPenPcD8S0rFTDncNWfs3E/WDXA==',
],
- "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=",
- "authenticatorGetInfo": {
- "versions": ["FIDO_2_0"],
- "extensions": ["hmac-secret"],
- "aaguid": "820d89edd65a409e85cbf73f0578f82a",
- "options": { "plat": false, "rk": true, "up": true, "uv": true },
- "maxMsgSize": 2048
- }
+ icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=',
+ authenticatorGetInfo: {
+ versions: ['FIDO_2_0'],
+ extensions: ['hmac-secret'],
+ aaguid: '820d89edd65a409e85cbf73f0578f82a',
+ options: { plat: false, rk: true, up: true, uv: true },
+ maxMsgSize: 2048,
+ },
};
const x5c = [
- 'MIIB6TCCAY+gAwIBAgIJAJz56pzvu76hMAoGCCqGSM49BAMCMEExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEZMBcGA1UEAwwQVmFuY29zeXMgUm9vdCBDQTAgFw0xODEyMjIxNzQzMjhaGA8yMDY4MTIwOTE3NDMyOFowfDELMAkGA1UEBhMCQ0ExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEjMCEGA1UEAwwaVmFuY29zeXMgaU9TIEF1dGhlbnRpY2F0b3IwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATZVzbtmSJbzsoN6mfdD+t1LV52zB+LWN0UusmV9+sRmfNB49adqPQ+h0JOlEwfL4zkbMmuDr6JhBRKJ5/c0SkeozMwMTAMBgNVHRMBAf8EAjAAMCEGCysGAQQBguUcAQEEBBIEEIINie3WWkCehcv3PwV4+CowCgYIKoZIzj0EAwIDSAAwRQIgV7++U2fQyy6Qido7fDhsi5Grrt76LTgZ5XJlA9UKEVECIQDJO0YHevdU77VlZ+Of58oKMjWD3SkzC1SWSlhl3nezHQ=='
+ 'MIIB6TCCAY+gAwIBAgIJAJz56pzvu76hMAoGCCqGSM49BAMCMEExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEZMBcGA1UEAwwQVmFuY29zeXMgUm9vdCBDQTAgFw0xODEyMjIxNzQzMjhaGA8yMDY4MTIwOTE3NDMyOFowfDELMAkGA1UEBhMCQ0ExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEjMCEGA1UEAwwaVmFuY29zeXMgaU9TIEF1dGhlbnRpY2F0b3IwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATZVzbtmSJbzsoN6mfdD+t1LV52zB+LWN0UusmV9+sRmfNB49adqPQ+h0JOlEwfL4zkbMmuDr6JhBRKJ5/c0SkeozMwMTAMBgNVHRMBAf8EAjAAMCEGCysGAQQBguUcAQEEBBIEEIINie3WWkCehcv3PwV4+CowCgYIKoZIzj0EAwIDSAAwRQIgV7++U2fQyy6Qido7fDhsi5Grrt76LTgZ5XJlA9UKEVECIQDJO0YHevdU77VlZ+Of58oKMjWD3SkzC1SWSlhl3nezHQ==',
];
- const credentialPublicKey = 'pQECAyYgASFYINuJbeLdkZwgKtUw2VSopICTTO5PKdj95GXJ7JCsQi7iIlggxygEp0_P0oMXhfw2BjtL0M7-yIpnk5uSHc0oNkXfdJw';
+ const credentialPublicKey =
+ 'pQECAyYgASFYINuJbeLdkZwgKtUw2VSopICTTO5PKdj95GXJ7JCsQi7iIlggxygEp0_P0oMXhfw2BjtL0M7-yIpnk5uSHc0oNkXfdJw';
const verified = await verifyAttestationWithMetadata({
statement: metadataStatement,
@@ -228,4 +230,4 @@ test('should verify idmelon attestation with updated root certificate', async ()
});
expect(verified).toEqual(true);
-})
+});
diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.ts
index 9b4c471..44f693c 100644
--- a/packages/server/src/metadata/verifyAttestationWithMetadata.ts
+++ b/packages/server/src/metadata/verifyAttestationWithMetadata.ts
@@ -4,14 +4,7 @@ import type { MetadataStatement, AlgSign } from '../metadata/mdsTypes';
import { convertCertBufferToPEM } from '../helpers/convertCertBufferToPEM';
import { validateCertificatePath } from '../helpers/validateCertificatePath';
import { decodeCredentialPublicKey } from '../helpers/decodeCredentialPublicKey';
-import {
- COSEALG,
- COSECRV,
- COSEKEYS,
- COSEKTY,
- COSEPublicKeyEC2,
- isCOSEPublicKeyEC2,
-} from '../helpers/cose';
+import { COSEALG, COSECRV, COSEKEYS, COSEKTY, isCOSEPublicKeyEC2 } from '../helpers/cose';
/**
* Match properties of the authenticator's attestation statement against expected values as
diff --git a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
index c665be3..95c7952 100644
--- a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
+++ b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
@@ -14,7 +14,6 @@ import { decodeCredentialPublicKey } from '../../../helpers/decodeCredentialPubl
import {
COSEKEYS,
isCOSEAlg,
- COSEKTY,
isCOSEPublicKeyRSA,
isCOSEPublicKeyEC2,
COSEALG,
@@ -215,7 +214,7 @@ export async function verifyAttestationTPM(
}
// Check that Subject sequence is empty.
- if (Object.keys(subject).length > 0) {
+ if (subject.combined.length > 0) {
throw new Error('Certificate subject was not empty (TPM)');
}
@@ -316,8 +315,8 @@ export async function verifyAttestationTPM(
return verifySignature({
signature: sig,
data: certInfo,
- leafCertificate: x5c[0],
- attestationHashAlgorithm: alg,
+ x509Certificate: x5c[0],
+ hashAlgorithm: alg,
});
}
diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
index 1f3eb83..0128c09 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
@@ -111,7 +111,7 @@ export async function verifyAttestationAndroidKey(
return verifySignature({
signature: sig,
data: signatureBase,
- leafCertificate: x5c[0],
- attestationHashAlgorithm: alg,
+ x509Certificate: x5c[0],
+ hashAlgorithm: alg,
});
}
diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
index d47dd70..40fcca2 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
@@ -129,7 +129,7 @@ export async function verifyAttestationAndroidSafetyNet(
const verified = await verifySignature({
signature: signatureBuffer,
data: signatureBaseBuffer,
- leafCertificate: leafCertBuffer,
+ x509Certificate: leafCertBuffer,
});
/**
* END Verify Signature
diff --git a/packages/server/src/registration/verifications/verifyAttestationApple.test.ts b/packages/server/src/registration/verifications/verifyAttestationApple.test.ts
index 1459df1..ef47e8e 100644
--- a/packages/server/src/registration/verifications/verifyAttestationApple.test.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationApple.test.ts
@@ -1,7 +1,3 @@
-// TODO: This test can take upwards of 7 seconds to complete locally, more in CI...need to figure
-// out why
-jest.setTimeout(30000);
-
import { verifyRegistrationResponse } from '../verifyRegistrationResponse';
test('should verify Apple attestation', async () => {
diff --git a/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts b/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts
index e271e48..2674502 100644
--- a/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts
@@ -62,7 +62,7 @@ export async function verifyAttestationFIDOU2F(
return verifySignature({
signature: sig,
data: signatureBase,
- leafCertificate: x5c[0],
- attestationHashAlgorithm: COSEALG.ES256,
+ x509Certificate: x5c[0],
+ hashAlgorithm: COSEALG.ES256,
});
}
diff --git a/packages/server/src/registration/verifications/verifyAttestationPacked.ts b/packages/server/src/registration/verifications/verifyAttestationPacked.ts
index a57bf13..2780764 100644
--- a/packages/server/src/registration/verifications/verifyAttestationPacked.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationPacked.ts
@@ -115,14 +115,14 @@ export async function verifyAttestationPacked(
verified = await verifySignature({
signature: sig,
data: signatureBase,
- leafCertificate: x5c[0],
+ x509Certificate: x5c[0],
});
} else {
verified = await verifySignature({
signature: sig,
data: signatureBase,
credentialPublicKey,
- attestationHashAlgorithm: alg,
+ hashAlgorithm: alg,
});
}
diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts
index aa0011e..0a97af5 100644
--- a/packages/server/src/registration/verifyRegistrationResponse.test.ts
+++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts
@@ -90,7 +90,7 @@ test('should verify Packed (EC2) attestation', async () => {
expect(verification.registrationInfo?.credentialID).toEqual(
isoBase64URL.toBuffer(
'AYThY1csINY4JrbHyGmqTl1nL_F1zjAF3hSAIngz8kAcjugmAMNVvxZRwqpEH-bNHHAIv291OX5ko9eDf_5mu3U' +
- 'B2BvsScr2K-ppM4owOpGsqwg5tZglqqmxIm1Q',
+ 'B2BvsScr2K-ppM4owOpGsqwg5tZglqqmxIm1Q',
),
);
});