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,222 @@
package test_core_asn1
// Deterministic structure-aware fuzzing for the DER reader. The test
// runner seeds context.random_generator and logs the seed, so any
// failure reproduces with -define:ODIN_TEST_RANDOM_SEED=n.
//
// The load-bearing invariant: DER is canonical, so for any element the
// strict reader ACCEPTS, re-encoding the header from (tag, len) must
// reproduce the input bytes exactly. Any acceptance of a non-minimal
// encoding fails the oracle without needing a crash.
import "core:bytes"
import "core:encoding/asn1"
import "core:math/rand"
import "core:testing"
FUZZ_RANDOM_ITERS :: 4096
FUZZ_MUTATE_ITERS :: 2048
FUZZ_MAX_INPUT :: 96
FUZZ_WALK_DEPTH :: 8
// _encode_header re-encodes a DER identifier + length the canonical
// way; used as the acceptance oracle.
@(private="file")
_encode_header :: proc(tag: asn1.Tag, length: int, out: ^[dynamic]byte) {
b := byte(u8(tag.class) << 6)
if tag.constructed {
b |= 0x20
}
if tag.number < 0x1F {
append(out, b | byte(tag.number))
} else {
append(out, b | 0x1F)
// Base-128, big-endian, minimal.
tmp: [5]byte
n := 0
v := tag.number
for {
tmp[n] = byte(v & 0x7F)
n += 1
v >>= 7
if v == 0 {
break
}
}
for i := n - 1; i >= 0; i -= 1 {
c := tmp[i]
if i > 0 {
c |= 0x80
}
append(out, c)
}
}
if length < 0x80 {
append(out, byte(length))
} else {
tmp: [4]byte
n := 0
v := length
for v > 0 {
tmp[n] = byte(v & 0xFF)
n += 1
v >>= 8
}
append(out, 0x80 | byte(n))
for i := n - 1; i >= 0; i -= 1 {
append(out, tmp[i])
}
}
}
// _walk recursively consumes every element in the reader, applying the
// canonical re-encode oracle to each accepted element.
@(private="file")
_walk :: proc(t: ^testing.T, r: ^asn1.Cursor, depth: int, scratch: ^[dynamic]byte) {
for !asn1.is_empty(r) {
start := r.pos
tag, content, err := asn1.read_any(r)
if err != .None {
return
}
element := r.data[start:r.pos]
// Oracle: canonical re-encode must reproduce the element.
clear(scratch)
_encode_header(tag, len(content), scratch)
append(scratch, ..content)
if !bytes.equal(scratch[:], element) {
testing.expectf(t, false, "accepted non-canonical element: % 02x", element)
return
}
if tag.constructed && depth < FUZZ_WALK_DEPTH {
sub := asn1.Cursor{data = content}
_walk(t, &sub, depth + 1, scratch)
}
}
}
@(test)
test_fuzz_read_any_random :: proc(t: ^testing.T) {
buf: [FUZZ_MAX_INPUT]byte
scratch: [dynamic]byte
defer delete(scratch)
for _ in 0 ..< FUZZ_RANDOM_ITERS {
n := rand.int_max(FUZZ_MAX_INPUT + 1)
input := buf[:n]
for i in 0 ..< n {
input[i] = byte(rand.uint32())
}
// Bias half the inputs towards plausible structure: a known
// universal tag and a length that fits.
if n >= 2 && rand.int_max(2) == 0 {
tags := [?]byte{0x02, 0x03, 0x04, 0x05, 0x06, 0x17, 0x18, 0x30, 0x31, 0xA0}
input[0] = rand.choice(tags[:])
input[1] = byte(rand.int_max(n))
}
r: asn1.Cursor
asn1.cursor_init(&r, input)
_walk(t, &r, 0, &scratch)
}
}
@(test)
test_fuzz_typed_readers_random :: proc(t: ^testing.T) {
buf: [FUZZ_MAX_INPUT]byte
for _ in 0 ..< FUZZ_RANDOM_ITERS {
n := rand.int_max(FUZZ_MAX_INPUT + 1)
input := buf[:n]
for i in 0 ..< n {
input[i] = byte(rand.uint32())
}
// Every typed reader must fail cleanly or uphold its contract;
// none may panic. Fresh reader per call: a failed read may
// leave the cursor mid-element by design.
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if v, err := asn1.read_i64(&r); err == .None {
_ = v
}
}
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if mag, err := asn1.read_unsigned_integer_bytes(&r); err == .None {
// Magnitude is minimal: no leading zero unless the
// value IS zero.
if len(mag) > 1 {
testing.expect(t, mag[0] != 0x00, "non-minimal magnitude")
}
}
}
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if bits, unused, err := asn1.read_bit_string(&r); err == .None {
testing.expect(t, unused <= 7)
if unused > 0 {
testing.expect(t, len(bits) > 0)
mask := byte(1 << uint(unused)) - 1
testing.expect(t, bits[len(bits) - 1] & mask == 0, "padding bits set")
}
}
}
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if raw, err := asn1.read_oid(&r); err == .None {
// Layer contract: structural acceptance by read_oid
// means the decoders yield either arcs or Arc_Overflow
// (X.660 arcs are unbounded) — never a structural error.
// This invariant caught a real bug on this fuzzer's
// first run.
arcs, aerr := asn1.oid_components(raw)
str, serr := asn1.oid_to_string(raw)
testing.expect(t, aerr == .None || aerr == .Arc_Overflow, "components: structural error after acceptance")
testing.expect_value(t, serr, aerr)
if aerr == .None {
testing.expect(t, len(arcs) >= 2)
testing.expect(t, len(str) >= 3)
}
delete(arcs)
delete(str)
}
}
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if _, err := asn1.read_time(&r); err == .None {
// Acceptance implies the RFC 5280 profile already
// validated ranges; nothing further to check here.
continue
}
}
}
}
@(test)
test_fuzz_mutated_spki :: proc(t: ^testing.T) {
spki := make_spki()
defer delete(spki)
buf := make([]byte, len(spki))
defer delete(buf)
for _ in 0 ..< FUZZ_MUTATE_ITERS {
copy(buf, spki[:])
// 1-8 random byte mutations; structure mostly survives, so the
// parser gets dragged deep before hitting the damage.
for _ in 0 ..< 1 + rand.int_max(8) {
buf[rand.int_max(len(buf))] = byte(rand.uint32())
}
// Must never panic; success or clean error are both fine.
_, _ = parse_spki(buf)
}
}

