Last active
November 7, 2020 00:32
-
-
Save mildsunrise/cc1a3edf0f7d34cbe37e73bf3e284c9d to your computer and use it in GitHub Desktop.
low-level DER encoding / decoding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Low-level DER parser / encoder | |
*/ | |
/** */ | |
export interface PrimitiveDerNode { | |
tagClass: TagClass | |
tagNumber: bigint | |
value: Buffer | |
} | |
export interface ConstructedDerNode { | |
tagClass: TagClass | |
tagNumber: bigint | |
value: DerNode[] | |
} | |
export type DerNode = PrimitiveDerNode | ConstructedDerNode | |
export enum TagClass { | |
UNIVERSAL = 0, | |
APPLICATION = 1, | |
CONTEXT_SPECIFIC = 2, | |
PRIVATE = 3, | |
} | |
export enum UniversalType { | |
EOC = 0, | |
BOOLEAN = 1, | |
INTEGER = 2, | |
BIT_STRING = 3, | |
OCTET_STRING = 4, | |
NULL = 5, | |
OBJECT_IDENTIFIER = 6, | |
Object_Descriptor = 7, | |
EXTERNAL = 8, | |
REAL = 9, // float | |
ENUMERATED = 10, | |
EMBEDDED_PDV = 11, | |
UTF8String = 12, | |
RELATIVE_OID = 13, | |
TIME = 14, | |
// 15 is reserved | |
SEQUENCE = 16, // and SEQUENCE OF | |
SET = 17, // and SET OF | |
NumericString = 18, | |
PrintableString = 19, | |
T61String = 20, | |
VideotexString = 21, | |
IA5String = 22, | |
UTCTime = 23, | |
GeneralizedTime = 24, | |
GraphicString = 25, | |
VisibleString = 26, | |
GeneralString = 27, | |
UniversalString = 28, | |
CHARACTER_STRING = 29, | |
BMPString = 30, | |
DATE = 31, | |
TIME_OF_DAY = 32, | |
DATE_TIME = 33, | |
DURATION = 34, | |
OID_IRI = 35, | |
RELATIVE_OID_IRI = 36, | |
} | |
export function decodeDer(data: Buffer) { | |
const result: DerNode[] = [] | |
function next() { | |
if (!data.length) | |
throw Error('unexpected EOF') | |
const x = data[0] | |
data = data.subarray(1) | |
return x | |
} | |
function readLength() { | |
let byte = next() | |
let rest = byte & 127 | |
if (!(byte >> 7)) | |
return BigInt(rest) | |
if (rest === 0) | |
throw Error('[DER] indefinite form not allowed') | |
if (rest === 127) | |
throw Error('reserved length value') | |
let length = BigInt(next()) | |
if (!length || (rest === 0 && length < 128)) | |
throw Error('[DER] length must be encoded with minimum octets') | |
while ((--rest) > 0) | |
length = (length << BigInt(8)) | BigInt(next()) | |
return length | |
} | |
while (data.length) { | |
let byte = next() | |
const tagClass = byte >> 6 | |
const constructed = Boolean((byte >> 5) & 1) | |
let tagNumber = BigInt(byte & 31) | |
if (tagNumber === BigInt(31)) { | |
byte = next() | |
tagNumber = BigInt(byte & 127) | |
if (!tagNumber || byte < 31) | |
throw Error('tag must be encoded with minimum octets') | |
while (byte >> 7) { | |
byte = next() | |
tagNumber = (tagNumber << BigInt(7)) | BigInt(byte & 127) | |
} | |
} | |
const length = readLength() | |
if (data.length < length) | |
throw Error('unexpected EOF') | |
const value = data.subarray(0, Number(length)) | |
data = data.subarray(Number(length)) | |
if (constructed) | |
result.push({ tagClass, tagNumber, value: decodeDer(value) }) | |
else | |
result.push({ tagClass, tagNumber, value }) | |
} | |
return result | |
} | |
export function encodeDerNode({ tagClass, tagNumber, value }: DerNode): Buffer { | |
if (!((tagClass >>> 0) === tagClass && tagClass < 4)) | |
throw Error(`invalid tagClass ${tagClass}`) | |
if (tagNumber < BigInt(0)) | |
throw Error(`invalid tagNumber ${tagNumber}`) | |
const tag: number[] = [] | |
if (tagNumber >= BigInt(31)) { | |
tag.unshift(Number(tagNumber & BigInt(127))) | |
while (tagNumber >>= BigInt(7)) | |
tag.unshift((1 << 7) | Number(tagNumber & BigInt(127))) | |
tagNumber = BigInt(31) | |
} | |
tag.unshift((tagClass << 6) | Number(tagNumber)) | |
if (!(value instanceof Uint8Array)) { | |
tag[0] |= 1 << 5 | |
value = encodeDer(value) | |
} | |
let length = [] | |
if (value.length <= 127) { | |
length.push(value.length) | |
} else { | |
let nlength = value.length | |
while (nlength) { | |
length.unshift(nlength & 0xFF) | |
nlength >>= 8 | |
} | |
length.unshift((1 << 7) | length.length) | |
} | |
return Buffer.concat([ Buffer.from(tag), Buffer.from(length), value ]) | |
} | |
export const encodeDer = (nodes: DerNode[]) => | |
Buffer.concat(nodes.map(encodeDerNode)) | |
// Helpers | |
export const isPrimitive = (node: DerNode): node is PrimitiveDerNode => | |
node.value instanceof Uint8Array | |
export const isConstructed = (node: DerNode): node is ConstructedDerNode => | |
!(node.value instanceof Uint8Array) | |
export const isUniversal = (node: DerNode, type: UniversalType) => | |
node.tagClass === TagClass.UNIVERSAL && node.tagNumber === BigInt(type) | |
export const isSequence = (node: DerNode): | |
node is { tagClass: 0, tagNumber: 16n, value: DerNode[] } => | |
isUniversal(node, UniversalType.SEQUENCE) && isConstructed(node) | |
export const isSet = (node: DerNode): | |
node is { tagClass: 0, tagNumber: 17n, value: DerNode[] } => | |
isUniversal(node, UniversalType.SET) && isConstructed(node) | |
export const isOctetString = (node: DerNode) => | |
isUniversal(node, UniversalType.OCTET_STRING) | |
export const isPrimitiveOctetString = (node: DerNode): | |
node is { tagClass: 0, tagNumber: 4n, value: Buffer } => | |
isOctetString(node) && isPrimitive(node) | |
export const isConstructedOctetString = (node: DerNode): | |
node is { tagClass: 0, tagNumber: 4n, value: DerNode[] } => | |
isOctetString(node) && isConstructed(node) | |
export const isInteger = (node: DerNode): | |
node is { tagClass: 0, tagNumber: 2n, value: Buffer } => | |
isUniversal(node, UniversalType.INTEGER) && isPrimitive(node) | |
// Value decoding / encoding | |
export const asUniversal = (type: UniversalType, value: DerNode[] | Buffer): DerNode => | |
({ tagClass: TagClass.UNIVERSAL, tagNumber: BigInt(type), value: value as any }) | |
export const asSequence = (...nodes: DerNode[]): ConstructedDerNode => | |
({ tagClass: TagClass.UNIVERSAL, tagNumber: BigInt(UniversalType.SEQUENCE), value: nodes }) | |
export function getSequence(node: DerNode) { | |
if (!isSequence(node)) | |
throw Error('expected SEQUENCE node') | |
return node.value | |
} | |
export function asInteger(x: bigint): DerNode { | |
let value: number[] = [] | |
do { | |
value.unshift(Number(x & BigInt(255))) | |
x >>= BigInt(8) | |
} while (x !== -BigInt(value[0] >> 7)) | |
return asUniversal(UniversalType.INTEGER, Buffer.from(value)) | |
} | |
export function getInteger(node: DerNode) { | |
if (!isInteger(node)) | |
throw Error('expected INTEGER node') | |
if (node.value.length < 1) | |
throw Error('integer requires at least one byte') | |
const first = (node.value[0] << 1) | (node.value[1] >> 7) | |
if (node.value.length > 1 && (first === 0 || first === 0x1FF)) | |
throw Error('must be encoded in the least possible amount') | |
const initial = -BigInt(node.value[0] >> 7) | |
return node.value.reduce((n, x) => BigInt(x) | (n << BigInt(8)), initial) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment