mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
123 lines
5.9 KiB
123 lines
5.9 KiB
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.verifyAttestationAndroidKey = verifyAttestationAndroidKey;
|
|
const asn1_schema_1 = require("@peculiar/asn1-schema");
|
|
const asn1_x509_1 = require("@peculiar/asn1-x509");
|
|
const asn1_android_1 = require("@peculiar/asn1-android");
|
|
const convertCertBufferToPEM_js_1 = require("../../helpers/convertCertBufferToPEM.js");
|
|
const validateCertificatePath_js_1 = require("../../helpers/validateCertificatePath.js");
|
|
const verifySignature_js_1 = require("../../helpers/verifySignature.js");
|
|
const convertCOSEtoPKCS_js_1 = require("../../helpers/convertCOSEtoPKCS.js");
|
|
const cose_js_1 = require("../../helpers/cose.js");
|
|
const index_js_1 = require("../../helpers/iso/index.js");
|
|
const metadataService_js_1 = require("../../services/metadataService.js");
|
|
const verifyAttestationWithMetadata_js_1 = require("../../metadata/verifyAttestationWithMetadata.js");
|
|
/**
|
|
* Verify an attestation response with fmt 'android-key'
|
|
*/
|
|
async function verifyAttestationAndroidKey(options) {
|
|
const { authData, clientDataHash, attStmt, credentialPublicKey, aaguid, rootCertificates, } = options;
|
|
const x5c = attStmt.get('x5c');
|
|
const sig = attStmt.get('sig');
|
|
const alg = attStmt.get('alg');
|
|
if (!x5c) {
|
|
throw new Error('No attestation certificate provided in attestation statement (Android Key)');
|
|
}
|
|
if (!sig) {
|
|
throw new Error('No attestation signature provided in attestation statement (Android Key)');
|
|
}
|
|
if (!alg) {
|
|
throw new Error(`Attestation statement did not contain alg (Android Key)`);
|
|
}
|
|
if (!(0, cose_js_1.isCOSEAlg)(alg)) {
|
|
throw new Error(`Attestation statement contained invalid alg ${alg} (Android Key)`);
|
|
}
|
|
/**
|
|
* Verify that the public key in the first certificate in x5c matches the credentialPublicKey in
|
|
* the attestedCredentialData in authenticatorData.
|
|
*/
|
|
// Find the public cert in the certificate as PKCS
|
|
const parsedCert = asn1_schema_1.AsnParser.parse(x5c[0], asn1_x509_1.Certificate);
|
|
const parsedCertPubKey = new Uint8Array(parsedCert.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey);
|
|
// Convert the credentialPublicKey to PKCS
|
|
const credPubKeyPKCS = (0, convertCOSEtoPKCS_js_1.convertCOSEtoPKCS)(credentialPublicKey);
|
|
if (!index_js_1.isoUint8Array.areEqual(credPubKeyPKCS, parsedCertPubKey)) {
|
|
throw new Error('Credential public key does not equal leaf cert public key (Android Key)');
|
|
}
|
|
/**
|
|
* Verify that the attestationChallenge field in the attestation certificate extension data is
|
|
* identical to clientDataHash.
|
|
*/
|
|
// Find Android KeyStore Extension in certificate extensions
|
|
const extKeyStore = parsedCert.tbsCertificate.extensions?.find((ext) => ext.extnID === asn1_android_1.id_ce_keyDescription);
|
|
if (!extKeyStore) {
|
|
throw new Error('Certificate did not contain extKeyStore (Android Key)');
|
|
}
|
|
const parsedExtKeyStore = asn1_schema_1.AsnParser.parse(extKeyStore.extnValue, asn1_android_1.KeyDescription);
|
|
// Verify extKeyStore values
|
|
const { attestationChallenge, teeEnforced, softwareEnforced } = parsedExtKeyStore;
|
|
if (!index_js_1.isoUint8Array.areEqual(new Uint8Array(attestationChallenge.buffer), clientDataHash)) {
|
|
throw new Error('Attestation challenge was not equal to client data hash (Android Key)');
|
|
}
|
|
/**
|
|
* The AuthorizationList.allApplications field is not present on either authorization list
|
|
* (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
|
|
*
|
|
* (i.e. These shouldn't contain the [600] tag)
|
|
*/
|
|
if (teeEnforced.allApplications !== undefined) {
|
|
throw new Error('teeEnforced contained "allApplications [600]" tag (Android Key)');
|
|
}
|
|
if (softwareEnforced.allApplications !== undefined) {
|
|
throw new Error('teeEnforced contained "allApplications [600]" tag (Android Key)');
|
|
}
|
|
const statement = await metadataService_js_1.MetadataService.getStatement(aaguid);
|
|
if (statement) {
|
|
try {
|
|
await (0, verifyAttestationWithMetadata_js_1.verifyAttestationWithMetadata)({
|
|
statement,
|
|
credentialPublicKey,
|
|
x5c,
|
|
attestationStatementAlg: alg,
|
|
});
|
|
}
|
|
catch (err) {
|
|
const _err = err;
|
|
throw new Error(`${_err.message} (Android Key)`, { cause: _err });
|
|
}
|
|
}
|
|
else {
|
|
/**
|
|
* Verify that x5c contains a full certificate path.
|
|
*/
|
|
const x5cNoRootPEM = x5c.slice(0, -1).map(convertCertBufferToPEM_js_1.convertCertBufferToPEM);
|
|
const x5cRootPEM = x5c.slice(-1).map(convertCertBufferToPEM_js_1.convertCertBufferToPEM);
|
|
try {
|
|
await (0, validateCertificatePath_js_1.validateCertificatePath)(x5cNoRootPEM, x5cRootPEM);
|
|
}
|
|
catch (err) {
|
|
const _err = err;
|
|
throw new Error(`${_err.message} (Android Key)`, { cause: _err });
|
|
}
|
|
/**
|
|
* Make sure the root certificate is one of the Google Hardware Attestation Root certificates
|
|
*
|
|
* https://developer.android.com/privacy-and-security/security-key-attestation#root_certificate
|
|
*/
|
|
if (rootCertificates.length > 0 && rootCertificates.indexOf(x5cRootPEM[0]) < 0) {
|
|
throw new Error('x5c root certificate was not a known root certificate (Android Key)');
|
|
}
|
|
}
|
|
/**
|
|
* Verify that sig is a valid signature over the concatenation of authenticatorData and
|
|
* clientDataHash using the public key in the first certificate in x5c with the algorithm
|
|
* specified in alg.
|
|
*/
|
|
const signatureBase = index_js_1.isoUint8Array.concat([authData, clientDataHash]);
|
|
return (0, verifySignature_js_1.verifySignature)({
|
|
signature: sig,
|
|
data: signatureBase,
|
|
x509Certificate: x5c[0],
|
|
hashAlgorithm: alg,
|
|
});
|
|
}
|
|
|