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