aere-quantum/pq-pki/der.mjs

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) };
}