From ddd5057f6379a1cc9ce594308c77ebd4ec06e656 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 8 Nov 2022 17:06:22 -0800 Subject: [PATCH] Allow root and federated root bundles This commit changes the parsing of root and federated roots to support a bundle of certificates, this makes easier to configure a root rotation when using helm charts, just appending the old root. --- authority/authority.go | 16 ++--- authority/authority_test.go | 127 ++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 8 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 3fb6f51f..7904a7ea 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -413,13 +413,13 @@ func (a *Authority) init() error { // Read root certificates and store them in the certificates map. if len(a.rootX509Certs) == 0 { - a.rootX509Certs = make([]*x509.Certificate, len(a.config.Root)) - for i, path := range a.config.Root { - crt, err := pemutil.ReadCertificate(path) + a.rootX509Certs = make([]*x509.Certificate, 0, len(a.config.Root)) + for _, path := range a.config.Root { + crts, err := pemutil.ReadCertificateBundle(path) if err != nil { return err } - a.rootX509Certs[i] = crt + a.rootX509Certs = append(a.rootX509Certs, crts...) } } for _, crt := range a.rootX509Certs { @@ -434,13 +434,13 @@ func (a *Authority) init() error { // Read federated certificates and store them in the certificates map. if len(a.federatedX509Certs) == 0 { - a.federatedX509Certs = make([]*x509.Certificate, len(a.config.FederatedRoots)) - for i, path := range a.config.FederatedRoots { - crt, err := pemutil.ReadCertificate(path) + a.federatedX509Certs = make([]*x509.Certificate, 0, len(a.config.FederatedRoots)) + for _, path := range a.config.FederatedRoots { + crts, err := pemutil.ReadCertificateBundle(path) if err != nil { return err } - a.federatedX509Certs[i] = crt + a.federatedX509Certs = append(a.federatedX509Certs, crts...) } } for _, crt := range a.federatedX509Certs { diff --git a/authority/authority_test.go b/authority/authority_test.go index 9f35f23e..82a05a3e 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -6,8 +6,10 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "encoding/pem" "net" "os" + "path/filepath" "reflect" "testing" "time" @@ -18,6 +20,7 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "go.step.sm/crypto/jose" + "go.step.sm/crypto/minica" "go.step.sm/crypto/pemutil" ) @@ -172,6 +175,130 @@ func TestAuthorityNew(t *testing.T) { } } +func TestAuthorityNew_bundles(t *testing.T) { + ca0, err := minica.New() + if err != nil { + t.Fatal(err) + } + ca1, err := minica.New() + if err != nil { + t.Fatal(err) + } + ca2, err := minica.New() + if err != nil { + t.Fatal(err) + } + + rootPath := t.TempDir() + writeCert := func(fn string, certs ...*x509.Certificate) error { + var b []byte + for _, crt := range certs { + b = append(b, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + })...) + } + return os.WriteFile(filepath.Join(rootPath, fn), b, 0600) + } + writeKey := func(fn string, signer crypto.Signer) error { + _, err := pemutil.Serialize(signer, pemutil.ToFile(filepath.Join(rootPath, fn), 0600)) + return err + } + + if err := writeCert("root0.crt", ca0.Root); err != nil { + t.Fatal(err) + } + if err := writeCert("int0.crt", ca0.Intermediate); err != nil { + t.Fatal(err) + } + if err := writeKey("int0.key", ca0.Signer); err != nil { + t.Fatal(err) + } + if err := writeCert("root1.crt", ca1.Root); err != nil { + t.Fatal(err) + } + if err := writeCert("int1.crt", ca1.Intermediate); err != nil { + t.Fatal(err) + } + if err := writeKey("int1.key", ca1.Signer); err != nil { + t.Fatal(err) + } + if err := writeCert("bundle0.crt", ca0.Root, ca1.Root); err != nil { + t.Fatal(err) + } + if err := writeCert("bundle1.crt", ca1.Root, ca2.Root); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + config *config.Config + wantErr bool + }{ + {"ok ca0", &config.Config{ + Address: "127.0.0.1:443", + Root: []string{filepath.Join(rootPath, "root0.crt")}, + IntermediateCert: filepath.Join(rootPath, "int0.crt"), + IntermediateKey: filepath.Join(rootPath, "int0.key"), + DNSNames: []string{"127.0.0.1"}, + AuthorityConfig: &AuthConfig{}, + }, false}, + {"ok bundle", &config.Config{ + Address: "127.0.0.1:443", + Root: []string{filepath.Join(rootPath, "bundle0.crt")}, + IntermediateCert: filepath.Join(rootPath, "int0.crt"), + IntermediateKey: filepath.Join(rootPath, "int0.key"), + DNSNames: []string{"127.0.0.1"}, + AuthorityConfig: &AuthConfig{}, + }, false}, + {"ok federated ca1", &config.Config{ + Address: "127.0.0.1:443", + Root: []string{filepath.Join(rootPath, "root0.crt")}, + FederatedRoots: []string{filepath.Join(rootPath, "root1.crt")}, + IntermediateCert: filepath.Join(rootPath, "int0.crt"), + IntermediateKey: filepath.Join(rootPath, "int0.key"), + DNSNames: []string{"127.0.0.1"}, + AuthorityConfig: &AuthConfig{}, + }, false}, + {"ok federated bundle", &config.Config{ + Address: "127.0.0.1:443", + Root: []string{filepath.Join(rootPath, "root0.crt")}, + FederatedRoots: []string{filepath.Join(rootPath, "bundle1.crt")}, + IntermediateCert: filepath.Join(rootPath, "int0.crt"), + IntermediateKey: filepath.Join(rootPath, "int0.key"), + DNSNames: []string{"127.0.0.1"}, + AuthorityConfig: &AuthConfig{}, + }, false}, + {"fail root", &config.Config{ + Address: "127.0.0.1:443", + Root: []string{filepath.Join(rootPath, "missing.crt")}, + IntermediateCert: filepath.Join(rootPath, "int0.crt"), + IntermediateKey: filepath.Join(rootPath, "int0.key"), + DNSNames: []string{"127.0.0.1"}, + AuthorityConfig: &AuthConfig{}, + }, true}, + {"fail federated", &config.Config{ + Address: "127.0.0.1:443", + Root: []string{filepath.Join(rootPath, "root0.crt")}, + FederatedRoots: []string{filepath.Join(rootPath, "missing.crt")}, + IntermediateCert: filepath.Join(rootPath, "int0.crt"), + IntermediateKey: filepath.Join(rootPath, "int0.key"), + DNSNames: []string{"127.0.0.1"}, + AuthorityConfig: &AuthConfig{}, + }, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := New(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + func TestAuthority_GetDatabase(t *testing.T) { auth := testAuthority(t) authWithDatabase, err := New(auth.config, WithDatabase(auth.db))