From 80fcac77d8c330931b3033fb7898332c1cbbc85b Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Sun, 5 Apr 2026 21:56:21 +0200 Subject: [PATCH] Add all other cert details --- internal/utils/utils.go | 116 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index d45acb1..ec593d4 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -5,6 +5,7 @@ package utils import ( "bytes" "compress/gzip" + "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/base64" @@ -74,11 +75,63 @@ type IssuerInfo struct { SerialNumber string `json:"serial_number"` } +// CertDetails holds fields parsed from the certificate beyond what TrimmedEntry provides. +type CertDetails struct { + NotBefore string `json:"not_before"` + NotAfter string `json:"not_after"` + SerialNumber string `json:"serial_number"` + Issuer string `json:"issuer"` + EmailSANs []string `json:"email_sans,omitempty"` + URISANs []string `json:"uri_sans,omitempty"` + SubjectKeyID string `json:"subject_key_id,omitempty"` + AuthorityKeyID string `json:"authority_key_id,omitempty"` + OCSPServers []string `json:"ocsp_servers,omitempty"` + IssuingCertURLs []string `json:"issuing_cert_urls,omitempty"` + CRLDistributionPoints []string `json:"crl_distribution_points,omitempty"` + KeyUsage []string `json:"key_usage,omitempty"` + ExtKeyUsage []string `json:"ext_key_usage,omitempty"` + IsCA bool `json:"is_ca"` +} + +var keyUsageNames = []struct { + usage x509.KeyUsage + name string +}{ + {x509.KeyUsageDigitalSignature, "DigitalSignature"}, + {x509.KeyUsageContentCommitment, "ContentCommitment"}, + {x509.KeyUsageKeyEncipherment, "KeyEncipherment"}, + {x509.KeyUsageDataEncipherment, "DataEncipherment"}, + {x509.KeyUsageKeyAgreement, "KeyAgreement"}, + {x509.KeyUsageCertSign, "CertSign"}, + {x509.KeyUsageCRLSign, "CRLSign"}, + {x509.KeyUsageEncipherOnly, "EncipherOnly"}, + {x509.KeyUsageDecipherOnly, "DecipherOnly"}, +} + +var extKeyUsageNames = map[x509.ExtKeyUsage]string{ + x509.ExtKeyUsageAny: "Any", + x509.ExtKeyUsageServerAuth: "ServerAuth", + x509.ExtKeyUsageClientAuth: "ClientAuth", + x509.ExtKeyUsageCodeSigning: "CodeSigning", + x509.ExtKeyUsageEmailProtection: "EmailProtection", + x509.ExtKeyUsageIPSECEndSystem: "IPSECEndSystem", + x509.ExtKeyUsageIPSECTunnel: "IPSECTunnel", + x509.ExtKeyUsageIPSECUser: "IPSECUser", + x509.ExtKeyUsageTimeStamping: "TimeStamping", + x509.ExtKeyUsageOCSPSigning: "OCSPSigning", + x509.ExtKeyUsageMicrosoftServerGatedCrypto: "MicrosoftServerGatedCrypto", + x509.ExtKeyUsageNetscapeServerGatedCrypto: "NetscapeServerGatedCrypto", + x509.ExtKeyUsageMicrosoftCommercialCodeSigning: "MicrosoftCommercialCodeSigning", + x509.ExtKeyUsageMicrosoftKernelCodeSigning: "MicrosoftKernelCodeSigning", +} + // Entry represents a CT log entry in JSON format. type Entry struct { EntryNumber int `json:"entry_number"` LeafIndex int64 `json:"leaf_index"` + MerkleLeafHash string `json:"merkle_leaf_hash"` Timestamp int64 `json:"timestamp"` + TimestampHuman string `json:"timestamp_human"` IsPrecert bool `json:"is_precert"` IssuerKeyHash string `json:"issuer_key_hash,omitempty"` CertificateSize int `json:"certificate_size"` @@ -86,6 +139,7 @@ type Entry struct { ChainFingerprints []string `json:"chain_fingerprints"` Issuers []IssuerInfo `json:"issuers,omitempty"` SCTs []SCT `json:"scts,omitempty"` + CertDetails *CertDetails `json:"cert_details,omitempty"` ParsedCertInfo json.RawMessage `json:"parsed_cert_info,omitempty"` } @@ -414,11 +468,64 @@ func fetchIssuer(logURL, fingerprint string) (*IssuerInfo, error) { return info, nil } +// parseCertDetails extracts certificate fields not covered by TrimmedEntry. +func parseCertDetails(certDER []byte) *CertDetails { + cert, err := x509.ParseCertificate(certDER) + if err != nil { + return nil + } + + d := &CertDetails{ + NotBefore: cert.NotBefore.UTC().Format(time.RFC3339), + NotAfter: cert.NotAfter.UTC().Format(time.RFC3339), + SerialNumber: cert.SerialNumber.String(), + Issuer: cert.Issuer.String(), + OCSPServers: cert.OCSPServer, + IssuingCertURLs: cert.IssuingCertificateURL, + CRLDistributionPoints: cert.CRLDistributionPoints, + IsCA: cert.IsCA, + } + + for _, e := range cert.EmailAddresses { + d.EmailSANs = append(d.EmailSANs, e) + } + for _, u := range cert.URIs { + d.URISANs = append(d.URISANs, u.String()) + } + if len(cert.SubjectKeyId) > 0 { + d.SubjectKeyID = hex.EncodeToString(cert.SubjectKeyId) + } + if len(cert.AuthorityKeyId) > 0 { + d.AuthorityKeyID = hex.EncodeToString(cert.AuthorityKeyId) + } + for _, ku := range keyUsageNames { + if cert.KeyUsage&ku.usage != 0 { + d.KeyUsage = append(d.KeyUsage, ku.name) + } + } + for _, eku := range cert.ExtKeyUsage { + if name, ok := extKeyUsageNames[eku]; ok { + d.ExtKeyUsage = append(d.ExtKeyUsage, name) + } + } + return d +} + +// merkleLeafHash computes the RFC 6962 Merkle leaf hash: SHA-256(0x00 || leaf). +func merkleLeafHash(leaf []byte) string { + h := sha256.New() + h.Write([]byte{0x00}) + h.Write(leaf) + return hex.EncodeToString(h.Sum(nil)) +} + func convertEntry(e *sunlight.LogEntry, entryNum int, opts Options) Entry { entry := Entry{ EntryNumber: entryNum, LeafIndex: e.LeafIndex, + MerkleLeafHash: merkleLeafHash(e.MerkleTreeLeaf()), Timestamp: e.Timestamp, + TimestampHuman: time.UnixMilli(e.Timestamp).UTC().Format(time.RFC3339), IsPrecert: e.IsPrecert, CertificateSize: len(e.Certificate), } @@ -462,6 +569,15 @@ func convertEntry(e *sunlight.LogEntry, entryNum int, opts Options) Entry { } } + // Parse extended certificate details. + certDER := e.Certificate + if e.IsPrecert { + certDER = e.PreCertificate + } + if len(certDER) > 0 { + entry.CertDetails = parseCertDetails(certDER) + } + // Try to extract parsed certificate info if trimmed, err := e.TrimmedEntry(); err == nil { if data, err := json.Marshal(trimmed); err == nil {