niko_trust/internal/protocol/decode.go
Niko Marmeladkov 9d66003689
Initial commit: signed-object trust relay, verifier, and docs
- server: relay storing signed objects (PUT/GET), per-IP rate limiting,
  per-subject quota (1000), one-response-per-request, pagination,
  /v1/healthz /v1/readyz /v1/metrics
- verify: signature-verifying trust evaluator; every object is checked via
  env.Verify(), approvals via VerifyApprovalResponse, revocations via
  VerifyRevocationOf; k-of-n approval quorum
- docs: TRUST-MODEL.md and API.md describing issuer-anchored signatures and
  the endpoint/status-code contract
- tests: server, verify, and ratelimit packages
2026-08-12 22:36:49 +03:00

339 lines
8.7 KiB
Go

package protocol
import (
"bytes"
"git.n1ko.dev/Niko/niko_trust/internal/address"
"git.n1ko.dev/Niko/niko_trust/internal/tce"
)
// Decoders for the six protocol objects.
//
// Each decoder reads the fields in the exact order PROTOCOL.md section 8
// lists them and enforces the whole-object size limit before touching the
// body. Identity public keys are validated as canonical curve points here,
// which is the step crypto/ed25519.Verify deliberately does not perform and
// therefore the one that must happen before any signature check
// (docs/PROTOCOL.md section 7.3).
//
// Every byte slice a decoder exposes is a copy: keys, nonces and the retained
// canonical bytes never alias the caller's buffer, so a decoded object cannot
// be altered after the fact by mutating the input.
// decodeIdentityField reads an identity field and validates its public key.
func decodeIdentityField(d *tce.Decoder, name string) ([]byte, error) {
pub, err := d.Identity()
if err != nil {
return nil, err
}
if err := address.ValidatePubKey(pub); err != nil {
return nil, fieldErr(name, err)
}
return pub, nil
}
// checkEnd asserts the object consumed the whole input: any byte after the
// last field would give the object a different ID and signature from the
// object it appears to contain.
func checkEnd(d *tce.Decoder) error {
return d.End()
}
// DecodeIdentity parses an IdentityRegistration from its canonical bytes.
func DecodeIdentity(b []byte) (*Identity, error) {
if len(b) > tce.MaxIdentityTCE {
return nil, tce.ErrObjectTooLarge
}
d := tce.NewDecoder(b)
tag, err := d.Header()
if err != nil {
return nil, err
}
if tag != tce.TagIdentity {
return nil, ErrWrongObject
}
pub, err := decodeIdentityField(d, "identity")
if err != nil {
return nil, err
}
alias, err := d.String(tce.MaxAliasLen)
if err != nil {
return nil, fieldErr("alias", err)
}
createdAt, err := d.Timestamp(false)
if err != nil {
return nil, fieldErr("created_at", err)
}
if err := checkEnd(d); err != nil {
return nil, err
}
o := &Identity{PubKey: pub, Alias: alias, CreatedAt: createdAt}
o.tce = bytes.Clone(b)
return o, nil
}
// DecodeClaim parses a Claim from its canonical bytes.
func DecodeClaim(b []byte) (*Claim, error) {
if len(b) > tce.MaxClaimTCE {
return nil, tce.ErrObjectTooLarge
}
d := tce.NewDecoder(b)
tag, err := d.Header()
if err != nil {
return nil, err
}
if tag != tce.TagClaim {
return nil, ErrWrongObject
}
issuer, err := decodeIdentityField(d, "issuer")
if err != nil {
return nil, err
}
subject, err := decodeIdentityField(d, "subject")
if err != nil {
return nil, err
}
claims, err := d.Map(1)
if err != nil {
return nil, fieldErr("claims", err)
}
createdAt, err := d.Timestamp(false)
if err != nil {
return nil, fieldErr("created_at", err)
}
expiresAt, err := d.Timestamp(true)
if err != nil {
return nil, fieldErr("expires_at", err)
}
if expiresAt != 0 && expiresAt <= createdAt {
return nil, fieldErr("expires_at", tce.ErrExpiry)
}
serial, err := d.Uvarint()
if err != nil {
return nil, fieldErr("serial", err)
}
nonce, err := d.FixedBytes(tce.NonceSize)
if err != nil {
return nil, fieldErr("nonce", err)
}
if err := checkEnd(d); err != nil {
return nil, err
}
o := &Claim{
Issuer: issuer, Subject: subject, Claims: claims,
CreatedAt: createdAt, ExpiresAt: expiresAt, Serial: serial,
Nonce: bytes.Clone(nonce),
}
o.tce = bytes.Clone(b)
return o, nil
}
// DecodeRevocation parses a Revocation from its canonical bytes.
func DecodeRevocation(b []byte) (*Revocation, error) {
if len(b) > tce.MaxRevocTCE {
return nil, tce.ErrObjectTooLarge
}
d := tce.NewDecoder(b)
tag, err := d.Header()
if err != nil {
return nil, err
}
if tag != tce.TagRevocation {
return nil, ErrWrongObject
}
issuer, err := decodeIdentityField(d, "issuer")
if err != nil {
return nil, err
}
claimHash, err := d.FixedBytes(tce.HashSize)
if err != nil {
return nil, fieldErr("claim_id", err)
}
claimID, err := tce.IDFromBytes(claimHash)
if err != nil {
return nil, fieldErr("claim_id", err)
}
reason, err := d.String(tce.MaxReasonLen)
if err != nil {
return nil, fieldErr("reason", err)
}
createdAt, err := d.Timestamp(false)
if err != nil {
return nil, fieldErr("created_at", err)
}
nonce, err := d.FixedBytes(tce.NonceSize)
if err != nil {
return nil, fieldErr("nonce", err)
}
if err := checkEnd(d); err != nil {
return nil, err
}
o := &Revocation{
Issuer: issuer, ClaimID: claimID, Reason: reason,
CreatedAt: createdAt, Nonce: bytes.Clone(nonce),
}
o.tce = bytes.Clone(b)
return o, nil
}
// DecodeApprovalRequest parses an ApprovalRequest from its canonical bytes.
func DecodeApprovalRequest(b []byte) (*ApprovalRequest, error) {
if len(b) > tce.MaxRequestTCE {
return nil, tce.ErrObjectTooLarge
}
d := tce.NewDecoder(b)
tag, err := d.Header()
if err != nil {
return nil, err
}
if tag != tce.TagApprovalRequest {
return nil, ErrWrongObject
}
sender, err := decodeIdentityField(d, "sender")
if err != nil {
return nil, err
}
recipient, err := decodeIdentityField(d, "recipient")
if err != nil {
return nil, err
}
action, err := d.String(tce.MaxActionLen)
if err != nil {
return nil, fieldErr("action", err)
}
payload, err := d.Map(0)
if err != nil {
return nil, fieldErr("payload", err)
}
message, err := d.String(tce.MaxMessageLen)
if err != nil {
return nil, fieldErr("message", err)
}
createdAt, err := d.Timestamp(false)
if err != nil {
return nil, fieldErr("created_at", err)
}
expiresAt, err := d.Timestamp(false)
if err != nil {
return nil, fieldErr("expires_at", err)
}
if expiresAt <= createdAt {
return nil, fieldErr("expires_at", tce.ErrExpiry)
}
if expiresAt-createdAt > tce.MaxApprovalLifetime {
return nil, fieldErr("expires_at", tce.ErrLifetime)
}
nonce, err := d.FixedBytes(tce.NonceSize)
if err != nil {
return nil, fieldErr("nonce", err)
}
if err := checkEnd(d); err != nil {
return nil, err
}
o := &ApprovalRequest{
Sender: sender, Recipient: recipient, Action: action,
Payload: payload, Message: message, CreatedAt: createdAt,
ExpiresAt: expiresAt, Nonce: bytes.Clone(nonce),
}
o.tce = bytes.Clone(b)
return o, nil
}
// DecodeApprovalResponse parses an ApprovalResponse from its canonical bytes.
//
// This only parses. Binding the response to a particular request is the job
// of VerifyApprovalResponse, which requires the request.
func DecodeApprovalResponse(b []byte) (*ApprovalResponse, error) {
if len(b) > tce.MaxResponseTCE {
return nil, tce.ErrObjectTooLarge
}
d := tce.NewDecoder(b)
tag, err := d.Header()
if err != nil {
return nil, err
}
if tag != tce.TagApprovalResponse {
return nil, ErrWrongObject
}
hashBytes, err := d.FixedBytes(tce.HashSize)
if err != nil {
return nil, fieldErr("request_hash", err)
}
requestHash, err := tce.IDFromBytes(hashBytes)
if err != nil {
return nil, fieldErr("request_hash", err)
}
responder, err := decodeIdentityField(d, "responder")
if err != nil {
return nil, err
}
decisionRaw, err := d.Uvarint()
if err != nil {
return nil, fieldErr("decision", err)
}
if Decision(decisionRaw) != Deny && Decision(decisionRaw) != Allow {
return nil, fieldErr("decision", tce.ErrDecision)
}
createdAt, err := d.Timestamp(false)
if err != nil {
return nil, fieldErr("created_at", err)
}
nonce, err := d.FixedBytes(tce.NonceSize)
if err != nil {
return nil, fieldErr("nonce", err)
}
if err := checkEnd(d); err != nil {
return nil, err
}
o := &ApprovalResponse{
RequestHash: requestHash, Responder: responder,
Decision: Decision(decisionRaw), CreatedAt: createdAt,
Nonce: bytes.Clone(nonce),
}
o.tce = bytes.Clone(b)
return o, nil
}
// DecodeAuthAssertion parses an AuthAssertion from its canonical bytes.
func DecodeAuthAssertion(b []byte) (*AuthAssertion, error) {
if len(b) > tce.MaxAuthTCE {
return nil, tce.ErrObjectTooLarge
}
d := tce.NewDecoder(b)
tag, err := d.Header()
if err != nil {
return nil, err
}
if tag != tce.TagAuthAssertion {
return nil, ErrWrongObject
}
pub, err := decodeIdentityField(d, "identity")
if err != nil {
return nil, err
}
challenge, err := d.FixedBytes(tce.ChallengeSize)
if err != nil {
return nil, fieldErr("challenge", err)
}
scope, err := d.String(tce.MaxScopeLen)
if err != nil {
return nil, fieldErr("scope", err)
}
audience, err := d.String(tce.MaxAudienceLen)
if err != nil {
return nil, fieldErr("audience", err)
}
createdAt, err := d.Timestamp(false)
if err != nil {
return nil, fieldErr("created_at", err)
}
if err := checkEnd(d); err != nil {
return nil, err
}
o := &AuthAssertion{
PubKey: pub, Challenge: bytes.Clone(challenge), Scope: scope,
Audience: audience, CreatedAt: createdAt,
}
o.tce = bytes.Clone(b)
return o, nil
}