Skip to content

Commit

Permalink
Add attestation only if nonce is provided.
Browse files Browse the repository at this point in the history
The config endpoint isn't sensitive, so there's no need to always attest
the response.  This PR makes config attestation opportunistic: we do it
if there's a nonce and otherwise we skip it.
  • Loading branch information
NullHypothesis committed Dec 8, 2024
1 parent b82315f commit 704f895
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 169 deletions.
49 changes: 0 additions & 49 deletions internal/service/attestation/aux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,6 @@ import (
"github.com/Amnesic-Systems/veil/internal/nonce"
)

// Builder is a helper for setting auxiliary attestation both at initialization
// time and at attestation time.
type Builder struct {
attester enclave.Attester
aux enclave.AuxInfo
}

type AuxField func(*Builder)

// NewBuilder returns a new Builder with the given attester and sets the given
// auxiliary fields.
func NewBuilder(attester enclave.Attester, opts ...AuxField) *Builder {
b := &Builder{attester: attester}
for _, opt := range opts {
opt(b)
}
return b
}

// Attest returns an attestation document with the auxiliary fields that were
// either already set, or are now passed in as options.
func (b *Builder) Attest(opts ...AuxField) (*enclave.RawDocument, error) {
for _, opt := range opts {
opt(b)
}
return b.attester.Attest(&b.aux)
}

// WithHashes sets the given hashes in an auxiliary field.
func WithHashes(h *Hashes) AuxField {
return func(b *Builder) {
b.aux.PublicKey = h.Serialize() // TODO: safe?
}
}

// WithNonce sets the given nonce in an auxiliary field.
func WithNonce(n *nonce.Nonce) AuxField {
return func(b *Builder) {
b.aux.Nonce = n.ToSlice() // TODO: safe?
}
}

// WithSHA256 sets the given SHA256 hash in an auxiliary field.
func WithSHA256(sha [sha256.Size]byte) AuxField {
return func(b *Builder) {
b.aux.UserData = sha[:]
}
}

// GetNonce returns the nonce from the given auxiliary info.
func GetNonce(aux *enclave.AuxInfo) (*nonce.Nonce, error) {
if aux.Nonce == nil {
Expand Down
67 changes: 0 additions & 67 deletions internal/service/attestation/aux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (

"github.com/Amnesic-Systems/veil/internal/addr"
"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/nonce"
"github.com/Amnesic-Systems/veil/internal/util"
Expand Down Expand Up @@ -75,68 +73,3 @@ func TestGetters(t *testing.T) {
})
}
}

func TestBuilder(t *testing.T) {
attester := noop.NewAttester()
if nitro.IsEnclave() {
attester = nitro.NewAttester()
}
nonce1, nonce2 := util.Must(nonce.New()), util.Must(nonce.New())
sha1, sha2 := sha256.Sum256([]byte("foo")), sha256.Sum256([]byte("bar"))
hashes1 := &Hashes{TlsKeyHash: addr.Of(sha256.Sum256([]byte("foo")))}
hashes2 := &Hashes{TlsKeyHash: addr.Of(sha256.Sum256([]byte("bar")))}

cases := []struct {
name string
initFields []AuxField
attestFields []AuxField
wantAux *enclave.AuxInfo
}{
{
name: "empty",
wantAux: &enclave.AuxInfo{},
},
{
name: "nonce at initialization",
initFields: []AuxField{WithNonce(nonce1)},
wantAux: &enclave.AuxInfo{Nonce: nonce1.ToSlice()},
},
{
name: "nonce at attestation",
attestFields: []AuxField{WithNonce(nonce1)},
wantAux: &enclave.AuxInfo{Nonce: nonce1.ToSlice()},
},
{
name: "nonce being overwritten",
initFields: []AuxField{WithNonce(nonce1)},
attestFields: []AuxField{WithNonce(nonce2)},
wantAux: &enclave.AuxInfo{Nonce: nonce2.ToSlice()},
},
{
name: "everything overwritten",
initFields: []AuxField{WithHashes(hashes1), WithNonce(nonce1), WithSHA256(sha1)},
attestFields: []AuxField{WithHashes(hashes2), WithNonce(nonce2), WithSHA256(sha2)},
wantAux: &enclave.AuxInfo{
Nonce: nonce2.ToSlice(),
PublicKey: hashes2.Serialize(),
UserData: sha2[:],
},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
b := NewBuilder(attester, c.initFields...)
rawDoc, err := b.Attest(c.attestFields...)
require.NoError(t, err)

// Verify the attestation document. We expect no error but if the
// test is run inside a Nitro Enclave, we will get ErrDebugMode.
doc, err := attester.Verify(rawDoc, nil)
if err != nil {
require.ErrorIs(t, err, nitro.ErrDebugMode)
}
require.Equal(t, c.wantAux, &doc.AuxInfo)
})
}
}
72 changes: 72 additions & 0 deletions internal/service/attestation/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package attestation

import (
"crypto/sha256"

"github.com/Amnesic-Systems/veil/internal/enclave"
"github.com/Amnesic-Systems/veil/internal/nonce"
)

// Builder is an abstraction purpose-built for veil's HTTP handlers. It bundles
// an attester with auxiliary fields because these two are always used together.
// As a Builder is passed through the stack, its auxiliary fields are updated
// and eventually used to create an attestation document.
type Builder struct {
enclave.Attester
enclave.AuxInfo
}

type AuxField func(*Builder)

// NewBuilder returns a new Builder with the given attester and sets the given
// auxiliary fields.
func NewBuilder(attester enclave.Attester, opts ...AuxField) *Builder {
b := &Builder{Attester: attester}
for _, opt := range opts {
opt(b)
}
return b
}

