Skip to content

Replay Protection

Every INK message MUST include:

  • nonce: A base64url-encoded random value, 16 to 256 characters. 22 characters (128 bits) is the recommended minimum; conforming receivers MUST accept any length in that range.
  • timestamp: ISO 8601 UTC timestamp.

Validation Rules

Receiving agents MUST:

  1. Reject messages with timestamps older than 5 minutes from the receiver’s clock.
  2. Reject messages with timestamps in the future by more than 30 seconds.
  3. Track seen nonces and reject duplicates for at least the timestamp window. A 10-minute retention window is recommended so a nonce stays tracked after its timestamp expires.
  4. Record a nonce only after the signature verifies, so a forged request cannot pollute the store, and reject an authentic replay inside the window.

Scoping the nonce store

A nonce store MAY be global to the receiver, or scoped per sender.

A global store needs no scope key. It enforces single use across every sender, which is strictly stronger than per-sender scoping and is the simplest thing to get right.

A per-sender store MUST key on the canonical principal, never on the raw from spelling. tulpa:zKEY and ink:zKEY are two spellings of the same key and therefore the same actor. A store keyed on the raw value splits one replay set into two, and a sender that replays a captured request under the other prefix presents the same signed bytes to an empty half of the store. The canonicalization is the one in Identity, exposed as canonicalAgentPrincipal(agentId), and it is the same value every other per-sender control has to key on: block lists, rate limits, duplicate-payload checks. Apply it once, at the storage boundary, on the raw agentId.

A distributed store SHOULD implement an atomic check-and-record rather than a read followed by a write, or two concurrent replays of one captured request can both pass the check before either records. The reference NonceStore interface takes an optional addIfAbsent for exactly this and prefers it when present.

Nonce storage is otherwise the integrator’s responsibility. The library exposes a checkReplay() primitive that decides whether a single (timestamp, nonce) pair is fresh given a caller-supplied seen-nonce set; the caller owns the cache, its scope key and its TTL. Because the primitive is stateless it cannot pin your choice of scope key for you, and no conformance vector can: the scope key is a property of the store you build around it.

Rationale

The 5-minute window accommodates reasonable clock skew between agents while limiting the replay attack surface. The 30-second future tolerance prevents rejection of messages from slightly fast clocks.

The recommended 10-minute nonce-retention window is intentionally larger than the timestamp window so a nonce is still tracked after the timestamp becomes invalid, preventing an edge case where a message’s timestamp expires but its nonce is purged from tracking, potentially allowing re-acceptance with a fresh timestamp.