Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions pkg/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ limitations under the License.
package cmd

import (
"crypto/x509"
"fmt"
"net"
"net/url"
Expand Down Expand Up @@ -46,18 +45,14 @@ func initCerts(cfg *config.Config) (*certchains.CertificateChains, error) {
// we cannot just remove the certs dir and regenerate all the certificates
// because there are some long-lived certs and CAs that shouldn't be swapped
// - for example system:admin client certs, KAS serving CAs
regenCerts, err := certsToRegenerate(certChains)
if err != nil {
return nil, err
}

regenCerts := certsToRegenerate(certChains)
for _, c := range regenCerts {
if err := certChains.Regenerate(c...); err != nil {
return nil, err
}
}

return certChains, err
return certChains, nil
}

func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) {
Expand Down Expand Up @@ -588,9 +583,11 @@ func initKubeconfigs(

// certsToRegenerate returns paths to certificates in the given certificate chains
// bundle that need to be regenerated
func certsToRegenerate(cs *certchains.CertificateChains) ([][]string, error) {
func certsToRegenerate(cs *certchains.CertificateChains) [][]string {
regenCerts := [][]string{}
err := cs.WalkChains(nil, func(certPath []string, c x509.Certificate) error {
for _, entry := range cs.Inventory() {
certPath := entry.Path
c := entry.Certificate
if now := time.Now(); now.Before(c.NotBefore) || now.After(c.NotAfter) {
regenCerts = append(regenCerts, certPath)
}
Expand All @@ -605,18 +602,16 @@ func certsToRegenerate(cs *certchains.CertificateChains) ([][]string, error) {
if timeLeft < until {
regenCerts = append(regenCerts, certPath)
}
return nil
continue
}

// long lived certs
if timeLeft < 18*month {
regenCerts = append(regenCerts, certPath)
}
}

return nil
})

return regenCerts, err
return regenCerts
}

func cleanupStaleKubeconfigs(cfg *config.Config, path string) error {
Expand Down
13 changes: 4 additions & 9 deletions pkg/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ import (

func Test_certsToRegenerate(t *testing.T) {
tests := []struct {
name string
chains *certchains.CertificateChains
want [][]string
wantErr bool
name string
chains *certchains.CertificateChains
want [][]string
}{
{
name: "empty chains",
Expand Down Expand Up @@ -118,11 +117,7 @@ func Test_certsToRegenerate(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := certsToRegenerate(tt.chains)
if (err != nil) != tt.wantErr {
t.Errorf("certsToRegenerate() error = %v, wantErr %v", err, tt.wantErr)
return
}
got := certsToRegenerate(tt.chains)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("certsToRegenerate() = %v, want %v", got, tt.want)
}
Expand Down
12 changes: 6 additions & 6 deletions pkg/util/cryptomaterial/certchains/certchains.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ func WhenToRotateAtEarliest(cs *CertificateChains) ([]string, time.Time, error)
rotationDate time.Time
)

err := cs.WalkChains(nil, func(currentPath []string, c x509.Certificate) error {
for _, entry := range cs.Inventory() {
currentPath := entry.Path
c := entry.Certificate
const month = 30 * time.Hour * 24

rotateAt := c.NotAfter.Add(-4 * month)
Expand All @@ -122,16 +124,14 @@ func WhenToRotateAtEarliest(cs *CertificateChains) ([]string, time.Time, error)
if rotationDate.IsZero() {
rotationDate = rotateAt
certPath = currentPath
return nil
continue
}

if rotateAt.Before(rotationDate) {
rotationDate = rotateAt
certPath = currentPath
}
}

return nil
})

return certPath, rotationDate, err
return certPath, rotationDate, nil
}
87 changes: 87 additions & 0 deletions pkg/util/cryptomaterial/certchains/inventory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package certchains

import "crypto/x509"

// CertificateRole identifies how a certificate is used by MicroShift.
type CertificateRole string

const (
CertificateRoleUnknown CertificateRole = "unknown"
CertificateRoleCA CertificateRole = "ca"
CertificateRoleClient CertificateRole = "client"
CertificateRoleServing CertificateRole = "serving"
CertificateRolePeer CertificateRole = "peer"
)

// CertificateInventoryEntry describes one certificate managed by the chain.
// Path can be passed to CertificateChains methods such as Regenerate.
type CertificateInventoryEntry struct {
Path []string
Role CertificateRole
Certificate x509.Certificate
}

// CertificateInventory is a snapshot of certificates managed by a chain.
type CertificateInventory []CertificateInventoryEntry

// ByRole returns the inventory entries matching role.
func (i CertificateInventory) ByRole(role CertificateRole) CertificateInventory {
entries := make(CertificateInventory, 0)
for _, entry := range i {
if entry.Role == role {
entries = append(entries, entry)
}
}
return entries
}

// Inventory returns a deterministic snapshot of all certificates managed by
// the chain. Signers are listed before their sub-CAs and leaf certificates.
func (cs *CertificateChains) Inventory() CertificateInventory {
signerNames := cs.GetSignerNames()
entries := make(CertificateInventory, 0, len(signerNames))
for _, signerName := range signerNames {
signer := cs.GetSigner(signerName)
entries = append(entries, signer.inventory([]string{signerName})...)
}
return entries
}

func (s *CertificateSigner) inventory(path []string) CertificateInventory {
entries := make(CertificateInventory, 0, 1+len(s.subCAs)+len(s.signedCertificates))
entries = append(entries, CertificateInventoryEntry{
Path: append([]string(nil), path...),
Role: CertificateRoleCA,
Certificate: *s.signerConfig.Config.Certs[0],
})

for _, subCAName := range s.GetSubCANames() {
subCAPath := append(append([]string(nil), path...), subCAName)
entries = append(entries, s.GetSubCA(subCAName).inventory(subCAPath)...)
}

for _, certName := range s.GetCertNames() {
cert := s.signedCertificates[certName]
certPath := append(append([]string(nil), path...), certName)
entries = append(entries, CertificateInventoryEntry{
Path: certPath,
Role: certificateRole(cert.CSRInfo),
Certificate: *cert.tlsConfig.Certs[0],
})
}

return entries
}

func certificateRole(info CSRInfo) CertificateRole {
switch info.(type) {
case *ClientCertificateSigningRequestInfo:
return CertificateRoleClient
case *ServingCertificateSigningRequestInfo:
return CertificateRoleServing
case *PeerCertificateSigningRequestInfo:
return CertificateRolePeer
default:
return CertificateRoleUnknown
}
}
104 changes: 104 additions & 0 deletions pkg/util/cryptomaterial/certchains/inventory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package certchains

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestCertificateChains_Inventory(t *testing.T) {
chains := testChains(t, t.TempDir())

type expectedEntry struct {
path string
role CertificateRole
}
expected := []expectedEntry{
{"test-signer1", CertificateRoleCA},
{"test-signer1/test-signer1-subca", CertificateRoleCA},
{"test-signer1/test-signer1-subca/test-signer1-subca-too", CertificateRoleCA},
{"test-signer1/test-signer1-subca/test-signer1-subca-too/test-signer1-subca-too-too", CertificateRoleCA},
{"test-signer1/test-signer1-subca/test-signer1-subca-too/test-signer1-subca-too-too/subca-too-too-test-client2", CertificateRoleClient},
{"test-signer1/test-signer1-subca/test-signer1-subca-too/test-signer1-subca-too-too2", CertificateRoleCA},
{"test-signer1/test-signer1-subca/test-signer1-subca-too/subca-too-test-client1", CertificateRoleClient},
{"test-signer1/test-signer1-subca/test-signer1-subca-server1", CertificateRoleServing},
{"test-signer1/test-client1", CertificateRoleClient},
{"test-signer1/test-client2", CertificateRoleClient},
{"test-signer1/test-signer1-server1", CertificateRoleServing},
{"test-signer2", CertificateRoleCA},
{"test-signer2/test-signer2-server1", CertificateRoleServing},
{"test-signer3", CertificateRoleCA},
{"test-signer3/test-signer3-subca1", CertificateRoleCA},
{"test-signer3/test-signer3-subca1/test-client1", CertificateRoleClient},
{"test-signer3/test-peer1", CertificateRolePeer},
{"test-signer3/test-signer3-server1", CertificateRoleServing},
}

inventory := chains.Inventory()
require.Len(t, inventory, len(expected))
for index, want := range expected {
entry := inventory[index]
require.Equal(t, want.path, strings.Join(entry.Path, "/"))
require.Equal(t, want.role, entry.Role)
require.False(t, entry.Certificate.NotAfter.IsZero())

if entry.Role == CertificateRoleCA {
require.NotNil(t, chains.GetSigner(entry.Path...))
continue
}
_, _, err := chains.GetCertKey(entry.Path...)
require.NoError(t, err)
}
}

func TestCertificateInventory_ByRole(t *testing.T) {
inventory := testChains(t, t.TempDir()).Inventory()

tests := []struct {
name string
role CertificateRole
paths []string
}{
{
name: "CAs",
role: CertificateRoleCA,
paths: []string{
"test-signer1",
"test-signer1/test-signer1-subca",
"test-signer1/test-signer1-subca/test-signer1-subca-too",
"test-signer1/test-signer1-subca/test-signer1-subca-too/test-signer1-subca-too-too",
"test-signer1/test-signer1-subca/test-signer1-subca-too/test-signer1-subca-too-too2",
"test-signer2",
"test-signer3",
"test-signer3/test-signer3-subca1",
},
},
{
name: "serving certificates",
role: CertificateRoleServing,
paths: []string{
"test-signer1/test-signer1-subca/test-signer1-subca-server1",
"test-signer1/test-signer1-server1",
"test-signer2/test-signer2-server1",
"test-signer3/test-signer3-server1",
},
},
{
name: "unknown role",
role: CertificateRoleUnknown,
paths: []string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entries := inventory.ByRole(tt.role)
paths := make([]string, 0, len(entries))
for _, entry := range entries {
paths = append(paths, strings.Join(entry.Path, "/"))
}
require.Equal(t, tt.paths, paths)
})
}
}