View File

@@ -0,0 +1,158 @@
package test_core_asn1
// Deterministic fuzzing for the DER WRITER. The runner seeds
// context.random_generator and logs the seed, so any failure reproduces
// with -define:ODIN_TEST_RANDOM_SEED=n. Run under -sanitize:address to turn
// the value-tree's borrow discipline into a checked property: a stray
// borrow of an out-of-scope composite-literal temporary is a stack
// use-after-scope ASan would catch here.
//
// Invariants:
// - marshal of any constructed tree never crashes / writes out of bounds;
// - the output is well-formed DER (a read_any walk consumes it exactly);
// - leaf values survive a marshal -> read round-trip.
import "core:bytes"
import "core:encoding/asn1"
import "core:math/rand"
import "core:testing"
import "core:time"
WRITER_FUZZ_ITERS :: 2048
WRITER_FUZZ_DEPTH :: 6
// _gen_bytes allocates 0..max random bytes from the temp arena.
@(private = "file")
_gen_bytes :: proc(max: int) -> []byte {
n := rand.int_max(max)
b := make([]byte, n, context.temp_allocator)
for i in 0 ..< n {
b[i] = byte(rand.int_max(256))
}
return b
}
@(private = "file")
_gen_leaf :: proc() -> asn1.Value {
switch rand.int_max(7) {
case 0:
return asn1.boolean(rand.int_max(2) == 1)
case 1:
return asn1.null()
case 2:
return asn1.integer_unsigned(_gen_bytes(20))
case 3:
return asn1.octet_string(_gen_bytes(20))
case 4:
return asn1.bit_string_octets(_gen_bytes(20))
case 5:
return asn1.object_identifier(_gen_bytes(12)) // raw OID octets: read_any tolerates any content
case:
return asn1.generalized_time(time.unix(i64(rand.int_max(2_000_000_000)), 0))
}
}
// _gen_value builds a random tree; constructed nodes draw their children
// arrays from the temp arena (pre-sized, so the slices set() / sequence()
// borrow never move), freed wholesale after each iteration.
@(private = "file")
_gen_value :: proc(depth: int) -> asn1.Value {
if depth <= 0 || rand.int_max(3) == 0 {
return _gen_leaf()
}
n := rand.int_max(4) // 0..3 children
kids := make([]asn1.Value, n, context.temp_allocator)
for i in 0 ..< n {
kids[i] = _gen_value(depth - 1)
}
switch rand.int_max(5) {
case 0:
return asn1.sequence(kids)
case 1:
return asn1.set(kids)
case 2:
return asn1.context_explicit(u32(rand.int_max(8)), kids)
case 3:
return asn1.bit_string_wrap(kids)
case:
sv, serr := asn1.set_of(kids, context.temp_allocator) // allocates scratch + sorts in place
if serr != .None {
return asn1.set(kids)
}
return sv
}
}
// _walk recurses through a marshalled tree with read_any, asserting every
// element frames cleanly and the input is consumed exactly.
@(private = "file")
_walk :: proc(t: ^testing.T, data: []byte, depth: int) {
cur: asn1.Cursor
asn1.cursor_init(&cur, data)
for !asn1.is_empty(&cur) {
tag, content, err := asn1.read_any(&cur)
testing.expect_value(t, err, asn1.Error.None)
if err != .None {
return
}
if tag.constructed && depth > 0 {
_walk(t, content, depth - 1)
}
}
}
@(test)
test_fuzz_writer_wellformed :: proc(t: ^testing.T) {
for _ in 0 ..< WRITER_FUZZ_ITERS {
tree := _gen_value(WRITER_FUZZ_DEPTH)
out, err := asn1.marshal(tree)
testing.expect_value(t, err, asn1.Error.None)
if err == .None {
testing.expect_value(t, len(out), asn1.encoded_len(tree)) // sizing == emission
_walk(t, out, 64)
delete(out)
}
free_all(context.temp_allocator)
}
}
@(test)
test_fuzz_writer_roundtrip :: proc(t: ^testing.T) {
for _ in 0 ..< WRITER_FUZZ_ITERS {
// OCTET STRING: content survives verbatim.
payload := _gen_bytes(32)
if o, e := asn1.marshal(asn1.octet_string(payload)); e == .None {
cur: asn1.Cursor
asn1.cursor_init(&cur, o)
got, re := asn1.read_octet_string(&cur)
testing.expect_value(t, re, asn1.Error.None)
testing.expect_value(t, asn1.done(&cur), asn1.Error.None)
testing.expect(t, bytes.equal(got, payload), "octet string round-trip")
delete(o)
}
// BIT STRING (whole octets): payload survives verbatim.
bits := _gen_bytes(32)
if o, e := asn1.marshal(asn1.bit_string_octets(bits)); e == .None {
cur: asn1.Cursor
asn1.cursor_init(&cur, o)
got, re := asn1.read_bit_string_octets(&cur)
testing.expect_value(t, re, asn1.Error.None)
testing.expect(t, bytes.equal(got, bits), "bit string round-trip")
delete(o)
}
// BOOLEAN.
bv := rand.int_max(2) == 1
if o, e := asn1.marshal(asn1.boolean(bv)); e == .None {
cur: asn1.Cursor
asn1.cursor_init(&cur, o)
got, re := asn1.read_boolean(&cur)
testing.expect_value(t, re, asn1.Error.None)
testing.expect_value(t, got, bv)
delete(o)
}
free_all(context.temp_allocator)
}
}

