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:
@@ -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"}}}}})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
Reference in New Issue
Block a user