Skip to content

Commit

Permalink
Merge pull request #28 from Amnesic-Systems/add-app-launcher
Browse files Browse the repository at this point in the history
Add application launcher flag.
  • Loading branch information
NullHypothesis authored Nov 22, 2024
2 parents ba7a419 + e8120e9 commit fbac670
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 53 deletions.
4 changes: 2 additions & 2 deletions cmd/veil-verify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"github.com/Amnesic-Systems/veil/internal/enclave/nitro"
"github.com/Amnesic-Systems/veil/internal/enclave/noop"
"github.com/Amnesic-Systems/veil/internal/errs"
"github.com/Amnesic-Systems/veil/internal/httputil"
"github.com/Amnesic-Systems/veil/internal/httpx"
"github.com/Amnesic-Systems/veil/internal/nonce"
"github.com/Amnesic-Systems/veil/internal/util"
)
Expand Down Expand Up @@ -153,7 +153,7 @@ func attestEnclave(ctx context.Context, cfg *config) (err error) {
// Request the enclave's attestation document. We don't verify HTTPS
// certificates because authentication is happening via the attestation
// document.
client := httputil.NewNoAuthHTTPClient()
client := httpx.NewUnauthClient()
url := cfg.addr + "/enclave/attestation?nonce=" + nonce.URLEncode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
Expand Down
75 changes: 75 additions & 0 deletions cmd/veil/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"context"
"errors"
"flag"
Expand All @@ -9,13 +10,17 @@ import (
"log"
"net/url"
"os"
"os/exec"
"os/signal"
"strings"
"time"

"github.com/Amnesic-Systems/veil/internal/config"
"github.com/Amnesic-Systems/veil/internal/enclave"
"github.com/Amnesic-Systems/veil/internal/enclave/nitro"
"github.com/Amnesic-Systems/veil/internal/enclave/noop"
"github.com/Amnesic-Systems/veil/internal/errs"
"github.com/Amnesic-Systems/veil/internal/httpx"
"github.com/Amnesic-Systems/veil/internal/service"
"github.com/Amnesic-Systems/veil/internal/tunnel"
"github.com/Amnesic-Systems/veil/internal/util"
Expand All @@ -30,6 +35,11 @@ func parseFlags(out io.Writer, args []string) (*config.Config, error) {
fs := flag.NewFlagSet("veil", flag.ContinueOnError)
fs.SetOutput(out)

appCmd := fs.String(
"app-cmd",
"",
"command to run to invoke application",
)
appWebSrv := fs.String(
"app-web-srv",
"localhost:8081",
Expand Down Expand Up @@ -83,6 +93,7 @@ func parseFlags(out io.Writer, args []string) (*config.Config, error) {

// Build and validate the config.
return &config.Config{
AppCmd: *appCmd,
AppWebSrv: util.Must(url.Parse(*appWebSrv)),
Debug: *debug,
EnclaveCodeURI: *enclaveCodeURI,
Expand Down Expand Up @@ -120,6 +131,18 @@ func run(ctx context.Context, out io.Writer, args []string) (err error) {
return err
}

// Run the application command, if specified.
if cfg.AppCmd != "" {
go func() {
if err := eventuallyRunAppCmd(ctx, cfg, cfg.AppCmd); err != nil {
log.Printf("App unavailable: %v", err)
}
// Shut down the service if the app command has terminated,
// successfully or not.
cancel()
}()
}

// Initialize dependencies and start the service.
var attester enclave.Attester = nitro.NewAttester()
var tunneler tunnel.Mechanism = tunnel.NewVSOCK()
Expand All @@ -131,6 +154,58 @@ func run(ctx context.Context, out io.Writer, args []string) (err error) {
return nil
}

func eventuallyRunAppCmd(ctx context.Context, cfg *config.Config, cmd string) (err error) {
defer errs.Wrap(&err, "failed to run app command")

// Wait for the internal service to be ready.
deadlineCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second))
defer cancel()
url := fmt.Sprintf("http://localhost:%d", cfg.IntPort)
if err := httpx.WaitForSvc(deadlineCtx, httpx.NewUnauthClient(), url); err != nil {
return err
}
log.Print("Internal service ready; running app command.")

return runAppCmd(ctx, cmd)
}

func runAppCmd(ctx context.Context, cmdStr string) error {
args := strings.Split(cmdStr, " ")
cmd := exec.CommandContext(ctx, args[0], args[1:]...)

// Discard the enclave application's stdout and stderr. Regardless, we have
// to consume its output to prevent the application from blocking.
appStderr, err := cmd.StderrPipe()
if err != nil {
return err
}
appStdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
go forward(appStderr, io.Discard)
go forward(appStdout, io.Discard)

// Start the application and wait for it to terminate.
log.Println("Starting application.")
if err := cmd.Start(); err != nil {
return err
}
log.Println("Waiting for application to terminate.")
defer log.Println("Application terminated.")
return cmd.Wait()
}

func forward(from io.Reader, to io.Writer) {
s := bufio.NewScanner(from)
for s.Scan() {
fmt.Fprintln(to, s.Text())
}
if err := s.Err(); err != nil {
log.Printf("Error reading application output: %v", err)
}
}

func main() {
if err := run(context.Background(), os.Stdout, os.Args[1:]); err != nil {
log.Fatalf("Failed to run veil: %v", err)
Expand Down
104 changes: 66 additions & 38 deletions cmd/veil/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
Expand All @@ -20,10 +19,12 @@ import (
"time"

"github.com/Amnesic-Systems/veil/internal/addr"
"github.com/Amnesic-Systems/veil/internal/config"
"github.com/Amnesic-Systems/veil/internal/enclave"
"github.com/Amnesic-Systems/veil/internal/enclave/nitro"
"github.com/Amnesic-Systems/veil/internal/enclave/noop"
"github.com/Amnesic-Systems/veil/internal/httperr"
"github.com/Amnesic-Systems/veil/internal/httpx"
"github.com/Amnesic-Systems/veil/internal/nonce"
"github.com/Amnesic-Systems/veil/internal/service/attestation"
"github.com/Amnesic-Systems/veil/internal/testutil"
Expand All @@ -40,61 +41,48 @@ func withFlags(flag ...string) []string {
return append(f, flag...)
}

func waitForSvc(t *testing.T, url string) error {
func startSvc(t *testing.T, cfg []string) (
context.CancelFunc,
*sync.WaitGroup,
) {
var (
start = time.Now()
retry = time.NewTicker(5 * time.Millisecond)
deadline = time.Second
)

for range retry.C {
if _, err := testutil.Client.Get(url); err == nil {
return nil
}
if time.Since(start) > deadline {
t.Logf("Web server %s still unavailable after %v.", url, deadline)
return errors.New("timeout")
}
}

return nil
}

func startSvc(t *testing.T, cfg []string) func() {
var (
ctx, cancelCtx = context.WithCancel(context.Background())
wg = new(sync.WaitGroup)
f = func() {
cancelCtx()
wg.Wait()
}
ctx, cancel = context.WithCancel(context.Background())
wg = new(sync.WaitGroup)
)

wg.Add(1)
go func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
// run blocks until the context is cancelled.
assert.NoError(t, run(ctx, os.Stderr, cfg))
cancel()
}(ctx, wg)

// Block until the services are ready.
if err := waitForSvc(t, intSrv("/")); err != nil {
t.Logf("error waiting for service: %v", err)
return f
deadline, cancelDl := context.WithDeadline(ctx, time.Now().Add(time.Second))
defer cancelDl()
if err := httpx.WaitForSvc(deadline, httpx.NewUnauthClient(), intSrv("/")); err != nil {
t.Logf("error waiting for internal service: %v", err)
return cancel, wg
}
if !slices.Contains(cfg, "-wait-for-app") {
if err := waitForSvc(t, extSrv("/")); err != nil {
t.Logf("error waiting for service: %v", err)
return f
deadline, cancelDl := context.WithDeadline(ctx, time.Now().Add(time.Second))
defer cancelDl()
if err := httpx.WaitForSvc(deadline, httpx.NewUnauthClient(), extSrv("/")); err != nil {
t.Logf("error waiting for external service: %v", err)
return cancel, wg
}
}
return cancel, wg
}

// Return function that shuts down the service.
return f
func waitForSvc(_ context.CancelFunc, wg *sync.WaitGroup) {
wg.Wait()
}

func stopSvc(stop func()) {
stop()
func stopSvc(cancel context.CancelFunc, wg *sync.WaitGroup) {
cancel()
wg.Wait()
}

func intSrv(path string) string {
Expand Down Expand Up @@ -421,3 +409,43 @@ func TestReverseProxy(t *testing.T) {
})
}
}

func TestRunApp(t *testing.T) {
fd, err := os.CreateTemp("", "veil-test")
require.NoError(t, err)
defer os.Remove(fd.Name())

cases := []struct {
name string
command string
}{
{
name: "curl",
// Run curl to fetch veil's configuration from its external Web
// server.
command: fmt.Sprintf("curl --silent --insecure --output %s "+
"https://localhost:%d/enclave/config?nonce=%s",
fd.Name(),
defaultExtPort,
util.Must(nonce.New()).URLEncode(),
),
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
waitForSvc(startSvc(t, withFlags("-app-cmd", c.command, "-insecure")))

// Read curl's output, which should be our JSON-encoded
// configuration file.
content, err := io.ReadAll(fd)
require.NoError(t, err)

// Decode the configuration file and verify that the application
// command is identical to what we just ran.
var cfg config.Config
require.NoError(t, json.Unmarshal(content, &cfg))
require.Equal(t, c.command, cfg.AppCmd)
})
}
}
10 changes: 10 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ var _ = util.Validator(&Config{})

// Config represents the configuration of our enclave service.
type Config struct {
// AppCmd can be set to the command that starts the enclave application.
// For example:
//
// nc -l -p 1234
//
// Veil starts the given application after its internal Web server is
// running, and subsequently waits for the application to finish. When the
// application stops or crashes, veil terminates.
AppCmd string

// 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
Expand Down
51 changes: 45 additions & 6 deletions internal/httputil/httputil.go → internal/httpx/httpx.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package httputil
package httpx

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
Expand All @@ -10,6 +11,7 @@ import (
"encoding/base64"
"encoding/pem"
"errors"
"log"
"math/big"
"net/http"
"time"
Expand All @@ -24,9 +26,10 @@ const (
)

var (
errBadForm = errors.New("failed to parse POST form data")
errNoNonce = errors.New("could not find nonce in URL query parameters")
errBadNonceFormat = errors.New("unexpected nonce format; must be Base64 string")
errBadForm = errors.New("failed to parse POST form data")
errNoNonce = errors.New("could not find nonce in URL query parameters")
errBadNonceFormat = errors.New("unexpected nonce format; must be Base64 string")
errDeadlineExceeded = errors.New("deadline exceeded")
)

// ExtractNonce extracts a nonce from the HTTP request's parameters, e.g.:
Expand Down Expand Up @@ -56,11 +59,11 @@ func ExtractNonce(r *http.Request) (n *nonce.Nonce, err error) {
return n, nil
}

// NewNoAuthHTTPClient returns an HTTP client that skips HTTPS certificate
// NewUnauthClient returns an HTTP client that skips HTTPS certificate
// validation. In the context of veil, this is fine because all we need is a
// confidential channel; not an authenticated channel. Authentication is
// handled by the next layer, using attestation documents.
func NewNoAuthHTTPClient() *http.Client {
func NewUnauthClient() *http.Client {
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
Expand All @@ -72,6 +75,42 @@ func NewNoAuthHTTPClient() *http.Client {
}
}

// WaitForSvc waits for the service (specified by the URL) to become available
// by making repeated HTTP GET requests using the given HTTP client. This
// function blocks until 1) the service responds with an HTTP response or 2) the
// given context expires.
func WaitForSvc(
ctx context.Context,
client *http.Client,
url string,
) (err error) {
defer errs.Wrap(&err, "failed to wait for service")

start := time.Now()
deadline, ok := ctx.Deadline()
if !ok {
return errors.New("context has no deadline")
}

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return err
}
req = req.WithContext(ctx)

for {
log.Print("Making request to service...")
if _, err := client.Do(req); err == nil {
log.Print("Service is ready.")
return nil
}
if time.Since(start) > deadline.Sub(start) {
return errDeadlineExceeded
}
time.Sleep(10 * time.Millisecond)
}
}

// CreateCertificate creates a self-signed certificate and returns the
// PEM-encoded certificate and key. Some of the code below was taken from:
// https://eli.thegreenplace.net/2021/go-https-servers-with-tls/
Expand Down
Loading

0 comments on commit fbac670

Please sign in to comment.