Skip to content

Commit 069ad14

Browse files
committed
feat: improve certificate fingerprint support for WebTransport
- Add automatic generation of WebTransport-compatible certificates - ECDSA algorithm, 14-day validity, self-signed - Generated when no cert files are provided - Add certificate validation to warn if requirements not met - Change default listening address to 0.0.0.0:4443 - Disable fingerprint server by default (port 0) - Add generate-webtransport-cert.sh script for manual cert generation - Update documentation with simplified fingerprint usage This makes it easier to use WebTransport fingerprints for development without needing to manually generate certificates with specific properties.
1 parent 7892a02 commit 069ad14

File tree

5 files changed

+164
-13
lines changed

5 files changed

+164
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Configuration options for `audiobatch` and `videobatch` to control how many frames should be sent in every MoQ object/CMAF chunk
1919
- systemd service script and helpers for mlmpub
2020
- fingerprint endpoint of mlmpub to be used with WebTransport browser clients like [warp-player[wp]
21+
- Certificate validation and auto-generation for WebTransport-compatible certificates (ECDSA, 14-day validity)
2122

2223
## [0.2.0] - 2025-04-28
2324

README.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,22 +114,42 @@ One way to do that is with mkcert:
114114
```sh
115115
> mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1
116116
> mkcert -install
117-
> go run . -cert cert.pem -key key.pem -addr localhost:4443
117+
> go run . -cert cert.pem -key key.pem
118118
```
119119

120120
#### Using certificate fingerprint
121121

122-
Alternatively, you can use the certificate fingerprint feature for self-signed certificates without installing them in the browser:
122+
For browsers that support WebTransport certificate fingerprints (e.g., Chrome), you can use self-signed certificates without installing them:
123123

124+
**Run mlmpub with fingerprint support**:
124125
```sh
125-
> go run . -cert cert.pem -key key.pem -addr 0.0.0.0:4443 -fingerprintport 8081
126+
> go run . -fingerprintport 8081
127+
```
128+
129+
This will automatically generate a WebTransport-compatible certificate with:
130+
- ECDSA algorithm (not RSA)
131+
- 14-day validity (WebTransport maximum)
132+
- Self-signed
133+
134+
Alternatively, you can use your own certificate (e.g., generated with the included `generate-webtransport-cert.sh` script):
135+
```sh
136+
cd cmd/mlmpub
137+
./generate-webtransport-cert.sh
138+
go run . -cert cert-fp.pem -key key-fp.pem -fingerprintport 8081
126139
```
127140

128141
This will:
129-
- Start the MoQ server on port 4443 (listening on all interfaces)
142+
- Start the MoQ server on port 4443 (default address is `0.0.0.0:4443`, listening on all interfaces)
130143
- Start an HTTP server on port 8081 that serves the certificate's SHA-256 fingerprint
144+
- Validate that the certificate meets WebTransport requirements
145+
146+
The warp-player (fingerprint branch) can then connect using:
147+
- Server URL: `https://localhost:4443/moq` or `https://127.0.0.1:4443/moq`
148+
- Fingerprint URL: `http://localhost:8081/fingerprint` or `http://127.0.0.1:8081/fingerprint`
131149

132-
The warp-player can then connect using the fingerprint URL to authenticate the self-signed certificate. Use `-fingerprintport 0` to disable the fingerprint server.
150+
**Notes**:
151+
- The fingerprint server is disabled by default (`-fingerprintport 0`). Only enable it when using certificates that meet WebTransport's strict requirements.
152+
- If no certificate files are provided, mlmpub will generate WebTransport-compatible certificates automatically.
133153

134154

135155
## Development
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
# Generate WebTransport-compatible certificate with fingerprint
4+
# Requirements: ECDSA, ≤14 days validity, self-signed
5+
6+
echo "Generating WebTransport-compatible certificate..."
7+
8+
# Generate ECDSA private key
9+
openssl ecparam -genkey -name prime256v1 -out key-fp.pem
10+
11+
# Generate self-signed certificate valid for 14 days
12+
openssl req -new -x509 -key key-fp.pem -out cert-fp.pem -days 14 \
13+
-subj "/CN=localhost" \
14+
-addext "subjectAltName=DNS:localhost,DNS:127.0.0.1,IP:127.0.0.1,IP:::1"
15+
16+
echo "Certificate generated: cert-fp.pem"
17+
echo "Private key generated: key-fp.pem"
18+
echo ""
19+
20+
# Verify the certificate
21+
echo "Certificate details:"
22+
openssl x509 -in cert-fp.pem -text -noout | grep -E "Signature Algorithm:|Not Before:|Not After:|Subject Alternative Name" -A1
23+
24+
echo ""
25+
echo "Certificate fingerprint (SHA-256):"
26+
# Get the fingerprint
27+
openssl x509 -in cert-fp.pem -noout -fingerprint -sha256 | sed 's/://g' | cut -d'=' -f2 | tr '[:upper:]' '[:lower:]'

cmd/mlmpub/handler.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ func (h *moqHandler) runServer(ctx context.Context) error {
8383
}
8484

8585
func (h *moqHandler) startFingerprintServer() {
86+
// Validate certificate for WebTransport requirements
87+
if err := h.validateCertificateForWebTransport(); err != nil {
88+
slog.Warn("Certificate does not meet WebTransport fingerprint requirements", "error", err)
89+
slog.Warn("Fingerprint server may not work properly with WebTransport")
90+
}
91+
8692
fingerprint := h.getCertificateFingerprint()
8793
if fingerprint == "" {
8894
slog.Error("failed to get certificate fingerprint")
@@ -135,6 +141,50 @@ func (h *moqHandler) getCertificateFingerprint() string {
135141
return hex.EncodeToString(fingerprint[:])
136142
}
137143

144+
func (h *moqHandler) validateCertificateForWebTransport() error {
145+
if len(h.tlsConfig.Certificates) == 0 {
146+
return fmt.Errorf("no certificates found")
147+
}
148+
149+
cert := h.tlsConfig.Certificates[0]
150+
if len(cert.Certificate) == 0 {
151+
return fmt.Errorf("certificate is empty")
152+
}
153+
154+
// Parse the certificate
155+
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
156+
if err != nil {
157+
return fmt.Errorf("failed to parse certificate: %w", err)
158+
}
159+
160+
// Check 1: Must be self-signed (issuer == subject)
161+
if x509Cert.Issuer.String() != x509Cert.Subject.String() {
162+
return fmt.Errorf("certificate is not self-signed (issuer: %s, subject: %s)",
163+
x509Cert.Issuer.String(), x509Cert.Subject.String())
164+
}
165+
166+
// Check 2: Must use ECDSA algorithm
167+
if x509Cert.PublicKeyAlgorithm != x509.ECDSA {
168+
return fmt.Errorf("certificate must use ECDSA algorithm, but uses %s",
169+
x509Cert.PublicKeyAlgorithm.String())
170+
}
171+
172+
// Check 3: Must be valid for 14 days or less
173+
validityDuration := x509Cert.NotAfter.Sub(x509Cert.NotBefore)
174+
maxDuration := 14 * 24 * time.Hour
175+
if validityDuration > maxDuration {
176+
validityDays := validityDuration.Hours() / 24
177+
return fmt.Errorf("certificate validity exceeds 14 days (valid for %.1f days)", validityDays)
178+
}
179+
180+
slog.Info("Certificate meets WebTransport fingerprint requirements",
181+
"algorithm", x509Cert.PublicKeyAlgorithm.String(),
182+
"validity_days", validityDuration.Hours()/24,
183+
"self_signed", true)
184+
185+
return nil
186+
}
187+
138188
func serveQUICConn(wt *webtransport.Server, conn quic.Connection) {
139189
err := wt.ServeQUICConn(conn)
140190
if err != nil {

cmd/mlmpub/main.go

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ package main
22

33
import (
44
"context"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
57
"crypto/rand"
6-
"crypto/rsa"
8+
"crypto/sha256"
79
"crypto/tls"
810
"crypto/x509"
11+
"crypto/x509/pkix"
12+
"encoding/hex"
913
"encoding/pem"
1014
"errors"
1115
"flag"
1216
"fmt"
1317
"io"
1418
"log/slog"
1519
"math/big"
20+
"net"
1621
"os"
22+
"time"
1723

1824
"github.com/Eyevinn/moqlivemock/internal"
1925
)
@@ -55,14 +61,14 @@ func parseOptions(fs *flag.FlagSet, args []string) (*options, error) {
5561
}
5662

5763
opts := options{}
58-
fs.StringVar(&opts.certFile, "cert", "localhost.pem", "TLS certificate file (only used for server)")
59-
fs.StringVar(&opts.keyFile, "key", "localhost-key.pem", "TLS key file (only used for server)")
60-
fs.StringVar(&opts.addr, "addr", "localhost:8080", "listen or connect address")
64+
fs.StringVar(&opts.certFile, "cert", "cert.pem", "TLS certificate file (only used for server)")
65+
fs.StringVar(&opts.keyFile, "key", "key.pem", "TLS key file (only used for server)")
66+
fs.StringVar(&opts.addr, "addr", "0.0.0.0:4443", "listen or connect address")
6167
fs.StringVar(&opts.asset, "asset", "../../content", "Asset to serve")
6268
fs.StringVar(&opts.qlogfile, "qlog", defaultQlogFileName, "qlog file to write to. Use '-' for stderr")
6369
fs.IntVar(&opts.audioSampleBatch, "audiobatch", 2, "Nr audio samples per MoQ object/CMAF chunk")
6470
fs.IntVar(&opts.videoSampleBatch, "videobatch", 1, "Nr video samples per MoQ object/CMAF chunk")
65-
fs.IntVar(&opts.fingerprintPort, "fingerprintport", 8081, "Port for HTTP fingerprint server (0 to disable)")
71+
fs.IntVar(&opts.fingerprintPort, "fingerprintport", 0, "Port for HTTP fingerprint server (0 to disable)")
6672
fs.BoolVar(&opts.version, "version", false, fmt.Sprintf("Get %s version", appName))
6773
err := fs.Parse(args[1:])
6874
return &opts, err
@@ -153,23 +159,70 @@ func generateTLSConfigWithCertAndKey(certFile, keyFile string) (*tls.Config, err
153159
}
154160

155161
// Setup a bare-bones TLS config for the server
162+
// Generates a certificate that meets WebTransport fingerprint requirements
156163
func generateTLSConfig() (*tls.Config, error) {
157-
key, err := rsa.GenerateKey(rand.Reader, 1024)
164+
// Generate ECDSA key (required for WebTransport fingerprints)
165+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
158166
if err != nil {
159167
return nil, err
160168
}
161-
template := x509.Certificate{SerialNumber: big.NewInt(1)}
169+
170+
// Create certificate template with WebTransport-compatible settings
171+
template := x509.Certificate{
172+
SerialNumber: big.NewInt(1),
173+
Subject: pkix.Name{
174+
CommonName: "localhost",
175+
},
176+
Issuer: pkix.Name{
177+
CommonName: "localhost", // Explicitly set issuer = subject for self-signed
178+
},
179+
NotBefore: time.Now(),
180+
NotAfter: time.Now().Add(14 * 24 * time.Hour), // 14 days max for WebTransport
181+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
182+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
183+
BasicConstraintsValid: true,
184+
IsCA: true, // Self-signed CA
185+
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
186+
DNSNames: []string{"localhost", "127.0.0.1"}, // Include IP as DNS too
187+
}
188+
189+
// Create self-signed certificate
162190
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
163191
if err != nil {
164192
return nil, err
165193
}
166-
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
194+
195+
// Encode key and certificate to PEM
196+
keyBytes, err := x509.MarshalECPrivateKey(key)
197+
if err != nil {
198+
return nil, err
199+
}
200+
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
167201
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
168202

169203
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
170204
if err != nil {
171205
return nil, err
172206
}
207+
208+
// Parse the generated certificate to get fingerprint
209+
parsedCert, err := x509.ParseCertificate(tlsCert.Certificate[0])
210+
if err == nil {
211+
fingerprint := sha256.Sum256(parsedCert.Raw)
212+
slog.Info("Generated WebTransport-compatible certificate",
213+
"algorithm", "ECDSA",
214+
"validity_days", 14,
215+
"self_signed", true,
216+
"fingerprint", hex.EncodeToString(fingerprint[:]),
217+
"subject", parsedCert.Subject.String(),
218+
"issuer", parsedCert.Issuer.String())
219+
} else {
220+
slog.Info("Generated WebTransport-compatible certificate",
221+
"algorithm", "ECDSA",
222+
"validity_days", 14,
223+
"self_signed", true)
224+
}
225+
173226
return &tls.Config{
174227
Certificates: []tls.Certificate{tlsCert},
175228
NextProtos: []string{"moq-00", "h3"},

0 commit comments

Comments
 (0)