mirror of
https://github.com/odin-lang/Odin.git
synced 2026-07-15 04:10:29 +00:00
asn1 DER reader/writer and X.509 reader
This commit is contained in:
222
tests/core/encoding/asn1/fuzz_asn1.odin
Normal file
222
tests/core/encoding/asn1/fuzz_asn1.odin
Normal 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)
|
||||
}
|
||||
}
|
||||
158
tests/core/encoding/asn1/fuzz_writer.odin
Normal file
158
tests/core/encoding/asn1/fuzz_writer.odin
Normal 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)
|
||||
}
|
||||
}
|
||||
119
tests/core/encoding/asn1/oom_asn1.odin
Normal file
119
tests/core/encoding/asn1/oom_asn1.odin
Normal 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))
|
||||
}
|
||||
}
|
||||
532
tests/core/encoding/asn1/test_core_asn1.odin
Normal file
532
tests/core/encoding/asn1/test_core_asn1.odin
Normal 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)
|
||||
}
|
||||
298
tests/core/encoding/asn1/test_writer.odin
Normal file
298
tests/core/encoding/asn1/test_writer.odin
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user