Skip to content

Commit c75a377

Browse files
committed
Add serverCertificateHashes test server
Add a second webtransport server, in order to test connection with a server, that has a self-signed certificate together with serverCertificateHashes. See web-platform-tests/rfcs#216
1 parent d852a80 commit c75a377

File tree

7 files changed

+130
-12
lines changed

7 files changed

+130
-12
lines changed

tools/serve/serve.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import traceback
1616
import urllib
1717
import uuid
18+
import datetime
1819
from collections import defaultdict, OrderedDict
1920
from io import IOBase
2021
from itertools import chain, product
@@ -991,7 +992,7 @@ def start_servers(logger, host, ports, paths, routes, bind_address, config,
991992
continue
992993

993994
# Skip WebTransport over HTTP/3 server unless if is enabled explicitly.
994-
if scheme == 'webtransport-h3' and not kwargs.get("webtransport_h3"):
995+
if scheme in ['webtransport-h3', 'webtransport-h3-cert-hash'] and not kwargs.get("webtransport_h3"):
995996
continue
996997

997998
for port in ports:
@@ -1009,6 +1010,7 @@ def start_servers(logger, host, ports, paths, routes, bind_address, config,
10091010
"ws": start_ws_server,
10101011
"wss": start_wss_server,
10111012
"webtransport-h3": start_webtransport_h3_server,
1013+
"webtransport-h3-cert-hash": start_webtransport_h3_server_cert_hash,
10121014
}[scheme]
10131015

10141016
server_proc = ServerProc(mp_context, scheme=scheme)
@@ -1174,18 +1176,40 @@ def start_webtransport_h3_server(logger, host, port, paths, routes, bind_address
11741176
try:
11751177
# TODO(bashi): Move the following import to the beginning of this file
11761178
# once WebTransportH3Server is enabled by default.
1177-
from webtransport.h3.webtransport_h3_server import WebTransportH3Server # type: ignore
1179+
from webtransport.h3.webtransport_h3_server import WebTransportH3Server, WebTransportCertificateGeneration # type: ignore
11781180
return WebTransportH3Server(host=host,
11791181
port=port,
11801182
doc_root=paths["doc_root"],
1183+
cert_mode=WebTransportCertificateGeneration.USEPREGENERATED,
11811184
cert_path=config.ssl_config["cert_path"],
11821185
key_path=config.ssl_config["key_path"],
1183-
logger=logger)
1186+
logger=logger,
1187+
cert_hash_info=None
1188+
)
11841189
except Exception as error:
11851190
logger.critical(
11861191
f"Failed to start WebTransport over HTTP/3 server: {error}")
11871192
sys.exit(0)
11881193

1194+
def start_webtransport_h3_server_cert_hash(logger, host, port, paths, routes, bind_address, config, **kwargs):
1195+
try:
1196+
# TODO(bashi): Move the following import to the beginning of this file
1197+
# once WebTransportH3Server is enabled by default.
1198+
from webtransport.h3.webtransport_h3_server import WebTransportH3Server, WebTransportCertificateGeneration
1199+
return WebTransportH3Server(host=host,
1200+
port=port,
1201+
doc_root=paths["doc_root"],
1202+
cert_mode=WebTransportCertificateGeneration.GENERATEDVALIDSERVERCERTIFICATEHASHCERT,
1203+
cert_path=None,
1204+
key_path=None,
1205+
logger=logger,
1206+
cert_hash_info=config["cert_hash_info"]
1207+
)
1208+
except Exception as error:
1209+
logger.critical(
1210+
f"Failed to start WebTransport over HTTP/3 server with certificate hashes: {error}")
1211+
sys.exit(0)
1212+
11891213

11901214
def start(logger, config, routes, mp_context, log_handlers, **kwargs):
11911215
host = config["server_host"]
@@ -1249,6 +1273,7 @@ class ConfigBuilder(config.ConfigBuilder):
12491273
"ws": ["auto"],
12501274
"wss": ["auto"],
12511275
"webtransport-h3": ["auto"],
1276+
"webtransport-h3-cert-hash": ["auto"],
12521277
},
12531278
"check_subdomains": True,
12541279
"bind_address": True,
@@ -1372,7 +1397,7 @@ def get_parser():
13721397
parser.add_argument("--no-h2", action="store_false", dest="h2", default=None,
13731398
help="Disable the HTTP/2.0 server")
13741399
parser.add_argument("--webtransport-h3", action="store_true",
1375-
help="Enable WebTransport over HTTP/3 server")
1400+
help="Enable WebTransport over HTTP/3 servers")
13761401
parser.add_argument("--exit-after-start", action="store_true",
13771402
help="Exit after starting servers")
13781403
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")

