From 30febc665ad52dc0fec6d0518bcdadec1e57c137 Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Thu, 25 Jun 2026 12:49:07 +0200 Subject: [PATCH 1/2] teeattestation: add Document.VerifyPCR for per-instance PCRs Add Document.VerifyPCR(index, expected) and VerifyExpectedPCRs(map) so a caller can assert per-instance PCRs (e.g. PCR4, the SHA384 of the parent instance ID) that differ per enclave and therefore cannot go in the shared trusted measurements that ValidateAndParse checks. It rejects an empty expected value, an absent or length-mismatched PCR, and an all-zero PCR (debug-mode enclaves emit all-zero PCRs, which must never be accepted as a real measurement). --- pkg/teeattestation/nitro/validate.go | 50 +++++++++++++++++ pkg/teeattestation/nitro/validate_test.go | 66 +++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/pkg/teeattestation/nitro/validate.go b/pkg/teeattestation/nitro/validate.go index a62332c06c..9491654371 100644 --- a/pkg/teeattestation/nitro/validate.go +++ b/pkg/teeattestation/nitro/validate.go @@ -76,6 +76,56 @@ 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", index) + } + 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. +func (d *Document) VerifyExpectedPCRs(expected map[uint][]byte) error { + for index, value := range expected { + if err := d.VerifyPCR(index, value); 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") +} From fc06c95f8b41b1f95248a6aa6a3fc6e12dbd0691 Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Thu, 25 Jun 2026 13:28:20 +0200 Subject: [PATCH 2/2] teeattestation: deterministic VerifyExpectedPCRs order, richer mismatch error Address review: sort PCR indices ascending in VerifyExpectedPCRs so the reported first failure is deterministic, and include expected/actual bytes in the PCR mismatch error (PCRs are measurements, not secrets), matching the existing PCR0/1/2 error style. --- pkg/teeattestation/nitro/validate.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/teeattestation/nitro/validate.go b/pkg/teeattestation/nitro/validate.go index 9491654371..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" ) @@ -100,17 +101,23 @@ func (d *Document) VerifyPCR(index uint, expected []byte) error { return fmt.Errorf("PCR%d is all zero (debug-mode enclave), refusing", index) } if !bytes.Equal(actual, expected) { - return fmt.Errorf("PCR%d mismatch", index) + 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. +// 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 { - for index, value := range expected { - if err := d.VerifyPCR(index, value); err != nil { + 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 } }