This repository has been archived by the owner on May 2, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathenclave.go
459 lines (405 loc) · 14.3 KB
/
enclave.go
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
// Package nitriding implements a lightweight framework to build networked
// Go applications that run in AWS Nitro Enclaves.
package nitriding
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"log"
"math/big"
"net/http"
"net/http/httputil"
"net/url"
"os"
"sync"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/brave/nitriding/randseed"
"golang.org/x/crypto/acme/autocert"
)
const (
acmeCertCacheDir = "cert-cache"
certificateOrg = "AWS Nitro enclave application"
certificateValidity = time.Hour * 24 * 356
// parentCID determines the CID (analogous to an IP address) of the parent
// EC2 instance. According to the AWS docs, it is always 3:
// https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave-concepts.html
parentCID = 3
// The following paths are handled by nitriding.
pathRoot = "/enclave"
pathNonce = "/enclave/nonce"
pathAttestation = "/enclave/attestation"
pathState = "/enclave/state"
pathSync = "/enclave/sync"
pathHash = "/enclave/hash"
pathReady = "/enclave/ready"
// All other paths are handled by the enclave application's Web server if
// it exists.
pathProxy = "/*"
)
var (
elog = log.New(os.Stderr, "nitriding: ", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile)
inEnclave = false
errNoKeyMaterial = errors.New("no key material registered")
errCfgMissingFQDN = errors.New("given config is missing FQDN")
errCfgMissingPort = errors.New("given config is missing port")
)
// Enclave represents a service running inside an AWS Nitro Enclave.
type Enclave struct {
sync.RWMutex
cfg *Config
pubSrv, privSrv http.Server
revProxy *httputil.ReverseProxy
hashes *AttestationHashes
nonceCache *cache
keyMaterial any
ready, stop chan bool
}
// Config represents the configuration of our enclave service.
type Config struct {
// FQDN contains the fully qualified domain name that's set in the HTTPS
// certificate of the enclave's Web server, e.g. "example.com". This field
// is required.
FQDN string
// ExtPort contains the VSOCK-facing TCP port that the Web server should
// listen on, e.g. 443. This port is not *directly* reachable by the
// Internet but the EC2 host's proxy *does* forward Internet traffic to
// this port. This field is required.
ExtPort uint16
// IntPort contains the enclave-internal TCP port of the Web server that
// provides an HTTP API to the enclave application. This field is
// required.
IntPort uint16
// HostProxyPort indicates the TCP port of the proxy application running on
// the EC2 host. Note that VSOCK ports are 32 bits large. This field is
// required.
HostProxyPort uint32
// UseACME must be set to true if you want your enclave application to
// request a Let's Encrypt-signed certificate. If this is set to false,
// the enclave creates a self-signed certificate.
UseACME bool
// Debug can be set to true to see debug messages, i.e., if you are
// starting the enclave in debug mode by running:
//
// nitro-cli run-enclave --debug-mode ....
//
// Do not set this to true in production because printing debug messages
// for each HTTP request slows down the enclave application, and you are
// not able to see debug messages anyway unless you start the enclave using
// nitro-cli's "--debug-mode" flag.
Debug bool
// FdCur and FdMax set the soft and hard resource limit, respectively. The
// default for both variables is 65536.
FdCur uint64
FdMax uint64
// AppURL should be set to the URL of the software repository that's
// running inside the enclave, e.g., "https://github.com/foo/bar". The URL
// is shown on the enclave's index page, as part of instructions on how to
// do remote attestation.
AppURL *url.URL
// AppWebSrv should be set to the enclave-internal Web server of the
// enclave application, e.g., "http://127.0.0.1:8080". Nitriding acts as a
// TLS-terminating reverse proxy and forwards incoming HTTP requests to
// this Web server. Note that this configuration option is only necessary
// if the enclave application exposes an HTTP server. Non-HTTP enclave
// applications can ignore this.
AppWebSrv *url.URL
// WaitForApp instructs nitriding to wait for the application's signal
// before launching the Internet-facing Web server. Set this flag if your
// application takes a while to bootstrap and you don't want to risk
// inconsistent state when syncing, or unexpected attestation documents.
// If set, your application must make the following request when ready:
//
// GET http://127.0.0.1:{IntPort}/enclave/ready
WaitForApp bool
}
// Validate returns an error if required fields in the config are not set.
func (c *Config) Validate() error {
if c.ExtPort == 0 || c.IntPort == 0 || c.HostProxyPort == 0 {
return errCfgMissingPort
}
if c.FQDN == "" {
return errCfgMissingFQDN
}
return nil
}
// init is called once, at package initialization time.
func init() {
var err error
// Determine if we're inside an enclave. Abort execution in the unexpected
// case that we cannot tell.
inEnclave, err = randseed.InEnclave()
if err != nil {
elog.Fatalf("Failed to determine if we're inside an enclave: %v", err)
}
elog.Printf("We're running inside an enclave: %v", inEnclave)
}
// NewEnclave creates and returns a new enclave with the given config.
func NewEnclave(cfg *Config) (*Enclave, error) {
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("failed to create enclave: %w", err)
}
e := &Enclave{
cfg: cfg,
pubSrv: http.Server{
Addr: fmt.Sprintf(":%d", cfg.ExtPort),
Handler: chi.NewRouter(),
},
privSrv: http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", cfg.IntPort),
Handler: chi.NewRouter(),
},
nonceCache: newCache(defaultItemExpiry),
hashes: new(AttestationHashes),
stop: make(chan bool),
ready: make(chan bool),
}
// Increase the maximum number of idle connections per host. This is
// critical to boosting the requests per second that our reverse proxy can
// sustain. See the following comment for more details:
// https://github.com/brave/nitriding/issues/45#issuecomment-1526012586
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100
http.DefaultTransport.(*http.Transport).MaxIdleConns = 100
if cfg.Debug {
e.pubSrv.Handler.(*chi.Mux).Use(middleware.Logger)
e.privSrv.Handler.(*chi.Mux).Use(middleware.Logger)
}
// Register public HTTP API.
m := e.pubSrv.Handler.(*chi.Mux)
m.Get(pathAttestation, attestationHandler(e.hashes))
m.Get(pathNonce, nonceHandler(e))
m.Get(pathRoot, rootHandler(e.cfg))
m.Post(pathSync, respSyncHandler(e))
// Register enclave-internal HTTP API.
m = e.privSrv.Handler.(*chi.Mux)
m.Get(pathSync, reqSyncHandler(e))
m.Get(pathReady, readyHandler(e))
m.Get(pathState, getStateHandler(e))
m.Put(pathState, putStateHandler(e))
m.Post(pathHash, hashHandler(e))
// Configure our reverse proxy if the enclave application exposes an HTTP
// server.
if cfg.AppWebSrv != nil {
e.revProxy = httputil.NewSingleHostReverseProxy(cfg.AppWebSrv)
e.revProxy.BufferPool = newBufPool()
e.pubSrv.Handler.(*chi.Mux).Handle(pathProxy, e.revProxy)
}
return e, nil
}
// Start starts the Nitro Enclave. If something goes wrong, the function
// returns an error.
func (e *Enclave) Start() error {
var err error
errPrefix := "failed to start Nitro Enclave"
// Set file descriptor limit. There's no need to exit if this fails.
if inEnclave {
if err = setFdLimit(e.cfg.FdCur, e.cfg.FdMax); err != nil {
elog.Printf("Failed to set new file descriptor limit: %s", err)
}
if err = configureLoIface(); err != nil {
return fmt.Errorf("%s: %w", errPrefix, err)
}
}
// Set up our networking environment which creates a TAP device that
// forwards traffic (via the VSOCK interface) to the EC2 host.
go runNetworking(e.cfg, e.stop)
// Get an HTTPS certificate.
if e.cfg.UseACME {
err = e.setupAcme()
} else {
err = e.genSelfSignedCert()
}
if err != nil {
return fmt.Errorf("%s: failed to create certificate: %w", errPrefix, err)
}
if err = startWebServers(e); err != nil {
return fmt.Errorf("%s: %w", errPrefix, err)
}
return nil
}
// Stop stops the enclave.
func (e *Enclave) Stop() error {
close(e.stop)
if err := e.privSrv.Shutdown(context.Background()); err != nil {
return err
}
if err := e.pubSrv.Shutdown(context.Background()); err != nil {
return err
}
return nil
}
// startWebServers starts both our public-facing and our enclave-internal Web
// server in a goroutine.
func startWebServers(e *Enclave) error {
elog.Printf("Starting public (%s) and private (%s) Web server.", e.pubSrv.Addr, e.privSrv.Addr)
go e.privSrv.ListenAndServe() //nolint:errcheck
go func() {
// If desired, don't launch our Internet-facing Web server until the
// application signalled that it's ready.
if e.cfg.WaitForApp {
<-e.ready
elog.Println("Application signalled that it's ready. Starting public Web server.")
}
e.pubSrv.ListenAndServeTLS("", "") //nolint:errcheck
}()
return nil
}
// genSelfSignedCert creates and returns a self-signed TLS certificate based on
// the given FQDN. Some of the code below was taken from:
// https://eli.thegreenplace.net/2021/go-https-servers-with-tls/
func (e *Enclave) genSelfSignedCert() error {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
elog.Println("Generated private key for self-signed certificate.")
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return err
}
elog.Println("Generated serial number for self-signed certificate.")
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{certificateOrg},
},
DNSNames: []string{e.cfg.FQDN},
NotBefore: time.Now(),
NotAfter: time.Now().Add(certificateValidity),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return err
}
elog.Println("Created certificate from template.")
pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if pemCert == nil {
return errors.New("failed to encode certificate to PEM")
}
// Determine and set the certificate's fingerprint because we need to add
// the fingerprint to our Nitro attestation document.
if err := e.setCertFingerprint(pemCert); err != nil {
return err
}
privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
elog.Fatalf("Unable to marshal private key: %v", err)
}
pemKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
if pemKey == nil {
elog.Fatal("Failed to encode key to PEM.")
}
cert, err := tls.X509KeyPair(pemCert, pemKey)
if err != nil {
return err
}
e.pubSrv.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}
return nil
}
// setupAcme attempts to retrieve an HTTPS certificate from Let's Encrypt for
// the given FQDN. Note that we are unable to cache certificates across
// enclave restarts, so the enclave requests a new certificate each time it
// starts. If the restarts happen often, we may get blocked by Let's Encrypt's
// rate limiter for a while.
func (e *Enclave) setupAcme() error {
var err error
elog.Printf("ACME hostname set to %s.", e.cfg.FQDN)
// By default, we use an in-memory certificate cache. We only use the
// directory cache when we're *not* in an enclave. There's no point in
// writing certificates to disk when in an enclave because the disk does
// not persist when the enclave shuts down. Besides, dealing with file
// permissions makes it more complicated to switch to an unprivileged user
// ID before execution.
var cache autocert.Cache = newCertCache()
if !inEnclave {
cache = autocert.DirCache(acmeCertCacheDir)
}
certManager := autocert.Manager{
Cache: cache,
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist([]string{e.cfg.FQDN}...),
}
e.pubSrv.TLSConfig = certManager.TLSConfig()
go func() {
var rawData []byte
for {
// Get the SHA-1 hash over our leaf certificate.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
rawData, err = cache.Get(ctx, e.cfg.FQDN)
if err != nil {
time.Sleep(5 * time.Second)
} else {
elog.Print("Got certificates from cache. Proceeding with start.")
break
}
}
if err := e.setCertFingerprint(rawData); err != nil {
elog.Fatalf("Failed to set certificate fingerprint: %s", err)
}
}()
return nil
}
// setCertFingerprint takes as input a PEM-encoded certificate and extracts its
// SHA-256 fingerprint. We need the certificate's fingerprint because we embed
// it in attestation documents, to bind the enclave's certificate to the
// attestation document.
func (e *Enclave) setCertFingerprint(rawData []byte) error {
rest := []byte{}
for rest != nil {
block, rest := pem.Decode(rawData)
if block == nil {
return errors.New("pem.Decode failed because it didn't find PEM data in the input we provided")
}
if block.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
if !cert.IsCA {
e.hashes.tlsKeyHash = sha256.Sum256(cert.Raw)
elog.Printf("Set SHA-256 fingerprint of server's certificate to: %x",
e.hashes.tlsKeyHash[:])
return nil
}
}
rawData = rest
}
return nil
}
// SetKeyMaterial registers the enclave's key material (e.g., secret encryption
// keys) as being ready to be synchronized to other, identical enclaves. Note
// that the key material's underlying data structure must be marshallable to
// JSON.
//
// This is only necessary if you intend to scale enclaves horizontally. If you
// will only ever run a single enclave, ignore this function.
func (e *Enclave) SetKeyMaterial(keyMaterial any) {
e.Lock()
defer e.Unlock()
e.keyMaterial = keyMaterial
}
// KeyMaterial returns the key material or, if none was registered, an error.
func (e *Enclave) KeyMaterial() (any, error) {
e.RLock()
defer e.RUnlock()
if e.keyMaterial == nil {
return nil, errNoKeyMaterial
}
return e.keyMaterial, nil
}