main
go 91 lines 2.6 KB
Raw
1 package keyless
2
3 import (
4 "bytes"
5 "context"
6 "crypto/tls"
7 "errors"
8 "fmt"
9 "net/http"
10 "net/url"
11 "strings"
12
13 keylesstls "github.com/gosuda/keyless_tls/keyless"
14
15 "github.com/gosuda/portal-tunnel/v2/utils"
16 )
17
18 func BuildClientTLSConfig(relayURL, hostname string, echKeys []tls.EncryptedClientHelloKey, headers func() http.Header) (*tls.Config, ioCloser, error) {
19 normalizedRelayURL, err := utils.NormalizeRelayURL(relayURL)
20 if err != nil {
21 return nil, nil, err
22 }
23
24 parsed, err := url.Parse(normalizedRelayURL)
25 if err != nil {
26 return nil, nil, fmt.Errorf("parse relay url: %w", err)
27 }
28 serverName := parsed.Hostname()
29 if serverName == "" {
30 return nil, nil, errors.New("relay hostname is required")
31 }
32
33 certPEM, rootCAPEM, err := ResolveMaterials(context.Background(), normalizedRelayURL, serverName)
34 if err != nil {
35 return nil, nil, fmt.Errorf("prepare keyless materials: %w", err)
36 }
37 hostname = strings.TrimSpace(hostname)
38 if hostname == "" {
39 return nil, nil, errors.New("keyless hostname is required")
40 }
41 if verifyErr := VerifyCertificateHostname(certPEM, hostname); verifyErr != nil {
42 return nil, nil, fmt.Errorf("keyless certificate does not cover %s: %w", hostname, verifyErr)
43 }
44
45 remoteSigner, err := keylesstls.NewRemoteSigner(keylesstls.RemoteSignerConfig{
46 Endpoint: normalizedRelayURL,
47 ServerName: serverName,
48 KeyID: RelayKeyID,
49 RootCAPEM: rootCAPEM,
50 Headers: headers,
51 }, certPEM)
52 if err != nil {
53 return nil, nil, fmt.Errorf("create keyless remote signer: %w", err)
54 }
55
56 tlsConfig, err := keylesstls.NewServerTLSConfig(keylesstls.ServerTLSConfig{
57 CertPEM: certPEM,
58 Signer: remoteSigner,
59 NextProtos: []string{"http/1.1"},
60 MinVersion: MinTLSVersion(len(echKeys) > 0),
61 EncryptedClientHelloKeys: echKeys,
62 })
63 if err != nil {
64 _ = remoteSigner.Close()
65 return nil, nil, fmt.Errorf("create keyless tls config: %w", err)
66 }
67 return tlsConfig, remoteSigner, nil
68 }
69
70 type ioCloser interface {
71 Close() error
72 }
73
74 func ResolveMaterials(ctx context.Context, endpoint, serverName string) ([]byte, []byte, error) {
75 chainPEM, err := utils.FetchEndpointCertificateChain(ctx, endpoint, serverName)
76 if err != nil {
77 return nil, nil, fmt.Errorf("fetch signer certificate chain: %w", err)
78 }
79 if len(chainPEM) == 0 {
80 return nil, nil, errors.New("keyless certificate chain is required")
81 }
82 return bytes.Clone(chainPEM), bytes.Clone(chainPEM), nil
83 }
84
85 func VerifyCertificateHostname(certPEM []byte, hostname string) error {
86 leaf, err := utils.ParseCertificatePEM(certPEM)
87 if err != nil {
88 return err
89 }
90 return leaf.VerifyHostname(hostname)
91 }