diff --git a/cmd/veil-verify/main.go b/cmd/veil-verify/main.go index ea5d69a..be84a65 100644 --- a/cmd/veil-verify/main.go +++ b/cmd/veil-verify/main.go @@ -1,8 +1,12 @@ package main import ( + "bytes" "context" + "crypto/sha512" + "encoding/hex" "encoding/json" + "errors" "flag" "fmt" "io" @@ -10,6 +14,7 @@ import ( "net/http" "os" "os/signal" + "strings" "github.com/Amnesic-Systems/veil/internal/enclave" "github.com/Amnesic-Systems/veil/internal/enclave/nitro" @@ -17,14 +22,19 @@ import ( "github.com/Amnesic-Systems/veil/internal/errs" "github.com/Amnesic-Systems/veil/internal/httputil" "github.com/Amnesic-Systems/veil/internal/nonce" + "github.com/Amnesic-Systems/veil/internal/util" ) type config struct { - Addr string - Testing bool + addr string + verbose bool + testing bool + pcrs enclave.PCR } -func parseFlags(out io.Writer, args []string) (*config, error) { +func parseFlags(out io.Writer, args []string) (_ *config, err error) { + defer errs.Wrap(&err, "failed to parse flags") + fs := flag.NewFlagSet("veil-verify", flag.ContinueOnError) fs.SetOutput(out) @@ -33,19 +43,84 @@ func parseFlags(out io.Writer, args []string) (*config, error) { "", "Address of the enclave, e.g.: https://example.com:8443", ) + pcrs := fs.String( + "pcrs", + "", + "JSON-encoded enclave image measurements as emitted by 'nitro-cli build'", + ) + verbose := fs.Bool( + "verbose", + false, + "Enable verbose logging", + ) testing := fs.Bool( "insecure", false, "Enable testing by disabling attestation", ) - if err := fs.Parse(args); err != nil { - return nil, fmt.Errorf("failed to parse flags: %w", err) + return nil, err + } + + // Ensure that required arguments are set. + if *addr == "" { + return nil, errors.New("flag -addr must be provided") + } + if *pcrs == "" { + return nil, errors.New("flag -pcrs must be provided") + } + + // Convert the given JSON-encoded enclave measurements to a PCR struct. + pcr, err := toPCR([]byte(*pcrs)) + if err != nil { + return nil, err } return &config{ - Addr: *addr, - Testing: *testing, + addr: *addr, + testing: *testing, + verbose: *verbose, + pcrs: pcr, + }, nil +} + +func toPCR(jsonMsmts []byte) (_ enclave.PCR, err error) { + defer errs.Wrap(&err, "failed to convert measurements to PCR") + + // This structs represents the JSON-encoded measurements of the enclave + // image. The JSON tags must match the output of the nitro-cli command + // line tool. An example: + // + // { + // "Measurements": { + // "HashAlgorithm": "Sha384 { ... }", + // "PCR0": "8b927cf0bbf2d668a8c24c69afd23bff2dda713b4f0d70195205950f9a5a1fbb7089ad937e3025ee8d5a084f3d6c9126", + // "PCR1": "4b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493", + // "PCR2": "22d2194eb27a7cda42e66dd5b91ef13e5a153d797c04ae179e59bef1c93438d6ad0365c175c119230e36d0f8d6b6b59e" + // } + // } + m := struct { + Measurements struct { + HashAlgorithm string `json:"HashAlgorithm"` + PCR0 string `json:"PCR0"` + PCR1 string `json:"PCR1"` + PCR2 string `json:"PCR2"` + } `json:"Measurements"` + }{} + if err := json.Unmarshal(jsonMsmts, &m); err != nil { + return nil, err + } + + const want = "sha384" + got := strings.ToLower(m.Measurements.HashAlgorithm) + if !strings.HasPrefix(got, want) { + return nil, fmt.Errorf("expected hash algorithm %q but got %q", want, got) + } + + return enclave.PCR{ + 0: util.Must(hex.DecodeString(m.Measurements.PCR0)), + 1: util.Must(hex.DecodeString(m.Measurements.PCR1)), + 2: util.Must(hex.DecodeString(m.Measurements.PCR2)), }, nil } @@ -57,26 +132,23 @@ func run(ctx context.Context, out *os.File, args []string) error { if err != nil { return err } - if cfg.Addr == "" { - return fmt.Errorf("missing addr argument") - } - return attestEnclave(ctx, cfg) } func attestEnclave(ctx context.Context, cfg *config) (err error) { defer errs.Wrap(&err, "failed to attest enclave") + // Generate a nonce to ensure that the attestation document is fresh. nonce, err := nonce.New() if err != nil { return err } - // Request the enclave's attestation document. We don't care about HTTPS + // Request the enclave's attestation document. We don't verify HTTPS // certificates because authentication is happening via the attestation // document. client := httputil.NewNoAuthHTTPClient() - url := cfg.Addr + "/enclave/attestation?nonce=" + nonce.URLEncode() + url := cfg.addr + "/enclave/attestation?nonce=" + nonce.URLEncode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err @@ -95,18 +167,45 @@ func attestEnclave(ctx context.Context, cfg *config) (err error) { return err } defer resp.Body.Close() - var doc enclave.RawDocument - if err := json.Unmarshal(body, &doc); err != nil { + var rawDoc enclave.RawDocument + if err := json.Unmarshal(body, &rawDoc); err != nil { return err } - // Finally, verify the attestation document. + // Verify the attestation document, which provides assurance that we are + // talking to an enclave. The nonce provides assurance that we are talking + // to an alive enclave (instead of a replayed attestation document). var attester enclave.Attester = nitro.NewAttester() - if cfg.Testing { + if cfg.testing { attester = noop.NewAttester() } - _, err = attester.Verify(&doc, nonce) - return err + doc, err := attester.Verify(&rawDoc, nonce) + if err != nil { + return err + } + + // Delete empty PCR values from the attestation document. This is not + // ideal; we should either have the rest of the code tolerate empty PCR + // values or fix the nsm package, so it doesn't return empty PCR values. + empty := make([]byte, sha512.Size384) + for i, pcr := range doc.PCRs { + if bytes.Equal(pcr, empty) { + delete(doc.PCRs, i) + } + } + + // Verify the attestation document's PCR values, which provide assurance + // that the remote enclave's image and kernel match the local copy. + if !cfg.pcrs.Equal(doc.PCRs) { + log.Println("Enclave's code DOES NOT match local code!") + if cfg.verbose { + log.Printf("Expected\n%sbut got\n%s", cfg.pcrs, doc.PCRs) + } + } else { + log.Println("Enclave's code matches local code!") + } + + return nil } func main() { @@ -114,5 +213,4 @@ func main() { if err := run(ctx, os.Stdout, os.Args[1:]); err != nil { log.Fatalf("Failed to run verifier: %v", err) } - log.Println("Attestation successful") } diff --git a/internal/enclave/attester.go b/internal/enclave/attester.go index e5c632b..fa435df 100644 --- a/internal/enclave/attester.go +++ b/internal/enclave/attester.go @@ -25,7 +25,7 @@ type Document struct { ModuleID string `cbor:"module_id" json:"module_id"` Timestamp uint64 `cbor:"timestamp" json:"timestamp"` Digest string `cbor:"digest" json:"digest"` - PCRs pcr `cbor:"pcrs" json:"pcrs"` + PCRs PCR `cbor:"pcrs" json:"pcrs"` Certificate []byte `cbor:"certificate" json:"certificate"` CABundle [][]byte `cbor:"cabundle" json:"cabundle"` AuxInfo diff --git a/internal/enclave/nitro/verifier.go b/internal/enclave/nitro/verifier.go index d393996..0f3edd6 100644 --- a/internal/enclave/nitro/verifier.go +++ b/internal/enclave/nitro/verifier.go @@ -33,7 +33,6 @@ import ( "crypto/sha256" "crypto/sha512" "crypto/x509" - "encoding/json" "errors" "fmt" "math/big" @@ -330,12 +329,6 @@ func verify(data []byte, options verifyOptions) (_ *Result, err error) { return nil, errors.New("payload's signature does not match signature from certificate") } - b, err := json.Marshal(doc.PCRs) - if err != nil { - return nil, err - } - fmt.Println(string(b)) - return &Result{ Document: &doc, Certificates: certificates, diff --git a/internal/enclave/pcr.go b/internal/enclave/pcr.go index d4925ed..c7c9991 100644 --- a/internal/enclave/pcr.go +++ b/internal/enclave/pcr.go @@ -3,16 +3,17 @@ package enclave import ( "bytes" "crypto/sha512" + "fmt" ) var emptyPCR = make([]byte, sha512.Size384) -// pcr represents the enclave's platform configuration register (PCR) values. -type pcr map[uint][]byte +// PCR represents the enclave's platform configuration register (PCR) values. +type PCR map[uint][]byte // FromDebugMode returns true if the given PCR map was generated by an enclave // in debug mode. -func (p pcr) FromDebugMode() bool { +func (p PCR) FromDebugMode() bool { // PCRs 0, 1, and 2 are set at compile time but will be unset if the enclave // is running in debug mode. return bytes.Equal(p[0], emptyPCR) && @@ -20,17 +21,21 @@ func (p pcr) FromDebugMode() bool { bytes.Equal(p[2], emptyPCR) } -// Equal returns true if (and only if) the two given PCR maps are identical. -func (ours pcr) Equal(theirs pcr) bool { - if len(ours) != len(theirs) { - return false +// String returns a human-readable representation of the PCR map. +func (p PCR) String() string { + s := "" + for key, val := range p { + s += fmt.Sprintf("%d: %x\n", key, val) } + return s +} +// Equal returns true if (and only if) the two given PCR maps are identical. +func (ours PCR) Equal(theirs PCR) bool { for i, ourValue := range ours { - // PCR4 contains a hash over the parent's instance ID. If horizontal - // scaling is enabled, enclaves run on different parent instances, so - // PCR4 will differ: - // https://docs.aws.amazon.com/enclaves/latest/user/set-up-attestation.html + // PCR4 contains a hash over the parent's instance ID, which is known at + // runtime. We ignore it for now, until we have a better solution on + // how to handle this. if i == 4 { continue } diff --git a/internal/enclave/pcr_test.go b/internal/enclave/pcr_test.go index 356d247..6a9b040 100644 --- a/internal/enclave/pcr_test.go +++ b/internal/enclave/pcr_test.go @@ -10,16 +10,16 @@ import ( func TestPCRsFromDebugMode(t *testing.T) { cases := []struct { name string - pcrs pcr + pcrs PCR want bool }{ { name: "empty", - pcrs: pcr{}, + pcrs: PCR{}, }, { name: "debug mode", - pcrs: pcr{ + pcrs: PCR{ 0: emptyPCR, 1: emptyPCR, 2: emptyPCR, @@ -30,7 +30,7 @@ func TestPCRsFromDebugMode(t *testing.T) { }, { name: "not debug mode", - pcrs: pcr{ + pcrs: PCR{ 0: []byte("foo"), 1: emptyPCR, 2: emptyPCR, @@ -48,43 +48,43 @@ func TestPCRsFromDebugMode(t *testing.T) { func TestPCRsEqual(t *testing.T) { cases := []struct { name string - pcr1 pcr - pcr2 pcr + pcr1 PCR + pcr2 PCR want bool }{ { name: "empty", - pcr1: pcr{}, - pcr2: pcr{}, + pcr1: PCR{}, + pcr2: PCR{}, want: true, }, { name: "identical", - pcr1: pcr{ + pcr1: PCR{ 1: []byte("foobar"), }, - pcr2: pcr{ + pcr2: PCR{ 1: []byte("foobar"), }, want: true, }, { name: "PCR mismatch", - pcr1: pcr{ + pcr1: PCR{ 1: []byte("foobar"), }, - pcr2: pcr{ + pcr2: PCR{ 1: []byte("barfoo"), }, want: false, }, { name: "ignore PCR4", - pcr1: pcr{ + pcr1: PCR{ 1: []byte("foobar"), 4: []byte("foo"), }, - pcr2: pcr{ + pcr2: PCR{ 1: []byte("foobar"), 4: []byte("bar"), }, @@ -92,21 +92,32 @@ func TestPCRsEqual(t *testing.T) { }, { name: "length mismatch", - pcr1: pcr{ + pcr1: PCR{ 1: []byte("foobar"), 2: []byte("foo"), }, - pcr2: pcr{ + pcr2: PCR{ 1: []byte("foobar"), }, want: false, }, + { + name: "length mismatch due to PCR4", + pcr1: PCR{ + 1: []byte("foo"), + 4: []byte("bar"), + }, + pcr2: PCR{ + 1: []byte("foo"), + }, + want: true, + }, { name: "PCR index mismatch", - pcr1: pcr{ + pcr1: PCR{ 1: []byte("foo"), }, - pcr2: pcr{ + pcr2: PCR{ 2: []byte("foo"), }, want: false, diff --git a/scripts/attest-enclave.sh b/scripts/attest-enclave.sh new file mode 100755 index 0000000..60b9a8e --- /dev/null +++ b/scripts/attest-enclave.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +if [ $# -ne 2 ] +then + echo "Usage: $0 /path/to/enclave/app/ https://enclave-app.com" >&2 + exit 1 +fi +repository="$1" +enclave="$2" +docker_image="enclave-app:latest" + +# Build the reproducible enclave app Docker image. +echo "[+] Building reproducible enclave app image." >&2 +(cd "$repository" && make) + +# The following Dockerfile is used to build the enclave image, which requires +# the nitro-cli tool. +dockerfile=Dockerfile +cat > "$dockerfile" </dev/null"] +EOF +trap "rm -f $dockerfile" EXIT +trap "rm -f $dockerfile" SIGINT + +# We're using --no-cache because AWS's nitro-cli may update, at which point the +# builder image will use an outdated copy, which will result in an unexpected +# PCR0 value. +builder_image="nitro-cli:latest" +echo "[+] Building ephemeral builder image." >&2 +docker build \ + --no-cache \ + --quiet \ + --tag "$builder_image" \ + --platform=linux/amd64 \ + - < Dockerfile 2>/dev/null + +echo "[+] Running builder image to obtain enclave PCRs." >&2 +measurements=$(docker run \ + --tty \ + --interactive \ + --platform=linux/amd64 \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + "$builder_image") + +# Request attestation document from the enclave. +echo "[+] Fetching remote attestation." >&2 +script_dir=$(dirname "$0") +go run "${script_dir}/../cmd/veil-verify/main.go" \ + -addr "$enclave" \ + -pcrs "$measurements"