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:
537
certificate/utils/certificate_test.go
Normal file
537
certificate/utils/certificate_test.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user