/* 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"; // Tests that QUIC resumption tokens (address validation tokens) are not reused // across first-party partition contexts. A token obtained in one first-party // context must not allow 0-RTT resumption in a different first-party context. var { setTimeout } = ChromeUtils.importESModule( "resource://gre/modules/Timer.sys.mjs" ); registerCleanupFunction(async () => { http3_clear_prefs(); Services.prefs.clearUserPref("network.http.http3.enable_0rtt"); }); add_task(async function setup() { await http3_setup_tests("h3"); }); function makeChan(uri, partitionKey) { let chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true, }).QueryInterface(Ci.nsIHttpChannel); chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; if (partitionKey) { chan.loadInfo.originAttributes = { partitionKey }; } return chan; } function chanPromise(chan) { return new Promise(resolve => { chan.asyncOpen({ onStartRequest(_request) {}, onDataAvailable(_request, stream, _offset, count) { read_stream(stream, count); }, onStopRequest(request) { resolve(request); }, }); }); } async function cancelAllAndWait() { Services.obs.notifyObservers(null, "net:cancel-all-connections"); // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(resolve => setTimeout(resolve, 1000)); } // Verifies that a QUIC resumption token stored under first-party context A // cannot be used for 0-RTT resumption in first-party context B, and that // resumption within the same first-party context still works. add_task(async function test_quic_token_not_reused_across_partitions() { Services.prefs.setBoolPref("network.http.http3.enable_0rtt", true); // Initial connection under partition key A. No prior token exists, so the // handshake is not resumed. The server sends a resumption token stored under // A's peerId. let chanA1 = makeChan("https://foo.example.com/30", "(https,a.example.com)"); let reqA1 = await chanPromise(chanA1); Assert.equal(reqA1.status, Cr.NS_OK, "first A connection succeeded"); Assert.equal(reqA1.protocolVersion, "h3", "first A connection used H3"); Assert.equal( reqA1.securityInfo.resumed, false, "first A connection not resumed (no prior token)" ); await cancelAllAndWait(); // Connection under partition key B to the same server. The token stored under // A's peerId must not be visible under B's peerId. let chanB = makeChan("https://foo.example.com/30", "(https,b.example.com)"); let reqB = await chanPromise(chanB); Assert.equal(reqB.status, Cr.NS_OK, "B connection succeeded"); Assert.equal(reqB.protocolVersion, "h3", "B connection used H3"); Assert.equal( reqB.securityInfo.resumed, false, "B connection not resumed (A's token must not be reused across partitions)" ); Assert.notEqual( reqA1.securityInfo.peerId, reqB.securityInfo.peerId, "A and B use distinct peerIds confirming they run on separate QUIC sessions" ); await cancelAllAndWait(); // Reconnect under partition key A. The token from the first A connection is // still cached under A's peerId, so this connection must be resumed. let chanA2 = makeChan("https://foo.example.com/30", "(https,a.example.com)"); let reqA2 = await chanPromise(chanA2); Assert.equal(reqA2.status, Cr.NS_OK, "second A connection succeeded"); Assert.equal(reqA2.protocolVersion, "h3", "second A connection used H3"); Assert.equal( reqA2.securityInfo.resumed, true, "second A connection resumed using A's own token" ); Assert.equal( reqA1.securityInfo.peerId, reqA2.securityInfo.peerId, "A1 and A2 have the same peerId" ); });