UNPKG

@walletpass/pass-js

Version:

Apple Wallet Pass generating and pushing updates from Node.js

222 lines 8.63 kB
// SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2017-2026 Konstantin Vyatkin <tino@vtkn.io> export function rawDer(value) { return Buffer.from(value); } export function encodeLength(length) { if (!Number.isSafeInteger(length) || length < 0) throw new RangeError(`Invalid DER length: ${length}`); if (length < 0x80) return Buffer.from([length]); const bytes = []; let remaining = length; while (remaining > 0) { bytes.unshift(remaining & 0xff); remaining = Math.floor(remaining / 0x100); } return Buffer.from([0x80 | bytes.length, ...bytes]); } function encodeTlv(tag, content) { if (!Number.isInteger(tag) || tag < 0 || tag > 0xff) throw new RangeError(`Invalid DER tag: ${tag}`); return Buffer.concat([ Buffer.from([tag]), encodeLength(content.length), content, ]); } export function sequence(...values) { return encodeTlv(0x30, Buffer.concat(values)); } export function setOfContent(...values) { const sorted = values.map(value => Buffer.from(value)); sorted.sort((a, b) => Buffer.compare(a, b)); return Buffer.concat(sorted); } export function setOf(...values) { return encodeTlv(0x31, setOfContent(...values)); } export function contextSpecificConstructed(tagNumber, content) { if (!Number.isInteger(tagNumber) || tagNumber < 0 || tagNumber > 30) throw new RangeError(`Unsupported context-specific tag: ${tagNumber}`); return encodeTlv(0xa0 | tagNumber, content); } export function objectIdentifier(value) { const parts = value.split('.'); if (parts.length < 2) throw new Error(`Invalid object identifier: ${value}`); const arcs = parts.map(part => { if (!/^(0|[1-9]\d*)$/.test(part)) throw new Error(`Invalid object identifier: ${value}`); return BigInt(part); }); const first = arcs[0]; const second = arcs[1]; if (first === undefined || second === undefined || first > 2n || (first < 2n && second > 39n)) throw new Error(`Invalid object identifier: ${value}`); const encoded = [ ...encodeOidArc(first * 40n + second), ...arcs.slice(2).flatMap(encodeOidArc), ]; return encodeTlv(0x06, Buffer.from(encoded)); } function encodeOidArc(value) { if (value < 0n) throw new RangeError('OID arcs must be non-negative'); if (value === 0n) return [0]; const bytes = []; let remaining = value; while (remaining > 0n) { bytes.unshift(Number(remaining & 0x7fn)); remaining >>= 7n; } for (let i = 0; i < bytes.length - 1; i += 1) bytes[i] |= 0x80; return bytes; } export function integer(value) { const asBigInt = typeof value === 'bigint' ? value : BigInt(value); if (asBigInt < 0n) throw new RangeError('Only non-negative INTEGERs are supported'); let content; if (asBigInt === 0n) { content = Buffer.from([0]); } else { const bytes = []; let remaining = asBigInt; while (remaining > 0n) { bytes.unshift(Number(remaining & 0xffn)); remaining >>= 8n; } if ((bytes[0] ?? 0) & 0x80) bytes.unshift(0); content = Buffer.from(bytes); } return encodeTlv(0x02, content); } export function octetString(value) { return encodeTlv(0x04, rawDer(value)); } export function nullValue() { return Buffer.from([0x05, 0x00]); } export function utcTime(value) { const timestamp = value.getTime(); if (!Number.isFinite(timestamp)) throw new Error('Invalid UTCTime date'); const year = value.getUTCFullYear(); if (year < 1950 || year > 2049) throw new RangeError(`UTCTime year out of range: ${year}`); const encoded = pad2(year % 100) + pad2(value.getUTCMonth() + 1) + pad2(value.getUTCDate()) + pad2(value.getUTCHours()) + pad2(value.getUTCMinutes()) + pad2(value.getUTCSeconds()) + 'Z'; return encodeTlv(0x17, Buffer.from(encoded, 'ascii')); } function pad2(value) { return value.toString().padStart(2, '0'); } export function extractCertificateInfo(certificateDer) { const der = rawDer(certificateDer); try { const certificate = readDerElement(der, 0); expectTag(certificate, 0x30, 'certificate'); if (certificate.end !== der.length) throw new Error('certificate has trailing data'); const tbsCertificate = readDerElement(der, certificate.contentStart); expectTag(tbsCertificate, 0x30, 'tbsCertificate'); let cursor = tbsCertificate.contentStart; const firstTbsField = readChildElement(der, cursor, tbsCertificate, 'first tbsCertificate field', 'tbsCertificate'); if (firstTbsField.tag === 0xa0) { const version = readChildElement(der, firstTbsField.contentStart, firstTbsField, 'certificate version', 'certificate version wrapper'); expectTag(version, 0x02, 'certificate version'); if (version.end !== firstTbsField.end) throw new Error('certificate version field has trailing data'); cursor = firstTbsField.end; } const serialNumber = readChildElement(der, cursor, tbsCertificate, 'certificate serialNumber', 'tbsCertificate'); expectTag(serialNumber, 0x02, 'certificate serialNumber'); cursor = serialNumber.end; const signature = readChildElement(der, cursor, tbsCertificate, 'certificate signature algorithm', 'tbsCertificate'); expectTag(signature, 0x30, 'certificate signature algorithm'); cursor = signature.end; const issuer = readChildElement(der, cursor, tbsCertificate, 'certificate issuer', 'tbsCertificate'); expectTag(issuer, 0x30, 'certificate issuer'); return { rawCertificate: Buffer.from(certificate.raw), tbsCertificate: Buffer.from(tbsCertificate.raw), serialNumber: Buffer.from(serialNumber.raw), issuer: Buffer.from(issuer.raw), }; } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error(`Failed to parse X.509 certificate DER: ${detail}`, { cause: error, }); } } function readChildElement(input, offset, parent, label, parentLabel) { if (offset < parent.contentStart || offset >= parent.end) throw new Error(`${label} starts outside ${parentLabel} bounds`); const element = readDerElement(input, offset); if (element.end > parent.end) throw new Error(`${label} exceeds ${parentLabel} bounds`); return element; } function readDerElement(input, offset) { if (!Number.isSafeInteger(offset) || offset < 0 || offset >= input.length) throw new Error(`element offset ${offset} is outside input`); const tag = input[offset]; if (tag === undefined) throw new Error(`missing tag at offset ${offset}`); if ((tag & 0x1f) === 0x1f) throw new Error(`high-tag-number form is unsupported at offset ${offset}`); const firstLength = input[offset + 1]; if (firstLength === undefined) throw new Error(`missing length at offset ${offset}`); let contentStart = offset + 2; let length; if ((firstLength & 0x80) === 0) { length = firstLength; } else { const lengthOctets = firstLength & 0x7f; if (lengthOctets === 0) throw new Error('indefinite lengths are not DER'); if (lengthOctets > 6) throw new Error('DER length is too large'); if (contentStart + lengthOctets > input.length) throw new Error(`truncated length at offset ${offset}`); if (input[contentStart] === 0) throw new Error(`non-minimal length at offset ${offset}`); length = 0; for (let i = 0; i < lengthOctets; i += 1) length = length * 0x100 + input[contentStart + i]; if (length < 0x80) throw new Error(`non-minimal length at offset ${offset}`); contentStart += lengthOctets; } const end = contentStart + length; if (end > input.length) throw new Error(`element overruns input at offset ${offset}`); return { tag, contentStart, end, raw: input.subarray(offset, end), }; } function expectTag(element, expected, label) { if (element.tag !== expected) throw new Error(`${label} has tag 0x${element.tag.toString(16)}, expected 0x${expected.toString(16)}`); } //# sourceMappingURL=der.js.map