コンテンツにスキップ

ブラウザーが必要とするもの

DNS 名も CA(認証局)も使わず、IP リテラル(IP アドレスの直接指定)で WebTransport に接続するブラウザーは、 サーバーの証明書に 3 つのものを要求し、3 つ目の値に接続を固定(ピン留め)します。 自己署名証明書に含まれる SAN の IP アドレス、証明書が実際にその範囲に収まっている有効期間、 そして証明書自身の SHA-256 ハッシュ(serverCertificateHashes)です。

コード

main.c
#define WIRED_MAIN
#include "wired.h"
/* A real browser dialing WebTransport by IP (no DNS name) needs three
* things from the server identity: a SAN IP address entry in the self-
* signed cert (RFC 5280 4.2.1.6), a validity window the cert is actually
* inside of (now_secs), and the cert's SHA-256 to pin via
* serverCertificateHashes. now_secs=0 keeps the SDK's fixed 2020-2030
* window, so this demo stays deterministic without a real clock. */
static const u8 SAN_IPV4[4] = {127, 0, 0, 1};
int wired_main(int argc, char** argv) {
/* The same fixed demo identity as the HTTP/3 hello page. */
static wired_srvboot_demo_keys keys;
wired_srvboot_id id;
wired_srvboot_demo(&id, &keys, 0x50, "guide-wt");
id.max_datagram_frame_size = 65535; /* WebTransport requires DATAGRAM */
id.san_ipv4 = SAN_IPV4;
id.now_secs = 0; /* the SDK's fixed 2020-2030 window */
wired_srvboot_log_fingerprint(2, &id);
wired_srvrun_opt opt = {0};
opt.incoming_cpu = -1;
u16 port = (u16)wired_cliargs_int(argc, argv, "--port", 4433);
wired_srvrun_handler h = {0};
wired_srvrun_obs obs = {0};
return wired_server_run_opt(port, &id, h, obs, &opt) ? 0 : 1;
}

クライアント

client.go
package main
import (
"context"
"crypto/sha256"
"crypto/tls"
"fmt"
"net/http"
"os"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/webtransport-go"
)
func main() {
ctx := context.Background()
// A browser verifies serverCertificateHashes against the leaf cert's
// own SHA-256, computed from the bytes it actually received -- this is
// that same check, done by hand.
tlsConf := &tls.Config{InsecureSkipVerify: true, NextProtos: []string{http3.NextProtoH3}}
conn, err := quic.DialAddr(ctx, os.Args[1][len("https://"):], tlsConf, nil)
if err != nil {
panic(err)
}
leaf := conn.ConnectionState().TLS.PeerCertificates[0].Raw
fmt.Printf("cert sha256: %x\n", sha256.Sum256(leaf))
conn.CloseWithError(0, "")
// Then the actual WebTransport CONNECT a browser would make to the
// same IP literal.
var wtConn *quic.Conn // kept so main can close it before exiting
d := webtransport.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DialAddr: func(ctx context.Context, addr string, t *tls.Config, q *quic.Config) (*quic.Conn, error) {
c, err := quic.DialAddrEarly(ctx, addr, t, q)
wtConn = c
return c, err
},
}
rsp, _, err := d.Dial(ctx, os.Args[1]+"/chat", http.Header{})
if err != nil {
panic(err)
}
fmt.Println("status", rsp.StatusCode)
wtConn.CloseWithError(0, "") // ending the connection ends its session too
}

実行結果

Terminal window
$ ./wt-browser --port 4433
cert sha-256 fingerprint: fd:ef:07:1f:c8:07:4b:45:60:67:f3:a0:52:de:62:e8:86:03:32:12:04:61:ce:77:7f:25:4f:0f:9b:77:1c:e8
conn closed by peer (CONNECTION_CLOSE)
conn closed by peer (CONNECTION_CLOSE)
Terminal window
$ go run . https://127.0.0.1:4433
cert sha256: fdef071fc8074b456067f3a052de62e8860332120461ce777f254f0f9b771ce8
status 200
  • id.san_ipv4 は、自己署名証明書に RFC 5280 4.2.1.6 の SAN IP アドレスエントリー (ここでは 127.0.0.1)を追加します。 これがないと、IP アドレスを直接指定して接続するブラウザーは証明書を拒否します (SAN には dNSName=localhost しか含まれないため)。
  • id.now_secs は、証明書の有効期間の基準になります。 0 を渡すと SDK の固定された 2020〜2030 年の有効期間になり、このページでは毎回同じ(決定的な)出力になります。 実運用では、代わりに wired_clock_epoch_secs() を渡します(examples/moqt_chat を参照)。
  • wired_srvboot_log_fingerprint(2, &id) は、サーバーが提示する証明書そのものを組み立て、 SHA-256 でハッシュを取り、ブラウザーの serverCertificateHashes と同じ方法でログに出します (生の 32 バイトが欲しいときは wired_srvboot_cert_sha256 を使います)。 クライアント側のページは、実際に受信したバイト列に対して同じハッシュ計算をしており、両者の行は一致します。
  • wt_resource_check を登録していないので、/chat への Extended CONNECT はそのまま通ります (WebTransport セッション のページで説明した、デフォルトの挙動と同じです)。