Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetters(lazy, {
CertDB: ["@mozilla.org/security/x509certdb;1", Ci.nsIX509CertDB],
});
function arrayToString(a) {
let s = "";
for (let b of a) {
s += String.fromCharCode(b);
}
return s;
}
function stringToArrayBuffer(str) {
let bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
export var QWACs = {
fromBase64URLEncoding(base64URLEncoded) {
return atob(base64URLEncoded.replaceAll("-", "+").replaceAll("_", "/"));
},
toBase64URLEncoding(str) {
return btoa(str)
.replaceAll("+", "-")
.replaceAll("/", "_")
.replaceAll("=", "");
},
// Validates and returns the decoded parameters of a TLS certificate binding
// header as specified by ETSI TS 119 411-5 V2.1.1 Annex B, ETSI TS 119 182-1
// V1.2.1, and RFC 7515.
// If the header contains invalid values or otherwise fails to validate,
// returns false.
// This should probably not be called directly outside of tests -
// verifyTLSCertificateBinding is the main entrypoint of this implementation.
validateTLSCertificateBindingHeader(header) {
// ETSI TS 119 411-5 V2.1.1 Annex B specifies the TLS Certificate Binding
// Profile and states that "Only header parameters specified in this
// profile may be present in the header of the generated JAdES signature."
const allowedHeaderKeys = new Set([
"alg",
"kid",
"cty",
"x5t#S256",
"x5c",
"iat",
"exp",
"sigD",
]);
let headerKeys = new Set(Object.keys(header));
if (!headerKeys.isSubsetOf(allowedHeaderKeys)) {
console.error("header contains invalid parameter");
return false;
}
// ETSI TS 119 182-1 V1.2.1 Section 5.1.2 specifies that "alg" shall be as
// described in RFC 7515 Section 4.1.1, which references RFC 7518. None of
// these specifications require support for a particular signature
// algorithm, but RFC 7518 recommends supporting "RS256" (RSASSA-PKCS1-v1_5
// with SHA-256) and "ES256" (ECDSA with P-256 and SHA-256), so those are
// supported. Additionally, "PS256" (RSASSA-PSS with SHA-256) is supported
// for compatibility and as better alternative to RSASSA-PKCS1-v1_5.
// The specification says "alg" can't conflict with signing certificate
// key. This is enforced when the signature is verified.
if (!("alg" in header)) {
console.error("header missing 'alg' field");
return false;
}
let algorithm;
switch (header.alg) {
case "RS256":
algorithm = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
break;
case "PS256":
algorithm = { name: "RSA-PSS", saltLength: 32, hash: "SHA-256" };
break;
case "ES256":
algorithm = { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" };
break;
default:
console.error("unsupported alg:", header.alg);
return false;
}
// RFC 7515 defines "kid" as an optional hint. It is unnecessary.
// ETSI TS 119 411-5 V2.1.1 Annex B says that "cty" will have the value
// "TLS-Certificate-Binding-v1". However, ETSI TS 119 182-1 V1.2.1 Section
// 5.1.3 states that "The cty header parameter should not be present if the
// sigD header parameter, specified in clause 5.2.8 of the present
// document, is present within the JAdES signature."
// ETSI TS 119 411-5 V2.1.1 Annex B also requires sigD to be present, so
// either this is a mistake, or ETSI TS 119 411-5 Annex B deliberately
// disregards ETSI TS 119 182-1.
// Chrome's implementation requires "cty", so for compatibility, this
// matches that behavior.
if (!("cty" in header)) {
console.error("header missing field 'cty'");
return false;
}
if (header.cty != "TLS-Certificate-Binding-v1") {
console.error("invalid value for cty:", header.cty);
return false;
}
// RFC 7515 defines "x5t#S256" as "base64url-encoded SHA-256 thumbprint
// (a.k.a. digest) of the DER encoding of the X.509 certificate [RFC5280]
// corresponding to the key used to digitally sign the JWS".
// It is optional. If present, it must match the digest of the 0th element
// of "x5c" (this is checked after processing x5c, below).
let x5tS256;
if ("x5t#S256" in header) {
x5tS256 = header["x5t#S256"];
}
// RFC 7515:
// The "x5c" (X.509 certificate chain) Header Parameter contains the
// X.509 public key certificate or certificate chain [RFC5280]
// corresponding to the key used to digitally sign the JWS. The
// certificate or certificate chain is represented as a JSON array of
// certificate value strings. Each string in the array is a
// base64-encoded (Section 4 of [RFC4648] -- not base64url-encoded) DER
// [ITU.X690.2008] PKIX certificate value.
if (!("x5c" in header)) {
console.error("header missing field 'x5c'");
return false;
}
let certificates = [];
for (let base64 of header.x5c) {
try {
certificates.push(lazy.CertDB.constructX509FromBase64(base64));
} catch (e) {
console.error("couldn't decode certificate");
return false;
}
}
// ETSI TS 119 411-5 V2.1.1 Annex B states that "x5c" consists of the full
// certificate chain, including the trust anchor. However, only the signing
// certificate and any intermediates relevant to path-building are strictly
// necessary.
if (certificates.length < 1) {
console.error("header must specify certificate chain");
return false;
}
if (x5tS256) {
// signingCertificateHashHex will be of the form "AA:BB:..."
let signingCertificateHashHex = certificates[0].sha256Fingerprint;
let signingCertificateHashBytes = signingCertificateHashHex
.split(":")
.map(hexStr => parseInt(hexStr, 16));
if (
x5tS256 !=
QWACs.toBase64URLEncoding(arrayToString(signingCertificateHashBytes))
) {
console.error("x5t#S256 does not match signing certificate");
return false;
}
}
// ETSI TS 119 411-5 V2.1.1 Annex B's definition of "iat" reads "This field
// contains the claimed signing time. The value shall be encoded as
// specified in IETF RFC 7519 [9]." However, in RFC 7519, "iat" is a claim
// that can be made by a JWT, not used as a header field in a JWS. In any
// case, ETSI TS 119 411-5 offers no guidance on how this header affects
// validation. Consequently, as it is optional, it is ignored.
// Similarly, the definition of "exp" reads "This field contains the expiry
// date of the binding. The maximum effective expiry time is whichever is
// soonest of this field, the longest-lived TLS certificate identified in
// the sigD member payload (below), or the notAfter time of the signing
// certificate. The value shall be encoded as specified in IETF RFC 7519
// [9]," again referencing a JWT claim and not a JWS header.
// We interpret this to be an optional mechanism to expire bindings earlier
// than the earliest "notAfter" value amongst the certificates specified in
// "x5c".
// RFC 7519 says this will be a NumericDate, which is a "JSON numeric value
// representing the number of seconds from 1970-01-01T00:00:00Z UTC".
if ("exp" in header) {
let expirationSeconds = parseInt(header.exp);
if (isNaN(expirationSeconds)) {
console.error("invalid expiration time");
return false;
}
let expiration = new Date(expirationSeconds * 1000);
if (expiration < new Date()) {
console.error("header has expired");
return false;
}
}
// "sigD" lists the TLS server certificates being bound, and must be
// present.
if (!("sigD" in header)) {
console.error("header missing field 'sigD'");
return false;
}
let sigD = header.sigD;
const allowedSigDKeys = new Set(["mId", "pars", "hashM", "hashV"]);
let sigDKeys = new Set(Object.keys(sigD));
if (!sigDKeys.isSubsetOf(allowedSigDKeys)) {
console.error("sigD contains invalid parameter");
return false;
}
// ETSI TS 119 411-5 V2.1.1 Annex B requires that "sigD.mId" be
if (!("mId" in sigD)) {
console.error("header missing field 'sigD.mId'");
return false;
}
console.error("invalid value for sigD.mId:", sigD.mId);
return false;
}
// ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.pars" as "A comma-separated
// list of TLS certificate file names." The only thing to validate here is
// that pars has as many elements as "hashV", later.
if (!("pars" in sigD)) {
console.error("header missing field 'sigD.pars'");
return false;
}
let pars = sigD.pars;
// ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.hashM" as 'The string
// identifying one of the approved hashing algorithms identified by ETSI TS
// 119 312 [8] for JAdES. This hashing algorithm is used to calculate the
// hashes described in the "hashV" member below.' It further requires that
// "SHA-256, SHA-384, and SHA-512 are supported, and it is assumed that
// strings identifying them are S256, S384, and S512 respectively".
if (!("hashM" in sigD)) {
console.error("header missing field 'sigD.hashM'");
return false;
}
let hashAlg;
switch (sigD.hashM) {
case "S256":
hashAlg = "SHA-256";
break;
case "S384":
hashAlg = "SHA-384";
break;
case "S512":
hashAlg = "SHA-512";
break;
default:
console.error("unsupported hashM:", sigD.hashM);
return false;
}
// ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.hashV" as 'A
// comma-separated list of TLS certificate file hashes. Each hash is
// produced by taking the corresponding X.509 certificate, computing its
// base64url encoding, and calculating its hash using the algorithm
// identified in the "hashM" member above.'
// This array must be the same length as the "sigD.pars" array.
if (!("hashV" in sigD)) {
console.error("header missing field 'sigD.hashV'");
return false;
}
let hashes = sigD.hashV;
if (hashes.length != pars.length) {
console.error("header sigD.pars/hashV mismatch");
return false;
}
for (let hash of hashes) {
if (typeof hash != "string") {
console.error("invalid hash:", hash);
return false;
}
}
return { algorithm, certificates, hashAlg, hashes };
},
// Given a TLS certificate binding, a TLS server certificate, and a hostname,
// this function validates the binding, extracts its parameters, verifies
// that the binding signing certificate is a 2-QWAC certificate valid for the
// given hostname that chains to a QWAC trust anchor, verifies the signature
// on the binding, and finally verifies that the binding covers the server
// certificate.
// Returns the QWAC upon success, and null otherwise.
async verifyTLSCertificateBinding(
tlsCertificateBinding,
serverCertificate,
hostname
) {
// tlsCertificateBinding is a JAdES signature, which is a JWS. Because ETSI
// TS 119 411-5 V2.1.1 Annex B requires sigD be present, and because ETSI
// TS 119 182-1 V1.2.1 states "The sigD header parameter shall not appear
// in JAdES signatures whose JWS Payload is attached",
// tlsCertificateBinding must have a detached payload.
// In other words, tlsCertificateBinding is a consists of:
// "<base64url-encoded header>..<base64url-encoded signature>"
let parts = tlsCertificateBinding.split(".");
if (parts.length != 3) {
console.error("invalid TLS certificate binding");
return null;
}
if (parts[1] != "") {
console.error("TLS certificate binding must have empty payload");
return null;
}
let header;
try {
header = JSON.parse(QWACs.fromBase64URLEncoding(parts[0]));
} catch (e) {
console.error("header is not base64(JSON)");
return null;
}
let params = QWACs.validateTLSCertificateBindingHeader(header);
if (!params) {
return null;
}
// The 0th certificate signed the binding. It must be a 2-QWAC that is
// valid for the given hostname (ETSI TS 119 411-5 V2.1.1 Section 6.2.2
// Step 4).
let signingCertificate = params.certificates[0];
let chain = params.certificates.slice(1);
if (
!(await lazy.CertDB.asyncVerifyQWAC(
Ci.nsIX509CertDB.TwoQWAC,
signingCertificate,
hostname,
chain
))
) {
console.error("signing certificate not 2-QWAC");
return null;
}
let spki = signingCertificate.subjectPublicKeyInfo;
let signingKey;
try {
signingKey = await crypto.subtle.importKey(
"spki",
new Uint8Array(spki),
params.algorithm,
true,
["verify"]
);
} catch (e) {
console.error("invalid signing key (algorithm mismatch?)");
return null;
}
let signature;
try {
signature = QWACs.fromBase64URLEncoding(parts[2]);
} catch (e) {
console.error("signature is not base64");
return null;
}
// Validate the signature (Step 5).
let signatureValid;
try {
signatureValid = await crypto.subtle.verify(
params.algorithm,
signingKey,
stringToArrayBuffer(signature),
stringToArrayBuffer(parts[0] + ".")
);
} catch (e) {
console.error("failed to verify signature");
return null;
}
if (!signatureValid) {
console.error("invalid signature");
return null;
}
// The binding must list the server certificate's hash (Step 6).
let serverCertificateHash = await crypto.subtle.digest(
params.hashAlg,
stringToArrayBuffer(
QWACs.toBase64URLEncoding(arrayToString(serverCertificate.getRawDER()))
)
);
if (
!params.hashes.includes(
QWACs.toBase64URLEncoding(
arrayToString(new Uint8Array(serverCertificateHash))
)
)
) {
console.error("TLS binding does not cover server certificate");
return null;
}
return signingCertificate;
},
/**
* Asynchronously determines the QWAC status of a document.
*
* @param secInfo {nsITransportSecurityInfo}
* The security information for the connection of the document.
* @param uri {nsIURI}
* The URI of the document.
* @param browsingContext {BrowsingContext}
* The browsing context of the load of the document.
* @returns {Promise}
* A promise that will resolve to an nsIX509Cert representing the QWAC in
* use, if any, and null otherwise.
*/
async determineQWACStatus(secInfo, uri, browsingContext) {
if (!secInfo || !secInfo.serverCert) {
return null;
}
// For some URIs, getting `host` will throw. ETSI TS 119 411-5 V2.1.1 only
// mentions domain names, so the assumed intention in such cases is to
// determine that the document is not using a QWAC.
let hostname;
try {
hostname = uri.host;
} catch {
return null;
}
let windowGlobal = browsingContext.currentWindowGlobal;
let actor = windowGlobal.getActor("TLSCertificateBinding");
let tlsCertificateBinding = null;
try {
tlsCertificateBinding = await actor.sendQuery(
"TLSCertificateBinding::Get"
);
} catch {
// If the page is closed before the query resolves, the actor will be
// destroyed, which causes a JS exception. We can safely ignore it,
// because the page is going away.
return null;
}
if (tlsCertificateBinding) {
let twoQwac = await QWACs.verifyTLSCertificateBinding(
tlsCertificateBinding,
secInfo.serverCert,
hostname
);
if (twoQwac) {
return twoQwac;
}
}
let is1qwac = await lazy.CertDB.asyncVerifyQWAC(
Ci.nsIX509CertDB.OneQWAC,
secInfo.serverCert,
hostname,
secInfo.handshakeCertificates.concat(secInfo.succeededCertChain)
);
if (is1qwac) {
return secInfo.serverCert;
}
return null;
},
};