tools/webtransport/h3/webtransport_h3_server.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
import sys
99
import threading
1010
import traceback
11-
from enum import IntEnum
11+
from enum import IntEnum, Enum
1212
from urllib.parse import urlparse
1313
from typing import Any, Dict, List, Optional, Tuple, cast
1414

15+
from cryptography import x509
16+
from cryptography.hazmat.primitives import serialization
17+
1518
# TODO(bashi): Remove import check suppressions once aioquic dependency is resolved.
1619
from aioquic.buffer import Buffer # type: ignore
1720
from aioquic.asyncio import QuicConnectionProtocol, serve # type: ignore
@@ -31,6 +34,7 @@
3134
from tools import localpaths # noqa: F401
3235
from wptserve import stash
3336
from .capsule import H3Capsule, H3CapsuleDecoder, CapsuleType
37+
from http.server import BaseHTTPRequestHandler, HTTPServer
3438

3539
"""
3640
A WebTransport over HTTP/3 server for testing.
@@ -499,6 +503,16 @@ def add(self, ticket: SessionTicket) -> None:
499503
def pop(self, label: bytes) -> Optional[SessionTicket]:
500504
return self.tickets.pop(label, None)
501505

506+
class WebTransportCertificateGeneration(Enum):
507+
"""
508+
Specify, if the server should generate a certificate or use an existing certificate
509+
USEPREGENERATED: use existing certificate
510+
GENERATEDVALIDSERVERCERTIFICATEHASHCERT: generate a certificate compatible to server cert hashes
511+
"""
512+
USEPREGENERATED = 1,
513+
GENERATEDVALIDSERVERCERTIFICATEHASHCERT = 2
514+
# TODO add cases for invalid certificates
515+
502516

503517
class WebTransportH3Server:
504518
"""
@@ -507,18 +521,31 @@ class WebTransportH3Server:
507521
:param host: Host from which to serve.
508522
:param port: Port from which to serve.
509523
:param doc_root: Document root for serving handlers.
524+
:paran cert_mode: The used certificate mode can be
525+
USEPREGENERATED or GENERATEDVALIDSERVERCERTIFICATEHASHCERT
510526
:param cert_path: Path to certificate file to use.
511527
:param key_path: Path to key file to use.
512528
:param logger: a Logger object for this server.
513529
"""
514530

515-
def __init__(self, host: str, port: int, doc_root: str, cert_path: str,
516-
key_path: str, logger: Optional[logging.Logger]) -> None:
531+
def __init__(self, host: str, port: int, doc_root: str, cert_mode: WebTransportCertificateGeneration,
532+
cert_path: Optional[str], key_path: Optional[str], logger: Optional[logging.Logger],
533+
cert_hash_info: Optional[Dict]) -> None:
517534
self.host = host
518535
self.port = port
519536
self.doc_root = doc_root
520-
self.cert_path = cert_path
521-
self.key_path = key_path
537+
if cert_path is not None:
538+
self.cert_path = cert_path
539+
if key_path is not None:
540+
self.key_path = key_path
541+
if cert_hash_info is not None:
542+
self.cert_hash_info = cert_hash_info
543+
self.cert_mode = cert_mode
544+
if (cert_path is None or key_path is None) and cert_mode == WebTransportCertificateGeneration.USEPREGENERATED:
545+
raise ValueError("Both cert_path and key_path must be provided, if cert_mode is USEPREGENERATED")
546+
if (cert_hash_info is None and cert_mode == WebTransportCertificateGeneration.GENERATEDVALIDSERVERCERTIFICATEHASHCERT):
547+
raise ValueError("cert_hash_info must be provided, if cert_mode is GENERATEDVALIDSERVERCERTIFICATEHASHCERT")
548+
522549
self.started = False
523550
global _doc_root
524551
_doc_root = self.doc_root
@@ -551,7 +578,16 @@ def _start_on_server_thread(self) -> None:
551578
_logger.info("Starting WebTransport over HTTP/3 server on %s:%s",
552579
self.host, self.port)
553580

554-
configuration.load_cert_chain(self.cert_path, self.key_path)
581+
if self.cert_mode == WebTransportCertificateGeneration.USEPREGENERATED:
582+
configuration.load_cert_chain(self.cert_path, self.key_path)
583+
else: # GENERATEDVALIDSERVERCERTIFICATEHASHCERT case
584+
configuration.private_key = serialization.load_pem_private_key(self.cert_hash_info["private_key"],
585+
password=None
586+
)
587+
configuration.certificate = x509.load_pem_x509_certificate(self.cert_hash_info["certificate"])
588+
configuration.certificate_chain = []
589+
590+
555591

556592
ticket_store = SessionTicketStore()
557593

tools/webtransport/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
aioquic==1.2.0
2+
cryptography

tools/wptrunner/wptrunner/environment.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@
99
import socket
1010
import sys
1111
import time
12+
import datetime
1213
from typing import Optional
1314

15+
from cryptography.hazmat.primitives.asymmetric import ec
16+
from cryptography.hazmat.primitives import hashes, serialization
17+
from cryptography.x509.oid import NameOID
18+
from cryptography import x509
19+
1420
import mozprocess
1521
from mozlog import get_default_logger, handlers
1622
from mozlog.structuredlog import StructuredLogger
@@ -46,6 +52,37 @@ def do_delayed_imports(logger, test_paths):
4652
(", ".join(failed), serve_root))
4753
sys.exit(1)
4854

55+
def generate_hash_certificate(host: str) -> str:
56+
private_key = ec.generate_private_key(ec.SECP256R1())
57+
subject = issuer = x509.Name([
58+
x509.NameAttribute(NameOID.COUNTRY_NAME, "DE"),
59+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Berlin"),
60+
x509.NameAttribute(NameOID.LOCALITY_NAME, "Berlin"),
61+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Wpt tests"),
62+
x509.NameAttribute(NameOID.COMMON_NAME, host),
63+
])
64+
now = datetime.datetime.now(datetime.timezone.utc)
65+
certificate = (
66+
x509.CertificateBuilder()
67+
.subject_name(subject)
68+
.issuer_name(issuer)
69+
.public_key(private_key.public_key())
70+
.serial_number(x509.random_serial_number())
71+
.not_valid_before(now)
72+
.not_valid_after(now + datetime.timedelta(days=13))
73+
.sign(private_key, hashes.SHA256())
74+
)
75+
fingerprint = certificate.fingerprint(hashes.SHA256())
76+
server_certificate_hash = ":".join(f"{byte:02x}" for byte in fingerprint)
77+
return { "certificate": certificate.public_bytes(
78+
encoding=serialization.Encoding.PEM
79+
),
80+
"private_key": private_key.private_bytes(
81+
encoding=serialization.Encoding.PEM,
82+
format=serialization.PrivateFormat.TraditionalOpenSSL,
83+
encryption_algorithm=serialization.NoEncryption()),
84+
"hash": server_certificate_hash
85+
}
4986

5087
def serve_path(test_paths):
5188
return test_paths["/"].tests_path
@@ -150,7 +187,8 @@ def __enter__(self):
150187
self.get_routes(),
151188
mp_context=mpcontext.get_context(),
152189
log_handlers=[server_log_handler],
153-
webtransport_h3=self.enable_webtransport)
190+
webtransport_h3=self.enable_webtransport,
191+
webtransport_h3_cert_hash=self.enable_webtransport)
154192

155193
if self.options.get("supports_debugger") and self.debug_info and self.debug_info.interactive:
156194
self._stack.enter_context(self.ignore_interrupts())
@@ -197,6 +235,7 @@ def build_config(self):
197235
"wss": [8889],
198236
"h2": [9000],
199237
"webtransport-h3": [11000],
238+
"webtransport-h3-cert-hash": [11001],
200239
}
201240
config.ports = ports
202241

@@ -221,6 +260,8 @@ def build_config(self):
221260
config.doc_root = serve_path(self.test_paths)
222261
config.inject_script = self.inject_script
223262

263+
config.cert_hash_info = generate_hash_certificate(config.server_host)
264+
224265
if self.suppress_handler_traceback is not None:
225266
config.logging["suppress_handler_traceback"] = self.suppress_handler_traceback
226267

@@ -323,10 +364,15 @@ def test_servers(self):
323364
for port, server in self.servers.get("webtransport-h3", []):
324365
if not webtranport_h3_server_is_running(host, port, timeout=5):
325366
pending.append((host, port))
367+
for port, server in self.servers.get("webtransport-h3-cert-hash", []):
368+
if not webtranport_h3_server_is_running(host, port, timeout=5):
369+
pending.append((host, port))
326370

327371
for scheme, servers in self.servers.items():
328372
if scheme == "webtransport-h3":
329373
continue
374+
if scheme == "webtransport-h3-cert-hash":
375+
continue
330376
for port, server in servers:
331377
s = socket.socket()
332378
s.settimeout(0.1)

tools/wptserve/wptserve/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class ConfigBuilder:
128128

129129
_default = {
130130
"browser_host": "localhost",
131+
"certificate_hash": {},
131132
"alternate_hosts": {},
132133
"doc_root": os.path.dirname("__file__"),
133134
"server_host": None,

tools/wptserve/wptserve/pipes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ def config_replacement(match):
478478
value = variables[field]
479479
elif hasattr(SubFunctions, field):
480480
value = getattr(SubFunctions, field)
481+
elif field == "server_certificate_hash":
482+
value = request.server.config["cert_hash_info"]["hash"]
481483
elif field == "headers":
482484
value = request.headers
483485
elif field == "GET":

webtransport/resources/webtransport-test-helpers.sub.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,23 @@
33

44
const HOST = get_host_info().ORIGINAL_HOST;
55
const PORT = '{{ports[webtransport-h3][0]}}';
6+
const PORT_CERT_HASH = '{{ports[webtransport-h3-cert-hash][0]}}';
67
const BASE = `https://${HOST}:${PORT}`;
8+
const BASE_CERT_HASH = `https://${HOST}:${PORT_CERT_HASH}`;
79

810
// Wait for the given number of milliseconds (ms).
911
function wait(ms) { return new Promise(res => step_timeout(res, ms)); }
1012

1113
// Create URL for WebTransport session.
12-
function webtransport_url(handler) {
14+
function webtransport_url(handler, options) {
15+
if (options?.cert_hashes) {
16+
return `${BASE_CERT_HASH}/webtransport/handlers/${handler}`;
17+
}
1318
return `${BASE}/webtransport/handlers/${handler}`;
1419
}
1520

21+
const cert_hash = new Uint8Array('{{server_certificate_hash}}'.split(':').map((el) => parseInt(el, 16)));
22+
const cert_hash_str = '{{server_certificate_hash}}'
1623
// Converts WebTransport stream error code to HTTP/3 error code.
1724
// https://ietf-wg-webtrans.github.io/draft-ietf-webtrans-http3/draft-ietf-webtrans-http3.html#section-4.3
1825
function webtransport_code_to_http_code(n) {

0 commit comments

Comments
 (0)