Skip to content
This repository has been archived by the owner on Nov 17, 2024. It is now read-only.

Support loading an enclave application at runtime. #13

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions app_loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"log"
"net/http"
"os/exec"
"time"
)

const (
appPath = "/tmp/enclave-application" // Where to store the enclave application.
)

// appRetriever implements an interface that retrieves an enclave application at
// runtime. This allows us to retrieve the application via various mechanisms,
// like a Web API or a Docker registry.
type appRetriever interface {
retrieve(*http.Server, chan []byte) error
}

// appLoader implements a mechanism that can retrieve and execute an enclave
// application at runtime.
type appLoader struct {
srv *http.Server
log transparencyLog
app chan []byte
appExited chan struct{}
appRetriever
}

// newAppLoader returns a new appLoader object.
func newAppLoader(srv *http.Server, r appRetriever) *appLoader {
return &appLoader{
srv: srv,
log: new(memLog),
app: make(chan []byte),
appExited: make(chan struct{}),
appRetriever: r,
}
}

// runCmd runs the enclave application. The function blocks for as long as the
// application is running.
func (l *appLoader) runCmd() {
cmd := exec.Command(appPath)
err := cmd.Run()
elog.Printf("Enclave application exited: %v", err)
l.appExited <- struct{}{}
}

// appendToLog appends the given digest to our append-only log.
func (l *appLoader) appendToLog(app []byte) {
l.log.append(newSHA256LogRecord(app))
}

// start executes the enclave application.
func (l *appLoader) start(stop chan struct{}) {
var (
err error
)
elog.Println("Starting app loader event loop.")
defer elog.Println("Stopping app loader event loop.")

go l.retrieve(l.srv, l.app)
go func() {
for {
select {
case <-stop:
return
case <-l.appExited:
time.Sleep(time.Second)
elog.Println(l.log)
go l.runCmd()

case app := <-l.app:
if err = writeToDisk(app); err != nil {
log.Fatalf("Error writing enclave application to disk: %v", err)
}
l.appendToLog(app)
go l.runCmd()
}
}
}()
}
27 changes: 27 additions & 0 deletions app_loader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"net/http"
"testing"
)

type appRetrieverDummy struct{}

func (d *appRetrieverDummy) retrieve(srv *http.Server, appChan chan []byte) error {
//var once sync.Once
go func(appChan chan []byte) {
// once.Do()
// sync.Once()
appChan <- []byte("") // Dummy application.
}(appChan)
return nil
}

func TestStartStop(t *testing.T) {
var (
loader = newAppLoader(nil, new(appRetrieverDummy))
stop = make(chan struct{})
)
defer close(stop)
loader.start(stop)
}
33 changes: 33 additions & 0 deletions app_loader_webapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"io"
"net/http"

"github.com/go-chi/chi/v5"
)

// appRetrieverViaWeb installs a PUT endpoint that is used to upload the enclave
// application at runtime.
type appRetrieverViaWeb struct{}

func newAppRetrieverViaWeb() *appRetrieverViaWeb {
return new(appRetrieverViaWeb)
}

func (l *appRetrieverViaWeb) retrieve(srv *http.Server, appChan chan []byte) error {
srv.Handler.(*chi.Mux).Put(pathApp, func(w http.ResponseWriter, r *http.Request) {
const maxAppLen = 1024 * 1024 * 50 // 50 MiB.

app, err := io.ReadAll(newLimitReader(r.Body, maxAppLen))
if err != nil {
http.Error(w, errFailedReqBody.Error(), http.StatusInternalServerError)
return
}
elog.Printf("Received %d-byte enclave application.", len(app))
appChan <- app
w.WriteHeader(http.StatusOK)
})
elog.Printf("Installed HTTP handler %s to receive enclave application.", pathApp)
return nil
}
34 changes: 24 additions & 10 deletions internal/enclave.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,18 @@ const (
// https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave-concepts.html
parentCID = 3
// The following paths are handled by nitriding.
pathRoot = "/enclave"
pathAttestation = "/enclave/attestation"
pathState = "/enclave/state"
pathSync = "/enclave/sync"
pathHash = "/enclave/hash"
pathReady = "/enclave/ready"
pathProfiling = "/enclave/debug"
pathConfig = "/enclave/config"
pathLeader = "/enclave/leader"
pathHeartbeat = "/enclave/heartbeat"
pathRoot = "/enclave"
pathAttestation = "/enclave/attestation"
pathState = "/enclave/state"
pathSync = "/enclave/sync"
pathHash = "/enclave/hash"
pathReady = "/enclave/ready"
pathProfiling = "/enclave/debug"
pathConfig = "/enclave/config"
pathLeader = "/enclave/leader"
pathHeartbeat = "/enclave/heartbeat"
pathTransparencyLog = "/enclave/log"
pathApp = "/enclave/app"
// All other paths are handled by the enclave application's Web server if
// it exists.
pathProxy = "/*"
Expand Down Expand Up @@ -78,11 +80,14 @@ type Enclave struct {
workers *workerManager
keys *enclaveKeys
httpsCert *certRetriever
loader *appLoader
ready, stop chan struct{}
}

// Config represents the configuration of our enclave service.
type Config struct {
Loader bool

// 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.
Expand Down Expand Up @@ -277,12 +282,16 @@ func NewEnclave(cfg *Config) (*Enclave, error) {
if cfg.isScalingEnabled() {
e.setSyncState(inProgress)
}
if cfg.Loader {
e.loader = newAppLoader(e.extPubSrv, newAppRetrieverViaWeb())
}

// Register external public HTTP API.
m := e.extPubSrv.Handler.(*chi.Mux)
m.Get(pathAttestation, attestationHandler(e.cfg.UseProfiling, e.hashes, e.attester))
m.Get(pathRoot, rootHandler(e.cfg))
m.Get(pathConfig, configHandler(e.cfg))
m.Get(pathTransparencyLog, transparencyLogHandler(e.loader.log))

// Register external but private HTTP API.
m = e.extPrivSrv.Handler.(*chi.Mux)
Expand Down Expand Up @@ -352,6 +361,11 @@ func (e *Enclave) Start() error {
return fmt.Errorf("%s: %w", errPrefix, err)
}

// TODO: Does this play well with key synchronization?
if e.cfg.Loader {
e.loader.start(e.stop)
}

if !e.cfg.isScalingEnabled() {
return nil
}
Expand Down
8 changes: 8 additions & 0 deletions internal/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,11 @@ func getLeaderHandler(ourNonce nonce, weAreLeader chan struct{}) http.HandlerFun
w.WriteHeader(http.StatusOK)
}
}

// transparencyLogHandler prints the transparency log of all previously-deployed
// enclave applications in human-readable form.
func transparencyLogHandler(log transparencyLog) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, log)
}
}
7 changes: 7 additions & 0 deletions internal/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"os"
"strings"
"time"

