nx_asn1.nx source
↩ module page · 199 lines · 7238 B
1// asn1.nx -- ASN.1 DER TLV (Tag-Length-Value) primitives.
2//
3// Distinguished Encoding Rules (ITU-T X.690) reader. DER is the
4// canonical form every X.509 cert uses, every TLS signature, every
5// PKCS#8 key, every CMS message. We implement a reader-only
6// subset -- write-side encoders are future work.
7//
8// DER structure:
9// TLV byte sequence:
10// - Tag: 1-N bytes. Low-tag-number form: 1 byte encoding
11// class (bits 7-6), constructed flag (bit 5), tag (bits 4-0).
12// High-tag-number form (tag >= 31): bits 4-0 = 0x1F, then
13// tag in base-128 continuation bytes. X.509 uses low form
14// exclusively in practice; we support both.
15// - Length: 1-N bytes. Short form (0-127): single byte.
16// Long form: byte 0x80+n indicates n bytes of length follow,
17// big-endian. n <= 126; indefinite-length form (0x80) is
18// explicitly forbidden in DER.
19// - Value: length bytes, content depends on tag.
20//
21// Tags relevant to X.509:
22// 0x02 INTEGER
23// 0x03 BIT STRING
24// 0x04 OCTET STRING
25// 0x05 NULL
26// 0x06 OBJECT IDENTIFIER
27// 0x0C UTF8String
28// 0x13 PrintableString
29// 0x17 UTCTime
30// 0x18 GeneralizedTime
31// 0x30 SEQUENCE (constructed)
32// 0x31 SET (constructed)
33// 0xA0-0xA7 context-specific [0]..[7]
34//
35// Invariants:
36// A1 All reads bounds-check against the caller-supplied length.
37// Negative return signals either "invalid input" (malformed
38// DER) or "out of bounds" (read would exceed buffer).
39// A2 Length field rejects indefinite-length form (DER forbids).
40// A3 No recursion; callers drive descent explicitly.
41// A4 No mallocs inside the reader. All state lives on the stack
42// (or in an Asn1Cursor struct the caller owns).
43
44// nx_safety_envelope:
45// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
46// sil_target: SIL1
47// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
48// verdict: NOT_YET_EVALUATED
49
50import "nx_syscalls.nx"
51
52// Common tag constants. Named so callers don't sprinkle magic hex.
53const ASN1_INTEGER: i64 = 0x02
54const ASN1_BIT_STRING: i64 = 0x03
55const ASN1_OCTET_STRING: i64 = 0x04
56const ASN1_NULL: i64 = 0x05
57const ASN1_OID: i64 = 0x06
58const ASN1_UTF8: i64 = 0x0C
59const ASN1_PRINTABLE: i64 = 0x13
60const ASN1_UTC_TIME: i64 = 0x17
61const ASN1_GENERALIZED: i64 = 0x18
62const ASN1_SEQUENCE: i64 = 0x30
63const ASN1_SET: i64 = 0x31
64
65// Error codes (negative).
66const ASN1_ERR_OOB: i64 = -1
67const ASN1_ERR_INDEFINITE: i64 = -2
68const ASN1_ERR_TAG_OVERFLOW:i64 = -3
69const ASN1_ERR_LENGTH_BAD: i64 = -4
70
71// Cursor tracks a position within a buffer; callers step it
72// forward as they read TLVs. The cursor and buffer are passed
73// explicitly to every reader -- no hidden state.
74struct Asn1Cursor {
75 pos: i64,
76 end: i64,
77}
78
79// Initialise a cursor over `[buf, buf+len)`.
80func asn1_cursor_init(c: *Asn1Cursor, len: i64) -> i64 {
81 c.pos = 0
82 c.end = len
83 return 0
84}
85
86// Read the tag byte(s). Returns the tag value (low form: just
87// the first byte; high form: accumulated from continuation
88// bytes). Caller bounds-checks via cursor.pos update.
89func asn1_read_tag(buf: *u8, c: *Asn1Cursor) -> i64 {
90 if c.pos >= c.end { return ASN1_ERR_OOB }
91 let b: i64 = buf[c.pos]
92 c.pos = c.pos + 1
93 // Low tag number (0-30) fits in bits 4-0. Mask 0x1F.
94 let low_bits: i64 = b & 0x1F
95 if low_bits != 0x1F {
96 return b // full first byte = class + constructed + tag
97 }
98 // High-tag-number: read base-128 continuation bytes until MSB
99 // is clear. Accumulate into tag (preserving class + constructed
100 // flags from the first byte in high bits).
101 let class_ctor: i64 = b & 0xE0 // bits 7-5
102 var tag: i64 = 0
103 while c.pos < c.end {
104 let cb: i64 = buf[c.pos]
105 c.pos = c.pos + 1
106 tag = (tag << 7) | (cb & 0x7F)
107 if (cb & 0x80) == 0 { return class_ctor | tag }
108 if tag > 0x10000000 { return ASN1_ERR_TAG_OVERFLOW }
109 }
110 return ASN1_ERR_OOB
111}
112
113// Read the length field. Returns length value on success, negative
114// on error. Rejects indefinite-length form (A2).
115func asn1_read_length(buf: *u8, c: *Asn1Cursor) -> i64 {
116 if c.pos >= c.end { return ASN1_ERR_OOB }
117 let b: i64 = buf[c.pos]
118 c.pos = c.pos + 1
119 if (b & 0x80) == 0 {
120 return b & 0x7F // short form: 0-127
121 }
122 let nbytes: i64 = b & 0x7F
123 if nbytes == 0 { return ASN1_ERR_INDEFINITE } // 0x80 forbidden in DER
124 if nbytes > 8 { return ASN1_ERR_LENGTH_BAD } // no 2^64+ messages
125 if c.pos + nbytes > c.end { return ASN1_ERR_OOB }
126 var len: i64 = 0
127 var i: i64 = 0
128 while i < nbytes {
129 len = (len << 8) | buf[c.pos + i]
130 i = i + 1
131 }
132 c.pos = c.pos + nbytes
133 return len
134}
135
136// Read one TLV element's tag + length. Writes tag to *out_tag
137// and length to *out_len; on exit cursor.pos points at the value's
138// first byte. Returns 0 on success, negative on error.
139func asn1_read_tlv_header(buf: *u8, c: *Asn1Cursor,
140 out_tag: *i64, out_len: *i64) -> i64 {
141 let tag: i64 = asn1_read_tag(buf, c)
142 if tag < 0 { return tag }
143 let len: i64 = asn1_read_length(buf, c)
144 if len < 0 { return len }
145 if c.pos + len > c.end { return ASN1_ERR_OOB }
146 *out_tag = tag
147 *out_len = len
148 return 0
149}
150
151// Skip the current TLV (value bytes). Caller has already read
152// the header via asn1_read_tlv_header; cursor is at value start.
153// `len` is the length returned from that header.
154func asn1_skip_value(c: *Asn1Cursor, len: i64) -> i64 {
155 if c.pos + len > c.end { return ASN1_ERR_OOB }
156 c.pos = c.pos + len
157 return 0
158}
159
160// Expect a specific tag; fails with ASN1_ERR_LENGTH_BAD on mismatch.
161// On success, writes value length to *out_len and leaves cursor at
162// value start. Useful shorthand for the common "navigate to this
163// specific field" pattern.
164func asn1_expect_tag(buf: *u8, c: *Asn1Cursor,
165 expected_tag: i64, out_len: *i64) -> i64 {
166 var tag_slot: i64 = 0
167 let tag_ptr: *i64 = (tag_slot as i64) as *i64
168 // We need a stable i64 slot; allocate scratch.
169 let scratch: *u8 = sys_mmap(16)
170 let t_p: *i64 = scratch as *i64
171 let rc: i64 = asn1_read_tlv_header(buf, c, t_p, out_len)
172 if rc < 0 { return rc }
173 if *t_p != expected_tag { return ASN1_ERR_LENGTH_BAD }
174 return 0
175}
176
177// Compile-only smoke: build a tiny SEQUENCE { INTEGER 42 } in
178// memory and parse it back.
179// 30 03 SEQUENCE, length 3
180// 02 01 2A INTEGER, length 1, value 42
181func main() -> i64 {
182 let buf: *u8 = sys_mmap(16)
183 buf[0] = 0x30; buf[1] = 0x03; buf[2] = 0x02; buf[3] = 0x01; buf[4] = 0x2A
184
185 let c_raw: *u8 = sys_mmap(32)
186 let c: *Asn1Cursor = c_raw as *Asn1Cursor
187 asn1_cursor_init(c, 5)
188
189 let len_p: *i64 = sys_mmap(16) as *i64
190 let rc1: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, len_p)
191 if rc1 != 0 { return 1 }
192 if *len_p != 3 { return 2 }
193
194 let rc2: i64 = asn1_expect_tag(buf, c, ASN1_INTEGER, len_p)
195 if rc2 != 0 { return 3 }
196 if *len_p != 1 { return 4 }
197 if buf[c.pos] != 42 { return 5 }
198 return 0
199}