code wiki / (root) / nx_asn1.nx

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}