- 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
182 lines
6.1 KiB
Go
182 lines
6.1 KiB
Go
// Package signer holds the only private key handling in the tree.
|
|
//
|
|
// CLIENT-SIDE ONLY. Server-side packages must never import this package.
|
|
// The rule is mechanically enforced by TestServerPackagesHaveNoSigner in the
|
|
// api layer and by TestSignerIsNotReachableFromProtocol here, so a violation
|
|
// fails the build rather than merely contradicting a comment.
|
|
//
|
|
// The separation exists because of INV-1: a compromise of trust.n1ko.dev must
|
|
// not allow forging an identity, claim, approval or response. That property
|
|
// holds only if the server never possesses a signing key. Keeping the signing
|
|
// capability in a package the server does not link makes the guarantee
|
|
// structural instead of a matter of discipline.
|
|
//
|
|
// It also serves INV-2: the server is never an issuer. It has nothing to issue
|
|
// with.
|
|
package signer
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"errors"
|
|
"io"
|
|
|
|
"git.n1ko.dev/Niko/niko_trust/internal/address"
|
|
"git.n1ko.dev/Niko/niko_trust/internal/identity"
|
|
)
|
|
|
|
var (
|
|
// ErrSeedSize is returned when a seed is not 32 bytes.
|
|
ErrSeedSize = errors.New("signer: seed must be 32 bytes")
|
|
|
|
// ErrKeySize is returned when a private key is not 64 bytes.
|
|
ErrKeySize = errors.New("signer: private key must be 64 bytes")
|
|
|
|
// ErrKeyMismatch is returned when a private key's embedded public half
|
|
// does not match the public key derived from its seed. Such a key would
|
|
// produce signatures that do not verify under the advertised identity.
|
|
ErrKeyMismatch = errors.New("signer: private key is inconsistent")
|
|
)
|
|
|
|
// Signer owns an Ed25519 private key and can sign canonical protocol bytes.
|
|
//
|
|
// The zero Signer is unusable. Signer is deliberately not comparable to a
|
|
// public type and provides no accessor returning the raw private key other
|
|
// than the explicitly named Seed and PrivateKey methods, which exist only so
|
|
// that the keystore can persist the key.
|
|
type Signer struct {
|
|
priv ed25519.PrivateKey
|
|
id identity.Identity
|
|
}
|
|
|
|
// Generate creates a new identity and its private key using crypto/rand.
|
|
//
|
|
// The generated public key is validated exactly as an imported one would be.
|
|
// The probability of ed25519.GenerateKey producing a small-order key is
|
|
// negligible, but the check costs nothing and removes the need to reason about
|
|
// whether this path is special.
|
|
func Generate() (*Signer, error) {
|
|
return GenerateFrom(rand.Reader)
|
|
}
|
|
|
|
// GenerateFrom creates a new identity using the supplied entropy source.
|
|
//
|
|
// Callers outside tests should use Generate. Passing a source that is not
|
|
// cryptographically secure produces a key an attacker can reproduce.
|
|
func GenerateFrom(random io.Reader) (*Signer, error) {
|
|
if random == nil {
|
|
random = rand.Reader
|
|
}
|
|
pub, priv, err := ed25519.GenerateKey(random)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newSigner(priv, pub)
|
|
}
|
|
|
|
// FromSeed reconstructs a Signer from a 32-byte seed.
|
|
func FromSeed(seed []byte) (*Signer, error) {
|
|
if len(seed) != ed25519.SeedSize {
|
|
return nil, ErrSeedSize
|
|
}
|
|
priv := ed25519.NewKeyFromSeed(seed)
|
|
pub, ok := priv.Public().(ed25519.PublicKey)
|
|
if !ok {
|
|
return nil, ErrKeySize
|
|
}
|
|
return newSigner(priv, pub)
|
|
}
|
|
|
|
// FromPrivateKey reconstructs a Signer from a 64-byte Ed25519 private key.
|
|
//
|
|
// The key's embedded public half is checked against the public key derived
|
|
// from its seed. Without that check a malformed or tampered key file would
|
|
// yield a Signer whose signatures never verify under the identity it reports,
|
|
// which is a confusing failure to debug and a plausible way to trick a user
|
|
// into believing an action succeeded.
|
|
func FromPrivateKey(priv ed25519.PrivateKey) (*Signer, error) {
|
|
if len(priv) != ed25519.PrivateKeySize {
|
|
return nil, ErrKeySize
|
|
}
|
|
derived := ed25519.NewKeyFromSeed(priv.Seed())
|
|
if subtleCompare(derived, priv) != 1 {
|
|
return nil, ErrKeyMismatch
|
|
}
|
|
pub, ok := priv.Public().(ed25519.PublicKey)
|
|
if !ok {
|
|
return nil, ErrKeySize
|
|
}
|
|
return newSigner(priv, pub)
|
|
}
|
|
|
|
// newSigner validates the public half and builds the Signer.
|
|
func newSigner(priv ed25519.PrivateKey, pub ed25519.PublicKey) (*Signer, error) {
|
|
if err := address.ValidatePubKey(pub); err != nil {
|
|
return nil, err
|
|
}
|
|
id, err := identity.FromPubKey(pub)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Copy so that the caller's slice cannot be mutated underneath us.
|
|
stored := make(ed25519.PrivateKey, len(priv))
|
|
copy(stored, priv)
|
|
return &Signer{priv: stored, id: id}, nil
|
|
}
|
|
|
|
// Identity returns the public identity corresponding to the private key.
|
|
func (s *Signer) Identity() identity.Identity { return s.id }
|
|
|
|
// Address returns the signer's trust address.
|
|
func (s *Signer) Address() address.Address { return s.id.Address() }
|
|
|
|
// Public returns the public key.
|
|
func (s *Signer) Public() ed25519.PublicKey { return s.id.PubKey() }
|
|
|
|
// Sign signs msg with the private key.
|
|
//
|
|
// msg must be the canonical encoding of a protocol object, produced by the
|
|
// protocol layer, and must already carry that layer's domain separation tag.
|
|
// This package intentionally adds no framing of its own: adding a second,
|
|
// invisible layer of framing here would make the signed bytes depend on which
|
|
// code path produced them, which is precisely the ambiguity INV-8 forbids.
|
|
func (s *Signer) Sign(msg []byte) []byte {
|
|
return ed25519.Sign(s.priv, msg)
|
|
}
|
|
|
|
// SignerCrypto exposes the key as a crypto.Signer for interoperability.
|
|
func (s *Signer) SignerCrypto() crypto.Signer { return s.priv }
|
|
|
|
// Seed returns a copy of the 32-byte seed for persistence by the keystore.
|
|
//
|
|
// The name is blunt on purpose: a call site that reads Seed() is obviously
|
|
// handling secret material and should be scrutinised in review.
|
|
func (s *Signer) Seed() []byte {
|
|
seed := s.priv.Seed()
|
|
out := make([]byte, len(seed))
|
|
copy(out, seed)
|
|
return out
|
|
}
|
|
|
|
// PrivateKey returns a copy of the full private key for persistence.
|
|
func (s *Signer) PrivateKey() ed25519.PrivateKey {
|
|
out := make(ed25519.PrivateKey, len(s.priv))
|
|
copy(out, s.priv)
|
|
return out
|
|
}
|
|
|
|
// subtleCompare is a constant-time equality test returning 1 when equal.
|
|
func subtleCompare(a, b []byte) int {
|
|
if len(a) != len(b) {
|
|
return 0
|
|
}
|
|
var v byte
|
|
for i := range a {
|
|
v |= a[i] ^ b[i]
|
|
}
|
|
if v == 0 {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|