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.
11 lines
234 B
Go
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"
|
|
)
|