QWACs.sys.mjs (16782B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 XPCOMUtils.defineLazyServiceGetters(lazy, { 10 CertDB: ["@mozilla.org/security/x509certdb;1", Ci.nsIX509CertDB], 11 }); 12 13 function arrayToString(a) { 14 let s = ""; 15 for (let b of a) { 16 s += String.fromCharCode(b); 17 } 18 return s; 19 } 20 21 function stringToArrayBuffer(str) { 22 let bytes = new Uint8Array(str.length); 23 for (let i = 0; i < str.length; i++) { 24 bytes[i] = str.charCodeAt(i); 25 } 26 return bytes; 27 } 28 29 export var QWACs = { 30 fromBase64URLEncoding(base64URLEncoded) { 31 return atob(base64URLEncoded.replaceAll("-", "+").replaceAll("_", "/")); 32 }, 33 34 toBase64URLEncoding(str) { 35 return btoa(str) 36 .replaceAll("+", "-") 37 .replaceAll("/", "_") 38 .replaceAll("=", ""); 39 }, 40 41 // Validates and returns the decoded parameters of a TLS certificate binding 42 // header as specified by ETSI TS 119 411-5 V2.1.1 Annex B, ETSI TS 119 182-1 43 // V1.2.1, and RFC 7515. 44 // If the header contains invalid values or otherwise fails to validate, 45 // returns false. 46 // This should probably not be called directly outside of tests - 47 // verifyTLSCertificateBinding is the main entrypoint of this implementation. 48 validateTLSCertificateBindingHeader(header) { 49 // ETSI TS 119 411-5 V2.1.1 Annex B specifies the TLS Certificate Binding 50 // Profile and states that "Only header parameters specified in this 51 // profile may be present in the header of the generated JAdES signature." 52 const allowedHeaderKeys = new Set([ 53 "alg", 54 "kid", 55 "cty", 56 "x5t#S256", 57 "x5c", 58 "iat", 59 "exp", 60 "sigD", 61 ]); 62 let headerKeys = new Set(Object.keys(header)); 63 if (!headerKeys.isSubsetOf(allowedHeaderKeys)) { 64 console.error("header contains invalid parameter"); 65 return false; 66 } 67 68 // ETSI TS 119 182-1 V1.2.1 Section 5.1.2 specifies that "alg" shall be as 69 // described in RFC 7515 Section 4.1.1, which references RFC 7518. None of 70 // these specifications require support for a particular signature 71 // algorithm, but RFC 7518 recommends supporting "RS256" (RSASSA-PKCS1-v1_5 72 // with SHA-256) and "ES256" (ECDSA with P-256 and SHA-256), so those are 73 // supported. Additionally, "PS256" (RSASSA-PSS with SHA-256) is supported 74 // for compatibility and as better alternative to RSASSA-PKCS1-v1_5. 75 // The specification says "alg" can't conflict with signing certificate 76 // key. This is enforced when the signature is verified. 77 if (!("alg" in header)) { 78 console.error("header missing 'alg' field"); 79 return false; 80 } 81 let algorithm; 82 switch (header.alg) { 83 case "RS256": 84 algorithm = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }; 85 break; 86 case "PS256": 87 algorithm = { name: "RSA-PSS", saltLength: 32, hash: "SHA-256" }; 88 break; 89 case "ES256": 90 algorithm = { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }; 91 break; 92 default: 93 console.error("unsupported alg:", header.alg); 94 return false; 95 } 96 97 // RFC 7515 defines "kid" as an optional hint. It is unnecessary. 98 99 // ETSI TS 119 411-5 V2.1.1 Annex B says that "cty" will have the value 100 // "TLS-Certificate-Binding-v1". However, ETSI TS 119 182-1 V1.2.1 Section 101 // 5.1.3 states that "The cty header parameter should not be present if the 102 // sigD header parameter, specified in clause 5.2.8 of the present 103 // document, is present within the JAdES signature." 104 // ETSI TS 119 411-5 V2.1.1 Annex B also requires sigD to be present, so 105 // either this is a mistake, or ETSI TS 119 411-5 Annex B deliberately 106 // disregards ETSI TS 119 182-1. 107 // Chrome's implementation requires "cty", so for compatibility, this 108 // matches that behavior. 109 if (!("cty" in header)) { 110 console.error("header missing field 'cty'"); 111 return false; 112 } 113 if (header.cty != "TLS-Certificate-Binding-v1") { 114 console.error("invalid value for cty:", header.cty); 115 return false; 116 } 117 118 // RFC 7515 defines "x5t#S256" as "base64url-encoded SHA-256 thumbprint 119 // (a.k.a. digest) of the DER encoding of the X.509 certificate [RFC5280] 120 // corresponding to the key used to digitally sign the JWS". 121 // It is optional. If present, it must match the digest of the 0th element 122 // of "x5c" (this is checked after processing x5c, below). 123 let x5tS256; 124 if ("x5t#S256" in header) { 125 x5tS256 = header["x5t#S256"]; 126 } 127 128 // RFC 7515: 129 // The "x5c" (X.509 certificate chain) Header Parameter contains the 130 // X.509 public key certificate or certificate chain [RFC5280] 131 // corresponding to the key used to digitally sign the JWS. The 132 // certificate or certificate chain is represented as a JSON array of 133 // certificate value strings. Each string in the array is a 134 // base64-encoded (Section 4 of [RFC4648] -- not base64url-encoded) DER 135 // [ITU.X690.2008] PKIX certificate value. 136 if (!("x5c" in header)) { 137 console.error("header missing field 'x5c'"); 138 return false; 139 } 140 let certificates = []; 141 for (let base64 of header.x5c) { 142 try { 143 certificates.push(lazy.CertDB.constructX509FromBase64(base64)); 144 } catch (e) { 145 console.error("couldn't decode certificate"); 146 return false; 147 } 148 } 149 // ETSI TS 119 411-5 V2.1.1 Annex B states that "x5c" consists of the full 150 // certificate chain, including the trust anchor. However, only the signing 151 // certificate and any intermediates relevant to path-building are strictly 152 // necessary. 153 if (certificates.length < 1) { 154 console.error("header must specify certificate chain"); 155 return false; 156 } 157 if (x5tS256) { 158 // signingCertificateHashHex will be of the form "AA:BB:..." 159 let signingCertificateHashHex = certificates[0].sha256Fingerprint; 160 let signingCertificateHashBytes = signingCertificateHashHex 161 .split(":") 162 .map(hexStr => parseInt(hexStr, 16)); 163 if ( 164 x5tS256 != 165 QWACs.toBase64URLEncoding(arrayToString(signingCertificateHashBytes)) 166 ) { 167 console.error("x5t#S256 does not match signing certificate"); 168 return false; 169 } 170 } 171 172 // ETSI TS 119 411-5 V2.1.1 Annex B's definition of "iat" reads "This field 173 // contains the claimed signing time. The value shall be encoded as 174 // specified in IETF RFC 7519 [9]." However, in RFC 7519, "iat" is a claim 175 // that can be made by a JWT, not used as a header field in a JWS. In any 176 // case, ETSI TS 119 411-5 offers no guidance on how this header affects 177 // validation. Consequently, as it is optional, it is ignored. 178 179 // Similarly, the definition of "exp" reads "This field contains the expiry 180 // date of the binding. The maximum effective expiry time is whichever is 181 // soonest of this field, the longest-lived TLS certificate identified in 182 // the sigD member payload (below), or the notAfter time of the signing 183 // certificate. The value shall be encoded as specified in IETF RFC 7519 184 // [9]," again referencing a JWT claim and not a JWS header. 185 // We interpret this to be an optional mechanism to expire bindings earlier 186 // than the earliest "notAfter" value amongst the certificates specified in 187 // "x5c". 188 // RFC 7519 says this will be a NumericDate, which is a "JSON numeric value 189 // representing the number of seconds from 1970-01-01T00:00:00Z UTC". 190 if ("exp" in header) { 191 let expirationSeconds = parseInt(header.exp); 192 if (isNaN(expirationSeconds)) { 193 console.error("invalid expiration time"); 194 return false; 195 } 196 let expiration = new Date(expirationSeconds * 1000); 197 if (expiration < new Date()) { 198 console.error("header has expired"); 199 return false; 200 } 201 } 202 203 // "sigD" lists the TLS server certificates being bound, and must be 204 // present. 205 if (!("sigD" in header)) { 206 console.error("header missing field 'sigD'"); 207 return false; 208 } 209 let sigD = header.sigD; 210 const allowedSigDKeys = new Set(["mId", "pars", "hashM", "hashV"]); 211 let sigDKeys = new Set(Object.keys(sigD)); 212 if (!sigDKeys.isSubsetOf(allowedSigDKeys)) { 213 console.error("sigD contains invalid parameter"); 214 return false; 215 } 216 // ETSI TS 119 411-5 V2.1.1 Annex B requires that "sigD.mId" be 217 // "http://uri.etsi.org/19182/ObjectIdByURIHash". 218 if (!("mId" in sigD)) { 219 console.error("header missing field 'sigD.mId'"); 220 return false; 221 } 222 if (sigD.mId != "http://uri.etsi.org/19182/ObjectIdByURIHash") { 223 console.error("invalid value for sigD.mId:", sigD.mId); 224 return false; 225 } 226 227 // ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.pars" as "A comma-separated 228 // list of TLS certificate file names." The only thing to validate here is 229 // that pars has as many elements as "hashV", later. 230 if (!("pars" in sigD)) { 231 console.error("header missing field 'sigD.pars'"); 232 return false; 233 } 234 let pars = sigD.pars; 235 236 // ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.hashM" as 'The string 237 // identifying one of the approved hashing algorithms identified by ETSI TS 238 // 119 312 [8] for JAdES. This hashing algorithm is used to calculate the 239 // hashes described in the "hashV" member below.' It further requires that 240 // "SHA-256, SHA-384, and SHA-512 are supported, and it is assumed that 241 // strings identifying them are S256, S384, and S512 respectively". 242 if (!("hashM" in sigD)) { 243 console.error("header missing field 'sigD.hashM'"); 244 return false; 245 } 246 let hashAlg; 247 switch (sigD.hashM) { 248 case "S256": 249 hashAlg = "SHA-256"; 250 break; 251 case "S384": 252 hashAlg = "SHA-384"; 253 break; 254 case "S512": 255 hashAlg = "SHA-512"; 256 break; 257 default: 258 console.error("unsupported hashM:", sigD.hashM); 259 return false; 260 } 261 262 // ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.hashV" as 'A 263 // comma-separated list of TLS certificate file hashes. Each hash is 264 // produced by taking the corresponding X.509 certificate, computing its 265 // base64url encoding, and calculating its hash using the algorithm 266 // identified in the "hashM" member above.' 267 // This array must be the same length as the "sigD.pars" array. 268 if (!("hashV" in sigD)) { 269 console.error("header missing field 'sigD.hashV'"); 270 return false; 271 } 272 let hashes = sigD.hashV; 273 if (hashes.length != pars.length) { 274 console.error("header sigD.pars/hashV mismatch"); 275 return false; 276 } 277 for (let hash of hashes) { 278 if (typeof hash != "string") { 279 console.error("invalid hash:", hash); 280 return false; 281 } 282 } 283 284 return { algorithm, certificates, hashAlg, hashes }; 285 }, 286 287 // Given a TLS certificate binding, a TLS server certificate, and a hostname, 288 // this function validates the binding, extracts its parameters, verifies 289 // that the binding signing certificate is a 2-QWAC certificate valid for the 290 // given hostname that chains to a QWAC trust anchor, verifies the signature 291 // on the binding, and finally verifies that the binding covers the server 292 // certificate. 293 // Returns the QWAC upon success, and null otherwise. 294 async verifyTLSCertificateBinding( 295 tlsCertificateBinding, 296 serverCertificate, 297 hostname 298 ) { 299 // tlsCertificateBinding is a JAdES signature, which is a JWS. Because ETSI 300 // TS 119 411-5 V2.1.1 Annex B requires sigD be present, and because ETSI 301 // TS 119 182-1 V1.2.1 states "The sigD header parameter shall not appear 302 // in JAdES signatures whose JWS Payload is attached", 303 // tlsCertificateBinding must have a detached payload. 304 // In other words, tlsCertificateBinding is a consists of: 305 // "<base64url-encoded header>..<base64url-encoded signature>" 306 let parts = tlsCertificateBinding.split("."); 307 if (parts.length != 3) { 308 console.error("invalid TLS certificate binding"); 309 return null; 310 } 311 if (parts[1] != "") { 312 console.error("TLS certificate binding must have empty payload"); 313 return null; 314 } 315 let header; 316 try { 317 header = JSON.parse(QWACs.fromBase64URLEncoding(parts[0])); 318 } catch (e) { 319 console.error("header is not base64(JSON)"); 320 return null; 321 } 322 let params = QWACs.validateTLSCertificateBindingHeader(header); 323 if (!params) { 324 return null; 325 } 326 327 // The 0th certificate signed the binding. It must be a 2-QWAC that is 328 // valid for the given hostname (ETSI TS 119 411-5 V2.1.1 Section 6.2.2 329 // Step 4). 330 let signingCertificate = params.certificates[0]; 331 let chain = params.certificates.slice(1); 332 if ( 333 !(await lazy.CertDB.asyncVerifyQWAC( 334 Ci.nsIX509CertDB.TwoQWAC, 335 signingCertificate, 336 hostname, 337 chain 338 )) 339 ) { 340 console.error("signing certificate not 2-QWAC"); 341 return null; 342 } 343 344 let spki = signingCertificate.subjectPublicKeyInfo; 345 let signingKey; 346 try { 347 signingKey = await crypto.subtle.importKey( 348 "spki", 349 new Uint8Array(spki), 350 params.algorithm, 351 true, 352 ["verify"] 353 ); 354 } catch (e) { 355 console.error("invalid signing key (algorithm mismatch?)"); 356 return null; 357 } 358 359 let signature; 360 try { 361 signature = QWACs.fromBase64URLEncoding(parts[2]); 362 } catch (e) { 363 console.error("signature is not base64"); 364 return null; 365 } 366 367 // Validate the signature (Step 5). 368 let signatureValid; 369 try { 370 signatureValid = await crypto.subtle.verify( 371 params.algorithm, 372 signingKey, 373 stringToArrayBuffer(signature), 374 stringToArrayBuffer(parts[0] + ".") 375 ); 376 } catch (e) { 377 console.error("failed to verify signature"); 378 return null; 379 } 380 if (!signatureValid) { 381 console.error("invalid signature"); 382 return null; 383 } 384 385 // The binding must list the server certificate's hash (Step 6). 386 let serverCertificateHash = await crypto.subtle.digest( 387 params.hashAlg, 388 stringToArrayBuffer( 389 QWACs.toBase64URLEncoding(arrayToString(serverCertificate.getRawDER())) 390 ) 391 ); 392 if ( 393 !params.hashes.includes( 394 QWACs.toBase64URLEncoding( 395 arrayToString(new Uint8Array(serverCertificateHash)) 396 ) 397 ) 398 ) { 399 console.error("TLS binding does not cover server certificate"); 400 return null; 401 } 402 return signingCertificate; 403 }, 404 405 /** 406 * Asynchronously determines the QWAC status of a document. 407 * 408 * @param secInfo {nsITransportSecurityInfo} 409 * The security information for the connection of the document. 410 * @param uri {nsIURI} 411 * The URI of the document. 412 * @param browsingContext {BrowsingContext} 413 * The browsing context of the load of the document. 414 * @returns {Promise} 415 * A promise that will resolve to an nsIX509Cert representing the QWAC in 416 * use, if any, and null otherwise. 417 */ 418 async determineQWACStatus(secInfo, uri, browsingContext) { 419 if (!secInfo || !secInfo.serverCert) { 420 return null; 421 } 422 423 // For some URIs, getting `host` will throw. ETSI TS 119 411-5 V2.1.1 only 424 // mentions domain names, so the assumed intention in such cases is to 425 // determine that the document is not using a QWAC. 426 let hostname; 427 try { 428 hostname = uri.host; 429 } catch { 430 return null; 431 } 432 433 let windowGlobal = browsingContext.currentWindowGlobal; 434 let actor = windowGlobal.getActor("TLSCertificateBinding"); 435 let tlsCertificateBinding = null; 436 try { 437 tlsCertificateBinding = await actor.sendQuery( 438 "TLSCertificateBinding::Get" 439 ); 440 } catch { 441 // If the page is closed before the query resolves, the actor will be 442 // destroyed, which causes a JS exception. We can safely ignore it, 443 // because the page is going away. 444 return null; 445 } 446 if (tlsCertificateBinding) { 447 let twoQwac = await QWACs.verifyTLSCertificateBinding( 448 tlsCertificateBinding, 449 secInfo.serverCert, 450 hostname 451 ); 452 if (twoQwac) { 453 return twoQwac; 454 } 455 } 456 457 let is1qwac = await lazy.CertDB.asyncVerifyQWAC( 458 Ci.nsIX509CertDB.OneQWAC, 459 secInfo.serverCert, 460 hostname, 461 secInfo.handshakeCertificates.concat(secInfo.succeededCertChain) 462 ); 463 if (is1qwac) { 464 return secInfo.serverCert; 465 } 466 467 return null; 468 }, 469 };