summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/attestation/verifications/tpm/verifyTPM.ts6
-rw-r--r--packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts7
-rw-r--r--packages/server/src/attestation/verifications/verifyPacked.test.ts50
-rw-r--r--packages/server/src/attestation/verifications/verifyPacked.ts4
-rw-r--r--packages/server/src/helpers/getCertificateInfo.ts96
5 files changed, 110 insertions, 53 deletions
diff --git a/packages/server/src/attestation/verifications/tpm/verifyTPM.ts b/packages/server/src/attestation/verifications/tpm/verifyTPM.ts
index fd9ff9d..fc549ff 100644
--- a/packages/server/src/attestation/verifications/tpm/verifyTPM.ts
+++ b/packages/server/src/attestation/verifications/tpm/verifyTPM.ts
@@ -177,8 +177,7 @@ export default async function verifyTPM(options: Options): Promise<boolean> {
}
// Pick a leaf AIK certificate of the x5c array and parse it.
- const leafCertPEM = convertX509CertToPEM(x5c[0]);
- const leafCertInfo = getCertificateInfo(leafCertPEM);
+ const leafCertInfo = getCertificateInfo(x5c[0]);
const { basicConstraintsCA, version, subject, notAfter, notBefore } = leafCertInfo;
if (basicConstraintsCA) {
@@ -186,7 +185,7 @@ export default async function verifyTPM(options: Options): Promise<boolean> {
}
// Check that certificate is of version 3 (value must be set to 2).
- if (version !== 3) {
+ if (version !== 2) {
throw new Error('Certificate version was not `3` (ASN.1 value of 2) (TPM)');
}
@@ -275,6 +274,7 @@ export default async function verifyTPM(options: Options): Promise<boolean> {
// Verify signature over certInfo with the public key extracted from AIK certificate.
// In the wise words of Yuriy Ackermann: "Get Martini friend, you are done!"
+ const leafCertPEM = convertX509CertToPEM(x5c[0]);
return verifySignature(sig, certInfo, leafCertPEM, hashAlg);
}
diff --git a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts
index 4ce7f36..6c0a5c8 100644
--- a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts
+++ b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts
@@ -81,8 +81,8 @@ export default async function verifyAttestationAndroidSafetyNet(
/**
* START Verify Header
*/
- const leafCert = convertX509CertToPEM(HEADER.x5c[0]);
- const leafCertInfo = getCertificateInfo(leafCert);
+ const leafCertBuffer = base64url.toBuffer(HEADER.x5c[0]);
+ const leafCertInfo = getCertificateInfo(leafCertBuffer);
const { subject } = leafCertInfo;
@@ -121,7 +121,8 @@ export default async function verifyAttestationAndroidSafetyNet(
const signatureBaseBuffer = Buffer.from(`${jwtParts[0]}.${jwtParts[1]}`);
const signatureBuffer = base64url.toBuffer(SIGNATURE);
- const verified = verifySignature(signatureBuffer, signatureBaseBuffer, leafCert);
+ const leafCertPEM = convertX509CertToPEM(leafCertBuffer);
+ const verified = verifySignature(signatureBuffer, signatureBaseBuffer, leafCertPEM);
/**
* END Verify Signature
*/
diff --git a/packages/server/src/attestation/verifications/verifyPacked.test.ts b/packages/server/src/attestation/verifications/verifyPacked.test.ts
new file mode 100644
index 0000000..3de7633
--- /dev/null
+++ b/packages/server/src/attestation/verifications/verifyPacked.test.ts
@@ -0,0 +1,50 @@
+import verifyAttestationResponse from '../verifyAttestationResponse';
+
+test('should verify Packed response from Chrome virtual authenticator', async () => {
+ /**
+ * This unit test will ensure future compatibility with Chrome virtual authenticators.
+ *
+ * Context:
+ *
+ * Chrome's WebAuthn dev tool enables developers to use "virtual" software authenticators in place
+ * of typical authenticator hardware. The reason this test exists is to ensure SimpleWebAuthn can
+ * handle leaf certs, such as the ones in these virtual authenticators, that specify the byte
+ * sequence "\x30\x03\x01\x01\x00" for the cert's Basic Constraints extension.
+ *
+ * As of March 2021 the jsrsasign@^10.0.5 library has a hardcoded check for "30030101ff", but
+ * not "3003010100" (notice the difference between "ff" and "00"), indicating whether or not this
+ * is a certificate authority certificate:
+ *
+ * https://github.com/kjur/jsrsasign/blob/482e651f2bb380dad3da4bbf0ae220fe3021d407/src/x509-1.1.js#L660
+ *
+ * Physical hardware authenticators have been observed to specify "3000" for this constraint;
+ * this value evaluates to `!!undefined` => `false`, satisfying the Packed attestation
+ * verification's requirement that, "the Basic Constraints extension MUST have the CA component
+ * set to false.""
+ *
+ * https://w3c.github.io/webauthn/#sctn-packed-attestation-cert-requirements
+ *
+ * SimpleWebAuthn will have to implement its own workaround until this issue is resolved in
+ * jsrsasign.
+ */
+ const verification = await verifyAttestationResponse({
+ credential: {
+ id: '5Hwc78jGjXrzOS8Mke9KhFZEtX54iYD-UEBKgvMXM64',
+ rawId: '5Hwc78jGjXrzOS8Mke9KhFZEtX54iYD-UEBKgvMXM64',
+ response: {
+ attestationObject:
+ 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhANUrPJzUYX7JGbo4yN_qsQ_2c7xw6br2U1y_OxNcFd1cAiAo6f7LtQ67viVKxs7TLo9nj6nxgxqwEaOpzQhGtdXbqGN4NWOBWQHgMIIB3DCCAYCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMB4XDTE3MDcxNDAyNDAwMFoXDTQxMDMyNjAzNDIzNFowYDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCENocm9taXVtMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMRowGAYDVQQDDBFCYXRjaCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI1hfmXJUI5kvMVnOsgqZ5naPBRGaCwljEY__99Y39L6Pmw3i1PXlcSk3_tBme3Xhi8jq68CA7S4kRugVpmU4QGjKDAmMBMGCysGAQQBguUcAgEBBAQDAgUgMA8GA1UdEwEB_wQFMAMBAQAwDQYJKoZIhvcNAQELBQADRwAwRAIgK8W82BY7-iHUcd5mSfWX4R-uGdOk49XKTkV3L6ilUPQCIEs68ZEr_yAjG39UwNexAVLBfbxkDdkLZlMtBvUsV27PaGF1dGhEYXRhWKQ93EcQ6cCIsinbqJ1WMiC7Ofcimv9GWwplaxr7mor4oEUAAAABAQIDBAUGBwgBAgMEBQYHCAAg5Hwc78jGjXrzOS8Mke9KhFZEtX54iYD-UEBKgvMXM66lAQIDJiABIVgghBdEOBTvUm-jPaYY0wvvO_HzCupmyS7YQzagxtn1T5IiWCDwJ5XQ_SzKoiV64TXfdsTrnxFoNljUCzJOJhwrDyhkRA',
+ clientDataJSON:
+ 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiOUdJczBRUUJuYTE2eWN3NHN0U25BcWgyQWI2QWlIN1NTMF9YbTR5SjF6ayIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmRvbnRuZWVkYS5wdyIsImNyb3NzT3JpZ2luIjpmYWxzZX0',
+ },
+ type: 'public-key',
+ clientExtensionResults: {},
+ transports: ['usb'],
+ },
+ expectedChallenge: '9GIs0QQBna16ycw4stSnAqh2Ab6AiH7SS0_Xm4yJ1zk',
+ expectedOrigin: 'https://dev.dontneeda.pw',
+ expectedRPID: 'dev.dontneeda.pw',
+ });
+
+ expect(verification.verified).toEqual(true);
+});
diff --git a/packages/server/src/attestation/verifications/verifyPacked.ts b/packages/server/src/attestation/verifications/verifyPacked.ts
index f16aa50..3068bbb 100644
--- a/packages/server/src/attestation/verifications/verifyPacked.ts
+++ b/packages/server/src/attestation/verifications/verifyPacked.ts
@@ -50,7 +50,7 @@ export default async function verifyAttestationPacked(options: Options): Promise
if (x5c) {
const leafCert = convertX509CertToPEM(x5c[0]);
const { subject, basicConstraintsCA, version, notBefore, notAfter } = getCertificateInfo(
- leafCert,
+ x5c[0],
);
const { OU, CN, O, C } = subject;
@@ -75,7 +75,7 @@ export default async function verifyAttestationPacked(options: Options): Promise
throw new Error('Certificate basic constraints CA was not `false` (Packed|Full)');
}
- if (version !== 3) {
+ if (version !== 2) {
throw new Error('Certificate version was not `3` (ASN.1 value of 2) (Packed|Full)');
}
diff --git a/packages/server/src/helpers/getCertificateInfo.ts b/packages/server/src/helpers/getCertificateInfo.ts
index d9efd3e..021ad77 100644
--- a/packages/server/src/helpers/getCertificateInfo.ts
+++ b/packages/server/src/helpers/getCertificateInfo.ts
@@ -1,74 +1,80 @@
-import { X509, zulutodate } from 'jsrsasign';
+import { AsnParser } from '@peculiar/asn1-schema';
+import { Certificate, BasicConstraints, id_ce_basicConstraints } from '@peculiar/asn1-x509';
export type CertificateInfo = {
- issuer: { [key: string]: string };
- subject: { [key: string]: string };
+ issuer: Issuer;
+ subject: Subject;
version: number;
basicConstraintsCA: boolean;
notBefore: Date;
notAfter: Date;
};
-type ExtInfo = {
- critical: boolean;
- oid: string;
- vidx: number;
+type Issuer = {
+ C?: string;
+ O?: string;
+ OU?: string;
+ CN?: string;
};
-interface x5cCertificate extends jsrsasign.X509 {
- version: number;
- foffset: number;
- aExtInfo: ExtInfo[];
-}
+type Subject = {
+ C?: string;
+ O?: string;
+ OU?: string;
+ CN?: string;
+};
+
+const issuerSubjectIDKey: { [key: string]: 'C' | 'O' | 'OU' | 'CN' } = {
+ '2.5.4.6': 'C',
+ '2.5.4.10': 'O',
+ '2.5.4.11': 'OU',
+ '2.5.4.3': 'CN',
+};
/**
* Extract PEM certificate info
*
* @param pemCertificate Result from call to `convertASN1toPEM(x5c[0])`
*/
-export default function getCertificateInfo(pemCertificate: string): CertificateInfo {
- const subjectCert = new X509();
- subjectCert.readCertPEM(pemCertificate);
-
- // Break apart the Issuer
- const issuerString = subjectCert.getIssuerString();
- const issuerParts = issuerString.slice(1).split('/');
+export default function getCertificateInfo(leafCertBuffer: Buffer): CertificateInfo {
+ const asnx509 = AsnParser.parse(leafCertBuffer, Certificate);
+ const parsedCert = asnx509.tbsCertificate;
- const issuer: { [key: string]: string } = {};
- issuerParts.forEach(field => {
- const [key, val] = field.split('=');
- issuer[key] = val;
- });
-
- // Break apart the Subject
- let subjectRaw = '/';
- try {
- subjectRaw = subjectCert.getSubjectString();
- } catch (err) {
- // Don't throw on an error that indicates an empty subject
- if (err !== 'malformed RDN') {
- throw err;
+ // Issuer
+ const issuer: Issuer = {};
+ parsedCert.issuer.forEach(([iss]) => {
+ const key = issuerSubjectIDKey[iss.type];
+ if (key) {
+ issuer[key] = iss.value.toString();
}
- }
- const subjectParts = subjectRaw.slice(1).split('/');
+ });
- const subject: { [key: string]: string } = {};
- subjectParts.forEach(field => {
- if (field) {
- const [key, val] = field.split('=');
- subject[key] = val;
+ // Subject
+ const subject: Subject = {};
+ parsedCert.subject.forEach(([iss]) => {
+ const key = issuerSubjectIDKey[iss.type];
+ if (key) {
+ subject[key] = iss.value.toString();
}
});
- const { version } = subjectCert as x5cCertificate;
- const basicConstraintsCA = !!subjectCert.getExtBasicConstraints()?.cA;
+ let basicConstraintsCA = false;
+ if (parsedCert.extensions) {
+ // console.log(parsedCert.extensions);
+ for (const ext of parsedCert.extensions) {
+ if (ext.extnID === id_ce_basicConstraints) {
+ const basicConstraints = AsnParser.parse(ext.extnValue, BasicConstraints);
+ basicConstraintsCA = basicConstraints.cA;
+ }
+ }
+ }
return {
issuer,
subject,
- version,
+ version: parsedCert.version,
basicConstraintsCA,
- notBefore: zulutodate(subjectCert.getNotBefore()),
- notAfter: zulutodate(subjectCert.getNotAfter()),
+ notBefore: parsedCert.validity.notBefore.getTime(),
+ notAfter: parsedCert.validity.notAfter.getTime(),
};
}