View File

@@ -0,0 +1,119 @@
package test_core_asn1
// Out-of-memory robustness for the allocating OID helpers. A failing
// allocator wraps a tracking allocator; sweeping the fail point across
// every allocation (including the Builder's internal growth in
// oid_to_string) must yield Allocation_Failed with nothing leaked.
import "base:runtime"
import "core:mem"
import "core:encoding/asn1"
import "core:testing"
@(private="file")
Failing_Allocator :: struct {
backing: runtime.Allocator,
count: int,
fail_at: int,
}
@(private="file")
failing_allocator_proc :: proc(
data: rawptr, mode: runtime.Allocator_Mode,
size, alignment: int, old_memory: rawptr, old_size: int,
loc := #caller_location,
) -> ([]byte, runtime.Allocator_Error) {
fa := (^Failing_Allocator)(data)
#partial switch mode {
case .Alloc, .Alloc_Non_Zeroed, .Resize, .Resize_Non_Zeroed:
if fa.count == fa.fail_at {
fa.count += 1
return nil, .Out_Of_Memory
}
fa.count += 1
}
return fa.backing.procedure(fa.backing.data, mode, size, alignment, old_memory, old_size, loc)
}
@(private="file")
failing_allocator :: proc(fa: ^Failing_Allocator) -> runtime.Allocator {
return {procedure = failing_allocator_proc, data = fa}
}
// rsaEncryption (1.2.840.113549.1.1.1) — seven arcs rendering to a
// 20-char string, enough to drive the Builder in oid_to_string past
// its initial capacity so the resize path is on the sweep.
@(private="file")
LONG_OID := []byte{0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}
@(test)
test_oom_oid_components :: proc(t: ^testing.T) {
raw: asn1.Cursor
asn1.cursor_init(&raw, LONG_OID)
oid, oerr := asn1.read_oid(&raw)
testing.expect_value(t, oerr, asn1.Error.None)
total: int
{
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = -1}
arcs, err := asn1.oid_components(oid, failing_allocator(&fa))
testing.expect_value(t, err, asn1.Error.None)
delete(arcs, failing_allocator(&fa))
total = fa.count
testing.expect(t, total >= 1)
testing.expect_value(t, len(track.allocation_map), 0)
}
for k in 0 ..< total {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = k}
arcs, err := asn1.oid_components(oid, failing_allocator(&fa))
if err == .None {
delete(arcs, failing_allocator(&fa))
} else {
testing.expectf(t, err == .Allocation_Failed, "k=%d: got %v", k, err)
}
testing.expectf(t, len(track.allocation_map) == 0, "k=%d: %d leaked", k, len(track.allocation_map))
}
}
@(test)
test_oom_oid_to_string :: proc(t: ^testing.T) {
raw: asn1.Cursor
asn1.cursor_init(&raw, LONG_OID)
oid, oerr := asn1.read_oid(&raw)
testing.expect_value(t, oerr, asn1.Error.None)
total: int
{
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = -1}
str, err := asn1.oid_to_string(oid, failing_allocator(&fa))
testing.expect_value(t, err, asn1.Error.None)
delete(str, failing_allocator(&fa))
total = fa.count
testing.expect(t, total >= 1)
testing.expect_value(t, len(track.allocation_map), 0)
}
for k in 0 ..< total {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = k}
str, err := asn1.oid_to_string(oid, failing_allocator(&fa))
if err == .None {
delete(str, failing_allocator(&fa))
} else {
testing.expectf(t, err == .Allocation_Failed, "k=%d: got %v", k, err)
}
testing.expectf(t, len(track.allocation_map) == 0, "k=%d: %d leaked", k, len(track.allocation_map))
}
}

View File

