asn1 DER reader/writer and X.509 reader

This commit is contained in:
kalsprite
2026-06-13 23:40:08 -07:00
parent 47cf0d3f42
commit 4876cf03bf
54 changed files with 4957 additions and 7 deletions

View File

@@ -0,0 +1,72 @@
package asn1
// Tag class from the identifier octet (X.690 section 8.1.2.2).
Class :: enum u8 {
Universal = 0,
Application = 1,
Context_Specific = 2,
Private = 3,
}
// Tag_Number enumerates the UNIVERSAL tag numbers relevant to DER as
// used by PKIX. Context-specific tags carry their number directly in
// Tag.number.
Tag_Number :: enum u32 {
Boolean = 1,
Integer = 2,
Bit_String = 3,
Octet_String = 4,
Null = 5,
Object_Identifier = 6,
Enumerated = 10,
UTF8_String = 12,
Sequence = 16,
Set = 17,
Numeric_String = 18,
Printable_String = 19,
Teletex_String = 20,
IA5_String = 22,
UTC_Time = 23,
Generalized_Time = 24,
Visible_String = 26,
BMP_String = 30,
}
// Tag is a decoded identifier octet (plus high-tag-number form).
Tag :: struct {
class: Class,
constructed: bool,
number: u32,
}
Error :: enum {
None,
Truncated, // The element (or its header) extends past the end of the input.
Invalid_Tag, // Malformed identifier octets (non-minimal high-tag-number form, or a tag number that overflows u32).
Invalid_Length, // Indefinite length, non-minimal length encoding, or a length beyond what this implementation supports.
Unexpected_Tag, // An expect_*/read_* procedure found a different tag than required.
Invalid_Boolean, // BOOLEAN content was not exactly one octet of 0x00 or 0xFF.
Invalid_Integer, // INTEGER content was empty or not minimally encoded.
Integer_Overflow, // INTEGER does not fit the requested machine type.
Negative_Integer, // INTEGER was negative where an unsigned value is required.
Invalid_Bit_String, // BIT STRING content violated X.690 sections 8.6/11.2 (bad unused-bit count, non-zero padding bits, or unused bits where none are permitted).
Invalid_Null, // NULL with non-empty content.
Invalid_Object_Identifier, // OBJECT IDENTIFIER content was empty or not minimally encoded.
Invalid_Time, // UTCTime/GeneralizedTime outside the RFC 5280 DER profile (YYMMDDHHMMSSZ / YYYYMMDDHHMMSSZ, Zulu only, seconds present, no fractional seconds), or an impossible date.
Leftover_Bytes, // done() was called with input remaining.
// An OBJECT IDENTIFIER arc exceeds u64. Arc magnitude is unbounded per X.660 (e.g. 2.25 UUID-derived OIDs carry a 128-bit arc), so
// this is a representation limit of oid_components/oid_to_string, not a malformed input; compare such OIDs by their raw bytes.
Arc_Overflow,
Allocation_Failed, // OOM
Buffer_Too_Small, // encode: the destination slice is shorter than encoded_len.
}
// universal builds a UNIVERSAL-class tag.
universal :: proc "contextless" (number: Tag_Number, constructed := false) -> Tag {
return Tag{class = .Universal, constructed = constructed, number = u32(number)}
}
// context_specific builds a CONTEXT-SPECIFIC-class tag ("[n]").
context_specific :: proc "contextless" (number: u32, constructed := true) -> Tag {
return Tag{class = .Context_Specific, constructed = constructed, number = number}
}

View File

