mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 21:11:30 +00:00
a579a84c7f
Add certificate profiles as named enrollment templates that control allowed key algorithms, max TTL, permitted EKUs, required SAN patterns, and optional SPIFFE URI SANs. CSR submissions are validated against profile rules at signing time (key type + minimum size). Short-lived certs (TTL < 1 hour) auto-expire via a new scheduler loop — expiry acts as revocation, no CRL/OCSP needed. New files: - Migration 000003: certificate_profiles table, FK columns on managed_certificates/renewal_policies, key metadata on certificate_versions - domain/profile.go: CertificateProfile + KeyAlgorithmRule structs - repository/postgres/profile.go: full CRUD with JSONB marshaling - service/profile.go: ProfileService with validation + audit logging - service/crypto_validation.go: CSR-against-profile validation (RSA/ECDSA/Ed25519) - handler/profiles.go: 5 HTTP endpoints under /api/v1/profiles - web/src/pages/ProfilesPage.tsx: profiles management page Modified: - renewal.go: CSR validation in CompleteAgentCSRRenewal, ExpireShortLivedCertificates - scheduler.go: 30s short-lived expiry check loop - certificate.go (repo): nullable profile FK, key metadata on versions - main.go: profile repo/service/handler wiring, 8-param NewRenewalService - router.go: 12-param RegisterHandlers with profile routes - seed_demo.sql: 4 demo profiles (standard, mtls, short-lived, high-security) - Frontend: types, API client, routing, sidebar nav Tests: 40 new tests across handler (15), service (13), crypto validation (12) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
86 lines
2.5 KiB
Go
86 lines
2.5 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
)
|
|
|
|
// CSRValidationResult contains metadata extracted from a validated CSR.
|
|
type CSRValidationResult struct {
|
|
KeyAlgorithm string
|
|
KeySize int
|
|
}
|
|
|
|
// ValidateCSRAgainstProfile parses a CSR PEM and validates that its key algorithm
|
|
// and size comply with the profile's allowed_key_algorithms rules.
|
|
// Returns extracted key metadata on success for storage in certificate_versions.
|
|
func ValidateCSRAgainstProfile(csrPEM string, profile *domain.CertificateProfile) (*CSRValidationResult, error) {
|
|
if profile == nil {
|
|
// No profile assigned — skip validation, extract metadata only
|
|
return extractCSRKeyInfo(csrPEM)
|
|
}
|
|
|
|
result, err := extractCSRKeyInfo(csrPEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check that the CSR's key algorithm + size matches at least one allowed rule
|
|
if len(profile.AllowedKeyAlgorithms) == 0 {
|
|
// No restrictions defined — allow anything
|
|
return result, nil
|
|
}
|
|
|
|
for _, rule := range profile.AllowedKeyAlgorithms {
|
|
if rule.Algorithm == result.KeyAlgorithm && result.KeySize >= rule.MinSize {
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("CSR key (%s %d-bit) does not match any allowed algorithm in profile %q: %v",
|
|
result.KeyAlgorithm, result.KeySize, profile.Name, profile.AllowedKeyAlgorithms)
|
|
}
|
|
|
|
// extractCSRKeyInfo parses a CSR PEM and extracts the key algorithm and size.
|
|
func extractCSRKeyInfo(csrPEM string) (*CSRValidationResult, error) {
|
|
block, _ := pem.Decode([]byte(csrPEM))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("failed to decode CSR PEM")
|
|
}
|
|
|
|
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse CSR: %w", err)
|
|
}
|
|
|
|
if err := csr.CheckSignature(); err != nil {
|
|
return nil, fmt.Errorf("CSR signature verification failed: %w", err)
|
|
}
|
|
|
|
switch key := csr.PublicKey.(type) {
|
|
case *rsa.PublicKey:
|
|
return &CSRValidationResult{
|
|
KeyAlgorithm: domain.KeyAlgorithmRSA,
|
|
KeySize: key.N.BitLen(),
|
|
}, nil
|
|
case *ecdsa.PublicKey:
|
|
return &CSRValidationResult{
|
|
KeyAlgorithm: domain.KeyAlgorithmECDSA,
|
|
KeySize: key.Curve.Params().BitSize,
|
|
}, nil
|
|
case ed25519.PublicKey:
|
|
return &CSRValidationResult{
|
|
KeyAlgorithm: domain.KeyAlgorithmEd25519,
|
|
KeySize: 256, // Ed25519 is fixed 256-bit
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported key type in CSR: %T", csr.PublicKey)
|
|
}
|
|
}
|