mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:01:32 +00:00
feat(M11c): crypto policy enforcement — CSR validation, MaxTTL caps, key metadata
Enforce certificate profile crypto constraints across all 5 issuance paths (renewal, agent CSR, EST, SCEP). ValidateCSRAgainstProfile() rejects CSRs with key algorithm/size that don't match profile rules. MaxTTL enforcement caps certificate validity per issuer connector (Local CA, Vault, step-ca enforce directly; ACME/DigiCert/Sectigo pass through). Key algorithm and size are now persisted in certificate_versions for audit compliance. 16 new tests (12 service-layer + 4 Local CA connector). Removes hardcoded version number from GUI sidebar. Documentation updated across architecture, features, connectors, and README. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+32
-2
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
)
|
||||
|
||||
// ESTService implements the EST (RFC 7030) enrollment protocol.
|
||||
@@ -20,6 +21,7 @@ type ESTService struct {
|
||||
auditService *AuditService
|
||||
logger *slog.Logger
|
||||
profileID string // optional: constrain enrollments to a specific profile
|
||||
profileRepo repository.CertificateProfileRepository
|
||||
}
|
||||
|
||||
// NewESTService creates a new ESTService for the given issuer connector.
|
||||
@@ -37,6 +39,11 @@ func (s *ESTService) SetProfileID(profileID string) {
|
||||
s.profileID = profileID
|
||||
}
|
||||
|
||||
// SetProfileRepo sets the profile repository for crypto policy enforcement during enrollment.
|
||||
func (s *ESTService) SetProfileRepo(repo repository.CertificateProfileRepository) {
|
||||
s.profileRepo = repo
|
||||
}
|
||||
|
||||
// GetCACerts returns the PEM-encoded CA certificate chain for this EST server.
|
||||
// RFC 7030 Section 4.1: /cacerts distributes the current CA certificates.
|
||||
func (s *ESTService) GetCACerts(ctx context.Context) (string, error) {
|
||||
@@ -109,15 +116,38 @@ func (s *ESTService) processEnrollment(ctx context.Context, csrPEM string, audit
|
||||
sans = append(sans, uri.String())
|
||||
}
|
||||
|
||||
// Validate CSR key algorithm/size against profile (crypto policy enforcement)
|
||||
var profile *domain.CertificateProfile
|
||||
var ekus []string
|
||||
if s.profileID != "" && s.profileRepo != nil {
|
||||
if p, profileErr := s.profileRepo.Get(ctx, s.profileID); profileErr == nil && p != nil {
|
||||
profile = p
|
||||
ekus = profile.AllowedEKUs
|
||||
}
|
||||
}
|
||||
if _, csrErr := ValidateCSRAgainstProfile(csrPEM, profile); csrErr != nil {
|
||||
s.logger.Error("EST enrollment rejected: crypto policy violation",
|
||||
"action", auditAction,
|
||||
"common_name", commonName,
|
||||
"error", csrErr)
|
||||
return nil, fmt.Errorf("EST enrollment rejected: %w", csrErr)
|
||||
}
|
||||
|
||||
s.logger.Info("EST enrollment request",
|
||||
"action", auditAction,
|
||||
"common_name", commonName,
|
||||
"sans", strings.Join(sans, ","),
|
||||
"issuer", s.issuerID)
|
||||
|
||||
// Resolve MaxTTL from profile
|
||||
var maxTTLSeconds int
|
||||
if profile != nil {
|
||||
maxTTLSeconds = profile.MaxTTLSeconds
|
||||
}
|
||||
|
||||
// Issue the certificate via the configured issuer connector
|
||||
// EST enrollments use default EKUs (nil = serverAuth + clientAuth fallback in connector)
|
||||
result, err := s.issuer.IssueCertificate(ctx, commonName, sans, csrPEM, nil)
|
||||
// EST enrollments use profile EKUs if available, otherwise default (serverAuth + clientAuth fallback)
|
||||
result, err := s.issuer.IssueCertificate(ctx, commonName, sans, csrPEM, ekus, maxTTLSeconds)
|
||||
if err != nil {
|
||||
s.logger.Error("EST enrollment failed",
|
||||
"action", auditAction,
|
||||
|
||||
Reference in New Issue
Block a user