351 lines
14 KiB
Go
351 lines
14 KiB
Go
package delivery
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// **The vault signs** (novox/hq ADR 0550 §2; design 47's L3 item d). The vault makes an ed25519 signing key and an x509
|
|
// authority once, keeps both as its own secrets with their operator copies, and publishes their public halves in its
|
|
// state `signers`, which only the vault writes and every node principal and composing part reads. Its verb `sign` signs a
|
|
// node's certificate for that node's own node-engine, from bus state alone (the name from the node's placement, the
|
|
// serving key from its record), and says `signed` naming the caller, the kind, the node and the digest, never the key.
|
|
//
|
|
// A node trusts the vault's key through the bus grants alone (the lead's design limit, 2026-10-28): only the vault may
|
|
// write `signers`, the same trust the wants and the placement rest on. Until the controller's lease is handed away the
|
|
// node trusts the controller's key beside it; from then on the vault's alone (LeaseHandedAway). What a node trusts now
|
|
// it states in its record, NodeRecord.Trusts, so the gate before the lease write reads it from the bus.
|
|
|
|
const (
|
|
// VaultModule is the module holding the seat mesh-vault, whose state `signers` is.
|
|
VaultModule = "mesh-vault"
|
|
// SignersState is the vault's state of its public signing halves, and SignersBucket its bucket.
|
|
SignersState = "signers"
|
|
SignersBucket = VaultModule + "_" + SignersState
|
|
// SignersRead is the state as a manifest's `reads` names it.
|
|
SignersRead = VaultModule + "." + SignersState
|
|
// SignersKeyKey holds the signing key's public half (SignerKey); SignersAuthorityKey the authority (SignerAuthority).
|
|
SignersKeyKey = "key"
|
|
SignersAuthorityKey = "authority"
|
|
|
|
// SignVerb is the vault's verb that signs.
|
|
SignVerb = "sign"
|
|
// SignedEvent is what the vault says when it signed (Signed), on SignedSubject.
|
|
SignedEvent = "signed"
|
|
SignedSubject = "mesh.mod." + VaultModule + ".event." + SignedEvent
|
|
|
|
// KindNodeCertificate is a node's certificate for its internal name, asked by that node's node-engine alone.
|
|
KindNodeCertificate = "node-certificate"
|
|
// KindEnrolment is an enrolment reply, signed under its own context; no caller is named until adoption's record
|
|
// (ADR 0561 §5).
|
|
KindEnrolment = "enrolment"
|
|
// KindAuthority is the vault's own authority certificate, which the vault signs for itself.
|
|
KindAuthority = "authority"
|
|
|
|
// SignerController and SignerVault name the two signers a node may trust.
|
|
SignerController = "controller"
|
|
SignerVault = "vault"
|
|
|
|
// CertificateLife is how long a certificate the vault signs for a node lasts: the controller's ten years
|
|
// (identity.forever), renewed by asking again, never by a clock running out unseen.
|
|
CertificateLife = 10 * 365 * 24 * time.Hour
|
|
)
|
|
|
|
// SignKinds are the kinds `sign` knows.
|
|
var SignKinds = []string{KindNodeCertificate, KindEnrolment, KindAuthority}
|
|
|
|
// SignContext is the line a message of kind is signed after, so a signature made for one kind never verifies as
|
|
// another's (as the composition's and the asks' contexts are kept apart).
|
|
func SignContext(kind string) (string, error) {
|
|
if !slices.Contains(SignKinds, kind) {
|
|
return "", fmt.Errorf("delivery: %q is not a kind the vault signs (%s)", kind, strings.Join(SignKinds, ", "))
|
|
}
|
|
return "novox " + kind + " v1\n", nil
|
|
}
|
|
|
|
// SignerKey is the key `key` of `signers`: the vault's signing key's public half in standard base64, its fingerprint,
|
|
// and when it was made.
|
|
type SignerKey struct {
|
|
Key string `json:"key"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
Made time.Time `json:"made"`
|
|
}
|
|
|
|
// SignerAuthority is the key `authority` of `signers`: the vault's authority certificate as PEM, its fingerprint, and
|
|
// when it was made.
|
|
type SignerAuthority struct {
|
|
Certificate string `json:"certificate"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
Made time.Time `json:"made"`
|
|
}
|
|
|
|
// SignerFingerprint is an ed25519 public key's fingerprint, `sha256:<hex>` of its 32 bytes.
|
|
func SignerFingerprint(key ed25519.PublicKey) string {
|
|
sum := sha256.Sum256(key)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// AuthorityFingerprint is an authority certificate's fingerprint, `sha256:<hex>` of its DER; refused when certificate is
|
|
// not one PEM certificate.
|
|
func AuthorityFingerprint(certificate string) (string, error) {
|
|
c, err := ParseCertificate(certificate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sum := sha256.Sum256(c.Raw)
|
|
return "sha256:" + hex.EncodeToString(sum[:]), nil
|
|
}
|
|
|
|
// ParseCertificate is the first certificate of a PEM text.
|
|
func ParseCertificate(text string) (*x509.Certificate, error) {
|
|
block, _ := pem.Decode([]byte(strings.TrimSpace(text)))
|
|
if block == nil || block.Type != "CERTIFICATE" {
|
|
return nil, errors.New("delivery: not a PEM certificate")
|
|
}
|
|
return x509.ParseCertificate(block.Bytes)
|
|
}
|
|
|
|
// NewSignerKey is the value of `key` for pub.
|
|
func NewSignerKey(pub ed25519.PublicKey, made time.Time) SignerKey {
|
|
return SignerKey{Key: base64.StdEncoding.EncodeToString(pub), Fingerprint: SignerFingerprint(pub), Made: made.UTC()}
|
|
}
|
|
|
|
// PublicKey is k's key, checked: an ed25519 public key whose fingerprint is the one k names.
|
|
func (k SignerKey) PublicKey() (ed25519.PublicKey, error) {
|
|
raw, err := base64.StdEncoding.DecodeString(k.Key)
|
|
if err != nil || len(raw) != ed25519.PublicKeySize {
|
|
return nil, errors.New("delivery: the signers' key is not an ed25519 public key in base64")
|
|
}
|
|
if fp := SignerFingerprint(raw); fp != k.Fingerprint {
|
|
return nil, fmt.Errorf("delivery: the signers' key has the fingerprint %s, and names %s", fp, k.Fingerprint)
|
|
}
|
|
return ed25519.PublicKey(raw), nil
|
|
}
|
|
|
|
// DecodeSignerKey reads the key `key` of `signers`, refused when it does not check.
|
|
func DecodeSignerKey(raw []byte) (SignerKey, error) {
|
|
var k SignerKey
|
|
if err := json.Unmarshal(raw, &k); err != nil {
|
|
return SignerKey{}, fmt.Errorf("delivery: %s %s: %w", SignersRead, SignersKeyKey, err)
|
|
}
|
|
if _, err := k.PublicKey(); err != nil {
|
|
return SignerKey{}, err
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
// DecodeSignerAuthority reads the key `authority` of `signers`: one authority certificate (a CA that signs leaves
|
|
// alone) whose fingerprint is the one named.
|
|
func DecodeSignerAuthority(raw []byte) (SignerAuthority, error) {
|
|
var a SignerAuthority
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return SignerAuthority{}, fmt.Errorf("delivery: %s %s: %w", SignersRead, SignersAuthorityKey, err)
|
|
}
|
|
c, err := ParseCertificate(a.Certificate)
|
|
if err != nil {
|
|
return SignerAuthority{}, err
|
|
}
|
|
if !c.IsCA {
|
|
return SignerAuthority{}, errors.New("delivery: the signers' authority is not an authority certificate")
|
|
}
|
|
if fp, _ := AuthorityFingerprint(a.Certificate); fp != a.Fingerprint {
|
|
return SignerAuthority{}, fmt.Errorf("delivery: the signers' authority has the fingerprint %s, and names %s", fp, a.Fingerprint)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// Signer is one key a node trusts, by who holds it (SignerController, SignerVault, or the operator's).
|
|
type Signer struct {
|
|
Name string
|
|
Key ed25519.PublicKey
|
|
}
|
|
|
|
// Fingerprint is the signer's key's fingerprint.
|
|
func (s Signer) Fingerprint() string { return SignerFingerprint(s.Key) }
|
|
|
|
// VerifyBy is the trusted signer whose key verifies sig over context and msg: refused when no trusted key does, saying
|
|
// so, and when a key is not an ed25519 key. A signature made under another context never verifies.
|
|
func VerifyBy(trusted []Signer, context string, msg, sig []byte) (Signer, error) {
|
|
if len(sig) != ed25519.SignatureSize {
|
|
return Signer{}, errors.New("delivery: not an ed25519 signature")
|
|
}
|
|
message := append([]byte(context), msg...)
|
|
for _, s := range trusted {
|
|
if len(s.Key) != ed25519.PublicKeySize {
|
|
continue
|
|
}
|
|
if ed25519.Verify(s.Key, message, sig) {
|
|
return s, nil
|
|
}
|
|
}
|
|
names := make([]string, 0, len(trusted))
|
|
for _, s := range trusted {
|
|
names = append(names, s.Name)
|
|
}
|
|
return Signer{}, fmt.Errorf("delivery: the signature verifies against no trusted key (%s)", strings.Join(names, ", "))
|
|
}
|
|
|
|
// SignMessage is sig over context and msg by key: what the vault's `sign` does for a kind whose bytes are given.
|
|
func SignMessage(key ed25519.PrivateKey, context string, msg []byte) []byte {
|
|
return ed25519.Sign(key, append([]byte(context), msg...))
|
|
}
|
|
|
|
// Signed is the vault's `signed`: who asked, the kind, the node, and the sha256 digest of what was signed. Never a key.
|
|
type Signed struct {
|
|
Caller string `json:"caller"`
|
|
Kind string `json:"kind"`
|
|
Node string `json:"node,omitempty"`
|
|
Digest string `json:"digest"`
|
|
Signer string `json:"signer"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// SignAnswer is what `sign` answers: for a node certificate the certificate as PEM and the authority that signed it,
|
|
// for an enrolment the signature in base64; always the signer's fingerprint and the digest. Never a key's private half.
|
|
type SignAnswer struct {
|
|
Kind string `json:"kind"`
|
|
Node string `json:"node,omitempty"`
|
|
Certificate string `json:"certificate,omitempty"`
|
|
Authority string `json:"authority,omitempty"`
|
|
Signature string `json:"signature,omitempty"`
|
|
Signer string `json:"signer"`
|
|
Digest string `json:"digest"`
|
|
}
|
|
|
|
// DigestOf is the digest a `signed` names: `sha256:<hex>` of the bytes signed.
|
|
func DigestOf(b []byte) string {
|
|
sum := sha256.Sum256(b)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// NodeCertificateTemplate is the certificate the vault signs for node's internal name, as the controller's authority
|
|
// shaped it (identity.Certify): the name in the subject and the subject alternative names, server and client use, the
|
|
// serial derived from the name and the key so the same ask gives the same certificate, and its life from notBefore.
|
|
func NodeCertificateTemplate(name string, servingKey ed25519.PublicKey, notBefore time.Time) *x509.Certificate {
|
|
sum := sha256.Sum256(append([]byte(name+"\n"), servingKey...))
|
|
return &x509.Certificate{
|
|
SerialNumber: new(big.Int).SetBytes(sum[:16]),
|
|
Subject: pkix.Name{CommonName: name},
|
|
DNSNames: []string{name},
|
|
NotBefore: notBefore.UTC().Truncate(time.Second),
|
|
NotAfter: notBefore.UTC().Truncate(time.Second).Add(CertificateLife),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
}
|
|
}
|
|
|
|
// AuthorityTemplate is the vault's authority certificate for its key, made once: an authority that signs leaves and no
|
|
// other authority, as the controller's.
|
|
func AuthorityTemplate(key ed25519.PublicKey, made time.Time) *x509.Certificate {
|
|
sum := sha256.Sum256(append([]byte("authority\n"), key...))
|
|
return &x509.Certificate{
|
|
SerialNumber: new(big.Int).SetBytes(sum[:16]),
|
|
Subject: pkix.Name{CommonName: "the mesh's vault"},
|
|
NotBefore: made.UTC().Truncate(time.Second),
|
|
NotAfter: made.UTC().Truncate(time.Second).Add(2 * CertificateLife),
|
|
IsCA: true,
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
BasicConstraintsValid: true,
|
|
MaxPathLen: 0,
|
|
MaxPathLenZero: true,
|
|
}
|
|
}
|
|
|
|
// CheckNodeCertificate says why certificate is not a certificate of name for servingKey by one of the authorities, or
|
|
// nil: a node uses it to tell whether its record holds a certificate from an authority it trusts, and another node to
|
|
// check one the vault signed (ADR 0550 §2's live check).
|
|
func CheckNodeCertificate(certificate string, authorities []string, name string, servingKey ed25519.PublicKey, at time.Time) error {
|
|
c, err := ParseCertificate(certificate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pool := x509.NewCertPool()
|
|
for _, a := range authorities {
|
|
pool.AppendCertsFromPEM([]byte(a)) // a bundle is every certificate it holds
|
|
}
|
|
if _, err := c.Verify(x509.VerifyOptions{Roots: pool, DNSName: name, CurrentTime: at,
|
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}); err != nil {
|
|
return fmt.Errorf("delivery: the certificate of %s is not one a trusted authority signed for it: %w", name, err)
|
|
}
|
|
held, ok := c.PublicKey.(ed25519.PublicKey)
|
|
if !ok || !held.Equal(servingKey) {
|
|
return fmt.Errorf("delivery: the certificate of %s binds another key than the node serves with", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AuthorityBundle is the authorities given as one PEM text, each certificate once, in the order given, blanks left out:
|
|
// what a module asking for the mesh's authority is given once the vault signs beside the controller (ADR 0550 §2). An
|
|
// authority given is split into its certificates, so a bundle given again (the one a node kept from its last
|
|
// composition) adds nothing twice; a text that holds no PEM block is taken whole.
|
|
func AuthorityBundle(authorities ...string) string {
|
|
var out []string
|
|
for _, a := range authorities {
|
|
for _, piece := range pemPieces(a) {
|
|
if !slices.Contains(out, piece) {
|
|
out = append(out, piece)
|
|
}
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return ""
|
|
}
|
|
return strings.Join(out, "\n") + "\n"
|
|
}
|
|
|
|
// pemPieces is each PEM block of text re-encoded, or the trimmed text whole when it holds none.
|
|
func pemPieces(text string) []string {
|
|
text = strings.TrimSpace(text)
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
var out []string
|
|
rest := []byte(text)
|
|
for {
|
|
block, more := pem.Decode(rest)
|
|
if block == nil {
|
|
break
|
|
}
|
|
out = append(out, strings.TrimSpace(string(pem.EncodeToMemory(block))))
|
|
rest = more
|
|
}
|
|
if len(out) == 0 {
|
|
return []string{text}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Trusts is what a node states it trusts now (NodeRecord.Trusts): `<who> key <fingerprint>` for each signer and
|
|
// `<who> authority <fingerprint>` for each authority certificate, by who holds it, each once, sorted. The gate before the
|
|
// lease write reads `vault key <fingerprint>` in every node's record.
|
|
func Trusts(signers []Signer, authorities map[string]string) []string {
|
|
var out []string
|
|
for _, s := range signers {
|
|
if len(s.Key) == ed25519.PublicKeySize {
|
|
out = append(out, s.Name+" key "+s.Fingerprint())
|
|
}
|
|
}
|
|
for who, a := range authorities {
|
|
if fp, err := AuthorityFingerprint(a); err == nil {
|
|
out = append(out, who+" authority "+fp)
|
|
}
|
|
}
|
|
slices.Sort(out)
|
|
return slices.Compact(out)
|
|
}
|
|
|
|
// TrustsKey says whether trusts names the key with fingerprint fp held by who.
|
|
func TrustsKey(trusts []string, who, fp string) bool { return slices.Contains(trusts, who+" key "+fp) }
|