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