diff --git a/pkg/teeattestation/nitro/validate_test.go b/pkg/teeattestation/nitro/validate_test.go index de38339402..a3b93caca6 100644 --- a/pkg/teeattestation/nitro/validate_test.go +++ b/pkg/teeattestation/nitro/validate_test.go @@ -2,6 +2,7 @@ package nitro import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,6 +23,29 @@ func TestValidateAttestation_Attestor(t *testing.T) { require.NoError(t, err) } +// TestVerifyAttestationDocument_MaxAge covers PRIV-438 / CL112-10: fresh +// attestations verify; stale or far-future ones are rejected even while the +// leaf cert is still valid. +func TestVerifyAttestationDocument_MaxAge(t *testing.T) { + fa, err := nitrofake.NewAttestor() + require.NoError(t, err) + doc, err := fa.CreateAttestation([]byte("user-data")) + require.NoError(t, err) + pool := fa.CARoots() + + // Fresh: accepted. + _, err = verifyAttestationDocument(doc, pool, time.Now()) + require.NoError(t, err) + + // Too old: rejected (cert is still valid, only age fails). + _, err = verifyAttestationDocument(doc, pool, time.Now().Add(maxAttestationAge+time.Minute)) + require.ErrorIs(t, err, errStaleAttestation) + + // Too far in the future: also rejected. + _, err = verifyAttestationDocument(doc, pool, time.Now().Add(-(maxAttestationAge + time.Minute))) + require.ErrorIs(t, err, errStaleAttestation) +} + func TestValidateAttestation_WrongUserData(t *testing.T) { fa, err := nitrofake.NewAttestor() require.NoError(t, err) diff --git a/pkg/teeattestation/nitro/verify.go b/pkg/teeattestation/nitro/verify.go index fe88b81ab3..ec37450f98 100644 --- a/pkg/teeattestation/nitro/verify.go +++ b/pkg/teeattestation/nitro/verify.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "errors" "fmt" + "math" "math/big" "time" @@ -21,7 +22,8 @@ var ( errBadAttestationDocument = errors.New("bad attestation document") errMandatoryFieldsMissing = errors.New("attestation document is missing mandatory fields") errBadDigest = errors.New("attestation digest is not SHA384") - errBadTimestamp = errors.New("attestation timestamp is 0") + errBadTimestamp = errors.New("attestation timestamp is zero or out of range") + errStaleAttestation = errors.New("attestation timestamp is outside the allowed window") errBadPCRs = errors.New("attestation pcrs is less than 1 or more than 32") errBadPCRIndex = errors.New("attestation pcr index is not in [0, 32)") errBadPCRValue = errors.New("attestation pcr value length is invalid") @@ -36,6 +38,9 @@ var ( errBadSignature = errors.New("attestation signature does not match certificate") ) +// maxAttestationAge rejects attestations not consumed promptly after issuance. [CL112-10] +const maxAttestationAge = 5 * time.Minute + type verifyResult struct { document *attestationDocument signatureOK bool @@ -114,6 +119,16 @@ func verifyAttestationDocument(data []byte, roots *x509.CertPool, currentTime ti if currentTime.IsZero() { currentTime = time.Now() } + + // doc.Timestamp is epoch milliseconds; reject if it overflows int64. + if doc.Timestamp > math.MaxInt64 { + return nil, errBadTimestamp + } + issued := time.UnixMilli(int64(doc.Timestamp)) + // Reject both stale and far-future timestamps (absolute skew). + if skew := currentTime.Sub(issued); skew > maxAttestationAge || skew < -maxAttestationAge { + return nil, errStaleAttestation + } if _, err := leafCert.Verify(x509.VerifyOptions{ Intermediates: intermediates, Roots: roots,