niko_trust/internal/identity/identity.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

150 lines
5.3 KiB
Go

// Package identity defines a trust identity: an Ed25519 public key, its
// canonical trust address, and signature verification over that key.
//
// A trust identity is self-sufficient. It is created by generating a key pair,
// not by registering with anyone, and it carries no issuer, no certificate and
// no server-assigned attributes. There is no account type: a person, a game
// server, a daemon and a script are all just identities.
//
// Normative invariants enforced here:
//
// - INV-1: this package contains no signing capability whatsoever. It can
// verify, never produce. The server links only this package, so a full
// compromise of the server yields no ability to forge anything.
// - INV-2: there is no distinguished "trust.n1ko.dev" identity. Nothing in
// this package can mark one identity as more authoritative than another.
// - INV-3: identity is the public key. There is no numeric or database
// identifier anywhere in this type.
// - INV-5: this package answers "who signed this", never "is this allowed".
// There is deliberately no permission, role or capability concept.
// - INV-6: identities have no relationships here. Nothing links one identity
// to another, so no transitive trust can be derived at this layer.
// - INV-7: an Alias is a display label. It is not part of Identity, does not
// affect Equal, and never reaches any verification routine.
// - INV-9: imports are limited to the standard library and the address
// package. No storage, transport or application semantics.
//
// Private keys are handled exclusively by the client-only subpackage
// identity/signer. Server-side code must not import it.
package identity
import (
"crypto/ed25519"
"errors"
"git.n1ko.dev/Niko/niko_trust/internal/address"
)
// ErrInvalidSignature is returned when a signature does not verify.
var ErrInvalidSignature = errors.New("identity: invalid signature")
// SignatureSize is the size of an Ed25519 signature in bytes.
const SignatureSize = ed25519.SignatureSize
// Identity is a validated public identity.
//
// The zero Identity is invalid. A non-zero Identity always holds a public key
// that has passed curve validation, because the only ways to construct one go
// through the address package.
//
// Identity is comparable and safe to use as a map key.
type Identity struct {
addr address.Address
}
// FromAddress builds an Identity from an already validated address.
func FromAddress(a address.Address) (Identity, error) {
if a.IsZero() {
return Identity{}, address.ErrEmpty
}
return Identity{addr: a}, nil
}
// FromPubKey builds an Identity from an Ed25519 public key, validating the key
// as a canonical prime-order curve point.
func FromPubKey(pub ed25519.PublicKey) (Identity, error) {
a, err := address.FromPubKey(pub)
if err != nil {
return Identity{}, err
}
return Identity{addr: a}, nil
}
// Parse builds an Identity from a textual trust address.
func Parse(s string) (Identity, error) {
a, err := address.Parse(s)
if err != nil {
return Identity{}, err
}
return Identity{addr: a}, nil
}
// MustParse is Parse for constants and fixtures. It panics on error.
func MustParse(s string) Identity {
id, err := Parse(s)
if err != nil {
panic("identity: MustParse: " + err.Error())
}
return id
}
// Address returns the identity's trust address.
func (i Identity) Address() address.Address { return i.addr }
// String returns the canonical trust address text.
func (i Identity) String() string { return i.addr.String() }
// PubKey returns a copy of the identity's public key.
func (i Identity) PubKey() ed25519.PublicKey { return i.addr.PubKey() }
// IsZero reports whether i is the unset zero value.
func (i Identity) IsZero() bool { return i.addr.IsZero() }
// Equal reports whether two identities are the same key.
//
// Aliases are intentionally not considered (INV-7).
func (i Identity) Equal(other Identity) bool { return i.addr.Equal(other.addr) }
// Verify reports whether sig is a valid signature by this identity over msg.
//
// The message passed here must be the canonical encoding of a protocol object,
// never a JSON document or any other ambiguous representation (INV-8). This
// package cannot enforce that on its own; the protocol layer is responsible
// for only ever calling Verify with canonical bytes.
//
// Verify returns a boolean rather than an error so that callers cannot
// accidentally treat a non-nil error as "verified". Use VerifyErr when an
// error value is more convenient.
func (i Identity) Verify(msg, sig []byte) bool {
if i.IsZero() {
return false
}
if len(sig) != SignatureSize {
return false
}
// The key was validated at construction, so ed25519.Verify cannot be fed
// a small-order or non-canonical key through this path.
return ed25519.Verify(i.addr.PubKey(), msg, sig)
}
// VerifyErr is Verify returning ErrInvalidSignature instead of false.
func (i Identity) VerifyErr(msg, sig []byte) error {
if !i.Verify(msg, sig) {
return ErrInvalidSignature
}
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (i Identity) MarshalText() ([]byte, error) { return i.addr.MarshalText() }
// UnmarshalText implements encoding.TextUnmarshaler, running full address
// validation.
func (i *Identity) UnmarshalText(b []byte) error {
var a address.Address
if err := a.UnmarshalText(b); err != nil {
return err
}
i.addr = a
return nil
}