- 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.
389 lines
12 KiB
Go
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)
|
|
})
|
|
}
|