- 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.
522 lines
14 KiB
Go
522 lines
14 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
"software.sslmate.com/src/go-pkcs12"
|
|
)
|
|
|
|
// CertFormat represents a certificate file format.
|
|
type CertFormat string
|
|
|
|
const (
|
|
FormatPEM CertFormat = "pem"
|
|
FormatDER CertFormat = "der"
|
|
FormatPFX CertFormat = "pfx"
|
|
FormatUnknown CertFormat = "unknown"
|
|
)
|
|
|
|
// ParsedCertificate contains the parsed result of certificate input.
|
|
type ParsedCertificate struct {
|
|
Leaf *x509.Certificate
|
|
Intermediates []*x509.Certificate
|
|
PrivateKey crypto.PrivateKey
|
|
CertPEM string
|
|
KeyPEM string
|
|
ChainPEM string
|
|
Format CertFormat
|
|
}
|
|
|
|
// CertificateMetadata contains extracted metadata from an x509 certificate.
|
|
type CertificateMetadata struct {
|
|
CommonName string
|
|
Domains []string
|
|
Fingerprint string
|
|
SerialNumber string
|
|
IssuerOrg string
|
|
KeyType string
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
}
|
|
|
|
// ValidationResult contains the result of a certificate validation.
|
|
type ValidationResult struct {
|
|
Valid bool `json:"valid"`
|
|
CommonName string `json:"common_name"`
|
|
Domains []string `json:"domains"`
|
|
IssuerOrg string `json:"issuer_org"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
KeyMatch bool `json:"key_match"`
|
|
ChainValid bool `json:"chain_valid"`
|
|
ChainDepth int `json:"chain_depth"`
|
|
Warnings []string `json:"warnings"`
|
|
Errors []string `json:"errors"`
|
|
}
|
|
|
|
// DetectFormat determines the certificate format from raw file content.
|
|
// Uses trial-parse strategy: PEM → PFX → DER.
|
|
func DetectFormat(data []byte) CertFormat {
|
|
block, _ := pem.Decode(data)
|
|
if block != nil {
|
|
return FormatPEM
|
|
}
|
|
|
|
if _, _, _, err := pkcs12.DecodeChain(data, ""); err == nil {
|
|
return FormatPFX
|
|
}
|
|
// PFX with empty password failed, but it could be password-protected
|
|
// If data starts with PKCS12 magic bytes (ASN.1 SEQUENCE), treat as PFX candidate
|
|
if len(data) > 2 && data[0] == 0x30 {
|
|
// Could be DER or PFX; try DER parse
|
|
if _, err := x509.ParseCertificate(data); err == nil {
|
|
return FormatDER
|
|
}
|
|
// If DER parse fails, it's likely PFX
|
|
return FormatPFX
|
|
}
|
|
|
|
if _, err := x509.ParseCertificate(data); err == nil {
|
|
return FormatDER
|
|
}
|
|
|
|
return FormatUnknown
|
|
}
|
|
|
|
// ParseCertificateInput handles PEM, PFX, and DER input parsing.
|
|
func ParseCertificateInput(certData []byte, keyData []byte, chainData []byte, pfxPassword string) (*ParsedCertificate, error) {
|
|
if len(certData) == 0 {
|
|
return nil, fmt.Errorf("certificate data is empty")
|
|
}
|
|
|
|
format := DetectFormat(certData)
|
|
|
|
switch format {
|
|
case FormatPEM:
|
|
return parsePEMInput(certData, keyData, chainData)
|
|
case FormatPFX:
|
|
return parsePFXInput(certData, pfxPassword)
|
|
case FormatDER:
|
|
return parseDERInput(certData, keyData)
|
|
default:
|
|
return nil, fmt.Errorf("unrecognized certificate format")
|
|
}
|
|
}
|
|
|
|
func parsePEMInput(certData []byte, keyData []byte, chainData []byte) (*ParsedCertificate, error) {
|
|
result := &ParsedCertificate{Format: FormatPEM}
|
|
|
|
// Parse leaf certificate
|
|
certs, err := parsePEMCertificates(certData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse certificate PEM: %w", err)
|
|
}
|
|
if len(certs) == 0 {
|
|
return nil, fmt.Errorf("no certificates found in PEM data")
|
|
}
|
|
|
|
result.Leaf = certs[0]
|
|
result.CertPEM = string(certData)
|
|
|
|
// If certData contains multiple certs, treat extras as intermediates
|
|
if len(certs) > 1 {
|
|
result.Intermediates = certs[1:]
|
|
}
|
|
|
|
// Parse chain file if provided
|
|
if len(chainData) > 0 {
|
|
chainCerts, err := parsePEMCertificates(chainData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse chain PEM: %w", err)
|
|
}
|
|
result.Intermediates = append(result.Intermediates, chainCerts...)
|
|
result.ChainPEM = string(chainData)
|
|
}
|
|
|
|
// Build chain PEM from intermediates if not set from chain file
|
|
if result.ChainPEM == "" && len(result.Intermediates) > 0 {
|
|
var chainBuilder strings.Builder
|
|
for _, ic := range result.Intermediates {
|
|
if err := pem.Encode(&chainBuilder, &pem.Block{Type: "CERTIFICATE", Bytes: ic.Raw}); err != nil {
|
|
return nil, fmt.Errorf("failed to encode intermediate certificate: %w", err)
|
|
}
|
|
}
|
|
result.ChainPEM = chainBuilder.String()
|
|
}
|
|
|
|
// Parse private key
|
|
if len(keyData) > 0 {
|
|
key, err := parsePEMPrivateKey(keyData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key PEM: %w", err)
|
|
}
|
|
result.PrivateKey = key
|
|
result.KeyPEM = string(keyData)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func parsePFXInput(pfxData []byte, password string) (*ParsedCertificate, error) {
|
|
privateKey, leaf, caCerts, err := pkcs12.DecodeChain(pfxData, password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode PFX/PKCS12: %w", err)
|
|
}
|
|
|
|
result := &ParsedCertificate{
|
|
Format: FormatPFX,
|
|
Leaf: leaf,
|
|
Intermediates: caCerts,
|
|
PrivateKey: privateKey,
|
|
}
|
|
|
|
// Convert to PEM for storage
|
|
result.CertPEM = encodeCertToPEM(leaf)
|
|
|
|
if len(caCerts) > 0 {
|
|
var chainBuilder strings.Builder
|
|
for _, ca := range caCerts {
|
|
chainBuilder.WriteString(encodeCertToPEM(ca))
|
|
}
|
|
result.ChainPEM = chainBuilder.String()
|
|
}
|
|
|
|
keyPEM, err := encodeKeyToPEM(privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode private key to PEM: %w", err)
|
|
}
|
|
result.KeyPEM = keyPEM
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func parseDERInput(certData []byte, keyData []byte) (*ParsedCertificate, error) {
|
|
cert, err := x509.ParseCertificate(certData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse DER certificate: %w", err)
|
|
}
|
|
|
|
result := &ParsedCertificate{
|
|
Format: FormatDER,
|
|
Leaf: cert,
|
|
CertPEM: encodeCertToPEM(cert),
|
|
}
|
|
|
|
if len(keyData) > 0 {
|
|
key, err := parsePEMPrivateKey(keyData)
|
|
if err != nil {
|
|
// Try DER key
|
|
key, err = x509.ParsePKCS8PrivateKey(keyData)
|
|
if err != nil {
|
|
key2, err2 := x509.ParseECPrivateKey(keyData)
|
|
if err2 != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
|
}
|
|
key = key2
|
|
}
|
|
}
|
|
result.PrivateKey = key
|
|
keyPEM, err := encodeKeyToPEM(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode private key to PEM: %w", err)
|
|
}
|
|
result.KeyPEM = keyPEM
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ValidateKeyMatch checks that the private key matches the certificate public key.
|
|
func ValidateKeyMatch(cert *x509.Certificate, key crypto.PrivateKey) error {
|
|
if cert == nil {
|
|
return fmt.Errorf("certificate is nil")
|
|
}
|
|
if key == nil {
|
|
return fmt.Errorf("private key is nil")
|
|
}
|
|
|
|
switch pub := cert.PublicKey.(type) {
|
|
case *rsa.PublicKey:
|
|
privKey, ok := key.(*rsa.PrivateKey)
|
|
if !ok {
|
|
return fmt.Errorf("key type mismatch: certificate has RSA public key but private key is not RSA")
|
|
}
|
|
if pub.N.Cmp(privKey.N) != 0 {
|
|
return fmt.Errorf("RSA key mismatch: certificate and private key modulus differ")
|
|
}
|
|
case *ecdsa.PublicKey:
|
|
privKey, ok := key.(*ecdsa.PrivateKey)
|
|
if !ok {
|
|
return fmt.Errorf("key type mismatch: certificate has ECDSA public key but private key is not ECDSA")
|
|
}
|
|
if pub.X.Cmp(privKey.X) != 0 || pub.Y.Cmp(privKey.Y) != 0 {
|
|
return fmt.Errorf("ECDSA key mismatch: certificate and private key points differ")
|
|
}
|
|
case ed25519.PublicKey:
|
|
privKey, ok := key.(ed25519.PrivateKey)
|
|
if !ok {
|
|
return fmt.Errorf("key type mismatch: certificate has Ed25519 public key but private key is not Ed25519")
|
|
}
|
|
pubFromPriv := privKey.Public().(ed25519.PublicKey)
|
|
if !pub.Equal(pubFromPriv) {
|
|
return fmt.Errorf("Ed25519 key mismatch: certificate and private key differ")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported public key type: %T", cert.PublicKey)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateChain verifies the certificate chain from leaf to root.
|
|
func ValidateChain(leaf *x509.Certificate, intermediates []*x509.Certificate) error {
|
|
if leaf == nil {
|
|
return fmt.Errorf("leaf certificate is nil")
|
|
}
|
|
|
|
pool := x509.NewCertPool()
|
|
for _, ic := range intermediates {
|
|
pool.AddCert(ic)
|
|
}
|
|
|
|
opts := x509.VerifyOptions{
|
|
Intermediates: pool,
|
|
CurrentTime: time.Now(),
|
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
|
}
|
|
|
|
if _, err := leaf.Verify(opts); err != nil {
|
|
return fmt.Errorf("chain verification failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ConvertDERToPEM converts DER-encoded certificate to PEM.
|
|
func ConvertDERToPEM(derData []byte) (string, error) {
|
|
cert, err := x509.ParseCertificate(derData)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid DER data: %w", err)
|
|
}
|
|
return encodeCertToPEM(cert), nil
|
|
}
|
|
|
|
// ConvertPFXToPEM extracts cert, key, and chain from PFX/PKCS12.
|
|
func ConvertPFXToPEM(pfxData []byte, password string) (certPEM string, keyPEM string, chainPEM string, err error) {
|
|
privateKey, leaf, caCerts, err := pkcs12.DecodeChain(pfxData, password)
|
|
if err != nil {
|
|
return "", "", "", fmt.Errorf("failed to decode PFX: %w", err)
|
|
}
|
|
|
|
certPEM = encodeCertToPEM(leaf)
|
|
|
|
keyPEM, err = encodeKeyToPEM(privateKey)
|
|
if err != nil {
|
|
return "", "", "", fmt.Errorf("failed to encode key: %w", err)
|
|
}
|
|
|
|
if len(caCerts) > 0 {
|
|
var builder strings.Builder
|
|
for _, ca := range caCerts {
|
|
builder.WriteString(encodeCertToPEM(ca))
|
|
}
|
|
chainPEM = builder.String()
|
|
}
|
|
|
|
return certPEM, keyPEM, chainPEM, nil
|
|
}
|
|
|
|
// ConvertPEMToPFX bundles cert, key, chain into PFX.
|
|
func ConvertPEMToPFX(certPEM string, keyPEM string, chainPEM string, password string) ([]byte, error) {
|
|
certs, err := parsePEMCertificates([]byte(certPEM))
|
|
if err != nil || len(certs) == 0 {
|
|
return nil, fmt.Errorf("failed to parse cert PEM: %w", err)
|
|
}
|
|
|
|
key, err := parsePEMPrivateKey([]byte(keyPEM))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse key PEM: %w", err)
|
|
}
|
|
|
|
var caCerts []*x509.Certificate
|
|
if chainPEM != "" {
|
|
caCerts, err = parsePEMCertificates([]byte(chainPEM))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse chain PEM: %w", err)
|
|
}
|
|
}
|
|
|
|
pfxData, err := pkcs12.Modern.Encode(key, certs[0], caCerts, password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode PFX: %w", err)
|
|
}
|
|
|
|
return pfxData, nil
|
|
}
|
|
|
|
// ConvertPEMToDER converts PEM certificate to DER.
|
|
func ConvertPEMToDER(certPEM string) ([]byte, error) {
|
|
block, _ := pem.Decode([]byte(certPEM))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("failed to decode PEM")
|
|
}
|
|
// Verify it's a valid certificate
|
|
if _, err := x509.ParseCertificate(block.Bytes); err != nil {
|
|
return nil, fmt.Errorf("invalid certificate PEM: %w", err)
|
|
}
|
|
return block.Bytes, nil
|
|
}
|
|
|
|
// ExtractCertificateMetadata extracts fingerprint, serial, issuer, key type, etc.
|
|
func ExtractCertificateMetadata(cert *x509.Certificate) *CertificateMetadata {
|
|
if cert == nil {
|
|
return nil
|
|
}
|
|
|
|
fingerprint := sha256.Sum256(cert.Raw)
|
|
fpHex := formatFingerprint(hex.EncodeToString(fingerprint[:]))
|
|
|
|
serial := formatSerial(cert.SerialNumber)
|
|
|
|
issuerOrg := ""
|
|
if len(cert.Issuer.Organization) > 0 {
|
|
issuerOrg = cert.Issuer.Organization[0]
|
|
}
|
|
|
|
domains := make([]string, 0, len(cert.DNSNames)+1)
|
|
if cert.Subject.CommonName != "" {
|
|
domains = append(domains, cert.Subject.CommonName)
|
|
}
|
|
for _, san := range cert.DNSNames {
|
|
if san != cert.Subject.CommonName {
|
|
domains = append(domains, san)
|
|
}
|
|
}
|
|
|
|
return &CertificateMetadata{
|
|
CommonName: cert.Subject.CommonName,
|
|
Domains: domains,
|
|
Fingerprint: fpHex,
|
|
SerialNumber: serial,
|
|
IssuerOrg: issuerOrg,
|
|
KeyType: detectKeyType(cert),
|
|
NotBefore: cert.NotBefore,
|
|
NotAfter: cert.NotAfter,
|
|
}
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func parsePEMCertificates(data []byte) ([]*x509.Certificate, error) {
|
|
var certs []*x509.Certificate
|
|
rest := data
|
|
for {
|
|
var block *pem.Block
|
|
block, rest = pem.Decode(rest)
|
|
if block == nil {
|
|
break
|
|
}
|
|
if block.Type != "CERTIFICATE" {
|
|
continue
|
|
}
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
|
}
|
|
certs = append(certs, cert)
|
|
}
|
|
return certs, nil
|
|
}
|
|
|
|
func parsePEMPrivateKey(data []byte) (crypto.PrivateKey, error) {
|
|
block, _ := pem.Decode(data)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("no PEM data found")
|
|
}
|
|
|
|
// Try PKCS8 first (handles RSA, ECDSA, Ed25519)
|
|
if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
|
|
return key, nil
|
|
}
|
|
|
|
// Try PKCS1 RSA
|
|
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
|
|
return key, nil
|
|
}
|
|
|
|
// Try EC
|
|
if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil {
|
|
return key, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported private key format")
|
|
}
|
|
|
|
func encodeCertToPEM(cert *x509.Certificate) string {
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))
|
|
}
|
|
|
|
func encodeKeyToPEM(key crypto.PrivateKey) (string, error) {
|
|
der, err := x509.MarshalPKCS8PrivateKey(key)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal private key: %w", err)
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})), nil
|
|
}
|
|
|
|
func formatFingerprint(hex string) string {
|
|
var parts []string
|
|
for i := 0; i < len(hex); i += 2 {
|
|
end := i + 2
|
|
if end > len(hex) {
|
|
end = len(hex)
|
|
}
|
|
parts = append(parts, strings.ToUpper(hex[i:end]))
|
|
}
|
|
return strings.Join(parts, ":")
|
|
}
|
|
|
|
func formatSerial(n *big.Int) string {
|
|
if n == nil {
|
|
return ""
|
|
}
|
|
b := n.Bytes()
|
|
parts := make([]string, len(b))
|
|
for i, v := range b {
|
|
parts[i] = fmt.Sprintf("%02X", v)
|
|
}
|
|
return strings.Join(parts, ":")
|
|
}
|
|
|
|
func detectKeyType(cert *x509.Certificate) string {
|
|
switch pub := cert.PublicKey.(type) {
|
|
case *rsa.PublicKey:
|
|
bits := pub.N.BitLen()
|
|
return fmt.Sprintf("RSA-%d", bits)
|
|
case *ecdsa.PublicKey:
|
|
switch pub.Curve {
|
|
case elliptic.P256():
|
|
return "ECDSA-P256"
|
|
case elliptic.P384():
|
|
return "ECDSA-P384"
|
|
default:
|
|
return "ECDSA"
|
|
}
|
|
case ed25519.PublicKey:
|
|
return "Ed25519"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|