-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathkbtls.go
More file actions
311 lines (251 loc) · 9.4 KB
/
Copy pathkbtls.go
File metadata and controls
311 lines (251 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// Package kbtls implements key-based TLS.
package kbtls
import (
"context"
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"fmt"
"math/big"
"net"
"time"
)
// ConnectionKey is a seed for an ed25519 private key with which the fundamental
// CA certificate is signed. The all-zero connection key is considered invalid
// in order to avoid accidentally using an uninitialized key. Due to the fixed
// size, connection keys are comparable.
type ConnectionKey [ed25519.SeedSize]byte
// ParseConnectionKey parses a base64-encoded connection key.
func ParseConnectionKey(key string) (ConnectionKey, error) {
var connectionKey ConnectionKey
if key == "" {
return connectionKey, fmt.Errorf("connection key is empty")
}
keyBytes, err := base64.RawStdEncoding.DecodeString(key)
if err != nil {
return connectionKey, fmt.Errorf("base64 decode: %w", err)
}
err = checkKeyBytes(keyBytes)
if err != nil {
return connectionKey, err
}
n := copy(connectionKey[:], keyBytes)
if n != ed25519.SeedSize { // just in case
return connectionKey, fmt.Errorf("only %d bytes were copied instead of %d", n, ed25519.SeedSize)
}
return connectionKey, nil
}
// GenerateConnectionKey generates a new connection key.
func GenerateConnectionKey() (ConnectionKey, error) {
var connectionKey ConnectionKey
maxAttempts := 10
for i := 0; i < maxAttempts; i++ {
n, err := rand.Read(connectionKey[:])
if err != nil {
return connectionKey, fmt.Errorf("read random bytes: %w", err)
}
if n != ed25519.SeedSize { // just in case
return connectionKey, fmt.Errorf("only %d bytes were generated instead of %d", n, ed25519.SeedSize)
}
if isZero(connectionKey[:]) {
continue
}
return connectionKey, nil
}
err := checkKeyBytes(connectionKey[:])
if err != nil {
return connectionKey, err
}
return connectionKey, fmt.Errorf("could not generate a valid non-zero connection key in %d attempts", maxAttempts)
}
// String returns the connection key as a base64-encoded string.
func (key ConnectionKey) String() string {
return base64.RawStdEncoding.EncodeToString(key[:])
}
// PublicKey returns the base64-encoded ed25519 public key that corresponds to the connection key.
func (key ConnectionKey) PublicKey() string {
//nolint:forcetypeassert
return base64.RawStdEncoding.EncodeToString(ed25519.NewKeyFromSeed(key[:]).Public().(ed25519.PublicKey))
}
// Valid returns falls if every byte in the connection key is zero.
func (key ConnectionKey) Valid() bool {
return !isZero(key[:])
}
// GenerateCA generates a deterministic CA certificate that never expires.
// Identical connection keys will always result in identical ceritificates.
func GenerateCA(key ConnectionKey) (caCert *x509.Certificate, caKey crypto.PrivateKey, err error) {
err = checkKeyBytes(key[:])
if err != nil {
return nil, nil, err
}
privateKey := ed25519.NewKeyFromSeed(key[:])
serialNumber := &big.Int{}
serialNumber.SetBytes(privateKey.Public().(ed25519.PublicKey)) //nolint:forcetypeassert
caCert = &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: key.PublicKey(),
},
NotBefore: time.Unix(0, 0),
NotAfter: time.Date(9999, 1, 1, 0, 0, 0, 0, time.FixedZone("", 0)),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
certBytes, err := x509.CreateCertificate(nil, caCert, caCert, privateKey.Public(), privateKey)
if err != nil {
return nil, nil, fmt.Errorf("generate certificate: %w", err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, nil, fmt.Errorf("parse certificate: %w", err)
}
return cert, privateKey, nil
}
// generateCertificate generates an x509 certificate.
func generateCertificate(
caCert *x509.Certificate, caKey crypto.PrivateKey, hostname string, usage x509.ExtKeyUsage,
) (pemCert []byte, pemKey []byte, err error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("generate certificate key: %w", err)
}
serialNumber := &big.Int{}
serialNumber.SetBytes(pubKey)
template := &x509.Certificate{
SerialNumber: serialNumber,
DNSNames: []string{hostname},
NotBefore: time.Unix(0, 0),
NotAfter: time.Date(9999, 1, 1, 0, 0, 0, 0, time.FixedZone("", 0)),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{usage},
BasicConstraintsValid: true,
}
derCert, err := x509.CreateCertificate(rand.Reader, template, caCert, pubKey, caKey)
if err != nil {
return nil, nil, fmt.Errorf("create client certificate: %w", err)
}
pkcs8Key, err := x509.MarshalPKCS8PrivateKey(privKey)
if err != nil {
return nil, nil, fmt.Errorf("marshal private key: %w", err)
}
pemCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derCert})
pemKey = pem.EncodeToMemory(&pem.Block{Type: "ED25519 PRIVATE KEY", Bytes: pkcs8Key})
return pemCert, pemKey, nil
}
// ServerTLSConfig generates a TLS server config based on the connection key.
// The server certificate will use the connection keys public key as server
// DNS name.
func ServerTLSConfig(key ConnectionKey) (*tls.Config, error) {
return ServerTLSConfigForServerName(key, key.PublicKey())
}
// ServerTLSConfigForServerName generates a TLS server config based on the
// connection key with the provided hostname in the server certificate's DNS
// name section.
func ServerTLSConfigForServerName(key ConnectionKey, hostname string) (*tls.Config, error) {
ca, caKey, err := GenerateCA(key)
if err != nil {
return nil, fmt.Errorf("generate CA: %w", err)
}
clientCAPool := x509.NewCertPool()
clientCAPool.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Raw}))
pemServerCert, pemServerKey, err := generateCertificate(ca, caKey, hostname, x509.ExtKeyUsageServerAuth)
if err != nil {
return nil, fmt.Errorf("generate server certificate: %w", err)
}
cert, err := tls.X509KeyPair(pemServerCert, pemServerKey)
if err != nil {
return nil, fmt.Errorf("load server certificate: %w", err)
}
cfg := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: clientCAPool,
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
}
return cfg, nil
}
// ClientTLSConfig generates a TLS client config based on the connection key.
// The client certificate's DNS name will be the connection keys's public key
// which is also set as ServerName in the returned *tls.Config.
func ClientTLSConfig(key ConnectionKey) (*tls.Config, error) {
return ClientTLSConfigForClientName(key, key.PublicKey())
}
// ClientTLSConfigForClientName generates a TLS client config for an arbitrary
// client DNS name. Note that the ServerName attribute is still set to the
// connection key's public key.
func ClientTLSConfigForClientName(key ConnectionKey, clientName string) (*tls.Config, error) {
ca, caKey, err := GenerateCA(key)
if err != nil {
return nil, fmt.Errorf("generate CA: %w", err)
}
pemClientCert, pemClientKey, err := generateCertificate(ca, caKey, clientName, x509.ExtKeyUsageClientAuth)
if err != nil {
return nil, fmt.Errorf("generate client certificate: %w", err)
}
clientCert, err := tls.X509KeyPair(pemClientCert, pemClientKey)
if err != nil {
return nil, fmt.Errorf("load client certificate: %w", err)
}
rootCAPool := x509.NewCertPool()
rootCAPool.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Raw}))
cfg := &tls.Config{
RootCAs: rootCAPool,
Certificates: []tls.Certificate{clientCert},
ServerName: key.PublicKey(),
MinVersion: tls.VersionTLS13,
}
return cfg, nil
}
// Dial works like tls.Dial with a TLS config based on the provided connection key.
func Dial(network string, address string, connectionKey string) (net.Conn, error) {
return DialContext(context.Background(), network, address, connectionKey)
}
// DialContext works like tls.Dial with a TLS config based on the provided connection key and a context.
func DialContext(ctx context.Context, network string, address string, connectionKey string) (net.Conn, error) {
key, err := ParseConnectionKey(connectionKey)
if err != nil {
return nil, fmt.Errorf("parse connection key: %w", err)
}
tlsConfig, err := ClientTLSConfig(key)
if err != nil {
return nil, fmt.Errorf("generate client TLS config: %w", err)
}
dialer := tls.Dialer{Config: tlsConfig}
return dialer.DialContext(ctx, network, address)
}
// Listen works like tls.Listen with a TLS config based on the provided connection key.
func Listen(network string, address, connectionKey string) (net.Listener, error) {
key, err := ParseConnectionKey(connectionKey)
if err != nil {
return nil, fmt.Errorf("parse connection key: %w", err)
}
tlsConfig, err := ServerTLSConfig(key)
if err != nil {
return nil, fmt.Errorf("generate server TLS config: %w", err)
}
return tls.Listen(network, address, tlsConfig)
}
func checkKeyBytes(key []byte) error {
if len(key) != ed25519.SeedSize {
return fmt.Errorf("key has only %d bytes instead of %d", len(key), ed25519.SeedSize)
}
if isZero(key) {
return fmt.Errorf("invalid all-zero connection key")
}
return nil
}
func isZero(s []byte) bool {
for _, v := range s {
if v != 0 {
return false
}
}
return true
}