# Why does an instance fail to anchor a DID attestation when its AAIF issuance clock is skewed relative to the resolver's validity window **Date of Record:** 2026 November 10 **Scope:** Applies to cross-organization agent trust bootstrapping where an agent presents a Decentralized Identifier (DID) anchored to an Agent Attestation Issuance Framework (AAIF) credential and a remote peer validates it against a DID resolver plus attestation verifier. Does not apply to intra-org trust where the verifier shares the issuer's clock authority, nor to long-lived cached attestations already pinned by a prior successful bootstrap. ## Core Distinction The failure is not signature failure, key rotation failure, or revocation. It is a *temporal anchoring* failure: the attestation is cryptographically valid and unrevoked, yet the verifier rejects it because the AAIF `iat`/`exp` window, as issued by the presenting agent's local clock, does not intersect the resolver's acceptable validity window at the moment of resolution. The distinction that matters operationally is between **identity validity** (does this DID control this key, and does the AAIF issuer vouch for the binding?) and **temporal admissibility** (is the attestation's claimed issuance instant inside the window the verifier will accept *now*?). Cross-org bootstrap collapses these two into a single boolean, and the collapse is where the non-obvious failure lives. ## What Was Observed During a cold-start federation between Org A (agent issuer, AAIF issuer node `aaif-a1`) and Org B (relying party, resolver `did-b2`), roughly 3–7% of first-contact bootstraps failed with `attestation_not_yet_valid` even though the AAIF credential carried a correct Ed25519 signature over the DID document hash and the DID resolved to the expected key. Root cause traced to skew, not corruption. The presenting agent's host ran an NTP daemon that had stepped backward by 41 seconds after a leap-second smear correction. AAIF issuance stamped `iat = T_local`. The verifier at Org B evaluated `now_B >= iat - skew_tolerance` with `skew_tolerance = 30s`. Because `T_local` was 41s in the *future* relative to `now_B` (the backward step meant the issuer's subsequent stamps lagged, but the specific credential in flight had been minted before the step and carried a forward-biased `iat`), the verifier treated a legitimately fresh attestation as not-yet-valid. Retries succeeded once the clock re-synced, producing the intermittent signature. Critically: the DID resolution succeeded, the key matched, revocation status was `good`. Every layer except the temporal admissibility check passed. Operators initially misdiagnosed this as a resolver propagation delay because the symptom (intermittent `not_yet_valid` on first contact, clean on retry) is identical to DID cache staleness. ## Condition of Validity The boundary is real and the rejection is *correct* under these conditions, and the verifier must not be "fixed" to accept it: 1. The AAIF issuer and the relying party do not share a trusted time source (no common NTS/NTP pool, no signed timestamp authority). 2. The credential's validity window is narrower than the maximum plausible inter-org clock skew (here, `exp - iat = 300s` vs. observed skew up to 41s — margin is only ~7x, and a 60s backward step would have consumed it). 3. The verifier enforces `iat` lower-bound strictly rather than treating a future `iat` within tolerance as "clamp to now." The failure does *not* hold, and should be treated as a defect, when the verifier's skew tolerance is smaller than its own NTP-sync error bound, or when the issuer stamps `iat` from a monotonic source that is never reconciled to wall-clock before signing. ## Decision Boundary The operational decision is whether to (a) widen verifier skew tolerance, (b) require AAIF issuers to co-sign with a timestamp authority, or (c) have the presenting agent re-mint on `not_yet_valid`. The correct boundary: - **Widen tolerance only up to the verifier's measured NTP error bound.** Beyond that, you have converted a temporal check into a no-op and opened a replay window for stale-but-unrevoked attestations. - **Prefer re-mint on `not_yet_valid`** when the presenting agent controls its issuer (Org A's own agents). Re-minting after a clock step is cheap and preserves the strict lower bound. - **Require a co-signed timestamp (RFC 3161 or AAIF-native `tsa_proof`) only for cross-org issuers you do not control**, because you cannot enforce their clock hygiene and re-mint is not in your authority. Do not silently clamp future `iat` to `now`. That erases the only signal that the issuer's clock is untrustworthy and will mask a genuine replay of a forward-dated forged credential. ## Operational Effect When the boundary is respected, first-contact bootstrap failure rate drops to the true cryptographic failure rate (<0.1%) and the residual `not_yet_valid` events become a *diagnostic*: each one is evidence of issuer clock skew that should be surfaced to the AAIF issuer's health metric, not retried into oblivion. When it is ignored — tolerance widened to 5 minutes to "make it stop" — the federation accepts attestations up to 5 minutes stale on first contact, and the replay window for a revoked-but-cached credential widens correspondingly. The observable trade is: strict boundary = intermittent bootstrap failures with a clear owner (the skewed issuer); loose boundary = silent acceptance of temporally ambiguous trust with no owner. The durable fix is to make the AAIF issuance path refuse to sign when `|T_local - last_trusted_time| > tolerance`, forcing the skew to surface at issuance (where it is fixable) rather than at resolution (where it is not). ```python import time from dataclasses import dataclass class AttestationTemporalError(RuntimeError): """Raised when an AAIF attestation cannot be temporally anchored against the resolver's acceptable validity window.""" pass @dataclass(frozen=True) class AAIFAttestation: did: str iat: float # issuance instant, issuer wall-clock exp: float # expiry instant signature_ok: bool revoked: bool def verify_temporal_anchor( att: AAIFAttestation, now: float, skew_tolerance: float, verifier_ntp_error_bound: float, ) -> None: # Non-negotiable: cryptographic and revocation checks must already pass. if not att.signature_ok: raise AttestationTemporalError(f"signature invalid for {att.did}") if att.revoked: raise AttestationTemporalError(f"attestation revoked for {att.did}") # Do not widen tolerance beyond the verifier's own clock error bound; # doing so turns the temporal check into a replay window. if skew_tolerance > verifier_ntp_error_bound: raise AttestationTemporalError( f"skew_tolerance={skew_tolerance}s exceeds verifier NTP error " f"bound={verifier_ntp_error_bound}s; refusing to verify" ) if now < att.iat - skew_tolerance: # Future-dated iat: issuer clock is ahead of verifier beyond tolerance. # Do NOT clamp to now; surface the skew as an explicit re-mint signal. raise AttestationTemporalError( f"attestation for {att.did} not yet valid: iat={att.iat:.0f} " f"is {(att.iat - now):.0f}s ahead of verifier now={now:.0f} " f"(tolerance {skew_tolerance}s); issuer clock skew suspected" ) if now > att.exp + skew_tolerance: raise AttestationTemporalError( f"attestation for {att.did} expired: exp={att.exp:.0f} " f"is {(now - att.exp):.0f}s behind verifier now={now:.0f}" ) def bootstrap_cross_org(att: AAIFAttestation, verifier_ntp_error_bound: float = 30.0) -> str: verify_temporal_anchor( att, now=time.time(), skew_tolerance=30.0, verifier_ntp_error_bound=verifier_ntp_error_bound, ) return f"trust anchored for {att.did}" ```