From 3a8997c58a953ea6d548ce5826840558b8dce61a Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 09:27:17 +0100 Subject: [PATCH] =?UTF-8?q?Bundle=20the=20mesh's=20authorities=20by=20cert?= =?UTF-8?q?ificate,=20and=20check=20a=20node's=20certificate=20against=20e?= =?UTF-8?q?very=20one=20a=20bundle=20holds=20(issue=20774,=20ADR=200550=20?= =?UTF-8?q?=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/delivery/composed_signature_test.go | 38 ++++++++++++++++++++++++ go/delivery/signers.go | 40 ++++++++++++++++++++------ 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/go/delivery/composed_signature_test.go b/go/delivery/composed_signature_test.go index 4d411bd..3098e27 100644 --- a/go/delivery/composed_signature_test.go +++ b/go/delivery/composed_signature_test.go @@ -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) + } + } } diff --git a/go/delivery/signers.go b/go/delivery/signers.go index 52834a2..811ea13 100644 --- a/go/delivery/signers.go +++ b/go/delivery/signers.go @@ -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): ` key ` for each signer and // ` authority ` for each authority certificate, by who holds it, each once, sorted. The gate before the // lease write reads `vault key ` in every node's record.