From 41b6cb20ba2df6e0d771b0251265040001be907e Mon Sep 17 00:00:00 2001 From: xyz <2523269+antojoseph@users.noreply.github.com> Date: Sun, 3 May 2026 23:02:23 -0400 Subject: [PATCH 1/2] fix: reject stale and future-dated attestation blobs at provider registration Calls attestation.CheckTimestamp with a 5-minute max-age window immediately after VerifyJSON succeeds. A replayed or future-dated attestation blob is rejected and the provider is marked untrusted before any trust level is granted. --- coordinator/internal/api/provider.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/coordinator/internal/api/provider.go b/coordinator/internal/api/provider.go index b3b8d45c..dfa0f619 100644 --- a/coordinator/internal/api/provider.go +++ b/coordinator/internal/api/provider.go @@ -1208,6 +1208,22 @@ func (s *Server) verifyProviderAttestation(providerID string, provider *registry return } + // Reject stale attestation blobs. A replayed blob from a previously-valid + // provider could otherwise grant trust to a new connection without fresh + // hardware verification. 5 minutes matches the challenge cycle interval. + const attestationMaxAge = 5 * time.Minute + if !attestation.CheckTimestamp(result, attestationMaxAge) { + s.logger.Warn("provider attestation timestamp out of range — rejecting stale or future-dated blob", + "provider_id", providerID, + "attestation_time", result.Timestamp, + ) + result.Valid = false + result.Error = "attestation timestamp out of acceptable range" + provider.SetAttestationResult(&result) + s.registry.MarkUntrusted(providerID) + return + } + // Bind the WebSocket X25519 key used for E2E text encryption to the // attested Secure Enclave identity. If a provider wants to serve private // text, the attestation must carry the same encryption public key. From acb977d6ab5c374563b0d4d9ff5657abbd82fc88 Mon Sep 17 00:00:00 2001 From: xyz <2523269+antojoseph@users.noreply.github.com> Date: Sun, 3 May 2026 23:29:44 -0400 Subject: [PATCH 2/2] fix: reject future-dated attestation timestamps in CheckTimestamp time.Since is negative for future timestamps, so the previous <= maxAge check was always true for future-dated blobs. Now explicitly rejects timestamps more than 30s in the future. --- coordinator/internal/attestation/attestation.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/coordinator/internal/attestation/attestation.go b/coordinator/internal/attestation/attestation.go index 9960e0f1..f7bc7b69 100644 --- a/coordinator/internal/attestation/attestation.go +++ b/coordinator/internal/attestation/attestation.go @@ -241,11 +241,19 @@ func VerifyJSON(jsonData []byte) (VerificationResult, error) { // CheckTimestamp verifies that the attestation timestamp is within the // given maximum age. This prevents replay of old attestations. +// Future-dated timestamps (clock skew beyond 30 s) are also rejected to +// prevent bypass via a far-future timestamp: time.Since is negative for +// future times, so without the upper-bound check it would always pass. func CheckTimestamp(result VerificationResult, maxAge time.Duration) bool { if result.Timestamp.IsZero() { return false } - return time.Since(result.Timestamp) <= maxAge + now := time.Now() + // Reject future-dated blobs (allow 30 s clock skew). + if result.Timestamp.After(now.Add(30 * time.Second)) { + return false + } + return now.Sub(result.Timestamp) <= maxAge } // ParseP256PublicKey parses a raw P-256 public key point.