Files
nexus/internal/leviathan/parsers/cert.go
T
2026-06-16 11:51:07 +01:00

197 lines
5.5 KiB
Go

package parsers
import (
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"strings"
"github.com/andydixon/nexus/internal/leviathan/document"
)
// ParseCerts scans the whole document for PEM-encoded certificates
// and for DER-encoded X.509 certificates appearing at arbitrary
// offsets (heuristic: SEQUENCE 0x30 0x82 <len>, then tbsCertificate
// SEQUENCE, then version INTEGER). Returns a list of decoded certs.
func ParseCerts(d *document.Document) (*Field, error) {
size := d.Size()
root := &Field{Name: "Certificates", Type: "container", Offset: 0, Length: size}
// Read whole doc — bounded by caller's judgment. For huge firmware
// blobs this might be expensive but certs are small; most real
// cases are <few MB of text or firmware tables.
if size > 32*1024*1024 {
return nil, errors.New("certs scan skipped: file > 32 MiB")
}
all, err := d.Range(0, size)
if err != nil {
return nil, err
}
// PEM pass.
rest := all
pemCount := 0
var pemOff int64 = 0
for {
block, next := pem.Decode(rest)
if block == nil {
break
}
// Approximate offset: the header "-----BEGIN" will be somewhere in `rest`.
idx := indexOfHeader(rest)
if idx < 0 {
break
}
start := pemOff + int64(idx)
end := pemOff + int64(len(rest)-len(next))
pemCount++
child := &Field{
Name: fmt.Sprintf("PEM %s #%d", block.Type, pemCount),
Type: "pem",
Offset: start,
Length: end - start,
}
if block.Type == "CERTIFICATE" {
if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
populateCert(child, cert)
} else {
child.Note = "x509 parse failed: " + err.Error()
}
}
root.Children = append(root.Children, child)
pemOff += int64(len(rest) - len(next))
rest = next
}
// DER pass — scan for SEQUENCE 0x30 0x82 xx yy (definite long-form, 2 length bytes).
//
// Each candidate runs a full x509.ParseCertificate over a blob up to 64 KiB.
// Without guards a buffer dense with "30 82" byte pairs (random/firmware
// data, or a hostile file) would attempt that parse at nearly every offset
// — O(n) expensive parses, enough to freeze the UI. Two guards keep it
// bounded: a cheap structural pre-filter (a DER certificate body begins
// with the tbsCertificate SEQUENCE, 0x30) rejects almost all false
// candidates without parsing, and a hard cap on parse attempts bounds even
// adversarial input that defeats the pre-filter.
const maxDERAttempts = 1 << 16
derCount := 0
derAttempts := 0
for i := 0; i+4 < len(all); i++ {
if all[i] != 0x30 || all[i+1] != 0x82 {
continue
}
if all[i+4] != 0x30 {
continue
}
totalLen := int(all[i+2])<<8 | int(all[i+3])
if totalLen < 100 || i+4+totalLen > len(all) {
continue
}
if derAttempts++; derAttempts > maxDERAttempts {
break
}
blob := all[i : i+4+totalLen]
cert, err := x509.ParseCertificate(blob)
if err != nil {
continue
}
derCount++
child := &Field{
Name: fmt.Sprintf("DER cert #%d", derCount),
Type: "der",
Offset: int64(i),
Length: int64(4 + totalLen),
}
populateCert(child, cert)
root.Children = append(root.Children, child)
i += 4 + totalLen - 1
}
if len(root.Children) == 0 {
return nil, errors.New("no certificates found")
}
root.Value = fmt.Sprintf("PEM=%d DER=%d", pemCount, derCount)
return root, nil
}
func populateCert(f *Field, c *x509.Certificate) {
f.Value = fmt.Sprintf("subject=%q issuer=%q serial=%x",
c.Subject.String(), c.Issuer.String(), c.SerialNumber)
add := func(name, value string) {
f.Children = append(f.Children, &Field{Name: name, Type: "field", Offset: f.Offset, Length: 0, Value: value})
}
add("subject", c.Subject.String())
add("issuer", c.Issuer.String())
add("serial", fmt.Sprintf("%x", c.SerialNumber))
add("valid_from", c.NotBefore.UTC().Format("2006-01-02 15:04:05 UTC"))
add("valid_until", c.NotAfter.UTC().Format("2006-01-02 15:04:05 UTC"))
add("pubkey_alg", c.PublicKeyAlgorithm.String())
add("sig_alg", c.SignatureAlgorithm.String())
if len(c.DNSNames) > 0 {
add("dns_names", strings.Join(c.DNSNames, ", "))
}
if len(c.IPAddresses) > 0 {
ips := make([]string, 0, len(c.IPAddresses))
for _, ip := range c.IPAddresses {
ips = append(ips, ip.String())
}
add("ip_addresses", strings.Join(ips, ", "))
}
if len(c.EmailAddresses) > 0 {
add("emails", strings.Join(c.EmailAddresses, ", "))
}
if c.IsCA {
add("ca", "true")
}
if c.KeyUsage != 0 {
add("key_usage", fmt.Sprintf("0x%x", int(c.KeyUsage)))
}
if len(c.ExtKeyUsage) > 0 {
parts := make([]string, 0, len(c.ExtKeyUsage))
for _, u := range c.ExtKeyUsage {
parts = append(parts, extKeyUsageName(u))
}
add("ext_key_usage", strings.Join(parts, ", "))
}
// SHA-256 fingerprint of DER
add("sha256_fingerprint", sha256Hex(c.Raw))
}
func extKeyUsageName(u x509.ExtKeyUsage) string {
switch u {
case x509.ExtKeyUsageAny:
return "any"
case x509.ExtKeyUsageServerAuth:
return "serverAuth"
case x509.ExtKeyUsageClientAuth:
return "clientAuth"
case x509.ExtKeyUsageCodeSigning:
return "codeSigning"
case x509.ExtKeyUsageEmailProtection:
return "emailProtection"
case x509.ExtKeyUsageTimeStamping:
return "timeStamping"
case x509.ExtKeyUsageOCSPSigning:
return "OCSPSigning"
}
return fmt.Sprintf("%d", int(u))
}
func sha256Hex(b []byte) string {
h := sha256.Sum256(b)
return fmt.Sprintf("%x", h[:])
}
// indexOfHeader returns the byte offset of the next PEM BEGIN line.
func indexOfHeader(b []byte) int {
const tag = "-----BEGIN"
for i := 0; i+len(tag) <= len(b); i++ {
if string(b[i:i+len(tag)]) == tag {
return i
}
}
return -1
}