@@ -0,0 +1,624 @@
package asn1
import "core:strings"
import dt "core:time"
// Cursor is a position into DER input, advanced by the read_* procs.
Cursor :: struct {
data: []byte,
pos: int,
}
// Points a Cursor at `data` and rewinds it to the start.
cursor_init :: proc "contextless" (r: ^Cursor, data: []byte) {
r.data = data
r.pos = 0
}
// Returns the number of unconsumed bytes.
remaining :: proc "contextless" (r: ^Cursor) -> int {
return len(r.data) - r.pos
}
// Reports whether the Cursor has been fully consumed.
is_empty :: proc "contextless" (r: ^Cursor) -> bool {
return r.pos >= len(r.data)
}
// Returns Leftover_Bytes if input remains. DER structures are exact: every SEQUENCE walk should end with done() on its sub-cursor.
done :: proc "contextless" (r: ^Cursor) -> Error {
if r.pos < len(r.data) {
return .Leftover_Bytes
}
return .None
}
// Reads one complete TLV element of any tag, returning the tag and a view of the content octets.
read_any :: proc "contextless" (r: ^Cursor) -> (tag: Tag, content: []byte, err: Error) {
tag, err = _read_tag(r)
if err != .None {
return
}
length: int
length, err = _read_length(r)
if err != .None {
return
}
if length > remaining(r) {
err = .Truncated
return
}
content = r.data[r.pos:r.pos + length]
r.pos += length
return
}
// Decodes the next element's tag without consuming anything.
peek_tag :: proc "contextless" (r: ^Cursor) -> (tag: Tag, err: Error) {
tmp := r^
return _read_tag(&tmp)
}
// Consumes one complete element of any tag.
skip :: proc "contextless" (r: ^Cursor) -> Error {
_, _, err := read_any(r)
return err
}
// Reads one element and requires its tag to match exactly.
expect :: proc "contextless" (r: ^Cursor, tag: Tag) -> (content: []byte, err: Error) {
got: Tag
got, content, err = read_any(r)
if err != .None {
return
}
if got != tag {
err = .Unexpected_Tag
}
return
}
// Enters a SEQUENCE, returning a sub-cursor over its content.
read_sequence :: proc "contextless" (r: ^Cursor) -> (seq: Cursor, err: Error) {
content, eerr := expect(r, universal(.Sequence, true))
if eerr != .None {
return {}, eerr
}
return Cursor{data = content}, .None
}
// Enters a SET, returning a sub-cursor over its content.
// NOTE: DER requires SET OF contents to be sorted; this cursor does
// not verify ordering, consumers that care (none in PKIX cert parsing) must check.
read_set :: proc "contextless" (r: ^Cursor) -> (set: Cursor, err: Error) {
content, eerr := expect(r, universal(.Set, true))
if eerr != .None {
return {}, eerr
}
return Cursor{data = content}, .None
}
// Handles `[number] EXPLICIT ... OPTIONAL`: if the next element is the given
// constructed context-specific tag, it is consumed and a sub-cursor over its
// content returned with present=true. Otherwise nothing is consumed.
read_explicit :: proc "contextless" (r: ^Cursor, number: u32) -> (inner: Cursor, present: bool, err: Error) {
if is_empty(r) {
return {}, false, .None
}
tag, perr := peek_tag(r)
if perr != .None {
return {}, false, perr
}
if tag != context_specific(number, true) {
return {}, false, .None
}
content, eerr := expect(r, tag)
if eerr != .None {
return {}, false, eerr
}
return Cursor{data = content}, true, .None
}
// Reads a BOOLEAN. DER: exactly one octet, 0x00 or 0xFF.
read_boolean :: proc "contextless" (r: ^Cursor) -> (value: bool, err: Error) {
content, eerr := expect(r, universal(.Boolean))
if eerr != .None {
return false, eerr
}
if len(content) != 1 {
return false, .Invalid_Boolean
}
switch content[0] {
case 0x00:
return false, .None
case 0xFF:
return true, .None
}
return false, .Invalid_Boolean
}
// Reads an INTEGER and returns the validated, minimally-encoded two's-complement content octets.
read_integer_bytes :: proc "contextless" (r: ^Cursor) -> (content: []byte, err: Error) {
content, err = expect(r, universal(.Integer))
if err != .None {
return
}
err = _check_integer(content)
return
}
// Reads an INTEGER that must fit in an i64.
read_i64 :: proc "contextless" (r: ^Cursor) -> (value: i64, err: Error) {
content, ierr := read_integer_bytes(r)
if ierr != .None {
return 0, ierr
}
if len(content) > 8 {
return 0, .Integer_Overflow
}
if content[0] & 0x80 != 0 {
value = -1 // sign-extend
}
for b in content {
value = value << 8 | i64(b)
}
return value, .None
}
// Reads a non-negative INTEGER and returns its magnitude octets with any leading 0x00
// sign octet stripped, the shape RSA moduli, public exponents, and certificate serials
// are consumed in.
read_unsigned_integer_bytes :: proc "contextless" (r: ^Cursor) -> (magnitude: []byte, err: Error) {
content, ierr := read_integer_bytes(r)
if ierr != .None {
return nil, ierr
}
if content[0] & 0x80 != 0 {
return nil, .Negative_Integer
}
if len(content) > 1 && content[0] == 0x00 {
content = content[1:]
}
return content, .None
}
// Reads a BIT STRING, returning the payload octets and the count of unused trailing bits
// in the final octet. DER: primitive form only, unused count 0..7 (0 if the payload is
// empty), and the unused bits themselves must be zero.
read_bit_string :: proc "contextless" (r: ^Cursor) -> (bits: []byte, unused: int, err: Error) {
content, eerr := expect(r, universal(.Bit_String))
if eerr != .None {
return nil, 0, eerr
}
if len(content) < 1 {
return nil, 0, .Invalid_Bit_String
}
unused = int(content[0])
bits = content[1:]
if unused > 7 {
return nil, 0, .Invalid_Bit_String
}
if len(bits) == 0 && unused != 0 {
return nil, 0, .Invalid_Bit_String
}
if unused > 0 {
mask := byte(1 << uint(unused)) - 1
if bits[len(bits) - 1] & mask != 0 {
return nil, 0, .Invalid_Bit_String
}
}
return bits, unused, .None
}
// Reads a BIT STRING that must be a whole number of octets (unused == 0),
// the only form PKIX uses for SubjectPublicKeyInfo keys and signature values.
read_bit_string_octets :: proc "contextless" (r: ^Cursor) -> (octets: []byte, err: Error) {
bits, unused, berr := read_bit_string(r)
if berr != .None {
return nil, berr
}
if unused != 0 {
return nil, .Invalid_Bit_String
}
return bits, .None
}
// Reads an OCTET STRING (primitive form only).
read_octet_string :: proc "contextless" (r: ^Cursor) -> (octets: []byte, err: Error) {
return expect(r, universal(.Octet_String))
}
// Reads a NULL (content must be empty).
read_null :: proc "contextless" (r: ^Cursor) -> Error {
content, err := expect(r, universal(.Null))
if err != .None {
return err
}
if len(content) != 0 {
return .Invalid_Null
}
return .None
}
// Reads an OBJECT IDENTIFIER and returns a view of its content octets,
// validated for minimal base-128 encoding. The validation is structural
// only: arc magnitude is unbounded per X.660, so PKIX consumers should
// compare these bytes directly against known-OID constants.
// oid_components/oid_to_string decode arcs when needed, reporting
// Arc_Overflow for arcs beyond u64.
read_oid :: proc "contextless" (r: ^Cursor) -> (raw: []byte, err: Error) {
raw, err = expect(r, universal(.Object_Identifier))
if err != .None {
return
}
if len(raw) == 0 {
return nil, .Invalid_Object_Identifier
}
// Validate: each subidentifier is base-128 with minimal encoding
// (no 0x80 lead octet) and terminates (last octet has bit 8 clear).
expect_start := true
for b in raw {
if expect_start && b == 0x80 {
return nil, .Invalid_Object_Identifier
}
expect_start = b & 0x80 == 0
}
if !expect_start {
return nil, .Invalid_Object_Identifier
}
return raw, .None
}
// Times are returned as core:time.Time. time.Time is i64 nanoseconds
// and so tops out near year 2262, while UTCTime/GeneralizedTime reach
// year 9999; dates beyond what time.Time can hold (notably RFC 5280's
// "99991231235959Z" no-well-defined-expiration sentinel) saturate to
// time.Time's bound rather than erroring, so a far-future cert still
// parses and reads as "effectively never expires". See _time_from_unix.
// read_utc_time reads a UTCTime in the RFC 5280 DER profile:
// "YYMMDDHHMMSSZ", with the sliding century window (00-49 → 20xx,
// 50-99 → 19xx).
read_utc_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
content, eerr := expect(r, universal(.UTC_Time))
if eerr != .None {
return {}, eerr
}
if len(content) != 13 || content[12] != 'Z' {
return {}, .Invalid_Time
}
yy, ok := _two_digits(content[0:2])
if !ok {
return {}, .Invalid_Time
}
year := 2000 + yy
if yy >= 50 {
year = 1900 + yy
}
secs := _unix_from_fields(year, content[2:12]) or_return
return _time_from_unix(secs), .None
}
// Reads a GeneralizedTime in the RFC 5280 DER profile: "YYYYMMDDHHMMSSZ", Zulu only, no fractional seconds.
read_generalized_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
content, eerr := expect(r, universal(.Generalized_Time))
if eerr != .None {
return {}, eerr
}
if len(content) != 15 || content[14] != 'Z' {
return {}, .Invalid_Time
}
hi, ok1 := _two_digits(content[0:2])
lo, ok2 := _two_digits(content[2:4])
if !ok1 || !ok2 {
return {}, .Invalid_Time
}
secs := _unix_from_fields(hi * 100 + lo, content[4:14]) or_return
return _time_from_unix(secs), .None
}
// Reads either time form, PKIX Validity uses UTCTime for dates through 2049 and GeneralizedTime from 2050 on.
read_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
tag, perr := peek_tag(r)
if perr != .None {
return {}, perr
}
if tag == universal(.Generalized_Time) {
return read_generalized_time(r)
}
return read_utc_time(r)
}
// OBJECT IDENTIFIER helpers (allocating).
// Decodes validated OID content octets (from read_oid) into their integer arcs,
// e.g. {1, 2, 840, 113549, 1, 1, 1}. Arcs beyond u64 (legal per X.660, see
// Arc_Overflow) are not representable; compare such OIDs by their raw bytes instead.
oid_components :: proc(raw: []byte, allocator := context.allocator) -> (arcs: []u64, err: Error) {
if len(raw) == 0 {
return nil, .Invalid_Object_Identifier
}
count := 1 // the first octet encodes two arcs
for b in raw {
if b & 0x80 == 0 {
count += 1
}
}
out, merr := make([]u64, count, allocator)
if merr != nil {
return nil, .Allocation_Failed
}
idx := 0
acc: u64 = 0
first := true
for b in raw {
if acc > max(u64) >> 7 {
delete(out, allocator)
return nil, .Arc_Overflow
}
acc = acc << 7 | u64(b & 0x7F)
if b & 0x80 != 0 {
continue
}
if first {
// X.690 section 8.19.4: the first subidentifier encodes the first
// two arcs as arc1*40 + arc2 (arc1 limited to 0..2; arc2
// unbounded only when arc1 == 2).
switch {
case acc < 40:
out[idx] = 0
out[idx + 1] = acc
case acc < 80:
out[idx] = 1
out[idx + 1] = acc - 40
case:
out[idx] = 2
out[idx + 1] = acc - 80
}
idx += 2
first = false
} else {
out[idx] = acc
idx += 1
}
acc = 0
}
return out, .None
}
// Renders OID content octets in dotted-decimal form ("1.2.840.113549.1.1.1") for diagnostics.
// The arcs are streamed directly into the result; the only allocation is the returned string.
oid_to_string :: proc(raw: []byte, allocator := context.allocator) -> (str: string, err: Error) {
if len(raw) == 0 {
return "", .Invalid_Object_Identifier
}
sb: strings.Builder
if _, berr := strings.builder_init(&sb, allocator); berr != nil {
return "", .Allocation_Failed
}
defer if err != .None {
strings.builder_destroy(&sb)
}
// Builder writes swallow allocator failures, so tally the written
// vs expected lengths and treat any shortfall as an allocation
// failure (the same defense pem.encode uses).
written, expected := 0, 0
acc: u64 = 0
first := true
for b in raw {
if acc > max(u64) >> 7 {
err = .Arc_Overflow
return "", err
}
acc = acc << 7 | u64(b & 0x7F)
if b & 0x80 != 0 {
continue
}
if first {
// See oid_components for the X.690 section 8.19.4 split of the first subidentifier.
arc1, arc2: u64
switch {
case acc < 40:
arc1, arc2 = 0, acc
case acc < 80:
arc1, arc2 = 1, acc - 40
case:
arc1, arc2 = 2, acc - 80
}
written += strings.write_u64(&sb, arc1)
written += strings.write_byte(&sb, '.')
written += strings.write_u64(&sb, arc2)
expected += _decimal_len(arc1) + 1 + _decimal_len(arc2)
first = false
} else {
written += strings.write_byte(&sb, '.')
written += strings.write_u64(&sb, acc)
expected += 1 + _decimal_len(acc)
}
acc = 0
}
if written != expected {
err = .Allocation_Failed
return "", err
}
return strings.to_string(sb), .None
}
@(private)
_decimal_len :: proc "contextless" (v: u64) -> (n: int) {
n = 1
x := v
for x >= 10 {
x /= 10
n += 1
}
return n
}
@(private)
_read_tag :: proc "contextless" (r: ^Cursor) -> (tag: Tag, err: Error) {
if is_empty(r) {
return {}, .Truncated
}
b := r.data[r.pos]
r.pos += 1
tag.class = Class(b >> 6)
tag.constructed = b & 0x20 != 0
number := u32(b & 0x1F)
if number != 0x1F {
tag.number = number
return tag, .None
}
// High-tag-number form (X.690 section 8.1.2.4): base-128, minimal (first
// octet may not be 0x80), and the resulting number must be >= 31.
number = 0
for i := 0; ; i += 1 {
if is_empty(r) {
return {}, .Truncated
}
nb := r.data[r.pos]
r.pos += 1
if i == 0 && nb == 0x80 {
return {}, .Invalid_Tag
}
if number > (max(u32) >> 7) {
return {}, .Invalid_Tag
}
number = number << 7 | u32(nb & 0x7F)
if nb & 0x80 == 0 {
break
}
}
if number < 0x1F {
return {}, .Invalid_Tag
}
tag.number = number
return tag, .None
}
@(private)
_read_length :: proc "contextless" (r: ^Cursor) -> (length: int, err: Error) {
if is_empty(r) {
return 0, .Truncated
}
b := r.data[r.pos]
r.pos += 1
if b & 0x80 == 0 {
return int(b), .None
}
n := int(b & 0x7F)
if n == 0 {
// 0x80: indefinite length, BER only.
return 0, .Invalid_Length
}
if n > 4 {
// Lengths beyond 2^31 are not plausible inputs here.
return 0, .Invalid_Length
}
if remaining(r) < n {
return 0, .Truncated
}
value := 0
for i in 0 ..< n {
value = value << 8 | int(r.data[r.pos + i])
}
r.pos += n
// DER minimality: no leading zero octet, and the long form may only be used for lengths >= 128.
if r.data[r.pos - n] == 0 || value < 0x80 {
return 0, .Invalid_Length
}
if value < 0 {
return 0, .Invalid_Length
}
return value, .None
}
// Enforces X.690 section 8.3: at least one octet, and minimal (the first nine bits may not be all-zero or all-one).
@(private)
_check_integer :: proc "contextless" (content: []byte) -> Error {
switch len(content) {
case 0:
return .Invalid_Integer
case 1:
return .None
}
if content[0] == 0x00 && content[1] & 0x80 == 0 {
return .Invalid_Integer
}
if content[0] == 0xFF && content[1] & 0x80 != 0 {
return .Invalid_Integer
}
return .None
}
@(private)
_two_digits :: proc "contextless" (b: []byte) -> (value: int, ok: bool) {
if b[0] < '0' || b[0] > '9' || b[1] < '0' || b[1] > '9' {
return 0, false
}
return int(b[0] - '0') * 10 + int(b[1] - '0'), true
}
// Converts Unix seconds to a time.Time, saturating at time.Time's
// representable bounds. time.Time counts i64 nanoseconds, so it tops
// out near year 2262; a far-future X.509 date (notably RFC 5280's
// "99991231235959Z" no-expiration sentinel) saturates to that bound
// rather than overflowing, and so reads as "effectively never".
@(private)
_time_from_unix :: proc "contextless" (secs: i64) -> dt.Time {
NS_PER_SEC :: i64(1_000_000_000)
if secs > max(i64) / NS_PER_SEC {
return dt.Time{_nsec = max(i64)}
}
if secs < min(i64) / NS_PER_SEC {
return dt.Time{_nsec = min(i64)}
}
return dt.Time{_nsec = secs * NS_PER_SEC}
}
// Converts a year plus "MMDDHHMMSS" into seconds since the Unix epoch,
// validating field ranges. Computed directly (via the civil-date algorithm
// below) rather than through time.Time so the whole year 1..9999 range is
// computable before _time_from_unix decides how to represent it.
@(private)
_unix_from_fields :: proc "contextless" (year: int, fields: []byte) -> (unix_seconds: i64, err: Error) {
month, mo_ok := _two_digits(fields[0:2])
day, d_ok := _two_digits(fields[2:4])
hour, h_ok := _two_digits(fields[4:6])
minute, min_ok := _two_digits(fields[6:8])
second, s_ok := _two_digits(fields[8:10])
if !mo_ok || !d_ok || !h_ok || !min_ok || !s_ok {
return 0, .Invalid_Time
}
if month < 1 || month > 12 || day < 1 || day > 31 {
return 0, .Invalid_Time
}
if hour > 23 || minute > 59 || second > 59 {
return 0, .Invalid_Time
}
days := _days_from_civil(i64(year), month, day)
return days * 86400 + i64(hour) * 3600 + i64(minute) * 60 + i64(second), .None
}
// Returns the number of days since 1970-01-01 for a proleptic-Gregorian date
// (Ref: http://howardhinnant.github.io/date_algorithms.html#days_from_civil).
// Exact for any representable year; no epoch-range limit.
@(private)
_days_from_civil :: proc "contextless" (y: i64, m, d: int) -> i64 {
yy := y - (m <= 2 ? 1 : 0)
era := (yy >= 0 ? yy : yy - 399) / 400
yoe := yy - era * 400 // [0, 399]
doy := i64((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1) // [0, 365]
doe := yoe * 365 + yoe / 4 - yoe / 100 + doy // [0, 146096]
return era * 146097 + doe - 719468
}

View File

@@ -0,0 +1,29 @@
/*
Strict DER (Distinguished Encoding Rules) reader and writer for the PKIX
subset of ASN.1, the substrate for X.509 certificates and related structures.
Reader: a `Cursor` over the input; `read_*` procs return VIEWS into it (only
`oid_components` / `oid_to_string` take an allocator). The input must outlive
the results.
Writer: build a declarative tree of `Value` nodes with the constructors, then
`encoded_len` + `encode` (no allocation, into a caller buffer) or `marshal`
(one allocation). Constructors BORROW their inputs, so build and encode
within one expression (or back children with a slice that outlives the call);
the encoded output is self-contained and aliases nothing.
Scope & limitations:
- DER only (no BER/CER); strict (minimal lengths, minimal integers, ...).
- PKIX subset: no typed readers for STRING/REAL/... (walk with `read_any`);
the writer emits low-tag-number identifiers only (tag number <= 30).
Times use core:time.Time per the RFC 5280 DER profile (Zulu, seconds present,
no fractional seconds). time.Time is i64 nanoseconds (tops out near year 2262)
while UTCTime/GeneralizedTime reach 9999; on read, dates beyond that saturate.
See:
- [[ https://www.itu.int/rec/T-REC-X.690 ]]
- [[ https://www.rfc-editor.org/rfc/rfc5280 ]]
*/
package asn1

View File

@@ -0,0 +1,498 @@
package asn1
/*
DER (Distinguished Encoding Rules) writer, the inverse of the cursor
reader, and the encoding substrate for signatures, keys, and certificates.
The model is declarative: build a tree of `Value` nodes with the
constructors below, then turn it into bytes with `encoded_len` + `encode`
(no allocation, into a caller buffer) or `marshal` (one allocation, owned
slice). A SEQUENCE/SET simply holds its children, so length is discovered
by a measure pass rather than back-patched, and `set` can sort its
children (DER SET OF ordering) without disturbing this surface, both are
additive.
Zero-copy, with a lifetime caveat: the constructors BORROW their byte and
child inputs (no copies), so a Value tree is only valid while those inputs
live. In practice build and encode the tree within one expression, or back
its children with a slice/array that outlives the encode call:
// one expression (inputs r, s outlive the call):
out := marshal(sequence({integer_unsigned(r), integer_unsigned(s)})) or_return
DER is canonical by construction: definite minimal-length headers, minimal
INTEGER magnitudes with the sign octet inserted only when required. The
writer emits low-tag-number identifiers only (tag number <= 30), which
covers all of PKIX; high-tag-number form is a future addition.
See:
- [[ https://www.itu.int/rec/T-REC-X.690 ]]
*/
import dt "core:time"
// Selects how a Value's content octets are produced when encoding.
@(private)
_Form :: enum u8 {
Primitive, // content holds the exact content octets, emitted verbatim.
Constructed, // children holds the sub-values, emitted in order.
Integer_Magnitude, // content holds an unsigned big-endian magnitude; DER INTEGER rules are applied on emit.
Bit_String_Octets, // content holds whole-octet payload; a leading 0x00 unused-bits count is added on emit.
Bit_String_Wrapped, // children's DER becomes the payload, behind the 0x00 unused-bits count.
Time, // _when is formatted to UTCTime/GeneralizedTime per the tag on emit.
Raw, // content is a complete pre-encoded element, emitted verbatim (no added tag/length).
}
// Value is a node in a to-be-encoded DER tree. Construct it with the
// helpers below rather than by hand; the fields are an implementation
// detail. The byte/child inputs are borrowed (see the package lifetime note).
Value :: struct {
tag: Tag,
form: _Form,
content: []byte,
children: []Value,
_when: dt.Time, // meaningful only when form == .Time: the instant to format on emit
}
@(rodata, private)
_BOOL_FALSE := []byte{0x00}
@(rodata, private)
_BOOL_TRUE := []byte{0xFF}
// Builds a primitive value with `tag` and the exact `content`
// octets (emitted verbatim). The caller owns canonical-form correctness.
primitive :: proc "contextless" (tag: Tag, content: []byte) -> Value {
return Value{tag = tag, form = .Primitive, content = content}
}
// Builds a value whose encoding IS `encoded` verbatim, a complete
// already-DER-encoded element spliced in as-is (no tag/length added). The
// composition primitive for nesting an independently-marshalled structure
// (a signed CertificationRequestInfo, a pre-built TBSCertificate) inside a
// parent without re-encoding it.
raw :: proc "contextless" (encoded: []byte) -> Value {
return Value{form = .Raw, content = encoded}
}
// Builds a BOOLEAN (DER: 0x00 / 0xFF).
boolean :: proc "contextless" (v: bool) -> Value {
return Value{tag = universal(.Boolean), form = .Primitive, content = v ? _BOOL_TRUE : _BOOL_FALSE}
}
// Builds an INTEGER from an unsigned big-endian magnitude: leading zero
// octets are dropped (minimal encoding) and a single 0x00 sign octet is
// inserted when the top bit would otherwise read as negative. An empty
// or all-zero magnitude encodes as 0. This is the shape RSA moduli /
// exponents and certificate serials are written in, and the inverse of
// read_unsigned_integer_bytes.
integer_unsigned :: proc "contextless" (magnitude: []byte) -> Value {
return Value{tag = universal(.Integer), form = .Integer_Magnitude, content = magnitude}
}
// Builds an INTEGER from content octets that are ALREADY a minimal
// two's-complement encoding (e.g. a serial preserved verbatim from
// read_integer_bytes). No normalization is applied.
integer_raw :: proc "contextless" (content: []byte) -> Value {
return Value{tag = universal(.Integer), form = .Primitive, content = content}
}
// Builds an OCTET STRING wrapping `content`.
octet_string :: proc "contextless" (content: []byte) -> Value {
return Value{tag = universal(.Octet_String), form = .Primitive, content = content}
}
// Builds an OCTET STRING whose content is the DER encoding of `children`,
// the form X.509 Extension.extnValue uses to carry an extension's value. The
// OCTET STRING stays primitive (0x04); its content is just the children's
// concatenated DER, so this reuses the constructed-content machinery without
// a wrapper octet.
octet_string_wrap :: proc "contextless" (children: []Value) -> Value {
return Value{tag = universal(.Octet_String), form = .Constructed, children = children}
}
// Builds a SEQUENCE from its sub-values (borrowed).
sequence :: proc "contextless" (children: []Value) -> Value {
return Value{tag = universal(.Sequence, true), form = .Constructed, children = children}
}
// Builds a SET from its sub-values, emitted in the given order. DER SET OF
// requires components sorted by their encoding (X.690 section 11.6); this
// constructor does NOT sort, so a SET OF with more than one element must be
// given pre-sorted (a single-element RDN, the common PKIX case, is trivially
// ordered).
set :: proc "contextless" (children: []Value) -> Value {
return Value{tag = universal(.Set, true), form = .Constructed, children = children}
}
// Builds a SET OF from its sub-values, sorted into the DER canonical order
// (X.690 section 11.6: component encodings ascending, shorter padded with
// trailing 0-octets). Unlike the other constructors this one ALLOCATES, it
// must encode each child to compare them, and it sorts `children` in place;
// the returned Value then borrows that reordered slice as usual. Scratch is
// taken from and released to `allocator`. 0/1-element sets need no work.
@(require_results)
set_of :: proc(children: []Value, allocator := context.allocator) -> (value: Value, err: Error) {
n := len(children)
if n <= 1 {
return set(children), .None
}
encs, merr := make([][]byte, n, allocator)
if merr != nil {
return {}, .Allocation_Failed
}
defer {
for e in encs {
delete(e, allocator)
}
delete(encs, allocator)
}
for i in 0 ..< n {
encs[i] = marshal(children[i], allocator) or_return
}
// Insertion sort (n is small for a SET OF) keying children on their encodings.
for i in 1 ..< n {
for j := i; j > 0 && _der_less(encs[j], encs[j - 1]); j -= 1 {
encs[j], encs[j - 1] = encs[j - 1], encs[j]
children[j], children[j - 1] = children[j - 1], children[j]
}
}
return set(children), .None
}
// Compares two encodings as octet strings with the shorter padded at its
// trailing end with 0-octets (X.690 section 11.6 SET OF ordering).
@(private)
_der_less :: proc "contextless" (a, b: []byte) -> bool {
n := min(len(a), len(b))
for i in 0 ..< n {
if a[i] != b[i] {
return a[i] < b[i]
}
}
if len(a) < len(b) {
for i in n ..< len(b) {
if b[i] != 0x00 {
return true // a's zero padding is below b's non-zero tail
}
}
return false
}
for i in n ..< len(a) {
if a[i] != 0x00 {
return false // a's non-zero tail is above b's zero padding
}
}
return false
}
// Builds a NULL (empty content).
null :: proc "contextless" () -> Value {
return Value{tag = universal(.Null), form = .Primitive, content = nil}
}
// Builds an OBJECT IDENTIFIER from already-encoded content octets (the form
// PKIX OIDs are held and compared in, e.g. the package's known-OID
// constants). The content is emitted verbatim; the caller owns its validity.
object_identifier :: proc "contextless" (content: []byte) -> Value {
return Value{tag = universal(.Object_Identifier), form = .Primitive, content = content}
}
// Builds a BIT STRING from a whole-octet payload (unused-bits count 0), the
// only form PKIX uses for SubjectPublicKeyInfo keys and signature values.
bit_string_octets :: proc "contextless" (payload: []byte) -> Value {
return Value{tag = universal(.Bit_String), form = .Bit_String_Octets, content = payload}
}
// Builds a BIT STRING (whole octets) whose payload is the DER encoding of
// `children`, the form SubjectPublicKeyInfo uses to carry an RSAPublicKey
// SEQUENCE inside the subjectPublicKey bit string.
bit_string_wrap :: proc "contextless" (children: []Value) -> Value {
return Value{tag = universal(.Bit_String), form = .Bit_String_Wrapped, children = children}
}
// Builds a UTCTime ("YYMMDDHHMMSSZ", RFC 5280 DER profile). Appropriate for
// instants in 1950..=2049; the sliding-window century is what the reader
// (read_utc_time) decodes, so use generalized_time outside that range.
utc_time :: proc "contextless" (at: dt.Time) -> Value {
return Value{tag = universal(.UTC_Time), form = .Time, _when = at}
}
// Builds a GeneralizedTime ("YYYYMMDDHHMMSSZ", RFC 5280 DER profile: Zulu,
// seconds present, no fractional part). The inverse of read_generalized_time.
generalized_time :: proc "contextless" (at: dt.Time) -> Value {
return Value{tag = universal(.Generalized_Time), form = .Time, _when = at}
}
// 2050-01-01T00:00:00Z: the RFC 5280 boundary between the two time forms.
@(private)
_UNIX_2050 :: i64(2_524_608_000)
// Builds a UTCTime or GeneralizedTime, auto-selecting the form per the RFC
// 5280 profile: UTCTime for instants in 1950..=2049, GeneralizedTime from
// 2050 on. (X.509 validity dates are written this way.)
time :: proc "contextless" (at: dt.Time) -> Value {
if dt.to_unix_seconds(at) < _UNIX_2050 {
return utc_time(at)
}
return generalized_time(at)
}
// Builds a primitive [number] IMPLICIT value carrying raw content octets,
// e.g. AuthorityKeyIdentifier's keyIdentifier [0] IMPLICIT OCTET STRING.
context_primitive :: proc "contextless" (number: u32, content: []byte) -> Value {
return Value{tag = context_specific(number, false), form = .Primitive, content = content}
}
// Builds a constructed [number] EXPLICIT wrapper around the given sub-values,
// e.g. TBSCertificate's version [0] EXPLICIT INTEGER.
context_explicit :: proc "contextless" (number: u32, children: []Value) -> Value {
return Value{tag = context_specific(number, true), form = .Constructed, children = children}
}
// Returns the exact number of bytes encode/marshal will write
// for `v`, including its identifier and length octets.
encoded_len :: proc(v: Value) -> int {
if v.form == .Raw {
return len(v.content) // already a complete element
}
clen := _content_len(v)
return _tag_len(v.tag) + _length_len(clen) + clen
}
// Writes the DER encoding of `v` into `dst` and returns the number
// of bytes written. `dst` must be at least encoded_len(v) bytes; if it is
// shorter, nothing is written and Buffer_Too_Small is returned.
encode :: proc(v: Value, dst: []byte) -> (n: int, err: Error) {
need := encoded_len(v)
if len(dst) < need {
return 0, .Buffer_Too_Small
}
_emit(v, dst[:need])
return need, .None
}
// Encodes `v` into a freshly allocated slice the caller owns.
marshal :: proc(v: Value, allocator := context.allocator) -> (out: []byte, err: Error) {
n := encoded_len(v)
buf, merr := make([]byte, n, allocator)
if merr != nil {
return nil, .Allocation_Failed
}
_emit(v, buf)
return buf, .None
}
@(private)
_content_len :: proc(v: Value) -> int {
switch v.form {
case .Primitive:
return len(v.content)
case .Integer_Magnitude:
start, pad := _int_shape(v.content)
if start == len(v.content) {
return 1 // zero encodes as a single 0x00 octet
}
return (len(v.content) - start) + (pad ? 1 : 0)
case .Bit_String_Octets:
return 1 + len(v.content) // leading unused-bits octet (0x00)
case .Bit_String_Wrapped:
total := 1 // leading unused-bits octet (0x00)
for child in v.children {
total += encoded_len(child)
}
return total
case .Time:
return _time_content_len(v.tag)
case .Constructed:
total := 0
for child in v.children {
total += encoded_len(child)
}
return total
case .Raw:
return len(v.content) // handled in encoded_len; here for exhaustiveness
}
return 0
}
// Writes v's complete encoding so that it ENDS at dst[len(dst)], i.e. into
// the tail of dst, and returns the bytes written. `dst` is the exactly-sized
// region this node may occupy (encoded_len(v) == len(dst) at the top call).
//
// Emitting back-to-front means a constructed node's content length falls out
// of where its children landed (no second measure pass): encoded_len does the
// single O(n) sizing pass, this does the single O(n) write pass. DER wants to
// be written this way — it is the tree generalization of the fixed-buffer
// trick in crypto/ecdsa's hand-rolled encoder.
@(private)
_emit :: proc(v: Value, dst: []byte) -> int {
if v.form == .Raw {
n := len(v.content)
copy(dst[len(dst) - n:], v.content)
return n
}
end := len(dst)
switch v.form {
case .Raw: // handled by the early return above
case .Primitive:
end -= len(v.content)
copy(dst[end:], v.content)
case .Integer_Magnitude:
start, pad := _int_shape(v.content)
if start == len(v.content) {
end -= 1
dst[end] = 0x00 // zero -> single 0x00 octet
} else {
body := v.content[start:]
end -= len(body)
copy(dst[end:], body)
if pad {
end -= 1
dst[end] = 0x00 // sign octet so the value reads as non-negative
}
}
case .Bit_String_Octets:
end -= len(v.content)
copy(dst[end:], v.content)
end -= 1
dst[end] = 0x00 // unused-bits count: whole octets
case .Bit_String_Wrapped:
for i := len(v.children) - 1; i >= 0; i -= 1 {
end -= _emit(v.children[i], dst[:end])
}
end -= 1
dst[end] = 0x00 // unused-bits count: whole octets
case .Time:
tmp: [15]byte // GeneralizedTime is the longest form
n := _format_time(tmp[:], v._when, v.tag.number == u32(Tag_Number.Generalized_Time))
end -= n
copy(dst[end:], tmp[:n])
case .Constructed:
for i := len(v.children) - 1; i >= 0; i -= 1 {
end -= _emit(v.children[i], dst[:end])
}
}
clen := len(dst) - end // content length, read off the cursor — never recomputed
tmp: [9]byte // identifier byte + up to 8 length octets covers any int length
lw := _write_length(tmp[:], clen)
end -= lw
copy(dst[end:], tmp[:lw])
end -= 1
dst[end] = _tag_byte(v.tag)
return len(dst) - end
}
// UTCTime content is "YYMMDDHHMMSSZ" (13 octets); GeneralizedTime is
// "YYYYMMDDHHMMSSZ" (15). The content length is fixed by the tag, so it is
// known for the measure pass without formatting.
@(private)
_time_content_len :: proc "contextless" (tag: Tag) -> int {
return tag.number == u32(Tag_Number.Generalized_Time) ? 15 : 13
}
// Formats `at` into dst as the RFC 5280 DER time profile (Zulu, seconds
// present, no fractional part) and returns the bytes written: 15 for
// GeneralizedTime ("YYYYMMDDHHMMSSZ"), 13 for UTCTime ("YYMMDDHHMMSSZ", with
// the low two year digits). dst must hold the full width.
@(private)
_format_time :: proc "contextless" (dst: []byte, at: dt.Time, generalized: bool) -> int {
// time_to_datetime decomposes the instant into UTC calendar fields; it only
// fails outside time.Time's range, which an in-range time.Time can't reach.
c, _ := dt.time_to_datetime(at)
p := 0
if generalized {
p += _write_digits(dst[p:], int(c.year), 4)
} else {
p += _write_digits(dst[p:], int(c.year % 100), 2)
}
p += _write_digits(dst[p:], int(c.month), 2)
p += _write_digits(dst[p:], int(c.day), 2)
p += _write_digits(dst[p:], int(c.hour), 2)
p += _write_digits(dst[p:], int(c.minute), 2)
p += _write_digits(dst[p:], int(c.second), 2)
dst[p] = 'Z'
p += 1
return p
}
// Writes `value` as exactly `width` zero-padded decimal digits.
@(private)
_write_digits :: proc "contextless" (dst: []byte, value, width: int) -> int {
x := value
for i := width - 1; i >= 0; i -= 1 {
dst[i] = byte('0' + x % 10)
x /= 10
}
return width
}
// Reports the index of the first significant magnitude octet
// (== len(mag) when the value is zero) and whether a 0x00 sign octet must
// be prepended so the result reads as non-negative.
@(private)
_int_shape :: proc "contextless" (mag: []byte) -> (start: int, pad: bool) {
start = 0
for start < len(mag) && mag[start] == 0x00 {
start += 1
}
if start == len(mag) {
return start, false
}
return start, mag[start] & 0x80 != 0
}
// _length_len / _write_length are the inverse of _read_length: definite
// form, minimal (short form below 128, otherwise the fewest octets).
@(private)
_length_len :: proc "contextless" (length: int) -> int {
if length < 0x80 {
return 1
}
n := 1
v := length
for v > 0 {
v >>= 8
n += 1
}
return n
}
@(private)
_write_length :: proc "contextless" (dst: []byte, length: int) -> int {
if length < 0x80 {
dst[0] = byte(length)
return 1
}
nbytes := 0
v := length
for v > 0 {
v >>= 8
nbytes += 1
}
dst[0] = 0x80 | byte(nbytes)
for i in 0 ..< nbytes {
dst[1 + i] = byte(length >> uint(8 * (nbytes - 1 - i)))
}
return 1 + nbytes
}
// _tag_len / _tag_byte are the inverse of _read_tag, low-tag-number form
// only (number <= 30); PKIX never needs high-tag-number identifiers.
@(private)
_tag_len :: proc "contextless" (tag: Tag) -> int {
return 1
}
@(private)
_tag_byte :: proc "contextless" (tag: Tag) -> byte {
assert_contextless(tag.number <= 30, "asn1: high-tag-number form is not supported by the writer")
b := byte(tag.class) << 6
if tag.constructed {
b |= 0x20
}
b |= byte(tag.number) & 0x1F
return b
}