166 lines
7.6 KiB
JavaScript
166 lines
7.6 KiB
JavaScript
// 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) };
|
|
}
|