From cfd040efef969258d4f3e9a560e6e088fe8aeb24 Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Sat, 19 Oct 2024 08:19:41 -0500 Subject: [PATCH 1/7] Extend unit tests. --- cmd/main_test.go | 2 +- internal/enclave/attester.go | 18 ++-- internal/enclave/attester_nitro.go | 58 ++++++------ internal/enclave/attester_nitro_test.go | 121 ++++++++++++++++++++++++ internal/enclave/attester_noop.go | 8 +- internal/enclave/attester_noop_test.go | 13 ++- internal/enclave/util.go | 6 ++ internal/nonce/nonce.go | 4 + 8 files changed, 180 insertions(+), 50 deletions(-) create mode 100644 internal/enclave/attester_nitro_test.go diff --git a/cmd/main_test.go b/cmd/main_test.go index 88315e2..8387545 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -253,7 +253,7 @@ func TestAttestation(t *testing.T) { require.NoError(t, json.Unmarshal(body, &a)) // "Verify" the attestation document using our noop attester. - aux, err := enclave.NewNoopAttester().Verify(a.Doc, c.nonce) + aux, err := enclave.NewNoopAttester().Verify(&a, c.nonce) require.NoError(t, err, errFromBody(t, resp)) // Ensure that the recovered nonce matches what we sent. diff --git a/internal/enclave/attester.go b/internal/enclave/attester.go index 19d1092..e7f0c7b 100644 --- a/internal/enclave/attester.go +++ b/internal/enclave/attester.go @@ -6,9 +6,13 @@ import ( "github.com/Amnesic-Systems/veil/internal/nonce" ) -// See page 65 of the AWS Nitro Enclaves user guide for reference: -// https://docs.aws.amazon.com/pdfs/enclaves/latest/user/enclaves-user.pdf -const userDataLen = 1024 +const ( + // See page 65 of the AWS Nitro Enclaves user guide for reference: + // https://docs.aws.amazon.com/pdfs/enclaves/latest/user/enclaves-user.pdf + userDataLen = 1024 + typeNoop = "noop" + typeNitro = "nitro" +) var ( errPCRMismatch = errors.New("platform configuration registers differ") @@ -35,17 +39,11 @@ type AuxInfo struct { Nonce [userDataLen]byte `json:"public_key"` } -func ToAuxField(s []byte) [userDataLen]byte { - var a [userDataLen]byte - copy(a[:], s) - return a -} - // Attester defines functions for the creation and verification of attestation // documents. Making this an interface helps with testing: It allows us to // implement a dummy attester that works without the AWS Nitro hypervisor. type Attester interface { Type() string Attest(*AuxInfo) (*AttestationDoc, error) - Verify(Attestation, *nonce.Nonce) (*AuxInfo, error) + Verify(*AttestationDoc, *nonce.Nonce) (*AuxInfo, error) } diff --git a/internal/enclave/attester_nitro.go b/internal/enclave/attester_nitro.go index dc95ff4..f3b25e3 100644 --- a/internal/enclave/attester_nitro.go +++ b/internal/enclave/attester_nitro.go @@ -10,7 +10,6 @@ import ( "github.com/hf/nitrite" "github.com/hf/nsm" "github.com/hf/nsm/request" - "github.com/hf/nsm/response" ) // NitroAttester implements the attester interface by drawing on the AWS Nitro @@ -32,66 +31,65 @@ func NewNitroAttester() (attester Attester, err error) { } func (*NitroAttester) Type() string { - return "nitro" + return typeNitro } -func (a *NitroAttester) Attest(aux *AuxInfo) (*AttestationDoc, error) { - var ( - err error - resp response.Response - ) +func (a *NitroAttester) Attest(aux *AuxInfo) (_ *AttestationDoc, err error) { + defer errs.Wrap(&err, "failed to create attestation document") + + if aux == nil { + return nil, errors.New("aux info is nil") + } req := &request.Attestation{ Nonce: aux.Nonce[:], UserData: aux.UserData[:], PublicKey: aux.PublicKey[:], } - - if resp, err = a.session.Send(req); err != nil { + resp, err := a.session.Send(req) + if err != nil { return nil, err } if resp.Attestation == nil || resp.Attestation.Document == nil { - return nil, errors.New("not good") + return nil, errors.New("required fields missing in attestation response") } + return &AttestationDoc{ - Type: "nitro", + Type: typeNitro, Doc: resp.Attestation.Document, }, nil } -func (*NitroAttester) Verify(a Attestation, ourNonce *nonce.Nonce) (_ *AuxInfo, err error) { +func (a *NitroAttester) Verify(doc *AttestationDoc, ourNonce *nonce.Nonce) (_ *AuxInfo, err error) { defer errs.Wrap(&err, "failed to verify attestation document") - // First, verify the remote enclave's attestation document. - opts := nitrite.VerifyOptions{CurrentTime: time.Now().UTC()} - their, err := nitrite.Verify(a, opts) - if err != nil { - return nil, err + if doc == nil { + return nil, errors.New("attestation document is nil") + } + if doc.Type != a.Type() { + return nil, errors.New("attestation document type mismatch") } - // Verify that the remote enclave's PCR values (e.g., the image ID) are - // identical to ours. - ourPCRs, err := getPCRs() + // First, verify the attestation document. + opts := nitrite.VerifyOptions{CurrentTime: time.Now().UTC()} + res, err := nitrite.Verify(doc.Doc, opts) if err != nil { return nil, err } - if !ourPCRs.Equal(their.Document.PCRs) { - return nil, errPCRMismatch - } - // Verify that the remote enclave's attestation document contains the nonce - // that we asked it to embed. - theirNonce, err := nonce.FromSlice(their.Document.Nonce) + // Verify that the attestation document contains the nonce that we may have + // asked it to embed. + docNonce, err := nonce.FromSlice(res.Document.Nonce) if err != nil { return nil, err } - if *ourNonce != *theirNonce { + if ourNonce != nil && *ourNonce != *docNonce { return nil, errNonceMismatch } return &AuxInfo{ - Nonce: [1024]byte(their.Document.Nonce), - UserData: [1024]byte(their.Document.UserData), - PublicKey: [1024]byte(their.Document.PublicKey), + Nonce: [userDataLen]byte(res.Document.Nonce), + UserData: [userDataLen]byte(res.Document.UserData), + PublicKey: [userDataLen]byte(res.Document.PublicKey), }, nil } diff --git a/internal/enclave/attester_nitro_test.go b/internal/enclave/attester_nitro_test.go new file mode 100644 index 0000000..a9c6784 --- /dev/null +++ b/internal/enclave/attester_nitro_test.go @@ -0,0 +1,121 @@ +package enclave + +import ( + "testing" + + "github.com/Amnesic-Systems/veil/internal/nonce" + "github.com/Amnesic-Systems/veil/internal/util" + "github.com/stretchr/testify/require" +) + +func getNonce(t *testing.T) [userDataLen]byte { + n, err := nonce.New() + require.NoError(t, err) + return ToAuxField(n.ToSlice()) +} + +func TestNitroAttest(t *testing.T) { + if !IsEnclave() { + t.Skip("skipping test; not running in an enclave") + } + attester, err := NewNitroAttester() + require.NoError(t, err) + + cases := []struct { + name string + aux *AuxInfo + wantErr bool + }{ + { + name: "nil aux info", + wantErr: true, + }, + { + name: "empty aux info", + aux: &AuxInfo{}, + }, + { + name: "aux info with nonce", + aux: &AuxInfo{ + Nonce: getNonce(t), + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + doc, err := attester.Attest(c.aux) + if c.wantErr { + require.NotNil(t, err) + return + } + require.Equal(t, doc.Type, typeNitro) + }) + } +} + +func TestNitroVerify(t *testing.T) { + if !IsEnclave() { + t.Skip("skipping test; not running in an enclave") + } + + attester, err := NewNitroAttester() + require.NoError(t, err) + + getDoc := func(t *testing.T, n *nonce.Nonce) *AttestationDoc { + doc, err := attester.Attest(&AuxInfo{Nonce: ToAuxField(n.ToSlice())}) + require.NoError(t, err) + return doc + } + testNonce := util.Must(nonce.New()) + + cases := []struct { + name string + doc *AttestationDoc + nonce *nonce.Nonce + wantErr bool + }{ + { + name: "nil document and nonce", + wantErr: true, + }, + { + name: "document type mismatch", + doc: &AttestationDoc{Type: "foo"}, + wantErr: true, + }, + { + name: "invalid document", + doc: &AttestationDoc{ + Type: typeNitro, + Doc: []byte("foobar"), + }, + wantErr: true, + }, + { + name: "nonce mismatch", + doc: getDoc(t, util.Must(nonce.New())), + nonce: util.Must(nonce.New()), + wantErr: true, + }, + { + name: "no nonce", + doc: getDoc(t, util.Must(nonce.New())), + }, + { + name: "valid document and nonce", + doc: getDoc(t, testNonce), + nonce: testNonce, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + _, err := attester.Verify(c.doc, c.nonce) + if c.wantErr { + require.Error(t, err) + return + } + }) + } +} diff --git a/internal/enclave/attester_noop.go b/internal/enclave/attester_noop.go index 5df614a..7af5184 100644 --- a/internal/enclave/attester_noop.go +++ b/internal/enclave/attester_noop.go @@ -14,7 +14,7 @@ func NewNoopAttester() Attester { } func (*NoopAttester) Type() string { - return "noop" + return typeNoop } func (*NoopAttester) Attest(aux *AuxInfo) (*AttestationDoc, error) { @@ -23,14 +23,14 @@ func (*NoopAttester) Attest(aux *AuxInfo) (*AttestationDoc, error) { return nil, err } return &AttestationDoc{ - Type: "noop", + Type: typeNoop, Doc: a, }, nil } -func (*NoopAttester) Verify(a Attestation, n *nonce.Nonce) (*AuxInfo, error) { +func (*NoopAttester) Verify(a *AttestationDoc, n *nonce.Nonce) (*AuxInfo, error) { var aux = new(AuxInfo) - if err := json.Unmarshal(a, &aux); err != nil { + if err := json.Unmarshal(a.Doc, &aux); err != nil { return nil, err } return aux, nil diff --git a/internal/enclave/attester_noop_test.go b/internal/enclave/attester_noop_test.go index 7d333b0..e5f2ea6 100644 --- a/internal/enclave/attester_noop_test.go +++ b/internal/enclave/attester_noop_test.go @@ -16,16 +16,16 @@ func TestSuccessfulVerification(t *testing.T) { // "Age": float64(42), // } origAux = &AuxInfo{ - PublicKey: [1024]byte{'a', 'b', 'c'}, - UserData: [1024]byte{'d', 'e', 'f'}, - Nonce: [1024]byte{'g', 'h', 'i'}, + PublicKey: [userDataLen]byte{'a', 'b', 'c'}, + UserData: [userDataLen]byte{'d', 'e', 'f'}, + Nonce: [userDataLen]byte{'g', 'h', 'i'}, } ) attestation, err := a.Attest(origAux) require.Nil(t, err) - aux, err := a.Verify(attestation.Doc, &nonce.Nonce{}) + aux, err := a.Verify(attestation, &nonce.Nonce{}) require.Nil(t, err) require.Equal(t, origAux, aux) } @@ -33,6 +33,9 @@ func TestSuccessfulVerification(t *testing.T) { func TestFailedVerification(t *testing.T) { var a = NewNoopAttester() - _, err := a.Verify([]byte(`"foo": "bar`), &nonce.Nonce{}) + _, err := a.Verify(&AttestationDoc{ + Type: typeNoop, + Doc: []byte(`"foo": "bar`), + }, &nonce.Nonce{}) require.NotNil(t, err) } diff --git a/internal/enclave/util.go b/internal/enclave/util.go index cdcadc9..a11ad8c 100644 --- a/internal/enclave/util.go +++ b/internal/enclave/util.go @@ -9,3 +9,9 @@ func IsEnclave() bool { } return false } + +func ToAuxField(s []byte) [userDataLen]byte { + var a [userDataLen]byte + copy(a[:], s) + return a +} diff --git a/internal/nonce/nonce.go b/internal/nonce/nonce.go index 7e96061..09ec21b 100644 --- a/internal/nonce/nonce.go +++ b/internal/nonce/nonce.go @@ -28,6 +28,10 @@ func (n *Nonce) URLEncode() string { ) } +func (n *Nonce) ToSlice() []byte { + return n[:] +} + // New creates a new nonce. func New() (*Nonce, error) { var newNonce Nonce From d778be3b7c4ef381027c2fd6781b0a6bed5c5533 Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Sat, 19 Oct 2024 09:57:12 -0500 Subject: [PATCH 2/7] Only use `-insecure` when outside an enclave. --- cmd/main_test.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/main_test.go b/cmd/main_test.go index 8387545..deda4f8 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -29,6 +29,14 @@ import ( "github.com/stretchr/testify/require" ) +func withFlags(flag ...string) []string { + var f []string + if !enclave.IsEnclave() { + f = append(f, "-insecure") + } + return append(f, flag...) +} + func waitForSvc(t *testing.T, url string) error { var ( start = time.Now() @@ -128,7 +136,7 @@ func TestHelp(t *testing.T) { } func TestPages(t *testing.T) { - defer stopSvc(startSvc(t, []string{"-insecure"})) + defer stopSvc(startSvc(t, withFlags())) cases := []struct { name string @@ -164,7 +172,7 @@ func TestPages(t *testing.T) { } func TestReadyHandler(t *testing.T) { - defer stopSvc(startSvc(t, []string{"-insecure", "-wait-for-app"})) + defer stopSvc(startSvc(t, withFlags("-wait-for-app"))) cases := []struct { name string @@ -211,7 +219,7 @@ func TestReadyHandler(t *testing.T) { } func TestAttestation(t *testing.T) { - defer stopSvc(startSvc(t, []string{"-insecure"})) + defer stopSvc(startSvc(t, withFlags())) cases := []struct { name string @@ -265,7 +273,7 @@ func TestAttestation(t *testing.T) { } func TestHashes(t *testing.T) { - defer stopSvc(startSvc(t, []string{"-insecure"})) + defer stopSvc(startSvc(t, withFlags())) var ( hashes = new(attestation.Hashes) @@ -347,7 +355,7 @@ func TestReverseProxy(t *testing.T) { }, )) defer srv.Close() - defer stopSvc(startSvc(t, []string{"-insecure", "-app-web-srv", srv.URL})) + defer stopSvc(startSvc(t, withFlags("-app-web-srv", srv.URL))) cases := []struct { name string From 1ffd4940352438befa9b5aa3e137540ccb5eecb4 Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Sat, 19 Oct 2024 11:04:37 -0500 Subject: [PATCH 3/7] Add test for `Type()`. --- internal/enclave/attester_noop_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/enclave/attester_noop_test.go b/internal/enclave/attester_noop_test.go index e5f2ea6..c73841d 100644 --- a/internal/enclave/attester_noop_test.go +++ b/internal/enclave/attester_noop_test.go @@ -7,14 +7,13 @@ import ( "github.com/stretchr/testify/require" ) +func TestType(t *testing.T) { + require.Equal(t, typeNoop, NewNoopAttester().Type()) +} + func TestSuccessfulVerification(t *testing.T) { var ( - a = NewNoopAttester() - // // JSON objects are map[string]interface{} and numbers are float64. - // origAux = map[string]any{ - // "Name": "John Doe", - // "Age": float64(42), - // } + a = NewNoopAttester() origAux = &AuxInfo{ PublicKey: [userDataLen]byte{'a', 'b', 'c'}, UserData: [userDataLen]byte{'d', 'e', 'f'}, From 6a7e2894cc95922059f4b1570d892a5d63cf334c Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Sat, 19 Oct 2024 11:19:06 -0500 Subject: [PATCH 4/7] Add VS Code directory. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2117c17..b1f3fd3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode cmd/veil internal/cover.html internal/cover.out From 975470cf7adb6234f794b4d37fde82fc714d8cb0 Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Sat, 19 Oct 2024 11:49:26 -0500 Subject: [PATCH 5/7] Remove unnecessary type. --- internal/enclave/attester.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/enclave/attester.go b/internal/enclave/attester.go index e7f0c7b..11480e2 100644 --- a/internal/enclave/attester.go +++ b/internal/enclave/attester.go @@ -23,12 +23,10 @@ var ( _ Attester = (*NoopAttester)(nil) ) -type Attestation []byte - // AttestationDoc holds the enclave's attestation document. type AttestationDoc struct { - Type string `json:"type"` - Doc Attestation `json:"attestation_document"` + Type string `json:"type"` + Doc []byte `json:"attestation_document"` } type AuxFunc func(*nonce.Nonce) *AuxInfo From ed8ce3df145e06aeeb8f6807764d9879f07ab97f Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Sat, 19 Oct 2024 11:54:37 -0500 Subject: [PATCH 6/7] Remove unused error message. --- internal/enclave/attester.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/enclave/attester.go b/internal/enclave/attester.go index 11480e2..84f74df 100644 --- a/internal/enclave/attester.go +++ b/internal/enclave/attester.go @@ -15,7 +15,6 @@ const ( ) var ( - errPCRMismatch = errors.New("platform configuration registers differ") errNonceMismatch = errors.New("nonce does not match") // Check at compile-time if types implement the attester interface. From 8009a94cb1136e316c277ab4bcc21dafdeb18881 Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Sat, 19 Oct 2024 11:58:19 -0500 Subject: [PATCH 7/7] Skip system setup when testing. --- internal/service/service.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/service/service.go b/internal/service/service.go index 8743d43..07bfc6a 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -86,10 +86,13 @@ func checkSystemSafety(config *config.Config) (err error) { func setupSystem(config *config.Config) (err error) { defer errs.Wrap(&err, "failed to set up system") - if !config.Testing { - if err := system.SeedRandomness(); err != nil { - return err - } + // GitHub Actions won't allow us to set up the lo interface. + if config.Testing { + return nil + } + + if err := system.SeedRandomness(); err != nil { + return err } return system.SetupLo() }