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:
Shankar
2026-04-15 21:05:14 -04:00
parent e8ddd3c327
commit ff223e2586
22 changed files with 779 additions and 70 deletions
+22 -3
View File
@@ -165,14 +165,29 @@ func (s *AgentService) SubmitCSR(ctx context.Context, agentID string, certID str
// Fallback: direct issuer signing (no AwaitingCSR job — ad-hoc CSR submission)
connector, ok := s.issuerRegistry.Get(cert.IssuerID)
if ok {
// Resolve EKUs from the certificate profile if available
// Resolve profile for EKU resolution and crypto policy enforcement
var ekus []string
var profile *domain.CertificateProfile
if cert.CertificateProfileID != "" && s.profileRepo != nil {
if profile, profileErr := s.profileRepo.Get(ctx, cert.CertificateProfileID); profileErr == nil && profile != nil {
if p, profileErr := s.profileRepo.Get(ctx, cert.CertificateProfileID); profileErr == nil && p != nil {
profile = p
ekus = profile.AllowedEKUs
}
}
result, err := connector.IssueCertificate(ctx, cert.CommonName, cert.SANs, string(csrPEM), ekus)
// Validate CSR key algorithm/size against profile (crypto policy enforcement)
csrInfo, csrErr := ValidateCSRAgainstProfile(string(csrPEM), profile)
if csrErr != nil {
return fmt.Errorf("CSR validation failed: %w", csrErr)
}
// Resolve MaxTTL from profile
var maxTTLSeconds int
if profile != nil {
maxTTLSeconds = profile.MaxTTLSeconds
}
result, err := connector.IssueCertificate(ctx, cert.CommonName, cert.SANs, string(csrPEM), ekus, maxTTLSeconds)
if err != nil {
return fmt.Errorf("issuer signing failed: %w", err)
}
@@ -188,6 +203,10 @@ func (s *AgentService) SubmitCSR(ctx context.Context, agentID string, certID str
CSRPEM: string(csrPEM),
CreatedAt: time.Now(),
}
if csrInfo != nil {
version.KeyAlgorithm = csrInfo.KeyAlgorithm
version.KeySize = csrInfo.KeySize
}
if err := s.certRepo.CreateVersion(ctx, version); err != nil {
return fmt.Errorf("failed to store certificate version: %w", err)