The vault signs: the contract of its state signers, its verb sign and its event signed, a node's grants to ask it and to read what it trusts, and the authority composed as a bundle (issue 774, ADR 0550 §2)

This commit is contained in:
2026-10-28 21:54:04 +01:00
parent 18a47ac3c7
commit d6db3f38e6
10 changed files with 465 additions and 10 deletions
+17
View File
@@ -131,6 +131,8 @@ func grantsAsTheControllers(t *testing.T, got, want string) {
}
}
const SignersBucketName = "mesh-vault_signers"
func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
// ADR 0567 §3, §9: a node reads every key of the seat's placement and its candidates (it composes with the whole
// proposal laid over), and none of its open proposals; it writes neither.
@@ -251,6 +253,21 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
if !slices.Contains(perms.Subscribe, VaultSecretChanged) {
t.Errorf("node one does not hear the vault's secret-changed: %v", perms.Subscribe)
}
// L3 s6 (ADR 0550 §2): while the vault serves `sign`, a node asks it in its own name alone, and reads the vault's
// `signers` and the lease key, never writing either.
signs, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", AsksTheVaultToSign: true})
signersGet := "$JS.API.DIRECT.GET.KV_" + SignersBucketName + ".$KV." + SignersBucketName + ".key"
leaseGet := "$JS.API.DIRECT.GET.KV_" + LeaseBucket + ".$KV." + LeaseBucket + ".holder"
for subject, want := range map[string]bool{VaultSignCall(one): true, signersGet: true, leaseGet: true,
VaultSignCall(Principal{Kind: KindNode, Node: "two"}): false, "$KV." + SignersBucketName + ".key": false,
"$KV." + LeaseBucket + ".holder": false} {
if MayPublish(signs, subject) != want {
t.Errorf("a node asking sign may publish %s: %v, want %v", subject, !want, want)
}
}
if MayPublish(perms, VaultSignCall(one)) || MayPublish(perms, signersGet) {
t.Error("a node asks sign or reads signers while the vault does not serve sign")
}
// And what a module on another node serves from its settings, that key alone (mailu's smtp domain, issue 1126).
users, err := Users(Records{Nodes: []string{"one", "two"}, Assigned: map[string][]Declared{
"two": {{Module: "mailer", Served: []string{"domain", "not.a.token"}}}}})
+22
View File
@@ -217,6 +217,11 @@ type Principal struct {
// ADR 0547 §2, ADR 0550 §5, issue 1126): its modules' own secrets sealed to its key, for the compositions it makes
// itself. Set while the vault's holder serves it.
AsksItsOwnSecrets bool
// AsksTheVaultToSign is a machine principal whose node-engine may ask the vault's `sign` in its own name, read the
// vault's public signing halves (`signers`) and the controller's lease key (novox/hq ADR 0550 §2): its own node's
// certificate, the keys it trusts, and the lease write that drops the controller's key. Set while the vault's holder
// serves `sign`.
AsksTheVaultToSign bool
// ServedReads are, for a machine principal, the keys of mesh-config's resolved values that other nodes' modules serve
// from (`<node>.<module>.<key>`, catalogue.ServedSettings): what every consumer is told, and so what a node composing
// its own modules reads of a provider elsewhere (issue 1126), each key alone. Sorted, none twice.
@@ -360,6 +365,10 @@ var VaultNodeKey = SeatVerb{Seat: "mesh-vault", Verb: "node-key"}
// key (issue 1126, ADR 0550 §5); granted as Principal.AsksItsOwnSecrets says.
var VaultOwn = SeatVerb{Seat: "mesh-vault", Verb: "own"}
// VaultSign is the vault's verb a machine's node-engine asks in its own name for its own node's certificate (novox/hq
// ADR 0550 §2); granted as Principal.AsksTheVaultToSign says.
var VaultSign = SeatVerb{Seat: "mesh-vault", Verb: delivery.SignVerb}
// VerbsTheControllerActsOnAWarrant are the other seat verbs the controller calls when the operator's warrant
// chooses them (novox/hq ADR 0259): a machine's service restarted, and a walk started or stopped through the
// controller's own `plans`, a machine's held builds sent by its own `push` and the builds held after a failed release
@@ -956,6 +965,14 @@ func PermissionsFor(p Principal) (Permissions, error) {
// on it rather than at the next unrelated change (ADR 0547 §2).
sub = append(sub, VaultSecretChanged)
}
// And, while the vault serves `sign`, its own node's certificate in its own name, the vault's public signing
// halves, read and watched, and the controller's lease key, read and watched, which says when the controller's key
// is no longer trusted (ADR 0550 §2). Read only.
if p.AsksTheVaultToSign {
pub = append(pub, VaultSignCall(p))
pub = append(pub, readGrants(delivery.SignersBucket, "")...)
pub = append(pub, readGrants(LeaseBucket, leaseKey)...)
}
// And, on the node holding mesh-broker, the grants part's sealed user list it follows from take-over (novox/hq ADR
// 0547 clause 7): one key, and an object by its name.
if p.FollowsGrantsUserList {
@@ -1848,6 +1865,11 @@ func VaultOwnCall(p Principal) string {
return "mesh.seat." + VaultOwn.Seat + "." + CallKind + "." + VaultOwn.Verb + "." + CallerToken(p.Username())
}
// VaultSignCall is the subject a machine principal's node-engine asks the vault's `sign` on, in its own name.
func VaultSignCall(p Principal) string {
return "mesh.seat." + VaultSign.Seat + "." + CallKind + "." + VaultSign.Verb + "." + CallerToken(p.Username())
}
// VaultSecretChanged is the vault's event that a secret changed (a name and its generation, never a value), which a
// node composing its own hears to compose again (issue 1126, ADR 0547 §2).
var VaultSecretChanged = "mesh.mod." + VaultOwn.Seat + ".event.secret-changed"
+1 -1
View File
@@ -165,7 +165,7 @@ func Users(r Records) ([]Principal, error) {
}
out = append(out, Principal{Kind: KindNode, Node: node, WitnessesController: witness, Checks: checks,
MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey),
AsksItsOwnSecrets: servesVerb(r, VaultOwn),
AsksItsOwnSecrets: servesVerb(r, VaultOwn), AsksTheVaultToSign: servesVerb(r, VaultSign),
FollowsGrantsUserList: bus, ServedReads: servedOn(served, node)})
// **And the machine's provisioner client, on an account of its own** (novox/hq ADR 0349 clause 11): one
// per machine that runs it, delivered to the module running it.
+8
View File
@@ -228,6 +228,14 @@ var defaultSeats = append([]Seat{
"ADR 0561 §9): only while the controller's lease is handed away, never a node's account or the " +
"controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.",
Input: schema(map[string]string{"user": "the bus user, e.g. ace.mesh-build-go"}, []string{"user"})},
{Name: "sign", Optional: true, Description: "Sign with the vault's key or authority (novox/hq ADR 0550 §2): " +
"kind node-certificate signs the certificate of the calling machine's internal name for the serving key " +
"its record states, built from bus state alone and asked by that machine's node-engine only; kind " +
"enrolment signs an enrolment reply under its own context, for a caller the adoption record names. " +
"Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.",
Input: schema(map[string]string{"kind": "node-certificate or enrolment",
"node": "the machine, for a node certificate: the caller's own",
"reply": "the enrolment reply's bytes in base64, for an enrolment"}, []string{"kind"})},
}},
// The owner of every config value (novox/hq ADR 0348 §9, design 57 K3): its holder alone writes the buckets
// mesh-config_values, _resolved, _predefined and _proposals, and serves the one verb `config` (§1, §10).
+17
View File
@@ -83,6 +83,16 @@ type Reader interface {
Served(ctx context.Context, node, module string, keys []string) (map[string]any, error)
}
// SignersReader is a Reader that also reads the vault's public authority from its state `signers` (delivery.SignersBucket,
// key delivery.SignersAuthorityKey; ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the
// controller's and the vault's; one that has not, or that answers none, composes the controller's alone, as before.
type SignersReader interface {
// VaultAuthority is the vault's authority certificate as PEM, checked (delivery.DecodeSignerAuthority); empty when the
// vault has published none. A read that fails is said by the reader and answered empty: a node that may not read the
// state yet composes as before rather than not at all.
VaultAuthority(ctx context.Context) string
}
// Mesh is what is true of the whole mesh and is no part's state on the bus yet: the private network's range and the
// internal names' suffix as genesis was told them, and the mesh's authority certificate (public). Empty fields take
// compose's defaults.
@@ -219,6 +229,13 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
}
}
}
// And the vault's authority beside it, once the vault publishes one (ADR 0550 §2): a certificate either signed checks
// against the bundle. The controller's stays while a certificate it issued is in a node's record.
if sr, ok := r.(SignersReader); ok {
if vault := sr.VaultAuthority(ctx); vault != "" {
in.Authority = delivery.AuthorityBundle(in.Authority, vault)
}
}
parse := func(raw []byte) (catalogue.Manifest, error) {
if mesh.Predefined != nil {
return catalogue.ParseManifestWith(raw, mesh.Predefined)
+18 -8
View File
@@ -25,14 +25,15 @@ import (
// bus is a Reader over values held in memory, counting whose config values were read.
type bus struct {
placements map[string]delivery.Placement
records map[string]delivery.NodeRecord
modules map[string]delivery.Module
manifests map[string][]byte
values map[string]map[string]any // by node.module
valuesRead []string
own []compose.OwnSecretRow
ownErr error
vaultAuthority string
placements map[string]delivery.Placement
records map[string]delivery.NodeRecord
modules map[string]delivery.Module
manifests map[string][]byte
values map[string]map[string]any // by node.module
valuesRead []string
own []compose.OwnSecretRow
ownErr error
// asked is every consumer credential asked; the fake refuses the whole ask, as the vault's own does, for a consuming
// module its placement does not put on the node.
asked []compose.CredentialKey
@@ -74,6 +75,7 @@ func (b *bus) Served(_ context.Context, node, module string, keys []string) (map
func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) {
return b.placements, nil
}
func (b *bus) VaultAuthority(context.Context) string { return b.vaultAuthority }
func (b *bus) Records(context.Context) (map[string]delivery.NodeRecord, error) { return b.records, nil }
func (b *bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.modules, nil }
func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) {
@@ -216,6 +218,14 @@ func TestInput(t *testing.T) {
wj, _ := json.MarshalIndent(want, "", " ")
t.Fatalf("Input from the bus differs from the store's:\n got %s\nwant %s", gj, wj)
}
// L3 s8 (ADR 0550 §2): with the vault's authority published, the mesh's authority is the bundle of the controller's
// and the vault's; with none, the controller's alone, as above.
signing := mk()
signing.vaultAuthority = "the vault's certificate"
if bundled, err := Input(ctx, signing, Mesh{}, Scope{Node: "laptop"}); err != nil ||
bundled.Authority != "the mesh's certificate\nthe vault's certificate\n" {
t.Errorf("the authority with the vault's published: %q, %v", bundled.Authority, err)
}
if !reflect.DeepEqual(b.valuesRead, []string{"laptop." + overlay.Name, "laptop.xterm", "laptop.media"}) {
t.Errorf("laptop read config values %v; a node reads its own slice alone", b.valuesRead)
}
+49
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"strings"
"testing"
"time"
)
func aSigningKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
@@ -100,4 +101,52 @@ func TestACompositionIsSignedByAKeptKey(t *testing.T) {
if _, err := SignComposedBy(c, pub, func(m []byte) ([]byte, error) { return ed25519.Sign(other, m), nil }); err == nil {
t.Errorf("a signature by another key than the one named was accepted")
}
// L3 s1 (ADR 0550 §2): a message the vault or the controller signed under a kind's context verifies against the
// signers a node trusts and names which; an unknown key and another kind's context are refused.
vaultPub, vault := aSigningKey(t)
controllerPub, controller := aSigningKey(t)
trusted := []Signer{{Name: SignerController, Key: controllerPub}, {Name: SignerVault, Key: vaultPub}}
enrol, _ := SignContext(KindEnrolment)
certCtx, _ := SignContext(KindNodeCertificate)
msg := []byte("the reply")
for _, row := range []struct {
name string
key ed25519.PrivateKey
signed string
checked string
want string
}{
{"the vault's key verifies", vault, enrol, enrol, SignerVault},
{"the controller's key verifies", controller, enrol, enrol, SignerController},
{"an unknown key is refused", other, enrol, enrol, ""},
{"the wrong context is refused", vault, certCtx, enrol, ""},
} {
got, err := VerifyBy(trusted, row.checked, msg, SignMessage(row.key, row.signed, msg))
if row.want == "" && err == nil {
t.Errorf("%s: verified as %s", row.name, got.Name)
}
if row.want != "" && (err != nil || got.Name != row.want) {
t.Errorf("%s: %+v, %v", row.name, got.Name, err)
}
}
if _, err := SignContext("hand-over"); err == nil {
t.Error("a kind the vault does not sign has a context")
}
// The signers' key reads back only when its fingerprint is its key's, and what a node trusts names both holders.
k := NewSignerKey(vaultPub, time.Unix(0, 0))
raw, _ := json.Marshal(k)
if back, err := DecodeSignerKey(raw); err != nil || back.Fingerprint != SignerFingerprint(vaultPub) {
t.Errorf("the signers' key: %+v, %v", back, err)
}
k.Fingerprint = SignerFingerprint(controllerPub)
if raw, _ := json.Marshal(k); true {
if _, err := DecodeSignerKey(raw); err == nil {
t.Error("a signers' key naming another fingerprint was taken")
}
}
if got := Trusts(trusted, nil); len(got) != 2 || !TrustsKey(got, SignerVault, SignerFingerprint(vaultPub)) ||
!TrustsKey(got, SignerController, SignerFingerprint(controllerPub)) {
t.Errorf("trusts: %v", got)
}
}
+4
View File
@@ -55,6 +55,10 @@ type NodeRecord struct {
// Authority is the mesh's own certificate as this machine holds it (public): the one its declaration last wrote where
// a module asked for it. A node composing its own composes a certificate with it (compose.Input.Authority).
Authority string `json:"authority,omitempty"`
// Trusts is what this machine trusts a mesh signature from now (Trusts in signers.go, ADR 0550 §2): the controller's
// key until the lease is handed away, and the vault's key and authority once it reads them from `signers`. The gate
// before the lease write reads every node's from the bus.
Trusts []string `json:"trusts,omitempty"`
}
func (r NodeRecord) check() error {
+1 -1
View File
@@ -18,7 +18,7 @@ var secretNamed = regexp.MustCompile(`(?i)(secret|passw|token|api[-_]?key|privat
func TestNodeRecord(t *testing.T) {
hub := NodeRecord{Node: "novox", TunnelKey: "k", Capabilities: []string{"docker"}, SealingKey: "age1x", ServingKey: "pub",
Certificate: "cert", OutwardLinks: []string{"eth0"}, Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}},
NetworksSaid: true, Contract: 3}
NetworksSaid: true, Contract: 3, Trusts: []string{"controller key sha256:aa", "vault key sha256:bb"}}
raw, err := EncodeNodeRecord(hub)
if err != nil {
t.Fatal(err)
+328
View File
@@ -0,0 +1,328 @@
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 {
if ac, err := ParseCertificate(a); err == nil {
pool.AddCert(ac)
}
}
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 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).
func AuthorityBundle(authorities ...string) string {
var out []string
for _, a := range authorities {
a = strings.TrimSpace(a)
if a == "" || slices.Contains(out, a) {
continue
}
out = append(out, a)
}
if len(out) == 0 {
return ""
}
return strings.Join(out, "\n") + "\n"
}
// 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) }