322 lines
7.7 KiB
Go
322 lines
7.7 KiB
Go
package parsers
|
|
|
|
import (
|
|
"encoding/asn1"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/andydixon/nexus/internal/leviathan/document"
|
|
)
|
|
|
|
// ParseASN1 produces a field tree for the DER TLV structure starting
|
|
// at `at`, spanning at most `length` bytes. Nested SEQUENCE / SET
|
|
// structures are decoded recursively; primitive content is rendered
|
|
// as best-effort.
|
|
func ParseASN1(d *document.Document, at, length int64) (*Field, error) {
|
|
size := d.Size()
|
|
if length < 0 || at+length > size {
|
|
length = size - at
|
|
}
|
|
if length <= 0 {
|
|
return nil, errors.New("asn1: empty range")
|
|
}
|
|
buf, err := d.Range(at, length)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// If the buffer looks like PEM, decode first.
|
|
if pemBlock, _ := pem.Decode(buf); pemBlock != nil {
|
|
root := &Field{Name: "PEM " + pemBlock.Type, Type: "container", Offset: at, Length: length}
|
|
inner, err := decodeASN1(pemBlock.Bytes, at)
|
|
if err == nil {
|
|
root.Children = append(root.Children, inner)
|
|
} else {
|
|
root.Note = "pem decoded but asn1 failed: " + err.Error()
|
|
}
|
|
return root, nil
|
|
}
|
|
return decodeASN1(buf, at)
|
|
}
|
|
|
|
// decodeASN1 walks buf recursively, treating it as one or more DER TLVs.
|
|
func decodeASN1(buf []byte, base int64) (*Field, error) {
|
|
root := &Field{Name: "ASN.1", Type: "container", Offset: base, Length: int64(len(buf))}
|
|
off := 0
|
|
for off < len(buf) {
|
|
raw, consumed, err := readTLV(buf[off:])
|
|
if err != nil {
|
|
if off == 0 {
|
|
return nil, err
|
|
}
|
|
break
|
|
}
|
|
node := asn1Node(raw, base+int64(off))
|
|
root.Children = append(root.Children, node)
|
|
off += consumed
|
|
}
|
|
if len(root.Children) == 0 {
|
|
return nil, errors.New("asn1: no TLV decoded")
|
|
}
|
|
if len(root.Children) == 1 {
|
|
return root.Children[0], nil
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
// tlv is one DER TLV we've split out of the byte stream.
|
|
type tlv struct {
|
|
Class int // 0=universal, 1=application, 2=context, 3=private
|
|
Constructed bool
|
|
Tag int
|
|
Content []byte // the V of the TLV
|
|
HeaderLen int // length of T+L bytes
|
|
}
|
|
|
|
func readTLV(buf []byte) (tlv, int, error) {
|
|
if len(buf) < 2 {
|
|
return tlv{}, 0, errors.New("short TLV")
|
|
}
|
|
b0 := buf[0]
|
|
t := tlv{
|
|
Class: int(b0 >> 6),
|
|
Constructed: b0&0x20 != 0,
|
|
}
|
|
tagVal := int(b0 & 0x1f)
|
|
i := 1
|
|
if tagVal == 0x1f {
|
|
// multi-byte tag
|
|
tagVal = 0
|
|
for i < len(buf) {
|
|
c := buf[i]
|
|
i++
|
|
tagVal = (tagVal << 7) | int(c&0x7f)
|
|
if c&0x80 == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
t.Tag = tagVal
|
|
if i >= len(buf) {
|
|
return tlv{}, 0, errors.New("truncated length")
|
|
}
|
|
ln := int(buf[i])
|
|
i++
|
|
if ln >= 0x80 {
|
|
n := ln & 0x7f
|
|
if n == 0 || n > 8 || i+n > len(buf) {
|
|
return tlv{}, 0, errors.New("bad length")
|
|
}
|
|
ln = 0
|
|
for k := 0; k < n; k++ {
|
|
ln = (ln << 8) | int(buf[i+k])
|
|
}
|
|
i += n
|
|
}
|
|
if i+ln > len(buf) {
|
|
return tlv{}, 0, errors.New("length exceeds buffer")
|
|
}
|
|
t.HeaderLen = i
|
|
t.Content = buf[i : i+ln]
|
|
return t, i + ln, nil
|
|
}
|
|
|
|
func asn1Node(t tlv, off int64) *Field {
|
|
name, kind := asn1TagName(t)
|
|
total := int64(t.HeaderLen + len(t.Content))
|
|
node := &Field{Name: name, Type: kind, Offset: off, Length: total}
|
|
if t.Class == 2 {
|
|
node.Name = fmt.Sprintf("[%d]", t.Tag)
|
|
}
|
|
if t.Constructed {
|
|
// Recurse into children.
|
|
inner := t.Content
|
|
childOff := off + int64(t.HeaderLen)
|
|
i := 0
|
|
for i < len(inner) {
|
|
child, consumed, err := readTLV(inner[i:])
|
|
if err != nil {
|
|
break
|
|
}
|
|
node.Children = append(node.Children, asn1Node(child, childOff+int64(i)))
|
|
i += consumed
|
|
}
|
|
} else {
|
|
node.Value = renderPrimitive(t)
|
|
}
|
|
return node
|
|
}
|
|
|
|
func asn1TagName(t tlv) (string, string) {
|
|
if t.Class != 0 {
|
|
prefix := []string{"", "APP ", "CTX ", "PRIV "}[t.Class]
|
|
return fmt.Sprintf("%s%d", prefix, t.Tag), "tag"
|
|
}
|
|
names := map[int]string{
|
|
1: "BOOLEAN",
|
|
2: "INTEGER",
|
|
3: "BIT STRING",
|
|
4: "OCTET STRING",
|
|
5: "NULL",
|
|
6: "OBJECT IDENTIFIER",
|
|
10: "ENUMERATED",
|
|
12: "UTF8String",
|
|
16: "SEQUENCE",
|
|
17: "SET",
|
|
19: "PrintableString",
|
|
20: "T61String",
|
|
22: "IA5String",
|
|
23: "UTCTime",
|
|
24: "GeneralizedTime",
|
|
}
|
|
if n, ok := names[t.Tag]; ok {
|
|
return n, strings.ToLower(n)
|
|
}
|
|
return fmt.Sprintf("tag %d", t.Tag), "tag"
|
|
}
|
|
|
|
func renderPrimitive(t tlv) string {
|
|
switch t.Tag {
|
|
case 1:
|
|
if len(t.Content) == 1 {
|
|
if t.Content[0] == 0 {
|
|
return "false"
|
|
}
|
|
return "true"
|
|
}
|
|
case 2:
|
|
return fmt.Sprintf("%s (%d bytes)", bigIntString(t.Content), len(t.Content))
|
|
case 3:
|
|
if len(t.Content) == 0 {
|
|
return "empty"
|
|
}
|
|
return fmt.Sprintf("unused=%d bits=%x", t.Content[0], t.Content[1:])
|
|
case 4:
|
|
if isPrintable(t.Content) {
|
|
return fmt.Sprintf("%q (%d bytes)", string(t.Content), len(t.Content))
|
|
}
|
|
return fmt.Sprintf("%x (%d bytes)", t.Content, len(t.Content))
|
|
case 5:
|
|
return "null"
|
|
case 6:
|
|
oid, err := parseOID(t.Content)
|
|
if err != nil {
|
|
return err.Error()
|
|
}
|
|
return oid + oidNote(oid)
|
|
case 12, 19, 20, 22:
|
|
return fmt.Sprintf("%q", string(t.Content))
|
|
case 23, 24:
|
|
return fmt.Sprintf("%q (time)", string(t.Content))
|
|
}
|
|
if isPrintable(t.Content) {
|
|
return fmt.Sprintf("%q", string(t.Content))
|
|
}
|
|
if len(t.Content) > 32 {
|
|
return fmt.Sprintf("%x… (%d bytes)", t.Content[:32], len(t.Content))
|
|
}
|
|
return fmt.Sprintf("%x", t.Content)
|
|
}
|
|
|
|
func bigIntString(b []byte) string {
|
|
if len(b) == 0 {
|
|
return "0"
|
|
}
|
|
if len(b) <= 8 {
|
|
var v int64
|
|
negative := b[0]&0x80 != 0
|
|
for _, c := range b {
|
|
v = (v << 8) | int64(c)
|
|
}
|
|
if negative {
|
|
// sign-extend
|
|
v -= 1 << uint(8*len(b))
|
|
}
|
|
return fmt.Sprintf("%d", v)
|
|
}
|
|
// large — hex is more useful than a decimal approximation.
|
|
return fmt.Sprintf("0x%x", b)
|
|
}
|
|
|
|
func parseOID(b []byte) (string, error) {
|
|
// Use Go's asn1 parser for correctness.
|
|
var oid asn1.ObjectIdentifier
|
|
rest, err := asn1.Unmarshal(append([]byte{0x06, byte(len(b))}, b...), &oid)
|
|
if err != nil || len(rest) != 0 {
|
|
// Fallback manual decode.
|
|
return manualOID(b), nil
|
|
}
|
|
return oid.String(), nil
|
|
}
|
|
|
|
func manualOID(b []byte) string {
|
|
if len(b) == 0 {
|
|
return ""
|
|
}
|
|
parts := []string{}
|
|
first := int(b[0])
|
|
parts = append(parts, fmt.Sprintf("%d.%d", first/40, first%40))
|
|
v := 0
|
|
for _, c := range b[1:] {
|
|
v = (v << 7) | int(c&0x7f)
|
|
if c&0x80 == 0 {
|
|
parts = append(parts, fmt.Sprintf("%d", v))
|
|
v = 0
|
|
}
|
|
}
|
|
return strings.Join(parts, ".")
|
|
}
|
|
|
|
// Known-OID table for human-readable hints in cert dumps.
|
|
var oidNames = map[string]string{
|
|
"1.2.840.113549.1.1.1": " rsaEncryption",
|
|
"1.2.840.113549.1.1.11": " sha256WithRSAEncryption",
|
|
"1.2.840.113549.1.1.5": " sha1WithRSAEncryption",
|
|
"1.2.840.113549.1.1.13": " sha512WithRSAEncryption",
|
|
"1.2.840.10045.2.1": " ecPublicKey",
|
|
"1.2.840.10045.4.3.2": " ecdsaWithSHA256",
|
|
"1.3.101.112": " Ed25519",
|
|
"2.5.4.3": " commonName",
|
|
"2.5.4.6": " countryName",
|
|
"2.5.4.7": " localityName",
|
|
"2.5.4.8": " stateOrProvinceName",
|
|
"2.5.4.10": " organizationName",
|
|
"2.5.4.11": " organizationalUnitName",
|
|
"2.5.4.5": " serialNumber",
|
|
"1.2.840.113549.1.9.1": " emailAddress",
|
|
"2.5.29.14": " subjectKeyIdentifier",
|
|
"2.5.29.15": " keyUsage",
|
|
"2.5.29.17": " subjectAltName",
|
|
"2.5.29.19": " basicConstraints",
|
|
"2.5.29.31": " cRLDistributionPoints",
|
|
"2.5.29.32": " certificatePolicies",
|
|
"2.5.29.35": " authorityKeyIdentifier",
|
|
"2.5.29.37": " extKeyUsage",
|
|
"1.3.6.1.5.5.7.3.1": " serverAuth",
|
|
"1.3.6.1.5.5.7.3.2": " clientAuth",
|
|
"1.3.6.1.5.5.7.1.1": " authorityInfoAccess",
|
|
}
|
|
|
|
func oidNote(oid string) string {
|
|
if n, ok := oidNames[oid]; ok {
|
|
return n
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isPrintable(b []byte) bool {
|
|
if len(b) == 0 {
|
|
return false
|
|
}
|
|
for _, c := range b {
|
|
if c == 0x09 || c == 0x0a || c == 0x0d {
|
|
continue
|
|
}
|
|
if c < 0x20 || c > 0x7e {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|