niko_trust/internal/protocol/objects.go
Niko Marmeladkov 20cc52c3a5 feat: network layer — PoW, checkpoint chain, gossip, light node, WS, delegation, rotation, BFT
- BLAKE3 keyed proof-of-work on object storage and auth challenges,
  with frozen vectors cross-checked against an independent Python
  reference implementing the single-block hash it needs.
- Sparse Merkle trie over object IDs: order-independent roots,
  inclusion and absence proofs (internal/smt).
- Signed checkpoint chain per relay: transport key amendment to INV-1,
  /v1/checkpoint/* and inclusion/absence proof endpoints, restart-safe
  epoch continuity (internal/checkpoint).
- Head gossip with TOFU pinning and equivocation detection; light node
  (cmd/lightnode) that stores no history: quorum of pinned relays,
  every served object proven against the agreed root, LRU disk cache.
- WebSocket streaming on relay and light node (coder/websocket):
  scoped channels mirroring REST, raw envelopes verified client-side;
  light node marks streamed objects unproven until checkpoint coverage.
- Protocol v1 additions: DelegationClaim tag 0x07 with deterministic
  chain resolution in verify.Graph, KeyRotationRequest/Confirm tags
  0x08/0x09 with hash-bound two-sided consent and Policy.RotationMaxAge;
  spec sections, frozen vectors appended byte-identically, Python
  reference extended.
- Optional permissioned BFT finality over gossip (internal/bft):
  prevote/precommit with quorum certificates verifiable offline.
- Quick wins: Policy.TrustedIssuers, per-type stored metrics,
  batch fetch, lexicographic lists with stable cursor pagination.
- Security review of the network layer (docs/SECURITY-REVIEW.md) with
  findings F-01..F-09; hub send/close race and unstable pagination
  fixed under review.

12 packages green, vet/gofmt clean, protocol fuzzing stable.
2026-08-25 20:38:40 +03:00

396 lines
13 KiB
Go

// Package protocol defines the signed objects of the trust protocol and the
// rules for encoding, decoding and verifying them.
//
// Every object here is a statement made by an identity. The protocol
// establishes who made a statement and that it has not been altered. It never
// decides whether the statement should be believed or what it means: that is
// the consuming application's job (INV-5).
//
// The package can verify signatures but cannot create them. Signing lives in
// internal/identity/signer, which server-side code does not import, so a
// compromised server has no ability to forge anything (INV-1).
//
// This package does not import encoding/json. There is exactly one signing
// representation, and JSON is a transport syntax handled elsewhere
// (docs/IMPLEMENTATION_NOTES.md property 8).
package protocol
import (
"bytes"
"errors"
"git.n1ko.dev/Niko/niko_trust/internal/tce"
)
// Errors returned when an object violates a protocol rule.
var (
// ErrNil is returned when an encoder or verifier is handed a nil object.
ErrNil = errors.New("protocol: nil object")
// ErrSignature means the Ed25519 signature did not verify over the
// received canonical bytes.
ErrSignature = errors.New("protocol: signature does not verify")
// ErrSignatureSize means the signature was not 64 bytes.
ErrSignatureSize = errors.New("protocol: signature must be 64 bytes")
// ErrWrongObject means the canonical bytes decoded to a different object
// type than the caller expected.
ErrWrongObject = errors.New("protocol: unexpected object type")
// ErrRequestMismatch means an approval response does not commit to the
// request it was presented with.
ErrRequestMismatch = errors.New("protocol: response does not match request")
// ErrWrongResponder means the response was signed by an identity other
// than the request's recipient.
ErrWrongResponder = errors.New("protocol: responder is not the request recipient")
// ErrResponseTiming means the response is dated outside the request's
// validity window.
ErrResponseTiming = errors.New("protocol: response timestamp outside request window")
// ErrWrongIssuer means a revocation was signed by someone other than the
// issuer of the claim it targets.
ErrWrongIssuer = errors.New("protocol: revocation issuer is not the claim issuer")
// ErrWrongClaim means a revocation targets a different claim.
ErrWrongClaim = errors.New("protocol: revocation does not target this claim")
// ErrAudience means an auth assertion was produced for a different
// server.
ErrAudience = errors.New("protocol: auth assertion audience mismatch")
// ErrEmptyAudience means the caller did not supply an expected audience.
ErrEmptyAudience = errors.New("protocol: expected audience must not be empty")
// ErrExpired means the object's expiry has passed.
ErrExpired = errors.New("protocol: object has expired")
// ErrNotYetValid means the object is dated too far in the future.
ErrNotYetValid = errors.New("protocol: object created too far in the future")
// ErrSelfRevocation means a revocation targets an object that is not a
// claim.
ErrSelfRevocation = errors.New("protocol: revocation target is not a claim")
)
// MaxClockSkew is the tolerance applied when comparing a signed timestamp
// with local time.
//
// Timestamps are asserted by the signer, whose clock may differ from the
// verifier's. Without an allowance, honest objects would be rejected; with too
// large an allowance, expiry becomes meaningless. See PROTOCOL.md section
// 13.1.
const MaxClockSkew = 120
// Decision is an approval outcome.
type Decision uint8
// Decision values. There are exactly two; any other encoded value is
// rejected, so a verifier cannot encounter an outcome it has no rule for.
const (
Deny Decision = tce.DecisionDeny
Allow Decision = tce.DecisionAllow
)
// String renders a decision.
func (d Decision) String() string {
switch d {
case Allow:
return "allow"
case Deny:
return "deny"
default:
return "invalid"
}
}
// Valid reports whether d is a defined decision.
func (d Decision) Valid() bool { return d == Allow || d == Deny }
// ClaimStatus is the lifecycle state of a claim as understood by the protocol
// layer.
//
// There is deliberately no "denied" or "not found" status. Absence of a claim
// is not a protocol state at all: a relay can withhold anything, so a consumer
// that treated silence as denial could be manipulated by censorship. What an
// absent claim means is a decision for the application (INV-5, and
// docs/IMPLEMENTATION_NOTES.md property 6).
type ClaimStatus uint8
// Claim statuses.
const (
// StatusActive means the claim verified and has not expired or been
// revoked.
StatusActive ClaimStatus = iota
// StatusExpired means the claim's expires_at has passed.
StatusExpired
// StatusRevoked means a verified revocation by the claim's issuer exists.
StatusRevoked
)
// String renders a status.
func (s ClaimStatus) String() string {
switch s {
case StatusActive:
return "active"
case StatusExpired:
return "expired"
case StatusRevoked:
return "revoked"
default:
return "unknown"
}
}
// Identity is a self-asserted identity registration, object tag 0x01.
//
// Registration is a convenience for discovery, not a prerequisite: an identity
// exists because its key exists.
//
// Instances returned by the decode and verify functions carry the canonical
// bytes they were read from; instances constructed in memory do not.
type Identity struct {
// PubKey is the Ed25519 public key that signed this registration.
PubKey []byte
// Alias is a self-asserted display label. It is signed here so that the
// self-assertion is tamper-evident, but it remains non-authoritative: not
// unique, not verified, and never consulted when verifying any other
// object (INV-7). This is the only object in which an alias appears.
Alias string
// CreatedAt is the signer's assertion of when this was made.
CreatedAt uint64
// tce is the canonical bytes this object was decoded from, and sig the
// signature verified over them. Both are set only by the decode and verify
// functions. The encoders ignore both and rebuild from the field list, so
// an encoding can never drift from this package's rules by accident.
tce []byte
sig []byte
}
// TCE returns a copy of the canonical bytes this object was decoded from, or
// nil for an object constructed in memory. Copies are returned so that a
// caller cannot alter the byte string a verifier relies on.
func (o *Identity) TCE() []byte { return bytes.Clone(o.tce) }
// Signature returns a copy of the signature verified over the canonical
// bytes, or nil if none was verified.
func (o *Identity) Signature() []byte { return bytes.Clone(o.sig) }
// Claim is a signed statement by an issuer about a subject, object tag 0x02.
//
// The meaning of the keys and values is entirely outside the protocol. To
// every component of the trust system they are opaque strings.
type Claim struct {
Issuer []byte
Subject []byte
// Claims holds the statements. At least one entry, at most 32.
Claims map[string]tce.Value
CreatedAt uint64
// ExpiresAt is 0 for a claim that does not expire, otherwise a timestamp
// strictly after CreatedAt.
ExpiresAt uint64
// Serial lets an issuer supersede an earlier claim about the same
// subject. It is guidance for consumers; no relay enforces an ordering.
Serial uint64
// Nonce makes otherwise identical claims distinct, so that two claims
// with the same content and timestamp have different object IDs.
Nonce []byte
tce []byte
sig []byte
}
// TCE returns a copy of the canonical bytes this claim was decoded from, or
// nil for a claim constructed in memory.
func (o *Claim) TCE() []byte { return bytes.Clone(o.tce) }
// Signature returns a copy of the signature verified over the canonical
// bytes, or nil if none was verified.
func (o *Claim) Signature() []byte { return bytes.Clone(o.sig) }
// Revocation withdraws a claim, object tag 0x03.
//
// It must be signed by the same issuer as the claim it targets. A revocation
// signed by anyone else is meaningless and is rejected.
type Revocation struct {
Issuer []byte
ClaimID tce.ID
Reason string
CreatedAt uint64
Nonce []byte
tce []byte
sig []byte
}
// TCE returns a copy of the canonical bytes this revocation was decoded
// from, or nil for one constructed in memory.
func (o *Revocation) TCE() []byte { return bytes.Clone(o.tce) }
// Signature returns a copy of the signature verified over the canonical
// bytes, or nil if none was verified.
func (o *Revocation) Signature() []byte { return bytes.Clone(o.sig) }
// ApprovalRequest asks a recipient to approve an opaque action, tag 0x04.
type ApprovalRequest struct {
Sender []byte
Recipient []byte
// Action and Payload are opaque. Neither the relay nor this package
// assigns them meaning.
Action string
Payload map[string]tce.Value
// Message is what a human will read when approving. It is signed, so it
// cannot be altered in transit, but it is written by the sender: a client
// must display the sender's address alongside it and must not present it
// as though the relay endorsed it.
Message string
CreatedAt uint64
// ExpiresAt must be after CreatedAt by at most 60 seconds. The bound is
// part of the format, so an over-long request is invalid everywhere
// rather than merely refused by one server.
ExpiresAt uint64
Nonce []byte
tce []byte
sig []byte
}
// TCE returns a copy of the canonical bytes this request was decoded from,
// or nil for one constructed in memory.
func (o *ApprovalRequest) TCE() []byte { return bytes.Clone(o.tce) }
// Signature returns a copy of the signature verified over the canonical
// bytes, or nil if none was verified.
func (o *ApprovalRequest) Signature() []byte { return bytes.Clone(o.sig) }
// ApprovalResponse is a recipient's signed decision, object tag 0x05.
type ApprovalResponse struct {
// RequestHash is the object ID of the exact request being answered. It
// commits to the full canonical request rather than to a sender-chosen
// label, which is what makes a signed decision impossible to move to a
// different request (INV-4).
RequestHash tce.ID
Responder []byte
Decision Decision
CreatedAt uint64
Nonce []byte
tce []byte
sig []byte
}
// TCE returns a copy of the canonical bytes this response was decoded from,
// or nil for one constructed in memory.
func (o *ApprovalResponse) TCE() []byte { return bytes.Clone(o.tce) }
// Signature returns a copy of the signature verified over the canonical
// bytes, or nil if none was verified.
func (o *ApprovalResponse) Signature() []byte { return bytes.Clone(o.sig) }
// AuthAssertion proves possession of a private key for one server-issued
// challenge, object tag 0x06.
//
// It is a transport capability only: it authenticates a connection and grants
// nothing.
type AuthAssertion struct {
PubKey []byte
Challenge []byte
Scope string
// Audience binds the assertion to one server, so that an assertion
// produced for one relay cannot be replayed to another. Claims and
// approvals carry no audience because they are global statements meant to
// be portable between relays.
Audience string
CreatedAt uint64
tce []byte
sig []byte
}
// TCE returns a copy of the canonical bytes this assertion was decoded from,
// or nil for one constructed in memory.
func (o *AuthAssertion) TCE() []byte { return bytes.Clone(o.tce) }
// Signature returns a copy of the signature verified over the canonical
// bytes, or nil if none was verified.
func (o *AuthAssertion) Signature() []byte { return bytes.Clone(o.sig) }
// DelegationClaim grants another identity the right to speak, object tag
// 0x07.
//
// The granter asserts: "grantee may issue claims whose predicate is one of
// Predicates, as if I had issued them myself, re-delegating at most MaxDepth
// further times." Resolution — walking a chain of such objects from a root
// the verifier already trusts down to a claim's actual issuer — lives in
// internal/verify. The protocol layer only establishes who granted what to
// whom and that the grant is intact.
//
// A delegation is revoked exactly like a claim: a Revocation signed by the
// granter targeting this object's ID.
type DelegationClaim struct {
// Granter signs this object and lends its authority.
Granter []byte
// Grantee is the identity allowed to speak under the grant.
Grantee []byte
// Predicates are the exact claim keys covered, each with value true.
// At least one entry, at most 32; prefix or wildcard matching is
// deliberately not a thing (docs/DELEGATION.md).
Predicates map[string]tce.Value
// MaxDepth is how many further delegation hops may sit below this one.
// Zero means the grantee speaks for itself only.
MaxDepth uint64
CreatedAt uint64
// ExpiresAt is 0 for a grant that does not expire, otherwise strictly
// after CreatedAt.
ExpiresAt uint64
// Serial lets the granter supersede an earlier grant with new terms.
Serial uint64
Nonce []byte
tce []byte
sig []byte
}
// TCE returns a copy of the canonical bytes this delegation was decoded
// from, or nil for one constructed in memory.
func (o *DelegationClaim) TCE() []byte { return bytes.Clone(o.tce) }
// Signature returns a copy of the signature verified over the canonical
// bytes, or nil if none was verified.
func (o *DelegationClaim) Signature() []byte { return bytes.Clone(o.sig) }
// Covers reports whether p is among the delegated predicates.
func (o *DelegationClaim) Covers(p string) bool {
v, ok := o.Predicates[p]
if !ok {
return false
}
b, isBool := v.Bool()
return isBool && b
}