v3 candidate
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user