feat: add certificate service and utility functions

- Implement CertificateService for end-to-end certificate management
- Add support for various certificate types (web, client, email, etc.)
- Include certificate generation, validation, and revocation
- Add utility functions for certificate operations and validation
- Implement comprehensive test coverage for certificate operations
- Support SAN (Subject Alternative Name) and IP address extensions
- Add proper error handling and validation for certificate requests
This commit is contained in:
Björn Benouarets
2025-09-30 11:45:05 +02:00
parent 5f6640d8f3
commit d9726d9204
4 changed files with 2558 additions and 0 deletions

View File

@@ -0,0 +1,537 @@
package utils
import (
"crypto/x509"
"testing"
"time"
"git.secnex.io/secnex/certman/models"
)
func TestDefaultCertificateConfig(t *testing.T) {
tests := []struct {
certType models.CertificateType
expected bool
}{
{models.CertificateTypeCA, true},
{models.CertificateTypeWeb, true},
{models.CertificateTypeClient, true},
{models.CertificateTypeEmail, true},
{models.CertificateTypeCode, true},
{models.CertificateTypeServer, true},
}
for _, test := range tests {
config := DefaultCertificateConfig(test.certType)
if config.CertificateType != test.certType {
t.Errorf("Expected certificate type %s, got %s", test.certType, config.CertificateType)
}
if config.SerialNumber == nil {
t.Error("Serial number should not be nil")
}
if config.NotBefore.After(config.NotAfter) {
t.Error("NotBefore should be before NotAfter")
}
}
}
func TestCACertificateConfig(t *testing.T) {
// Test root CA
rootConfig := CACertificateConfig(true)
if !rootConfig.IsCA {
t.Error("Root CA should have IsCA = true")
}
if !rootConfig.KeyCertSign {
t.Error("Root CA should have KeyCertSign = true")
}
if !rootConfig.CRLSign {
t.Error("Root CA should have CRLSign = true")
}
if rootConfig.MaxPathLen != -1 {
t.Error("Root CA should have MaxPathLen = -1")
}
// Test intermediate CA
intermediateConfig := CACertificateConfig(false)
if !intermediateConfig.IsCA {
t.Error("Intermediate CA should have IsCA = true")
}
if !intermediateConfig.KeyCertSign {
t.Error("Intermediate CA should have KeyCertSign = true")
}
if !intermediateConfig.CRLSign {
t.Error("Intermediate CA should have CRLSign = true")
}
if intermediateConfig.MaxPathLen != 0 {
t.Error("Intermediate CA should have MaxPathLen = 0")
}
}
func TestWebServerCertificateConfig(t *testing.T) {
domains := []string{"example.com", "www.example.com"}
config := WebServerCertificateConfig(domains)
if config.CertificateType != models.CertificateTypeWeb {
t.Error("Should be web certificate type")
}
if !config.DigitalSignature {
t.Error("Should have digital signature")
}
if !config.KeyEncipherment {
t.Error("Should have key encipherment")
}
if !config.ServerAuth {
t.Error("Should have server auth")
}
if len(config.DNSNames) != len(domains) {
t.Error("DNS names should match input domains")
}
}
func TestClientCertificateConfig(t *testing.T) {
config := ClientCertificateConfig()
if config.CertificateType != models.CertificateTypeClient {
t.Error("Should be client certificate type")
}
if !config.DigitalSignature {
t.Error("Should have digital signature")
}
if !config.KeyEncipherment {
t.Error("Should have key encipherment")
}
if !config.ClientAuth {
t.Error("Should have client auth")
}
}
func TestEmailCertificateConfig(t *testing.T) {
email := "test@example.com"
config := EmailCertificateConfig(email)
if config.CertificateType != models.CertificateTypeEmail {
t.Error("Should be email certificate type")
}
if !config.DigitalSignature {
t.Error("Should have digital signature")
}
if !config.KeyEncipherment {
t.Error("Should have key encipherment")
}
if !config.EmailProtection {
t.Error("Should have email protection")
}
if len(config.EmailAddresses) != 1 || config.EmailAddresses[0] != email {
t.Error("Email addresses should match input")
}
}
func TestCodeSigningCertificateConfig(t *testing.T) {
config := CodeSigningCertificateConfig()
if config.CertificateType != models.CertificateTypeCode {
t.Error("Should be code signing certificate type")
}
if !config.DigitalSignature {
t.Error("Should have digital signature")
}
if !config.CodeSigning {
t.Error("Should have code signing")
}
}
func TestGenerateKeyPair(t *testing.T) {
tests := []struct {
keyType KeyType
keySize KeySize
curve Curve
}{
{KeyTypeRSA, KeySize2048, CurveP256},
{KeyTypeRSA, KeySize3072, CurveP256},
{KeyTypeRSA, KeySize4096, CurveP256},
{KeyTypeECDSA, KeySize2048, CurveP256},
{KeyTypeECDSA, KeySize2048, CurveP384},
{KeyTypeECDSA, KeySize2048, CurveP521},
}
for _, test := range tests {
config := DefaultCertificateConfig(models.CertificateTypeWeb)
config.KeyType = test.keyType
config.KeySize = test.keySize
config.Curve = test.curve
generator := NewCertificateGenerator(config)
key, err := generator.GenerateKeyPair()
if err != nil {
t.Errorf("Failed to generate key pair for %s: %v", test.keyType, err)
}
if key == nil {
t.Errorf("Generated key should not be nil for %s", test.keyType)
}
}
}
func TestGenerateSelfSignedCertificate(t *testing.T) {
config := CACertificateConfig(true)
config.CommonName = "Test Root CA"
config.Organization = "Test Org"
config.Country = "DE"
generator := NewCertificateGenerator(config)
cert, privateKey, err := generator.GenerateSelfSignedCertificate()
if err != nil {
t.Fatalf("Failed to generate self-signed certificate: %v", err)
}
if cert == nil {
t.Error("Certificate should not be nil")
}
if privateKey == nil {
t.Error("Private key should not be nil")
}
if !cert.IsCA {
t.Error("CA certificate should have IsCA = true")
}
if cert.Subject.CommonName != config.CommonName {
t.Error("Certificate subject should match configuration")
}
}
func TestGenerateCertificate(t *testing.T) {
// Generate root CA first
rootConfig := CACertificateConfig(true)
rootConfig.CommonName = "Test Root CA"
rootConfig.Organization = "Test Org"
rootConfig.Country = "DE"
rootGenerator := NewCertificateGenerator(rootConfig)
rootCert, rootPrivateKey, err := rootGenerator.GenerateSelfSignedCertificate()
if err != nil {
t.Fatalf("Failed to generate root CA: %v", err)
}
// Generate intermediate CA
intermediateConfig := CACertificateConfig(false)
intermediateConfig.CommonName = "Test Intermediate CA"
intermediateConfig.Organization = "Test Org"
intermediateConfig.Country = "DE"
intermediateGenerator := NewCertificateGenerator(intermediateConfig)
intermediateCert, intermediatePrivateKey, err := intermediateGenerator.GenerateCertificate(rootCert, rootPrivateKey)
if err != nil {
t.Fatalf("Failed to generate intermediate CA: %v", err)
}
if intermediateCert == nil {
t.Error("Intermediate certificate should not be nil")
}
if intermediatePrivateKey == nil {
t.Error("Intermediate private key should not be nil")
}
if !intermediateCert.IsCA {
t.Error("Intermediate CA certificate should have IsCA = true")
}
if intermediateCert.Issuer.CommonName != rootCert.Subject.CommonName {
t.Error("Intermediate certificate issuer should match root certificate subject")
}
// Generate end entity certificate
endConfig := WebServerCertificateConfig([]string{"example.com"})
endConfig.CommonName = "example.com"
endConfig.Organization = "Test Org"
endConfig.Country = "DE"
endGenerator := NewCertificateGenerator(endConfig)
endCert, endPrivateKey, err := endGenerator.GenerateCertificate(intermediateCert, intermediatePrivateKey)
if err != nil {
t.Fatalf("Failed to generate end entity certificate: %v", err)
}
if endCert == nil {
t.Error("End entity certificate should not be nil")
}
if endPrivateKey == nil {
t.Error("End entity private key should not be nil")
}
if endCert.IsCA {
t.Error("End entity certificate should not have IsCA = true")
}
if endCert.Issuer.CommonName != intermediateCert.Subject.CommonName {
t.Error("End entity certificate issuer should match intermediate certificate subject")
}
}
func TestValidateCertificateConfig(t *testing.T) {
tests := []struct {
name string
config *CertificateConfig
wantErr bool
}{
{
name: "valid config",
config: &CertificateConfig{
CommonName: "example.com",
Organization: "Test Org",
Country: "DE",
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
},
wantErr: false,
},
{
name: "missing common name",
config: &CertificateConfig{
Organization: "Test Org",
Country: "DE",
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
},
wantErr: true,
},
{
name: "missing organization",
config: &CertificateConfig{
CommonName: "example.com",
Country: "DE",
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
},
wantErr: true,
},
{
name: "missing country",
config: &CertificateConfig{
CommonName: "example.com",
Organization: "Test Org",
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
},
wantErr: true,
},
{
name: "invalid date range",
config: &CertificateConfig{
CommonName: "example.com",
Organization: "Test Org",
Country: "DE",
NotBefore: time.Now().AddDate(1, 0, 0),
NotAfter: time.Now(),
},
wantErr: true,
},
{
name: "CA without key cert sign",
config: &CertificateConfig{
CommonName: "example.com",
Organization: "Test Org",
Country: "DE",
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
IsCA: true,
KeyCertSign: false,
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := ValidateCertificateConfig(test.config)
if (err != nil) != test.wantErr {
t.Errorf("ValidateCertificateConfig() error = %v, wantErr %v", err, test.wantErr)
}
})
}
}
func TestParseSANs(t *testing.T) {
tests := []struct {
input string
expectedDNS []string
expectedIPs int
expectedEmails []string
expectedURIs []string
}{
{
input: "example.com,www.example.com",
expectedDNS: []string{"example.com", "www.example.com"},
expectedIPs: 0,
expectedEmails: []string{},
expectedURIs: []string{},
},
{
input: "example.com,192.168.1.1,user@example.com,https://example.com",
expectedDNS: []string{"example.com"},
expectedIPs: 1,
expectedEmails: []string{"user@example.com"},
expectedURIs: []string{"https://example.com"},
},
{
input: "",
expectedDNS: []string{},
expectedIPs: 0,
expectedEmails: []string{},
expectedURIs: []string{},
},
}
for _, test := range tests {
dnsNames, ipAddresses, emailAddresses, uris := ParseSANs(test.input)
if len(dnsNames) != len(test.expectedDNS) {
t.Errorf("Expected %d DNS names, got %d", len(test.expectedDNS), len(dnsNames))
}
if len(ipAddresses) != test.expectedIPs {
t.Errorf("Expected %d IP addresses, got %d", test.expectedIPs, len(ipAddresses))
}
if len(emailAddresses) != len(test.expectedEmails) {
t.Errorf("Expected %d email addresses, got %d", len(test.expectedEmails), len(emailAddresses))
}
if len(uris) != len(test.expectedURIs) {
t.Errorf("Expected %d URIs, got %d", len(test.expectedURIs), len(uris))
}
}
}
func TestExportCertificateToPEM(t *testing.T) {
// Generate a test certificate
config := CACertificateConfig(true)
config.CommonName = "Test CA"
config.Organization = "Test Org"
config.Country = "DE"
generator := NewCertificateGenerator(config)
cert, _, err := generator.GenerateSelfSignedCertificate()
if err != nil {
t.Fatalf("Failed to generate certificate: %v", err)
}
exporter := NewCertificateExporter()
pemData, err := exporter.ExportCertificateToPEM(cert)
if err != nil {
t.Fatalf("Failed to export certificate to PEM: %v", err)
}
if len(pemData) == 0 {
t.Error("PEM data should not be empty")
}
// Verify PEM format
if !contains(pemData, []byte("-----BEGIN CERTIFICATE-----")) {
t.Error("PEM data should contain certificate header")
}
if !contains(pemData, []byte("-----END CERTIFICATE-----")) {
t.Error("PEM data should contain certificate footer")
}
}
func TestExportPrivateKeyToPEM(t *testing.T) {
// Generate a test certificate with private key
config := CACertificateConfig(true)
config.CommonName = "Test CA"
config.Organization = "Test Org"
config.Country = "DE"
generator := NewCertificateGenerator(config)
_, privateKey, err := generator.GenerateSelfSignedCertificate()
if err != nil {
t.Fatalf("Failed to generate certificate: %v", err)
}
exporter := NewCertificateExporter()
pemData, err := exporter.ExportPrivateKeyToPEM(privateKey)
if err != nil {
t.Fatalf("Failed to export private key to PEM: %v", err)
}
if len(pemData) == 0 {
t.Error("PEM data should not be empty")
}
// Verify PEM format
if !contains(pemData, []byte("-----BEGIN")) {
t.Error("PEM data should contain private key header")
}
if !contains(pemData, []byte("-----END")) {
t.Error("PEM data should contain private key footer")
}
}
func TestExportCertificateToDER(t *testing.T) {
// Generate a test certificate
config := CACertificateConfig(true)
config.CommonName = "Test CA"
config.Organization = "Test Org"
config.Country = "DE"
generator := NewCertificateGenerator(config)
cert, _, err := generator.GenerateSelfSignedCertificate()
if err != nil {
t.Fatalf("Failed to generate certificate: %v", err)
}
exporter := NewCertificateExporter()
derData := exporter.ExportCertificateToDER(cert)
if len(derData) == 0 {
t.Error("DER data should not be empty")
}
// Verify DER format by parsing it
_, err = x509.ParseCertificate(derData)
if err != nil {
t.Errorf("DER data should be valid certificate: %v", err)
}
}
func TestExportPrivateKeyToDER(t *testing.T) {
// Generate a test certificate with private key
config := CACertificateConfig(true)
config.CommonName = "Test CA"
config.Organization = "Test Org"
config.Country = "DE"
generator := NewCertificateGenerator(config)
_, privateKey, err := generator.GenerateSelfSignedCertificate()
if err != nil {
t.Fatalf("Failed to generate certificate: %v", err)
}
exporter := NewCertificateExporter()
derData, err := exporter.ExportPrivateKeyToDER(privateKey)
if err != nil {
t.Fatalf("Failed to export private key to DER: %v", err)
}
if len(derData) == 0 {
t.Error("DER data should not be empty")
}
}
// Helper function to check if a slice contains a subslice
func contains(s, subslice []byte) bool {
if len(subslice) > len(s) {
return false
}
for i := 0; i <= len(s)-len(subslice); i++ {
if bytesEqual(s[i:i+len(subslice)], subslice) {
return true
}
}
return false
}
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}