"golang.org/x/sys/unix"
)

const (
Expand Down Expand Up @@ -260,3 +262,8 @@ func makeLeaderRequest(leader *url.URL, ourNonce nonce, areWeLeader chan bool, e
}
errChan <- fmt.Errorf("leader designation endpoint returned %d", resp.StatusCode)
}

func writeToDisk(appBlob []byte) error {
_ = unix.Umask(0)
return os.WriteFile(appPath, appBlob, 0755)
}
10 changes: 9 additions & 1 deletion internal/util_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package internal

import "testing"
import (
"os"
"testing"
)

func TestSliceToNonce(t *testing.T) {
var err error
Expand All @@ -11,3 +14,8 @@ func TestSliceToNonce(t *testing.T) {
_, err = sliceToNonce(make([]byte, nonceLen))
assertEqual(t, err, nil)
}

func TestWriteToDisk(t *testing.T) {
assertEqual(t, writeToDisk([]byte("foo")), nil)
defer os.Remove(appPath)
}
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var (
func main() {
var fqdn, fqdnLeader, appURL, appWebSrv, appCmd, prometheusNamespace, mockCertFp string
var extPubPort, extPrivPort, intPort, hostProxyPort, prometheusPort uint
var useACME, waitForApp, useProfiling, useVsockForExtPort, disableKeepAlives, debug bool
var useACME, waitForApp, useProfiling, useVsockForExtPort, disableKeepAlives, debug, loader bool
var err error

flag.StringVar(&fqdn, "fqdn", "",
Expand Down Expand Up @@ -61,6 +61,8 @@ func main() {
"Print extra debug messages and use dummy attester for testing outside enclaves.")
flag.StringVar(&mockCertFp, "mock-cert-fp", "",
"Mock certificate fingerprint to use in attestation documents (hexadecimal)")
flag.BoolVar(&loader, "loader", false,
"Dynamically load enclave application.")
flag.Parse()

if fqdn == "" {
Expand Down Expand Up @@ -101,6 +103,7 @@ func main() {
UseProfiling: useProfiling,
MockCertFp: mockCertFp,
Debug: debug,
Loader: loader,
}
if appURL != "" {
u, err := url.Parse(appURL)
Expand Down
76 changes: 76 additions & 0 deletions memory_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"crypto/sha256"
"fmt"
"sync"
"time"
)

// transparencyLog implements an interface for an append-only data structure
// that serves as a transparency log for enclave image IDs.
type transparencyLog interface {
append(*logRecord) error
String() string // human-readable representation
}

// logRecord represents a single record in our transparency log.
type logRecord struct {
digestType byte
digestSize byte
digest []byte
time time.Time
}

// newSHA256LogRecord creates a new logRecord that uses SHA-2-256 for the given
// byte blob.
func newSHA256LogRecord(blob []byte) *logRecord {
digest := sha256.Sum256(blob)
return &logRecord{
digestType: 0x12, // SHA-2-256.
digestSize: 0x20, // 32 bytes, in the "variable integer" multiformat.
digest: digest[:],
time: time.Now(),
}
}

// String returns a string representation of the log record.
func (r *logRecord) String() string {
return fmt.Sprintf("%s: %x (type=%x)\n", r.time.Format(time.RFC3339), r.digest, r.digestType)
}

// memLog implements a transparencyLog in memory.
type memLog struct {
sync.Mutex
log []*logRecord
}

// append appends the given logRecord to the memory log.
func (m *memLog) append(r *logRecord) error {
m.Lock()
defer m.Unlock()

m.log = append(m.log, r)
elog.Printf("Appended %s to transparency log of new size %d.", r, len(m.log))
return nil
}

// size returns the memory log's size.
func (m *memLog) size() int {
m.Lock()
defer m.Unlock()

return len(m.log)
}

// String returns a string representation of the memory log.
func (m *memLog) String() string {
m.Lock()
defer m.Unlock()

var s string
for _, r := range m.log {
s += r.String()
}
return s
}
Loading