// der.mjs: codor si decodor DER minimal (ASN.1), numai ce trebuie pentru X.509 v3 si CRL v2. Fara dependinte. // Decodorul e STRICT: lungimi in forma cea mai scurta, fara lungime nedefinita, fara octeti in plus; un DER necanonic e o eroare, // nu o interpretare (doua codificari ale aceluiasi certificat ar da doua amprente). export const TAG = { BOOLEAN: 0x01, INTEGER: 0x02, BIT_STRING: 0x03, OCTET_STRING: 0x04, NULL: 0x05, OID: 0x06, UTF8: 0x0c, PRINTABLE: 0x13, IA5: 0x16, UTC_TIME: 0x17, GEN_TIME: 0x18, SEQUENCE: 0x30, SET: 0x31, }; function lenBytes(n) { if (n < 0x80) return Buffer.from([n]); const out = []; while (n > 0) { out.unshift(n & 0xff); n = Math.floor(n / 256); } return Buffer.from([0x80 | out.length, ...out]); } export function tlv(tag, content) { const c = Buffer.isBuffer(content) ? content : Buffer.from(content); return Buffer.concat([Buffer.from([tag]), lenBytes(c.length), c]); } export const seq = (...items) => tlv(TAG.SEQUENCE, Buffer.concat(items.filter(Boolean))); export const set = (...items) => tlv(TAG.SET, Buffer.concat(items.filter(Boolean))); export const ctx = (n, content, constructed = true) => tlv((constructed ? 0xa0 : 0x80) | n, content); export const octets = (b) => tlv(TAG.OCTET_STRING, b); export const bits = (b, unused = 0) => tlv(TAG.BIT_STRING, Buffer.concat([Buffer.from([unused]), b])); export const bool = (v) => tlv(TAG.BOOLEAN, Buffer.from([v ? 0xff : 0x00])); export const utf8 = (s) => tlv(TAG.UTF8, Buffer.from(s, 'utf8')); export const ia5 = (s) => tlv(TAG.IA5, Buffer.from(s, 'ascii')); /** INTEGER din Buffer fara semn (big-endian) sau din numar/BigInt nenegativ, in forma DER minima. */ export function int(v) { let b; if (Buffer.isBuffer(v)) b = v; else { let x = BigInt(v); if (x < 0n) throw new Error('DER: numai intregi nenegativi'); const a = []; do { a.unshift(Number(x & 0xffn)); x >>= 8n; } while (x > 0n); b = Buffer.from(a); } let i = 0; while (i < b.length - 1 && b[i] === 0 && (b[i + 1] & 0x80) === 0) i++; b = b.subarray(i); if (b[0] & 0x80) b = Buffer.concat([Buffer.from([0]), b]); return tlv(TAG.INTEGER, b); } /** Un subidentificator in baza 128, cu bitul 7 pus pe toti octetii in afara de ultimul (X.690 8.19.2). */ function base128(x) { const a = [Number(x & 0x7fn)]; x >>= 7n; while (x > 0n) { a.unshift(Number(x & 0x7fn) | 0x80); x >>= 7n; } return a; } export function oid(dotted) { const p = String(dotted).split('.').map((x) => { if (!/^\d+$/.test(x)) throw new Error('DER: OID cu componenta nevalida: ' + dotted); return BigInt(x); }); if (p.length < 2) throw new Error('DER: OID prea scurt'); if (p[0] > 2n || (p[0] < 2n && p[1] > 39n)) throw new Error('DER: OID cu arc nevalid: ' + dotted); // [A10] primul subidentificator poarta impreuna arcul si al doilea numar (40*arc + al doilea) si, ca oricare altul, se scrie in baza 128 // pe mai multi octeti cand trece de 127: 2.999 e 88 37, nu un singur octet trunchiat. Pe arcul 2 al doilea numar nu are limita. const out = base128(p[0] * 40n + p[1]); for (const v of p.slice(2)) out.push(...base128(v)); return tlv(TAG.OID, Buffer.from(out)); } /** Timp X.509: UTCTime pana in 2049, GeneralizedTime dupa (RFC 5280 4.1.2.5), cu secunde si Z. */ export function time(d) { const y = d.getUTCFullYear(); const pad = (n, w = 2) => String(n).padStart(w, '0'); const rest = pad(d.getUTCMonth() + 1) + pad(d.getUTCDate()) + pad(d.getUTCHours()) + pad(d.getUTCMinutes()) + pad(d.getUTCSeconds()) + 'Z'; if (y >= 1950 && y < 2050) return tlv(TAG.UTC_TIME, Buffer.from(pad(y % 100) + rest, 'ascii')); return tlv(TAG.GEN_TIME, Buffer.from(pad(y, 4) + rest, 'ascii')); } // ------------------------------------------------------------------------------------------------ decodare /** Un element: { tag, start, hdr, len, end, raw (tot TLV-ul), content } peste acelasi Buffer. */ export function read(buf, off = 0) { if (off + 2 > buf.length) throw new Error('DER: trunchiat la antet'); const tag = buf[off]; if ((tag & 0x1f) === 0x1f) throw new Error('DER: eticheta cu forma lunga nesuportata'); let l = buf[off + 1], hdr = 2; if (l === 0x80) throw new Error('DER: lungime nedefinita (BER), refuzata'); if (l & 0x80) { const n = l & 0x7f; if (n === 0 || n > 4) throw new Error('DER: lungime pe ' + n + ' octeti'); if (off + 2 + n > buf.length) throw new Error('DER: trunchiat la lungime'); if (buf[off + 2] === 0) throw new Error('DER: lungime cu zero initial (necanonica)'); l = 0; for (let i = 0; i < n; i++) l = l * 256 + buf[off + 2 + i]; if (l < 0x80) throw new Error('DER: lungime scurta scrisa in forma lunga (necanonica)'); hdr = 2 + n; } const end = off + hdr + l; if (end > buf.length) throw new Error('DER: continut trunchiat'); return { tag, start: off, hdr, len: l, end, raw: buf.subarray(off, end), content: buf.subarray(off + hdr, end) }; } /** Toate elementele din continutul unui element construit (SEQUENCE, SET, [n]). */ export function children(el) { const out = []; let o = 0; while (o < el.content.length) { const c = read(el.content, o); out.push(c); o = c.end; } return out; } /** Decodeaza un document DER intreg si refuza octetii in plus. */ export function parse(buf) { const el = read(buf, 0); if (el.end !== buf.length) throw new Error('DER: ' + (buf.length - el.end) + ' octeti dupa structura'); return el; } export function expect(el, tag, what) { if (el.tag !== tag) throw new Error(`DER: ${what}: eticheta 0x${el.tag.toString(16)}, asteptat 0x${tag.toString(16)}`); return el; } export function oidToString(el) { expect(el, TAG.OID, 'OID'); const b = el.content; if (b.length === 0) throw new Error('DER: OID gol'); if (b[b.length - 1] & 0x80) throw new Error('DER: OID trunchiat'); // [A10] intai se citesc TOTI subidentificatorii in baza 128 (si primul poate avea mai multi octeti), abia apoi primul se desparte in // arc si al doilea numar: sub 80 arcul e catul impartirii la 40, de la 80 in sus arcul e 2 (X.690 8.19.4). Un octet 0x80 la inceputul // unui subidentificator e o codificare necanonica si se refuza. const subs = []; let v = 0n, inceput = true; for (let i = 0; i < b.length; i++) { if (inceput && b[i] === 0x80) throw new Error('DER: OID necanonic'); v = (v << 7n) | BigInt(b[i] & 0x7f); inceput = false; if ((b[i] & 0x80) === 0) { subs.push(v); v = 0n; inceput = true; } } const first = subs[0]; const arc = first < 80n ? first / 40n : 2n; return [arc, first - arc * 40n, ...subs.slice(1)].map(String).join('.'); } export function intToBigInt(el) { expect(el, TAG.INTEGER, 'INTEGER'); const b = el.content; if (b.length === 0) throw new Error('DER: INTEGER gol'); if (b.length > 1 && ((b[0] === 0 && (b[1] & 0x80) === 0) || (b[0] === 0xff && (b[1] & 0x80)))) throw new Error('DER: INTEGER necanonic'); if (b[0] & 0x80) throw new Error('DER: INTEGER negativ'); return BigInt('0x' + (b.toString('hex') || '0')); } export function timeToDate(el) { const s = el.content.toString('ascii'); let m; if (el.tag === TAG.UTC_TIME && (m = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(s))) { const yy = +m[1]; const y = yy >= 50 ? 1900 + yy : 2000 + yy; return new Date(Date.UTC(y, +m[2] - 1, +m[3], +m[4], +m[5], +m[6])); } if (el.tag === TAG.GEN_TIME && (m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(s))) { return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])); } throw new Error('DER: timp in forma nepermisa de RFC 5280: ' + s); } export function bitString(el) { expect(el, TAG.BIT_STRING, 'BIT STRING'); if (el.content.length === 0) throw new Error('DER: BIT STRING gol'); return { unused: el.content[0], bytes: el.content.subarray(1) }; }