Files
Charon/backend/internal/services/certificate_validator_test.go
GitHub Actions 4b925418f2 feat: Add certificate validation service with parsing and metadata extraction
- Implemented certificate parsing for PEM, DER, and PFX formats.
- Added functions to validate key matches and certificate chains.
- Introduced metadata extraction for certificates including common name, domains, and issuer organization.
- Created unit tests for all new functionalities to ensure reliability and correctness.
2026-04-11 07:17:45 +00:00

389 lines
12 KiB
Go

package services
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- helpers ---
func makeRSACertAndKey(t *testing.T, cn string, expiry time.Time) (*x509.Certificate, *rsa.PrivateKey, []byte, []byte) {
t.Helper()
priv, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: cn},
NotBefore: time.Now(),
NotAfter: expiry,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
return cert, priv, certPEM, keyPEM
}
func makeECDSACertAndKey(t *testing.T, cn string) (*x509.Certificate, *ecdsa.PrivateKey, []byte, []byte) {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: cn},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyDER, err := x509.MarshalECPrivateKey(priv)
require.NoError(t, err)
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
return cert, priv, certPEM, keyPEM
}
func makeEd25519CertAndKey(t *testing.T, cn string) (*x509.Certificate, ed25519.PrivateKey, []byte, []byte) {
t.Helper()
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(3),
Subject: pkix.Name{CommonName: cn},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyDER, err := x509.MarshalPKCS8PrivateKey(priv)
require.NoError(t, err)
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
return cert, priv, certPEM, keyPEM
}
// --- DetectFormat ---
func TestDetectFormat(t *testing.T) {
cert, _, certPEM, _ := makeRSACertAndKey(t, "test.com", time.Now().Add(time.Hour))
t.Run("PEM format", func(t *testing.T) {
assert.Equal(t, FormatPEM, DetectFormat(certPEM))
})
t.Run("DER format", func(t *testing.T) {
assert.Equal(t, FormatDER, DetectFormat(cert.Raw))
})
t.Run("unknown format", func(t *testing.T) {
assert.Equal(t, FormatUnknown, DetectFormat([]byte("not a cert")))
})
t.Run("empty data", func(t *testing.T) {
assert.Equal(t, FormatUnknown, DetectFormat([]byte{}))
})
}
// --- ParseCertificateInput ---
func TestParseCertificateInput(t *testing.T) {
t.Run("PEM cert only", func(t *testing.T) {
_, _, certPEM, _ := makeRSACertAndKey(t, "pem.test", time.Now().Add(time.Hour))
parsed, err := ParseCertificateInput(certPEM, nil, nil, "")
require.NoError(t, err)
assert.NotNil(t, parsed.Leaf)
assert.Equal(t, FormatPEM, parsed.Format)
assert.Nil(t, parsed.PrivateKey)
})
t.Run("PEM cert with key", func(t *testing.T) {
_, _, certPEM, keyPEM := makeRSACertAndKey(t, "pem-key.test", time.Now().Add(time.Hour))
parsed, err := ParseCertificateInput(certPEM, keyPEM, nil, "")
require.NoError(t, err)
assert.NotNil(t, parsed.Leaf)
assert.NotNil(t, parsed.PrivateKey)
assert.Equal(t, FormatPEM, parsed.Format)
})
t.Run("DER cert", func(t *testing.T) {
cert, _, _, _ := makeRSACertAndKey(t, "der.test", time.Now().Add(time.Hour))
parsed, err := ParseCertificateInput(cert.Raw, nil, nil, "")
require.NoError(t, err)
assert.NotNil(t, parsed.Leaf)
assert.Equal(t, FormatDER, parsed.Format)
})
t.Run("empty data returns error", func(t *testing.T) {
_, err := ParseCertificateInput(nil, nil, nil, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "empty")
})
t.Run("unrecognized format returns error", func(t *testing.T) {
_, err := ParseCertificateInput([]byte("garbage"), nil, nil, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unrecognized")
})
t.Run("invalid key PEM returns error", func(t *testing.T) {
_, _, certPEM, _ := makeRSACertAndKey(t, "badkey.test", time.Now().Add(time.Hour))
_, err := ParseCertificateInput(certPEM, []byte("not-key"), nil, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "private key")
})
}
// --- ValidateKeyMatch ---
func TestValidateKeyMatch(t *testing.T) {
t.Run("RSA matching", func(t *testing.T) {
cert, priv, _, _ := makeRSACertAndKey(t, "rsa.test", time.Now().Add(time.Hour))
assert.NoError(t, ValidateKeyMatch(cert, priv))
})
t.Run("RSA mismatched", func(t *testing.T) {
cert, _, _, _ := makeRSACertAndKey(t, "rsa1.test", time.Now().Add(time.Hour))
_, otherPriv, _, _ := makeRSACertAndKey(t, "rsa2.test", time.Now().Add(time.Hour))
err := ValidateKeyMatch(cert, otherPriv)
assert.Error(t, err)
assert.Contains(t, err.Error(), "mismatch")
})
t.Run("ECDSA matching", func(t *testing.T) {
cert, priv, _, _ := makeECDSACertAndKey(t, "ecdsa.test")
assert.NoError(t, ValidateKeyMatch(cert, priv))
})
t.Run("ECDSA mismatched", func(t *testing.T) {
cert, _, _, _ := makeECDSACertAndKey(t, "ec1.test")
_, other, _, _ := makeECDSACertAndKey(t, "ec2.test")
assert.Error(t, ValidateKeyMatch(cert, other))
})
t.Run("Ed25519 matching", func(t *testing.T) {
cert, priv, _, _ := makeEd25519CertAndKey(t, "ed.test")
assert.NoError(t, ValidateKeyMatch(cert, priv))
})
t.Run("Ed25519 mismatched", func(t *testing.T) {
cert, _, _, _ := makeEd25519CertAndKey(t, "ed1.test")
_, other, _, _ := makeEd25519CertAndKey(t, "ed2.test")
assert.Error(t, ValidateKeyMatch(cert, other))
})
t.Run("type mismatch RSA cert with ECDSA key", func(t *testing.T) {
cert, _, _, _ := makeRSACertAndKey(t, "rsa.test", time.Now().Add(time.Hour))
_, ecKey, _, _ := makeECDSACertAndKey(t, "ec.test")
err := ValidateKeyMatch(cert, ecKey)
assert.Error(t, err)
assert.Contains(t, err.Error(), "type mismatch")
})
t.Run("nil certificate", func(t *testing.T) {
_, priv, _, _ := makeRSACertAndKey(t, "rsa.test", time.Now().Add(time.Hour))
assert.Error(t, ValidateKeyMatch(nil, priv))
})
t.Run("nil key", func(t *testing.T) {
cert, _, _, _ := makeRSACertAndKey(t, "rsa.test", time.Now().Add(time.Hour))
assert.Error(t, ValidateKeyMatch(cert, nil))
})
}
// --- ValidateChain ---
func TestValidateChain(t *testing.T) {
t.Run("nil leaf returns error", func(t *testing.T) {
err := ValidateChain(nil, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "nil")
})
t.Run("self-signed cert validates", func(t *testing.T) {
cert, _, _, _ := makeRSACertAndKey(t, "self.test", time.Now().Add(time.Hour))
// Self-signed won't pass chain validation without being a CA
err := ValidateChain(cert, nil)
assert.Error(t, err)
})
}
// --- ConvertDERToPEM ---
func TestConvertDERToPEM(t *testing.T) {
t.Run("valid DER", func(t *testing.T) {
cert, _, _, _ := makeRSACertAndKey(t, "der.test", time.Now().Add(time.Hour))
pemStr, err := ConvertDERToPEM(cert.Raw)
require.NoError(t, err)
assert.Contains(t, pemStr, "BEGIN CERTIFICATE")
})
t.Run("invalid DER", func(t *testing.T) {
_, err := ConvertDERToPEM([]byte("not-der"))
assert.Error(t, err)
})
}
// --- ConvertPEMToDER ---
func TestConvertPEMToDER(t *testing.T) {
t.Run("valid PEM", func(t *testing.T) {
_, _, certPEM, _ := makeRSACertAndKey(t, "p2d.test", time.Now().Add(time.Hour))
der, err := ConvertPEMToDER(string(certPEM))
require.NoError(t, err)
assert.NotEmpty(t, der)
// Round-trip
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
assert.Equal(t, "p2d.test", cert.Subject.CommonName)
})
t.Run("invalid PEM", func(t *testing.T) {
_, err := ConvertPEMToDER("not-pem")
assert.Error(t, err)
})
}
// --- ExtractCertificateMetadata ---
func TestExtractCertificateMetadata(t *testing.T) {
t.Run("nil cert returns nil", func(t *testing.T) {
assert.Nil(t, ExtractCertificateMetadata(nil))
})
t.Run("RSA cert metadata", func(t *testing.T) {
cert, _, _, _ := makeRSACertAndKey(t, "meta.test", time.Now().Add(time.Hour))
m := ExtractCertificateMetadata(cert)
require.NotNil(t, m)
assert.Equal(t, "meta.test", m.CommonName)
assert.Contains(t, m.KeyType, "RSA")
assert.NotEmpty(t, m.Fingerprint)
assert.NotEmpty(t, m.SerialNumber)
assert.Contains(t, m.Domains, "meta.test")
})
t.Run("ECDSA cert metadata", func(t *testing.T) {
cert, _, _, _ := makeECDSACertAndKey(t, "ec-meta.test")
m := ExtractCertificateMetadata(cert)
require.NotNil(t, m)
assert.Contains(t, m.KeyType, "ECDSA")
})
t.Run("Ed25519 cert metadata", func(t *testing.T) {
cert, _, _, _ := makeEd25519CertAndKey(t, "ed-meta.test")
m := ExtractCertificateMetadata(cert)
require.NotNil(t, m)
assert.Equal(t, "Ed25519", m.KeyType)
})
t.Run("cert with SANs", func(t *testing.T) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(10),
Subject: pkix.Name{CommonName: "main.test"},
DNSNames: []string{"main.test", "alt1.test", "alt2.test"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
require.NoError(t, err)
cert, _ := x509.ParseCertificate(der)
m := ExtractCertificateMetadata(cert)
require.NotNil(t, m)
assert.Contains(t, m.Domains, "main.test")
assert.Contains(t, m.Domains, "alt1.test")
assert.Contains(t, m.Domains, "alt2.test")
// CN should not be duplicated when it matches a SAN
count := 0
for _, d := range m.Domains {
if d == "main.test" {
count++
}
}
assert.Equal(t, 1, count, "CN should not be duplicated in domains list")
})
t.Run("cert with issuer org", func(t *testing.T) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(11),
Subject: pkix.Name{CommonName: "org.test"},
Issuer: pkix.Name{Organization: []string{"Test Org Inc"}},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
require.NoError(t, err)
cert, _ := x509.ParseCertificate(der)
m := ExtractCertificateMetadata(cert)
require.NotNil(t, m)
// Self-signed cert's issuer org may differ from template
assert.NotEmpty(t, m.Fingerprint)
})
}
// --- Helpers ---
func TestFormatFingerprint(t *testing.T) {
assert.Equal(t, "AB:CD:EF", formatFingerprint("abcdef"))
assert.Equal(t, "01:23", formatFingerprint("0123"))
assert.Equal(t, "", formatFingerprint(""))
}
func TestFormatSerial(t *testing.T) {
assert.Equal(t, "01", formatSerial(big.NewInt(1)))
assert.Equal(t, "FF", formatSerial(big.NewInt(255)))
assert.Equal(t, "", formatSerial(nil))
}
func TestDetectKeyType(t *testing.T) {
t.Run("RSA key type", func(t *testing.T) {
cert, _, _, _ := makeRSACertAndKey(t, "rsa.test", time.Now().Add(time.Hour))
kt := detectKeyType(cert)
assert.Contains(t, kt, "RSA-2048")
})
t.Run("ECDSA-P256 key type", func(t *testing.T) {
cert, _, _, _ := makeECDSACertAndKey(t, "ec.test")
kt := detectKeyType(cert)
assert.Equal(t, "ECDSA-P256", kt)
})
t.Run("Ed25519 key type", func(t *testing.T) {
cert, _, _, _ := makeEd25519CertAndKey(t, "ed.test")
kt := detectKeyType(cert)
assert.Equal(t, "Ed25519", kt)
})
}