mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add Support for Proxy with Interceptor (#77)
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package certmanager
|
||||
|
||||
import "sync"
|
||||
|
||||
// InMemoryCache implements CertificateCache using an in-memory map
|
||||
type InMemoryCache struct {
|
||||
mu sync.RWMutex
|
||||
cache map[string]*Certificate
|
||||
}
|
||||
|
||||
// NewInMemoryCache creates a new in-memory certificate cache
|
||||
func NewInMemoryCache() *InMemoryCache {
|
||||
return &InMemoryCache{
|
||||
cache: make(map[string]*Certificate),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *InMemoryCache) Get(hostname string) (*Certificate, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
cert, found := c.cache[hostname]
|
||||
return cert, found
|
||||
}
|
||||
|
||||
func (c *InMemoryCache) Set(hostname string, cert *Certificate) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.cache[hostname] = cert
|
||||
}
|
||||
|
||||
func (c *InMemoryCache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.cache = make(map[string]*Certificate)
|
||||
}
|
||||
|
||||
func (c *InMemoryCache) Size() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return len(c.cache)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package certmanager
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Certificate represents a TLS certificate with its private key
|
||||
// Both certificate and private key are stored in PEM-encoded format
|
||||
type Certificate struct {
|
||||
// PEM encoded certificate
|
||||
Certificate []byte
|
||||
|
||||
// PEM encoded private key
|
||||
PrivateKey []byte
|
||||
|
||||
// Parsed X.509 certificate
|
||||
X509Cert *x509.Certificate
|
||||
|
||||
// Parsed private key
|
||||
PrivKey crypto.PrivateKey
|
||||
}
|
||||
|
||||
// CertificateCache defines the interface for certificate caching
|
||||
type CertificateCache interface {
|
||||
// Get retrieves a cached certificate for the given hostname
|
||||
Get(hostname string) (*Certificate, bool)
|
||||
|
||||
// Set stores a certificate for the given hostname
|
||||
Set(hostname string, cert *Certificate)
|
||||
|
||||
// Clear removes all cached certificates
|
||||
Clear()
|
||||
|
||||
// Size returns the number of cached certificates
|
||||
Size() int
|
||||
}
|
||||
|
||||
// CertificateManager handles TLS certificate lifecycle management
|
||||
type CertificateManager interface {
|
||||
// GetCA returns the Certificate Authority certificate and key
|
||||
GetCA() (*Certificate, error)
|
||||
|
||||
// GenerateCertForHost creates a certificate for the given hostname
|
||||
// Uses caching to avoid regeneration of certificates
|
||||
// The certificate is signed by the CA and includes the hostname in the SAN
|
||||
GenerateCertForHost(hostname string) (*Certificate, error)
|
||||
|
||||
// GetTLSConfig returns a tls.Config for the given hostname
|
||||
// This is a convenience method that generates/retrieves the certificate
|
||||
// and creates a tls.Config
|
||||
GetTLSConfig(hostname string) (*tls.Config, error)
|
||||
}
|
||||
|
||||
// CertManagerConfig holds configuration for certificate generation
|
||||
type CertManagerConfig struct {
|
||||
// CAValidityDays specifies how many days the CA certificate is valid
|
||||
CAValidityDays int
|
||||
|
||||
// HostCertValidityDays specifies how many days host certificates are valid
|
||||
HostCertValidityDays int
|
||||
|
||||
// KeySize specifies the RSA key size in bits
|
||||
KeySize int
|
||||
}
|
||||
|
||||
// DefaultCertManagerConfig returns a configuration with reasonable defaults
|
||||
func DefaultCertManagerConfig() CertManagerConfig {
|
||||
return CertManagerConfig{
|
||||
CAValidityDays: 365,
|
||||
HostCertValidityDays: 1,
|
||||
KeySize: 2048,
|
||||
}
|
||||
}
|
||||
|
||||
// SetDefaults sets reasonable defaults for zero values in the configuration
|
||||
func (c *CertManagerConfig) SetDefaults() {
|
||||
if c.CAValidityDays <= 0 {
|
||||
c.CAValidityDays = 365
|
||||
}
|
||||
|
||||
if c.HostCertValidityDays <= 0 {
|
||||
c.HostCertValidityDays = 1
|
||||
}
|
||||
|
||||
// Default to 2048 bits if key size is not set
|
||||
if c.KeySize == 0 {
|
||||
c.KeySize = 2048
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid after defaults have been set
|
||||
func (c *CertManagerConfig) Validate() error {
|
||||
if c.CAValidityDays <= 0 {
|
||||
return fmt.Errorf("CA validity days must be greater than 0: %d", c.CAValidityDays)
|
||||
}
|
||||
|
||||
if c.HostCertValidityDays <= 0 {
|
||||
return fmt.Errorf("host certificate validity days must be greater than 0: %d", c.HostCertValidityDays)
|
||||
}
|
||||
|
||||
if c.KeySize < 2048 {
|
||||
return fmt.Errorf("key size must be at least 2048 bits: %d", c.KeySize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExpired checks if a certificate is expired or will expire within the given threshold
|
||||
func (c *Certificate) IsExpired(threshold time.Duration) bool {
|
||||
if c.X509Cert == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
expiryTime := c.X509Cert.NotAfter
|
||||
return time.Until(expiryTime) < threshold
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package certmanager
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateCA(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
|
||||
ca, err := GenerateCA(config)
|
||||
assert.NoError(t, err, "Failed to generate CA")
|
||||
|
||||
assert.NotNil(t, ca, "CA certificate should not be nil")
|
||||
|
||||
assert.NotNil(t, ca.Certificate, "CA certificate should not be nil")
|
||||
assert.NotEmpty(t, ca.Certificate, "CA certificate should not be empty")
|
||||
|
||||
assert.NotNil(t, ca.PrivateKey, "CA private key should not be nil")
|
||||
assert.NotEmpty(t, ca.PrivateKey, "CA private key should not be empty")
|
||||
|
||||
assert.NotNil(t, ca.X509Cert, "Parsed X509 certificate should not be nil")
|
||||
|
||||
assert.NotNil(t, ca.PrivKey, "Parsed private key should not be nil")
|
||||
|
||||
assert.True(t, ca.X509Cert.IsCA, "Certificate should be marked as CA")
|
||||
|
||||
assert.Equal(t, "PMG Proxy CA", ca.X509Cert.Subject.CommonName, "Common name should be PMG Proxy CA")
|
||||
|
||||
assert.Greater(t, ca.X509Cert.NotAfter.Sub(ca.X509Cert.NotBefore).Hours(),
|
||||
float64(config.CAValidityDays*24-1), "CA certificate validity period should be greater than the configured validity days")
|
||||
}
|
||||
|
||||
func TestInMemoryCache(t *testing.T) {
|
||||
cache := NewInMemoryCache()
|
||||
|
||||
assert.Equal(t, 0, cache.Size(), "New cache should be empty")
|
||||
|
||||
cert := &Certificate{
|
||||
Certificate: []byte("test cert"),
|
||||
PrivateKey: []byte("test key"),
|
||||
}
|
||||
|
||||
cache.Set("example.com", cert)
|
||||
|
||||
assert.Equal(t, 1, cache.Size(), "Cache size should be 1")
|
||||
|
||||
retrieved, found := cache.Get("example.com")
|
||||
assert.True(t, found, "Certificate should be found in cache")
|
||||
assert.Equal(t, "test cert", string(retrieved.Certificate), "Retrieved certificate should match")
|
||||
|
||||
_, found = cache.Get("nonexistent.com")
|
||||
assert.False(t, found, "Non-existent certificate should not be found")
|
||||
|
||||
cache.Clear()
|
||||
assert.Equal(t, 0, cache.Size(), "Cache should be empty after Clear")
|
||||
}
|
||||
|
||||
func TestNewCertificateManagerWithCA(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
ca, err := GenerateCA(config)
|
||||
assert.NoError(t, err, "Failed to generate CA")
|
||||
|
||||
cm, err := NewCertificateManagerWithCA(ca, config)
|
||||
assert.NoError(t, err, "Failed to create certificate manager")
|
||||
|
||||
assert.NotNil(t, cm, "Certificate manager should not be nil")
|
||||
|
||||
retrievedCA, err := cm.GetCA()
|
||||
assert.NoError(t, err, "Failed to get CA")
|
||||
|
||||
assert.Equal(t, ca, retrievedCA, "Retrieved CA should match original")
|
||||
}
|
||||
|
||||
func TestGenerateCertForHost(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
ca, err := GenerateCA(config)
|
||||
assert.NoError(t, err, "Failed to generate CA")
|
||||
|
||||
cm, err := NewCertificateManagerWithCA(ca, config)
|
||||
assert.NoError(t, err, "Failed to create certificate manager")
|
||||
|
||||
hostname := "registry.npmjs.org"
|
||||
|
||||
cert, err := cm.GenerateCertForHost(hostname)
|
||||
assert.NoError(t, err, "Failed to generate host certificate")
|
||||
|
||||
assert.NotNil(t, cert, "Host certificate should not be nil")
|
||||
assert.NotEmpty(t, cert.Certificate, "Host certificate should not be empty")
|
||||
assert.NotEmpty(t, cert.PrivateKey, "Host private key should not be empty")
|
||||
assert.NotNil(t, cert.X509Cert, "Parsed X509 certificate should not be nil")
|
||||
assert.False(t, cert.X509Cert.IsCA, "Host certificate should not be marked as CA")
|
||||
assert.Contains(t, cert.X509Cert.DNSNames, hostname, "Certificate SAN should include hostname")
|
||||
assert.Equal(t, hostname, cert.X509Cert.Subject.CommonName, "Common name should match hostname")
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(ca.X509Cert)
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
DNSName: hostname,
|
||||
Roots: roots,
|
||||
}
|
||||
|
||||
_, err = cert.X509Cert.Verify(opts)
|
||||
assert.NoError(t, err, "Certificate should be verified by CA")
|
||||
}
|
||||
|
||||
func TestCertificateCaching(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
ca, err := GenerateCA(config)
|
||||
assert.NoError(t, err, "Failed to generate CA")
|
||||
|
||||
cm, err := NewCertificateManagerWithCA(ca, config)
|
||||
assert.NoError(t, err, "Failed to create certificate manager")
|
||||
|
||||
hostname := "registry.npmjs.org"
|
||||
|
||||
// First generation
|
||||
cert1, err := cm.GenerateCertForHost(hostname)
|
||||
assert.NoError(t, err, "Failed to generate first certificate")
|
||||
|
||||
// Second generation should return cached certificate
|
||||
cert2, err := cm.GenerateCertForHost(hostname)
|
||||
assert.NoError(t, err, "Failed to generate second certificate")
|
||||
|
||||
// Should be the same certificate (pointer equality)
|
||||
assert.Equal(t, cert1, cert2, "Second call should return cached certificate")
|
||||
}
|
||||
|
||||
func TestGetTLSConfig(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
ca, err := GenerateCA(config)
|
||||
assert.NoError(t, err, "Failed to generate CA")
|
||||
|
||||
cm, err := NewCertificateManagerWithCA(ca, config)
|
||||
assert.NoError(t, err, "Failed to create certificate manager")
|
||||
|
||||
hostname := "registry.npmjs.org"
|
||||
|
||||
tlsConfig, err := cm.GetTLSConfig(hostname)
|
||||
assert.NoError(t, err, "Failed to get TLS config")
|
||||
|
||||
assert.NotNil(t, tlsConfig, "TLS config should not be nil")
|
||||
|
||||
assert.NotEmpty(t, tlsConfig.Certificates, "TLS config should have certificates")
|
||||
assert.Equal(t, uint16(tls.VersionTLS12), tlsConfig.MinVersion, "TLS min version should be TLS 1.2")
|
||||
}
|
||||
|
||||
func TestCertificateExpiry(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
config.HostCertValidityDays = 1
|
||||
|
||||
ca, err := GenerateCA(config)
|
||||
assert.NoError(t, err, "Failed to generate CA")
|
||||
|
||||
cm, err := NewCertificateManagerWithCA(ca, config)
|
||||
assert.NoError(t, err, "Failed to create certificate manager")
|
||||
|
||||
cert, err := cm.GenerateCertForHost("example.com")
|
||||
assert.NoError(t, err, "Failed to generate certificate")
|
||||
|
||||
// Certificate should not be expired with 1 hour threshold
|
||||
assert.False(t, cert.IsExpired(1*time.Hour), "Certificate should not be expired yet")
|
||||
|
||||
// Certificate should be "expired" with threshold > validity period
|
||||
assert.True(t, cert.IsExpired(25*time.Hour), "Certificate should be considered expired with large threshold")
|
||||
}
|
||||
|
||||
func TestCertManagerConfigValidation(t *testing.T) {
|
||||
config := CertManagerConfig{
|
||||
CAValidityDays: 0,
|
||||
HostCertValidityDays: 0,
|
||||
KeySize: 2048,
|
||||
}
|
||||
|
||||
config.SetDefaults()
|
||||
err := config.Validate()
|
||||
assert.NoError(t, err, "Validate should not return error")
|
||||
|
||||
assert.Equal(t, 365, config.CAValidityDays, "CAValidityDays should default to 365")
|
||||
assert.Equal(t, 1, config.HostCertValidityDays, "HostCertValidityDays should default to 1")
|
||||
assert.Equal(t, 2048, config.KeySize, "KeySize should default to 2048")
|
||||
|
||||
config.KeySize = 1024
|
||||
err = config.Validate()
|
||||
assert.Error(t, err, "Validate should return error for key size less than 2048")
|
||||
}
|
||||
|
||||
func TestNewCertificateManagerWithNilCA(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
|
||||
_, err := NewCertificateManagerWithCA(nil, config)
|
||||
if err == nil {
|
||||
t.Error("Expected error when creating manager with nil CA")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerateCA(b *testing.B) {
|
||||
config := DefaultCertManagerConfig()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GenerateCA(config)
|
||||
assert.NoError(b, err, "Failed to generate CA")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerateHostCert(b *testing.B) {
|
||||
config := DefaultCertManagerConfig()
|
||||
ca, err := GenerateCA(config)
|
||||
assert.NoError(b, err, "Failed to generate CA")
|
||||
|
||||
cm, err := NewCertificateManagerWithCA(ca, config)
|
||||
assert.NoError(b, err, "Failed to create certificate manager")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := cm.GenerateCertForHost("example.com")
|
||||
assert.NoError(b, err, "Failed to generate host certificate")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCacheOperations(b *testing.B) {
|
||||
cache := NewInMemoryCache()
|
||||
cert := &Certificate{
|
||||
Certificate: []byte("test cert"),
|
||||
PrivateKey: []byte("test key"),
|
||||
}
|
||||
|
||||
b.Run("Set", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.Set("example.com", cert)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Get", func(b *testing.B) {
|
||||
cache.Set("example.com", cert)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.Get("example.com")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package certmanager
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
// certManager implements the CertificateManager interface
|
||||
type certManager struct {
|
||||
ca *Certificate
|
||||
cache CertificateCache
|
||||
config CertManagerConfig
|
||||
}
|
||||
|
||||
// NewCertificateManagerWithCA creates a new certificate manager with an existing CA certificate
|
||||
func NewCertificateManagerWithCA(ca *Certificate, config CertManagerConfig) (CertificateManager, error) {
|
||||
if ca == nil {
|
||||
return nil, fmt.Errorf("CA certificate cannot be nil")
|
||||
}
|
||||
|
||||
config.SetDefaults()
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
if ca.X509Cert == nil || ca.PrivKey == nil {
|
||||
parsedCA, err := parseCertificate(ca)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse CA certificate: %w", err)
|
||||
}
|
||||
|
||||
ca = parsedCA
|
||||
}
|
||||
|
||||
if ca.IsExpired(1 * time.Hour) {
|
||||
return nil, fmt.Errorf("CA certificate is expired")
|
||||
}
|
||||
|
||||
return &certManager{
|
||||
ca: ca,
|
||||
cache: NewInMemoryCache(),
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCA returns the Certificate Authority certificate
|
||||
func (cm *certManager) GetCA() (*Certificate, error) {
|
||||
return cm.ca, nil
|
||||
}
|
||||
|
||||
// GenerateCertForHost creates a certificate for the given hostname
|
||||
// Uses caching to avoid regeneration
|
||||
func (cm *certManager) GenerateCertForHost(hostname string) (*Certificate, error) {
|
||||
if cached, found := cm.cache.Get(hostname); found {
|
||||
if !cached.IsExpired(1 * time.Hour) {
|
||||
return cached, nil
|
||||
}
|
||||
}
|
||||
|
||||
cert, err := cm.generateHostCert(hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate certificate for %s: %w", hostname, err)
|
||||
}
|
||||
|
||||
cm.cache.Set(hostname, cert)
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// GetTLSConfig returns a tls.Config for the given hostname
|
||||
func (cm *certManager) GetTLSConfig(hostname string) (*tls.Config, error) {
|
||||
cert, err := cm.GenerateCertForHost(hostname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create tls.Certificate: %w", err)
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cm *certManager) generateHostCert(hostname string) (*Certificate, error) {
|
||||
privKey, err := rsa.GenerateKey(rand.Reader, cm.config.KeySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(time.Duration(cm.config.HostCertValidityDays) * 24 * time.Hour)
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: hostname,
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{hostname},
|
||||
}
|
||||
|
||||
caPrivKey, ok := cm.ca.PrivKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("CA private key is not RSA")
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, cm.ca.X509Cert, &privKey.PublicKey, caPrivKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate: %w", err)
|
||||
}
|
||||
|
||||
x509Cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse generated certificate: %w", err)
|
||||
}
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
})
|
||||
|
||||
privKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
|
||||
})
|
||||
|
||||
return &Certificate{
|
||||
Certificate: certPEM,
|
||||
PrivateKey: privKeyPEM,
|
||||
X509Cert: x509Cert,
|
||||
PrivKey: privKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateCA generates a new self-signed CA certificate using the given configuration
|
||||
func GenerateCA(config CertManagerConfig) (*Certificate, error) {
|
||||
config.SetDefaults()
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
privKey, err := rsa.GenerateKey(rand.Reader, config.KeySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate CA private key: %w", err)
|
||||
}
|
||||
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(time.Duration(config.CAValidityDays) * 24 * time.Hour)
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "PMG Proxy CA",
|
||||
Organization: []string{"SafeDep PMG"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create CA certificate: %w", err)
|
||||
}
|
||||
|
||||
x509Cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse generated CA certificate: %w", err)
|
||||
}
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
})
|
||||
|
||||
privKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
|
||||
})
|
||||
|
||||
return &Certificate{
|
||||
Certificate: certPEM,
|
||||
PrivateKey: privKeyPEM,
|
||||
X509Cert: x509Cert,
|
||||
PrivKey: privKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseCertificate(cert *Certificate) (*Certificate, error) {
|
||||
certBlock, _ := pem.Decode(cert.Certificate)
|
||||
if certBlock == nil {
|
||||
return nil, fmt.Errorf("failed to decode PEM certificate")
|
||||
}
|
||||
|
||||
x509Cert, err := x509.ParseCertificate(certBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse X509 certificate: %w", err)
|
||||
}
|
||||
|
||||
keyBlock, _ := pem.Decode(cert.PrivateKey)
|
||||
if keyBlock == nil {
|
||||
return nil, fmt.Errorf("failed to decode PEM private key")
|
||||
}
|
||||
|
||||
privKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse RSA private key: %w", err)
|
||||
}
|
||||
|
||||
return &Certificate{
|
||||
Certificate: cert.Certificate,
|
||||
PrivateKey: cert.PrivateKey,
|
||||
X509Cert: x509Cert,
|
||||
PrivKey: privKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseTLSCertificate converts a Certificate to a tls.Certificate
|
||||
// This is useful for integrating with libraries that expect tls.Certificate
|
||||
func ParseTLSCertificate(cert *Certificate) (tls.Certificate, error) {
|
||||
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("failed to create X509 key pair: %w", err)
|
||||
}
|
||||
|
||||
// Populate Leaf field if we have the parsed X.509 certificate
|
||||
// This is important for libraries (like goproxy) that need to inspect the certificate
|
||||
if cert.X509Cert != nil {
|
||||
tlsCert.Leaf = cert.X509Cert
|
||||
}
|
||||
|
||||
return tlsCert, nil
|
||||
}
|
||||
Reference in New Issue
Block a user