/* 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(
"resource://gre/modules/Timer.sys.mjs"
);
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
);
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" },
{ 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");
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
);
});