Newer
Older
EnvoyControlPlane / internal / pkg / cert / tool / cert_parser.go
package tool

import (
	"crypto/x509"
	"encoding/hex"
	"encoding/pem"
	"errors"
	"fmt"
	"math/big"
	"net"
	"net/url"
	"strings"
	"time"
)

// CertInfo holds the relevant data extracted from an X.509 certificate.
type CertInfo struct {
	Subject               string
	Issuer                string
	SerialNumber          string
	NotBefore             time.Time
	NotAfter              time.Time
	SignatureAlgorithm    string
	PublicKeyAlgorithm    string
	KeyUsage              []string
	ExtendedKeyUsage      []string
	DNSNames              []string
	IPAddresses           []string
	EmailAddresses        []string
	URIs                  []*url.URL
	IsCA                  bool
	AuthorityKeyId        string
	SubjectKeyId          string
	BasicConstraintsValid bool
}

// CertificateParser represents the "class" responsible for parsing certificates.
type CertificateParser struct{}

// mapKeyUsage translates the x509.KeyUsage bitmask into a slice of descriptive strings.
func mapKeyUsage(ku x509.KeyUsage) []string {
	var usages []string
	if ku&x509.KeyUsageDigitalSignature != 0 {
		usages = append(usages, "Digital Signature")
	}
	if ku&x509.KeyUsageContentCommitment != 0 {
		usages = append(usages, "Content Commitment")
	}
	if ku&x509.KeyUsageKeyEncipherment != 0 {
		usages = append(usages, "Key Encipherment")
	}
	if ku&x509.KeyUsageDataEncipherment != 0 {
		usages = append(usages, "Data Encipherment")
	}
	if ku&x509.KeyUsageKeyAgreement != 0 {
		usages = append(usages, "Key Agreement")
	}
	if ku&x509.KeyUsageCertSign != 0 {
		usages = append(usages, "Cert Sign")
	}
	if ku&x509.KeyUsageCRLSign != 0 {
		usages = append(usages, "CRL Sign")
	}
	if ku&x509.KeyUsageEncipherOnly != 0 {
		usages = append(usages, "Encipher Only")
	}
	if ku&x509.KeyUsageDecipherOnly != 0 {
		usages = append(usages, "Decipher Only")
	}
	return usages
}

// mapExtKeyUsage translates the x509.ExtKeyUsage slice into a slice of descriptive strings.
func mapExtKeyUsage(eku []x509.ExtKeyUsage) []string {
	var usages []string
	for _, u := range eku {
		switch u {
		case x509.ExtKeyUsageAny:
			usages = append(usages, "Any")
		case x509.ExtKeyUsageServerAuth:
			usages = append(usages, "Server Auth")
		case x509.ExtKeyUsageClientAuth:
			usages = append(usages, "Client Auth")
		case x509.ExtKeyUsageCodeSigning:
			usages = append(usages, "Code Signing")
		case x509.ExtKeyUsageEmailProtection:
			usages = append(usages, "Email Protection")
		case x509.ExtKeyUsageTimeStamping:
			usages = append(usages, "Time Stamping")
		case x509.ExtKeyUsageOCSPSigning:
			usages = append(usages, "OCSP Signing")
		case x509.ExtKeyUsageMicrosoftServerGatedCrypto:
			usages = append(usages, "Microsoft Server Gated Crypto")
		case x509.ExtKeyUsageNetscapeServerGatedCrypto:
			usages = append(usages, "Netscape Server Gated Crypto")
		case x509.ExtKeyUsageMicrosoftCommercialCodeSigning:
			usages = append(usages, "Microsoft Commercial Code Signing")
		case x509.ExtKeyUsageMicrosoftKernelCodeSigning:
			usages = append(usages, "Microsoft Kernel Code Signing")
		default:
			usages = append(usages, fmt.Sprintf("Unknown (%d)", u))
		}
	}
	return usages
}

// formatSerialNumber formats a big.Int serial number into a hexadecimal string.
func formatSerialNumber(sn *big.Int) string {
	if sn == nil {
		return ""
	}
	return strings.ToUpper(hex.EncodeToString(sn.Bytes()))
}

