- 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
339 lines
8.7 KiB
Go
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
|
|
}
|