// 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) }