// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. "use strict"; do_get_profile(); // must be called before getting nsIX509CertDB const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); const { QWACs } = ChromeUtils.importESModule( "resource://gre/modules/psm/QWACs.sys.mjs" ); async function verify_1_qwacs(filename, expectSuccess, extraCertNames = []) { let cert = constructCertFromFile(filename); let result = await certdb.asyncVerifyQWAC( Ci.nsIX509CertDB.OneQWAC, cert, "example.com", extraCertNames.map(filename => constructCertFromFile(filename)) ); equal( result, expectSuccess, `${filename} ${expectSuccess ? "should" : "should not"} verify as 1-QWAC` ); } add_task(async function test_verify_1_qwacs() { Services.prefs.clearUserPref("security.qwacs.enable_test_trust_anchors"); // By default, the QWACs test trust anchors are not used. await verify_1_qwacs("test_qwacs/1-qwac.pem", false); await verify_1_qwacs("test_qwacs/1-qwac-qevcpw.pem", false); Services.prefs.setBoolPref("security.qwacs.enable_test_trust_anchors", true); await verify_1_qwacs("test_qwacs/1-qwac.pem", true); await verify_1_qwacs("test_qwacs/1-qwac-qevcpw.pem", true); await verify_1_qwacs("test_qwacs/1-qwac-other-optional-qcs.pem", true); // One or more intermediates may be necessary for path building. await verify_1_qwacs("test_qwacs/1-qwac-via-intermediate.pem", false); await verify_1_qwacs("test_qwacs/1-qwac-via-intermediate.pem", true, [ "test_qwacs/test-int.pem", ]); await verify_1_qwacs("test_qwacs/empty-qc-type-statement.pem", false); await verify_1_qwacs("test_qwacs/missing-qc-type-statement.pem", false); await verify_1_qwacs("test_qwacs/missing-qcs-compliance.pem", false); await verify_1_qwacs("test_qwacs/wrong-qc-type.pem", false); await verify_1_qwacs("test_qwacs/no-1-qwac-policies.pem", false); await verify_1_qwacs("test_qwacs/no-policies.pem", false); await verify_1_qwacs("test_qwacs/2-qwac.pem", false); }); async function verify_2_qwacs( filename, expectSuccess, hostname = "example.com" ) { let cert = constructCertFromFile(filename); let result = await certdb.asyncVerifyQWAC( Ci.nsIX509CertDB.TwoQWAC, cert, hostname, [] ); equal( result, expectSuccess, `${filename} ${expectSuccess ? "should" : "should not"} verify as 2-QWAC` ); } add_task(async function test_verify_2_qwacs() { Services.prefs.clearUserPref("security.qwacs.enable_test_trust_anchors"); // By default, the QWACs test trust anchors are not used. await verify_2_qwacs("test_qwacs/2-qwac.pem", false); Services.prefs.setBoolPref("security.qwacs.enable_test_trust_anchors", true); await verify_2_qwacs("test_qwacs/2-qwac.pem", true); await verify_2_qwacs("test_qwacs/1-qwac.pem", false); await verify_2_qwacs("test_qwacs/2-qwac-no-eku.pem", false); await verify_2_qwacs("test_qwacs/2-qwac-tls-server-eku.pem", false); await verify_2_qwacs("test_qwacs/2-qwac-multiple-key-purpose-eku.pem", false); await verify_2_qwacs("test_qwacs/2-qwac.pem", false, "example.org"); }); // Produces base64url(hash(base64url(certificate DER))) async function certificateHash(certificate, hashAlg) { let hash = await crypto.subtle.digest( hashAlg.replace("S", "SHA-"), new Uint8Array( stringToArray( QWACs.toBase64URLEncoding(arrayToString(certificate.getRawDER())) ) ) ); return QWACs.toBase64URLEncoding(arrayToString(new Uint8Array(hash))); } const kTLSCertificateBindingEE = constructCertFromFile("test_qwacs/2-qwac.pem"); const kTLSCertificateBindingHeader = { alg: "", cty: "TLS-Certificate-Binding-v1", // RFC 7515 Section 4.1.6: "Each string in the array is a base64-encoded // (Section 4 of [RFC4648] -- not base64url-encoded) DER [ITU.X690.2008] PKIX // certificate value." x5c: [], sigD: { mId: "http://uri.etsi.org/19182/ObjectIdByURIHash", pars: [], hashM: "", hashV: [], }, }; async function makeBindingHeader( certificateChain, certificatesToBind, signingAlg, hashAlg ) { let header = structuredClone(kTLSCertificateBindingHeader); header.alg = signingAlg; header.x5c = certificateChain.map(c => btoa(arrayToString(c.getRawDER()))); header.sigD.hashM = hashAlg; for (let toBind of certificatesToBind) { header.sigD.pars.push(""); header.sigD.hashV.push(await certificateHash(toBind, hashAlg)); } return header; } add_task(async function test_validate_tls_certificate_binding_header() { let serverCertificate = constructCertFromFile("bad_certs/default-ee.pem"); let testHeader = await makeBindingHeader( [kTLSCertificateBindingEE], [serverCertificate], "RS256", "S256" ); let validatedHeader = QWACs.validateTLSCertificateBindingHeader(testHeader); ok(validatedHeader, "header should validate successfully"); deepEqual(validatedHeader.algorithm, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256", }); equal(validatedHeader.certificates.length, 1); equal(validatedHeader.hashAlg, "SHA-256"); equal(validatedHeader.hashes.length, 1); let headerWithExtraKey = structuredClone(testHeader); headerWithExtraKey.extra = "foo"; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithExtraKey), "header with extra key should not validate" ); let headerWithExtraSigDKey = structuredClone(testHeader); headerWithExtraSigDKey.sigD.additional = "bar"; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithExtraSigDKey), "header with extra key in sigD should not parse" ); let headerWithWrongCty = structuredClone(testHeader); headerWithWrongCty.cty = "TLS-Certificate-Binding-v2"; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithWrongCty), "header with wrong cty should not parse" ); let headerWithWrongMId = structuredClone(testHeader); headerWithWrongMId.sigD.mId = "http://example.org"; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithWrongMId), "header with wrong sigD.mId should not parse" ); let headerWithTooManyPars = structuredClone(testHeader); headerWithTooManyPars.sigD.pars.push(""); ok( !QWACs.validateTLSCertificateBindingHeader(headerWithTooManyPars), "header with too many sigD.pars elements should not parse" ); let headerWithInvalidHashV = structuredClone(testHeader); headerWithInvalidHashV.sigD.hashV[0] = 1234; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithInvalidHashV), "header with invalid sigD.hashV should not parse" ); let headerWithTooManyHashV = structuredClone(testHeader); headerWithTooManyHashV.sigD.hashV.push(headerWithTooManyHashV.sigD.hashV[0]); ok( !QWACs.validateTLSCertificateBindingHeader(headerWithTooManyHashV), "header with too many sigD.hashV elements should not parse" ); let headerWithEmptyX5c = structuredClone(testHeader); headerWithEmptyX5c.x5c = []; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithEmptyX5c), "header with empty x5c should not parse" ); let headerWithUnsupportedAlg = structuredClone(testHeader); headerWithUnsupportedAlg.alg = "RS384"; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithUnsupportedAlg), "header with unsupported alg should not parse" ); let headerWithUnsupportedHashM = structuredClone(testHeader); headerWithUnsupportedHashM.sigD.hashM = "S224"; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithUnsupportedHashM), "header with unsupported sigD.hashM should not parse" ); let headerWithOptionalHeaders = structuredClone(testHeader); headerWithOptionalHeaders.kid = "optional kid"; headerWithOptionalHeaders.iat = "optional iat"; ok( QWACs.validateTLSCertificateBindingHeader(headerWithOptionalHeaders), "header with optional headers should parse" ); let headerWithX5tS256Match = structuredClone(testHeader); let signingCertificateHash = await crypto.subtle.digest( "SHA-256", new Uint8Array(kTLSCertificateBindingEE.getRawDER()) ); headerWithX5tS256Match["x5t#S256"] = QWACs.toBase64URLEncoding( arrayToString(new Uint8Array(signingCertificateHash)) ); ok( QWACs.validateTLSCertificateBindingHeader(headerWithX5tS256Match), "header with matching x5t#S256 should parse" ); let headerWithX5tS256Mismatch = structuredClone(testHeader); headerWithX5tS256Mismatch["x5t#S256"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithX5tS256Mismatch), "header with x5t#S256 mismatch should not parse" ); let headerWithBadExp = structuredClone(testHeader); headerWithBadExp.exp = "not a number"; ok( !QWACs.validateTLSCertificateBindingHeader(headerWithBadExp), "header with bad expiration time should not parse" ); let expiredHeader = structuredClone(testHeader); expiredHeader.exp = "946684800"; ok( !QWACs.validateTLSCertificateBindingHeader(expiredHeader), "expired header should not parse" ); let unexpiredHeader = structuredClone(testHeader); unexpiredHeader.exp = "4102444800"; ok( QWACs.validateTLSCertificateBindingHeader(unexpiredHeader), "unexpired header should parse" ); }); async function validate_tls_certificate_binding_header_with_algorithms( signatureAlg, hashAlg, expectedSignatureAlg, expectedHashAlg ) { let serverCertificate = constructCertFromFile("bad_certs/default-ee.pem"); let testHeader = await makeBindingHeader( [kTLSCertificateBindingEE], [serverCertificate], signatureAlg, hashAlg ); let validatedHeader = QWACs.validateTLSCertificateBindingHeader(testHeader); ok(validatedHeader, "header should validate successfully"); deepEqual(validatedHeader.algorithm, expectedSignatureAlg); equal(validatedHeader.certificates.length, 1); equal(validatedHeader.hashAlg, expectedHashAlg); equal(validatedHeader.hashes.length, 1); } add_task( async function test_validate_tls_certificate_binding_header_with_algorithms() { let options = [ { signatureAlg: "RS256", hashAlg: "S256", expectedSignatureAlg: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, expectedHashAlg: "SHA-256", }, { signatureAlg: "PS256", hashAlg: "S384", expectedSignatureAlg: { name: "RSA-PSS", saltLength: 32, hash: "SHA-256", }, expectedHashAlg: "SHA-384", }, { signatureAlg: "ES256", hashAlg: "S512", expectedSignatureAlg: { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256", }, expectedHashAlg: "SHA-512", }, ]; for (let option of options) { await validate_tls_certificate_binding_header_with_algorithms( option.signatureAlg, option.hashAlg, option.expectedSignatureAlg, option.expectedHashAlg ); } } ); async function sign(privateKeyInfo, algorithm, header) { let toSign = new Uint8Array(stringToArray(header + ".")); let key = await crypto.subtle.importKey( "pkcs8", privateKeyInfo, algorithm, true, ["sign"] ); let signature = await crypto.subtle.sign(algorithm, key, toSign); return ( header + ".." + QWACs.toBase64URLEncoding(arrayToString(new Uint8Array(signature))) ); } async function signTLSCertificateBinding(header, key, algorithm) { return sign( key, algorithm, QWACs.toBase64URLEncoding(JSON.stringify(header)) ); } async function verify_tls_certificate_binding_signature( headerSignatureAlgorithm, headerHashAlgorithm, signatureAlgorithm, signingKeyFilename, bindingCertificateFilename, serverCertificateFilename, presentedServerCertificateFilename, expectSuccess, expectedCertificateSubject ) { let signingKey = new Uint8Array( stringToArray( QWACs.fromBase64URLEncoding( pemToBase64(readFile(do_get_file(signingKeyFilename, false))) ) ) ); let bindingCertificate = constructCertFromFile(bindingCertificateFilename); let serverCertificate = constructCertFromFile(serverCertificateFilename); let testHeader = await makeBindingHeader( [bindingCertificate], [serverCertificate], headerSignatureAlgorithm, headerHashAlgorithm ); let binding = await signTLSCertificateBinding( testHeader, signingKey, signatureAlgorithm ); let presentedServerCertificate = constructCertFromFile( presentedServerCertificateFilename ); let qwac = await QWACs.verifyTLSCertificateBinding( binding, presentedServerCertificate, "example.com" ); equal( !!qwac, expectSuccess, `TLS certificate binding ${expectSuccess ? "should" : "should not"} verify correctly` ); if (expectSuccess) { equal( qwac.commonName, expectedCertificateSubject, "Verification should return the expected certificate" ); } } add_task(async function test_verify_tls_certificate_binding_signatures() { let options = [ { headerSignatureAlgorithm: "RS256", headerHashAlgorithm: "S512", signatureAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, signingKeyFilename: "bad_certs/default-ee.key", bindingCertificateFilename: "test_qwacs/2-qwac.pem", serverCertificateFilename: "bad_certs/default-ee.pem", presentedServerCertificateFilename: "bad_certs/default-ee.pem", expectSuccess: true, expectedCertificateSubject: "2-QWAC", }, // presented server certificate / bound server certificate mismatch { headerSignatureAlgorithm: "RS256", headerHashAlgorithm: "S512", signatureAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, signingKeyFilename: "bad_certs/default-ee.key", bindingCertificateFilename: "test_qwacs/2-qwac.pem", serverCertificateFilename: "bad_certs/default-ee.pem", presentedServerCertificateFilename: "bad_certs/ee-from-missing-intermediate.pem", expectSuccess: false, expectedCertificateSubject: null, }, { headerSignatureAlgorithm: "PS256", headerHashAlgorithm: "S256", signatureAlgorithm: { name: "RSA-PSS", hash: "SHA-256", saltLength: 32, }, signingKeyFilename: "bad_certs/default-ee.key", bindingCertificateFilename: "test_qwacs/2-qwac.pem", serverCertificateFilename: "bad_certs/default-ee.pem", presentedServerCertificateFilename: "bad_certs/default-ee.pem", expectSuccess: true, expectedCertificateSubject: "2-QWAC", }, { headerSignatureAlgorithm: "ES256", headerHashAlgorithm: "S384", signatureAlgorithm: { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256", }, signingKeyFilename: "test_qwacs/secp256r1.key", bindingCertificateFilename: "test_qwacs/2-qwac-ec.pem", serverCertificateFilename: "bad_certs/default-ee.pem", presentedServerCertificateFilename: "bad_certs/default-ee.pem", expectSuccess: true, expectedCertificateSubject: "2-QWAC with EC key", }, // header signature algorithm / actual signature algorithm mismatch { headerSignatureAlgorithm: "RS256", headerHashAlgorithm: "S384", signatureAlgorithm: { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256", }, signingKeyFilename: "test_qwacs/secp256r1.key", bindingCertificateFilename: "test_qwacs/2-qwac-ec.pem", serverCertificateFilename: "bad_certs/default-ee.pem", presentedServerCertificateFilename: "bad_certs/default-ee.pem", expectSuccess: false, expectedCertificateSubject: null, }, ]; for (let option of options) { await verify_tls_certificate_binding_signature( option.headerSignatureAlgorithm, option.headerHashAlgorithm, option.signatureAlgorithm, option.signingKeyFilename, option.bindingCertificateFilename, option.serverCertificateFilename, option.presentedServerCertificateFilename, option.expectSuccess, option.expectedCertificateSubject ); } });