UNPKG

@instun/sm2-multikey

Version:

A JavaScript library for generating and working with SM2Multikey key pairs and digital signatures. Compatible with both Node.js and fibjs runtimes.

475 lines (442 loc) 13 kB
/*! * Copyright (c) 2024 Instun, Inc. All rights reserved. */ /** * @fileoverview DER (Distinguished Encoding Rules) Format Handling * * This module implements the DER encoding rules as specified in ITU-T X.690, * with specific focus on SM2 cryptographic data structures. DER is a restricted * variant of BER that ensures canonical encoding for each value, making it * suitable for cryptographic applications. * * Key Features: * - ASN.1 type encoding/decoding * - Strict DER compliance * - Zero-copy operations * - Input format flexibility * - Type-preserving output * * Security Considerations: * - Strict DER validation * - Length field verification * - Buffer overflow protection * - ASN.1 type checking * - Memory safety * * Performance Notes: * - Zero-copy operations * - Minimal allocations * - Early validation * - Efficient buffering * - Type preservation * * DER Format Structure: * ``` * | Type | Length | Value | * |------|---------|-------| * | Tag | Size | Data | * | 1B | 1-5B | nB | * ``` * * Standards Compliance: * - ITU-T X.690: DER Encoding Rules * - ITU-T X.680: ASN.1 Notation * - RFC 5280: X.509 Certificate Format * - GM/T 0009-2012: SM2 Digital Signature * - GM/T 0010-2012: SM2 Public Key Format * * Usage Example: * ```javascript * import { encodeDERSequence, encodeDERLength } from './der.js'; * * // Create a DER sequence * const sequence = encodeDERSequence([ * // SM2 signature (r,s) values * encodeDERInteger(r), * encodeDERInteger(s) * ]); * * // Parse DER length * const [length, offset] = readDERLength(sequence, 0); * console.log('Sequence length:', length); * ``` * * @module formats/der * @see {@link https://www.itu.int/rec/T-REC-X.690/|X.690 Specification} * @see {@link https://www.itu.int/rec/T-REC-X.680/|X.680 Specification} */ import { FormatError, ErrorCodes } from '../core/errors.js'; import { isValidBinaryData, toBuffer, matchBinaryType } from '../utils/binary.js'; /** * ASN.1 type tags used in DER encoding * * These tags identify the type of data being encoded according to * ASN.1 (Abstract Syntax Notation One) X.680 specification. Each * tag has specific encoding rules and constraints. * * Tag Structure: * ``` * |Class|P/C| Tag Number | * | 0 0 | 0 | x x x x x | * ``` * * Tag Classes: * - Universal (00): Standard ASN.1 types * - Application (01): Application-specific * - Context-specific (10): Context-dependent * - Private (11): Private use * * @enum {number} * @readonly */ export const ASN1 = { /** * Universal type for sequences (0x30) * Used for ordered collections of values */ SEQUENCE: 0x30, /** * Universal type for integers (0x02) * Used for arbitrary precision integers */ INTEGER: 0x02, /** * Universal type for octet strings (0x04) * Used for arbitrary byte sequences */ OCTET_STRING: 0x04, /** * Universal type for object identifiers (0x06) * Used for OID values like algorithm IDs */ OBJECT_IDENTIFIER: 0x06, /** * Context-specific type (0xa1) * Used for application-dependent values */ CONTEXT_SPECIFIC: 0xa1, /** * Universal type for bit strings (0x03) * Used for arbitrary bit sequences */ BIT_STRING: 0x03, }; /** * Read and decode a DER length field * * This function implements the DER length field decoding rules from X.690, * supporting both short and long form encodings. It performs strict validation * to ensure compliance and security. * * Processing Steps: * 1. Input validation * 2. Initial byte analysis * 3. Length field decoding * 4. Minimal encoding check * 5. Range validation * * Security Considerations: * - Strict format validation * - Integer overflow protection * - Buffer bounds checking * - Minimal encoding enforcement * - Memory safety checks * * Performance Notes: * - Early validation failures * - Zero-copy operations * - Minimal allocations * - Efficient bit operations * - Type preservation * * Length Field Format: * ``` * Short form (0-127): * | 0 | Length Value | * | 0 | x x x x x x x | * * Long form (>=128): * | 1 | Byte Count | Length Bytes | * | 1 | n n n n n n n | n bytes | * ``` * * @param {Buffer|Uint8Array} data - DER encoded data * @param {number} offset - Starting offset in data * @returns {[number, number]} Tuple of [length value, new offset] * @throws {FormatError} If length encoding is invalid * * @example * ```javascript * // Read a DER length field * const buffer = Buffer.from([0x82, 0x02, 0x7F]); // 639 in long form * const [length, newOffset] = readDERLength(buffer, 0); * console.log(length); // 639 * console.log(newOffset); // 3 * * // Handle short form * const short = Buffer.from([0x7F]); // 127 in short form * const [len, off] = readDERLength(short, 0); * console.log(len); // 127 * console.log(off); // 1 * ``` */ export function readDERLength(data, offset) { if (!isValidBinaryData(data)) { throw new FormatError('Input must be a Buffer or Uint8Array', { code: ErrorCodes.ERR_FORMAT_INPUT }); } if (typeof offset !== 'number' || offset < 0) { throw new FormatError('Invalid offset', { code: ErrorCodes.ERR_FORMAT_INPUT }); } // Convert to Buffer for consistent byte access const buffer = toBuffer(data); if (offset >= buffer.length) { throw new FormatError('Invalid DER length offset', { code: ErrorCodes.ERR_FORMAT_LENGTH }); } const firstByte = buffer[offset++]; if (firstByte < 0x80) { return [firstByte, offset]; } const lenBytes = firstByte & 0x7f; if (lenBytes === 0 || offset + lenBytes > buffer.length) { throw new FormatError('Invalid DER length encoding', { code: ErrorCodes.ERR_FORMAT_INVALID }); } let length = 0; for (let i = 0; i < lenBytes; i++) { length = (length << 8) | buffer[offset++]; if (length > 0x7fffffff) { throw new FormatError('DER length too large', { code: ErrorCodes.ERR_FORMAT_LENGTH }); } } if (length <= 0x7f) { throw new FormatError('Non-minimal DER length encoding', { code: ErrorCodes.ERR_FORMAT_INVALID }); } return [length, offset]; } /** * Encode a length value in DER format * * This function implements the DER length field encoding rules from X.690, * automatically selecting between short and long form based on the value. * It ensures minimal encoding and strict DER compliance. * * Processing Steps: * 1. Value validation * 2. Form selection * 3. Byte encoding * 4. Type matching * * Security Considerations: * - Range validation * - Integer overflow protection * - Type safety checks * - Memory bounds checking * - Safe allocation * * Performance Notes: * - Minimal allocations * - Efficient encoding * - Zero-copy operations * - Type preservation * - Early validation * * Encoding Format: * ``` * Short form (0-127): * | 0xxxxxxx | Single byte value * * Long form (>=128): * | 1nnnnnnn | Length bytes | * | Count | Big-endian value | * ``` * * @param {number} length - Length value to encode * @param {Buffer|Uint8Array} [outputType] - Optional type to match output format * @returns {Buffer|Uint8Array} DER encoded length field * @throws {FormatError} If length value is invalid * * @example * ```javascript * // Encode short form length * const short = encodeDERLength(127); * console.log(short); // <Buffer 7F> * * // Encode long form length * const long = encodeDERLength(639); * console.log(long); // <Buffer 82 02 7F> * * // Match output type * const uint8 = new Uint8Array(1); * const matched = encodeDERLength(127, uint8); * console.log(matched instanceof Uint8Array); // true * ``` */ export function encodeDERLength(length, outputType) { if (typeof length !== 'number' || length < 0 || length > 0x7fffffff) { throw new FormatError('Invalid length value', { code: ErrorCodes.ERR_FORMAT_INPUT }); } if (outputType && !isValidBinaryData(outputType)) { throw new FormatError('Output type must be a Buffer or Uint8Array', { code: ErrorCodes.ERR_FORMAT_INPUT }); } let result; if (length < 0x80) { result = Buffer.from([length]); } else { const bytes = []; let temp = length; while (temp > 0) { bytes.unshift(temp & 0xff); temp >>= 8; } bytes.unshift(0x80 | bytes.length); result = Buffer.from(bytes); } return outputType ? matchBinaryType(outputType, result) : result; } /** * Encode a sequence of items in DER format * * This function implements the DER sequence encoding rules from X.690, * creating a properly formatted SEQUENCE type with the provided items. * It ensures proper ordering and type consistency. * * Processing Steps: * 1. Input validation * 2. Item conversion * 3. Length calculation * 4. Sequence assembly * 5. Type matching * * Security Considerations: * - Type validation * - Length verification * - Memory safety * - Buffer overflow protection * - Safe concatenation * * Performance Notes: * - Minimal copying * - Efficient assembly * - Pre-allocation * - Type preservation * - Early validation * * Sequence Format: * ``` * | Tag | Length | Value | * | 0x30 | DER | Encoded Items | * | 1 byte | 1-5B | Concatenated | * ``` * * @param {(Buffer|Uint8Array)[]} items - Array of pre-encoded items * @param {Buffer|Uint8Array} [outputType] - Optional type to match output format * @returns {Buffer|Uint8Array} DER encoded sequence * @throws {FormatError} If items array or any item is invalid * * @example * ```javascript * // Create a DER sequence for SM2 signature * const sequence = encodeDERSequence([ * encodeDERInteger(r), // r value * encodeDERInteger(s) // s value * ]); * * // Create a sequence with type matching * const uint8 = new Uint8Array(); * const typed = encodeDERSequence([ * encodeDERInteger(123), * encodeDERString('test') * ], uint8); * ``` */ export function encodeDERSequence(items, outputType) { if (!Array.isArray(items)) { throw new FormatError('Items must be an array', { code: ErrorCodes.ERR_FORMAT_INPUT }); } if (outputType && !isValidBinaryData(outputType)) { throw new FormatError('Output type must be a Buffer or Uint8Array', { code: ErrorCodes.ERR_FORMAT_INPUT }); } // Convert all items to Buffer and calculate total length const buffers = items.map(item => { if (!isValidBinaryData(item)) { throw new FormatError('Each item must be a Buffer or Uint8Array', { code: ErrorCodes.ERR_FORMAT_INPUT }); } return toBuffer(item); }); const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0); const lengthField = encodeDERLength(totalLength); // Combine all parts const result = Buffer.concat([ Buffer.from([ASN1.SEQUENCE]), lengthField, ...buffers ]); return outputType ? matchBinaryType(outputType, result) : result; } /** * Encode an Object Identifier in DER format * * This function implements the DER object identifier encoding rules from * X.690, creating a properly formatted OBJECT IDENTIFIER type. It handles * the specific encoding rules for OID components. * * Processing Steps: * 1. Input validation * 2. OID verification * 3. Length encoding * 4. Value assembly * 5. Type matching * * Security Considerations: * - OID validation * - Length verification * - Buffer safety * - Type checking * - Safe concatenation * * Performance Notes: * - Zero-copy when possible * - Minimal allocations * - Efficient assembly * - Type preservation * - Early validation * * OID Format: * ``` * | Tag | Length | Value | * | 0x06 | DER | OID Data | * | 1 byte | 1-5B | n bytes | * ``` * * @param {Buffer|Uint8Array} oid - Pre-encoded OID value * @param {Buffer|Uint8Array} [outputType] - Optional type to match output format * @returns {Buffer|Uint8Array} DER encoded OID * @throws {FormatError} If OID is invalid * * @example * ```javascript * // Encode an OID for SM2 signature * const oid = Buffer.from([0x2A, 0x81, 0x1C, 0xCF, 0x55, 0x01, 0x83, 0x75]); * const encoded = encodeDEROID(oid); * * // Encode with type matching * const uint8 = new Uint8Array(); * const typed = encodeDEROID(oid, uint8); * console.log(typed instanceof Uint8Array); // true * ``` */ export function encodeDEROID(oid, outputType) { if (!isValidBinaryData(oid)) { throw new FormatError('OID must be a Buffer or Uint8Array', { code: ErrorCodes.ERR_FORMAT_INPUT }); } if (outputType && !isValidBinaryData(outputType)) { throw new FormatError('Output type must be a Buffer or Uint8Array', { code: ErrorCodes.ERR_FORMAT_INPUT }); } const oidBuf = toBuffer(oid); const lengthField = encodeDERLength(oidBuf.length); const result = Buffer.concat([ Buffer.from([ASN1.OBJECT_IDENTIFIER]), lengthField, oidBuf ]); return outputType ? matchBinaryType(outputType, result) : result; }