@@ -0,0 +1,532 @@
package test_core_asn1
import "core:encoding/asn1"
import "core:testing"
import "core:time"
// ============================================================
// Tag and length forms
// ============================================================
@(test)
test_tag_forms :: proc(t: ^testing.T) {
// Low tag, primitive universal.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x02, 0x01, 0x05})
tag, content, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, tag, asn1.universal(.Integer))
testing.expect_value(t, len(content), 1)
testing.expect_value(t, asn1.done(&r), asn1.Error.None)
}
// Constructed context-specific [0].
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0xA0, 0x00})
tag, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, tag, asn1.context_specific(0))
}
// High-tag-number form: [31] primitive → 9F 1F.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x9F, 0x1F, 0x00})
tag, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, tag.number, u32(31))
}
// High-tag form used for a number < 31 is non-minimal → invalid.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x9F, 0x1E, 0x00})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Tag)
}
// High-tag form with 0x80 lead continuation octet is non-minimal.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x9F, 0x80, 0x1F, 0x00})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Tag)
}
}
@(test)
test_length_forms :: proc(t: ^testing.T) {
// Long-form length for 128 bytes: 81 80.
{
buf: [131]byte
buf[0] = 0x04
buf[1] = 0x81
buf[2] = 0x80
r: asn1.Cursor
asn1.cursor_init(&r, buf[:])
_, content, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(content), 128)
}
// Long form for a short length (81 05) is non-minimal.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x04, 0x81, 0x05, 1, 2, 3, 4, 5})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Length)
}
// Leading zero octet in long form (82 00 80) is non-minimal.
{
buf: [131]byte
buf[0] = 0x04
buf[1] = 0x82
buf[2] = 0x00
buf[3] = 0x80
r: asn1.Cursor
asn1.cursor_init(&r, buf[:])
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Length)
}
// Indefinite length (80) is BER, never DER.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x30, 0x80, 0x00, 0x00})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Length)
}
// Length past end of input.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x04, 0x05, 1, 2})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Truncated)
}
}
// ============================================================
// Scalar types
// ============================================================
@(test)
test_boolean :: proc(t: ^testing.T) {
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x01, 0x01, 0xFF})
v, err := asn1.read_boolean(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, v, true)
}
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x01, 0x01, 0x00})
v, err := asn1.read_boolean(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, v, false)
}
// DER: any value other than 0x00/0xFF is invalid.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x01, 0x01, 0x01})
_, err := asn1.read_boolean(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Boolean)
}
// Wrong width.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x01, 0x02, 0xFF, 0xFF})
_, err := asn1.read_boolean(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Boolean)
}
}
@(test)
test_integer :: proc(t: ^testing.T) {
Case :: struct {
der: []byte,
value: i64,
err: asn1.Error,
}
cases := []Case{
{der = {0x02, 0x01, 0x00}, value = 0},
{der = {0x02, 0x01, 0x7F}, value = 127},
{der = {0x02, 0x02, 0x00, 0x80}, value = 128},
{der = {0x02, 0x01, 0x80}, value = -128},
{der = {0x02, 0x02, 0xFF, 0x7F}, value = -129},
{der = {0x02, 0x08, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, value = max(i64)},
{der = {0x02, 0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, value = min(i64)},
// Non-minimal: redundant leading 0x00 / 0xFF.
{der = {0x02, 0x02, 0x00, 0x05}, err = .Invalid_Integer},
{der = {0x02, 0x02, 0xFF, 0x85}, err = .Invalid_Integer},
// Empty content.
{der = {0x02, 0x00}, err = .Invalid_Integer},
// Too wide for i64.
{der = {0x02, 0x09, 0x01, 0, 0, 0, 0, 0, 0, 0, 0}, err = .Integer_Overflow},
}
for c in cases {
r: asn1.Cursor
asn1.cursor_init(&r, c.der)
v, err := asn1.read_i64(&r)
testing.expect_value(t, err, c.err)
if c.err == .None {
testing.expect_value(t, v, c.value)
}
}
}
@(test)
test_unsigned_integer :: proc(t: ^testing.T) {
// 0x00 sign octet stripped: 255 encodes as 02 02 00 FF.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x02, 0x02, 0x00, 0xFF})
mag, err := asn1.read_unsigned_integer_bytes(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(mag), 1)
testing.expect_value(t, mag[0], u8(0xFF))
}
// Zero stays one octet.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x02, 0x01, 0x00})
mag, err := asn1.read_unsigned_integer_bytes(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(mag), 1)
testing.expect_value(t, mag[0], u8(0x00))
}
// Negative rejected.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x02, 0x01, 0x80})
_, err := asn1.read_unsigned_integer_bytes(&r)
testing.expect_value(t, err, asn1.Error.Negative_Integer)
}
}
@(test)
test_bit_string :: proc(t: ^testing.T) {
// Whole octets: 03 03 00 A0 0F.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x03, 0x00, 0xA0, 0x0F})
octets, err := asn1.read_bit_string_octets(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(octets), 2)
}
// 4 unused bits, correctly zero-padded: A0 = 1010_0000.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x02, 0x04, 0xA0})
bits, unused, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, unused, 4)
testing.expect_value(t, len(bits), 1)
}
// Non-zero padding bits violate DER.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x02, 0x04, 0xA1})
_, _, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
// Unused count > 7.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x02, 0x08, 0xA0})
_, _, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
// Empty payload must declare zero unused bits.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x01, 0x03})
_, _, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
// Empty content entirely.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x00})
_, _, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
// PKIX shape requires unused == 0.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x02, 0x04, 0xA0})
_, err := asn1.read_bit_string_octets(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
}
@(test)
test_null :: proc(t: ^testing.T) {
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x05, 0x00})
testing.expect_value(t, asn1.read_null(&r), asn1.Error.None)
}
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x05, 0x01, 0x00})
testing.expect_value(t, asn1.read_null(&r), asn1.Error.Invalid_Null)
}
}
// ============================================================
// OBJECT IDENTIFIER
// ============================================================
@(test)
test_oid :: proc(t: ^testing.T) {
// rsaEncryption: 1.2.840.113549.1.1.1
rsa_encryption := []byte{0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}
{
r: asn1.Cursor
asn1.cursor_init(&r, rsa_encryption)
raw, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(raw), 9)
str, serr := asn1.oid_to_string(raw)
defer delete(str)
testing.expect_value(t, serr, asn1.Error.None)
testing.expect_value(t, str, "1.2.840.113549.1.1.1")
arcs, aerr := asn1.oid_components(raw)
defer delete(arcs)
testing.expect_value(t, aerr, asn1.Error.None)
testing.expect_value(t, len(arcs), 7)
testing.expect_value(t, arcs[3], u64(113549))
}
// ecPublicKey: 1.2.840.10045.2.1
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01})
raw, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.None)
str, serr := asn1.oid_to_string(raw)
defer delete(str)
testing.expect_value(t, serr, asn1.Error.None)
testing.expect_value(t, str, "1.2.840.10045.2.1")
}
// Arc-2 base offset: 2.5.4.3 (id-at-commonName) → 55 04 03.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x03, 0x55, 0x04, 0x03})
raw, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.None)
str, serr := asn1.oid_to_string(raw)
defer delete(str)
testing.expect_value(t, serr, asn1.Error.None)
testing.expect_value(t, str, "2.5.4.3")
}
// Empty content is invalid.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x00})
_, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Object_Identifier)
}
// Non-minimal subidentifier (leading 0x80 continuation octet).
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x03, 0x2A, 0x80, 0x01})
_, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Object_Identifier)
}
// Truncated subidentifier (continuation bit set on last octet).
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x02, 0x2A, 0x86})
_, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Object_Identifier)
}
}
// ============================================================
// Time
// ============================================================
@(test)
test_time :: proc(t: ^testing.T) {
// Times are returned as time.Time; compare via Unix seconds.
// Reference epochs computed independently (e.g.
// `date -u -d '1999-01-01T00:00:00Z' +%s`). UTCTime century window:
// 990101000000Z → 1999, 490101000000Z → 2049.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x17, 0x0D, '9', '9', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', 'Z'})
v, err := asn1.read_utc_time(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(v), i64(915148800)) // 1999-01-01T00:00:00Z
}
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x17, 0x0D, '4', '9', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', 'Z'})
v, err := asn1.read_utc_time(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(v), i64(2493072000)) // 2049-01-01T00:00:00Z
}
// GeneralizedTime: 20260612153000Z.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x18, 0x0F, '2', '0', '2', '6', '0', '6', '1', '2', '1', '5', '3', '0', '0', '0', 'Z'})
v, err := asn1.read_generalized_time(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(v), i64(1781278200)) // 2026-06-12T15:30:00Z
}
// read_time dispatches on tag.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x18, 0x0F, '2', '0', '5', '0', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', 'Z'})
v, err := asn1.read_time(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(v), i64(2524608000)) // 2050-01-01T00:00:00Z
}
// Far-future dates (year 9999, the RFC 5280 no-expiration sentinel)
// must still parse, but time.Time tops out near year 2262, so the
// value SATURATES rather than carrying the literal 9999 epoch. This
// is an intentional limitation (see asn1's _time_from_unix): the
// 9999 sentinel just needs to read as "effectively never expires".
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x18, 0x0F, '9', '9', '9', '9', '1', '2', '3', '1', '2', '3', '5', '9', '5', '9', 'Z'})
v, err := asn1.read_generalized_time(&r)
testing.expect_value(t, err, asn1.Error.None)
// Saturated near year 2262, NOT the true 9999 epoch (253402300799).
testing.expect(t, time.to_unix_seconds(v) >= i64(9_223_372_036), "9999 saturates to time.Time max")
testing.expect(t, time.to_unix_seconds(v) < i64(253402300799), "saturated, not the literal 9999 epoch")
}
// RFC 5280 profile rejections: offset instead of Z, missing
// seconds, fractional seconds, month/day out of range.
bad := [][]byte{
{0x17, 0x11, '9', '9', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', '+', '0', '1', '0', '0'},
{0x17, 0x0B, '9', '9', '0', '1', '0', '1', '0', '0', '0', '0', 'Z'},
{0x18, 0x12, '2', '0', '2', '6', '0', '6', '1', '2', '1', '5', '3', '0', '0', '0', '.', '5', '0', 'Z'},
{0x17, 0x0D, '9', '9', '1', '3', '0', '1', '0', '0', '0', '0', '0', '0', 'Z'},
{0x17, 0x0D, '9', '9', '0', '1', '0', '0', '0', '0', '0', '0', '0', '0', 'Z'},
{0x17, 0x0D, '9', '9', '0', '1', '0', '1', '2', '4', '0', '0', '0', '0', 'Z'},
}
for der in bad {
r: asn1.Cursor
asn1.cursor_init(&r, der)
_, err := asn1.read_time(&r)
testing.expect(t, err != .None, "malformed time must be rejected")
}
}
// ============================================================
// Compound structures
// ============================================================
// A real SubjectPublicKeyInfo for an EC P-256 key:
// SEQUENCE {
// SEQUENCE { OID ecPublicKey, OID prime256v1 }
// BIT STRING (65 octets of uncompressed point, 0 unused bits)
// }
@(private)
make_spki :: proc(allocator := context.allocator) -> [dynamic]byte {
out: [dynamic]byte
out.allocator = allocator
append(&out, 0x30, 0x59) // SEQUENCE, len 89
append(&out, 0x30, 0x13) // SEQUENCE, len 19
append(&out, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01) // ecPublicKey
append(&out, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07) // prime256v1
append(&out, 0x03, 0x42, 0x00) // BIT STRING, len 66, 0 unused
append(&out, 0x04) // uncompressed point marker
for i in 0 ..< 64 {
append(&out, u8(i))
}
return out
}
@(private)
parse_spki :: proc(data: []byte) -> (point: []byte, err: asn1.Error) {
r: asn1.Cursor
asn1.cursor_init(&r, data)
spki, serr := asn1.read_sequence(&r)
if serr != .None {
return nil, serr
}
if derr := asn1.done(&r); derr != .None {
return nil, derr
}
alg, aerr := asn1.read_sequence(&spki)
if aerr != .None {
return nil, aerr
}
alg_oid, oerr := asn1.read_oid(&alg)
if oerr != .None {
return nil, oerr
}
_ = alg_oid
curve_oid, cerr := asn1.read_oid(&alg)
if cerr != .None {
return nil, cerr
}
_ = curve_oid
if derr := asn1.done(&alg); derr != .None {
return nil, derr
}
key, kerr := asn1.read_bit_string_octets(&spki)
if kerr != .None {
return nil, kerr
}
if derr := asn1.done(&spki); derr != .None {
return nil, derr
}
return key, .None
}
@(test)
test_spki_walk :: proc(t: ^testing.T) {
spki := make_spki()
defer delete(spki)
point, err := parse_spki(spki[:])
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(point), 65)
testing.expect_value(t, point[0], u8(0x04))
}
// Every truncation of a valid structure must error cleanly — never
// panic, never succeed.
@(test)
test_truncation_sweep :: proc(t: ^testing.T) {
spki := make_spki()
defer delete(spki)
for n in 0 ..< len(spki) {
_, err := parse_spki(spki[:n])
testing.expect(t, err != .None, "truncated input must be rejected")
}
}
@(test)
test_explicit_optional :: proc(t: ^testing.T) {
// [0] EXPLICIT INTEGER 2 (the X.509 version field shape), then an
// INTEGER at the outer level.
der := []byte{0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x01, 0x07}
r: asn1.Cursor
asn1.cursor_init(&r, der)
inner, present, err := asn1.read_explicit(&r, 0)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, present, true)
version, verr := asn1.read_i64(&inner)
testing.expect_value(t, verr, asn1.Error.None)
testing.expect_value(t, version, i64(2))
testing.expect_value(t, asn1.done(&inner), asn1.Error.None)
// Absent optional: next element is [1]? No — it's the INTEGER, so
// read_explicit(1) must not consume.
_, present2, err2 := asn1.read_explicit(&r, 1)
testing.expect_value(t, err2, asn1.Error.None)
testing.expect_value(t, present2, false)
serial, serr := asn1.read_i64(&r)
testing.expect_value(t, serr, asn1.Error.None)
testing.expect_value(t, serial, i64(7))
testing.expect_value(t, asn1.done(&r), asn1.Error.None)
}

