Skip to content

Commit d26a677

Browse files
authored
Add security info explainer (#152)
* add security info * remove table of contents
1 parent 8aece80 commit d26a677

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed

security-info-explainer.md

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
## Table of Contents
2+
3+
<!-- Update this table of contents by running `npx doctoc README.md` -->
4+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
5+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
6+
7+
- [Introduction](#introduction)
8+
- [Goals](#goals)
9+
- [Non-goals](#non-goals)
10+
- [Use cases](#use-cases)
11+
- [Use case 1: Secure custom protocol for a server](#use-case-1-secure-custom-protocol-for-a-server)
12+
- [Potential Solution](#potential-solution)
13+
- [Example usage for https](#example-usage-for-https)
14+
- [Example usage for web sockets](#example-usage-for-web-sockets)
15+
- [API Definitions](#api-definitions)
16+
- [How this solution would solve the use cases](#how-this-solution-would-solve-the-use-cases)
17+
- [Use case 1](#use-case-1)
18+
- [Detailed design discussion](#detailed-design-discussion)
19+
- [Why attach `SecurityInfo` to the `onHeadersReceived` event?](#why-attach-securityinfo-to-the-onheadersreceived-event)
20+
- [Why require `securityInfo` and `securityInfoRawDer` options?](#why-require-securityinfo-and-securityinforawder-options)
21+
- [Considered alternatives](#considered-alternatives)
22+
- [A new `verifyTLSServerCertificate` API for IWAs](#a-new-verifytlsservercertificate-api-for-iwas)
23+
- [Bundling root certificates with the app](#bundling-root-certificates-with-the-app)
24+
- [Modifying the `fetch` API](#modifying-the-fetch-api)
25+
- [Porting Firefox Extensions API `getSecurityInfo`](#porting-firefox-extensions-api-getsecurityinfo)
26+
- [Security and Privacy Considerations](#security-and-privacy-considerations)
27+
- [Compatibility with Extensions API](#compatibility-with-extensions-api)
28+
29+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
30+
31+
## Introduction
32+
33+
Web apps sometimes need to establish secure raw TCP/UDP connections (e.g., via [Direct Sockets](https://wicg.github.io/direct-sockets/)) for custom protocols, often to support legacy servers that cannot be updated to modern alternatives like WebTransport. Unlike standard HTTPS, these raw sockets don't have a built-in mechanism to verify the server's TLS certificate against a trusted root store.
34+
35+
This proposal introduces a `WebRequest SecurityInfo` API for [`ControlledFrame`](https://wicg.github.io/controlled-frame). It allows a web app to intercept an HTTPS, WSS or WebTransport request to a server, retrieve the server's certificate fingerprint (as verified by the browser), and then use that fingerprint to manually verify the certificate of a separate raw TCP/UDP connection to the same server. This provides a simple way for the app to confirm it's talking to the correct server.
36+
37+
## Goals
38+
39+
Enable isolated web apps to verify the trustworthiness of a server's (D)TLS certificate when establishing raw TCP/UDP connections via APIs like [Direct Sockets](https://wicg.github.io/direct-sockets/). This allows apps using custom (non web) protocols to ensure they are communicating with the correct, browser-trusted server, preventing man-in-the-middle (MITM) attacks.
40+
Only in [Isolated Context](https://wicg.github.io/isolated-web-apps/isolated-contexts.html).
41+
42+
## Non-goals
43+
44+
* This API does **not** provide a general-purpose mechanism for web apps to verify arbitrary certificates against the browser's or OS's root store. The verification is limited to "pinning" a certificate fingerprint from a concurrent browser-verified connection.
45+
* This API does **not** directly add (D)TLS capabilities to Direct Sockets. The app is still responsible for implementing the (D)TLS handshake (e.g., via a WASM-based library like OpenSSL).
46+
47+
48+
## Use cases
49+
50+
### Use case 1: Secure custom protocol for a server
51+
A web app needs to communicate with a server that uses a custom protocol over raw TCP. This server cannot be updated to use a modern, secure-by-default protocol like WebTransport. The web app bundles a (D)TLS library (compiled to WASM) to secure the connection using Direct Sockets. To prevent MITM attacks, the web app needs to verify that the server's (D)TLS certificate is the one it trusts. The server *also* serves a simple HTTPS status page from the same domain, using the same certificate. The web app needs a way to get the certificate details from the HTTPS connection to verify the TCP connection.
52+
One of the reasons, why the server might need UDP is for better streaming performance, which is not possible to achieve via usual TCP.
53+
54+
## Potential Solution
55+
56+
We propose extending the `<controlledframe>` [`WebRequest`](https://wicg.github.io/controlled-frame/#api-web-request) API, which is available in Isolated Context. Specifically, we will add a new `securityInfo` field to the event object of the [`onHeadersReceived`](https://wicg.github.io/controlled-frame/#webrequestheadersreceivedevent) listener.
57+
58+
A web app will opt-in to receiving this information by setting true new dictionary members in the [`createWebRequestInterceptor`](https://wicg.github.io/controlled-frame/#dom-webrequest-createwebrequestinterceptor) options.
59+
60+
### Example usage for https
61+
62+
```javascript
63+
// Global variable to a trusted certificate to be used later.
64+
let trustedCert = undefined;
65+
66+
// html page must have <controlledframe id="cf">.
67+
// src can be set as "about:blank".
68+
const cf = document.querySelector('controlledframe');
69+
70+
// Set up https fetch interceptor.
71+
const interceptor = cf.request.createWebRequestInterceptor({
72+
urlPatterns: ["*://*/*"],
73+
resourceTypes: ["xmlhttprequest"],
74+
securityInfoRawDer: true
75+
});
76+
77+
// Save trusted certificate from intercepted request.
78+
interceptor.addEventListener('headersreceived', (e) => {
79+
const securityInfo = e.securityInfo;
80+
81+
if (securityInfo && securityInfo.state == "secure") {
82+
trustedCert = securityInfo.certificates[0].rawDER;
83+
console.log('obtained trusted certificate bytes' + trustedCert);
84+
}
85+
});
86+
87+
// Execute https request.
88+
cf.executeScript({
89+
code: `fetch('https://simple-push-demo.vercel.app/');`
90+
});
91+
```
92+
93+
### Example usage for web sockets
94+
95+
```javascript
96+
// Global variable to a trusted certificate to be used later.
97+
let trustedCert = undefined;
98+
99+
// html page must have <controlledframe id="cf">.
100+
// src can be set as "about:blank".
101+
const cf = document.querySelector('controlledframe');
102+
103+
// Set up wss fetch interceptor.
104+
const interceptor = cf.request.createWebRequestInterceptor({
105+
// The urlPattern: ["*://*/*"] works for wss scheme only
106+
// from 142 version of Chrome.
107+
urlPatterns: ["wss://*/*"],
108+
resourceTypes: ["websocket"],
109+
securityInfoRawDer: true
110+
});
111+
112+
// Save trusted certificate from intercepted request.
113+
interceptor.addEventListener('headersreceived', (e) => {
114+
const securityInfo = e.securityInfo;
115+
116+
if (securityInfo && securityInfo.state == "secure") {
117+
trustedCert = securityInfo.certificates[0].rawDER;
118+
console.log('obtained trusted certificate' + trustedCert);
119+
}
120+
});
121+
122+
// Execute wss request.
123+
cf.executeScript({
124+
code: `
125+
console.log('web socket is started.');
126+
const socket = new WebSocket('wss://echo.websocket.org/');
127+
128+
socket.onopen = (event) => {
129+
console.log('✅ WebSocket connection established.');
130+
131+
socket.close();
132+
};
133+
`
134+
});
135+
```
136+
137+
### API Definitions
138+
139+
The `securityInfo` object will be added to the `WebRequestHeadersReceivedEvent`:
140+
141+
```javascript
142+
[Exposed=Window, IsolatedContext]
143+
interface WebRequestHeadersReceivedEvent : WebRequestEvent {
144+
readonly attribute WebRequestResponse response;
145+
// New field.
146+
readonly attribute SecurityInfo? securityInfo;
147+
};
148+
```
149+
The new dictionaries are defined as follows, closely matching the [Firefox extensions API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/SecurityInfo) for compatibility:
150+
151+
```javascript
152+
[Exposed=Window, IsolatedContext]
153+
dictionary SecurityInfo {
154+
required sequence<CertificateInfo> certificates;
155+
required ConnectionState state;
156+
};
157+
[Exposed=Window, IsolatedContext]
158+
enum ConnectionState {
159+
"broken", "insecure", "secure"
160+
};
161+
[Exposed=Window, IsolatedContext]
162+
dictionary Fingerprint {
163+
required DOMString sha256;
164+
};
165+
166+
[Exposed=Window, IsolatedContext]
167+
dictionary CertificateInfo {
168+
required Fingerprint fingerprint;
169+
// Included only if securityInfoRawDer: true provided in WebRequestInterceptorOptions.
170+
Uint8Array rawDER;
171+
};
172+
```
173+
174+
And the new options for `createWebRequestInterceptor`:
175+
176+
```javascript
177+
dictionary WebRequestInterceptorOptions {
178+
// ... existing options
179+
boolean securityInfo = false;
180+
boolean securityInfoRawDer = false;
181+
};
182+
```
183+
184+
* New `onHeadersReceivedOptions` are necessary for performance reasons. They specify whether ssl data must be kept for as long as the web request is alive. Without them, unnecessary data can be kept for longer. This is very undesirable especially in post quantum cryptography, since certificates can take a significant amount of memory space.
185+
186+
* A new `securityInfo` object can be obtained in the `onHeadersReceived` event listener.
187+
188+
* To receive this information, a web app **must** include `"securityInfo=true"` or `"securityInfoRawDer=true"` when calling `createWebRequestInterceptor`. This opt-in design prevents performance overhead for the majority of extensions that don't need this data.
189+
190+
* The `securityInfo` object will only be populated for requests made over a secure protocol (e.g., HTTPS, WSS) where the TLS/QUIC handshake has successfully completed or also in case of certificate errors.
191+
Browsers interrupt connections when there's a certificate error, unless user has explicitely allowed it in the browser UI, only in this case it is possible to have SecurityInfo with `state = "broken"`.
192+
193+
* `certificates` - will contain only the leaf server certificate. This is done for future extensibility and Firefox API compatibility, because Firefox API provides a leaf if [certificateChain](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/getSecurityInfo#certificatechain) is not included in getSecurityInfo options.
194+
195+
* `state` - State of the connection. One of:
196+
* `"broken"`: the TLS handshake failed (for example, the certificate has expired)
197+
* `"insecure"`: the connection is not a TLS connection
198+
* `"secure"`: the connection is a secure TLS connection
199+
* Note that Firefox extension API has a `“weak”` state of connection, which does not exist in Chrome.
200+
201+
* The `CertificateInfo.rawDER` field contains the raw certificate bytes in DER format, which can be parsed by the extension using a third-party library. This field is only provided if `WebRequestInterceptorOptions` includes `securityInfoRawDer=true`. The reason for it is a performance optimization to not pass raw bytes when it is not necessary, and compatibility with Firefox extensions API.
202+
203+
204+
### How this solution would solve the use cases
205+
206+
#### Use case 1
207+
The solution described in the code example above directly addresses the use case. A web app would:
208+
1. Use `<controlledframe>` to make a standard `fetch` (HTTPS) or `WebSocket` (WSS) connection to its server.
209+
2. Register a `webRequest` interceptor with `securityInfo: true` or `securityInfoRawDer: true`.
210+
3. In the `onHeadersReceived` listener, capture the `securityInfo.certificates[0].fingerprint.sha256` or `rawDER` from the browser-verified `secure` connection.
211+
4. Initiate its raw TCP/UDP connection via Direct Sockets.
212+
5. Perform its own (D)TLS handshake using a WASM-based library.
213+
6. During the handshake's certificate verification step, the app compares the fingerprint or full certificate provided by the server against the trusted data captured in step 3.
214+
7. If they match, the app trusts the connection. If not, it aborts.
215+
216+
## Detailed design discussion
217+
218+
### Why attach `SecurityInfo` to the `onHeadersReceived` event?
219+
To support non-blocking web request model.
220+
221+
Another alternative, that was considered is [Firefox's extension API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/getSecurityInfo). It has a separate asynchronous function `getSecurityInfo(requestId)`. We chose to attach the data to the `onHeadersReceived` event to support non-blocking event-driven architecture (Manifest V3, which the extension version is based on).
222+
223+
In a non-blocking model, the internal network and certificate data associated with a request is often discarded immediately after the request pipeline advances. An asynchronous `getSecurityInfo()` call would be racy and unreliable. By requiring an opt-in (`securityInfo: true`) *before* the request, the browser knows to hold onto this data just long enough to deliver it with the `onHeadersReceived` event, ensuring data availability without blocking.
224+
225+
### Why require `securityInfo` and `securityInfoRawDer` options?
226+
We introduced two separate boolean dictionary members, `securityInfo` and `securityInfoRawDer`, to minimize performance overhead.
227+
228+
1. **`securityInfo`**: Calculating and retaining certificate information (even just a fingerprint) for every request would add overhead. This dictionary member ensures we only do this work for interceptors that actually need the data.
229+
2. **`securityInfoRawDer`**: Providing the raw DER-encoded certificate bytes (`rawDER`) is a further optimization. Many use cases (like fingerprint pinning) only need the `sha256` hash, which is small. Passing the full certificate bytes (which can be large, especially with post-quantum cryptography) is unnecessary unless the app explicitly requests it for parsing. This aligns with compatibility goals with the Firefox API.
230+
231+
## Considered alternatives
232+
233+
### A new `verifyTLSServerCertificate` API for IWAs
234+
We considered adding a new API, similar to [`platformKeys.verifyTLSServerCertificate` deprecated extension APIs](https://developer.chrome.com/docs/extensions/mv2/reference/platformKeys#method-verifyTLSServerCertificate), that would verify a certificate against the browser's root store. However, this was rejected as it would mean using the Web PKI's Browser Root Store for non-web protocols, which is against security policy. The CRS is intended only for certs used for HTTPS, and using it for other protocols could slow the evolution of Web PKI security practices.
235+
236+
### Bundling root certificates with the app
237+
The app could bundle its own set of trusted root CAs. This has significant drawbacks:
238+
* **Maintenance:** The app developer is now responsible for tracking and updating the root store, which is a significant maintenance burden.
239+
* **Revocation:** The app would have no way to perform revocation checks.
240+
* **Restrictive Environments:** Apps in highly restricted firewall configurations might not be able to connect to external endpoints to update their bundled root store.
241+
242+
### Modifying the `fetch` API
243+
We considered adding certificate access to the standard `fetch` API. This was deemed undesirable because `fetch` is a very general-purpose API available to the entire web. This `WebRequest SecurityInfo` feature is for the niche, high-privilege use case of Isolated Web Apps, and the `ControlledFrame` `webRequest` API is the appropriate, isolated surface for it. Furthermore, `fetch` only works for HTTPS, whereas `webRequest` also covers WebSockets (WSS) and WebTransport.
244+
245+
### Porting Firefox Extensions API `getSecurityInfo`
246+
We considered porting [Firefox `getSecurityInfo`](https://developer.chrome.com/docs/extensions/mv2/reference/platformKeys#method-verifyTLSServerCertificate) extensions API, which is partially what happened, except that the current proposal adds non blocking compatible way to obtain `SecurityInfo`.
247+
248+
## Security and Privacy Considerations
249+
250+
This API exposes the server's leaf certificate and fingerprint to the web app. This is not considered a new security or privacy risk.
251+
252+
A web app with Isolated Context and the `direct-sockets` permission can already open a raw TCP connection to any server, perform a (D)TLS handshake using a WASM library, and retrieve the *exact same* server certificate.
253+
254+
This proposal simply makes the process more reliable by allowing the app to get the *browser-verified* certificate information, rather than one from a separate, (potentially) different connection. It does not expose any new information that a privileged web app couldn't already obtain. The API is restricted to `IsolatedContext` environments and is not available to the general web.
255+
256+
## Compatibility with Extensions API
257+
258+
`WebRequest SecurityInfo` will also be implemented for [Extensions API in Chrome](https://github.com/w3c/webextensions/pull/899), whereas in Firefox it [already exists](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/getSecurityInfo).
259+
The reason for keeping Extension API aligned with Isolated Context Web API:
260+
261+
* To reduce maintenance burden, since internally the same code is responsible for both API.
262+
* Developer ergonomics: authors can easily reuse the API in different contexts, without having to learn an entirely new one.

0 commit comments

Comments
 (0)