Files
pinpoxandbrianmcgee df6b6d62df {model, state, http, cmd}: add per-file TTL via signed ValidFor
Adds an optional TTL to file signatures so files can self-expire across
the cluster without requiring a tombstone. This is the missing primitive
for ephemeral discovery use cases (e.g. heartbeat-based peer registries)
where the publisher cannot rely on any other node having authority to
delete its files.

Design

  A new ValidFor time.Duration field is signed into the buffer as a
  conditional tail: 8 bytes big-endian int64 (nanoseconds), appended only
  when ValidFor > 0. When zero, the signed buffer is byte-identical to
  the pre-TTL format, so non-expiring files remain wire-compatible across
  mixed-version clusters.

  Verify() stays purely cryptographic. Expiry is enforced as a policy
  check at five call sites that all use the new Signature.Expired(now)
  helper:

    - Files.Get returns ErrFileNotFound for expired sigs (mirrors the
      existing tombstone-as-not-found behaviour).
    - listFiles HTTP handler skips expired entries.
    - putFile HTTP handler returns 400 for already-expired uploads.
    - shouldImportSignature (gossip ingress) rejects already-expired
      remote sigs with ClockSkewTolerance slack.
    - The new background sweeper.

  The gossip sender (localState) is unchanged: receivers reject expired
  imports, the sweeper bounds accumulation, and filtering on send adds
  complexity for negligible bandwidth savings.

Strip resistance

  ed25519 signs and verifies over exact bytes. A MITM that zeros
  ValidFor in transit causes the verifier to reconstruct a buffer
  without the conditional tail, which doesn't match the signature made
  with it, and verification fails. Tested as a regression guard.

Sweeper

  pkg/state/sweeper.go runs in the server's errgroup at
  SweepInterval (default 60s). It opens one write tx, walks
  ListSignatures in batches collecting expired entries (BoltDB cursors
  are invalidated by writes through them, so collection is a separate
  pass from deletion), then deletes the sig from BoltDB and removes
  the file from disk. Mirrors PutTombstone's tx-then-os.Remove-then-
  commit ordering to keep concurrent reads safe. No tombstone is
  produced — expiry is decided independently per node from signed
  metadata.

CLI / HTTP

  data-mesher file update gains --expires-in <duration>. The CLI sends
  X-Validfor as the duration's String() form (e.g. "10m0s") which is
  parseable by time.ParseDuration. echo's default binder cannot decode
  time.Duration from a string, so the field has no header tag and
  putFile parses X-Validfor manually after BindHeaders.

Config

  Two new fields: SweepInterval (default 60s) and ClockSkewTolerance
  (default 5m), both with --sweep_interval and --clock_skew_tolerance
  flags.

Tests

  - signature_test: SignFileWithTTL roundtrip, byte-identical-to-SignFile
    when validFor=0, ValidFor tampering breaks Verify, strip-resistance
    (zeroing ValidFor breaks Verify), Expired predicate.
  - files_test: Get hides expired, Get returns future-expiry and
    ValidFor=0 entries, SweepExpired removes only expired and is
    idempotent.
  - http files_test: PUT with already-expired ValidFor returns 400,
    PUT with live TTL persists the field round-trip.

Out of scope

  The millisecond-resolution PutIfLater race
  (pkg/state/signatures.go) is unchanged. Two SignFile calls in the
  same millisecond silently drop the second regardless of which had
  the longer/shorter ValidFor. Fixing requires changing the comparison
  to >= with a tiebreak, which has its own merge-semantics
  implications. Heartbeat loops at sub-millisecond rates are out of
  scope for now.
2026-04-10 15:26:30 +01:00

11 lines
234 B
Go

package http
const (
HeaderSignedBy = "X-Signedby"
HeaderSignedAt = "X-Signedat"
HeaderSignature = "X-Signature"
HeaderNetworkID = "X-Network-Id"
HeaderCertificate = "X-Certificate"
HeaderValidFor = "X-Validfor"
)