Bundle the mesh's authorities by certificate, and check a node's certificate against every one a bundle holds (issue 774, ADR 0550 §2)

This commit is contained in:
2026-10-28 21:54:04 +01:00
parent d6db3f38e6
commit 3a8997c58a
2 changed files with 69 additions and 9 deletions
+38
View File
@@ -3,7 +3,9 @@ package delivery
import (
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"strings"
"testing"
"time"
@@ -149,4 +151,40 @@ func TestACompositionIsSignedByAKeptKey(t *testing.T) {
!TrustsKey(got, SignerController, SignerFingerprint(controllerPub)) {
t.Errorf("trusts: %v", got)
}
// A certificate the vault's authority signs for a node's name and serving key checks against a bundle holding that
// authority, and not for another name, another key, or against the controller's alone; a bundle given again adds no
// certificate twice.
authority := func(pub ed25519.PublicKey, priv ed25519.PrivateKey) string {
tpl := AuthorityTemplate(pub, time.Now().Add(-time.Hour))
der, err := x509.CreateCertificate(nil, tpl, tpl, pub, priv)
if err != nil {
t.Fatal(err)
}
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
}
vaultCA, controllerCA := authority(vaultPub, vault), authority(controllerPub, controller)
parent, _ := ParseCertificate(vaultCA)
serving, _ := aSigningKey(t)
der, err := x509.CreateCertificate(nil, NodeCertificateTemplate("ace.internal", serving, time.Now().Add(-time.Hour)), parent, serving, vault)
if err != nil {
t.Fatal(err)
}
leaf := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
bundle := AuthorityBundle(controllerCA, AuthorityBundle(controllerCA, vaultCA))
if n := strings.Count(bundle, "BEGIN CERTIFICATE"); n != 2 {
t.Errorf("the bundle holds %d certificates, want 2", n)
}
now := time.Now()
if err := CheckNodeCertificate(leaf, []string{bundle}, "ace.internal", serving, now); err != nil {
t.Errorf("the vault's certificate does not check: %v", err)
}
for name, err := range map[string]error{
"another name": CheckNodeCertificate(leaf, []string{vaultCA}, "g14.internal", serving, now),
"another key": CheckNodeCertificate(leaf, []string{vaultCA}, "ace.internal", controllerPub, now),
"the controller's alone": CheckNodeCertificate(leaf, []string{controllerCA}, "ace.internal", serving, now),
} {
if err == nil {
t.Errorf("%s: checked", name)
}
}
}
+31 -9
View File
@@ -273,9 +273,7 @@ func CheckNodeCertificate(certificate string, authorities []string, name string,
}
pool := x509.NewCertPool()
for _, a := range authorities {
if ac, err := ParseCertificate(a); err == nil {
pool.AddCert(ac)
}
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 {
@@ -288,16 +286,18 @@ func CheckNodeCertificate(certificate string, authorities []string, name string,
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).
// 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 {
a = strings.TrimSpace(a)
if a == "" || slices.Contains(out, a) {
continue
for _, piece := range pemPieces(a) {
if !slices.Contains(out, piece) {
out = append(out, piece)
}
}
out = append(out, a)
}
if len(out) == 0 {
return ""
@@ -305,6 +305,28 @@ func AuthorityBundle(authorities ...string) string {
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.