package security import ( "context" "crypto/x509" "errors" "fmt" "time" "git.cloudinit.dev/coreci/orca/internal/store" ) // RotationWindow is the lead-time before expiry at which we start warning // the operator. REQ-034 says 30 days. const RotationWindow = 30 * 24 * time.Hour // RotationAlarm checks cert's remaining validity. Returns nil if the cert // has more than RotationWindow of life left. If remaining <= RotationWindow, // returns a non-nil error wrapping the days-remaining message so callers // can log it. Callers MUST treat a non-nil result as a warning, not a fatal // error — the cert is still usable; we want to alert the operator ahead // of time. func RotationAlarm(cert *x509.Certificate) error { if cert == nil { return errors.New("RotationAlarm: nil cert") } now := time.Now() remaining := cert.NotAfter.Sub(now) if remaining > RotationWindow { return nil } days := int(remaining.Hours() / 24) if days < 0 { days = 0 } return fmt.Errorf("cert rotates in %d days (NotAfter=%s) — renew soon (REQ-034)", days, cert.NotAfter.UTC().Format(time.RFC3339)) } // RotationAlarmAt is identical to RotationAlarm but takes an explicit "now" // for deterministic testing. func RotationAlarmAt(cert *x509.Certificate, now time.Time) error { if cert == nil { return errors.New("RotationAlarmAt: nil cert") } remaining := cert.NotAfter.Sub(now) if remaining > RotationWindow { return nil } days := int(remaining.Hours() / 24) if days < 0 { days = 0 } return fmt.Errorf("cert rotates in %d days (NotAfter=%s) — renew soon (REQ-034)", days, cert.NotAfter.UTC().Format(time.RFC3339)) } // PruneOldCerts deletes all certs for (nodeID, kind) beyond the most recent // `keep` rows, ordered by created_at DESC. Per REQ-025, the rotation // history is bounded at 10 generations per cert kind. Returns the number // of rows deleted. // // `keep` is a positive integer; values <= 0 are treated as 10 (the // documented max). func PruneOldCerts(ctx context.Context, repo *store.CertRepo, nodeID, kind string, keep int) (int64, error) { if repo == nil { return 0, errors.New("PruneOldCerts: nil repo") } if keep <= 0 { keep = 10 } return repo.PruneOlderThan(ctx, nodeID, kind, keep) }