- 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
150 lines
5.3 KiB
Go
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
|
|
}
|