// formatIPAddresses formats a slice of net.IP into a slice of strings.
func formatIPAddresses(ips []net.IP) []string {
	var ipStrs []string
	for _, ip := range ips {
		ipStrs = append(ipStrs, ip.String())
	}
	return ipStrs
}

// mapCertificateToInfo takes an x509.Certificate and maps its fields to the CertInfo struct.
func mapCertificateToInfo(cert *x509.Certificate) *CertInfo {
	return &CertInfo{
		Subject:               cert.Subject.String(),
		Issuer:                cert.Issuer.String(),
		SerialNumber:          formatSerialNumber(cert.SerialNumber),
		NotBefore:             cert.NotBefore,
		NotAfter:              cert.NotAfter,
		SignatureAlgorithm:    cert.SignatureAlgorithm.String(),
		PublicKeyAlgorithm:    cert.PublicKeyAlgorithm.String(),
		IsCA:                  cert.IsCA,
		KeyUsage:              mapKeyUsage(cert.KeyUsage),
		ExtendedKeyUsage:      mapExtKeyUsage(cert.ExtKeyUsage),
		DNSNames:              cert.DNSNames,
		IPAddresses:           formatIPAddresses(cert.IPAddresses),
		EmailAddresses:        cert.EmailAddresses,
		URIs:                  cert.URIs,
		BasicConstraintsValid: cert.BasicConstraintsValid,
		AuthorityKeyId:        strings.ToUpper(hex.EncodeToString(cert.AuthorityKeyId)),
		SubjectKeyId:          strings.ToUpper(hex.EncodeToString(cert.SubjectKeyId)),
	}
}

// parseCertificates extracts and returns all x509.Certificate objects from the raw PEM-encoded data.
func (cp *CertificateParser) parseCertificates(certData []byte) ([]*x509.Certificate, error) {
	var certs []*x509.Certificate
	data := certData

	for len(data) > 0 {
		block, rest := pem.Decode(data)
		if block == nil {
			break
		}

		if block.Type == "CERTIFICATE" {
			// x509.ParseCertificates handles both single and multiple certificates in the block bytes
			parsedCerts, err := x509.ParseCertificates(block.Bytes)
			if err != nil {
				// Skip malformed blocks but continue
				fmt.Printf("Warning: skipping malformed certificate (%v)\n", err)
				data = rest
				continue
			}
			certs = append(certs, parsedCerts...)
		}

		data = rest
	}

	if len(certs) == 0 {
		return nil, errors.New("no valid CERTIFICATE blocks found")
	}

	return certs, nil
}

// Parse extracts information from one or more PEM-encoded certificates into CertInfo structs.
func (cp *CertificateParser) Parse(certData []byte) ([]*CertInfo, error) {
	certs, err := cp.parseCertificates(certData)
	if err != nil {
		return nil, err
	}

	var certsInfo []*CertInfo
	for _, cert := range certs {
		certsInfo = append(certsInfo, mapCertificateToInfo(cert))
	}

	return certsInfo, nil
}

// IsValid checks if all certificates in the provided data are currently valid.
// It returns true if all certificates are valid, or false otherwise.
// If the certificates are parseable but one or more are expired or not yet valid, it returns false.
func (cp *CertificateParser) IsValid(certData []byte) (bool, error) {
	certs, err := cp.parseCertificates(certData)
	if err != nil {
		// Handle parsing errors by returning false. The requirement was to return
		// false with no error if the certificate is *parseable* but expired.
		// A parsing error means it's not even parseable, so it's not valid.
		return false, fmt.Errorf("failed to parse certificates: %w", err)
	}

	now := time.Now()

	for _, cert := range certs {
		// Check NotBefore
		if now.Before(cert.NotBefore) {
			// Certificate is not yet valid (future-dated)
			return false, nil
		}

		// Check NotAfter
		if now.After(cert.NotAfter) {
			// Certificate has expired
			return false, nil
		}
	}

	// If the loop completes without returning false, all certificates are currently valid.
	return true, nil
}