View File

@@ -0,0 +1,298 @@
package test_core_asn1
import "core:bytes"
import "core:encoding/asn1"
import "core:testing"
import "core:time"
@(private = "file")
_marshal :: proc(t: ^testing.T, v: asn1.Value) -> []byte {
out, err := asn1.marshal(v)
testing.expect_value(t, err, asn1.Error.None)
return out
}
@(private = "file")
_expect_der :: proc(t: ^testing.T, v: asn1.Value, want: []byte) {
got := _marshal(t, v)
defer delete(got)
testing.expect_value(t, len(got), asn1.encoded_len(v)) // encoded_len must match what was written
testing.expectf(t, bytes.equal(got, want), "got %x, want %x", got, want)
}
// INTEGER from an unsigned magnitude: minimal stripping + sign-octet
// insertion, the inverse of read_unsigned_integer_bytes.
@(test)
test_writer_integer_unsigned :: proc(t: ^testing.T) {
_expect_der(t, asn1.integer_unsigned({}), {0x02, 0x01, 0x00}) // empty -> 0
_expect_der(t, asn1.integer_unsigned({0x00}), {0x02, 0x01, 0x00}) // 0
_expect_der(t, asn1.integer_unsigned({0x00, 0x00}), {0x02, 0x01, 0x00}) // all-zero -> 0
_expect_der(t, asn1.integer_unsigned({0x2A}), {0x02, 0x01, 0x2A}) // 42
_expect_der(t, asn1.integer_unsigned({0x00, 0x2A}), {0x02, 0x01, 0x2A}) // strip leading zero
_expect_der(t, asn1.integer_unsigned({0x80}), {0x02, 0x02, 0x00, 0x80}) // 128: insert sign octet
_expect_der(t, asn1.integer_unsigned({0xFF, 0xFF}), {0x02, 0x03, 0x00, 0xFF, 0xFF}) // 65535
_expect_der(t, asn1.integer_unsigned({0x01, 0x00, 0x01}), {0x02, 0x03, 0x01, 0x00, 0x01}) // 65537 (RSA e)
}
// Length octets: short form below 128, otherwise minimal long form. Drive
// the boundary with OCTET STRINGs of crafted sizes and check the header.
@(test)
test_writer_length_forms :: proc(t: ^testing.T) {
cases := []struct {
n: int,
head: []byte,
} {
{0, {0x04, 0x00}},
{1, {0x04, 0x01}},
{127, {0x04, 0x7F}}, // last short-form length
{128, {0x04, 0x81, 0x80}}, // first long form
{255, {0x04, 0x81, 0xFF}},
{256, {0x04, 0x82, 0x01, 0x00}},
{300, {0x04, 0x82, 0x01, 0x2C}},
}
for c in cases {
content := make([]byte, c.n)
defer delete(content)
got := _marshal(t, asn1.octet_string(content))
defer delete(got)
testing.expectf(t, len(got) >= len(c.head), "n=%d: short output", c.n)
testing.expectf(t, bytes.equal(got[:len(c.head)], c.head), "n=%d: header %x, want %x", c.n, got[:len(c.head)], c.head)
testing.expect_value(t, len(got), len(c.head) + c.n)
}
}
// encode into a buffer one byte too small must write nothing and report it.
@(test)
test_writer_buffer_too_small :: proc(t: ^testing.T) {
v := asn1.integer_unsigned({0x80}) // encodes to 4 bytes
need := asn1.encoded_len(v)
testing.expect_value(t, need, 4)
short := make([]byte, need - 1)
defer delete(short)
n, err := asn1.encode(v, short)
testing.expect_value(t, err, asn1.Error.Buffer_Too_Small)
testing.expect_value(t, n, 0)
exact := make([]byte, need)
defer delete(exact)
n2, err2 := asn1.encode(v, exact)
testing.expect_value(t, err2, asn1.Error.None)
testing.expect_value(t, n2, need)
}
// Round-trip every write back through the cursor reader: SEQUENCE { INTEGER
// r, INTEGER s }, the ECDSA signature shape, with s's top bit set so a sign
// octet is inserted on write and stripped on read.
@(test)
test_writer_roundtrip_ecdsa_sig :: proc(t: ^testing.T) {
r := []byte{0x01, 0x23, 0x45, 0x67}
s := []byte{0x80, 0x00, 0x00, 0x01} // top bit set
sig := _marshal(t, asn1.sequence({asn1.integer_unsigned(r), asn1.integer_unsigned(s)}))
defer delete(sig)
// s gained a 0x00 sign octet: 2 + (2+4) + (2+5) = 15 bytes.
testing.expect_value(t, len(sig), 15)
cur: asn1.Cursor
asn1.cursor_init(&cur, sig)
seq, serr := asn1.read_sequence(&cur)
testing.expect_value(t, serr, asn1.Error.None)
gr, rerr := asn1.read_unsigned_integer_bytes(&seq)
gs, srerr := asn1.read_unsigned_integer_bytes(&seq)
testing.expect_value(t, rerr, asn1.Error.None)
testing.expect_value(t, srerr, asn1.Error.None)
testing.expect_value(t, asn1.done(&seq), asn1.Error.None)
testing.expect_value(t, asn1.done(&cur), asn1.Error.None)
testing.expect(t, bytes.equal(gr, r), "r round-trips")
testing.expect(t, bytes.equal(gs, s), "s round-trips")
}
// Structural Tier 1 encoders: NULL, OID passthrough, BIT STRING (whole
// octets), SET, and the context-specific [n] wrappers, by exact bytes.
@(test)
test_writer_structural :: proc(t: ^testing.T) {
_expect_der(t, asn1.null(), {0x05, 0x00})
// BIT STRING, whole octets: 03 <len+1> 00 <payload>.
_expect_der(t, asn1.bit_string_octets({0xCA, 0xFE}), {0x03, 0x03, 0x00, 0xCA, 0xFE})
_expect_der(t, asn1.bit_string_octets({}), {0x03, 0x01, 0x00})
// OID passthrough: rsaEncryption (1.2.840.113549.1.1.1) content octets.
rsa_oid := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}
_expect_der(t, asn1.object_identifier(rsa_oid), {0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01})
// [0] IMPLICIT primitive, and [0] EXPLICIT wrapping INTEGER 2.
_expect_der(t, asn1.context_primitive(0, {0xAB, 0xCD}), {0x80, 0x02, 0xAB, 0xCD})
_expect_der(t, asn1.context_explicit(0, {asn1.integer_unsigned({0x02})}), {0xA0, 0x03, 0x02, 0x01, 0x02})
// SET emits in the given order (single/pre-sorted element is the contract).
_expect_der(t, asn1.set({asn1.boolean(false)}), {0x31, 0x03, 0x01, 0x01, 0x00})
}
// A SubjectPublicKeyInfo-shaped tree round-trips through the reader:
// SEQUENCE { SEQUENCE { OID, NULL }, BIT STRING }.
@(test)
test_writer_spki_shape :: proc(t: ^testing.T) {
oid := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01} // rsaEncryption
key := []byte{0x30, 0x06, 0x02, 0x01, 0x2A, 0x02, 0x01, 0x03} // opaque inner key octets
der := _marshal(t, asn1.sequence({asn1.sequence({asn1.object_identifier(oid), asn1.null()}), asn1.bit_string_octets(key)}))
defer delete(der)
cur: asn1.Cursor
asn1.cursor_init(&cur, der)
spki, e0 := asn1.read_sequence(&cur)
testing.expect_value(t, e0, asn1.Error.None)
alg, e1 := asn1.read_sequence(&spki)
testing.expect_value(t, e1, asn1.Error.None)
got_oid, e2 := asn1.read_oid(&alg)
testing.expect_value(t, e2, asn1.Error.None)
testing.expect(t, bytes.equal(got_oid, oid), "oid round-trips")
testing.expect_value(t, asn1.read_null(&alg), asn1.Error.None)
testing.expect_value(t, asn1.done(&alg), asn1.Error.None)
got_key, e3 := asn1.read_bit_string_octets(&spki)
testing.expect_value(t, e3, asn1.Error.None)
testing.expect(t, bytes.equal(got_key, key), "key bits round-trip")
testing.expect_value(t, asn1.done(&spki), asn1.Error.None)
testing.expect_value(t, asn1.done(&cur), asn1.Error.None)
}
// time() auto-selects UTCTime (<2050) vs GeneralizedTime (>=2050) per RFC 5280.
@(test)
test_writer_time_auto :: proc(t: ^testing.T) {
// 2049-12-31T23:59:59Z -> UTCTime (tag 0x17).
before := _marshal(t, asn1.time(time.unix(2524607999, 0)))
defer delete(before)
testing.expect_value(t, before[0], u8(0x17))
// 2050-01-01T00:00:00Z -> GeneralizedTime (tag 0x18).
after := _marshal(t, asn1.time(time.unix(2524608000, 0)))
defer delete(after)
testing.expect_value(t, after[0], u8(0x18))
}
// set_of sorts components into DER canonical order (X.690 11.6, by encoding).
@(test)
test_writer_set_of :: proc(t: ^testing.T) {
// Integers given 3,1,2 must emit sorted 1,2,3.
kids := [3]asn1.Value{asn1.integer_unsigned({0x03}), asn1.integer_unsigned({0x01}), asn1.integer_unsigned({0x02})}
v, err := asn1.set_of(kids[:])
testing.expect_value(t, err, asn1.Error.None)
out := _marshal(t, v)
defer delete(out)
cur: asn1.Cursor
asn1.cursor_init(&cur, out)
s, e := asn1.read_set(&cur)
testing.expect_value(t, e, asn1.Error.None)
for want in ([]byte{0x01, 0x02, 0x03}) {
got, ge := asn1.read_unsigned_integer_bytes(&s)
testing.expect_value(t, ge, asn1.Error.None)
testing.expect(t, len(got) == 1 && got[0] == want, "sorted ascending")
}
testing.expect_value(t, asn1.done(&s), asn1.Error.None)
// Ordering is by ENCODING, not value: 2 (02 01 02) sorts before 256
// (02 02 01 00) because the length octet 0x01 < 0x02.
kids2 := [2]asn1.Value{asn1.integer_unsigned({0x01, 0x00}), asn1.integer_unsigned({0x02})}
v2, err2 := asn1.set_of(kids2[:])
testing.expect_value(t, err2, asn1.Error.None)
_expect_der(t, v2, {0x31, 0x07, 0x02, 0x01, 0x02, 0x02, 0x02, 0x01, 0x00})
}
// raw() splices a complete pre-encoded element in verbatim, the composition
// primitive for nesting independently-marshalled structures.
@(test)
test_writer_raw :: proc(t: ^testing.T) {
pre := []byte{0x02, 0x01, 0x2A} // a pre-encoded INTEGER 42
_expect_der(t, asn1.raw(pre), {0x02, 0x01, 0x2A})
// embedded beside another value inside a SEQUENCE.
_expect_der(t, asn1.sequence({asn1.raw(pre), asn1.boolean(true)}), {0x30, 0x06, 0x02, 0x01, 0x2A, 0x01, 0x01, 0xFF})
}
@(private = "file")
_expect_time :: proc(t: ^testing.T, v: asn1.Value, tag: byte, ascii: string) {
got := _marshal(t, v)
defer delete(got)
want := make([]byte, 2 + len(ascii))
defer delete(want)
want[0] = tag
want[1] = byte(len(ascii))
copy(want[2:], transmute([]byte)ascii)
testing.expectf(t, bytes.equal(got, want), "got %x, want %x", got, want)
}
// UTCTime / GeneralizedTime formatting (option B: time.Time formatted into
// the output at emit) by exact bytes, plus a round-trip through the reader.
// Tags: UTCTime 0x17, GeneralizedTime 0x18.
@(test)
test_writer_time :: proc(t: ^testing.T) {
epoch := time.unix(0, 0) // 1970-01-01 00:00:00Z
_expect_time(t, asn1.generalized_time(epoch), 0x18, "19700101000000Z")
_expect_time(t, asn1.utc_time(epoch), 0x17, "700101000000Z")
y2027 := time.unix(1798761600, 0) // 2027-01-01 00:00:00Z
_expect_time(t, asn1.generalized_time(y2027), 0x18, "20270101000000Z")
_expect_time(t, asn1.utc_time(y2027), 0x17, "270101000000Z")
// Nonzero month/day/time-of-day: 2023-11-14 22:13:20Z.
tod := time.unix(1700000000, 0)
_expect_time(t, asn1.generalized_time(tod), 0x18, "20231114221320Z")
// Round-trip both forms through the reader.
{
der := _marshal(t, asn1.generalized_time(tod))
defer delete(der)
cur: asn1.Cursor
asn1.cursor_init(&cur, der)
got, err := asn1.read_generalized_time(&cur)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(got), i64(1700000000))
}
{
der := _marshal(t, asn1.utc_time(y2027))
defer delete(der)
cur: asn1.Cursor
asn1.cursor_init(&cur, der)
got, err := asn1.read_utc_time(&cur)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(got), i64(1798761600))
}
}
// Nesting + a second leaf type: SEQUENCE { BOOLEAN, SEQUENCE { INTEGER } }.
// Re-encoding the parsed structure must reproduce the bytes exactly
// (DER is canonical), which is the core correctness property for a writer.
@(test)
test_writer_nested_and_idempotent :: proc(t: ^testing.T) {
inner := asn1.sequence({asn1.integer_unsigned({0x2A})})
outer := asn1.sequence({asn1.boolean(true), inner})
der := _marshal(t, outer)
defer delete(der)
// 30 08 01 01 FF 30 03 02 01 2A
want := []byte{0x30, 0x08, 0x01, 0x01, 0xFF, 0x30, 0x03, 0x02, 0x01, 0x2A}
testing.expectf(t, bytes.equal(der, want), "got %x, want %x", der, want)
// Walk it back through the reader to confirm it parses as DER.
cur: asn1.Cursor
asn1.cursor_init(&cur, der)
o, oerr := asn1.read_sequence(&cur)
testing.expect_value(t, oerr, asn1.Error.None)
b, berr := asn1.read_boolean(&o)
testing.expect_value(t, berr, asn1.Error.None)
testing.expect_value(t, b, true)
i, ierr := asn1.read_sequence(&o)
testing.expect_value(t, ierr, asn1.Error.None)
mag, merr := asn1.read_unsigned_integer_bytes(&i)
testing.expect_value(t, merr, asn1.Error.None)
testing.expect(t, bytes.equal(mag, {0x2A}), "inner integer round-trips")
testing.expect_value(t, asn1.done(&o), asn1.Error.None)
testing.expect_value(t, asn1.done(&cur), asn1.Error.None)
}