test_httpssvc_retry_with_ech.js (14381B)
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 "use strict"; 6 7 var { setTimeout } = ChromeUtils.importESModule( 8 "resource://gre/modules/Timer.sys.mjs" 9 ); 10 11 let trrServer; 12 let h3Port; 13 let h3EchConfig; 14 15 const certOverrideService = Cc[ 16 "@mozilla.org/security/certoverride;1" 17 ].getService(Ci.nsICertOverrideService); 18 19 function checkSecurityInfo(chan, expectPrivateDNS, expectAcceptedECH) { 20 let securityInfo = chan.securityInfo; 21 Assert.equal( 22 securityInfo.isAcceptedEch, 23 expectAcceptedECH, 24 "ECH Status == Expected Status" 25 ); 26 Assert.equal( 27 securityInfo.usedPrivateDNS, 28 expectPrivateDNS, 29 "Private DNS Status == Expected Status" 30 ); 31 } 32 33 add_setup(async function setup() { 34 // Allow telemetry probes which may otherwise be disabled for some 35 // applications (e.g. Thunderbird). 36 Services.prefs.setBoolPref( 37 "toolkit.telemetry.testing.overrideProductsCheck", 38 true 39 ); 40 41 trrServer = new TRRServer(); 42 await trrServer.start(); 43 trr_test_setup(); 44 45 Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); 46 Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); 47 Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); 48 Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); 49 Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); 50 Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); 51 52 await asyncStartTLSTestServer( 53 "EncryptedClientHelloServer", 54 "../../../security/manager/ssl/tests/unit/test_encrypted_client_hello" 55 ); 56 57 h3Port = Services.env.get("MOZHTTP3_PORT_ECH"); 58 Assert.notEqual(h3Port, null); 59 Assert.notEqual(h3Port, ""); 60 61 h3EchConfig = Services.env.get("MOZHTTP3_ECH"); 62 Assert.notEqual(h3EchConfig, null); 63 Assert.notEqual(h3EchConfig, ""); 64 65 registerCleanupFunction(async () => { 66 trr_clear_prefs(); 67 Services.prefs.clearUserPref("network.trr.mode"); 68 Services.prefs.clearUserPref("network.trr.uri"); 69 Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); 70 Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); 71 Services.prefs.clearUserPref("network.dns.echconfig.enabled"); 72 Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled"); 73 Services.prefs.clearUserPref( 74 "network.dns.echconfig.fallback_to_origin_when_all_failed" 75 ); 76 Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); 77 Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr"); 78 Services.prefs.clearUserPref("security.tls.ech.grease_http3"); 79 Services.prefs.clearUserPref("security.tls.ech.grease_probability"); 80 if (trrServer) { 81 await trrServer.stop(); 82 } 83 }); 84 }); 85 86 function makeChan(url) { 87 let chan = NetUtil.newChannel({ 88 uri: url, 89 loadUsingSystemPrincipal: true, 90 contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, 91 }).QueryInterface(Ci.nsIHttpChannel); 92 return chan; 93 } 94 95 function channelOpenPromise(chan, flags) { 96 return new Promise(resolve => { 97 function finish(req, buffer) { 98 certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( 99 false 100 ); 101 resolve([req, buffer]); 102 } 103 certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( 104 true 105 ); 106 chan.asyncOpen(new ChannelListener(finish, null, flags)); 107 }); 108 } 109 110 function ActivityObserver() {} 111 112 ActivityObserver.prototype = { 113 activites: [], 114 observeConnectionActivity( 115 aHost, 116 aPort, 117 aSSL, 118 aHasECH, 119 aIsHttp3, 120 aActivityType, 121 aActivitySubtype, 122 aTimestamp, 123 aExtraStringData 124 ) { 125 dump( 126 "*** Connection Activity 0x" + 127 aActivityType.toString(16) + 128 " 0x" + 129 aActivitySubtype.toString(16) + 130 " " + 131 aExtraStringData + 132 "\n" 133 ); 134 this.activites.push({ host: aHost, subType: aActivitySubtype }); 135 }, 136 }; 137 138 function checkHttpActivities(activites, expectECH) { 139 let foundDNSAndSocket = false; 140 let foundSettingECH = false; 141 let foundConnectionCreated = false; 142 for (let activity of activites) { 143 switch (activity.subType) { 144 case Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED: 145 case Ci.nsIHttpActivityObserver 146 .ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED: 147 foundDNSAndSocket = true; 148 break; 149 case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_ECH_SET: 150 foundSettingECH = true; 151 break; 152 case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_CONNECTION_CREATED: 153 foundConnectionCreated = true; 154 break; 155 default: 156 break; 157 } 158 } 159 160 Assert.equal(foundDNSAndSocket, true, "Should have one DnsAndSock created"); 161 Assert.equal(foundSettingECH, expectECH, "Should have echConfig"); 162 Assert.equal( 163 foundConnectionCreated, 164 true, 165 "Should have one connection created" 166 ); 167 } 168 169 add_task(async function testConnectWithECH() { 170 const ECH_CONFIG_FIXED = 171 "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA"; 172 trrServer = new TRRServer(); 173 await trrServer.start(); 174 175 let observerService = Cc[ 176 "@mozilla.org/network/http-activity-distributor;1" 177 ].getService(Ci.nsIHttpActivityDistributor); 178 let observer = new ActivityObserver(); 179 observerService.addObserver(observer); 180 observerService.observeConnection = true; 181 182 Services.prefs.setCharPref( 183 "network.trr.uri", 184 `https://foo.example.com:${trrServer.port()}/dns-query` 185 ); 186 187 // Only the last record is valid to use. 188 await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", { 189 answers: [ 190 { 191 name: "ech-private.example.com", 192 ttl: 55, 193 type: "HTTPS", 194 flush: false, 195 data: { 196 priority: 1, 197 name: "ech-private.example.com", 198 values: [ 199 { key: "alpn", value: "http/1.1" }, 200 { key: "port", value: 8443 }, 201 { 202 key: "echconfig", 203 value: ECH_CONFIG_FIXED, 204 needBase64Decode: true, 205 }, 206 ], 207 }, 208 }, 209 ], 210 }); 211 212 await trrServer.registerDoHAnswers("ech-private.example.com", "A", { 213 answers: [ 214 { 215 name: "ech-private.example.com", 216 ttl: 55, 217 type: "A", 218 flush: false, 219 data: "127.0.0.1", 220 }, 221 ], 222 }); 223 224 await new TRRDNSListener("ech-private.example.com", { 225 type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, 226 }); 227 228 HandshakeTelemetryHelpers.resetHistograms(); 229 let chan = makeChan(`https://ech-private.example.com`); 230 await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); 231 checkSecurityInfo(chan, true, true); 232 // Only check telemetry if network process is disabled. 233 if (!mozinfo.socketprocess_networking) { 234 HandshakeTelemetryHelpers.checkSuccess(["", "_ECH", "_FIRST_TRY"]); 235 HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]); 236 } 237 238 observerService.removeObserver(observer); 239 observerService.observeConnection = false; 240 241 let filtered = observer.activites.filter( 242 activity => activity.host === "ech-private.example.com" 243 ); 244 checkHttpActivities(filtered, true); 245 }); 246 247 add_task(async function testEchRetry() { 248 Services.obs.notifyObservers(null, "net:cancel-all-connections"); 249 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 250 await new Promise(resolve => setTimeout(resolve, 1000)); 251 252 Services.dns.clearCache(true); 253 254 const ECH_CONFIG_TRUSTED_RETRY = 255 "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAMAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA"; 256 Services.prefs.setIntPref("network.trr.mode", 3); 257 Services.prefs.setCharPref( 258 "network.trr.uri", 259 `https://foo.example.com:${trrServer.port()}/dns-query` 260 ); 261 262 // Only the last record is valid to use. 263 await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", { 264 answers: [ 265 { 266 name: "ech-private.example.com", 267 ttl: 55, 268 type: "HTTPS", 269 flush: false, 270 data: { 271 priority: 1, 272 name: "ech-private.example.com", 273 values: [ 274 { key: "alpn", value: "http/1.1" }, 275 { key: "port", value: 8443 }, 276 { 277 key: "echconfig", 278 value: ECH_CONFIG_TRUSTED_RETRY, 279 needBase64Decode: true, 280 }, 281 ], 282 }, 283 }, 284 ], 285 }); 286 287 await trrServer.registerDoHAnswers("ech-private.example.com", "A", { 288 answers: [ 289 { 290 name: "ech-private.example.com", 291 ttl: 55, 292 type: "A", 293 flush: false, 294 data: "127.0.0.1", 295 }, 296 ], 297 }); 298 299 await new TRRDNSListener("ech-private.example.com", { 300 type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, 301 }); 302 303 Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); 304 305 HandshakeTelemetryHelpers.resetHistograms(); 306 let chan = makeChan(`https://ech-private.example.com`); 307 await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); 308 checkSecurityInfo(chan, true, true); 309 // Only check telemetry if network process is disabled. 310 if (!mozinfo.socketprocess_networking) { 311 for (let hName of ["SSL_HANDSHAKE_RESULT", "SSL_HANDSHAKE_RESULT_ECH"]) { 312 let h = Services.telemetry.getHistogramById(hName); 313 HandshakeTelemetryHelpers.assertHistogramMap( 314 h.snapshot(), 315 new Map([ 316 ["0", 1], 317 ["188", 1], 318 ]) 319 ); 320 } 321 HandshakeTelemetryHelpers.checkEntry(["_FIRST_TRY"], 188, 1); 322 HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]); 323 } 324 }); 325 326 async function H3ECHTest( 327 echConfig, 328 expectedHistKey, 329 expectedHistEntries, 330 advertiseECH 331 ) { 332 Services.dns.clearCache(true); 333 Services.obs.notifyObservers(null, "net:cancel-all-connections"); 334 /* eslint-disable mozilla/no-arbitrary-setTimeout */ 335 await new Promise(resolve => setTimeout(resolve, 1000)); 336 resetEchTelemetry(); 337 338 Services.prefs.setCharPref( 339 "network.trr.uri", 340 `https://foo.example.com:${trrServer.port()}/dns-query` 341 ); 342 Services.prefs.setBoolPref("network.dns.port_prefixed_qname_https_rr", true); 343 344 let observerService = Cc[ 345 "@mozilla.org/network/http-activity-distributor;1" 346 ].getService(Ci.nsIHttpActivityDistributor); 347 Services.obs.notifyObservers(null, "net:cancel-all-connections"); 348 let observer = new ActivityObserver(); 349 observerService.addObserver(observer); 350 observerService.observeConnection = true; 351 // Clear activities for past connections 352 observer.activites = []; 353 354 let portPrefixedName = `_${h3Port}._https.public.example.com`; 355 let vals = [ 356 { key: "alpn", value: "h3" }, 357 { key: "port", value: h3Port }, 358 ]; 359 if (advertiseECH) { 360 vals.push({ 361 key: "echconfig", 362 value: echConfig, 363 needBase64Decode: true, 364 }); 365 } 366 // Only the last record is valid to use. 367 368 await trrServer.registerDoHAnswers(portPrefixedName, "HTTPS", { 369 answers: [ 370 { 371 name: portPrefixedName, 372 ttl: 55, 373 type: "HTTPS", 374 flush: false, 375 data: { 376 priority: 1, 377 name: ".", 378 values: vals, 379 }, 380 }, 381 ], 382 }); 383 384 await trrServer.registerDoHAnswers("public.example.com", "A", { 385 answers: [ 386 { 387 name: "public.example.com", 388 ttl: 55, 389 type: "A", 390 flush: false, 391 data: "127.0.0.1", 392 }, 393 ], 394 }); 395 396 await new TRRDNSListener("public.example.com", { 397 type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, 398 port: h3Port, 399 }); 400 401 let chan = makeChan(`https://public.example.com:${h3Port}`); 402 let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); 403 req.QueryInterface(Ci.nsIHttpChannel); 404 Assert.equal(req.protocolVersion, "h3"); 405 checkSecurityInfo(chan, true, advertiseECH); 406 407 observerService.removeObserver(observer); 408 observerService.observeConnection = false; 409 410 let filtered = observer.activites.filter( 411 activity => activity.host === "public.example.com" 412 ); 413 checkHttpActivities(filtered, advertiseECH); 414 await checkEchTelemetry(expectedHistKey, expectedHistEntries); 415 } 416 417 function resetEchTelemetry() { 418 Services.telemetry.getKeyedHistogramById("HTTP3_ECH_OUTCOME").clear(); 419 } 420 421 async function checkEchTelemetry(histKey, histEntries) { 422 Services.obs.notifyObservers(null, "net:cancel-all-connections"); 423 /* eslint-disable mozilla/no-arbitrary-setTimeout */ 424 await new Promise(resolve => setTimeout(resolve, 1000)); 425 let values = Services.telemetry 426 .getKeyedHistogramById("HTTP3_ECH_OUTCOME") 427 .snapshot()[histKey]; 428 if (!mozinfo.socketprocess_networking) { 429 HandshakeTelemetryHelpers.assertHistogramMap(values, histEntries); 430 } 431 } 432 433 add_task(async function testH3WithNoEch() { 434 Services.prefs.setBoolPref("security.tls.ech.grease_http3", false); 435 Services.prefs.setIntPref("security.tls.ech.grease_probability", 0); 436 await H3ECHTest( 437 h3EchConfig, 438 "NONE", 439 new Map([ 440 ["0", 1], 441 ["1", 0], 442 ]), 443 false 444 ); 445 }); 446 447 add_task(async function testH3WithECH() { 448 await H3ECHTest( 449 h3EchConfig, 450 "REAL", 451 new Map([ 452 ["0", 1], 453 ["1", 0], 454 ]), 455 true 456 ); 457 }); 458 459 add_task(async function testH3WithGreaseEch() { 460 Services.prefs.setBoolPref("security.tls.ech.grease_http3", true); 461 Services.prefs.setIntPref("security.tls.ech.grease_probability", 100); 462 await H3ECHTest( 463 h3EchConfig, 464 "GREASE", 465 new Map([ 466 ["0", 1], 467 ["1", 0], 468 ]), 469 false 470 ); 471 }); 472 473 add_task(async function testH3WithECHRetry() { 474 Services.dns.clearCache(true); 475 Services.obs.notifyObservers(null, "net:cancel-all-connections"); 476 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 477 await new Promise(resolve => setTimeout(resolve, 1000)); 478 479 function base64ToArray(base64) { 480 var binary_string = atob(base64); 481 var len = binary_string.length; 482 var bytes = new Uint8Array(len); 483 for (var i = 0; i < len; i++) { 484 bytes[i] = binary_string.charCodeAt(i); 485 } 486 return bytes; 487 } 488 489 let decodedConfig = base64ToArray(h3EchConfig); 490 decodedConfig[6] ^= 0x94; 491 let encoded = btoa(String.fromCharCode.apply(null, decodedConfig)); 492 await H3ECHTest( 493 encoded, 494 "REAL", 495 new Map([ 496 ["0", 1], 497 ["1", 1], 498 ]), 499 true 500 ); 501 });