Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* 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";
var { setTimeout } = ChromeUtils.importESModule(
);
let trrServer;
let h3Port;
let h3EchConfig;
const certOverrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
function checkSecurityInfo(chan, expectPrivateDNS, expectAcceptedECH) {
let securityInfo = chan.securityInfo;
Assert.equal(
securityInfo.isAcceptedEch,
expectAcceptedECH,
"ECH Status == Expected Status"
);
Assert.equal(
securityInfo.usedPrivateDNS,
expectPrivateDNS,
"Private DNS Status == Expected Status"
);
}
add_setup(async function setup() {
// Allow telemetry probes which may otherwise be disabled for some
// applications (e.g. Thunderbird).
Services.prefs.setBoolPref(
"toolkit.telemetry.testing.overrideProductsCheck",
true
);
trr_test_setup();
Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true);
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
await asyncStartTLSTestServer(
"EncryptedClientHelloServer",
"../../../security/manager/ssl/tests/unit/test_encrypted_client_hello"
);
h3Port = Services.env.get("MOZHTTP3_PORT_ECH");
Assert.notEqual(h3Port, null);
Assert.notEqual(h3Port, "");
h3EchConfig = Services.env.get("MOZHTTP3_ECH");
Assert.notEqual(h3EchConfig, null);
Assert.notEqual(h3EchConfig, "");
});
registerCleanupFunction(async () => {
trr_clear_prefs();
Services.prefs.clearUserPref("network.trr.mode");
Services.prefs.clearUserPref("network.trr.uri");
Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
Services.prefs.clearUserPref("network.dns.echconfig.enabled");
Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled");
Services.prefs.clearUserPref(
"network.dns.echconfig.fallback_to_origin_when_all_failed"
);
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr");
Services.prefs.clearUserPref("security.tls.ech.grease_http3");
Services.prefs.clearUserPref("security.tls.ech.grease_probability");
if (trrServer) {
await trrServer.stop();
}
});
function makeChan(url) {
let chan = NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIHttpChannel);
return chan;
}
function channelOpenPromise(chan, flags) {
return new Promise(resolve => {
function finish(req, buffer) {
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
false
);
resolve([req, buffer]);
}
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
true
);
let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
internal.setWaitForHTTPSSVCRecord();
chan.asyncOpen(new ChannelListener(finish, null, flags));
});
}
function ActivityObserver() {}
ActivityObserver.prototype = {
activites: [],
observeConnectionActivity(
aHost,
aPort,
aSSL,
aHasECH,
aIsHttp3,
aActivityType,
aActivitySubtype,
aTimestamp,
aExtraStringData
) {
dump(
"*** Connection Activity 0x" +
aActivityType.toString(16) +
" 0x" +
aActivitySubtype.toString(16) +
" " +
aExtraStringData +
"\n"
);
this.activites.push({ host: aHost, subType: aActivitySubtype });
},
};
function checkHttpActivities(activites, expectECH) {
let foundDNSAndSocket = false;
let foundSettingECH = false;
let foundConnectionCreated = false;
for (let activity of activites) {
switch (activity.subType) {
case Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED:
case Ci.nsIHttpActivityObserver
.ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED:
foundDNSAndSocket = true;
break;
case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_ECH_SET:
foundSettingECH = true;
break;
case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_CONNECTION_CREATED:
foundConnectionCreated = true;
break;
default:
break;
}
}
Assert.equal(foundDNSAndSocket, true, "Should have one DnsAndSock created");
Assert.equal(foundSettingECH, expectECH, "Should have echConfig");
Assert.equal(
foundConnectionCreated,
true,
"Should have one connection created"
);
}
add_task(async function testConnectWithECH() {
const ECH_CONFIG_FIXED =
"AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA";
trrServer = new TRRServer();
await trrServer.start();
let observerService = Cc[
"@mozilla.org/network/http-activity-distributor;1"
].getService(Ci.nsIHttpActivityDistributor);
let observer = new ActivityObserver();
observerService.addObserver(observer);
observerService.observeConnection = true;
Services.prefs.setCharPref(
"network.trr.uri",
`https://foo.example.com:${trrServer.port()}/dns-query`
);
// Only the last record is valid to use.
await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", {
answers: [
{
name: "ech-private.example.com",
ttl: 55,
type: "HTTPS",
flush: false,
data: {
priority: 1,
name: "ech-private.example.com",
values: [
{ key: "alpn", value: "http/1.1" },
{ key: "port", value: 8443 },
{
key: "echconfig",
value: ECH_CONFIG_FIXED,
needBase64Decode: true,
},
],
},
},
],
});
await trrServer.registerDoHAnswers("ech-private.example.com", "A", {
answers: [
{
name: "ech-private.example.com",
ttl: 55,
type: "A",
flush: false,
data: "127.0.0.1",
},
],
});
await new TRRDNSListener("ech-private.example.com", {
type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
});
HandshakeTelemetryHelpers.resetHistograms();
let chan = makeChan(`https://ech-private.example.com`);
await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
checkSecurityInfo(chan, true, true);
// Only check telemetry if network process is disabled.
if (!mozinfo.socketprocess_networking) {
HandshakeTelemetryHelpers.checkSuccess(["", "_ECH", "_FIRST_TRY"]);
HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]);
}
await trrServer.stop();
observerService.removeObserver(observer);
observerService.observeConnection = false;
let filtered = observer.activites.filter(
activity => activity.host === "ech-private.example.com"
);
checkHttpActivities(filtered, true);
});
add_task(async function testEchRetry() {
Services.obs.notifyObservers(null, "net:cancel-all-connections");
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 1000));
Services.dns.clearCache(true);
const ECH_CONFIG_TRUSTED_RETRY =
"AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAMAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA";
trrServer = new TRRServer();
await trrServer.start();
Services.prefs.setIntPref("network.trr.mode", 3);
Services.prefs.setCharPref(
"network.trr.uri",
`https://foo.example.com:${trrServer.port()}/dns-query`
);
// Only the last record is valid to use.
await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", {
answers: [
{
name: "ech-private.example.com",
ttl: 55,
type: "HTTPS",
flush: false,
data: {
priority: 1,
name: "ech-private.example.com",
values: [
{ key: "alpn", value: "http/1.1" },
{ key: "port", value: 8443 },
{
key: "echconfig",
value: ECH_CONFIG_TRUSTED_RETRY,
needBase64Decode: true,
},
],
},
},
],
});
await trrServer.registerDoHAnswers("ech-private.example.com", "A", {
answers: [
{
name: "ech-private.example.com",
ttl: 55,
type: "A",
flush: false,
data: "127.0.0.1",
},
],
});
await new TRRDNSListener("ech-private.example.com", {
type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
});
Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
HandshakeTelemetryHelpers.resetHistograms();
let chan = makeChan(`https://ech-private.example.com`);
await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
checkSecurityInfo(chan, true, true);
// Only check telemetry if network process is disabled.
if (!mozinfo.socketprocess_networking) {
for (let hName of ["SSL_HANDSHAKE_RESULT", "SSL_HANDSHAKE_RESULT_ECH"]) {
let h = Services.telemetry.getHistogramById(hName);
HandshakeTelemetryHelpers.assertHistogramMap(
h.snapshot(),
new Map([
["0", 1],
["188", 1],
])
);
}
HandshakeTelemetryHelpers.checkEntry(["_FIRST_TRY"], 188, 1);
HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]);
}
await trrServer.stop();
});
async function H3ECHTest(
echConfig,
expectedHistKey,
expectedHistEntries,
advertiseECH
) {
Services.dns.clearCache(true);
Services.obs.notifyObservers(null, "net:cancel-all-connections");
/* eslint-disable mozilla/no-arbitrary-setTimeout */
await new Promise(resolve => setTimeout(resolve, 1000));
resetEchTelemetry();
trrServer = new TRRServer();
await trrServer.start();
Services.prefs.setCharPref(
"network.trr.uri",
`https://foo.example.com:${trrServer.port()}/dns-query`
);
Services.prefs.setBoolPref("network.dns.port_prefixed_qname_https_rr", true);
let observerService = Cc[
"@mozilla.org/network/http-activity-distributor;1"
].getService(Ci.nsIHttpActivityDistributor);
Services.obs.notifyObservers(null, "net:cancel-all-connections");
let observer = new ActivityObserver();
observerService.addObserver(observer);
observerService.observeConnection = true;
// Clear activities for past connections
observer.activites = [];
let portPrefixedName = `_${h3Port}._https.public.example.com`;
let vals = [
{ key: "alpn", value: "h3-29" },
{ key: "port", value: h3Port },
];
if (advertiseECH) {
vals.push({
key: "echconfig",
value: echConfig,
needBase64Decode: true,
});
}
// Only the last record is valid to use.
await trrServer.registerDoHAnswers(portPrefixedName, "HTTPS", {
answers: [
{
name: portPrefixedName,
ttl: 55,
type: "HTTPS",
flush: false,
data: {
priority: 1,
name: ".",
values: vals,
},
},
],
});
await trrServer.registerDoHAnswers("public.example.com", "A", {
answers: [
{
name: "public.example.com",
ttl: 55,
type: "A",
flush: false,
data: "127.0.0.1",
},
],
});
await new TRRDNSListener("public.example.com", {
type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
port: h3Port,
});
let chan = makeChan(`https://public.example.com:${h3Port}`);
let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
req.QueryInterface(Ci.nsIHttpChannel);
Assert.equal(req.protocolVersion, "h3-29");
checkSecurityInfo(chan, true, advertiseECH);
await trrServer.stop();
observerService.removeObserver(observer);
observerService.observeConnection = false;
let filtered = observer.activites.filter(
activity => activity.host === "public.example.com"
);
checkHttpActivities(filtered, advertiseECH);
await checkEchTelemetry(expectedHistKey, expectedHistEntries);
}
function resetEchTelemetry() {
Services.telemetry.getKeyedHistogramById("HTTP3_ECH_OUTCOME").clear();
}
async function checkEchTelemetry(histKey, histEntries) {
Services.obs.notifyObservers(null, "net:cancel-all-connections");
/* eslint-disable mozilla/no-arbitrary-setTimeout */
await new Promise(resolve => setTimeout(resolve, 1000));
let values = Services.telemetry
.getKeyedHistogramById("HTTP3_ECH_OUTCOME")
.snapshot()[histKey];
if (!mozinfo.socketprocess_networking) {
HandshakeTelemetryHelpers.assertHistogramMap(values, histEntries);
}
}
add_task(async function testH3WithNoEch() {
Services.prefs.setBoolPref("security.tls.ech.grease_http3", false);
Services.prefs.setIntPref("security.tls.ech.grease_probability", 0);
await H3ECHTest(
h3EchConfig,
"NONE",
new Map([
["0", 1],
["1", 0],
]),
false
);
});
add_task(async function testH3WithECH() {
await H3ECHTest(
h3EchConfig,
"REAL",
new Map([
["0", 1],
["1", 0],
]),
true
);
});
add_task(async function testH3WithGreaseEch() {
Services.prefs.setBoolPref("security.tls.ech.grease_http3", true);
Services.prefs.setIntPref("security.tls.ech.grease_probability", 100);
await H3ECHTest(
h3EchConfig,
"GREASE",
new Map([
["0", 1],
["1", 0],
]),
false
);
});
add_task(async function testH3WithECHRetry() {
Services.dns.clearCache(true);
Services.obs.notifyObservers(null, "net:cancel-all-connections");
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 1000));
function base64ToArray(base64) {
var binary_string = atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes;
}
let decodedConfig = base64ToArray(h3EchConfig);
decodedConfig[6] ^= 0x94;
let encoded = btoa(String.fromCharCode.apply(null, decodedConfig));
await H3ECHTest(
encoded,
"REAL",
new Map([
["0", 1],
["1", 1],
]),
true
);
});