// Update updates the builder with the given auxiliary fields.
func (b *Builder) Update(opts ...AuxField) {
for _, opt := range opts {
opt(b)
}
}

// Attest returns an attestation document with the auxiliary fields that were
// either already set, or are now passed in as options.
func (b *Builder) Attest(opts ...AuxField) (*enclave.RawDocument, error) {
for _, opt := range opts {
opt(b)
}
return b.Attester.Attest(&b.AuxInfo)
}

// WithHashes sets the given hashes in an auxiliary field.
func WithHashes(h *Hashes) AuxField {
return func(b *Builder) {
if h == nil {
return
}
b.PublicKey = h.Serialize()
}
}

// WithNonce sets the given nonce in an auxiliary field.
func WithNonce(n *nonce.Nonce) AuxField {
return func(b *Builder) {
if n == nil {
return
}
b.Nonce = n.ToSlice()
}
}

// WithSHA256 sets the given SHA256 hash in an auxiliary field.
func WithSHA256(sha [sha256.Size]byte) AuxField {
return func(b *Builder) {
b.UserData = sha[:]
}
}
79 changes: 79 additions & 0 deletions internal/service/attestation/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package attestation

import (
"crypto/sha256"
"testing"

"github.com/Amnesic-Systems/veil/internal/addr"
"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/nonce"
"github.com/Amnesic-Systems/veil/internal/util"
"github.com/stretchr/testify/require"
)

func TestBuilder(t *testing.T) {
attester := noop.NewAttester()
if nitro.IsEnclave() {
attester = nitro.NewAttester()
}
nonce1, nonce2 := util.Must(nonce.New()), util.Must(nonce.New())
sha1, sha2 := sha256.Sum256([]byte("foo")), sha256.Sum256([]byte("bar"))
hashes1 := &Hashes{TlsKeyHash: addr.Of(sha256.Sum256([]byte("foo")))}
hashes2 := &Hashes{TlsKeyHash: addr.Of(sha256.Sum256([]byte("bar")))}

cases := []struct {
name string
initFields []AuxField
attestFields []AuxField
wantAux *enclave.AuxInfo
}{
{
name: "empty",
wantAux: &enclave.AuxInfo{},
},
{
name: "nonce at initialization",
initFields: []AuxField{WithNonce(nonce1)},
wantAux: &enclave.AuxInfo{Nonce: nonce1.ToSlice()},
},
{
name: "nonce at attestation",
attestFields: []AuxField{WithNonce(nonce1)},
wantAux: &enclave.AuxInfo{Nonce: nonce1.ToSlice()},
},
{
name: "nonce being overwritten",
initFields: []AuxField{WithNonce(nonce1)},
attestFields: []AuxField{WithNonce(nonce2)},
wantAux: &enclave.AuxInfo{Nonce: nonce2.ToSlice()},
},
{
name: "everything overwritten",
initFields: []AuxField{WithHashes(hashes1), WithNonce(nonce1), WithSHA256(sha1)},
attestFields: []AuxField{WithHashes(hashes2), WithNonce(nonce2), WithSHA256(sha2)},
wantAux: &enclave.AuxInfo{
Nonce: nonce2.ToSlice(),
PublicKey: hashes2.Serialize(),
UserData: sha2[:],
},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
b := NewBuilder(attester, c.initFields...)
rawDoc, err := b.Attest(c.attestFields...)
require.NoError(t, err)

// Verify the attestation document. We expect no error but if the
// test is run inside a Nitro Enclave, we will get ErrDebugMode.
doc, err := attester.Verify(rawDoc, nil)
if err != nil {
require.ErrorIs(t, err, nitro.ErrDebugMode)
}
require.Equal(t, c.wantAux, &doc.AuxInfo)
})
}
}
33 changes: 5 additions & 28 deletions internal/service/handle/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"log"
"net/http"

"github.com/Amnesic-Systems/veil/internal/httperr"
"github.com/Amnesic-Systems/veil/internal/httpx"
"github.com/Amnesic-Systems/veil/internal/service/attestation"
)

Expand All @@ -23,37 +21,17 @@ func encode[T any](w http.ResponseWriter, status int, v T) {
}
}

func encodeAndMaybeAttest[T any](
w http.ResponseWriter,
r *http.Request,
status int,
builder *attestation.Builder,
v T,
) {
// Depending on if the request contains a nonce, either return the JSON
// response without attestation or include an attestation document in the
// response.
if _, err := httpx.ExtractNonce(r); err != nil {
encode(w, status, v)
} else {
encodeAndAttest(w, r, status, builder, v)
}
}

func encodeAndAttest[T any](
w http.ResponseWriter,
r *http.Request,
status int,
builder *attestation.Builder,
v T,
) {
// Try to extract the client's nonce from the request. If this fails, abort
// attestation because the client no longer has a way to verify the
// attestation document's freshness.
n, err := httpx.ExtractNonce(r)
if err != nil {
log.Println(err)
encode(w, http.StatusBadRequest, httperr.New("found no valid nonce in HTTP request"))
// It's a bug if the caller didn't set a nonce in the builder. Attestation
// documents can be replayed if they're not tied to a nonce, so it's best to
// return an error.
if builder.Nonce == nil {
encode(w, http.StatusInternalServerError, httperr.New("caller didn't set nonce"))
return
}

Expand All @@ -67,7 +45,6 @@ func encodeAndAttest[T any](
// hash and the client's nonce.
hash := sha256.Sum256(body)
attestation, err := builder.Attest(
attestation.WithNonce(n),
attestation.WithSHA256(hash),
)
if err != nil {
Expand Down
Loading

0 comments on commit 704f895

Please sign in to comment.