Skip to content

Instantly share code, notes, and snippets.

@mildsunrise
Last active November 7, 2020 00:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mildsunrise/cc1a3edf0f7d34cbe37e73bf3e284c9d to your computer and use it in GitHub Desktop.
Save mildsunrise/cc1a3edf0f7d34cbe37e73bf3e284c9d to your computer and use it in GitHub Desktop.
low-level DER encoding / decoding
/**
* 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