Skip to content
File

Blob: tun/client/keyless.go

go182 lines
1package client
2 
3import (
4 "context"
5 "crypto"
6 "crypto/tls"
7 "crypto/x509"
8 "errors"
9 "io"
10 "time"
11 
12 "go.miragespace.co/specter/spec/protocol"
13 "go.miragespace.co/specter/spec/rpc"
14 
15 "github.com/Yiling-J/theine-go"
16 "go.uber.org/zap"
17)
18 
19const (
20 cacheTotalCost = 1 << 21 // 2MiB
21 positiveTTL = time.Second * 60
22 failedTTL = time.Second * 10
23)
24 
25type keylessCertificateResult struct {
26 err error
27 cert *tls.Certificate
28}
29 
30type keylessSigner struct {
31 cli *Client
32 publicKey crypto.PublicKey
33 hostname string
34}
35 
36func (k *keylessSigner) Public() crypto.PublicKey {
37 return k.publicKey
38}
39 
40func (k *keylessSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
41 start := time.Now()
42 defer func() {
43 k.cli.Logger.Info("Keyless signing request",
44 zap.String("hostname", k.hostname),
45 zap.Duration("duration", time.Since(start)),
46 zap.Error(err),
47 )
48 }()
49 
50 proof, err := k.cli.obtainAcmeProof(k.hostname)
51 if err != nil {
52 return nil, err
53 }
54 
55 var hashAlg protocol.KeylessSignRequest_HashAlgorithm
56 switch opts.HashFunc() {
57 case crypto.SHA256:
58 hashAlg = protocol.KeylessSignRequest_SHA256
59 case crypto.SHA384:
60 hashAlg = protocol.KeylessSignRequest_SHA384
61 case crypto.SHA512:
62 hashAlg = protocol.KeylessSignRequest_SHA512
63 default:
64 hashAlg = protocol.KeylessSignRequest_UNKNOWN
65 }
66 
67 req := &protocol.KeylessSignRequest{
68 Proof: proof,
69 Hostname: k.hostname,
70 Digest: digest,
71 Algo: hashAlg,
72 }
73 
74 ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
75 defer cancel()
76 
77 resp, err := retryRPC(k.cli, ctx, func(node *protocol.Node) (*protocol.KeylessSignResponse, error) {
78 ctx = rpc.WithNode(ctx, node)
79 return k.cli.tunnelClient.Sign(ctx, req)
80 })
81 if err != nil {
82 return nil, err
83 }
84 
85 return resp.GetSignature(), nil
86}
87 
88var _ crypto.Signer = (*keylessSigner)(nil)
89 
90func (c *Client) keylesCertificateCacheLoader(ctx context.Context, hostname string) (ret theine.Loaded[keylessCertificateResult], loadErr error) {
91 start := time.Now()
92 defer func() {
93 c.Logger.Debug("Keyless certificate loader invoked",
94 zap.String("hostname", hostname),
95 zap.Duration("duration", time.Since(start)),
96 zap.Bool("err", ret.Value.err != nil),
97 zap.Int64("cost", ret.Cost),
98 zap.Duration("ttl", ret.TTL),
99 )
100 }()
101 
102 proof, err := c.obtainAcmeProof(hostname)
103 if err != nil {
104 return ret, err
105 }
106 
107 ctx, cancel := context.WithTimeout(ctx, rpcTimeout)
108 defer cancel()
109 
110 cert, err := retryRPC(c, ctx, func(node *protocol.Node) (*protocol.KeylessGetCertificateResponse, error) {
111 ctx = rpc.WithNode(ctx, node)
112 return c.tunnelClient.GetCertificate(ctx, &protocol.KeylessGetCertificateRequest{
113 Proof: proof,
114 Hostname: hostname,
115 })
116 })
117 if err != nil {
118 ret.Value.err = err
119 ret.TTL = failedTTL
120 return
121 }
122 
123 if len(cert.GetCertificates()) == 0 {
124 ret.Value.err = errors.New("no certificate returned by server")
125 ret.TTL = failedTTL
126 return
127 }
128 
129 leaf, err := x509.ParseCertificate(cert.GetCertificates()[0])
130 if err != nil {
131 ret.Value.err = err
132 ret.TTL = failedTTL
133 return
134 }
135 
136 pubKey, ok := leaf.PublicKey.(crypto.PublicKey)
137 if !ok {
138 ret.Value.err = errors.New("leaf public key is not a crypto.PublicKey")
139 ret.TTL = failedTTL
140 return
141 }
142 
143 signer := &keylessSigner{
144 cli: c,
145 hostname: hostname,
146 publicKey: pubKey,
147 }
148 
149 keylessCert := &tls.Certificate{
150 Certificate: cert.GetCertificates(),
151 PrivateKey: signer,
152 Leaf: leaf,
153 }
154 
155 ret.Value.cert = keylessCert
156 ret.TTL = positiveTTL
157 // Cost is only an approximation
158 ret.Cost = int64(len(cert.Certificates[0]) + len(leaf.Raw))
159 return
160}
161 
162func (c *Client) getCertificate(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
163 var (
164 ctx = context.Background()
165 hostname = chi.ServerName
166 )
167 if chi.Context() != nil {
168 ctx = chi.Context()
169 }
170 
171 ret, err := c.keylessCertificateCache.Get(ctx, hostname)
172 if ret.err != nil {
173 c.Logger.Error("error getting keyless certificate", zap.Error(ret.err))
174 return nil, ret.err
175 }
176 if err != nil {
177 c.Logger.Error("error getting keyless certificate", zap.Error(err))
178 return nil, err
179 }
180 return ret.cert, nil
181}