diff --git a/pkg/teeattestation/nitro/validate.go b/pkg/teeattestation/nitro/validate.go index a62332c06c..6525dd2fa2 100644 --- a/pkg/teeattestation/nitro/validate.go +++ b/pkg/teeattestation/nitro/validate.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "time" ) @@ -76,6 +77,62 @@ type Document struct { ModuleID string } +// VerifyPCR checks that the PCR at index equals expected. Use it for +// per-instance PCRs such as PCR4 (the SHA384 of the parent instance ID), which +// differ per enclave and therefore cannot be part of the shared trusted +// measurements checked by ValidateAndParse. +// +// It returns an error if expected is empty, the PCR is absent or +// length-mismatched, the PCR is all zero (debug-mode enclaves emit all-zero +// PCRs, which must never be accepted as a real measurement), or the values +// differ. +func (d *Document) VerifyPCR(index uint, expected []byte) error { + if len(expected) == 0 { + return fmt.Errorf("expected PCR%d value is empty", index) + } + actual, ok := d.PCRs[index] + if !ok { + return fmt.Errorf("attestation has no PCR%d", index) + } + if len(actual) != len(expected) { + return fmt.Errorf("PCR%d length mismatch: expected %d bytes, got %d", index, len(expected), len(actual)) + } + if allZero(actual) { + return fmt.Errorf("PCR%d is all zero (debug-mode enclave), refusing", index) + } + if !bytes.Equal(actual, expected) { + return fmt.Errorf("PCR%d mismatch: expected %x, got %x", index, expected, actual) + } + return nil +} + +// VerifyExpectedPCRs checks each index/value in expected against the document +// via VerifyPCR. It is a convenience for callers asserting several per-instance +// PCRs at once and returns the first failure. Indices are checked in ascending +// order so the reported failure is deterministic. +func (d *Document) VerifyExpectedPCRs(expected map[uint][]byte) error { + indices := make([]uint, 0, len(expected)) + for index := range expected { + indices = append(indices, index) + } + sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] }) + for _, index := range indices { + if err := d.VerifyPCR(index, expected[index]); err != nil { + return err + } + } + return nil +} + +func allZero(b []byte) bool { + for _, x := range b { + if x != 0 { + return false + } + } + return len(b) != 0 +} + // ValidateAttestation verifies an AWS Nitro attestation document against // expected user data and trusted PCR measurements. Always validates against // the AWS Nitro Enclaves root certificate. diff --git a/pkg/teeattestation/nitro/validate_test.go b/pkg/teeattestation/nitro/validate_test.go index a3b93caca6..76748f8deb 100644 --- a/pkg/teeattestation/nitro/validate_test.go +++ b/pkg/teeattestation/nitro/validate_test.go @@ -118,3 +118,69 @@ func TestValidateAndParse_FailsOnWrongUserData(t *testing.T) { require.Nil(t, parsed) require.Contains(t, err.Error(), "expected user data") } + +func TestDocument_VerifyPCR(t *testing.T) { + pcr4 := make([]byte, 48) + for i := range pcr4 { + pcr4[i] = byte(i + 1) + } + doc := &Document{PCRs: map[uint][]byte{4: pcr4}} + + t.Run("match", func(t *testing.T) { + require.NoError(t, doc.VerifyPCR(4, pcr4)) + }) + + t.Run("mismatch", func(t *testing.T) { + other := make([]byte, 48) + copy(other, pcr4) + other[0] ^= 0xff + err := doc.VerifyPCR(4, other) + require.Error(t, err) + require.Contains(t, err.Error(), "PCR4 mismatch") + }) + + t.Run("absent index", func(t *testing.T) { + err := doc.VerifyPCR(3, pcr4) + require.Error(t, err) + require.Contains(t, err.Error(), "no PCR3") + }) + + t.Run("length mismatch", func(t *testing.T) { + err := doc.VerifyPCR(4, pcr4[:32]) + require.Error(t, err) + require.Contains(t, err.Error(), "length mismatch") + }) + + t.Run("empty expected", func(t *testing.T) { + err := doc.VerifyPCR(4, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "empty") + }) + + t.Run("all-zero PCR is rejected (debug mode)", func(t *testing.T) { + zero := make([]byte, 48) + debugDoc := &Document{PCRs: map[uint][]byte{4: zero}} + err := debugDoc.VerifyPCR(4, zero) + require.Error(t, err) + require.Contains(t, err.Error(), "all zero") + }) +} + +func TestDocument_VerifyExpectedPCRs(t *testing.T) { + pcr4 := make([]byte, 48) + pcr8 := make([]byte, 48) + for i := range pcr4 { + pcr4[i] = byte(i + 1) + pcr8[i] = byte(i + 100) + } + doc := &Document{PCRs: map[uint][]byte{4: pcr4, 8: pcr8}} + + require.NoError(t, doc.VerifyExpectedPCRs(map[uint][]byte{4: pcr4, 8: pcr8})) + + wrong := make([]byte, 48) + copy(wrong, pcr8) + wrong[0] ^= 0xff + err := doc.VerifyExpectedPCRs(map[uint][]byte{4: pcr4, 8: wrong}) + require.Error(t, err) + require.Contains(t, err.Error(), "PCR8 mismatch") +}