From 31e402c4633936e2999a99ec42cc983cef6bf3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Mart=C3=ADnez=20Fay=C3=B3?= Date: Wed, 3 Jan 2024 12:47:34 -0300 Subject: [PATCH] Introduce support to save and load the CA journal from the datastore (#4690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Save and load the CA journal from datastore Signed-off-by: Agustín Martínez Fayó --- pkg/common/telemetry/names.go | 6 + .../telemetry/server/datastore/ca_journal.go | 29 ++ .../telemetry/server/datastore/wrapper.go | 24 ++ .../server/datastore/wrapper_test.go | 32 ++ pkg/server/api/localauthority/v1/service.go | 8 +- .../api/localauthority/v1/service_test.go | 4 +- pkg/server/ca/manager/journal.go | 244 +++++++++--- pkg/server/ca/manager/journal_test.go | 368 ++++++++++++------ pkg/server/ca/manager/manager.go | 76 ++-- pkg/server/ca/manager/manager_test.go | 180 +++++++-- pkg/server/ca/manager/slot.go | 92 ++--- pkg/server/ca/manager/slot_test.go | 81 ++-- pkg/server/ca/rotator/rotator.go | 43 +- pkg/server/ca/rotator/rotator_test.go | 102 +++-- pkg/server/datastore/datastore.go | 12 + pkg/server/datastore/sqlstore/sqlstore.go | 177 +++++++++ .../datastore/sqlstore/sqlstore_test.go | 149 +++++++ proto/private/server/journal/journal.pb.go | 69 ++-- proto/private/server/journal/journal.proto | 3 + test/fakes/fakedatastore/fakedatastore.go | 28 ++ 20 files changed, 1341 insertions(+), 386 deletions(-) create mode 100644 pkg/common/telemetry/server/datastore/ca_journal.go diff --git a/pkg/common/telemetry/names.go b/pkg/common/telemetry/names.go index f3daba1823..70f1a9ad8d 100644 --- a/pkg/common/telemetry/names.go +++ b/pkg/common/telemetry/names.go @@ -175,6 +175,12 @@ const ( // BySelectors tags selectors used when filtering BySelectors = "by_selectors" + // CAJournal is a CA journal record + CAJournal = "ca_journal" + + // CAJournalID tags a CA journal ID + CAJournalID = "ca_journal_id" + // CallerAddr labels an API caller address CallerAddr = "caller_addr" diff --git a/pkg/common/telemetry/server/datastore/ca_journal.go b/pkg/common/telemetry/server/datastore/ca_journal.go new file mode 100644 index 0000000000..18fb1d940c --- /dev/null +++ b/pkg/common/telemetry/server/datastore/ca_journal.go @@ -0,0 +1,29 @@ +package datastore + +import ( + "github.com/spiffe/spire/pkg/common/telemetry" +) + +// StartSetCAJournal return metric for server's datastore, on setting a CA +// journal. +func StartSetCAJournal(m telemetry.Metrics) *telemetry.CallCounter { + return telemetry.StartCall(m, telemetry.Datastore, telemetry.CAJournal, telemetry.Set) +} + +// StartFetchCAJournal return metric +// for server's datastore, on fetching a CA journal. +func StartFetchCAJournal(m telemetry.Metrics) *telemetry.CallCounter { + return telemetry.StartCall(m, telemetry.Datastore, telemetry.CAJournal, telemetry.Fetch) +} + +// StartPruneCAJournalsCall return metric for server's datastore, on pruning CA +// journals. +func StartPruneCAJournalsCall(m telemetry.Metrics) *telemetry.CallCounter { + return telemetry.StartCall(m, telemetry.Datastore, telemetry.CAJournal, telemetry.Prune) +} + +// StartListCAJournalsForTesting return metric +// for server's datastore, on listing CA journals for testing. +func StartListCAJournalsForTesting(m telemetry.Metrics) *telemetry.CallCounter { + return telemetry.StartCall(m, telemetry.Datastore, telemetry.CAJournal, telemetry.List) +} diff --git a/pkg/common/telemetry/server/datastore/wrapper.go b/pkg/common/telemetry/server/datastore/wrapper.go index 99db2e276c..645b8501a2 100644 --- a/pkg/common/telemetry/server/datastore/wrapper.go +++ b/pkg/common/telemetry/server/datastore/wrapper.go @@ -293,3 +293,27 @@ func (w metricsWrapper) UpdateFederationRelationship(ctx context.Context, fr *da defer callCounter.Done(&err) return w.ds.UpdateFederationRelationship(ctx, fr, mask) } + +func (w metricsWrapper) SetCAJournal(ctx context.Context, caJournal *datastore.CAJournal) (_ *datastore.CAJournal, err error) { + callCounter := StartSetCAJournal(w.m) + defer callCounter.Done(&err) + return w.ds.SetCAJournal(ctx, caJournal) +} + +func (w metricsWrapper) FetchCAJournal(ctx context.Context, activeX509AuthorityID string) (_ *datastore.CAJournal, err error) { + callCounter := StartFetchCAJournal(w.m) + defer callCounter.Done(&err) + return w.ds.FetchCAJournal(ctx, activeX509AuthorityID) +} + +func (w metricsWrapper) ListCAJournalsForTesting(ctx context.Context) (_ []*datastore.CAJournal, err error) { + callCounter := StartListCAJournalsForTesting(w.m) + defer callCounter.Done(&err) + return w.ds.ListCAJournalsForTesting(ctx) +} + +func (w metricsWrapper) PruneCAJournals(ctx context.Context, allCAsExpireBefore int64) (err error) { + callCounter := StartPruneCAJournalsCall(w.m) + defer callCounter.Done(&err) + return w.ds.PruneCAJournals(ctx, allCAsExpireBefore) +} diff --git a/pkg/common/telemetry/server/datastore/wrapper_test.go b/pkg/common/telemetry/server/datastore/wrapper_test.go index 1f814d1b51..d085ee34d4 100644 --- a/pkg/common/telemetry/server/datastore/wrapper_test.go +++ b/pkg/common/telemetry/server/datastore/wrapper_test.go @@ -218,6 +218,22 @@ func TestWithMetrics(t *testing.T) { key: "datastore.registration_entry.update", methodName: "UpdateRegistrationEntry", }, + { + key: "datastore.ca_journal.set", + methodName: "SetCAJournal", + }, + { + key: "datastore.ca_journal.fetch", + methodName: "FetchCAJournal", + }, + { + key: "datastore.ca_journal.prune", + methodName: "PruneCAJournals", + }, + { + key: "datastore.ca_journal.list", + methodName: "ListCAJournalsForTesting", + }, } { tt := tt methodType, ok := wt.MethodByName(tt.methodName) @@ -477,3 +493,19 @@ func (ds *fakeDataStore) UpdateRegistrationEntry(context.Context, *common.Regist func (ds *fakeDataStore) UpdateFederationRelationship(context.Context, *datastore.FederationRelationship, *types.FederationRelationshipMask) (*datastore.FederationRelationship, error) { return &datastore.FederationRelationship{}, ds.err } + +func (ds *fakeDataStore) SetCAJournal(context.Context, *datastore.CAJournal) (*datastore.CAJournal, error) { + return &datastore.CAJournal{}, ds.err +} + +func (ds *fakeDataStore) FetchCAJournal(context.Context, string) (*datastore.CAJournal, error) { + return &datastore.CAJournal{}, ds.err +} + +func (ds *fakeDataStore) ListCAJournalsForTesting(context.Context) ([]*datastore.CAJournal, error) { + return []*datastore.CAJournal{}, ds.err +} + +func (ds *fakeDataStore) PruneCAJournals(context.Context, int64) error { + return ds.err +} diff --git a/pkg/server/api/localauthority/v1/service.go b/pkg/server/api/localauthority/v1/service.go index 550ec65879..100e9927f0 100644 --- a/pkg/server/api/localauthority/v1/service.go +++ b/pkg/server/api/localauthority/v1/service.go @@ -25,13 +25,13 @@ type CAManager interface { GetCurrentJWTKeySlot() manager.Slot GetNextJWTKeySlot() manager.Slot PrepareJWTKey(ctx context.Context) error - RotateJWTKey() + RotateJWTKey(ctx context.Context) // X509 GetCurrentX509CASlot() manager.Slot GetNextX509CASlot() manager.Slot PrepareX509CA(ctx context.Context) error - RotateX509CA() + RotateX509CA(ctx context.Context) } // Config is the service configuration @@ -140,7 +140,7 @@ func (s *Service) ActivateJWTAuthority(ctx context.Context, req *localauthorityv return nil, api.MakeErr(log, codes.Internal, "only Prepared authorities can be activated", fmt.Errorf("unsupported local authority status: %v", nextSlot.Status())) } - s.ca.RotateJWTKey() + s.ca.RotateJWTKey(ctx) current := s.ca.GetCurrentJWTKeySlot() state := &localauthorityv1.AuthorityState{ @@ -308,7 +308,7 @@ func (s *Service) ActivateX509Authority(ctx context.Context, req *localauthority } // Move next into current and reset next to clean CA - s.ca.RotateX509CA() + s.ca.RotateX509CA(ctx) current := s.ca.GetCurrentX509CASlot() state := &localauthorityv1.AuthorityState{ diff --git a/pkg/server/api/localauthority/v1/service_test.go b/pkg/server/api/localauthority/v1/service_test.go index f91a8f9e9e..e631130183 100644 --- a/pkg/server/api/localauthority/v1/service_test.go +++ b/pkg/server/api/localauthority/v1/service_test.go @@ -1865,7 +1865,7 @@ func (m *fakeCAManager) PrepareJWTKey(context.Context) error { return m.prepareJWTKeyErr } -func (m *fakeCAManager) RotateJWTKey() { +func (m *fakeCAManager) RotateJWTKey(context.Context) { m.rotateJWTKeyCalled = true } @@ -1881,7 +1881,7 @@ func (m *fakeCAManager) PrepareX509CA(context.Context) error { return m.prepareX509CAErr } -func (m *fakeCAManager) RotateX509CA() { +func (m *fakeCAManager) RotateX509CA(context.Context) { m.rotateX509CACalled = true } diff --git a/pkg/server/ca/manager/journal.go b/pkg/server/ca/manager/journal.go index 831585bc07..5fe39e7e0d 100644 --- a/pkg/server/ca/manager/journal.go +++ b/pkg/server/ca/manager/journal.go @@ -1,6 +1,7 @@ package manager import ( + "context" "crypto/x509" "encoding/pem" "fmt" @@ -8,9 +9,13 @@ import ( "sync" "time" + "github.com/sirupsen/logrus" "github.com/spiffe/spire/pkg/common/diskutil" + "github.com/spiffe/spire/pkg/common/telemetry" "github.com/spiffe/spire/pkg/common/x509util" "github.com/spiffe/spire/pkg/server/ca" + "github.com/spiffe/spire/pkg/server/catalog" + "github.com/spiffe/spire/pkg/server/datastore" "github.com/spiffe/spire/proto/private/server/journal" "github.com/zeebo/errs" "google.golang.org/protobuf/proto" @@ -25,61 +30,63 @@ const ( journalPEMType = "SPIRE CA JOURNAL" ) -type JournalEntries = journal.Entries -type X509CAEntry = journal.X509CAEntry -type JWTKeyEntry = journal.JWTKeyEntry +type journalConfig struct { + cat catalog.Catalog + log logrus.FieldLogger + filePath string +} // Journal stores X509 CAs and JWT keys on disk as they are rotated by the // manager. The data format on disk is a PEM encoded protocol buffer. type Journal struct { - path string + config *journalConfig - mu sync.RWMutex - entries *JournalEntries + mu sync.RWMutex + activeX509AuthorityID string + caJournalID uint + entries *journal.Entries } -func LoadJournal(path string) (*Journal, error) { - j := &Journal{ - path: path, - entries: new(JournalEntries), - } - - pemBytes, err := os.ReadFile(path) +func LoadJournal(ctx context.Context, config *journalConfig) (*Journal, error) { + // Look for the CA journal of this server in the datastore. + journalDS, err := loadJournalFromDS(ctx, config) if err != nil { - if os.IsNotExist(err) { - return j, nil - } - return nil, errs.Wrap(err) + return nil, fmt.Errorf("failed to load journal from datastore: %w", err) } - pemBlock, _ := pem.Decode(pemBytes) - if pemBlock == nil { - return nil, errs.New("invalid PEM block") - } - if pemBlock.Type != journalPEMType { - return nil, errs.New("invalid PEM block type %q", pemBlock.Type) + if journalDS != nil { + // A CA journal record corresponding to this server was found in the + // datastore. + return journalDS, nil } - if err := proto.Unmarshal(pemBlock.Bytes, j.entries); err != nil { - return nil, errs.New("unable to unmarshal entries: %v", err) + // There is no CA journal record corresponding to this server in the + // datastore. Try to load the journal from disk. + + // TODO: stop trying to load the journal from disk in v1.10 and delete + // the journal file if exists. + journalDisk, err := loadJournalFromDisk(config) + if err != nil { + return nil, fmt.Errorf("failed to load journal from disk: %w", err) } - return j, nil + return journalDisk, nil } -func (j *Journal) Entries() *JournalEntries { +func (j *Journal) getEntries() *journal.Entries { j.mu.RLock() defer j.mu.RUnlock() - return proto.Clone(j.entries).(*JournalEntries) + return proto.Clone(j.entries).(*journal.Entries) } -func (j *Journal) AppendX509CA(slotID string, issuedAt time.Time, x509CA *ca.X509CA) error { +func (j *Journal) AppendX509CA(ctx context.Context, slotID string, issuedAt time.Time, x509CA *ca.X509CA) error { j.mu.Lock() defer j.mu.Unlock() backup := j.entries.X509CAs - j.entries.X509CAs = append(j.entries.X509CAs, &X509CAEntry{ + j.entries.X509CAs = append(j.entries.X509CAs, &journal.X509CAEntry{ SlotId: slotID, IssuedAt: issuedAt.Unix(), + NotAfter: x509CA.Certificate.NotAfter.Unix(), Certificate: x509CA.Certificate.Raw, UpstreamChain: chainDER(x509CA.UpstreamChain), Status: journal.Status_PREPARED, @@ -89,12 +96,12 @@ func (j *Journal) AppendX509CA(slotID string, issuedAt time.Time, x509CA *ca.X50 exceeded := len(j.entries.X509CAs) - journalCap if exceeded > 0 { // make a new slice so we keep growing the backing array to drop the first - x509CAs := make([]*X509CAEntry, journalCap) + x509CAs := make([]*journal.X509CAEntry, journalCap) copy(x509CAs, j.entries.X509CAs[exceeded:]) j.entries.X509CAs = x509CAs } - if err := j.save(); err != nil { + if err := j.save(ctx); err != nil { j.entries.X509CAs = backup return err } @@ -103,7 +110,7 @@ func (j *Journal) AppendX509CA(slotID string, issuedAt time.Time, x509CA *ca.X50 } // UpdateX509CAStatus updates a stored X509CA entry to have the given status, updating the journal file. -func (j *Journal) UpdateX509CAStatus(issuedAt time.Time, status journal.Status) error { +func (j *Journal) UpdateX509CAStatus(ctx context.Context, issuedAt time.Time, status journal.Status) error { j.mu.Lock() defer j.mu.Unlock() @@ -119,6 +126,9 @@ func (j *Journal) UpdateX509CAStatus(issuedAt time.Time, status journal.Status) if issuedAtUnix == entry.IssuedAt { found = true entry.Status = status + if status == journal.Status_ACTIVE { + j.activeX509AuthorityID = entry.AuthorityId + } break } } @@ -127,7 +137,7 @@ func (j *Journal) UpdateX509CAStatus(issuedAt time.Time, status journal.Status) return fmt.Errorf("no journal entry found issued at: %v", issuedAtUnix) } - if err := j.save(); err != nil { + if err := j.save(ctx); err != nil { j.entries.X509CAs = backup return err } @@ -135,7 +145,7 @@ func (j *Journal) UpdateX509CAStatus(issuedAt time.Time, status journal.Status) return nil } -func (j *Journal) AppendJWTKey(slotID string, issuedAt time.Time, jwtKey *ca.JWTKey) error { +func (j *Journal) AppendJWTKey(ctx context.Context, slotID string, issuedAt time.Time, jwtKey *ca.JWTKey) error { j.mu.Lock() defer j.mu.Unlock() @@ -145,7 +155,7 @@ func (j *Journal) AppendJWTKey(slotID string, issuedAt time.Time, jwtKey *ca.JWT } backup := j.entries.JwtKeys - j.entries.JwtKeys = append(j.entries.JwtKeys, &JWTKeyEntry{ + j.entries.JwtKeys = append(j.entries.JwtKeys, &journal.JWTKeyEntry{ SlotId: slotID, IssuedAt: issuedAt.Unix(), Kid: jwtKey.Kid, @@ -158,12 +168,12 @@ func (j *Journal) AppendJWTKey(slotID string, issuedAt time.Time, jwtKey *ca.JWT exceeded := len(j.entries.JwtKeys) - journalCap if exceeded > 0 { // make a new slice so we keep growing the backing array to drop the first - jwtKeys := make([]*JWTKeyEntry, journalCap) + jwtKeys := make([]*journal.JWTKeyEntry, journalCap) copy(jwtKeys, j.entries.JwtKeys[exceeded:]) j.entries.JwtKeys = jwtKeys } - if err := j.save(); err != nil { + if err := j.save(ctx); err != nil { j.entries.JwtKeys = backup return err } @@ -172,7 +182,7 @@ func (j *Journal) AppendJWTKey(slotID string, issuedAt time.Time, jwtKey *ca.JWT } // UpdateJWTKeyStatus updates a stored JWTKey entry to have the given status, updating the journal file. -func (j *Journal) UpdateJWTKeyStatus(issuedAt time.Time, status journal.Status) error { +func (j *Journal) UpdateJWTKeyStatus(ctx context.Context, issuedAt time.Time, status journal.Status) error { j.mu.Lock() defer j.mu.Unlock() @@ -196,7 +206,7 @@ func (j *Journal) UpdateJWTKeyStatus(issuedAt time.Time, status journal.Status) return fmt.Errorf("no journal entry found issued at: %v", issuedAtUnix) } - if err := j.save(); err != nil { + if err := j.save(ctx); err != nil { j.entries.JwtKeys = backup return err } @@ -204,22 +214,105 @@ func (j *Journal) UpdateJWTKeyStatus(issuedAt time.Time, status journal.Status) return nil } -func (j *Journal) save() error { - return saveJournalEntries(j.path, j.entries) +func (j *Journal) setEntries(entries *journal.Entries) { + j.mu.Lock() + defer j.mu.Unlock() + + j.entries = entries +} + +// saveInDatastore saves the provided marshaled entries in the datastore. +// If caJournalID has not been defined yet (it's value is 0), it first finds +// the CA journal records that corresponds to this server. In case that there is +// no CA record for this server, it creates one. +// The ID of the CA journal record that was saved is returned, in addition to +// the error (if any) of the operation. +func (j *Journal) saveInDatastore(ctx context.Context, entriesBytes []byte) (caJournalID uint, err error) { + // Check if we already identified what's the CA journal for this server in + // the datastore. If not, log that we are creating a new CA journal entry. + if j.caJournalID == 0 { + j.config.log.Info("Creating a new CA journal entry") + } + + ds := j.config.cat.GetDataStore() + caJournal, err := ds.SetCAJournal(ctx, &datastore.CAJournal{ + ID: j.caJournalID, + Data: entriesBytes, + ActiveX509AuthorityID: j.activeX509AuthorityID, + }) + if err != nil { + return 0, err + } + + j.config.log.WithFields(logrus.Fields{ + telemetry.CAJournalID: caJournal.ID, + telemetry.LocalAuthorityID: j.activeX509AuthorityID, + }).Debug("Successfully stored CA journal entry in datastore") + + return caJournal.ID, nil } -func saveJournalEntries(path string, entries *JournalEntries) error { - entriesBytes, err := proto.Marshal(entries) +// findCAJournal finds the corresponding CA journal record in the datastore for +// this server. It does that by retrieving all the public keys managed by the +// KeyManager and trying to get a match with a record which last active +// X509 authority ID correspond to one of the keys. +func (j *Journal) findCAJournal(ctx context.Context) (*datastore.CAJournal, error) { + km := j.config.cat.GetKeyManager() + ds := j.config.cat.GetDataStore() + + // Get all the public keys managed by the KeyManager. + kmKeys, err := km.GetKeys(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get keys from key manager: %w", err) + } + + for _, k := range kmKeys { + subjectKeyID, err := x509util.GetSubjectKeyID(k.Public()) + if err != nil { + return nil, fmt.Errorf("failed to calculate the subject key identifier for public key with ID %q", k.ID()) + } + + authorityID := x509util.SubjectKeyIDToString(subjectKeyID) + caJournal, err := ds.FetchCAJournal(ctx, authorityID) + if err != nil { + return nil, fmt.Errorf("failed to fetch CA journal from datastore: %w", err) + } + if caJournal != nil { + // There is a CA journal record that has an active X509 authority + // ID that matches with one of the public keys of this server. This + // means that this record belongs to this server. + j.config.log.WithFields(logrus.Fields{ + telemetry.CAJournalID: caJournal.ID, + telemetry.LocalAuthorityID: authorityID, + }).Debug("Found a CA journal record that matches with a local X509 authority ID") + + return caJournal, nil + } + } + + return nil, nil +} + +// save saves the CA journal both on disk and in the datastore. +// TODO: stop saving the CA journal on disk in v1.10. +func (j *Journal) save(ctx context.Context) error { + entriesBytes, err := proto.Marshal(j.entries) if err != nil { return errs.Wrap(err) } + caJournalID, err := j.saveInDatastore(ctx, entriesBytes) + if err != nil { + return fmt.Errorf("could not save CA journal in the datastore: %w", err) + } + j.caJournalID = caJournalID + pemBytes := pem.EncodeToMemory(&pem.Block{ Type: journalPEMType, Bytes: entriesBytes, }) - if err := diskutil.AtomicWritePubliclyReadableFile(path, pemBytes); err != nil { + if err := diskutil.AtomicWritePubliclyReadableFile(j.config.filePath, pemBytes); err != nil { return errs.Wrap(err) } @@ -233,3 +326,64 @@ func chainDER(chain []*x509.Certificate) [][]byte { } return der } + +// loadJournalFromDisk loads the journal from disk if it exists. +// TODO: stop loading the journal from disk in v1.10 and remove this function. +func loadJournalFromDisk(config *journalConfig) (*Journal, error) { + config.log.WithField(telemetry.Path, config.filePath).Debug("Loading journal from disk") + + j := &Journal{ + config: config, + entries: new(journal.Entries), + } + + pemBytes, err := os.ReadFile(config.filePath) + if err != nil { + if os.IsNotExist(err) { + // There is no journal on disk. A new CA journal is created and will + // be stored in the next save operation. + return j, nil + } + return nil, errs.Wrap(err) + } + pemBlock, _ := pem.Decode(pemBytes) + if pemBlock == nil { + return nil, errs.New("invalid PEM block") + } + if pemBlock.Type != journalPEMType { + return nil, errs.New("invalid PEM block type %q", pemBlock.Type) + } + + if err := proto.Unmarshal(pemBlock.Bytes, j.entries); err != nil { + return nil, errs.New("unable to unmarshal entries: %v", err) + } + + return j, nil +} + +// loadJournalFromDS loads the CA journal from the datastore. +// It does that by looking for a CA journal record that matches with one of the +// public keys of this server. +func loadJournalFromDS(ctx context.Context, config *journalConfig) (*Journal, error) { + config.log.Debug("Loading journal from datastore") + + j := &Journal{ + config: config, + entries: new(journal.Entries), + } + + caJournal, err := j.findCAJournal(ctx) + if err != nil { + return nil, fmt.Errorf("failed to find CA journal record: %w", err) + } + if caJournal == nil { + j.config.log.Info("There is not a CA journal record that matches any of the local X509 authority IDs") + return nil, nil + } + + j.caJournalID = caJournal.ID + if err := proto.Unmarshal(caJournal.Data, j.entries); err != nil { + return nil, errs.New("unable to unmarshal entries from CA journal record: %v", err) + } + return j, nil +} diff --git a/pkg/server/ca/manager/journal_test.go b/pkg/server/ca/manager/journal_test.go index 8bf273ab91..adeccfc692 100644 --- a/pkg/server/ca/manager/journal_test.go +++ b/pkg/server/ca/manager/journal_test.go @@ -1,90 +1,196 @@ package manager import ( + "context" "crypto/x509" + "crypto/x509/pkix" "encoding/pem" + "errors" "os" "path/filepath" "testing" "time" - "github.com/spiffe/spire/pkg/common/pemutil" + "github.com/andres-erbsen/clock" + "github.com/sirupsen/logrus/hooks/test" "github.com/spiffe/spire/pkg/server/ca" + "github.com/spiffe/spire/pkg/server/credtemplate" + "github.com/spiffe/spire/pkg/server/plugin/keymanager" "github.com/spiffe/spire/proto/private/server/journal" + "github.com/spiffe/spire/test/fakes/fakedatastore" + "github.com/spiffe/spire/test/fakes/fakeservercatalog" + "github.com/spiffe/spire/test/fakes/fakeserverkeymanager" "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "google.golang.org/protobuf/proto" ) var ( - testSigner, _ = pemutil.ParseSigner([]byte(`-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgt/OIyb8Ossz/5bNk -XtnzFe1T2d0D9quX9Loi1O55b8yhRANCAATDe/2d6z+P095I3dIkocKr4b3zAy+1 -qQDuoXqa8i3YOPk5fLib4ORzqD9NJFcrKjI+LLtipQe9yu/eY1K0yhBa ------END PRIVATE KEY----- -`)) + ctx = context.Background() testChain = []*x509.Certificate{ {Raw: []byte("A")}, {Raw: []byte("B")}, {Raw: []byte("C")}, } + + km keymanager.KeyManager + kmKeys = map[string]keymanager.Key{} + rootCerts = map[string]*x509.Certificate{} ) -func TestJournal(t *testing.T) { - suite.Run(t, new(JournalSuite)) -} +func setupJournalTest(t *testing.T) *journalTest { + log, _ := test.NewNullLogger() -type JournalSuite struct { - spiretest.Suite - dir string -} + clk := clock.New() + credBuilder, err := credtemplate.NewBuilder(credtemplate.Config{ + TrustDomain: testTrustDomain, + X509CASubject: pkix.Name{CommonName: "SPIRE"}, + Clock: clk, + X509CATTL: testCATTL, + }) + require.NoError(t, err) + + ds := fakedatastore.New(t) + cat := fakeservercatalog.New() + cat.SetDataStore(ds) + + if km == nil { + km := fakeserverkeymanager.New(t) + cat.SetKeyManager(km) + + kmKeys["X509-CA-A"], rootCerts["X509-Root-A"], err = createSelfSigned(ctx, credBuilder, km, "X509-CA-A") + require.NoError(t, err) + + kmKeys["X509-CA-B"], rootCerts["X509-Root-B"], err = createSelfSigned(ctx, credBuilder, km, "x509-CA-B") + require.NoError(t, err) + + kmKeys["X509-CA-C"], rootCerts["X509-Root-C"], err = createSelfSigned(ctx, credBuilder, km, "x509-CA-C") + require.NoError(t, err) -func (s *JournalSuite) SetupTest() { - s.dir = s.TempDir() + kmKeys["JWT-Signer-A"], err = km.GenerateKey(ctx, "JWT-Signer-A", keymanager.ECP256) + require.NoError(t, err) + + kmKeys["JWT-Signer-B"], err = km.GenerateKey(ctx, "JWT-Signer-B", keymanager.ECP256) + require.NoError(t, err) + + kmKeys["JWT-Signer-C"], err = km.GenerateKey(ctx, "JWT-Signer-C", keymanager.ECP256) + require.NoError(t, err) + } + + return &journalTest{ + ds: ds, + jc: &journalConfig{ + cat: cat, + log: log, + filePath: filepath.Join(t.TempDir(), "journal.pem"), + }, + } } -func (s *JournalSuite) TestNew() { - journal, err := LoadJournal(s.journalPath()) - s.NoError(err) - if s.NotNil(journal) { +func TestNew(t *testing.T) { + test := setupJournalTest(t) + j, err := LoadJournal(ctx, test.jc) + require.NoError(t, err) + if assert.NotNil(t, j) { // Verify entries is empty - s.AssertProtoEqual(&JournalEntries{}, journal.Entries()) + spiretest.RequireProtoEqual(t, &journal.Entries{}, j.getEntries()) } + caJournals, err := test.ds.ListCAJournalsForTesting(ctx) + require.NoError(t, err) + require.Empty(t, caJournals) } -func (s *JournalSuite) TestPersistence() { - now := s.now() +func TestJournalPersistence(t *testing.T) { + test := setupJournalTest(t) + now := test.now() - journal := s.loadJournal() + j := test.loadJournal(t) - err := journal.AppendX509CA("A", now, &ca.X509CA{ - Signer: testSigner, - Certificate: testChain[0], + err := j.AppendX509CA(ctx, "A", now, &ca.X509CA{ + Signer: kmKeys["X509-CA-A"], + Certificate: rootCerts["X509-Root-A"], UpstreamChain: testChain, }) - s.Require().NoError(err) + require.NoError(t, err) - err = journal.AppendJWTKey("B", now, &ca.JWTKey{ - Signer: testSigner, - Kid: "KID", + err = j.AppendJWTKey(ctx, "B", now, &ca.JWTKey{ + Signer: kmKeys["JWT-Signer-B"], + Kid: "kid1", NotAfter: now.Add(time.Hour), }) - s.Require().NoError(err) + require.NoError(t, err) + + require.NoError(t, j.UpdateX509CAStatus(ctx, now, journal.Status_ACTIVE)) + + // Check that the CA journal was properly stored in the datastore. + journalDS := test.loadJournalFromDS(t) + require.NotNil(t, journalDS) + spiretest.RequireProtoEqual(t, j.getEntries(), journalDS.getEntries()) + + // TODO: the following checks assume that the CA journal is stored both in + // datastore and on disk. Revisit this in v1.10. + journalDisk := test.loadJournalFromDisk(t) + require.NotNil(t, journalDisk) + spiretest.RequireProtoEqual(t, j.getEntries(), journalDisk.getEntries()) + + // Test for the case when SPIRE starts with a CA journal on disk and does + // not yet have a CA journal stored in the datastore. Reset the datastore so + // we only have the CA journal on disk. + test.ds = fakedatastore.New(t) + test.jc.cat.(*fakeservercatalog.Catalog).SetDataStore(test.ds) + + // Load the journal again. It should still get the CA journal stored on + // disk. + j = test.loadJournal(t) + journalDisk = test.loadJournalFromDisk(t) + require.NotNil(t, journalDisk) + spiretest.RequireProtoEqual(t, j.getEntries(), journalDisk.getEntries()) + + // Append a new X.509 CA, which will make the CA journal to be stored + // on disk and in the datastore. + now = now.Add(time.Minute) + err = j.AppendX509CA(ctx, "C", now, &ca.X509CA{ + Signer: kmKeys["X509-CA-C"], + Certificate: rootCerts["X509-Root-C"], + UpstreamChain: testChain, + }) + require.NoError(t, err) + require.NoError(t, j.UpdateX509CAStatus(ctx, now, journal.Status_ACTIVE)) + + journalDS = test.loadJournalFromDS(t) + require.NotNil(t, journalDS) + spiretest.RequireProtoEqual(t, j.getEntries(), journalDS.getEntries()) + journalDisk = test.loadJournalFromDisk(t) + require.NotNil(t, journalDisk) + spiretest.RequireProtoEqual(t, j.getEntries(), journalDisk.getEntries()) + + // Simulate a datastore error + dsError := errors.New("ds error") + test.ds.SetNextError(dsError) + err = j.AppendX509CA(ctx, "C", now, &ca.X509CA{ + Signer: kmKeys["X509-CA-C"], + Certificate: rootCerts["X509-Root-C"], + UpstreamChain: testChain, + }) + require.Error(t, err) + require.EqualError(t, err, "could not save CA journal in the datastore: ds error") - s.requireProtoEqual(journal.Entries(), s.loadJournal().Entries()) + // CA journal on disk should have been saved successfully + journalDisk = test.loadJournalFromDisk(t) + require.NotNil(t, journalDisk) + spiretest.RequireProtoEqual(t, j.getEntries(), journalDisk.getEntries()) } -func (s *JournalSuite) TestAppendSetPreparedStatus() { - t := s.T() - now := s.now() +func TestAppendSetPreparedStatus(t *testing.T) { + test := setupJournalTest(t) + now := test.now() - testJournal := s.loadJournal() + testJournal := test.loadJournal(t) - err := testJournal.AppendX509CA("A", now, &ca.X509CA{ - Signer: testSigner, - Certificate: testChain[0], + err := testJournal.AppendX509CA(ctx, "A", now, &ca.X509CA{ + Signer: kmKeys["X509-CA-A"], + Certificate: rootCerts["X509-Root-A"], UpstreamChain: testChain, }) require.NoError(t, err) @@ -94,8 +200,8 @@ func (s *JournalSuite) TestAppendSetPreparedStatus() { require.Equal(t, "A", lastX509CA.SlotId) require.Equal(t, journal.Status_PREPARED, lastX509CA.Status) - err = testJournal.AppendJWTKey("B", now, &ca.JWTKey{ - Signer: testSigner, + err = testJournal.AppendJWTKey(ctx, "B", now, &ca.JWTKey{ + Signer: kmKeys["X509-CA-B"], Kid: "KID", NotAfter: now.Add(time.Hour), }) @@ -107,49 +213,51 @@ func (s *JournalSuite) TestAppendSetPreparedStatus() { require.Equal(t, journal.Status_PREPARED, lastJWTKey.Status) } -func (s *JournalSuite) TestX509CAOverflow() { - now := s.now() +func TestX509CAOverflow(t *testing.T) { + test := setupJournalTest(t) + now := test.now() - journal := s.loadJournal() + journal := test.loadJournal(t) for i := 0; i < (journalCap + 1); i++ { now = now.Add(time.Minute) - err := journal.AppendX509CA("A", now, &ca.X509CA{ - Signer: testSigner, - Certificate: testChain[0], + err := journal.AppendX509CA(ctx, "A", now, &ca.X509CA{ + Signer: kmKeys["X509-CA-A"], + Certificate: rootCerts["X509-Root-A"], }) - s.Require().NoError(err) + require.NoError(t, err) } - entries := journal.Entries() - s.Require().Len(entries.X509CAs, journalCap, "X509CA entries exceeds cap") + entries := journal.getEntries() + require.Len(t, entries.X509CAs, journalCap, "X509CA entries exceeds cap") lastEntry := entries.X509CAs[len(entries.X509CAs)-1] - s.Require().Equal(now, time.Unix(lastEntry.IssuedAt, 0).UTC()) + require.Equal(t, now, time.Unix(lastEntry.IssuedAt, 0).UTC()) } -func (s *JournalSuite) TestUpdateX509CAStatus() { - t := s.T() - firstIssuedAt := s.now() +func TestUpdateX509CAStatus(t *testing.T) { + test := setupJournalTest(t) + + firstIssuedAt := test.now() secondIssuedAt := firstIssuedAt.Add(time.Minute) thirdIssuedAt := secondIssuedAt.Add(time.Minute) - testJournal := s.loadJournal() + testJournal := test.loadJournal(t) - err := testJournal.AppendX509CA("A", firstIssuedAt, &ca.X509CA{ - Signer: testSigner, - Certificate: testChain[0], + err := testJournal.AppendX509CA(ctx, "A", firstIssuedAt, &ca.X509CA{ + Signer: kmKeys["X509-CA-A"], + Certificate: rootCerts["X509-Root-A"], }) require.NoError(t, err) - err = testJournal.AppendX509CA("B", secondIssuedAt, &ca.X509CA{ - Signer: testSigner, - Certificate: testChain[0], + err = testJournal.AppendX509CA(ctx, "B", secondIssuedAt, &ca.X509CA{ + Signer: kmKeys["X509-CA-B"], + Certificate: rootCerts["X509-Root-B"], }) require.NoError(t, err) - err = testJournal.AppendX509CA("C", thirdIssuedAt, &ca.X509CA{ - Signer: testSigner, - Certificate: testChain[0], + err = testJournal.AppendX509CA(ctx, "C", thirdIssuedAt, &ca.X509CA{ + Signer: kmKeys["X509-CA-C"], + Certificate: rootCerts["X509-Root-C"], }) require.NoError(t, err) @@ -159,10 +267,10 @@ func (s *JournalSuite) TestUpdateX509CAStatus() { require.Equal(t, journal.Status_PREPARED, ca.Status) } - err = testJournal.UpdateX509CAStatus(secondIssuedAt, journal.Status_ACTIVE) + err = testJournal.UpdateX509CAStatus(ctx, secondIssuedAt, journal.Status_ACTIVE) require.NoError(t, err) - for _, ca := range testJournal.Entries().X509CAs { + for _, ca := range testJournal.getEntries().X509CAs { expectedStatus := journal.Status_PREPARED if ca.SlotId == "B" { expectedStatus = journal.Status_ACTIVE @@ -171,47 +279,48 @@ func (s *JournalSuite) TestUpdateX509CAStatus() { require.Equal(t, expectedStatus, ca.Status) } - unusedTime := s.now().Add(time.Hour) - err = testJournal.UpdateX509CAStatus(unusedTime, journal.Status_OLD) + unusedTime := test.now().Add(time.Hour) + err = testJournal.UpdateX509CAStatus(ctx, unusedTime, journal.Status_OLD) require.ErrorContains(t, err, "no journal entry found issued at:") } -func (s *JournalSuite) TestUpdateJWTKeyStatus() { - t := s.T() - firstIssuedAt := s.now() +func TestUpdateJWTKeyStatus(t *testing.T) { + test := setupJournalTest(t) + + firstIssuedAt := test.now() secondIssuedAt := firstIssuedAt.Add(time.Minute) thirdIssuedAt := secondIssuedAt.Add(time.Minute) - testJournal := s.loadJournal() + testJournal := test.loadJournal(t) - err := testJournal.AppendJWTKey("A", firstIssuedAt, &ca.JWTKey{ - Signer: testSigner, + err := testJournal.AppendJWTKey(ctx, "A", firstIssuedAt, &ca.JWTKey{ + Signer: kmKeys["JWT-Signer-A"], Kid: "kid1", }) require.NoError(t, err) - err = testJournal.AppendJWTKey("B", secondIssuedAt, &ca.JWTKey{ - Signer: testSigner, + err = testJournal.AppendJWTKey(ctx, "B", secondIssuedAt, &ca.JWTKey{ + Signer: kmKeys["JWT-Signer-B"], Kid: "kid2", }) require.NoError(t, err) - err = testJournal.AppendJWTKey("C", thirdIssuedAt, &ca.JWTKey{ - Signer: testSigner, + err = testJournal.AppendJWTKey(ctx, "C", thirdIssuedAt, &ca.JWTKey{ + Signer: kmKeys["JWT-Signer-C"], Kid: "kid3", }) require.NoError(t, err) - keys := testJournal.Entries().JwtKeys + keys := testJournal.getEntries().JwtKeys require.Len(t, keys, 3) for _, key := range keys { require.Equal(t, journal.Status_PREPARED, key.Status) } - err = testJournal.UpdateJWTKeyStatus(secondIssuedAt, journal.Status_ACTIVE) + err = testJournal.UpdateJWTKeyStatus(ctx, secondIssuedAt, journal.Status_ACTIVE) require.NoError(t, err) - for _, key := range testJournal.Entries().JwtKeys { + for _, key := range testJournal.getEntries().JwtKeys { expectedStatus := journal.Status_PREPARED if key.SlotId == "B" { expectedStatus = journal.Status_ACTIVE @@ -220,86 +329,97 @@ func (s *JournalSuite) TestUpdateJWTKeyStatus() { require.Equal(t, expectedStatus, key.Status) } - unusedTime := s.now().Add(time.Hour) - err = testJournal.UpdateJWTKeyStatus(unusedTime, journal.Status_OLD) + unusedTime := test.now().Add(time.Hour) + err = testJournal.UpdateJWTKeyStatus(ctx, unusedTime, journal.Status_OLD) require.ErrorContains(t, err, "no journal entry found issued at:") } -func (s *JournalSuite) TestJWTKeyOverflow() { - now := s.now() +func TestJWTKeyOverflow(t *testing.T) { + test := setupJournalTest(t) + + now := test.now() - journal := s.loadJournal() + journal := test.loadJournal(t) for i := 0; i < (journalCap + 1); i++ { now = now.Add(time.Minute) - err := journal.AppendJWTKey("B", now, &ca.JWTKey{ - Signer: testSigner, + err := journal.AppendJWTKey(ctx, "B", now, &ca.JWTKey{ + Signer: kmKeys["JWT-Signer-B"], Kid: "KID", NotAfter: now.Add(time.Hour), }) - s.Require().NoError(err) + require.NoError(t, err) } - entries := journal.Entries() - s.Require().Len(entries.JwtKeys, journalCap, "JWT key entries exceeds cap") + entries := journal.getEntries() + require.Len(t, entries.JwtKeys, journalCap, "JWT key entries exceeds cap") lastEntry := entries.JwtKeys[len(entries.JwtKeys)-1] - s.Require().Equal(now, time.Unix(lastEntry.IssuedAt, 0).UTC()) + require.Equal(t, now, time.Unix(lastEntry.IssuedAt, 0).UTC()) } -func (s *JournalSuite) TestBadPEM() { - s.writeString(s.journalPath(), "NOT PEM") - _, err := LoadJournal(s.journalPath()) - s.EqualError(err, "invalid PEM block") +func TestBadPEM(t *testing.T) { + test := setupJournalTest(t) + + test.writeString(t, test.jc.filePath, "NOT PEM") + _, err := LoadJournal(ctx, test.jc) + require.EqualError(t, err, "failed to load journal from disk: invalid PEM block") } -func (s *JournalSuite) TestUnexpectedPEMType() { - s.writeBytes(s.journalPath(), pem.EncodeToMemory(&pem.Block{ +func TestUnexpectedPEMType(t *testing.T) { + test := setupJournalTest(t) + + test.writeBytes(t, test.jc.filePath, pem.EncodeToMemory(&pem.Block{ Type: "WHATEVER", Bytes: []byte("FOO"), })) - _, err := LoadJournal(s.journalPath()) - s.EqualError(err, `invalid PEM block type "WHATEVER"`) + _, err := LoadJournal(ctx, test.jc) + require.EqualError(t, err, `failed to load journal from disk: invalid PEM block type "WHATEVER"`) } -func (s *JournalSuite) TestBadProto() { - s.writeBytes(s.journalPath(), pem.EncodeToMemory(&pem.Block{ +func TestBadProto(t *testing.T) { + test := setupJournalTest(t) + + test.writeBytes(t, test.jc.filePath, pem.EncodeToMemory(&pem.Block{ Type: journalPEMType, Bytes: []byte("FOO"), })) - _, err := LoadJournal(s.journalPath()) - s.Require().Error(err) - s.Contains(err.Error(), `unable to unmarshal entries: `) + _, err := LoadJournal(ctx, test.jc) + require.Error(t, err) + require.Contains(t, err.Error(), `unable to unmarshal entries: `) } -func (s *JournalSuite) loadJournal() *Journal { - journal, err := LoadJournal(s.journalPath()) - s.Require().NoError(err) +func (j *journalTest) loadJournal(t *testing.T) *Journal { + journal, err := LoadJournal(ctx, j.jc) + require.NoError(t, err) return journal } -func (s *JournalSuite) journalPath() string { - return s.pathTo("journal.pem") +func (j *journalTest) loadJournalFromDisk(t *testing.T) *Journal { + journal, err := loadJournalFromDisk(j.jc) + require.NoError(t, err) + return journal } -func (s *JournalSuite) pathTo(relativePath string) string { - return filepath.Join(s.dir, relativePath) +func (j *journalTest) loadJournalFromDS(t *testing.T) *Journal { + journal, err := loadJournalFromDS(ctx, j.jc) + require.NoError(t, err) + return journal } -func (s *JournalSuite) writeString(path, data string) { - s.writeBytes(path, []byte(data)) +func (j *journalTest) writeString(t *testing.T, path, data string) { + j.writeBytes(t, path, []byte(data)) } -func (s *JournalSuite) writeBytes(path string, data []byte) { - s.Require().NoError(os.WriteFile(path, data, 0600)) +func (j *journalTest) writeBytes(t *testing.T, path string, data []byte) { + require.NoError(t, os.WriteFile(path, data, 0600)) } -func (s *JournalSuite) now() time.Time { +func (j *journalTest) now() time.Time { // return truncated UTC time for cleaner failure messages return time.Now().UTC().Truncate(time.Second) } -func (s *JournalSuite) requireProtoEqual(expected, actual proto.Message) { - if !proto.Equal(expected, actual) { - s.Require().Equal(expected, actual) - } +type journalTest struct { + jc *journalConfig + ds *fakedatastore.DataStore } diff --git a/pkg/server/ca/manager/manager.go b/pkg/server/ca/manager/manager.go index 7868291d6e..9477ce2ea1 100644 --- a/pkg/server/ca/manager/manager.go +++ b/pkg/server/ca/manager/manager.go @@ -31,8 +31,9 @@ import ( ) const ( - publishJWKTimeout = 5 * time.Second - safetyThreshold = 24 * time.Hour + publishJWKTimeout = 5 * time.Second + safetyThresholdBundle = 24 * time.Hour + safetyThresholdCAJournals = time.Hour * 24 * 14 // Two weeks thirtyDays = 30 * 24 * time.Hour preparationThresholdCap = thirtyDays @@ -73,12 +74,12 @@ type Manager struct { upstreamClient *ca.UpstreamClient upstreamPluginName string - currentX509CA *X509CASlot - nextX509CA *X509CASlot + currentX509CA *x509CASlot + nextX509CA *x509CASlot x509CAMutex sync.RWMutex - currentJWTKey *JwtKeySlot - nextJWTKey *JwtKeySlot + currentJWTKey *jwtKeySlot + nextJWTKey *jwtKeySlot jwtKeyMutex sync.RWMutex journal *Journal @@ -119,7 +120,7 @@ func NewManager(ctx context.Context, c Config) (*Manager, error) { UpstreamClient: m.upstreamClient, } - journal, slots, err := loader.Load(ctx) + journal, slots, err := loader.load(ctx) if err != nil { return nil, err } @@ -127,21 +128,21 @@ func NewManager(ctx context.Context, c Config) (*Manager, error) { now := m.c.Clock.Now() m.journal = journal if currentX509CA, ok := slots[CurrentX509CASlot]; ok { - m.currentX509CA = currentX509CA.(*X509CASlot) + m.currentX509CA = currentX509CA.(*x509CASlot) if !currentX509CA.IsEmpty() && !currentX509CA.ShouldActivateNext(now) { // activate the X509CA immediately if it is set and not within // activation time of the next X509CA. - m.activateX509CA() + m.activateX509CA(ctx) } } if nextX509CA, ok := slots[NextX509CASlot]; ok { - m.nextX509CA = nextX509CA.(*X509CASlot) + m.nextX509CA = nextX509CA.(*x509CASlot) } if currentJWTKey, ok := slots[CurrentJWTKeySlot]; ok { - m.currentJWTKey = currentJWTKey.(*JwtKeySlot) + m.currentJWTKey = currentJWTKey.(*jwtKeySlot) // TODO: Activation on journal depends on dates, it will need to be // refactored to allow to set a status, because when forcing a rotation, @@ -149,12 +150,12 @@ func NewManager(ctx context.Context, c Config) (*Manager, error) { if !currentJWTKey.IsEmpty() && !currentJWTKey.ShouldActivateNext(now) { // activate the JWT key immediately if it is set and not within // activation time of the next JWT key. - m.activateJWTKey() + m.activateJWTKey(ctx) } } if nextJWTKey, ok := slots[NextJWTKeySlot]; ok { - m.nextJWTKey = nextJWTKey.(*JwtKeySlot) + m.nextJWTKey = nextJWTKey.(*jwtKeySlot) } return m, nil @@ -229,7 +230,7 @@ func (m *Manager) PrepareX509CA(ctx context.Context) (err error) { slot.publicKey = slot.x509CA.Certificate.PublicKey slot.notAfter = slot.x509CA.Certificate.NotAfter - if err := m.journal.AppendX509CA(slot.id, slot.issuedAt, slot.x509CA); err != nil { + if err := m.journal.AppendX509CA(ctx, slot.id, slot.issuedAt, slot.x509CA); err != nil { log.WithError(err).Error("Unable to append X509 CA to journal") } @@ -243,24 +244,24 @@ func (m *Manager) PrepareX509CA(ctx context.Context) (err error) { return nil } -func (m *Manager) ActivateX509CA() { +func (m *Manager) ActivateX509CA(ctx context.Context) { m.x509CAMutex.RLock() defer m.x509CAMutex.RUnlock() - m.activateX509CA() + m.activateX509CA(ctx) } -func (m *Manager) RotateX509CA() { +func (m *Manager) RotateX509CA(ctx context.Context) { m.x509CAMutex.Lock() defer m.x509CAMutex.Unlock() m.currentX509CA, m.nextX509CA = m.nextX509CA, m.currentX509CA m.nextX509CA.Reset() - if err := m.journal.UpdateX509CAStatus(m.nextX509CA.issuedAt, journal.Status_OLD); err != nil { + if err := m.journal.UpdateX509CAStatus(ctx, m.nextX509CA.issuedAt, journal.Status_OLD); err != nil { m.c.Log.WithError(err).Error("Failed to update status on X509CA journal entry") } - m.activateX509CA() + m.activateX509CA(ctx) } func (m *Manager) GetCurrentJWTKeySlot() Slot { @@ -324,7 +325,7 @@ func (m *Manager) PrepareJWTKey(ctx context.Context) (err error) { slot.authorityID = jwtKey.Kid slot.notAfter = jwtKey.NotAfter - if err := m.journal.AppendJWTKey(slot.id, slot.issuedAt, slot.jwtKey); err != nil { + if err := m.journal.AppendJWTKey(ctx, slot.id, slot.issuedAt, slot.jwtKey); err != nil { log.WithError(err).Error("Unable to append JWT key to journal") } @@ -337,25 +338,25 @@ func (m *Manager) PrepareJWTKey(ctx context.Context) (err error) { return nil } -func (m *Manager) ActivateJWTKey() { +func (m *Manager) ActivateJWTKey(ctx context.Context) { m.jwtKeyMutex.RLock() defer m.jwtKeyMutex.RUnlock() - m.activateJWTKey() + m.activateJWTKey(ctx) } -func (m *Manager) RotateJWTKey() { +func (m *Manager) RotateJWTKey(ctx context.Context) { m.jwtKeyMutex.Lock() defer m.jwtKeyMutex.Unlock() m.currentJWTKey, m.nextJWTKey = m.nextJWTKey, m.currentJWTKey m.nextJWTKey.Reset() - if err := m.journal.UpdateJWTKeyStatus(m.nextJWTKey.issuedAt, journal.Status_OLD); err != nil { + if err := m.journal.UpdateJWTKeyStatus(ctx, m.nextJWTKey.issuedAt, journal.Status_OLD); err != nil { m.c.Log.WithError(err).Error("Failed to update status on JWTKey journal entry") } - m.activateJWTKey() + m.activateJWTKey(ctx) } // PublishJWTKey publishes the passed JWK to the upstream server using the configured @@ -408,7 +409,7 @@ func (m *Manager) PruneBundle(ctx context.Context) (err error) { defer counter.Done(&err) ds := m.c.Catalog.GetDataStore() - expiresBefore := m.c.Clock.Now().Add(-safetyThreshold) + expiresBefore := m.c.Clock.Now().Add(-safetyThresholdBundle) changed, err := ds.PruneBundle(ctx, m.c.TrustDomain.IDString(), expiresBefore) @@ -425,6 +426,21 @@ func (m *Manager) PruneBundle(ctx context.Context) (err error) { return nil } +func (m *Manager) PruneCAJournals(ctx context.Context) (err error) { + counter := telemetry_server.StartCAManagerPruneBundleCall(m.c.Metrics) + defer counter.Done(&err) + + ds := m.c.Catalog.GetDataStore() + expiresBefore := m.c.Clock.Now().Add(-safetyThresholdCAJournals) + + err = ds.PruneCAJournals(ctx, expiresBefore.Unix()) + + if err != nil { + return fmt.Errorf("unable to prune CA journals: %w", err) + } + return nil +} + func (m *Manager) NotifyOnBundleUpdate(ctx context.Context) { for { select { @@ -457,7 +473,7 @@ func (m *Manager) NotifyBundleLoaded(ctx context.Context) error { ) } -func (m *Manager) activateJWTKey() { +func (m *Manager) activateJWTKey(ctx context.Context) { log := m.c.Log.WithFields(logrus.Fields{ telemetry.Slot: m.currentJWTKey.id, telemetry.IssuedAt: m.currentJWTKey.issuedAt, @@ -468,14 +484,14 @@ func (m *Manager) activateJWTKey() { telemetry_server.IncrActivateJWTKeyManagerCounter(m.c.Metrics) m.currentJWTKey.status = journal.Status_ACTIVE - if err := m.journal.UpdateJWTKeyStatus(m.currentJWTKey.issuedAt, journal.Status_ACTIVE); err != nil { + if err := m.journal.UpdateJWTKeyStatus(ctx, m.currentJWTKey.issuedAt, journal.Status_ACTIVE); err != nil { log.WithError(err).Error("Failed to update to activated status on JWTKey journal entry") } m.c.CA.SetJWTKey(m.currentJWTKey.jwtKey) } -func (m *Manager) activateX509CA() { +func (m *Manager) activateX509CA(ctx context.Context) { log := m.c.Log.WithFields(logrus.Fields{ telemetry.Slot: m.currentX509CA.id, telemetry.IssuedAt: m.currentX509CA.issuedAt, @@ -486,7 +502,7 @@ func (m *Manager) activateX509CA() { telemetry_server.IncrActivateX509CAManagerCounter(m.c.Metrics) m.currentX509CA.status = journal.Status_ACTIVE - if err := m.journal.UpdateX509CAStatus(m.currentX509CA.issuedAt, journal.Status_ACTIVE); err != nil { + if err := m.journal.UpdateX509CAStatus(ctx, m.currentX509CA.issuedAt, journal.Status_ACTIVE); err != nil { log.WithError(err).Error("Failed to update to activated status on X509CA journal entry") } diff --git a/pkg/server/ca/manager/manager_test.go b/pkg/server/ca/manager/manager_test.go index 917f1497cb..8c464eba27 100644 --- a/pkg/server/ca/manager/manager_test.go +++ b/pkg/server/ca/manager/manager_test.go @@ -23,6 +23,7 @@ import ( "github.com/spiffe/spire/pkg/server/ca" "github.com/spiffe/spire/pkg/server/credtemplate" "github.com/spiffe/spire/pkg/server/credvalidator" + "github.com/spiffe/spire/pkg/server/datastore" "github.com/spiffe/spire/pkg/server/plugin/keymanager" "github.com/spiffe/spire/pkg/server/plugin/notifier" "github.com/spiffe/spire/pkg/server/plugin/upstreamauthority" @@ -39,6 +40,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/proto" ) const ( @@ -60,7 +62,7 @@ func TestGetCurrentJWTKeySlot(t *testing.T) { t.Run("no authority created", func(t *testing.T) { currentSlot := test.m.GetCurrentJWTKeySlot() - slot := currentSlot.(*JwtKeySlot) + slot := currentSlot.(*jwtKeySlot) require.True(t, slot.IsEmpty()) require.Empty(t, slot.issuedAt) @@ -75,7 +77,7 @@ func TestGetCurrentJWTKeySlot(t *testing.T) { require.NoError(t, test.m.PrepareJWTKey(ctx)) currentSlot := test.m.GetCurrentJWTKeySlot() - slot := currentSlot.(*JwtKeySlot) + slot := currentSlot.(*jwtKeySlot) require.NotNil(t, slot.jwtKey) require.NotEmpty(t, slot.authorityID) require.Equal(t, expectIssuedAt, slot.issuedAt) @@ -91,7 +93,7 @@ func TestGetNextJWTKeySlot(t *testing.T) { t.Run("no next created", func(t *testing.T) { nextSlot := test.m.GetNextJWTKeySlot() - slot := nextSlot.(*JwtKeySlot) + slot := nextSlot.(*jwtKeySlot) require.Nil(t, slot.jwtKey) require.Empty(t, slot.issuedAt) @@ -106,7 +108,7 @@ func TestGetNextJWTKeySlot(t *testing.T) { require.NoError(t, test.m.PrepareJWTKey(ctx)) nextSlot := test.m.GetNextJWTKeySlot() - slot := nextSlot.(*JwtKeySlot) + slot := nextSlot.(*jwtKeySlot) require.NotNil(t, slot.jwtKey) require.NotEmpty(t, slot.authorityID) require.Equal(t, expectIssuedAt, slot.issuedAt) @@ -123,7 +125,7 @@ func TestGetCurrentX509CASlot(t *testing.T) { t.Run("no authority created", func(t *testing.T) { currentSlot := test.m.GetCurrentX509CASlot() - slot := currentSlot.(*X509CASlot) + slot := currentSlot.(*x509CASlot) require.Nil(t, slot.x509CA) require.Empty(t, slot.authorityID) require.Empty(t, slot.issuedAt) @@ -138,7 +140,7 @@ func TestGetCurrentX509CASlot(t *testing.T) { require.NoError(t, test.m.PrepareX509CA(ctx)) currentSlot := test.m.GetCurrentX509CASlot() - slot := currentSlot.(*X509CASlot) + slot := currentSlot.(*x509CASlot) require.NotNil(t, slot.x509CA) require.NotEmpty(t, slot.authorityID) require.NotNil(t, slot.publicKey) @@ -155,7 +157,7 @@ func TestGetNextX509CASlot(t *testing.T) { t.Run("no next created", func(t *testing.T) { nextSlot := test.m.GetNextX509CASlot() - slot := nextSlot.(*X509CASlot) + slot := nextSlot.(*x509CASlot) require.Nil(t, slot.x509CA) require.Empty(t, slot.authorityID) @@ -171,7 +173,7 @@ func TestGetNextX509CASlot(t *testing.T) { require.NoError(t, test.m.PrepareX509CA(ctx)) nextSlot := test.m.GetNextX509CASlot() - slot := nextSlot.(*X509CASlot) + slot := nextSlot.(*x509CASlot) require.NotNil(t, slot.x509CA) require.NotEmpty(t, slot.authorityID) require.NotNil(t, slot.publicKey) @@ -192,9 +194,9 @@ func TestPersistence(t *testing.T) { // Prepare authority and activate authority require.NoError(t, test.m.PrepareJWTKey(ctx)) - test.m.ActivateJWTKey() + test.m.ActivateJWTKey(ctx) require.NoError(t, test.m.PrepareX509CA(ctx)) - test.m.ActivateX509CA() + test.m.ActivateX509CA(ctx) firstX509CA, firstJWTKey := test.currentX509CA(), test.currentJWTKey() @@ -418,7 +420,7 @@ func TestX509CARotation(t *testing.T) { // Rotate "next" should become "current" and // "next" should be reset. - test.m.RotateX509CA() + test.m.RotateX509CA(ctx) test.requireX509CAEqual(t, second, test.currentX509CA()) require.Equal(t, journal.Status_ACTIVE, test.currentX509CAStatus()) assert.Nil(t, test.nextX509CA()) @@ -440,7 +442,7 @@ func TestX509CARotation(t *testing.T) { // Rotate again, "next" should become "current" and // "next" should be reset. - test.m.RotateX509CA() + test.m.RotateX509CA(ctx) test.requireX509CAEqual(t, third, test.currentX509CA()) require.Equal(t, journal.Status_ACTIVE, test.currentX509CAStatus()) assert.Nil(t, test.nextX509CA()) @@ -458,7 +460,7 @@ func TestX509CARotationMetric(t *testing.T) { // reset the metrics rotate CA to activate mark test.metrics.Reset() - test.m.RotateX509CA() + test.m.RotateX509CA(ctx) // create expected metrics with ttl from certificate expected := fakemetrics.New() @@ -507,7 +509,7 @@ func TestJWTKeyRotation(t *testing.T) { // rotate, "next" should become "current" and // "next" should be reset. - test.m.RotateJWTKey() + test.m.RotateJWTKey(ctx) test.requireJWTKeyEqual(t, second, test.currentJWTKey()) require.Equal(t, journal.Status_ACTIVE, test.currentJWTKeyStatus()) assert.Nil(t, test.nextJWTKey()) @@ -529,7 +531,7 @@ func TestJWTKeyRotation(t *testing.T) { // rotate again. "next" should become "current" and // "next" should be reset. - test.m.RotateJWTKey() + test.m.RotateJWTKey(ctx) test.requireJWTKeyEqual(t, third, test.currentJWTKey()) require.Equal(t, journal.Status_ACTIVE, test.currentJWTKeyStatus()) assert.Nil(t, test.nextJWTKey()) @@ -578,7 +580,7 @@ func TestPruneBundle(t *testing.T) { // advance beyond the safety threshold of the first, prune, and assert that // the first has been pruned - test.addTimeAndPrune(safetyThreshold) + test.addTimeAndPrune(safetyThresholdBundle) test.requireBundleRootCAs(ctx, t, secondX509CA.Certificate) test.requireBundleJWTKeys(ctx, t, secondJWTKey) @@ -587,12 +589,138 @@ func TestPruneBundle(t *testing.T) { // advance beyond the second expiration time, prune, and assert nothing // changes because we can't prune out the whole bundle. - test.clock.Set(secondExpiresTime.Add(time.Minute + safetyThreshold)) + test.clock.Set(secondExpiresTime.Add(time.Minute + safetyThresholdBundle)) require.EqualError(t, test.m.PruneBundle(context.Background()), "unable to prune bundle: rpc error: code = Unknown desc = prune failed: would prune all certificates") test.requireBundleRootCAs(ctx, t, secondX509CA.Certificate) test.requireBundleJWTKeys(ctx, t, secondJWTKey) } +func TestPruneCAJournals(t *testing.T) { + test := setupTest(t) + test.initSelfSignedManager() + + type testJournal struct { + Journal + shouldBePruned bool + } + + timeNow := test.clock.Now() + now := timeNow.Unix() + tomorrow := timeNow.Add(time.Hour * 24).Unix() + beforeThreshold := timeNow.Add(-safetyThresholdCAJournals).Add(-time.Minute).Unix() + + jc := &journalConfig{ + cat: test.cat, + log: test.log, + } + testCases := []struct { + name string + entries *journal.Entries + testJournals []*testJournal + }{ + { + name: "no journals with CAs expired before the threshold - no journals to be pruned", + testJournals: []*testJournal{ + { + Journal: Journal{ + config: jc, + entries: &journal.Entries{ + X509CAs: []*journal.X509CAEntry{{NotAfter: now}, {NotAfter: tomorrow}}, + JwtKeys: []*journal.JWTKeyEntry{{NotAfter: now}, {NotAfter: tomorrow}}, + }, + }, + }, + { + Journal: Journal{ + config: jc, + entries: &journal.Entries{ + X509CAs: []*journal.X509CAEntry{{NotAfter: now}, {NotAfter: tomorrow}}, + JwtKeys: []*journal.JWTKeyEntry{{NotAfter: now}, {NotAfter: tomorrow}}, + }, + }, + }, + }, + }, + { + name: "some journals with CAs expired before the threshold, but not all - no journals to be pruned", + testJournals: []*testJournal{ + { + Journal: Journal{ + config: jc, + entries: &journal.Entries{ + X509CAs: []*journal.X509CAEntry{{NotAfter: tomorrow}, {NotAfter: beforeThreshold}}, + JwtKeys: []*journal.JWTKeyEntry{{NotAfter: beforeThreshold}, {NotAfter: tomorrow}}, + }, + }, + }, + { + Journal: Journal{ + config: jc, + entries: &journal.Entries{ + X509CAs: []*journal.X509CAEntry{{NotAfter: tomorrow}, {NotAfter: beforeThreshold}}, + JwtKeys: []*journal.JWTKeyEntry{{NotAfter: beforeThreshold}, {NotAfter: tomorrow}}, + }, + }, + }, + }, + }, + { + name: "all CAs expired before the threshold in a journal - one journal to be pruned", + testJournals: []*testJournal{ + { + shouldBePruned: true, + Journal: Journal{ + config: jc, + entries: &journal.Entries{ + X509CAs: []*journal.X509CAEntry{{NotAfter: beforeThreshold}, {NotAfter: beforeThreshold}}, + JwtKeys: []*journal.JWTKeyEntry{{NotAfter: beforeThreshold}, {NotAfter: beforeThreshold}}, + }, + }, + }, + { + Journal: Journal{ + config: jc, + entries: &journal.Entries{ + X509CAs: []*journal.X509CAEntry{{NotAfter: tomorrow}, {NotAfter: beforeThreshold}}, + JwtKeys: []*journal.JWTKeyEntry{{NotAfter: beforeThreshold}, {NotAfter: tomorrow}}, + }, + }, + }, + }, + }, + } + + var expectedCAJournals []*datastore.CAJournal + for _, testCase := range testCases { + testCase := testCase + expectedCAJournals = []*datastore.CAJournal{} + t.Run(testCase.name, func(t *testing.T) { + // Have a fresh data store in each test case + test.ds = fakedatastore.New(t) + test.m.c.Catalog.(*fakeservercatalog.Catalog).SetDataStore(test.ds) + + for _, j := range testCase.testJournals { + entriesBytes, err := proto.Marshal(j.entries) + require.NoError(t, err) + caJournal, err := test.ds.SetCAJournal(ctx, &datastore.CAJournal{ + ActiveX509AuthorityID: "", + Data: entriesBytes, + }) + require.NoError(t, err) + + if !j.shouldBePruned { + expectedCAJournals = append(expectedCAJournals, caJournal) + } + } + + require.NoError(t, test.m.PruneCAJournals(ctx)) + caJournals, err := test.ds.ListCAJournalsForTesting(ctx) + require.NoError(t, err) + require.ElementsMatch(t, expectedCAJournals, caJournals) + }) + } +} + func TestRunNotifiesBundleLoaded(t *testing.T) { test := setupTest(t) test.initAndActivateSelfSignedManager(context.Background()) @@ -805,9 +933,9 @@ func TestAlternateKeyTypes(t *testing.T) { // Prepare and activate a bundle require.NoError(t, test.m.PrepareJWTKey(ctx)) - test.m.ActivateJWTKey() + test.m.ActivateJWTKey(ctx) require.NoError(t, test.m.PrepareX509CA(ctx)) - test.m.activateX509CA() + test.m.activateX509CA(ctx) testCase.checkX509CA(t, test.currentX509CA().Signer) testCase.checkJWTKey(t, test.currentJWTKey().Signer) @@ -889,9 +1017,9 @@ func (m *managerTest) initAndActivateSelfSignedManager(ctx context.Context) { require.NoError(m.t, err) require.NoError(m.t, manager.PrepareJWTKey(ctx)) - manager.ActivateJWTKey() + manager.ActivateJWTKey(ctx) require.NoError(m.t, manager.PrepareX509CA(ctx)) - manager.ActivateX509CA() + manager.ActivateX509CA(ctx) m.m = manager } @@ -914,9 +1042,9 @@ func (m *managerTest) initAndActivateUpstreamSignedManager(ctx context.Context, m.initUpstreamSignedManager(upstreamAuthority) require.NoError(m.t, m.m.PrepareJWTKey(ctx)) - m.m.ActivateJWTKey() + m.m.ActivateJWTKey(ctx) require.NoError(m.t, m.m.PrepareX509CA(ctx)) - m.m.ActivateX509CA() + m.m.ActivateX509CA(ctx) } func (m *managerTest) selfSignedConfig() Config { @@ -1088,7 +1216,13 @@ func (m *managerTest) addTimeAndPrune(d time.Duration) { } func (m *managerTest) wipeJournal(t *testing.T) { + // TODO: journal saved on this will no longer be supported in v1.10. Remove + // this. require.NoError(t, os.Remove(filepath.Join(m.m.c.Dir, "journal.pem"))) + + // Have a clean datastore. + m.ds = fakedatastore.New(t) + m.cat.SetDataStore(m.ds) } func (m *managerTest) waitForBundleUpdatedNotification(ctx context.Context, ch <-chan *common.Bundle) { diff --git a/pkg/server/ca/manager/slot.go b/pkg/server/ca/manager/slot.go index 871ad55fb8..85fdc80625 100644 --- a/pkg/server/ca/manager/slot.go +++ b/pkg/server/ca/manager/slot.go @@ -53,19 +53,23 @@ type SlotLoader struct { UpstreamClient *ca.UpstreamClient } -func (s *SlotLoader) Load(ctx context.Context) (*Journal, map[SlotPosition]Slot, error) { +func (s *SlotLoader) load(ctx context.Context) (*Journal, map[SlotPosition]Slot, error) { log := s.Log - journalPath := s.journalPath() + + jc := &journalConfig{ + cat: s.Catalog, + log: log, + filePath: s.journalPath(), + } // Load the journal and see if we can figure out the next and current // X509CA and JWTKey entries, if any. - log.WithField(telemetry.Path, journalPath).Debug("Loading journal") - loadedJournal, err := LoadJournal(journalPath) + loadedJournal, err := LoadJournal(ctx, jc) if err != nil { return nil, nil, err } - entries := loadedJournal.Entries() + entries := loadedJournal.getEntries() log.WithFields(logrus.Fields{ telemetry.X509CAs: len(entries.X509CAs), @@ -112,9 +116,9 @@ func (s *SlotLoader) Load(ctx context.Context) (*Journal, map[SlotPosition]Slot, // - If all the statuses are unknown, the two most recent slots are returned. // - Active entry is returned on current slot if set. // - The most recent Prepared or Old entry is returned on next slot. -func (s *SlotLoader) getX509CASlots(ctx context.Context, entries []*X509CAEntry) (*X509CASlot, *X509CASlot, error) { - var current *X509CASlot - var next *X509CASlot +func (s *SlotLoader) getX509CASlots(ctx context.Context, entries []*journal.X509CAEntry) (*x509CASlot, *x509CASlot, error) { + var current *x509CASlot + var next *x509CASlot // Search from oldest for i := len(entries) - 1; i >= 0; i-- { @@ -185,9 +189,9 @@ func (s *SlotLoader) getX509CASlots(ctx context.Context, entries []*X509CAEntry) // - If all status are unknown, choose the two newest on the list // - Active entry is returned on current if set // - Newest Prepared or Old entry is returned on next -func (s *SlotLoader) getJWTKeysSlots(ctx context.Context, entries []*journal.JWTKeyEntry) (*JwtKeySlot, *JwtKeySlot, error) { - var current *JwtKeySlot - var next *JwtKeySlot +func (s *SlotLoader) getJWTKeysSlots(ctx context.Context, entries []*journal.JWTKeyEntry) (*jwtKeySlot, *jwtKeySlot, error) { + var current *jwtKeySlot + var next *jwtKeySlot // Search from oldest for i := len(entries) - 1; i >= 0; i-- { @@ -262,7 +266,7 @@ func (s *SlotLoader) getJWTKeysSlots(ctx context.Context, entries []*journal.JWT // If we find such a discrepancy, removing the entry from the journal prior to beginning signing // operations prevents us from using a signing key that consumers may not be able to validate. // Instead, we'll rotate into a new one. -func (s *SlotLoader) filterInvalidEntries(ctx context.Context, entries *journal.Entries) ([]*JWTKeyEntry, []*X509CAEntry, error) { +func (s *SlotLoader) filterInvalidEntries(ctx context.Context, entries *journal.Entries) ([]*journal.JWTKeyEntry, []*journal.X509CAEntry, error) { bundle, err := s.fetchOptionalBundle(ctx) if err != nil { @@ -273,7 +277,7 @@ func (s *SlotLoader) filterInvalidEntries(ctx context.Context, entries *journal. return entries.JwtKeys, entries.X509CAs, nil } - filteredEntriesJwtKeys := []*JWTKeyEntry{} + filteredEntriesJwtKeys := []*journal.JWTKeyEntry{} for _, entry := range entries.GetJwtKeys() { if containsJwtSigningKeyID(bundle.JwtSigningKeys, entry.Kid) { @@ -288,7 +292,7 @@ func (s *SlotLoader) filterInvalidEntries(ctx context.Context, entries *journal. return filteredEntriesJwtKeys, entries.X509CAs, nil } - filteredEntriesX509CAs := []*X509CAEntry{} + filteredEntriesX509CAs := []*journal.X509CAEntry{} for _, entry := range entries.GetX509CAs() { if containsX509CA(bundle.RootCas, entry.Certificate) { @@ -309,7 +313,7 @@ func (s *SlotLoader) fetchOptionalBundle(ctx context.Context) (*common.Bundle, e return bundle, nil } -func (s *SlotLoader) tryLoadX509CASlotFromEntry(ctx context.Context, entry *X509CAEntry) (*X509CASlot, error) { +func (s *SlotLoader) tryLoadX509CASlotFromEntry(ctx context.Context, entry *journal.X509CAEntry) (*x509CASlot, error) { slot, badReason, err := s.loadX509CASlotFromEntry(ctx, entry) if err != nil { s.Log.WithError(err).WithFields(logrus.Fields{ @@ -332,7 +336,7 @@ func (s *SlotLoader) tryLoadX509CASlotFromEntry(ctx context.Context, entry *X509 return slot, nil } -func (s *SlotLoader) loadX509CASlotFromEntry(ctx context.Context, entry *X509CAEntry) (*X509CASlot, string, error) { +func (s *SlotLoader) loadX509CASlotFromEntry(ctx context.Context, entry *journal.X509CAEntry) (*x509CASlot, string, error) { if entry.SlotId == "" { return nil, "no slot id", nil } @@ -363,7 +367,7 @@ func (s *SlotLoader) loadX509CASlotFromEntry(ctx context.Context, entry *X509CAE return nil, "public key does not match key manager key", nil } - return &X509CASlot{ + return &x509CASlot{ id: entry.SlotId, issuedAt: time.Unix(entry.IssuedAt, 0), x509CA: &ca.X509CA{ @@ -378,7 +382,7 @@ func (s *SlotLoader) loadX509CASlotFromEntry(ctx context.Context, entry *X509CAE }, "", nil } -func (s *SlotLoader) tryLoadJWTKeySlotFromEntry(ctx context.Context, entry *JWTKeyEntry) (*JwtKeySlot, error) { +func (s *SlotLoader) tryLoadJWTKeySlotFromEntry(ctx context.Context, entry *journal.JWTKeyEntry) (*jwtKeySlot, error) { slot, badReason, err := s.loadJWTKeySlotFromEntry(ctx, entry) if err != nil { s.Log.WithError(err).WithFields(logrus.Fields{ @@ -401,7 +405,7 @@ func (s *SlotLoader) tryLoadJWTKeySlotFromEntry(ctx context.Context, entry *JWTK return slot, nil } -func (s *SlotLoader) loadJWTKeySlotFromEntry(ctx context.Context, entry *JWTKeyEntry) (*JwtKeySlot, string, error) { +func (s *SlotLoader) loadJWTKeySlotFromEntry(ctx context.Context, entry *journal.JWTKeyEntry) (*jwtKeySlot, string, error) { if entry.SlotId == "" { return nil, "no slot id", nil } @@ -423,7 +427,7 @@ func (s *SlotLoader) loadJWTKeySlotFromEntry(ctx context.Context, entry *JWTKeyE return nil, "public key does not match key manager key", nil } - return &JwtKeySlot{ + return &jwtKeySlot{ id: entry.SlotId, issuedAt: time.Unix(entry.IssuedAt, 0), jwtKey: &ca.JWTKey{ @@ -515,7 +519,7 @@ func keyActivationThreshold(issuedAt, notAfter time.Time) time.Time { return notAfter.Add(-threshold) } -type X509CASlot struct { +type x509CASlot struct { id string issuedAt time.Time x509CA *ca.X509CA @@ -525,50 +529,50 @@ type X509CASlot struct { notAfter time.Time } -func newX509CASlot(id string) *X509CASlot { - return &X509CASlot{ +func newX509CASlot(id string) *x509CASlot { + return &x509CASlot{ id: id, } } -func (s *X509CASlot) KmKeyID() string { +func (s *x509CASlot) KmKeyID() string { return x509CAKmKeyID(s.id) } -func (s *X509CASlot) IsEmpty() bool { +func (s *x509CASlot) IsEmpty() bool { return s.x509CA == nil || s.status == journal.Status_OLD } -func (s *X509CASlot) Reset() { +func (s *x509CASlot) Reset() { s.x509CA = nil s.status = journal.Status_OLD } -func (s *X509CASlot) ShouldPrepareNext(now time.Time) bool { +func (s *x509CASlot) ShouldPrepareNext(now time.Time) bool { return s.x509CA != nil && now.After(preparationThreshold(s.issuedAt, s.x509CA.Certificate.NotAfter)) } -func (s *X509CASlot) ShouldActivateNext(now time.Time) bool { +func (s *x509CASlot) ShouldActivateNext(now time.Time) bool { return s.x509CA != nil && now.After(keyActivationThreshold(s.issuedAt, s.x509CA.Certificate.NotAfter)) } -func (s *X509CASlot) Status() journal.Status { +func (s *x509CASlot) Status() journal.Status { return s.status } -func (s *X509CASlot) AuthorityID() string { +func (s *x509CASlot) AuthorityID() string { return s.authorityID } -func (s *X509CASlot) PublicKey() crypto.PublicKey { +func (s *x509CASlot) PublicKey() crypto.PublicKey { return s.publicKey } -func (s *X509CASlot) NotAfter() time.Time { +func (s *x509CASlot) NotAfter() time.Time { return s.notAfter } -type JwtKeySlot struct { +type jwtKeySlot struct { id string issuedAt time.Time jwtKey *ca.JWTKey @@ -577,48 +581,48 @@ type JwtKeySlot struct { notAfter time.Time } -func newJWTKeySlot(id string) *JwtKeySlot { - return &JwtKeySlot{ +func newJWTKeySlot(id string) *jwtKeySlot { + return &jwtKeySlot{ id: id, } } -func (s *JwtKeySlot) KmKeyID() string { +func (s *jwtKeySlot) KmKeyID() string { return jwtKeyKmKeyID(s.id) } -func (s *JwtKeySlot) Status() journal.Status { +func (s *jwtKeySlot) Status() journal.Status { return s.status } -func (s *JwtKeySlot) AuthorityID() string { +func (s *jwtKeySlot) AuthorityID() string { return s.authorityID } -func (s *JwtKeySlot) PublicKey() crypto.PublicKey { +func (s *jwtKeySlot) PublicKey() crypto.PublicKey { if s.jwtKey == nil { return nil } return s.jwtKey.Signer.Public() } -func (s *JwtKeySlot) IsEmpty() bool { +func (s *jwtKeySlot) IsEmpty() bool { return s.jwtKey == nil || s.status == journal.Status_OLD } -func (s *JwtKeySlot) Reset() { +func (s *jwtKeySlot) Reset() { s.jwtKey = nil s.status = journal.Status_OLD } -func (s *JwtKeySlot) ShouldPrepareNext(now time.Time) bool { +func (s *jwtKeySlot) ShouldPrepareNext(now time.Time) bool { return s.jwtKey == nil || now.After(preparationThreshold(s.issuedAt, s.jwtKey.NotAfter)) } -func (s *JwtKeySlot) ShouldActivateNext(now time.Time) bool { +func (s *jwtKeySlot) ShouldActivateNext(now time.Time) bool { return s.jwtKey == nil || now.After(keyActivationThreshold(s.issuedAt, s.jwtKey.NotAfter)) } -func (s *JwtKeySlot) NotAfter() time.Time { +func (s *jwtKeySlot) NotAfter() time.Time { return s.notAfter } diff --git a/pkg/server/ca/manager/slot_test.go b/pkg/server/ca/manager/slot_test.go index 1c273792a2..0a77dcd833 100644 --- a/pkg/server/ca/manager/slot_test.go +++ b/pkg/server/ca/manager/slot_test.go @@ -30,7 +30,7 @@ func TestX509CASlotShouldPrepareNext(t *testing.T) { clock := clock.NewMock() now := clock.Now() - slot := &X509CASlot{ + slot := &x509CASlot{ id: "A", issuedAt: clock.Now(), x509CA: nil, @@ -60,7 +60,7 @@ func TestX509CASlotShouldActivateNext(t *testing.T) { clock := clock.NewMock() now := clock.Now() - slot := &X509CASlot{ + slot := &x509CASlot{ id: "A", issuedAt: now, x509CA: nil, @@ -90,7 +90,7 @@ func TestJWTKeySlotShouldPrepareNext(t *testing.T) { clock := clock.NewMock() now := clock.Now() - slot := &JwtKeySlot{ + slot := &jwtKeySlot{ id: "A", issuedAt: now, jwtKey: nil, @@ -116,7 +116,7 @@ func TestJWTKeySlotShouldPrepareNext(t *testing.T) { func TestJWTKeySlotShouldActivateNext(t *testing.T) { now := time.Now() - slot := &JwtKeySlot{ + slot := &jwtKeySlot{ id: "A", issuedAt: now, jwtKey: nil, @@ -199,7 +199,7 @@ func TestJournalLoad(t *testing.T) { for _, tt := range []struct { name string - entries *JournalEntries + entries *journal.Entries expectSlots map[SlotPosition]Slot expectError string expectLogs []spiretest.LogEntry @@ -208,10 +208,10 @@ func TestJournalLoad(t *testing.T) { name: "Journal has no entries", entries: &journal.Entries{}, expectSlots: map[SlotPosition]Slot{ - CurrentX509CASlot: &X509CASlot{id: "A"}, - NextX509CASlot: &X509CASlot{id: "B"}, - CurrentJWTKeySlot: &JwtKeySlot{id: "A"}, - NextJWTKeySlot: &JwtKeySlot{id: "B"}, + CurrentX509CASlot: &x509CASlot{id: "A"}, + NextX509CASlot: &x509CASlot{id: "B"}, + CurrentJWTKeySlot: &jwtKeySlot{id: "A"}, + NextJWTKeySlot: &jwtKeySlot{id: "B"}, }, expectLogs: []spiretest.LogEntry{ { @@ -226,7 +226,7 @@ func TestJournalLoad(t *testing.T) { }, { name: "stored file has a single entry", - entries: &JournalEntries{ + entries: &journal.Entries{ X509CAs: []*journal.X509CAEntry{ { SlotId: "B", @@ -245,7 +245,7 @@ func TestJournalLoad(t *testing.T) { }, }, expectSlots: map[SlotPosition]Slot{ - CurrentX509CASlot: &X509CASlot{ + CurrentX509CASlot: &x509CASlot{ id: "B", issuedAt: secondIssuedAt, status: journal.Status_UNKNOWN, @@ -257,8 +257,8 @@ func TestJournalLoad(t *testing.T) { publicKey: x509KeyB.Public(), notAfter: x509RootB.NotAfter, }, - NextX509CASlot: &X509CASlot{id: "A"}, - CurrentJWTKeySlot: &JwtKeySlot{ + NextX509CASlot: &x509CASlot{id: "A"}, + CurrentJWTKeySlot: &jwtKeySlot{ id: "B", issuedAt: secondIssuedAt, status: journal.Status_UNKNOWN, @@ -270,7 +270,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "", notAfter: notAfter, }, - NextJWTKeySlot: &JwtKeySlot{id: "A"}, + NextJWTKeySlot: &jwtKeySlot{id: "A"}, }, expectLogs: []spiretest.LogEntry{ { @@ -285,7 +285,7 @@ func TestJournalLoad(t *testing.T) { }, { name: "Stored entries has unknown status", - entries: &JournalEntries{ + entries: &journal.Entries{ X509CAs: []*journal.X509CAEntry{ { SlotId: "A", @@ -334,7 +334,7 @@ func TestJournalLoad(t *testing.T) { }, }, expectSlots: map[SlotPosition]Slot{ - CurrentX509CASlot: &X509CASlot{ + CurrentX509CASlot: &x509CASlot{ id: "B", issuedAt: secondIssuedAt, status: journal.Status_UNKNOWN, @@ -346,7 +346,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "2", notAfter: x509RootB.NotAfter, }, - NextX509CASlot: &X509CASlot{ + NextX509CASlot: &x509CASlot{ id: "A", issuedAt: thirdIssuedAt, status: journal.Status_UNKNOWN, @@ -358,7 +358,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "1", notAfter: x509RootA.NotAfter, }, - CurrentJWTKeySlot: &JwtKeySlot{ + CurrentJWTKeySlot: &jwtKeySlot{ id: "B", issuedAt: secondIssuedAt, status: journal.Status_UNKNOWN, @@ -370,7 +370,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "b", notAfter: notAfter, }, - NextJWTKeySlot: &JwtKeySlot{ + NextJWTKeySlot: &jwtKeySlot{ id: "A", issuedAt: thirdIssuedAt, status: journal.Status_UNKNOWN, @@ -396,7 +396,7 @@ func TestJournalLoad(t *testing.T) { }, { name: "Stored entry has a single Prepared entry", - entries: &JournalEntries{ + entries: &journal.Entries{ X509CAs: []*journal.X509CAEntry{ { SlotId: "A", @@ -419,7 +419,7 @@ func TestJournalLoad(t *testing.T) { }, }, expectSlots: map[SlotPosition]Slot{ - CurrentX509CASlot: &X509CASlot{ + CurrentX509CASlot: &x509CASlot{ id: "A", issuedAt: thirdIssuedAt, status: journal.Status_PREPARED, @@ -431,10 +431,10 @@ func TestJournalLoad(t *testing.T) { authorityID: "1", notAfter: x509RootA.NotAfter, }, - NextX509CASlot: &X509CASlot{ + NextX509CASlot: &x509CASlot{ id: "B", }, - CurrentJWTKeySlot: &JwtKeySlot{ + CurrentJWTKeySlot: &jwtKeySlot{ id: "A", issuedAt: thirdIssuedAt, status: journal.Status_PREPARED, @@ -446,7 +446,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "a", notAfter: notAfter, }, - NextJWTKeySlot: &JwtKeySlot{ + NextJWTKeySlot: &jwtKeySlot{ id: "B", }, }, @@ -463,7 +463,7 @@ func TestJournalLoad(t *testing.T) { }, { name: "Stored entries has old and active", - entries: &JournalEntries{ + entries: &journal.Entries{ X509CAs: []*journal.X509CAEntry{ { SlotId: "A", @@ -518,7 +518,7 @@ func TestJournalLoad(t *testing.T) { }, }, expectSlots: map[SlotPosition]Slot{ - CurrentX509CASlot: &X509CASlot{ + CurrentX509CASlot: &x509CASlot{ id: "A", issuedAt: thirdIssuedAt, status: journal.Status_ACTIVE, @@ -530,7 +530,7 @@ func TestJournalLoad(t *testing.T) { publicKey: x509KeyA.Public(), notAfter: x509RootA.NotAfter, }, - NextX509CASlot: &X509CASlot{ + NextX509CASlot: &x509CASlot{ id: "B", issuedAt: secondIssuedAt, status: journal.Status_OLD, @@ -542,7 +542,7 @@ func TestJournalLoad(t *testing.T) { publicKey: x509KeyB.Public(), notAfter: x509RootB.NotAfter, }, - CurrentJWTKeySlot: &JwtKeySlot{ + CurrentJWTKeySlot: &jwtKeySlot{ id: "A", issuedAt: thirdIssuedAt, status: journal.Status_ACTIVE, @@ -554,7 +554,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "a", notAfter: notAfter, }, - NextJWTKeySlot: &JwtKeySlot{ + NextJWTKeySlot: &jwtKeySlot{ id: "B", issuedAt: secondIssuedAt, status: journal.Status_OLD, @@ -580,7 +580,7 @@ func TestJournalLoad(t *testing.T) { }, { name: "There are another entries before Active entry", - entries: &JournalEntries{ + entries: &journal.Entries{ X509CAs: []*journal.X509CAEntry{ // This can happens when force rotation is executed { @@ -637,7 +637,7 @@ func TestJournalLoad(t *testing.T) { }, }, expectSlots: map[SlotPosition]Slot{ - CurrentX509CASlot: &X509CASlot{ + CurrentX509CASlot: &x509CASlot{ id: "A", issuedAt: firstIssuedAt, status: journal.Status_ACTIVE, @@ -649,7 +649,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "3", notAfter: x509RootA.NotAfter, }, - NextX509CASlot: &X509CASlot{ + NextX509CASlot: &x509CASlot{ id: "B", issuedAt: thirdIssuedAt, status: journal.Status_PREPARED, @@ -661,7 +661,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "1", notAfter: x509RootB.NotAfter, }, - CurrentJWTKeySlot: &JwtKeySlot{ + CurrentJWTKeySlot: &jwtKeySlot{ id: "A", issuedAt: firstIssuedAt, status: journal.Status_ACTIVE, @@ -673,7 +673,7 @@ func TestJournalLoad(t *testing.T) { authorityID: "c", notAfter: notAfter, }, - NextJWTKeySlot: &JwtKeySlot{ + NextJWTKeySlot: &jwtKeySlot{ id: "B", issuedAt: thirdIssuedAt, status: journal.Status_PREPARED, @@ -774,7 +774,14 @@ func TestJournalLoad(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { loghook.Reset() - err = saveJournalEntries(journalFile, tt.entries) + journal := new(Journal) + journal.config = &journalConfig{ + cat: cat, + filePath: journalFile, + log: log, + } + journal.setEntries(tt.entries) + err = journal.save(ctx) require.NoError(t, err) loader := &SlotLoader{ @@ -784,8 +791,8 @@ func TestJournalLoad(t *testing.T) { Catalog: cat, } - loadedJournal, slots, err := loader.Load(context.Background()) - spiretest.AssertLogs(t, loghook.AllEntries(), tt.expectLogs) + loadedJournal, slots, err := loader.load(ctx) + spiretest.AssertLastLogs(t, loghook.AllEntries(), tt.expectLogs) if tt.expectError != "" { spiretest.AssertErrorPrefix(t, err, tt.expectError) assert.Nil(t, loadedJournal) diff --git a/pkg/server/ca/rotator/rotator.go b/pkg/server/ca/rotator/rotator.go index 0790b72c70..45424513fd 100644 --- a/pkg/server/ca/rotator/rotator.go +++ b/pkg/server/ca/rotator/rotator.go @@ -15,8 +15,9 @@ import ( ) const ( - rotateInterval = 10 * time.Second - pruneInterval = 6 * time.Hour + rotateInterval = 10 * time.Second + pruneBundleInterval = 6 * time.Hour + pruneCAJournalsInterval = 8 * time.Hour ) type CAManager interface { @@ -27,17 +28,18 @@ type CAManager interface { GetNextX509CASlot() manager.Slot PrepareX509CA(ctx context.Context) error - ActivateX509CA() - RotateX509CA() + ActivateX509CA(ctx context.Context) + RotateX509CA(ctx context.Context) GetCurrentJWTKeySlot() manager.Slot GetNextJWTKeySlot() manager.Slot PrepareJWTKey(ctx context.Context) error - ActivateJWTKey() - RotateJWTKey() + ActivateJWTKey(ctx context.Context) + RotateJWTKey(ctx context.Context) PruneBundle(ctx context.Context) error + PruneCAJournals(ctx context.Context) error } type Config struct { @@ -81,7 +83,10 @@ func (r *Rotator) Run(ctx context.Context) error { return r.rotateEvery(ctx, rotateInterval) }, func(ctx context.Context) error { - return r.pruneBundleEvery(ctx, pruneInterval) + return r.pruneBundleEvery(ctx, pruneBundleInterval) + }, + func(ctx context.Context) error { + return r.pruneCAJournalsEvery(ctx, pruneCAJournalsInterval) }, func(ctx context.Context) error { // notifyOnBundleUpdate does not fail but rather logs any errors @@ -139,7 +144,7 @@ func (r *Rotator) rotateJWTKey(ctx context.Context) error { if err := r.c.Manager.PrepareJWTKey(ctx); err != nil { return err } - r.c.Manager.ActivateJWTKey() + r.c.Manager.ActivateJWTKey(ctx) } // if there is no next keypair set and the current is within the @@ -151,7 +156,7 @@ func (r *Rotator) rotateJWTKey(ctx context.Context) error { } if currentJWTKey.ShouldActivateNext(now) { - r.c.Manager.RotateJWTKey() + r.c.Manager.RotateJWTKey(ctx) } return nil @@ -166,7 +171,7 @@ func (r *Rotator) rotateX509CA(ctx context.Context) error { if err := r.c.Manager.PrepareX509CA(ctx); err != nil { return err } - r.c.Manager.ActivateX509CA() + r.c.Manager.ActivateX509CA(ctx) } // if there is no next keypair set and the current is within the @@ -178,7 +183,7 @@ func (r *Rotator) rotateX509CA(ctx context.Context) error { } if currentX509CA.ShouldActivateNext(now) { - r.c.Manager.RotateX509CA() + r.c.Manager.RotateX509CA(ctx) } return nil @@ -200,6 +205,22 @@ func (r *Rotator) pruneBundleEvery(ctx context.Context, interval time.Duration) } } +func (r *Rotator) pruneCAJournalsEvery(ctx context.Context, interval time.Duration) error { + ticker := r.c.Clock.Ticker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := r.c.Manager.PruneCAJournals(ctx); err != nil { + r.c.Log.WithError(err).Error("Could not prune CA journals") + } + case <-ctx.Done(): + return nil + } + } +} + func (r *Rotator) failedRotationResult() uint64 { return atomic.LoadUint64(&r.failedRotationNum) } diff --git a/pkg/server/ca/rotator/rotator_test.go b/pkg/server/ca/rotator/rotator_test.go index 61df55c182..b8fb1bb6d8 100644 --- a/pkg/server/ca/rotator/rotator_test.go +++ b/pkg/server/ca/rotator/rotator_test.go @@ -185,12 +185,6 @@ func TestRunJWTKeyRotation(t *testing.T) { defer cancel() test := setupTest() - now := test.clock.Now() - test.fakeCAManager.currentJWTKeySlot = createSlot("jwt-a", now, true) - test.fakeCAManager.currentX509CASlot = createSlot("x509-a", now, true) - test.fakeCAManager.nextJWTKeySlot = createSlot("jwt-b", now, false) - test.fakeCAManager.nextX509CASlot = createSlot("x509-b", now, false) - go func() { err := test.rotator.Run(ctx) assert.NoError(t, err) @@ -243,12 +237,6 @@ func TestRunX509CARotation(t *testing.T) { defer cancel() test := setupTest() - now := test.clock.Now() - test.fakeCAManager.currentJWTKeySlot = createSlot("jwt-a", now, true) - test.fakeCAManager.currentX509CASlot = createSlot("x509-a", now, true) - test.fakeCAManager.nextJWTKeySlot = createSlot("jwt-b", now, false) - test.fakeCAManager.nextX509CASlot = createSlot("x509-b", now, false) - go func() { err := test.rotator.Run(ctx) assert.NoError(t, err) @@ -296,24 +284,18 @@ func TestRunX509CARotation(t *testing.T) { require.True(t, test.fakeCAManager.nextX509CASlot.IsEmpty()) } -func TestPrune(t *testing.T) { +func TestPruneBundle(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() test := setupTest() - now := test.clock.Now() - test.fakeCAManager.currentJWTKeySlot = createSlot("jwt-a", now, true) - test.fakeCAManager.currentX509CASlot = createSlot("x509-a", now, true) - test.fakeCAManager.nextJWTKeySlot = createSlot("jwt-b", now, false) - test.fakeCAManager.nextX509CASlot = createSlot("x509-b", now, false) - go func() { err := test.rotator.Run(ctx) assert.NoError(t, err) }() test.clock.Add(time.Minute + time.Second) - require.False(t, test.fakeCAManager.pruneWasCalled) + require.False(t, test.fakeCAManager.pruneBundleWasCalled) currentJWTKey := test.fakeCAManager.GetCurrentJWTKeySlot() require.Equal(t, "jwt-a", currentJWTKey.KmKeyID()) @@ -331,11 +313,31 @@ func TestPrune(t *testing.T) { require.Equal(t, "x509-b", nextX509CA.KmKeyID()) require.True(t, nextX509CA.IsEmpty()) - // Prune was called successfully - test.clock.Add(pruneInterval) - test.fakeCAManager.waitPruneCalled(ctx, t) + // Prune bundle was called successfully + test.clock.Add(pruneBundleInterval) + test.fakeCAManager.waitPruneBundleCalled(ctx, t) + + require.True(t, test.fakeCAManager.pruneBundleWasCalled) +} + +func TestPruneCAJournals(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + test := setupTest() + + go func() { + err := test.rotator.Run(ctx) + assert.NoError(t, err) + }() + + test.clock.Add(time.Minute + time.Second) + require.False(t, test.fakeCAManager.pruneCAJournalsWasCalled) + + // Prune CA journals was called successfully + test.clock.Add(pruneCAJournalsInterval) + test.fakeCAManager.waitPruneCAJournalsCalled(ctx, t) - require.True(t, test.fakeCAManager.pruneWasCalled) + require.True(t, test.fakeCAManager.pruneCAJournalsWasCalled) } type rotationTest struct { @@ -353,12 +355,19 @@ func setupTest() *rotationTest { fManager := &fakeCAManager{ clk: clock, - x509CACh: make(chan struct{}, 1), - jwtKeyCh: make(chan struct{}, 1), - pruneCh: make(chan struct{}, 1), + x509CACh: make(chan struct{}, 1), + jwtKeyCh: make(chan struct{}, 1), + pruneBundleCh: make(chan struct{}, 1), + pruneCAJournalsCh: make(chan struct{}, 1), } fakeHealthChecker := fakehealthchecker.New() + now := clock.Now() + fManager.currentJWTKeySlot = createSlot("jwt-a", now, true) + fManager.currentX509CASlot = createSlot("x509-a", now, true) + fManager.nextJWTKeySlot = createSlot("jwt-b", now, false) + fManager.nextX509CASlot = createSlot("x509-b", now, false) + rotator := NewRotator(Config{ Manager: fManager, Log: log, @@ -391,8 +400,10 @@ type fakeCAManager struct { x509CACh chan struct{} jwtKeyCh chan struct{} - pruneWasCalled bool - pruneCh chan struct{} + pruneBundleWasCalled bool + pruneBundleCh chan struct{} + pruneCAJournalsCh chan struct{} + pruneCAJournalsWasCalled bool } func (f *fakeCAManager) NotifyBundleLoaded(context.Context) error { @@ -434,13 +445,13 @@ func (f *fakeCAManager) PrepareX509CA(context.Context) error { return nil } -func (f *fakeCAManager) ActivateX509CA() { +func (f *fakeCAManager) ActivateX509CA(context.Context) { f.cleanX509CACh() f.currentX509CASlot.isActive = true f.x509CACh <- struct{}{} } -func (f *fakeCAManager) RotateX509CA() { +func (f *fakeCAManager) RotateX509CA(context.Context) { f.cleanX509CACh() currentID := f.currentX509CASlot.keyID @@ -478,13 +489,13 @@ func (f *fakeCAManager) PrepareJWTKey(context.Context) error { return nil } -func (f *fakeCAManager) ActivateJWTKey() { +func (f *fakeCAManager) ActivateJWTKey(context.Context) { f.cleanJWTKeyCh() f.currentJWTKeySlot.isActive = true f.jwtKeyCh <- struct{}{} } -func (f *fakeCAManager) RotateJWTKey() { +func (f *fakeCAManager) RotateJWTKey(context.Context) { f.cleanJWTKeyCh() currentID := f.currentJWTKeySlot.keyID @@ -497,9 +508,18 @@ func (f *fakeCAManager) RotateJWTKey() { func (f *fakeCAManager) PruneBundle(context.Context) error { defer func() { - f.pruneCh <- struct{}{} + f.pruneBundleCh <- struct{}{} }() - f.pruneWasCalled = true + f.pruneBundleWasCalled = true + + return nil +} + +func (f *fakeCAManager) PruneCAJournals(context.Context) error { + defer func() { + f.pruneCAJournalsCh <- struct{}{} + }() + f.pruneCAJournalsWasCalled = true return nil } @@ -534,11 +554,19 @@ func (f *fakeCAManager) waitJWTKeyUpdate(ctx context.Context, t *testing.T) { } } -func (f *fakeCAManager) waitPruneCalled(ctx context.Context, t *testing.T) { +func (f *fakeCAManager) waitPruneBundleCalled(ctx context.Context, t *testing.T) { + select { + case <-ctx.Done(): + assert.Fail(t, "context finished") + case <-f.pruneBundleCh: + } +} + +func (f *fakeCAManager) waitPruneCAJournalsCalled(ctx context.Context, t *testing.T) { select { case <-ctx.Done(): assert.Fail(t, "context finished") - case <-f.pruneCh: + case <-f.pruneCAJournalsCh: } } diff --git a/pkg/server/datastore/datastore.go b/pkg/server/datastore/datastore.go index 8042952aed..db96a1976a 100644 --- a/pkg/server/datastore/datastore.go +++ b/pkg/server/datastore/datastore.go @@ -75,6 +75,12 @@ type DataStore interface { ListFederationRelationships(context.Context, *ListFederationRelationshipsRequest) (*ListFederationRelationshipsResponse, error) DeleteFederationRelationship(context.Context, spiffeid.TrustDomain) error UpdateFederationRelationship(context.Context, *FederationRelationship, *types.FederationRelationshipMask) (*FederationRelationship, error) + + // CA Journals + SetCAJournal(ctx context.Context, caJournal *CAJournal) (*CAJournal, error) + FetchCAJournal(ctx context.Context, activeX509AuthorityID string) (*CAJournal, error) + PruneCAJournals(ctx context.Context, allCAsExpireBefore int64) error + ListCAJournalsForTesting(ctx context.Context) ([]*CAJournal, error) } // DataConsistency indicates the required data consistency for a read operation. @@ -202,6 +208,12 @@ type ListRegistrationEntriesRequest struct { ByHint string } +type CAJournal struct { + ID uint + Data []byte + ActiveX509AuthorityID string +} + type ListRegistrationEntriesResponse struct { Entries []*common.RegistrationEntry Pagination *Pagination diff --git a/pkg/server/datastore/sqlstore/sqlstore.go b/pkg/server/datastore/sqlstore/sqlstore.go index 03ecec00a3..a6a4719985 100644 --- a/pkg/server/datastore/sqlstore/sqlstore.go +++ b/pkg/server/datastore/sqlstore/sqlstore.go @@ -27,6 +27,7 @@ import ( "github.com/spiffe/spire/pkg/common/protoutil" "github.com/spiffe/spire/pkg/common/telemetry" "github.com/spiffe/spire/pkg/server/datastore" + "github.com/spiffe/spire/proto/private/server/journal" "github.com/spiffe/spire/proto/spire/common" "github.com/zeebo/errs" "google.golang.org/grpc/codes" @@ -658,6 +659,100 @@ func (ds *Plugin) SetUseServerTimestamps(useServerTimestamps bool) { ds.useServerTimestamps = useServerTimestamps } +// FetchCAJournal fetches the CA journal that has the given active X509 +// authority domain. If the CA journal is not found, nil is returned. +func (ds *Plugin) FetchCAJournal(ctx context.Context, activeX509AuthorityID string) (caJournal *datastore.CAJournal, err error) { + if activeX509AuthorityID == "" { + return nil, status.Error(codes.InvalidArgument, "active X509 authority ID is required") + } + + if err = ds.withReadTx(ctx, func(tx *gorm.DB) (err error) { + caJournal, err = fetchCAJournal(tx, activeX509AuthorityID) + return err + }); err != nil { + return nil, err + } + + return caJournal, nil +} + +// ListCAJournalsForTesting returns all the CA journal records, and is meant to +// be used in tests. +func (ds *Plugin) ListCAJournalsForTesting(ctx context.Context) (caJournals []*datastore.CAJournal, err error) { + if err = ds.withReadTx(ctx, func(tx *gorm.DB) (err error) { + caJournals, err = listCAJournalsForTesting(tx) + return err + }); err != nil { + return nil, err + } + return caJournals, nil +} + +// SetCAJournal sets the content for the specified CA journal. If the CA journal +// does not exist, it is created. +func (ds *Plugin) SetCAJournal(ctx context.Context, caJournal *datastore.CAJournal) (caj *datastore.CAJournal, err error) { + if err := validateCAJournal(caJournal); err != nil { + return nil, err + } + + if err = ds.withReadModifyWriteTx(ctx, func(tx *gorm.DB) (err error) { + if caJournal.ID == 0 { + caj, err = createCAJournal(tx, caJournal) + return err + } + + // The CA journal already exists, update it. + caj, err = updateCAJournal(tx, caJournal) + return err + }); err != nil { + return nil, err + } + return caj, nil +} + +// PruneCAJournals prunes the CA journals that have all of their authorities +// expired. +func (ds *Plugin) PruneCAJournals(ctx context.Context, allAuthoritiesExpireBefore int64) error { + return ds.withWriteTx(ctx, func(tx *gorm.DB) (err error) { + err = ds.pruneCAJournals(tx, allAuthoritiesExpireBefore) + return err + }) +} + +func (ds *Plugin) pruneCAJournals(tx *gorm.DB, allAuthoritiesExpireBefore int64) error { + var caJournals []CAJournal + if err := tx.Find(&caJournals).Error; err != nil { + return sqlError.Wrap(err) + } + +checkAuthorities: + for _, model := range caJournals { + entries := new(journal.Entries) + if err := proto.Unmarshal(model.Data, entries); err != nil { + return status.Errorf(codes.Internal, "unable to unmarshal entries from CA journal record: %v", err) + } + + for _, x509CA := range entries.X509CAs { + if x509CA.NotAfter > allAuthoritiesExpireBefore { + continue checkAuthorities + } + } + for _, jwtKey := range entries.JwtKeys { + if jwtKey.NotAfter > allAuthoritiesExpireBefore { + continue checkAuthorities + } + } + if err := deleteCAJournal(tx, model.ID); err != nil { + return status.Errorf(codes.Internal, "failed to delete CA journal: %v", err) + } + ds.log.WithFields(logrus.Fields{ + telemetry.CAJournalID: model.ID, + }).Info("Pruned stale CA journal record") + } + + return nil +} + // Configure parses HCL config payload into config struct, opens new DB based on the result, and // prunes all orphaned records func (ds *Plugin) Configure(_ context.Context, hclConfiguration string) error { @@ -4266,6 +4361,14 @@ func modelToJoinToken(model JoinToken) *datastore.JoinToken { } } +func modelToCAJournal(model CAJournal) *datastore.CAJournal { + return &datastore.CAJournal{ + ID: model.ID, + Data: model.Data, + ActiveX509AuthorityID: model.ActiveX509AuthorityID, + } +} + func makeFederatesWith(tx *gorm.DB, ids []string) ([]*Bundle, error) { var bundles []*Bundle if err := tx.Where("trust_domain in (?)", ids).Find(&bundles).Error; err != nil { @@ -4398,3 +4501,77 @@ func lookupSimilarEntry(ctx context.Context, db *sqlDB, tx *gorm.DB, entry *comm func roundedInSecondsUnix(t time.Time) int64 { return t.Round(time.Second).Unix() } + +func createCAJournal(tx *gorm.DB, caJournal *datastore.CAJournal) (*datastore.CAJournal, error) { + model := CAJournal{ + Data: caJournal.Data, + ActiveX509AuthorityID: caJournal.ActiveX509AuthorityID, + } + + if err := tx.Create(&model).Error; err != nil { + return nil, sqlError.Wrap(err) + } + + return modelToCAJournal(model), nil +} + +func fetchCAJournal(tx *gorm.DB, activeX509AuthorityID string) (*datastore.CAJournal, error) { + var model CAJournal + err := tx.Find(&model, "active_x509_authority_id = ?", activeX509AuthorityID).Error + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return nil, nil + case err != nil: + return nil, sqlError.Wrap(err) + } + + return modelToCAJournal(model), nil +} + +func listCAJournalsForTesting(tx *gorm.DB) (caJournals []*datastore.CAJournal, err error) { + var caJournalsModel []CAJournal + if err := tx.Find(&caJournals).Error; err != nil { + return nil, sqlError.Wrap(err) + } + + for _, model := range caJournalsModel { + model := model // alias the loop variable since we pass it by reference below + caJournals = append(caJournals, modelToCAJournal(model)) + } + return caJournals, nil +} + +func updateCAJournal(tx *gorm.DB, caJournal *datastore.CAJournal) (*datastore.CAJournal, error) { + var model CAJournal + if err := tx.Find(&model, "id = ?", caJournal.ID).Error; err != nil { + return nil, sqlError.Wrap(err) + } + + model.ActiveX509AuthorityID = caJournal.ActiveX509AuthorityID + model.Data = caJournal.Data + + if err := tx.Save(&model).Error; err != nil { + return nil, sqlError.Wrap(err) + } + + return modelToCAJournal(model), nil +} + +func validateCAJournal(caJournal *datastore.CAJournal) error { + if caJournal == nil { + return status.Error(codes.InvalidArgument, "ca journal is required") + } + + return nil +} + +func deleteCAJournal(tx *gorm.DB, caJournalID uint) error { + model := new(CAJournal) + if err := tx.Find(model, "id = ?", caJournalID).Error; err != nil { + return sqlError.Wrap(err) + } + if err := tx.Delete(model).Error; err != nil { + return sqlError.Wrap(err) + } + return nil +} diff --git a/pkg/server/datastore/sqlstore/sqlstore_test.go b/pkg/server/datastore/sqlstore/sqlstore_test.go index e8dd8120ba..1b26d614dd 100644 --- a/pkg/server/datastore/sqlstore/sqlstore_test.go +++ b/pkg/server/datastore/sqlstore/sqlstore_test.go @@ -28,6 +28,7 @@ import ( "github.com/spiffe/spire/pkg/common/telemetry" "github.com/spiffe/spire/pkg/common/util" "github.com/spiffe/spire/pkg/server/datastore" + "github.com/spiffe/spire/proto/private/server/journal" "github.com/spiffe/spire/proto/spire/common" "github.com/spiffe/spire/test/clock" "github.com/spiffe/spire/test/spiretest" @@ -4876,6 +4877,145 @@ func (s *PluginSuite) TestBindVar() { s.Require().Equal("SELECT whatever FROM foo WHERE x = $1 AND y = $2", bound) } +func (s *PluginSuite) TestSetCAJournal() { + testCases := []struct { + name string + code codes.Code + msg string + caJournal *datastore.CAJournal + }{ + { + name: "creating a new CA journal succeeds", + caJournal: &datastore.CAJournal{ + Data: []byte("test data"), + ActiveX509AuthorityID: "x509-authority-id", + }, + }, + { + name: "nil CA journal", + code: codes.InvalidArgument, + msg: "ca journal is required", + }, + { + name: "try to update a non existing CA journal", + code: codes.NotFound, + msg: "datastore-sql: record not found", + caJournal: &datastore.CAJournal{ + ID: 999, + Data: []byte("test data"), + ActiveX509AuthorityID: "x509-authority-id", + }, + }, + } + + for _, tt := range testCases { + s.T().Run(tt.name, func(t *testing.T) { + caJournal, err := s.ds.SetCAJournal(ctx, tt.caJournal) + spiretest.RequireGRPCStatus(t, err, tt.code, tt.msg) + if tt.code != codes.OK { + require.Nil(t, caJournal) + return + } + + assertCAJournal(t, tt.caJournal, caJournal) + }) + } +} + +func (s *PluginSuite) TestFetchCAJournal() { + testCases := []struct { + name string + activeX509AuthorityID string + code codes.Code + msg string + caJournal *datastore.CAJournal + }{ + { + name: "fetching an existent CA journal", + activeX509AuthorityID: "x509-authority-id", + caJournal: func() *datastore.CAJournal { + caJournal, err := s.ds.SetCAJournal(ctx, &datastore.CAJournal{ + ActiveX509AuthorityID: "x509-authority-id", + Data: []byte("test data"), + }) + s.Require().NoError(err) + return caJournal + }(), + }, + { + name: "non-existent X509 authority ID returns nil", + activeX509AuthorityID: "non-existent-x509-authority-id", + }, + { + name: "fetching without specifying an active authority ID fails", + code: codes.InvalidArgument, + msg: "active X509 authority ID is required", + }, + } + + for _, tt := range testCases { + s.T().Run(tt.name, func(t *testing.T) { + caJournal, err := s.ds.FetchCAJournal(ctx, tt.activeX509AuthorityID) + spiretest.RequireGRPCStatus(t, err, tt.code, tt.msg) + if tt.code != codes.OK { + require.Nil(t, caJournal) + return + } + + assert.Equal(t, tt.caJournal, caJournal) + }) + } +} + +func (s *PluginSuite) TestPruneCAJournal() { + now := time.Now() + t := now.Add(time.Hour) + entries := &journal.Entries{ + X509CAs: []*journal.X509CAEntry{ + { + NotAfter: t.Add(-time.Hour * 6).Unix(), + }, + }, + JwtKeys: []*journal.JWTKeyEntry{ + { + NotAfter: t.Add(time.Hour * 6).Unix(), + }, + }, + } + + entriesBytes, err := proto.Marshal(entries) + s.Require().NoError(err) + + // Store CA journal in datastore + caJournal, err := s.ds.SetCAJournal(ctx, &datastore.CAJournal{ + ActiveX509AuthorityID: "x509-authority-1", + Data: entriesBytes, + }) + s.Require().NoError(err) + + // Run a PruneCAJournals operation specifying a time that is before the + // expiration of all the authorities. The CA journal should not be pruned. + s.Require().NoError(s.ds.PruneCAJournals(ctx, t.Add(-time.Hour*12).Unix())) + caj, err := s.ds.FetchCAJournal(ctx, "x509-authority-1") + s.Require().NoError(err) + s.Require().Equal(caJournal, caj) + + // Run a PruneCAJournals operation specifying a time that is before the + // expiration of one of the authorities, but not all the authorities. + // The CA journal should not be pruned. + s.Require().NoError(s.ds.PruneCAJournals(ctx, t.Unix())) + caj, err = s.ds.FetchCAJournal(ctx, "x509-authority-1") + s.Require().NoError(err) + s.Require().Equal(caJournal, caj) + + // Run a PruneCAJournals operation specifying a time that is after the + // expiration of all the authorities. The CA journal should be pruned. + s.Require().NoError(s.ds.PruneCAJournals(ctx, t.Add(time.Hour*12).Unix())) + caj, err = s.ds.FetchCAJournal(ctx, "x509-authority-1") + s.Require().NoError(err) + s.Require().Nil(caj) +} + func (s *PluginSuite) getTestDataFromJSONFile(filePath string, jsonValue any) { entriesJSON, err := os.ReadFile(filePath) s.Require().NoError(err) @@ -5185,3 +5325,12 @@ func assertFederationRelationship(t *testing.T, exp, actual *datastore.Federatio assert.Equal(t, exp.TrustDomain, actual.TrustDomain) spiretest.AssertProtoEqual(t, exp.TrustDomainBundle, actual.TrustDomainBundle) } + +func assertCAJournal(t *testing.T, exp, actual *datastore.CAJournal) { + if exp == nil { + assert.Nil(t, actual) + return + } + assert.Equal(t, exp.ActiveX509AuthorityID, actual.ActiveX509AuthorityID) + assert.Equal(t, exp.Data, actual.Data) +} diff --git a/proto/private/server/journal/journal.pb.go b/proto/private/server/journal/journal.pb.go index 38848e9522..36198c8736 100644 --- a/proto/private/server/journal/journal.pb.go +++ b/proto/private/server/journal/journal.pb.go @@ -94,6 +94,8 @@ type X509CAEntry struct { Status Status `protobuf:"varint,5,opt,name=status,proto3,enum=Status" json:"status,omitempty"` // The X.509 Subject Key Identifier (SKID) AuthorityId string `protobuf:"bytes,6,opt,name=authority_id,json=authorityId,proto3" json:"authority_id,omitempty"` + // When the CA expires (unix epoch in seconds) + NotAfter int64 `protobuf:"varint,7,opt,name=not_after,json=notAfter,proto3" json:"not_after,omitempty"` } func (x *X509CAEntry) Reset() { @@ -170,6 +172,13 @@ func (x *X509CAEntry) GetAuthorityId() string { return "" } +func (x *X509CAEntry) GetNotAfter() int64 { + if x != nil { + return x.NotAfter + } + return 0 +} + type JWTKeyEntry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -332,7 +341,7 @@ var File_private_server_journal_journal_proto protoreflect.FileDescriptor var file_private_server_journal_journal_proto_rawDesc = []byte{ 0x0a, 0x24, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x6a, 0x6f, 0x75, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6a, 0x6f, 0x75, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd0, 0x01, 0x0a, 0x0b, 0x58, 0x35, 0x30, 0x39, 0x43, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xed, 0x01, 0x0a, 0x0b, 0x58, 0x35, 0x30, 0x39, 0x43, 0x41, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6c, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, @@ -345,34 +354,36 @@ var file_private_server_journal_journal_proto_rawDesc = []byte{ 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x07, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x49, 0x64, 0x22, 0xd5, 0x01, 0x0a, 0x0b, 0x4a, 0x57, - 0x54, 0x4b, 0x65, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x6c, 0x6f, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6c, 0x6f, 0x74, - 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x41, 0x74, 0x12, - 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x12, 0x1d, - 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x07, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, - 0x0a, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x49, - 0x64, 0x22, 0x59, 0x0a, 0x07, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x07, - 0x78, 0x35, 0x30, 0x39, 0x43, 0x41, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, - 0x58, 0x35, 0x30, 0x39, 0x43, 0x41, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x78, 0x35, 0x30, - 0x39, 0x43, 0x41, 0x73, 0x12, 0x26, 0x0a, 0x07, 0x6a, 0x77, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x4a, 0x57, 0x54, 0x4b, 0x65, 0x79, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x07, 0x6a, 0x77, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x2a, 0x38, 0x0a, 0x06, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x52, 0x45, 0x50, 0x41, 0x52, 0x45, 0x44, 0x10, - 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x03, 0x12, 0x07, 0x0a, - 0x03, 0x4f, 0x4c, 0x44, 0x10, 0x04, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x2f, 0x73, 0x70, 0x69, 0x72, - 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x6a, 0x6f, 0x75, 0x72, 0x6e, 0x61, 0x6c, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, + 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6e, 0x6f, + 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x22, 0xd5, 0x01, 0x0a, 0x0b, 0x4a, 0x57, 0x54, 0x4b, 0x65, + 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6c, 0x6f, 0x74, 0x49, 0x64, 0x12, + 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x07, 0x2e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x49, 0x64, 0x22, 0x59, + 0x0a, 0x07, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x07, 0x78, 0x35, 0x30, + 0x39, 0x43, 0x41, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x58, 0x35, 0x30, + 0x39, 0x43, 0x41, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x78, 0x35, 0x30, 0x39, 0x43, 0x41, + 0x73, 0x12, 0x26, 0x0a, 0x07, 0x6a, 0x77, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x4a, 0x57, 0x54, 0x4b, 0x65, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x07, 0x6a, 0x77, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x2a, 0x38, 0x0a, 0x06, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, + 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x52, 0x45, 0x50, 0x41, 0x52, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0a, + 0x0a, 0x06, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x4c, + 0x44, 0x10, 0x04, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x2f, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x2f, 0x6a, 0x6f, 0x75, 0x72, 0x6e, 0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/proto/private/server/journal/journal.proto b/proto/private/server/journal/journal.proto index ee31cf283f..798339bcb2 100644 --- a/proto/private/server/journal/journal.proto +++ b/proto/private/server/journal/journal.proto @@ -19,6 +19,9 @@ message X509CAEntry { // The X.509 Subject Key Identifier (SKID) string authority_id = 6; + + // When the CA expires (unix epoch in seconds) + int64 not_after = 7; } message JWTKeyEntry { diff --git a/test/fakes/fakedatastore/fakedatastore.go b/test/fakes/fakedatastore/fakedatastore.go index bcb68be7c5..ee88729cee 100644 --- a/test/fakes/fakedatastore/fakedatastore.go +++ b/test/fakes/fakedatastore/fakedatastore.go @@ -383,6 +383,34 @@ func (s *DataStore) UpdateFederationRelationship(ctx context.Context, fr *datast return s.ds.UpdateFederationRelationship(ctx, fr, mask) } +func (s *DataStore) FetchCAJournal(ctx context.Context, activeX509AuthorityID string) (*datastore.CAJournal, error) { + if err := s.getNextError(); err != nil { + return nil, err + } + return s.ds.FetchCAJournal(ctx, activeX509AuthorityID) +} + +func (s *DataStore) ListCAJournalsForTesting(ctx context.Context) ([]*datastore.CAJournal, error) { + if err := s.getNextError(); err != nil { + return nil, err + } + return s.ds.ListCAJournalsForTesting(ctx) +} + +func (s *DataStore) SetCAJournal(ctx context.Context, caJournal *datastore.CAJournal) (*datastore.CAJournal, error) { + if err := s.getNextError(); err != nil { + return nil, err + } + return s.ds.SetCAJournal(ctx, caJournal) +} + +func (s *DataStore) PruneCAJournals(ctx context.Context, allCAsExpireBefore int64) error { + if err := s.getNextError(); err != nil { + return err + } + return s.ds.PruneCAJournals(ctx, allCAsExpireBefore) +} + func (s *DataStore) SetNextError(err error) { s.errs = []error{err} }