baseline: P1 契约、agent 核心与中继投递(既有工作首次入库)
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
// Package runtime is where a verified envelope becomes a local HTTP call, or is refused.
|
||||
//
|
||||
// The checks here are ordered, and the order is the security property: a request is only decrypted
|
||||
// after its signature verifies, only routed after its policy decision, and only executed after its
|
||||
// confirmation intent is shown to match the exact bytes about to be forwarded. Every refusal is
|
||||
// classified, because the failure modes are indistinguishable from the outside — a node whose clock
|
||||
// has drifted looks exactly like an attacker replaying a request — and an operator who cannot tell
|
||||
// them apart will "fix" it by widening the policy.
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReplayClass names why a request was refused, in terms an operator can act on.
|
||||
type ReplayClass string
|
||||
|
||||
const (
|
||||
// ReplayDuplicate means this request id was already executed.
|
||||
ReplayDuplicate ReplayClass = "replay"
|
||||
// ReplayExpired means the timestamp is outside the clock-skew window.
|
||||
ReplayExpired ReplayClass = "expired"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrReplay is returned for a request id that has already been executed.
|
||||
ErrReplay = errors.New("runtime: request id was already used")
|
||||
// ErrExpired is returned for a timestamp outside the clock-skew window.
|
||||
ErrExpired = errors.New("runtime: request timestamp is outside the allowed window")
|
||||
)
|
||||
|
||||
// ReplayCache is a bounded set of request ids that have already run.
|
||||
//
|
||||
// Bounded, not growing: an attacker who can send traffic can also send distinct request ids, and a
|
||||
// cache that grows to fit them is a memory exhaustion primitive. The bound is set from
|
||||
// limits.replayCacheSize, which the policy asserts is larger than the relay mailbox depth, so an
|
||||
// entry can never be evicted while a copy of that request is still queued.
|
||||
//
|
||||
// Eviction order is insertion order rather than LRU. LRU would let a flood of fresh ids push out
|
||||
// the one id an attacker wants to reuse; FIFO eviction still admits that eventually, but only after
|
||||
// the whole window has turned over, and the timestamp window expires it first in any case.
|
||||
type ReplayCache struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]time.Time
|
||||
order []string
|
||||
limit int
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewReplayCache builds a cache holding at most limit ids, each forgotten after ttl even if the
|
||||
// cache is not yet full. The ttl is what makes the bound independent of traffic volume: ids older
|
||||
// than the skew window can no longer be accepted anyway.
|
||||
func NewReplayCache(limit int, ttl time.Duration) (*ReplayCache, error) {
|
||||
if limit <= 0 {
|
||||
return nil, errors.New("runtime: replay cache limit must be positive")
|
||||
}
|
||||
if ttl <= 0 {
|
||||
return nil, errors.New("runtime: replay cache ttl must be positive")
|
||||
}
|
||||
return &ReplayCache{
|
||||
entries: make(map[string]time.Time, limit),
|
||||
order: make([]string, 0, limit),
|
||||
limit: limit,
|
||||
ttl: ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Seen reports whether id has already been executed, and records it if not.
|
||||
//
|
||||
// The check and the insert are one call, not two. Splitting them invites a caller to check, decide,
|
||||
// and then forget to record — which is a replay window that no test would name.
|
||||
func (c *ReplayCache) Seen(id string, now time.Time) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.expire(now)
|
||||
if _, ok := c.entries[id]; ok {
|
||||
return true
|
||||
}
|
||||
if len(c.order) >= c.limit {
|
||||
evicted := c.order[0]
|
||||
c.order = c.order[1:]
|
||||
delete(c.entries, evicted)
|
||||
}
|
||||
c.order = append(c.order, id)
|
||||
c.entries[id] = now
|
||||
return false
|
||||
}
|
||||
|
||||
// Len reports how many ids are held. Used by the health surface and by tests.
|
||||
func (c *ReplayCache) Len(now time.Time) int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.expire(now)
|
||||
return len(c.entries)
|
||||
}
|
||||
|
||||
// expire drops entries older than the ttl. Called under the caller's lock, never exported.
|
||||
func (c *ReplayCache) expire(now time.Time) {
|
||||
cutoff := now.Add(-c.ttl)
|
||||
for len(c.order) > 0 {
|
||||
oldest := c.order[0]
|
||||
stamp, ok := c.entries[oldest]
|
||||
if !ok || stamp.After(cutoff) {
|
||||
return
|
||||
}
|
||||
c.order = c.order[1:]
|
||||
delete(c.entries, oldest)
|
||||
}
|
||||
}
|
||||
|
||||
// Freshness applies the timestamp window. It is separate from the cache so the two refusals stay
|
||||
// distinguishable in the audit log: "expired" means set the clock, "replay" means investigate.
|
||||
func Freshness(requestTS, now uint64, skewSeconds int) error {
|
||||
if skewSeconds <= 0 {
|
||||
return errors.New("runtime: clock skew window must be positive")
|
||||
}
|
||||
delta := int64(now) - int64(requestTS)
|
||||
if delta < 0 {
|
||||
delta = -delta
|
||||
}
|
||||
if delta > int64(skewSeconds) {
|
||||
return ErrExpired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user