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
}