├── .mailmap ├── source ├── interfaces │ ├── Named.mts │ ├── Long.mts │ ├── Enbyteable.mts │ ├── Enstringable.mts │ ├── Destringable.mts │ ├── Enelementable.mts │ ├── Deelementable.mts │ ├── Identified.mts │ ├── Byteable.mts │ ├── Debyteable.mts │ ├── Stringable.mts │ ├── Elementable.mts │ ├── CharacterValidationFunction.mts │ └── index.mts ├── classes │ ├── index.mts │ ├── TYPE-IDENTIFIER.mts │ └── ABSTRACT-SYNTAX.mts ├── codecs │ ├── x690 │ │ ├── encoders │ │ │ ├── encodeBoolean.mts │ │ │ ├── encodeInteger.mts │ │ │ ├── encodeObjectIdentifier.mts │ │ │ ├── encodeOIDIRI.mts │ │ │ ├── encodeTime.mts │ │ │ ├── encodeRelativeOIDIRI.mts │ │ │ ├── encodeSequence.mts │ │ │ ├── encodeReal.mts │ │ │ ├── encodeBitString.mts │ │ │ ├── encodeCharacterString.mts │ │ │ ├── encodeRelativeObjectIdentifier.mts │ │ │ ├── encodeGeneralizedTime.mts │ │ │ ├── encodeDate.mts │ │ │ ├── encodeUTCTime.mts │ │ │ ├── encodeTimeOfDay.mts │ │ │ ├── encodeDateTime.mts │ │ │ ├── encodeEmbeddedPDV.mts │ │ │ ├── encodeExternal.mts │ │ │ └── encodeDuration.mts │ │ └── decoders │ │ │ ├── decodeTime.mts │ │ │ ├── decodeOIDIRI.mts │ │ │ ├── decodeRelativeOIDIRI.mts │ │ │ ├── decodeObjectIdentifier.mts │ │ │ ├── decodeEmbeddedPDV.mts │ │ │ ├── decodeDate.mts │ │ │ ├── decodeCharacterString.mts │ │ │ ├── decodeGeneralString.mts │ │ │ ├── decodeNumericString.mts │ │ │ ├── decodeGraphicString.mts │ │ │ ├── decodeTimeOfDay.mts │ │ │ ├── decodeVisibleString.mts │ │ │ ├── decodeObjectDescriptor.mts │ │ │ ├── decodePrintableString.mts │ │ │ ├── decodeDateTime.mts │ │ │ ├── decodeRelativeObjectIdentifier.mts │ │ │ ├── decodeInteger.mts │ │ │ └── decodeExternal.mts │ ├── ber │ │ ├── encoders │ │ │ ├── encodeGeneralString.mts │ │ │ ├── encodeGraphicString.mts │ │ │ ├── encodeNumericString.mts │ │ │ ├── encodeVisibleString.mts │ │ │ ├── encodePrintableString.mts │ │ │ └── encodeObjectDescriptor.mts │ │ └── decoders │ │ │ ├── decodeBoolean.mts │ │ │ ├── decodeSequence.mts │ │ │ ├── decodeBitString.mts │ │ │ ├── decodeUTCTime.mts │ │ │ └── decodeDuration.mts │ ├── der │ │ └── decoders │ │ │ ├── decodeBoolean.mts │ │ │ ├── decodeSequence.mts │ │ │ ├── decodeBitString.mts │ │ │ ├── decodeUTCTime.mts │ │ │ ├── decodeGeneralizedTime.mts │ │ │ └── decodeDuration.mts │ ├── coer │ │ └── decoders │ │ │ └── decodeBoolean.mts │ └── cer │ │ └── decoders │ │ └── decodeSequence.mts ├── types │ ├── time │ │ ├── YEAR-ENCODING.mts │ │ ├── HOURS-ENCODING.mts │ │ ├── YEAR-MONTH-ENCODING.mts │ │ ├── HOURS-MINUTES-ENCODING.mts │ │ ├── DATE-ENCODING.mts │ │ ├── HOURS-DIFF-ENCODING.mts │ │ ├── TIME-OF-DAY-ENCODING.mts │ │ ├── HOURS-MINUTES-DIFF-ENCODING.mts │ │ ├── TIME-OF-DAY-DIFF-ENCODING.mts │ │ ├── TIME-OF-DAY-FRACTION-ENCODING.mts │ │ ├── TIME-OF-DAY-FRACTION-DIFF-ENCODING.mts │ │ ├── DURATION-INTERVAL-ENCODING.mts │ │ └── DURATION-EQUIVALENT.mts │ ├── TypeIdentifier.mts │ ├── index.mts │ ├── CharacterString.mts │ ├── External.mts │ └── EmbeddedPDV.mts ├── utils │ ├── base128Length.mts │ ├── decodeIEEE754SinglePrecisionFloat.mts │ ├── encodeIEEE754DoublePrecisionFloat.mts │ ├── encodeIEEE754SinglePrecisionFloat.mts │ ├── decodeIEEE754DoublePrecisionFloat.mts │ ├── splitOctetsCanonically.mts │ ├── getBitFromBase256.mts │ ├── getBitFromBase128.mts │ ├── setBitInBase128.mts │ ├── setBitInBase256.mts │ ├── trimLeadingPaddingBytes.mts │ ├── unpackBits.mts │ ├── packBits.mts │ ├── decodeUnsignedBigEndianInteger.mts │ ├── dissectFloat.mts │ ├── convertTextToBytes.mts │ ├── compareSetOfElementsCanonically.mts │ ├── decodeSignedBigEndianInteger.mts │ ├── sortCanonically.mts │ ├── isUniquelyTagged.mts │ ├── encodeX690Base10RealNumber.mts │ ├── isInCanonicalOrder.mts │ ├── encodeUnsignedBigEndianInteger.mts │ ├── convertBytesToText.mts │ ├── encodeSignedBigEndianInteger.mts │ ├── index.mts │ └── encodeX690BinaryRealNumber.mts ├── validators │ ├── isVisibleCharacter.mts │ ├── index.mts │ ├── isGraphicCharacter.mts │ ├── isObjectDescriptorCharacter.mts │ ├── isNumericCharacter.mts │ ├── isGeneralCharacter.mts │ ├── isPrintableCharacter.mts │ ├── validateDateTime.mts │ ├── datetimeComponentValidator.mts │ ├── validateTime.mts │ └── validateDate.mts ├── index.mts └── macros.mts ├── jsr.json ├── .editorconfig ├── denotest.mts ├── .gitattributes ├── test ├── jer.test.mjs ├── x690 │ ├── tag.test.mjs │ ├── invalid │ │ ├── overflow.test.mjs │ │ ├── truncation.test.mjs │ │ ├── padding.test.mjs │ │ ├── empty.test.mjs │ │ └── date.test.mjs │ ├── time.test.mjs │ ├── fromSeqOrSet.test.mjs │ └── real.test.mjs ├── utils │ └── reversal.test.mjs ├── asn1 │ ├── encode.test.mjs │ └── constraint.test.mjs ├── ber │ ├── invalid.test.mjs │ ├── utc.test.mjs │ └── gt.test.mjs ├── der │ ├── utc.test.mjs │ ├── invalid.test.mjs │ └── gt.test.mjs ├── cer │ └── cer.test.mjs └── misc.test.mjs ├── .github ├── workflows │ ├── publish.yml │ └── nodejs.yml └── SECURITY.md ├── deno.lock ├── LICENSE.txt ├── package.json ├── tsconfig.json ├── documentation └── library.md ├── CONTRIBUTING.md └── benchmark └── oids.mjs /.mailmap: -------------------------------------------------------------------------------- 1 | Jonathan M. Wilbur -------------------------------------------------------------------------------- /source/interfaces/Named.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents something that has a name. 3 | */ 4 | export default 5 | interface Named { 6 | name: string; 7 | } 8 | -------------------------------------------------------------------------------- /source/classes/index.mts: -------------------------------------------------------------------------------- 1 | export type { default as ABSTRACT_SYNTAX } from "./ABSTRACT-SYNTAX.mjs"; 2 | export type { default as TYPE_IDENTIFIER } from "./TYPE-IDENTIFIER.mjs"; 3 | -------------------------------------------------------------------------------- /source/interfaces/Long.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents something that has a length. (No specific units specified.) 3 | */ 4 | export default 5 | interface Long { 6 | length: number; 7 | } 8 | -------------------------------------------------------------------------------- /source/interfaces/Enbyteable.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents something that can be converted to bytes. 3 | */ 4 | export default 5 | interface Enbyteable { 6 | toBytes (): Uint8Array; 7 | } 8 | -------------------------------------------------------------------------------- /source/interfaces/Enstringable.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents something that can be converted to a string. 3 | */ 4 | export default 5 | interface Enstringable { 6 | toString(): string; 7 | } 8 | -------------------------------------------------------------------------------- /source/interfaces/Destringable.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents something that can be generated from a string. 3 | */ 4 | export default 5 | interface Destringable { 6 | fromString (str: string): T; 7 | } 8 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wildboar/asn1", 3 | "version": "11.0.0", 4 | "license": "MIT", 5 | "exports": { 6 | ".": "./source/index.mts", 7 | "./functional": "./source/functional.mts" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeBoolean.mts: -------------------------------------------------------------------------------- 1 | import type { BOOLEAN } from "../../../macros.mjs"; 2 | 3 | export default 4 | function encodeBoolean (value: BOOLEAN): Uint8Array { 5 | return new Uint8Array([ (value ? 0xFF : 0x00) ]); 6 | } 7 | -------------------------------------------------------------------------------- /source/interfaces/Enelementable.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | 3 | /** 4 | * Represents something that can be converted to an ASN.1 element. 5 | */ 6 | export default 7 | interface Enelementable { 8 | toElement(): ASN1Element; 9 | } 10 | -------------------------------------------------------------------------------- /source/interfaces/Deelementable.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | 3 | /** 4 | * Represents something that can be generated from an ASN.1 element. 5 | */ 6 | export default 7 | interface Deelementable { 8 | fromElement (el: ASN1Element): void; 9 | } 10 | -------------------------------------------------------------------------------- /source/interfaces/Identified.mts: -------------------------------------------------------------------------------- 1 | import type { OBJECT_IDENTIFIER } from "../macros.mjs"; 2 | 3 | /** 4 | * Represents an object that is associated with an X.660 Object Identifier. 5 | */ 6 | export default 7 | interface Identified { 8 | oid: OBJECT_IDENTIFIER; 9 | } 10 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeTime.mts: -------------------------------------------------------------------------------- 1 | import type { TIME } from "../../../macros.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | 4 | export default 5 | function decodeTime (bytes: Uint8Array): TIME { 6 | return convertBytesToText(bytes); 7 | } 8 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeInteger.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../../macros.mjs"; 2 | import { integerToBuffer } from "../../../utils/bigint.mjs"; 3 | 4 | export default 5 | function encodeInteger (value: INTEGER): Uint8Array { 6 | return integerToBuffer(value); 7 | } 8 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeObjectIdentifier.mts: -------------------------------------------------------------------------------- 1 | import type { OBJECT_IDENTIFIER } from "../../../macros.mjs"; 2 | import { Buffer } from "node:buffer"; 3 | 4 | export default 5 | function encodeObjectIdentifier (value: OBJECT_IDENTIFIER): Buffer { 6 | return value.toBytes(); 7 | } 8 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeOIDIRI.mts: -------------------------------------------------------------------------------- 1 | import type { OID_IRI } from "../../../macros.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | 4 | export default 5 | function decodeOIDIRI (bytes: Uint8Array): OID_IRI { 6 | return convertBytesToText(bytes); 7 | } 8 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeOIDIRI.mts: -------------------------------------------------------------------------------- 1 | import type { OID_IRI } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | 4 | export default 5 | function encodeOIDIRI (value: OID_IRI): Uint8Array { 6 | return convertTextToBytes(value); 7 | } 8 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeTime.mts: -------------------------------------------------------------------------------- 1 | import type { TIME } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | 4 | export default 5 | function encodeTime (value: TIME): Uint8Array { 6 | return convertTextToBytes(value.replace(/,/g, ".")); 7 | } 8 | -------------------------------------------------------------------------------- /source/interfaces/Byteable.mts: -------------------------------------------------------------------------------- 1 | import type Enbyteable from "./Enbyteable.mjs"; 2 | import type Debyteable from "./Debyteable.mjs"; 3 | 4 | /** 5 | * Represents something that can be converted to and from bytes. 6 | */ 7 | export default 8 | interface Byteable extends Enbyteable, Debyteable { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeRelativeOIDIRI.mts: -------------------------------------------------------------------------------- 1 | import type { RELATIVE_OID_IRI } from "../../../macros.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | 4 | export default 5 | function decodeRelativeOIDIRI (bytes: Uint8Array): RELATIVE_OID_IRI { 6 | return convertBytesToText(bytes); 7 | } 8 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeRelativeOIDIRI.mts: -------------------------------------------------------------------------------- 1 | import type { RELATIVE_OID_IRI } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | 4 | export default 5 | function encodeRelativeOIDIRI (value: RELATIVE_OID_IRI): Uint8Array { 6 | return convertTextToBytes(value); 7 | } 8 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeObjectIdentifier.mts: -------------------------------------------------------------------------------- 1 | import ObjectIdentifier from "../../../types/ObjectIdentifier.mjs"; 2 | import type { OBJECT_IDENTIFIER } from "../../../macros.mjs"; 3 | 4 | export default 5 | function decodeObjectIdentifier (value: Uint8Array): OBJECT_IDENTIFIER { 6 | return ObjectIdentifier.fromBytes(value); 7 | } 8 | -------------------------------------------------------------------------------- /source/interfaces/Debyteable.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents something that can be generated from bytes. 3 | */ 4 | export default 5 | interface Debyteable { 6 | /** 7 | * @param bytes The stream of bytes to read. 8 | * @returns The number of bytes read. 9 | */ 10 | fromBytes (bytes: Uint8Array): number; 11 | } 12 | -------------------------------------------------------------------------------- /source/interfaces/Stringable.mts: -------------------------------------------------------------------------------- 1 | import type Enstringable from "./Enstringable.mjs"; 2 | import type Destringable from "./Destringable.mjs"; 3 | 4 | /** 5 | * Represents something that can be converted to or from a string. 6 | */ 7 | export default 8 | interface Stringable extends Enstringable, Destringable { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = crlf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [Makefile] 12 | indent_size = 4 13 | indent_style = tab 14 | end_of_line = lf 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /denotest.mts: -------------------------------------------------------------------------------- 1 | // This is just a simple test file to make sure that Deno can import and use this. 2 | import { decodeUnsignedBigEndianInteger } from "./dist/index.mjs"; 3 | import { assertEquals } from "jsr:@std/assert"; 4 | 5 | Deno.test("simple test", () => { 6 | assertEquals(decodeUnsignedBigEndianInteger(new Uint8Array([ 0, 5 ])), 5); 7 | }); 8 | -------------------------------------------------------------------------------- /source/interfaces/Elementable.mts: -------------------------------------------------------------------------------- 1 | import type Deelementable from "./Deelementable.mjs"; 2 | import type Enelementable from "./Enelementable.mjs"; 3 | 4 | /** 5 | * Represents something that can be converted to or from an Element. 6 | */ 7 | export default 8 | interface Elementable extends Deelementable, Enelementable { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeSequence.mts: -------------------------------------------------------------------------------- 1 | import ASN1Element from "../../../asn1.mjs"; 2 | import type { SEQUENCE } from "../../../macros.mjs"; 3 | import { Buffer } from "node:buffer"; 4 | 5 | export default 6 | function encodeSequence (value: SEQUENCE): Uint8Array { 7 | return Buffer.concat(value.map((v) => v.toBytes())); 8 | } 9 | -------------------------------------------------------------------------------- /source/codecs/ber/encoders/encodeGeneralString.mts: -------------------------------------------------------------------------------- 1 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 2 | import type { GeneralString } from "../../../macros.mjs"; 3 | 4 | export default 5 | function encodeGeneralString (value: GeneralString): Uint8Array { 6 | const bytes: Uint8Array = convertTextToBytes(value); 7 | return bytes; 8 | } 9 | -------------------------------------------------------------------------------- /source/codecs/ber/encoders/encodeGraphicString.mts: -------------------------------------------------------------------------------- 1 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 2 | import type { GraphicString } from "../../../macros.mjs"; 3 | 4 | export default 5 | function encodeGraphicString (value: GraphicString): Uint8Array { 6 | const bytes: Uint8Array = convertTextToBytes(value); 7 | return bytes; 8 | } 9 | -------------------------------------------------------------------------------- /source/codecs/ber/encoders/encodeNumericString.mts: -------------------------------------------------------------------------------- 1 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 2 | import type { NumericString } from "../../../macros.mjs"; 3 | 4 | export default 5 | function encodeNumericString (value: NumericString): Uint8Array { 6 | const bytes: Uint8Array = convertTextToBytes(value); 7 | return bytes; 8 | } 9 | -------------------------------------------------------------------------------- /source/codecs/ber/encoders/encodeVisibleString.mts: -------------------------------------------------------------------------------- 1 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 2 | import type { VisibleString } from "../../../macros.mjs"; 3 | 4 | export default 5 | function encodeVisibleString (value: VisibleString): Uint8Array { 6 | const bytes: Uint8Array = convertTextToBytes(value); 7 | return bytes; 8 | } 9 | -------------------------------------------------------------------------------- /source/codecs/ber/encoders/encodePrintableString.mts: -------------------------------------------------------------------------------- 1 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 2 | import type { PrintableString } from "../../../macros.mjs"; 3 | 4 | export default 5 | function encodeNumericString (value: PrintableString): Uint8Array { 6 | const bytes: Uint8Array = convertTextToBytes(value); 7 | return bytes; 8 | } 9 | -------------------------------------------------------------------------------- /source/codecs/ber/encoders/encodeObjectDescriptor.mts: -------------------------------------------------------------------------------- 1 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 2 | import type { ObjectDescriptor } from "../../../macros.mjs"; 3 | 4 | export default 5 | function encodeObjectDescriptor (value: ObjectDescriptor): Uint8Array { 6 | const bytes: Uint8Array = convertTextToBytes(value); 7 | return bytes; 8 | } 9 | -------------------------------------------------------------------------------- /source/codecs/ber/decoders/decodeBoolean.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../../../errors.mjs"; 2 | import type { BOOLEAN } from "../../../macros.mjs"; 3 | 4 | export default 5 | function decodeBoolean (value: Uint8Array): BOOLEAN { 6 | if (value.length !== 1) { 7 | throw new errors.ASN1SizeError("BOOLEAN not one byte"); 8 | } 9 | return (value[0] !== 0); 10 | } 11 | -------------------------------------------------------------------------------- /source/types/time/YEAR-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | 3 | /** 4 | * Defined in ITU Recommendation X.696:2015, Section 29: 5 | * 6 | * `YEAR-ENCODING ::= SEQUENCE { 7 | * year INTEGER 8 | * }` 9 | */ 10 | export default 11 | class YEAR_ENCODING { 12 | constructor ( 13 | readonly year: INTEGER, 14 | ) {} 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | test/* linguist-vendored 2 | documentation/* linguist-documentation=true 3 | dist/* linguist-generated=true 4 | *.js linguist-detectable=false 5 | 6 | # Text files 7 | * text=crlf 8 | *.vcproj text eol=crlf 9 | *.sh text eol=lf 10 | Makefile text eol=lf 11 | 12 | # Binary files 13 | *.jpg -text 14 | *.pdf -text -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeReal.mts: -------------------------------------------------------------------------------- 1 | import encodeX690BinaryRealNumber from "../../../utils/encodeX690BinaryRealNumber.mjs"; 2 | import type { REAL } from "../../../macros.mjs"; 3 | 4 | /** 5 | * Only encodes with seven digits of precision. 6 | * @param value 7 | */ 8 | export default 9 | function encodeReal (value: REAL): Uint8Array { 10 | return encodeX690BinaryRealNumber(value); 11 | } 12 | -------------------------------------------------------------------------------- /source/utils/base128Length.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Calculates the number of bytes needed to encode a value in base-128, as used in ASN.1 OID encoding. 3 | * @param {number} numberOfBytes - The number of bytes in the original value. 4 | * @returns {number} The number of base-128 encoded bytes required. 5 | * @function 6 | */ 7 | export default 8 | function base128Length (numberOfBytes: number): number { 9 | return Math.ceil(numberOfBytes * (8 / 7)); 10 | } 11 | -------------------------------------------------------------------------------- /source/utils/decodeIEEE754SinglePrecisionFloat.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Decodes a single-precision IEEE 754 floating-point number from a `Uint8Array` 3 | * @param {Uint8Array} bytes - The bytes representing the single-precision float. 4 | * @returns {number} The decoded floating-point number. 5 | * @function 6 | */ 7 | export default 8 | function decodeIEEE754SinglePrecisionFloat (bytes: Uint8Array): number { 9 | return new Float32Array(bytes.reverse().buffer)[0]; 10 | } 11 | -------------------------------------------------------------------------------- /source/validators/isVisibleCharacter.mts: -------------------------------------------------------------------------------- 1 | import isGraphicCharacter from "./isGraphicCharacter.mjs"; 2 | 3 | const isVisibleCharacter = isGraphicCharacter; 4 | 5 | /** 6 | * @summary Checks if a character code is a valid ASN.1 `VisibleString` character 7 | * @param {number} characterCode - The character code to check. 8 | * @returns {boolean} True if the character is valid for `VisibleString`, false otherwise. 9 | * @function 10 | */ 11 | export default isVisibleCharacter; 12 | -------------------------------------------------------------------------------- /source/interfaces/CharacterValidationFunction.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a function that returns `true` or `false` to indicate the 3 | * membership of a character code point to a subset. 4 | * 5 | * For instance, this can be used to return `true` when the character 9 6 | * (character code 0x39) is supplied to a function that checks for 7 | * numeric characters. 8 | */ 9 | export default 10 | interface CharacterValidationFunction { 11 | (charCode: number): boolean; 12 | } 13 | -------------------------------------------------------------------------------- /source/utils/encodeIEEE754DoublePrecisionFloat.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Encodes a number as a double-precision IEEE 754 floating-point value in a `Uint8Array` 3 | * @param {number} value - The number to encode. 4 | * @returns {Uint8Array} The encoded double-precision float bytes. 5 | * @function 6 | */ 7 | export default 8 | function encodeIEEE754DoublePrecisionFloat (value: number): Uint8Array { 9 | return new Uint8Array(new Float64Array([ value ]).buffer).reverse(); 10 | } 11 | -------------------------------------------------------------------------------- /source/utils/encodeIEEE754SinglePrecisionFloat.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Encodes a number as a single-precision IEEE 754 floating-point value in a `Uint8Array` 3 | * @param {number} value - The number to encode. 4 | * @returns {Uint8Array} The encoded single-precision float bytes. 5 | * @function 6 | */ 7 | export default 8 | function encodeIEEE754SinglePrecisionFloat (value: number): Uint8Array { 9 | return new Uint8Array(new Float32Array([ value ]).buffer).reverse(); 10 | } 11 | -------------------------------------------------------------------------------- /source/validators/index.mts: -------------------------------------------------------------------------------- 1 | export { default as isGeneralCharacter } from "./isGeneralCharacter.mjs"; 2 | export { default as isGraphicCharacter } from "./isGraphicCharacter.mjs"; 3 | export { default as isNumericCharacter } from "./isNumericCharacter.mjs"; 4 | export { default as isObjectDescriptorCharacter } from "./isObjectDescriptorCharacter.mjs"; 5 | export { default as isPrintableCharacter } from "./isPrintableCharacter.mjs"; 6 | export { default as isVisibleCharacter } from "./isVisibleCharacter.mjs"; 7 | -------------------------------------------------------------------------------- /source/validators/isGraphicCharacter.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Checks if a character code is a valid ASN.1 `GraphicString` character (ASCII 0x20-0x7E). 3 | * @param {number} characterCode - The character code to check. 4 | * @returns {boolean} True if the character is valid for `GraphicString`, false otherwise. 5 | * @function 6 | */ 7 | export default 8 | function isGraphicCharacter (characterCode: number): boolean { 9 | return (characterCode >= 0x20 && characterCode <= 0x7E); 10 | } 11 | -------------------------------------------------------------------------------- /source/utils/decodeIEEE754DoublePrecisionFloat.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Decodes a double-precision IEEE 754 floating-point number from a `Uint8Array` 3 | * @description 4 | * @param {Uint8Array} bytes - The bytes representing the double-precision float. 5 | * @returns {number} The decoded floating-point number. 6 | * @function 7 | */ 8 | export default 9 | function decodeIEEE754DoublePrecisionFloat (bytes: Uint8Array): number { 10 | return new Float64Array(bytes.reverse().buffer)[0]; 11 | } 12 | -------------------------------------------------------------------------------- /test/jer.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | describe("JSON Encoding Rules", () => { 6 | it("Converts a BigInt into a string", () => { 7 | const el = new asn1.BERElement(); 8 | el.tagNumber = 2; 9 | el.integer = 45081095376109356095179030960913561n; 10 | assert.equal(el.toJSON(), "45081095376109356095179030960913561"); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Taken from https://jsr.io/docs/publishing-packages#publishing-from-github-actions 2 | # .github/workflows/publish.yml 3 | 4 | name: Publish 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | id-token: write # The OIDC ID token is used for authentication with JSR. 17 | steps: 18 | - uses: actions/checkout@v4 19 | - run: npx jsr publish 20 | -------------------------------------------------------------------------------- /source/validators/isObjectDescriptorCharacter.mts: -------------------------------------------------------------------------------- 1 | import isGraphicCharacter from "./isGraphicCharacter.mjs"; 2 | 3 | const isObjectDescriptorCharacter = isGraphicCharacter; 4 | 5 | /** 6 | * @summary Checks if a character code is a valid ASN.1 `ObjectDescriptor` character 7 | * @param {number} characterCode - The character code to check. 8 | * @returns {boolean} True if the character is valid for `ObjectDescriptor`, false otherwise. 9 | * @function 10 | */ 11 | export default isObjectDescriptorCharacter; 12 | -------------------------------------------------------------------------------- /source/validators/isNumericCharacter.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Checks if a character code is a valid ASN.1 `NumericString` character (digits and space). 3 | * @param {number} characterCode - The character code to check. 4 | * @returns {boolean} True if the character is valid for `NumericString`, false otherwise. 5 | * @function 6 | */ 7 | export default 8 | function isNumericString (characterCode: number): boolean { 9 | return ((characterCode >= 0x30 && characterCode <= 0x39) || characterCode === 0x20); 10 | } 11 | -------------------------------------------------------------------------------- /source/codecs/der/decoders/decodeBoolean.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../../../errors.mjs"; 2 | import type { BOOLEAN } from "../../../macros.mjs"; 3 | 4 | export default 5 | function decodeBoolean (value: Uint8Array): BOOLEAN { 6 | if (value.length !== 1) { 7 | throw new errors.ASN1SizeError("BOOLEAN not one byte"); 8 | } 9 | if ((value[0] !== 0x00) && (value[0] !== 0xFF)) { 10 | throw new errors.ASN1Error("BOOLEAN must be encoded as 0xFF or 0x00."); 11 | } 12 | return (value[0] !== 0); 13 | } 14 | -------------------------------------------------------------------------------- /source/codecs/coer/decoders/decodeBoolean.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../../../errors.mjs"; 2 | import type { BOOLEAN } from "../../../macros.mjs"; 3 | 4 | export default 5 | function decodeBoolean (value: Uint8Array): BOOLEAN { 6 | if (value.length !== 1) { 7 | throw new errors.ASN1SizeError("BOOLEAN not one byte"); 8 | } 9 | if ((value[0] !== 0x00) && (value[0] !== 0xFF)) { 10 | throw new errors.ASN1Error("BOOLEAN must be encoded as 0xFF or 0x00."); 11 | } 12 | return (value[0] !== 0); 13 | } 14 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeEmbeddedPDV.mts: -------------------------------------------------------------------------------- 1 | import EmbeddedPDV from "../../../types/EmbeddedPDV.mjs"; 2 | import decodeSequence from "../../der/decoders/decodeSequence.mjs"; 3 | import type { EMBEDDED_PDV } from "../../../macros.mjs"; 4 | 5 | export default 6 | function decodeEmbeddedPDV (value: Uint8Array): EMBEDDED_PDV { 7 | const components = decodeSequence(value); 8 | const identification = components[0]; 9 | const dataValue = components[1].octetString; 10 | return new EmbeddedPDV(identification, dataValue); 11 | } 12 | -------------------------------------------------------------------------------- /source/codecs/ber/decoders/decodeSequence.mts: -------------------------------------------------------------------------------- 1 | import BERElement from "../../ber.mjs"; 2 | import type { SEQUENCE } from "../../../macros.mjs"; 3 | 4 | export default 5 | function decodeSequence (value: Uint8Array): SEQUENCE { 6 | const encodedElements: BERElement[] = []; 7 | let i: number = 0; 8 | while (i < value.length) { 9 | const next: BERElement = new BERElement(); 10 | i += next.fromBytes(value.subarray(i)); 11 | encodedElements.push(next); 12 | } 13 | return encodedElements; 14 | } 15 | -------------------------------------------------------------------------------- /source/types/time/HOURS-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `HOURS-ENCODING ::= SEQUENCE { 8 | * hours INTEGER (0..24) 9 | * }` 10 | */ 11 | export default 12 | class HOURS_ENCODING { 13 | constructor ( 14 | readonly hours: INTEGER, 15 | ) { 16 | datetimeComponentValidator("hour", 0, 24)("HOURS-ENCODING", hours); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/utils/splitOctetsCanonically.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Splits a `Uint8Array` into chunks of up to 1000 bytes, as required for canonical ASN.1 encoding 3 | * @param {Uint8Array} value - The byte array to split. 4 | * @yields {Uint8Array} Chunks of up to 1000 bytes. 5 | * @function 6 | */ 7 | export default 8 | function *splitOctetsCanonically (value: Uint8Array): IterableIterator { 9 | for (let i: number = 0; i < value.length; i += 1000) { 10 | // This will not throw an index error. 11 | yield value.subarray(i, i + 1000); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/types/TypeIdentifier.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | import ObjectIdentifier from "./ObjectIdentifier.mjs"; 3 | 4 | /** 5 | * From ITU X.681, Annex A.2: 6 | * ```asn1 7 | * TYPE-IDENTIFIER ::= CLASS { 8 | * &id OBJECT IDENTIFIER UNIQUE, 9 | * &Type } 10 | * WITH SYNTAX { 11 | * &Type 12 | * IDENTIFIED BY &id 13 | * } 14 | * ``` 15 | */ 16 | export default 17 | class TypeIdentifier { 18 | constructor ( 19 | readonly id: ObjectIdentifier, 20 | readonly type: Element, 21 | ) {} 22 | } 23 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeBitString.mts: -------------------------------------------------------------------------------- 1 | import type { BIT_STRING } from "../../../macros.mjs"; 2 | import packBits from "../../../utils/packBits.mjs"; 3 | 4 | export default 5 | function encodeBitString (value: BIT_STRING): Uint8Array { 6 | if (value.length === 0) { 7 | return new Uint8Array([ 0 ]); 8 | } 9 | const ret: Uint8Array = new Uint8Array(((value.length >>> 3) + ((value.length % 8) ? 1 : 0)) + 1); 10 | ret[0] = (8 - (value.length % 8)); 11 | if (ret[0] === 8) { 12 | ret[0] = 0; 13 | } 14 | ret.set(packBits(value), 1); 15 | return ret; 16 | } 17 | -------------------------------------------------------------------------------- /source/codecs/cer/decoders/decodeSequence.mts: -------------------------------------------------------------------------------- 1 | import CERElement from "../../cer.mjs"; 2 | import type { SEQUENCE } from "../../../macros.mjs"; 3 | 4 | export default 5 | function decodeSequence (value: Uint8Array): SEQUENCE { 6 | if (value.length === 0) { 7 | return []; 8 | } 9 | const encodedElements: CERElement[] = []; 10 | let i: number = 0; 11 | while (i < value.length) { 12 | const next: CERElement = new CERElement(); 13 | i += next.fromBytes(value.subarray(i)); 14 | encodedElements.push(next); 15 | } 16 | return encodedElements; 17 | } 18 | -------------------------------------------------------------------------------- /source/codecs/der/decoders/decodeSequence.mts: -------------------------------------------------------------------------------- 1 | import DERElement from "../../der.mjs"; 2 | import type { SEQUENCE } from "../../../macros.mjs"; 3 | 4 | export default 5 | function decodeSequence (value: Uint8Array): SEQUENCE { 6 | if (value.length === 0) { 7 | return []; 8 | } 9 | const encodedElements: DERElement[] = []; 10 | let i: number = 0; 11 | while (i < value.length) { 12 | const next: DERElement = new DERElement(); 13 | i += next.fromBytes(value.subarray(i)); 14 | encodedElements.push(next); 15 | } 16 | return encodedElements; 17 | } 18 | -------------------------------------------------------------------------------- /source/utils/getBitFromBase256.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Gets the value of a bit at a given index from a base-256 encoded `Uint8Array` 3 | * @description 4 | * Used for ASN.1 `BIT STRING` encoding. 5 | * @param {Uint8Array} from - The base-256 encoded byte array. 6 | * @param {number} bitIndex - The index of the bit to retrieve. 7 | * @returns {boolean} True if the bit is set, false otherwise. 8 | * @function 9 | */ 10 | export default 11 | function getBit (from: Uint8Array, bitIndex: number): boolean { 12 | return ((from[from.length - (Math.floor(bitIndex / 8) + 1)] & (0x01 << (bitIndex % 8))) > 0); 13 | } 14 | -------------------------------------------------------------------------------- /test/x690/tag.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(CodecElement.constructor.name, () => { 11 | it("decodes tag class correctly", () => { 12 | const el = new CodecElement(); 13 | el.fromBytes(new Uint8Array([ 0xF0, 0x00 ])); 14 | assert.equal(el.tagClass, asn1.ASN1TagClass.private); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /source/utils/getBitFromBase128.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Gets the value of a bit at a given index from a base-128 encoded `Uint8Array` 3 | * @description 4 | * @param {Uint8Array} from - The base-128 encoded byte array. 5 | * @param {number} bitIndex - The index of the bit to retrieve. 6 | * @returns {boolean} True if the bit is set, false otherwise. 7 | * @function 8 | */ 9 | export default 10 | function getBitFromBase128 (from: Uint8Array, bitIndex: number): boolean { 11 | const byteIndex: number = (from.length - (Math.floor(bitIndex / 7) + 1)); 12 | return ((from[byteIndex] & (0x01 << (bitIndex % 7))) > 0); 13 | } 14 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeDate.mts: -------------------------------------------------------------------------------- 1 | import type { DATE } from "../../../macros.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import validateDate from "../../../validators/validateDate.mjs"; 4 | 5 | export default 6 | function decodeDate (bytes: Uint8Array): DATE { 7 | const str: string = convertBytesToText(bytes); 8 | const year: number = parseInt(str.slice(0, 4), 10); 9 | const month: number = parseInt(str.slice(4, 6), 10) - 1; 10 | const day: number = parseInt(str.slice(6, 8), 10); 11 | validateDate("DATE", year, month, day); 12 | return new Date(year, month, day); 13 | } 14 | -------------------------------------------------------------------------------- /source/types/time/YEAR-MONTH-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `YEAR-MONTH-ENCODING ::= SEQUENCE { 8 | * year INTEGER, 9 | * month INTEGER (1..12) 10 | * }` 11 | */ 12 | export default 13 | class YEAR_MONTH_ENCODING { 14 | constructor ( 15 | readonly year: INTEGER, 16 | readonly month: INTEGER, 17 | ) { 18 | datetimeComponentValidator("month", 1, 12)("YEAR-MONTH-ENCODING", month); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | For security issues--and security issues **only**--you may directly email 4 | [Jonathan M. Wilbur](mailto:jonathan@wilbur.space) (jonathan@wilbur.space). 5 | If you would like, you may also create an issue on the GitHub issues page, 6 | **as long as you do not mention any details about the vulnerability.** 7 | 8 | If you do not hear back from him in a few days, and if the issue is of low 9 | severity, you may create an issue on GitHub, or add the details as to what 10 | it is. Remember that bad people can read GitHub issues and use that information 11 | to hack other people, so disclose information responsibly. 12 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@std/assert@*": "1.0.11", 5 | "jsr:@std/internal@^1.0.5": "1.0.5" 6 | }, 7 | "jsr": { 8 | "@std/assert@1.0.11": { 9 | "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1", 10 | "dependencies": [ 11 | "jsr:@std/internal" 12 | ] 13 | }, 14 | "@std/internal@1.0.5": { 15 | "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" 16 | } 17 | }, 18 | "workspace": { 19 | "packageJson": { 20 | "dependencies": [ 21 | "npm:@types/node@^22.10.5", 22 | "npm:typescript@^5.7.2" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [22.x, 23.x] 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@v4 14 | - name: Install Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Install NPM Dependencies 19 | run: npm ci 20 | - name: Build 21 | run: npm run build 22 | - name: Run Tests 23 | run: npm test 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /test/utils/reversal.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | // const decodeSignedBigEndianInteger = require("../../dist/node/utils/decodeSignedBigEndianInteger.js").default; 6 | // const encodeSignedBigEndianInteger = require("../../dist/node/utils/encodeSignedBigEndianInteger.js").default; 7 | 8 | describe("encodeSignedBigEndianInteger()", () => { 9 | it("works", () => { 10 | for (let i = -100000; i < 100000; i += 7) { 11 | assert.equal(asn1.decodeSignedBigEndianInteger(asn1.encodeSignedBigEndianInteger(i)), i); 12 | } 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /source/utils/setBitInBase128.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Sets or clears a bit at a given index in a base-128 encoded `Uint8Array` 3 | * @param {Uint8Array} to - The base-128 encoded byte array to modify. 4 | * @param {number} bitIndex - The index of the bit to set or clear. 5 | * @param {boolean} value - True to set the bit, false to clear it. 6 | * @function 7 | */ 8 | export default 9 | function setBit (to: Uint8Array, bitIndex: number, value: boolean): void { 10 | const byteIndex = to.length - (Math.floor(bitIndex / 7) + 1); 11 | if (value) { 12 | to[byteIndex] |= (0x01 << (bitIndex % 7)); 13 | } else { 14 | to[byteIndex] &= ~(0x01 << (bitIndex % 7)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeCharacterString.mts: -------------------------------------------------------------------------------- 1 | import CharacterString from "../../../types/CharacterString.mjs"; 2 | import DERElement from "../../../codecs/der.mjs"; 3 | import { ASN1TagClass, ASN1UniversalType, ASN1Construction } from "../../../values.mjs"; 4 | import encodeSequence from "./encodeSequence.mjs"; 5 | 6 | export default 7 | function encodeCharacterString (value: CharacterString): Uint8Array { 8 | return encodeSequence([ 9 | value.identification, 10 | new DERElement( 11 | ASN1TagClass.universal, 12 | ASN1Construction.primitive, 13 | ASN1UniversalType.octetString, 14 | value.stringValue, 15 | ), 16 | ]); 17 | } 18 | -------------------------------------------------------------------------------- /source/utils/setBitInBase256.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Sets or clears a bit at a given index in a base-256 encoded `Uint8Array` 3 | * @param {Uint8Array} to - The base-256 encoded byte array to modify. 4 | * @param {number} bitIndex - The index of the bit to set or clear. 5 | * @param {boolean} value - True to set the bit, false to clear it. 6 | * @function 7 | */ 8 | export default 9 | function setBitInBase256 (to: Uint8Array, bitIndex: number, value: boolean): void { 10 | const byteIndex = (to.length - (Math.floor(bitIndex / 8) + 1)); 11 | if (value) { 12 | to[byteIndex] |= (0x01 << (bitIndex % 8)); 13 | } else { 14 | to[byteIndex] &= ~(0x01 << (bitIndex % 8)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/utils/trimLeadingPaddingBytes.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Removes leading padding bytes (0x80) from a `Uint8Array` 3 | * @param {Uint8Array} value - The byte array to trim. 4 | * @returns {Uint8Array} The trimmed byte array. 5 | * @function 6 | */ 7 | export default 8 | function trimLeadingPaddingBytes (value: Uint8Array): Uint8Array { 9 | if (value.length <= 1) { 10 | return value; 11 | } 12 | let startOfNonPadding: number = 0; 13 | while (startOfNonPadding < value.length) { 14 | if (value[startOfNonPadding] === 0x80) { 15 | startOfNonPadding++; 16 | } else { 17 | break; 18 | } 19 | } 20 | return value.subarray(startOfNonPadding); 21 | } 22 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeCharacterString.mts: -------------------------------------------------------------------------------- 1 | import CharacterString from "../../../types/CharacterString.mjs"; 2 | import decodeSequence from "../../der/decoders/decodeSequence.mjs"; 3 | import { ASN1ConstructionError } from "../../../errors.mjs"; 4 | 5 | export default 6 | function decodeCharacterString (value: Uint8Array): CharacterString { 7 | const components = decodeSequence(value); 8 | if (components.length !== 2) { 9 | throw new ASN1ConstructionError(`CharacterString must contain 2 components, not ${components.length}.`); 10 | } 11 | const identification = components[0]; 12 | const stringValue = components[1].octetString; 13 | return new CharacterString(identification, stringValue); 14 | } 15 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeGeneralString.mts: -------------------------------------------------------------------------------- 1 | import isGeneralCharacter from "../../../validators/isGeneralCharacter.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import { ASN1CharactersError } from "../../../errors.mjs"; 4 | import type { GeneralString } from "../../../macros.mjs"; 5 | 6 | export default 7 | function decodeGeneralString (value: Uint8Array): GeneralString { 8 | for (const char of value) { 9 | if (!isGeneralCharacter(char)) { 10 | throw new ASN1CharactersError( 11 | "GeneralString can only contain ASCII characters." 12 | + `Encountered character code ${char}.`, 13 | ); 14 | } 15 | } 16 | return convertBytesToText(value); 17 | } 18 | -------------------------------------------------------------------------------- /source/types/time/HOURS-MINUTES-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `HOURS-MINUTES-ENCODING ::= SEQUENCE { 8 | * hours INTEGER (0..24), 9 | * minutes INTEGER (0..59) 10 | * }` 11 | */ 12 | export default 13 | class HOURS_MINUTES_ENCODING { 14 | constructor ( 15 | readonly hours: INTEGER, 16 | readonly minutes: INTEGER, 17 | ) { 18 | datetimeComponentValidator("hour", 0, 24)("HOURS-MINUTES-ENCODING", hours); 19 | datetimeComponentValidator("minute", 0, 59)("HOURS-MINUTES-ENCODING", minutes); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeNumericString.mts: -------------------------------------------------------------------------------- 1 | import isNumericCharacter from "../../../validators/isNumericCharacter.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import { ASN1CharactersError } from "../../../errors.mjs"; 4 | import type { NumericString } from "../../../macros.mjs"; 5 | 6 | export default 7 | function decodeNumericString (value: Uint8Array): NumericString { 8 | for (const char of value) { 9 | if (!isNumericCharacter(char)) { 10 | throw new ASN1CharactersError( 11 | "NumericString can only contain characters 0 - 9 and space. " 12 | + `Encountered character code ${char}.`, 13 | ); 14 | } 15 | } 16 | return convertBytesToText(value); 17 | } 18 | -------------------------------------------------------------------------------- /source/types/time/DATE-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `DATE-ENCODING ::= SEQUENCE { 8 | * year INTEGER, 9 | * month INTEGER (1..12), 10 | * day INTEGER (1..31) 11 | * }` 12 | */ 13 | export default 14 | class DATE_ENCODING { 15 | constructor ( 16 | readonly year: INTEGER, 17 | readonly month: INTEGER, 18 | readonly day: INTEGER, 19 | ) { 20 | datetimeComponentValidator("month", 1, 12)("DATE-ENCODING", month); 21 | datetimeComponentValidator("day", 1, 31)("DATE-ENCODING", day); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeGraphicString.mts: -------------------------------------------------------------------------------- 1 | import isGraphicCharacter from "../../../validators/isGraphicCharacter.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import { ASN1CharactersError } from "../../../errors.mjs"; 4 | import type { GraphicString } from "../../../macros.mjs"; 5 | 6 | export default 7 | function decodeGraphicString (value: Uint8Array): GraphicString { 8 | for (const char of value) { 9 | if (!isGraphicCharacter(char)) { 10 | throw new ASN1CharactersError( 11 | "GraphicString can only contain characters between 0x20 and 0x7E. " 12 | + `Encountered character code ${char}.`, 13 | ); 14 | } 15 | } 16 | return convertBytesToText(value); 17 | } 18 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeTimeOfDay.mts: -------------------------------------------------------------------------------- 1 | import type { TIME_OF_DAY } from "../../../macros.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import validateTime from "../../../validators/validateTime.mjs"; 4 | 5 | export default 6 | function decodeTimeOfDay (bytes: Uint8Array): TIME_OF_DAY { 7 | const str: string = convertBytesToText(bytes); 8 | const hours: number = parseInt(str.slice(0, 2), 10); 9 | const minutes: number = parseInt(str.slice(2, 4), 10); 10 | const seconds: number = parseInt(str.slice(4, 6), 10); 11 | validateTime("TIME-OF-DAY", hours, minutes, seconds); 12 | const ret: Date = new Date(); 13 | ret.setHours(hours); 14 | ret.setMinutes(minutes); 15 | ret.setSeconds(seconds); 16 | return ret; 17 | } 18 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeVisibleString.mts: -------------------------------------------------------------------------------- 1 | import isVisibleCharacter from "../../../validators/isVisibleCharacter.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import { ASN1CharactersError } from "../../../errors.mjs"; 4 | import type { PrintableString } from "../../../macros.mjs"; 5 | 6 | export default 7 | function decodePrintableString (value: Uint8Array): PrintableString { 8 | for (const char of value) { 9 | if (!isVisibleCharacter(char)) { 10 | throw new ASN1CharactersError( 11 | "VisibleString can only contain characters between 0x20 and 0x7E. " 12 | + `Encountered character code ${char}.`, 13 | ); 14 | } 15 | } 16 | return convertBytesToText(value); 17 | } 18 | -------------------------------------------------------------------------------- /source/types/time/HOURS-DIFF-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `HOURS-DIFF-ENCODING ::= SEQUENCE { 8 | * hours INTEGER (0..24), 9 | * minutes-diff INTEGER (-900..900) 10 | * }` 11 | */ 12 | export default 13 | class HOURS_DIFF_ENCODING { 14 | constructor ( 15 | readonly hours: INTEGER, 16 | readonly minutes_diff: INTEGER, 17 | ) { 18 | datetimeComponentValidator("hour", 0, 24)("HOURS-DIFF-ENCODING", hours); 19 | datetimeComponentValidator("minute-diff", -900, 900)("HOURS-DIFF-ENCODING", minutes_diff); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /source/validators/isGeneralCharacter.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Checks if a character code is a valid ASN.1 `GeneralString` character 3 | * @description 4 | * 5 | * Note that this is not exactly correct. A `GeneralString` can contain any character from any 6 | * encoding, which is big part of why it is not recommended to use. 7 | * 8 | * This is a simple check for ASCII characters (0x00-0x7F). This is probably what you should 9 | * assume and use if you ever use `GeneralString`. 10 | * 11 | * @param {number} characterCode - The character code to check. 12 | * @returns {boolean} True if the character is valid for `GeneralString`, false otherwise. 13 | * @function 14 | */ 15 | export default 16 | function isGeneralCharacter (characterCode: number): boolean { 17 | return (characterCode <= 0x7F); 18 | } 19 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeObjectDescriptor.mts: -------------------------------------------------------------------------------- 1 | import isObjectDescriptorCharacter from "../../../validators/isObjectDescriptorCharacter.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import { ASN1CharactersError } from "../../../errors.mjs"; 4 | import type { ObjectDescriptor } from "../../../macros.mjs"; 5 | 6 | export default 7 | function decodeObjectDescriptor (value: Uint8Array): ObjectDescriptor { 8 | for (const char of value) { 9 | if (!isObjectDescriptorCharacter(char)) { 10 | throw new ASN1CharactersError( 11 | "ObjectDescriptor can only contain characters between 0x20 and 0x7E. " 12 | + `Encountered character code ${char}.`, 13 | ); 14 | } 15 | } 16 | return convertBytesToText(value); 17 | } 18 | -------------------------------------------------------------------------------- /source/interfaces/index.mts: -------------------------------------------------------------------------------- 1 | export type { default as Byteable } from "./Byteable.mjs"; 2 | export type { default as CharacterValidationFunction } from "./CharacterValidationFunction.mjs"; 3 | export type { default as Debyteable } from "./Debyteable.mjs"; 4 | export type { default as Deelementable } from "./Deelementable.mjs"; 5 | export type { default as Destringable } from "./Destringable.mjs"; 6 | export type { default as Elementable } from "./Elementable.mjs"; 7 | export type { default as Enbyteable } from "./Enbyteable.mjs"; 8 | export type { default as Enelementable } from "./Enelementable.mjs"; 9 | export type { default as Enstringable } from "./Enstringable.mjs"; 10 | export type { default as Identified } from "./Identified.mjs"; 11 | export type { default as Named } from "./Named.mjs"; 12 | export type { default as Stringable } from "./Stringable.mjs"; 13 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeRelativeObjectIdentifier.mts: -------------------------------------------------------------------------------- 1 | import type { RELATIVE_OID } from "../../../macros.mjs"; 2 | import { Buffer } from "node:buffer"; 3 | 4 | export default 5 | function encodeRelativeObjectIdentifier (value: RELATIVE_OID): Uint8Array { 6 | const ret: number[] = []; 7 | for (const arc of value) { 8 | if (arc < 128) { 9 | ret.push(arc); 10 | continue; 11 | } 12 | let l = 0; 13 | let i = arc; 14 | while (i > 0) { 15 | l++; 16 | i >>>= 7; 17 | } 18 | for (let j = l - 1; j >= 0; j--) { 19 | let o = (arc >>> (j * 7)); 20 | o &= 0x7f; 21 | if (j !== 0) { 22 | o |= 0x80; 23 | } 24 | ret.push(o); 25 | } 26 | } 27 | return Buffer.from(ret); 28 | } 29 | -------------------------------------------------------------------------------- /test/asn1/encode.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(CodecElement.constructor.name, () => { 11 | it("encodes SET OF correctly", () => { 12 | const el1 = new CodecElement(); 13 | el1.integer = 5; 14 | const el2 = new CodecElement(); 15 | el2.utf8String = "Hello"; 16 | const el3 = new CodecElement(); 17 | el3.boolean = true; 18 | 19 | const setty = new CodecElement(); 20 | setty.encode(new Set([ el1, el2, el3, 5, null, "hey", 4.5 ])); 21 | assert.equal(setty.setOf.length, 7); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodePrintableString.mts: -------------------------------------------------------------------------------- 1 | import isPrintableCharacter from "../../../validators/isPrintableCharacter.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import { ASN1CharactersError } from "../../../errors.mjs"; 4 | import { printableStringCharacters } from "../../../values.mjs"; 5 | import type { PrintableString } from "../../../macros.mjs"; 6 | 7 | export default 8 | function decodePrintableString (value: Uint8Array): PrintableString { 9 | for (const char of value) { 10 | if (!isPrintableCharacter(char)) { 11 | throw new ASN1CharactersError( 12 | `PrintableString can only contain these characters: ${printableStringCharacters}. ` 13 | + `Encountered character code ${char}.`, 14 | ); 15 | } 16 | } 17 | return convertBytesToText(value); 18 | } 19 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeGeneralizedTime.mts: -------------------------------------------------------------------------------- 1 | import type { GeneralizedTime } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | 4 | export default 5 | function encodeGeneralizedTime (value: GeneralizedTime): Uint8Array { 6 | const year: string = value.getUTCFullYear().toString().padStart(4, "0"); 7 | const month: string = (value.getUTCMonth() + 1).toString().padStart(2, "0"); 8 | const day: string = value.getUTCDate().toString().padStart(2, "0"); 9 | const hour: string = value.getUTCHours().toString().padStart(2, "0"); 10 | const minute: string = value.getUTCMinutes().toString().padStart(2, "0"); 11 | const second: string = value.getUTCSeconds().toString().padStart(2, "0"); 12 | const timeString = `${year}${month}${day}${hour}${minute}${second}Z`; 13 | return convertTextToBytes(timeString); 14 | } 15 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeDate.mts: -------------------------------------------------------------------------------- 1 | import type { DATE } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | import * as errors from "../../../errors.mjs"; 4 | 5 | export default 6 | function encodeDate (date: DATE): Uint8Array { 7 | if (date.getFullYear() < 1582 || date.getFullYear() > 9999) { 8 | throw new errors.ASN1Error( 9 | `The DATE ${date.toISOString()} may not be encoded, because the ` 10 | + "year must be greater than 1581 and less than 10000.", 11 | ); 12 | } 13 | // NOTE: Using .toISOString() will not work, because it is in UTC time. 14 | return convertTextToBytes( 15 | date.getFullYear().toString().padStart(4, "0") 16 | + (date.getMonth() + 1).toString().padStart(2, "0") 17 | + date.getDate().toString().padStart(2, "0"), 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /source/utils/unpackBits.mts: -------------------------------------------------------------------------------- 1 | import type { BIT_STRING } from "../macros.mjs"; 2 | import { TRUE_BIT } from "../macros.mjs"; 3 | 4 | /** 5 | * @summary Unpacks a `Uint8Array` into a `BIT_STRING` 6 | * @description 7 | * Note: The result may be longer than the original bit string due to byte alignment. 8 | * @param {Uint8Array} bytes - The bytes to unpack. 9 | * @returns {BIT_STRING} The unpacked bit string. 10 | * @function 11 | */ 12 | export default 13 | function unpackBits (bytes: Uint8Array): BIT_STRING { 14 | const ret: Uint8ClampedArray = new Uint8ClampedArray(bytes.length << 3); 15 | for (let byte: number = 0; byte < bytes.length; byte++) { 16 | for (let bit: number = 0; bit < 8; bit++) { 17 | if (bytes[byte] & (0x01 << (7 - bit))) { 18 | ret[(byte << 3) + bit] = TRUE_BIT; 19 | } 20 | } 21 | } 22 | return ret; 23 | } 24 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeDateTime.mts: -------------------------------------------------------------------------------- 1 | import type { DATE_TIME } from "../../../macros.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import validateDateTime from "../../../validators/validateDateTime.mjs"; 4 | 5 | export default 6 | function decodeDateTime (bytes: Uint8Array): DATE_TIME { 7 | const str: string = convertBytesToText(bytes); 8 | const year: number = parseInt(str.slice(0, 4), 10); 9 | const month: number = parseInt(str.slice(4, 6), 10) - 1; 10 | const day: number = parseInt(str.slice(6, 8), 10); 11 | const hours: number = parseInt(str.slice(8, 10), 10); 12 | const minutes: number = parseInt(str.slice(10, 12), 10); 13 | const seconds: number = parseInt(str.slice(12, 14), 10); 14 | validateDateTime("DATE-TIME", year, month, day, hours, minutes, seconds); 15 | return new Date(year, month, day, hours, minutes, seconds); 16 | } 17 | -------------------------------------------------------------------------------- /source/types/time/TIME-OF-DAY-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `TIME-OF-DAY-ENCODING ::= SEQUENCE { 8 | * hours INTEGER (0..24), 9 | * minutes INTEGER (0..59), 10 | * seconds INTEGER (0..60) 11 | * }` 12 | */ 13 | export default 14 | class TIME_OF_DAY_ENCODING { 15 | constructor ( 16 | readonly hours: INTEGER, 17 | readonly minutes: INTEGER, 18 | readonly seconds: INTEGER, 19 | ) { 20 | datetimeComponentValidator("hour", 0, 24)("TIME-OF-DAY-ENCODING", hours); 21 | datetimeComponentValidator("minute", 0, 59)("TIME-OF-DAY-ENCODING", minutes); 22 | datetimeComponentValidator("seconds", 0, 60)("TIME-OF-DAY-ENCODING", seconds); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/utils/packBits.mts: -------------------------------------------------------------------------------- 1 | import type { BIT_STRING } from "../macros.mjs"; 2 | import { FALSE_BIT } from "../macros.mjs"; 3 | 4 | /** 5 | * @summary Packs a `BIT STRING` into a `Uint8Array` 6 | * @description 7 | * Used for ASN.1 `BIT STRING` encoding. 8 | * @param {BIT_STRING} bits - The bit string to pack. 9 | * @returns {Uint8Array} The packed bytes. 10 | * @function 11 | */ 12 | export default 13 | function packBits (bits: BIT_STRING): Uint8Array { 14 | const bytesNeeded: number = Math.ceil(bits.length / 8); 15 | const ret: Uint8Array = new Uint8Array(bytesNeeded); 16 | let byte = -1; 17 | for (let bit: number = 0; bit < bits.length; bit++) { 18 | const bitMod8 = bit % 8; 19 | if (bitMod8 === 0) { 20 | byte++; 21 | } 22 | if (bits[bit] !== FALSE_BIT) { 23 | ret[byte] |= (0x01 << (7 - bitMod8)); 24 | } 25 | } 26 | return ret; 27 | } 28 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeUTCTime.mts: -------------------------------------------------------------------------------- 1 | import type { UTCTime } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | 4 | export default 5 | function encodeUTCTime (value: UTCTime): Uint8Array { 6 | let year: string = value.getUTCFullYear().toString(); 7 | year = (year.substring(year.length - 2, year.length)).padStart(2, "0"); // Will fail if you supply a <2 digit date. 8 | const month: string = (value.getUTCMonth() + 1).toString().padStart(2, "0"); 9 | const day: string = value.getUTCDate().toString().padStart(2, "0"); 10 | const hour: string = value.getUTCHours().toString().padStart(2, "0"); 11 | const minute: string = value.getUTCMinutes().toString().padStart(2, "0"); 12 | const second: string = value.getUTCSeconds().toString().padStart(2, "0"); 13 | const utcString = `${year}${month}${day}${hour}${minute}${second}Z`; 14 | return convertTextToBytes(utcString); 15 | } 16 | -------------------------------------------------------------------------------- /source/validators/isPrintableCharacter.mts: -------------------------------------------------------------------------------- 1 | // TODO: This could be written in WebAssembly, but could using a Set be faster? 2 | /** 3 | * @summary Checks if a character code is a valid ASN.1 `PrintableString` character 4 | * @param {number} characterCode - The character code to check. 5 | * @returns {boolean} True if the character is valid for `PrintableString`, false otherwise. 6 | * @function 7 | */ 8 | export default 9 | function isPrintableCharacter (characterCode: number): boolean { 10 | return ( 11 | (characterCode >= 0x27 && characterCode <= 0x39 && characterCode !== 0x2A) // '()+,-./ AND 0 - 9 BUT NOT * 12 | || (characterCode >= 0x41 && characterCode <= 0x5A) // A - Z 13 | || (characterCode >= 0x61 && characterCode <= 0x7A) // a - z 14 | || (characterCode === 0x20) // SPACE 15 | || (characterCode === 0x3A) // : 16 | || (characterCode === 0x3D) // = 17 | || (characterCode === 0x3F) // ? 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeTimeOfDay.mts: -------------------------------------------------------------------------------- 1 | import type { TIME_OF_DAY } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | 4 | /** 5 | * Note that, even though it might seem like this should have a leading "T", 6 | * the specification notes that the leading "T" should not be included in 7 | * the abstract value notation when the time string is of type "Time." 8 | * This is specified in ITU X.680 2015, Section 38.3.3, in Table 7, in the 9 | * first "Hours component" row. (There are two of them, the first of which 10 | * is for "Time" types.) 11 | * 12 | * @param time {TIME_OF_DAY} The time to be encoded. 13 | */ 14 | export default 15 | function encodeTimeOfDay (time: TIME_OF_DAY): Uint8Array { 16 | return convertTextToBytes( 17 | time.getHours().toString().padStart(2, "0") 18 | + time.getMinutes().toString().padStart(2, "0") 19 | + time.getSeconds().toString().padStart(2, "0") 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /source/types/time/HOURS-MINUTES-DIFF-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `HOURS-MINUTES-DIFF-ENCODING ::= SEQUENCE { 8 | * hours INTEGER (0..24), 9 | * minutes INTEGER (0..59), 10 | * minutes-diff INTEGER (-900..900) 11 | * }` 12 | */ 13 | export default 14 | class HOURS_MINUTES_DIFF_ENCODING { 15 | constructor ( 16 | readonly hours: INTEGER, 17 | readonly minutes: INTEGER, 18 | readonly minutes_diff: INTEGER, 19 | ) { 20 | datetimeComponentValidator("hour", 0, 24)("HOURS-MINUTES-DIFF-ENCODING", hours); 21 | datetimeComponentValidator("minute", 0, 59)("HOURS-MINUTES-DIFF-ENCODING", minutes); 22 | datetimeComponentValidator("minute-diff", -900, 900)("HOURS-MINUTES-DIFF-ENCODING", minutes_diff); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/utils/decodeUnsignedBigEndianInteger.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../errors.mjs"; 2 | import { Buffer } from "node:buffer"; 3 | 4 | /** 5 | * @summary Decodes an unsigned big-endian integer from a `Uint8Array` 6 | * @description 7 | * Throws if the value is too large for a 32-bit unsigned integer. 8 | * @param {Uint8Array} value - The bytes representing the unsigned integer in big-endian order. 9 | * @returns {number} The decoded unsigned integer. 10 | * @throws {ASN1OverflowError} If the input is too long to decode as a 32-bit unsigned integer. 11 | * @function 12 | */ 13 | export default 14 | function decodeUnsignedBigEndianInteger (value: Uint8Array): number { 15 | if (value.length === 0) { 16 | return 0; 17 | } 18 | if (value.length > 4) { 19 | throw new errors.ASN1OverflowError(`Number on ${value.length} bytes is too long to decode.`); 20 | } 21 | const ret = Buffer.alloc(4); 22 | ret.set(value, (4 - value.length)); 23 | return ret.readUInt32BE(); 24 | } 25 | -------------------------------------------------------------------------------- /source/utils/dissectFloat.mts: -------------------------------------------------------------------------------- 1 | const EXPONENT_BITMASK: number = 0b0111_1111_1111_0000_0000_0000_0000_0000; 2 | 3 | /** 4 | * @summary Dissects a JavaScript number into its IEEE 754 sign, exponent, and mantissa components 5 | * @description 6 | * Used for ASN.1 `REAL` encoding/decoding. 7 | * @param {number} value - The number to dissect. 8 | * @returns {Object} The float components. 9 | * @function 10 | */ 11 | export default 12 | function dissectFloat (value: number): { negative: boolean; exponent: number; mantissa: number } { 13 | const float: Float64Array = new Float64Array([ value ]); 14 | const uints: Uint32Array = new Uint32Array(float.buffer); 15 | const exponent: number = (((uints[1] & EXPONENT_BITMASK) >>> 20) - 1023 - 31); 16 | const mantissa: number = 0x8000_0000 + (( 17 | ((uints[1] & 0x000F_FFFF) << 11) 18 | | ((uints[0] & 0xFFE0_0000) >>> 21) 19 | )); 20 | return { 21 | negative: (value < 0), 22 | exponent, 23 | mantissa, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /source/utils/convertTextToBytes.mts: -------------------------------------------------------------------------------- 1 | import { ASN1Error } from "../errors.mjs"; 2 | import { Buffer } from "node:buffer"; 3 | 4 | /** 5 | * @summary Converts a string to a byte array using the specified codec, supporting both Node.js and browser environments. 6 | * @description 7 | * Used for encoding ASN.1 string types to their byte representation. 8 | * 9 | * @param {string} text - The text to convert to bytes. 10 | * @param {string} [codec="utf-8"] - The codec to use (e.g., "utf-8"). 11 | * @returns {Uint8Array} The encoded bytes. 12 | * @function 13 | */ 14 | export default 15 | function convertTextToBytes (text: string, codec: string = "utf-8"): Uint8Array { 16 | if (typeof TextEncoder !== "undefined") { // Browser JavaScript 17 | return (new TextEncoder()).encode(text); 18 | } else if (typeof Buffer !== "undefined") { // NodeJS 19 | return Buffer.from(text, codec as any); 20 | } 21 | throw new ASN1Error("Neither TextEncoder nor Buffer are defined to encode text into bytes."); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /source/classes/TYPE-IDENTIFIER.mts: -------------------------------------------------------------------------------- 1 | import type { ASN1Decoder, ASN1Encoder } from "../functional.mjs"; 2 | import type { OBJECT_IDENTIFIER } from "../macros.mjs"; 3 | 4 | /** 5 | * From ITU X.681, Annex A.2: 6 | * ```asn1 7 | * TYPE-IDENTIFIER ::= CLASS { 8 | * &id OBJECT IDENTIFIER UNIQUE, 9 | * &Type } 10 | * WITH SYNTAX { 11 | * &Type 12 | * IDENTIFIED BY &id 13 | * } 14 | * ``` 15 | */ 16 | export default 17 | interface TYPE_IDENTIFIER< 18 | Type = any /* OBJECT_CLASS_TYPE_FIELD_PARAMETER */ 19 | > { 20 | class: string; // This is often aliased, so it cannot be const "TYPE-IDENTIFIER". 21 | decoderFor: Partial<{ // For decoding types supplied in type fields 22 | [_K in keyof TYPE_IDENTIFIER]: ASN1Decoder[_K]>; 23 | }>; 24 | encoderFor: Partial<{ // For encoding types supplied in type fields 25 | [_K in keyof TYPE_IDENTIFIER]: ASN1Encoder[_K]>; 26 | }>; 27 | "&id": OBJECT_IDENTIFIER, /* UNIQUE */ 28 | "&Type": Type, 29 | } 30 | -------------------------------------------------------------------------------- /source/utils/compareSetOfElementsCanonically.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | 3 | /** 4 | * @summary Compares two ASN.1 `SET OF` elements for canonical order as per ITU X.690-2015, Section 11.6. 5 | * @description 6 | * Intended for use with `Array.prototype.sort()` for DER/CER encoding. 7 | * 8 | * @param {ASN1Element} a - The first ASN.1 element to compare. 9 | * @param {ASN1Element} b - The second ASN.1 element to compare. 10 | * @returns {number} Negative if a < b, positive if a > b, zero if equal. 11 | * @function 12 | */ 13 | export default 14 | function compareSetOfElementsCanonically (a: ASN1Element, b: ASN1Element): number { 15 | const longestLength: number = (a.value.length > b.value.length) 16 | ? a.value.length 17 | : b.value.length; 18 | for (let i: number = 0; i < longestLength; i++) { 19 | const x: number = a.value[i] ?? 0; 20 | const y: number = b.value[i] ?? 0; 21 | if (x !== y) { 22 | return (x - y); 23 | } 24 | } 25 | return 0; 26 | } 27 | -------------------------------------------------------------------------------- /source/utils/decodeSignedBigEndianInteger.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../errors.mjs"; 2 | import { Buffer } from "node:buffer"; 3 | 4 | /** 5 | * @summary Decodes a signed big-endian integer from a `Uint8Array` 6 | * @description 7 | * Throws if the value is too large for a 32-bit signed integer. 8 | * @param {Uint8Array} value - The bytes representing the signed integer in big-endian order. 9 | * @returns {number} The decoded signed integer. 10 | * @throws {ASN1OverflowError} If the input is too long to decode as a 32-bit signed integer. 11 | * @function 12 | */ 13 | export default 14 | function decodeSignedBigEndianInteger (value: Uint8Array): number { 15 | if (value.length === 0) { 16 | return 0; 17 | } 18 | if (value.length > 4) { 19 | throw new errors.ASN1OverflowError("Number too long to decode."); 20 | } 21 | // TODO: Could you do allocUnsafe? 22 | const ret = Buffer.alloc(4, (value[0] >= 0b10000000) ? 0xFF : 0x00); 23 | ret.set(value, (4 - value.length)); 24 | return ret.readInt32BE(); 25 | } 26 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeRelativeObjectIdentifier.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../../../errors.mjs"; 2 | import type { RELATIVE_OID } from "../../../macros.mjs"; 3 | 4 | export default 5 | function decodeRelativeObjectIdentifier (value: Uint8Array): RELATIVE_OID { 6 | if (value.length === 0) { 7 | return []; 8 | } 9 | if (value.length > 1 && value[value.length - 1] & 0b10000000) { 10 | throw new errors.ASN1TruncationError("Relative OID was truncated."); 11 | } 12 | const nodes: number[] = []; 13 | let current_node: number = 0; 14 | for (let i = 0; i < value.length; i++) { 15 | const byte = value[i]; 16 | if ((byte === 0x80) && (current_node === 0)) { 17 | throw new errors.ASN1PaddingError("Prohibited padding on RELATIVE-OID node."); 18 | } 19 | current_node <<= 7; 20 | current_node += (byte & 0b0111_1111); 21 | if ((byte & 0b1000_0000) === 0) { 22 | nodes.push(current_node); 23 | current_node = 0; 24 | } 25 | } 26 | return nodes; 27 | } 28 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeDateTime.mts: -------------------------------------------------------------------------------- 1 | import type { DATE_TIME } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | import * as errors from "../../../errors.mjs"; 4 | 5 | export default 6 | function encodeDateTime (value: DATE_TIME): Uint8Array { 7 | if (value.getFullYear() < 1582 || value.getFullYear() > 9999) { 8 | throw new errors.ASN1Error( 9 | `The DATE ${value.toISOString()} may not be encoded, because the ` 10 | + "year must be greater than 1581 and less than 10000.", 11 | ); 12 | } 13 | // NOTE: Using .toISOString() will not work, because it is in UTC time. 14 | return convertTextToBytes( 15 | value.getFullYear().toString().padStart(4, "0") 16 | + (value.getMonth() + 1).toString().padStart(2, "0") 17 | + value.getDate().toString().padStart(2, "0") 18 | + value.getHours().toString().padStart(2, "0") 19 | + value.getMinutes().toString().padStart(2, "0") 20 | + value.getSeconds().toString().padStart(2, "0"), 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeInteger.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../../../errors.mjs"; 2 | import type { INTEGER } from "../../../macros.mjs"; 3 | import { bufferToInteger } from "../../../utils/bigint.mjs"; 4 | import { Buffer } from "node:buffer"; 5 | 6 | export default 7 | function decodeInteger (value: Uint8Array): INTEGER { 8 | if (value.length === 0) { 9 | throw new errors.ASN1SizeError("INTEGER or ENUMERATED encoded on zero bytes"); 10 | } 11 | if ( 12 | value.length > 2 13 | && ( 14 | (value[0] === 0xFF && value[1] >= 0b10000000) 15 | || (value[0] === 0x00 && value[1] < 0b10000000) 16 | ) 17 | ) { 18 | // We slice so nefarious users cannot inundate logs with gigantic arbitrary integers. 19 | const buf = Buffer.from(value.slice(0, 16)); 20 | throw new errors.ASN1PaddingError( 21 | "Unnecessary padding bytes on INTEGER or ENUMERATED. " 22 | + `First 16 bytes of the offending value were: 0x${buf.toString("hex")}`); 23 | } 24 | return bufferToInteger(value); 25 | } 26 | -------------------------------------------------------------------------------- /source/types/time/TIME-OF-DAY-DIFF-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `TIME-OF-DAY-DIFF-ENCODING ::= SEQUENCE { 8 | * hours INTEGER (0..24), 9 | * minutes INTEGER (0..59), 10 | * seconds INTEGER (0..60), 11 | * minutes-diff INTEGER (-900..900) 12 | * }` 13 | */ 14 | export default 15 | class TIME_OF_DAY_DIFF_ENCODING { 16 | constructor ( 17 | readonly hours: INTEGER, 18 | readonly minutes: INTEGER, 19 | readonly seconds: INTEGER, 20 | readonly minutes_diff: INTEGER, 21 | ) { 22 | datetimeComponentValidator("hour", 0, 24)("TIME-OF-DAY-DIFF-ENCODING", hours); 23 | datetimeComponentValidator("minute", 0, 59)("TIME-OF-DAY-DIFF-ENCODING", minutes); 24 | datetimeComponentValidator("seconds", 0, 60)("TIME-OF-DAY-DIFF-ENCODING", seconds); 25 | datetimeComponentValidator("minute-diff", -900, 900)("TIME-OF-DAY-DIFF-ENCODING", minutes_diff); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Jonathan M. Wilbur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /source/utils/sortCanonically.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | import { Buffer } from "node:buffer"; 3 | 4 | /** 5 | * @summary Sorts ASN.1 elements in canonical order as specified by ITU X.690 (2021), Section 11.6 6 | * @description 7 | * Used for DER and CER encoding of `SET OF` and similar types. 8 | * @param {ASN1Element[]} elements - The ASN.1 elements to sort. 9 | * @returns {ASN1Element[]} The sorted array of ASN.1 elements. 10 | * @function 11 | */ 12 | export default 13 | function sortCanonically (elements: ASN1Element[]): ASN1Element[] { 14 | return elements.sort((a, b): number => { 15 | const aClassOrder = a.tagClass as number; 16 | const bClassOrder = b.tagClass as number; 17 | if (aClassOrder !== bClassOrder) { 18 | return (aClassOrder - bClassOrder); 19 | } 20 | /** 21 | * Buffer.compare() has the same semantics as the encoding comparison 22 | * algorithm described in ITU X.690 (2021), Section 11.6 (which builds 23 | * off of the conventions defined in Section 6.3). 24 | */ 25 | return Buffer.compare(a.toBytes(), b.toBytes()); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /source/classes/ABSTRACT-SYNTAX.mts: -------------------------------------------------------------------------------- 1 | import type { ASN1Decoder, ASN1Encoder } from "../functional.mjs"; 2 | import type { OBJECT_IDENTIFIER, BIT_STRING } from "../macros.mjs"; 3 | 4 | 5 | /** 6 | * From ITU X.681, Annex B.2: 7 | * 8 | * ```asn1 9 | * ABSTRACT-SYNTAX ::= CLASS 10 | * { 11 | * &id OBJECT IDENTIFIER UNIQUE, 12 | * &Type, 13 | * &property BIT STRING {handles-invalid-encodings(0)} DEFAULT {} 14 | * } 15 | * WITH SYNTAX { 16 | * &Type 17 | * IDENTIFIED BY &id 18 | * [HAS PROPERTY &property] 19 | * } 20 | * ``` 21 | */ 22 | export default 23 | interface ABSTRACT_SYNTAX < 24 | Type = any, 25 | > { 26 | class: string; // This is often aliased, so it cannot be const "ABSTRACT-SYNTAX". 27 | decoderFor: Partial<{ // For decoding types supplied in type fields 28 | [_K in keyof ABSTRACT_SYNTAX]: ASN1Decoder[_K]>; 29 | }>; 30 | encoderFor: Partial<{ // For encoding types supplied in type fields 31 | [_K in keyof ABSTRACT_SYNTAX]: ASN1Encoder[_K]>; 32 | }>; 33 | "&id": OBJECT_IDENTIFIER, /* UNIQUE */ 34 | "&Type": Type, 35 | "&property": BIT_STRING, 36 | } 37 | -------------------------------------------------------------------------------- /source/types/time/TIME-OF-DAY-FRACTION-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `TIME-OF-DAY-FRACTION-ENCODING ::= SEQUENCE { 8 | * hours INTEGER (0..24), 9 | * minutes INTEGER (0..59), 10 | * seconds INTEGER (0..60), 11 | * fractional-part INTEGER (0..MAX) 12 | * }` 13 | */ 14 | export default 15 | class TIME_OF_DAY_FRACTION_ENCODING { 16 | constructor ( 17 | readonly hours: INTEGER, 18 | readonly minutes: INTEGER, 19 | readonly seconds: INTEGER, 20 | readonly fractional_part: INTEGER, 21 | ) { 22 | datetimeComponentValidator("hour", 0, 24)("TIME-OF-DAY-FRACTION-ENCODING", hours); 23 | datetimeComponentValidator("minute", 0, 59)("TIME-OF-DAY-FRACTION-ENCODING", minutes); 24 | datetimeComponentValidator("seconds", 0, 60)("TIME-OF-DAY-FRACTION-ENCODING", seconds); 25 | datetimeComponentValidator("fractional-part", 0, Number.MAX_SAFE_INTEGER)( 26 | "TIME-OF-DAY-FRACTION-ENCODING", fractional_part); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /source/utils/isUniquelyTagged.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | 3 | /** 4 | * @summary Checks if all ASN.1 elements in an array have unique tag class and tag number combinations 5 | * @description 6 | * Useful for validating ASN.1 `CHOICE` and `SET` types. 7 | * @param {ASN1Element[]} elements - The ASN.1 elements to check. 8 | * @returns {boolean} True if all elements are uniquely tagged, false otherwise. 9 | * @function 10 | */ 11 | export default 12 | function isUniquelyTagged (elements: ASN1Element[]): boolean { 13 | const finds: Set = new Set([]); 14 | for (let i = 0; i < elements.length; i++) { 15 | // Theoretically, it is possible to supply a ridiculously large 16 | // tagNumber that would make this incorrectly flag it as duplicate, but 17 | // large tag numbers (.e.g > 1000000) should never be used. 18 | const key: number = ( 19 | (elements[i].tagClass << 30) // Bit shifts are mod 32, FYI. 20 | + elements[i].tagNumber // Technically, we _should_ mod 30 this, but its not necessary. 21 | ); 22 | if (finds.has(key)) { 23 | return false; 24 | } 25 | finds.add(key); 26 | } 27 | return true; 28 | } 29 | -------------------------------------------------------------------------------- /source/utils/encodeX690Base10RealNumber.mts: -------------------------------------------------------------------------------- 1 | import { ASN1SpecialRealValue } from "../values.mjs"; 2 | import convertTextToBytes from "./convertTextToBytes.mjs"; 3 | 4 | /** 5 | * @summary Encodes a JavaScript number as an ASN.1 `REAL` value using X.690 base-10 (NR3) encoding 6 | * @description 7 | * Handles special values as per ITU X.690. 8 | * @param {number} value - The number to encode. 9 | * @returns {Uint8Array} The encoded REAL value bytes. 10 | * @function 11 | */ 12 | export default 13 | function encodeX690Base10RealNumber (value: number): Uint8Array { 14 | if (value === 0.0) { 15 | return new Uint8Array(0); 16 | } else if (Number.isNaN(value)) { 17 | return new Uint8Array([ ASN1SpecialRealValue.notANumber ]); 18 | } else if (value === -0.0) { 19 | return new Uint8Array([ ASN1SpecialRealValue.minusZero ]); 20 | } else if (value === Infinity) { 21 | return new Uint8Array([ ASN1SpecialRealValue.plusInfinity ]); 22 | } else if (value === -Infinity) { 23 | return new Uint8Array([ ASN1SpecialRealValue.minusInfinity ]); 24 | } 25 | const valueString: string = (String.fromCharCode(0b00000011) + value.toFixed(7)); // Encodes as NR3 26 | return convertTextToBytes(valueString); 27 | } 28 | -------------------------------------------------------------------------------- /source/validators/validateDateTime.mts: -------------------------------------------------------------------------------- 1 | import validateDate from "./validateDate.mjs"; 2 | import validateTime from "./validateTime.mjs"; 3 | 4 | /** 5 | * @summary Validates an ASN.1 date-time value (year, month, day, hour, minute, second). 6 | * @description 7 | * This could be used for validating `DATE-TIME`, `GeneralizedTime`, and `UTCTime`. 8 | * 9 | * @param {string} dataType - The ASN.1 type being validated (for error messages). 10 | * @param {number} year - The year value. 11 | * @param {number} month - The month value (0-based: 0=Jan, 1=Feb, ... 11=Dec). 12 | * @param {number} date - The day of the month (1-based). 13 | * @param {number} hours - The hour value (0-23). 14 | * @param {number} minutes - The minute value (0-59). 15 | * @param {number} seconds - The second value (0-59). 16 | * @returns {void} 17 | * @throws {ASN1Error} if the date or time is invalid. 18 | * @function 19 | */ 20 | export default function validateDateTime ( 21 | dataType: string, 22 | year: number, 23 | month: number, 24 | date: number, 25 | hours: number, 26 | minutes: number, 27 | seconds: number, 28 | ): void { 29 | validateDate(dataType, year, month, date); 30 | validateTime(dataType, hours, minutes, seconds); 31 | } 32 | -------------------------------------------------------------------------------- /source/validators/datetimeComponentValidator.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../errors.mjs"; 2 | import type { INTEGER } from "../macros.mjs"; 3 | 4 | /** 5 | * @summary Creates a validator function for a date/time component (e.g., year, month, day, hour, etc.) 6 | * @param {string} unitName - The name of the date/time unit (e.g., 'year', 'month'). 7 | * @param {number | bigint} min - The minimum allowed value for the unit. 8 | * @param {number | bigint} max - The maximum allowed value for the unit. 9 | * @returns {Function} A validator function for the unit. 10 | * @function 11 | */ 12 | export default 13 | function datetimeComponentValidator ( 14 | unitName: string, 15 | min: INTEGER, 16 | max: INTEGER, 17 | ): (dataType: string, value: INTEGER) => void { 18 | return function ( 19 | dataType: string, 20 | value: INTEGER, 21 | ): void { 22 | if (!Number.isInteger(value)) { 23 | throw new errors.ASN1Error(`Non-integral ${unitName} supplied to ${dataType}.`); 24 | } 25 | if (value > max) { 26 | throw new errors.ASN1Error(`Encountered ${unitName} greater than ${max} in ${dataType}.`); 27 | } 28 | if (value < min) { 29 | throw new errors.ASN1Error(`Encountered ${unitName} less than ${min} in ${dataType}.`); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeEmbeddedPDV.mts: -------------------------------------------------------------------------------- 1 | import type { EMBEDDED_PDV } from "../../../macros.mjs"; 2 | import DERElement from "../../../codecs/der.mjs"; 3 | import { ASN1TagClass, ASN1UniversalType, ASN1Construction } from "../../../values.mjs"; 4 | import encodeSequence from "./encodeSequence.mjs"; 5 | 6 | // `EmbeddedPDV ::= [UNIVERSAL 11] IMPLICIT SEQUENCE { 7 | // identification CHOICE { 8 | // syntaxes SEQUENCE { 9 | // abstract OBJECT IDENTIFIER, 10 | // transfer OBJECT IDENTIFIER }, 11 | // syntax OBJECT IDENTIFIER, 12 | // presentation-context-id INTEGER, 13 | // context-negotiation SEQUENCE { 14 | // presentation-context-id INTEGER, 15 | // transfer-syntax OBJECT IDENTIFIER }, 16 | // transfer-syntax OBJECT IDENTIFIER, 17 | // fixed NULL }, 18 | // data-value-descriptor ObjectDescriptor OPTIONAL, 19 | // data-value OCTET STRING } 20 | // (WITH COMPONENTS { ... , data-value-descriptor ABSENT })` 21 | export default 22 | function encodeEmbeddedPDV (value: EMBEDDED_PDV): Uint8Array { 23 | return encodeSequence([ 24 | value.identification, 25 | new DERElement( 26 | ASN1TagClass.universal, 27 | ASN1Construction.primitive, 28 | ASN1UniversalType.octetString, 29 | value.dataValue, 30 | ), 31 | ]); 32 | } 33 | -------------------------------------------------------------------------------- /source/codecs/ber/decoders/decodeBitString.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../../../errors.mjs"; 2 | import type { BIT_STRING } from "../../../macros.mjs"; 3 | import { TRUE_BIT, FALSE_BIT } from "../../../macros.mjs"; 4 | 5 | export default 6 | function decodeBitString (value: Uint8Array): BIT_STRING { 7 | if (value.length === 0) { 8 | throw new errors.ASN1Error("ASN.1 BIT STRING cannot be encoded on zero bytes!"); 9 | } 10 | if (value.length === 1 && value[0] !== 0) { 11 | throw new errors.ASN1Error("ASN.1 BIT STRING encoded with deceptive first byte!"); 12 | } 13 | if (value[0] > 7) { 14 | throw new errors.ASN1Error("First byte of an ASN.1 BIT STRING must be <= 7!"); 15 | } 16 | 17 | const ret: number[] = []; 18 | for (let i = 1; i < value.length; i++) { 19 | ret.push( 20 | ((value[i] & 0b10000000) ? TRUE_BIT : FALSE_BIT), 21 | ((value[i] & 0b01000000) ? TRUE_BIT : FALSE_BIT), 22 | ((value[i] & 0b00100000) ? TRUE_BIT : FALSE_BIT), 23 | ((value[i] & 0b00010000) ? TRUE_BIT : FALSE_BIT), 24 | ((value[i] & 0b00001000) ? TRUE_BIT : FALSE_BIT), 25 | ((value[i] & 0b00000100) ? TRUE_BIT : FALSE_BIT), 26 | ((value[i] & 0b00000010) ? TRUE_BIT : FALSE_BIT), 27 | ((value[i] & 0b00000001) ? TRUE_BIT : FALSE_BIT), 28 | ); 29 | } 30 | ret.length -= value[0]; 31 | return new Uint8ClampedArray(ret); 32 | } 33 | -------------------------------------------------------------------------------- /source/types/time/TIME-OF-DAY-FRACTION-DIFF-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { INTEGER } from "../../macros.mjs"; 2 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 3 | 4 | /** 5 | * Defined in ITU Recommendation X.696:2015, Section 29: 6 | * 7 | * `TIME-OF-DAY-FRACTION-DIFF-ENCODING ::= SEQUENCE { 8 | * hours INTEGER (0..24), 9 | * minutes INTEGER (0..59), 10 | * seconds INTEGER (0..60), 11 | * fractional-part INTEGER (0..MAX), 12 | * minutes-diff INTEGER (-900..900) 13 | * }` 14 | */ 15 | export default 16 | class TIME_OF_DAY_FRACTION_DIFF_ENCODING { 17 | constructor ( 18 | readonly hours: INTEGER, 19 | readonly minutes: INTEGER, 20 | readonly seconds: INTEGER, 21 | readonly fractional_part: INTEGER, 22 | readonly minutes_diff: INTEGER, 23 | ) { 24 | datetimeComponentValidator("hour", 0, 24)("TIME-OF-DAY-FRACTION-DIFF-ENCODING", hours); 25 | datetimeComponentValidator("minute", 0, 59)("TIME-OF-DAY-FRACTION-DIFF-ENCODING", minutes); 26 | datetimeComponentValidator("seconds", 0, 60)("TIME-OF-DAY-FRACTION-DIFF-ENCODING", seconds); 27 | datetimeComponentValidator("fractional-part", 0, Number.MAX_SAFE_INTEGER)( 28 | "TIME-OF-DAY-FRACTION-DIFF-ENCODING", fractional_part); 29 | datetimeComponentValidator("minute-diff", -900, 900)("TIME-OF-DAY-FRACTION-DIFF-ENCODING", minutes_diff); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /source/validators/validateTime.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../errors.mjs"; 2 | 3 | /** 4 | * @summary Validates an ASN.1 time value (hour, minute, second) for correctness and range. 5 | * @param {string} dataType - The ASN.1 type being validated (for error messages). 6 | * @param {number} hours - The hour value (0-23). 7 | * @param {number} minutes - The minute value (0-59). 8 | * @param {number} seconds - The second value (0-59). 9 | * @returns {void} 10 | * @throws {ASN1Error} if any of the time values are invalid or out of range. 11 | * @function 12 | */ 13 | export default function validateTime ( 14 | dataType: string, 15 | hours: number, 16 | minutes: number, 17 | seconds: number, 18 | ): void { 19 | if (!Number.isSafeInteger(hours)) { 20 | throw new errors.ASN1Error(`Invalid hours in ${dataType}`); 21 | } 22 | if (!Number.isSafeInteger(minutes)) { 23 | throw new errors.ASN1Error(`Invalid minutes in ${dataType}`); 24 | } 25 | if (!Number.isSafeInteger(seconds) || (seconds < 0)) { 26 | throw new errors.ASN1Error(`Invalid seconds in ${dataType}`); 27 | } 28 | if (hours > 23) { 29 | throw new errors.ASN1Error(`Hours > 23 encountered in ${dataType}.`); 30 | } 31 | if (minutes > 59) { 32 | throw new errors.ASN1Error(`Minutes > 60 encountered in ${dataType}.`); 33 | } 34 | if (seconds > 59) { 35 | throw new errors.ASN1Error(`Seconds > 60 encountered in ${dataType}.`); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /source/utils/isInCanonicalOrder.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | import { ASN1TagClass, CANONICAL_TAG_CLASS_ORDERING } from "../values.mjs"; 3 | 4 | /** 5 | * @summary Checks if `SET OF` ASN.1 elements is in canonical order as per ITU X.690 6 | * @description 7 | * Used for validating DER/CER `SET OF` encodings. 8 | * @param {ASN1Element[]} elements - The ASN.1 elements to check. 9 | * @returns {boolean} True if the elements are in canonical order, false otherwise. 10 | * @function 11 | */ 12 | export default 13 | function isInCanonicalOrder (elements: ASN1Element[]): boolean { 14 | let previousTagClass: ASN1TagClass | null = null; 15 | let previousTagNumber: number | null = null; 16 | return (elements.every((element): boolean => { 17 | // Checks that the tag classes are in canonical order 18 | if ( 19 | previousTagClass !== null 20 | && element.tagClass !== previousTagClass 21 | && CANONICAL_TAG_CLASS_ORDERING.indexOf(element.tagClass) 22 | <= CANONICAL_TAG_CLASS_ORDERING.indexOf(previousTagClass) 23 | ) return false; 24 | // Checks that the tag numbers are in canonical order 25 | if (element.tagClass !== previousTagClass) previousTagNumber = null; 26 | if (previousTagNumber !== null && element.tagNumber < previousTagNumber) return false; 27 | previousTagClass = element.tagClass; 28 | previousTagNumber = element.tagNumber; 29 | return true; 30 | })); 31 | } 32 | -------------------------------------------------------------------------------- /source/utils/encodeUnsignedBigEndianInteger.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../errors.mjs"; 2 | import { MIN_UINT_32, MAX_UINT_32 } from "../values.mjs"; 3 | import { Buffer } from "node:buffer"; 4 | 5 | /** 6 | * @summary Encodes a number as an unsigned big-endian integer 7 | * @description 8 | * Throws if the value is out of the 32-bit unsigned integer range. 9 | * @param {number} value - The unsigned integer to encode. 10 | * @returns {Uint8Array} The encoded big-endian bytes. 11 | * @throws {ASN1OverflowError} If the value is out of range for a 32-bit unsigned integer. 12 | * @function 13 | */ 14 | export default 15 | function encodeUnsignedBigEndianInteger (value: number): Uint8Array { 16 | if (value < MIN_UINT_32) { 17 | throw new errors.ASN1OverflowError( 18 | `Number ${value} too small to be encoded as a big-endian unsigned integer.`, 19 | ); 20 | } 21 | if (value > MAX_UINT_32) { 22 | throw new errors.ASN1OverflowError( 23 | `Number ${value} too big to be encoded as a big-endian unsigned integer.`, 24 | ); 25 | } 26 | // TODO: Could you do allocUnsafe() here? 27 | const bytes: Buffer = Buffer.alloc(4); 28 | bytes.writeUInt32BE(value); 29 | let startOfNonPadding: number = 0; 30 | for (let i: number = 0; i < bytes.length - 1; i++) { 31 | if (bytes[i] === 0x00) { 32 | startOfNonPadding++; 33 | } else { 34 | break; 35 | } 36 | } 37 | return bytes.subarray(startOfNonPadding); 38 | } 39 | -------------------------------------------------------------------------------- /source/types/index.mts: -------------------------------------------------------------------------------- 1 | export { default as CharacterString } from "./CharacterString.mjs"; 2 | export { default as EmbeddedPDV } from "./EmbeddedPDV.mjs"; 3 | export { default as External } from "./External.mjs"; 4 | export { default as ObjectIdentifier } from "./ObjectIdentifier.mjs"; 5 | export { default as TypeIdentifier } from "./TypeIdentifier.mjs"; 6 | 7 | // Time structured types, as specified in ITU X.696 8 | export { default as DATE_ENCODING } from "./time/DATE-ENCODING.mjs"; 9 | export { default as DURATION_EQUIVALENT } from "./time/DURATION-EQUIVALENT.mjs"; 10 | export { default as DURATION_INTERVAL_ENCODING } from "./time/DURATION-INTERVAL-ENCODING.mjs"; 11 | export { default as HOURS_DIFF_ENCODING } from "./time/HOURS-DIFF-ENCODING.mjs"; 12 | export { default as HOURS_ENCODING } from "./time/HOURS-ENCODING.mjs"; 13 | export { default as HOURS_MINUTES_DIFF_ENCODING } from "./time/HOURS-MINUTES-DIFF-ENCODING.mjs"; 14 | export { default as HOURS_MINUTES_ENCODING } from "./time/HOURS-MINUTES-ENCODING.mjs"; 15 | export { default as TIME_OF_DAY_DIFF_ENCODING } from "./time/TIME-OF-DAY-DIFF-ENCODING.mjs"; 16 | export { default as TIME_OF_DAY_ENCODING } from "./time/TIME-OF-DAY-ENCODING.mjs"; 17 | export { default as TIME_OF_DAY_FRACTION_DIFF_ENCODING } from "./time/TIME-OF-DAY-FRACTION-DIFF-ENCODING.mjs"; 18 | export { default as TIME_OF_DAY_FRACTION_ENCODING } from "./time/TIME-OF-DAY-FRACTION-ENCODING.mjs"; 19 | export { default as YEAR_ENCODING } from "./time/YEAR-ENCODING.mjs"; 20 | export { default as YEAR_MONTH_ENCODING } from "./time/YEAR-MONTH-ENCODING.mjs"; 21 | -------------------------------------------------------------------------------- /test/ber/invalid.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | describe("Basic Encoding Rules", () => { 6 | it("throws an exception when decoding a multi-byte BOOLEAN", () => { 7 | const el = new asn1.BERElement(); 8 | el.value = new Uint8Array([ 0x01, 0x01 ]); 9 | assert.throws(() => el.boolean); 10 | }); 11 | 12 | it("throws an exception when decoding a BIT STRING with a deceptive first byte", () => { 13 | const el = new asn1.BERElement(); 14 | el.value = new Uint8Array([ 0x05 ]); 15 | assert.throws(() => el.bitString); 16 | }); 17 | 18 | it("throws an exception when decoding a BIT STRING with a first byte greater than 7", () => { 19 | const el = new asn1.BERElement(); 20 | el.value = new Uint8Array([ 0x08, 0x0F, 0xF0 ]); 21 | assert.throws(() => el.bitString); 22 | }); 23 | 24 | it("throws an exception when decoding a constructed BIT STRING whose non-terminal subcomponents start with non-zero value bytes", () => { 25 | const data = [ 26 | 0x23, 0x0E, 27 | 0x03, 0x02, 0x03, 0x0F, // The 0x03 is what should cause this to throw. 28 | 0x23, 0x04, 29 | 0x03, 0x02, 0x00, 0x0F, 30 | 0x03, 0x02, 0x05, 0xF0, 31 | ]; 32 | const element = new asn1.BERElement(); 33 | element.fromBytes(data); 34 | assert.throws(() => element.bitString); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/x690/invalid/overflow.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(CodecElement.constructor.name, () => { 11 | it("throws when decoding an excessively large INTEGER", () => { 12 | const el = new CodecElement(); 13 | el.value = new Uint8Array([ 0xFF, 0xFF, 0xFF, 0xFF, 0xF3 ]); 14 | assert.throws(() => el.integer); 15 | }); 16 | 17 | it("throws when decoding an OBJECT IDENTIFIER that contains an excessively large number", () => { 18 | const el = new CodecElement(); 19 | el.value = new Uint8Array([ 0x43, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ]); 20 | assert.throws(() => el.objectIdentifier); 21 | }); 22 | 23 | it("throws when decoding an excessively large ENUMERATED", () => { 24 | const el = new CodecElement(); 25 | el.value = new Uint8Array([ 0xFF, 0xFF, 0xFF, 0xFF, 0xF3 ]); 26 | assert.throws(() => el.enumerated); 27 | }); 28 | 29 | it("throws when decoding a RELATIVE OID that contains an excessively large number", () => { 30 | const el = new CodecElement(); 31 | el.value = new Uint8Array([ 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ]); 32 | assert.throws(() => el.relativeObjectIdentifier); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /source/utils/convertBytesToText.mts: -------------------------------------------------------------------------------- 1 | import { ASN1Error } from "../errors.mjs"; 2 | import { Buffer } from "node:buffer"; 3 | 4 | /** 5 | * @summary Converts a byte array to a string using the specified codec, supporting both Node.js and browser environments. 6 | * @description 7 | * Used for decoding ASN.1 string types from their byte representation. 8 | * 9 | * @param {Uint8Array} bytes - The bytes to convert to text. 10 | * @param {string} [codec="utf-8"] - The codec to use (e.g., "utf-8"). 11 | * @returns {string} The decoded string. 12 | * @function 13 | */ 14 | export default 15 | function convertBytesToText (bytes: Uint8Array, codec: string = "utf-8"): string { 16 | if (typeof Buffer !== "undefined") { // NodeJS 17 | /** 18 | * If you call `Buffer.from(bytes.buffer)` (as seen below this `if` 19 | * statement), where `bytes` is of type `Buffer` (not `Uint8Array`), the 20 | * string will be the underlying memory block reserved for one (or more) 21 | * buffers, rather than what you intend! 22 | * 23 | * You can only use `Buffer.from(bytes.buffer)` if `bytes` is only a 24 | * `Uint8Array`. 25 | */ 26 | if (bytes instanceof Buffer) { 27 | return bytes.toString(codec as any); 28 | } 29 | return (Buffer.from(bytes.buffer, bytes.byteOffset, bytes.length)).toString(codec as any); 30 | } else if (typeof TextEncoder !== "undefined") { // Browser JavaScript 31 | return (new TextDecoder(codec)).decode(bytes); 32 | } 33 | throw new ASN1Error("Neither TextDecoder nor Buffer are defined to decode bytes into text."); 34 | } 35 | -------------------------------------------------------------------------------- /test/x690/invalid/truncation.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(CodecElement.constructor.name, () => { 11 | it("throws when decoding an element with a truncated tag number", () => { 12 | const el = new CodecElement(); 13 | assert.throws(() => el.fromBytes(new Uint8Array([ 31, 0x80, 0x06 ]))); 14 | }); 15 | 16 | it("throws when decoding a truncated OBJECT IDENTIFIER", () => { 17 | const el = new CodecElement(); 18 | el.value = new Uint8Array([ 0x42, 0x80 ]); 19 | assert.throws(() => el.objectIdentifier); 20 | }); 21 | 22 | it("throws when decoding a truncated RELATIVE OID", () => { 23 | const el = new CodecElement(); 24 | el.value = new Uint8Array([ 0x80, 0x81 ]); 25 | assert.throws(() => el.relativeObjectIdentifier); 26 | }); 27 | 28 | it("throws when decoding a truncated UniversalString", () => { 29 | const el = new CodecElement(); 30 | el.value = new Uint8Array([ 0x00, 0x00, 0x00 ]); 31 | assert.throws(() => el.universalString); 32 | }); 33 | 34 | it("throws when decoding a truncated BMPString", () => { 35 | const el = new CodecElement(); 36 | el.value = new Uint8Array([ 0x00 ]); 37 | assert.throws(() => el.bmpString); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "bugs": { 3 | "email": "jonathan@wilbur.space", 4 | "url": "https://github.com/JonathanWilbur/asn1-ts/issues" 5 | }, 6 | "contributors": [ 7 | { 8 | "email": "jonathan@wilbur.space", 9 | "name": "Jonathan M. Wilbur", 10 | "url": "https://jonathan.wilbur.space/" 11 | } 12 | ], 13 | "description": "ASN.1 encoding and decoding, including BER, CER, and DER.", 14 | "devDependencies": { 15 | "@types/node": "^22.10.5", 16 | "typescript": "^5.7.2" 17 | }, 18 | "directories": { 19 | "doc": "documentation", 20 | "test": "test" 21 | }, 22 | "files": [ 23 | "dist/**/*" 24 | ], 25 | "homepage": "https://github.com/JonathanWilbur/asn1-ts", 26 | "keywords": [ 27 | "ASN1", 28 | "ASN.1", 29 | "X.690", 30 | "X690", 31 | "BER", 32 | "DER" 33 | ], 34 | "license": "MIT", 35 | "main": "./dist/index.mjs", 36 | "name": "asn1-ts", 37 | "private": false, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/JonathanWilbur/asn1-ts.git" 41 | }, 42 | "scripts": { 43 | "benchmark": "node ./test/benchmark.mjs", 44 | "build": "npx tsc", 45 | "buntest": "bun test", 46 | "clean": "rm -rf dist; mkdir -p dist", 47 | "denocheck": "deno test ./denotest.mts", 48 | "test": "node --test" 49 | }, 50 | "types": "./dist/index.d.mts", 51 | "version": "11.0.1", 52 | "exports": { 53 | "./functional": "./dist/functional.mjs" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /source/codecs/der/decoders/decodeBitString.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../../../errors.mjs"; 2 | import type { BIT_STRING } from "../../../macros.mjs"; 3 | import { TRUE_BIT, FALSE_BIT } from "../../../macros.mjs"; 4 | 5 | /** 6 | * This assumes primitive encoding. 7 | */ 8 | export default 9 | function decodeBitString (value: Uint8Array): BIT_STRING { 10 | if (value.length === 0) { 11 | throw new errors.ASN1Error("ASN.1 BIT STRING cannot be encoded on zero bytes!"); 12 | } 13 | if (value.length === 1 && value[0] !== 0) { 14 | throw new errors.ASN1Error("ASN.1 BIT STRING encoded with deceptive first byte!"); 15 | } 16 | if (value[0] > 7) { 17 | throw new errors.ASN1Error("First byte of an ASN.1 BIT STRING must be <= 7!"); 18 | } 19 | 20 | const ret: number[] = []; 21 | for (let i = 1; i < value.length; i++) { 22 | ret.push( 23 | ((value[i] & 0b10000000) ? TRUE_BIT : FALSE_BIT), 24 | ((value[i] & 0b01000000) ? TRUE_BIT : FALSE_BIT), 25 | ((value[i] & 0b00100000) ? TRUE_BIT : FALSE_BIT), 26 | ((value[i] & 0b00010000) ? TRUE_BIT : FALSE_BIT), 27 | ((value[i] & 0b00001000) ? TRUE_BIT : FALSE_BIT), 28 | ((value[i] & 0b00000100) ? TRUE_BIT : FALSE_BIT), 29 | ((value[i] & 0b00000010) ? TRUE_BIT : FALSE_BIT), 30 | ((value[i] & 0b00000001) ? TRUE_BIT : FALSE_BIT), 31 | ); 32 | } 33 | for (const bit of ret.slice((ret.length - value[0]))) { 34 | if (bit) throw new errors.ASN1Error("BIT STRING had a trailing set bit."); 35 | } 36 | ret.length -= value[0]; 37 | return new Uint8ClampedArray(ret); 38 | } 39 | -------------------------------------------------------------------------------- /test/x690/invalid/padding.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(CodecElement.constructor.name, () => { 11 | it("throws when decoding an element with unnecessary padding bytes on the tag number", () => { 12 | const el = new CodecElement(); 13 | assert.throws(() => el.fromBytes(new Uint8Array([ 31, 0x80, 0x06 ]))); 14 | }); 15 | 16 | it("throws when decoding an INTEGER with unnecessary padding", () => { 17 | const el = new CodecElement(); 18 | el.value = new Uint8Array([ 0xFF, 0xFF, 0xF3 ]); 19 | assert.throws(() => el.integer); 20 | }); 21 | 22 | it("throws when decoding an OBJECT IDENTIFIER with unnecessary padding", () => { 23 | const el = new CodecElement(); 24 | el.value = new Uint8Array([ 0x42, 0x80, 0x06 ]); 25 | assert.throws(() => el.objectIdentifier); 26 | }); 27 | 28 | it("throws when decoding an ENUMERATED with unnecessary padding", () => { 29 | const el = new CodecElement(); 30 | el.value = new Uint8Array([ 0xFF, 0xFF, 0xF3 ]); 31 | assert.throws(() => el.enumerated); 32 | }); 33 | 34 | it("throws when decoding a RELATIVE OID with unnecessary padding", () => { 35 | const el = new CodecElement(); 36 | el.value = new Uint8Array([ 0x80, 0x06 ]); 37 | assert.throws(() => el.relativeObjectIdentifier); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/der/utc.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { test } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | const validTests = [ 6 | [ "010203040506Z", "2001-02-03T04:05:06.000Z" ], 7 | ]; 8 | 9 | const invalidTest = [ 10 | [ "21030303030303003030030030300303003003030303330033" ], 11 | [ "21150204Z" ], 12 | [ "21016204Z" ], 13 | [ "21020329Z" ], 14 | [ "2102032067Z" ], 15 | [ "210203205967Z" ], 16 | [ "21" ], 17 | [ "2102" ], 18 | [ "210203" ], 19 | [ "21020304.05.06" ], 20 | [ "21020304,05,06" ], 21 | [ "21020304,05,06" ], 22 | [ "21020320.25-0" ], 23 | [ "21020320.25+0" ], 24 | [ "21020320.25-081" ], 25 | [ "21020320.25+081" ], 26 | [ "21020320.25-08105" ], 27 | [ "21020320.25+08105" ], 28 | [ "21020320.25-0810Z" ], 29 | [ "21020320.25+0810Z" ], 30 | [ "0102030405Z" ], 31 | [ "0102030405-0400" ], 32 | [ "010203040506-0400" ], 33 | [ "0102030405+0400" ], 34 | [ "010203040506+0400" ], 35 | [ "0102030405-0415" ], 36 | [ "010203040506-0415" ], 37 | [ "0102030405+0415" ], 38 | [ "010203040506+0415" ], 39 | ]; 40 | 41 | for (const [ gt, isot ] of validTests) { 42 | test("DER-encoded UTCTime " + gt + " should decode correctly", () => { 43 | const el = new asn1.DERElement(); 44 | el.value = Buffer.from(gt); 45 | const g = el.utcTime; 46 | assert.equal(g.toISOString(), isot); 47 | }); 48 | } 49 | 50 | for (const [ gt ] of invalidTest) { 51 | test("DER-encoded UTCTime " + gt + " should fail to decode by throwing", () => { 52 | const el = new asn1.DERElement(); 53 | el.value = Buffer.from(gt); 54 | assert.throws(() => el.utcTime); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /source/index.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module contains all of the main exports for this library. This includes 3 | * `BERElement`, `CERElement`, `DERElement`, `ABSTRACT_SYNTAX`, 4 | * `TYPE_IDENTIFIER`, the error types, the type aliases and other macros, 5 | * constants, and some utilities. 6 | * 7 | * Encoding and decoding example: 8 | * 9 | * @example 10 | * ```ts 11 | * let el: BERElement = new BERElement(); 12 | * el.tagClass = ASN1TagClass.universal; // Not technically necessary. 13 | * el.construction = ASN1Construction.primitive; // Not technically necessary. 14 | * el.tagNumber = ASN1UniversalType.integer; 15 | * el.integer = 1433; // Now the data is encoded. 16 | * console.log(el.integer); // Logs '1433' 17 | * ``` 18 | * 19 | * ... and here is how you would decode that same element: 20 | * 21 | * @example 22 | * ```ts 23 | * const encodedData: Uint8Array = el.toBytes(); 24 | * const el2: BERElement = new BERElement(); 25 | * el2.fromBytes(encodedData); 26 | * console.log(el2.integer); // Logs 1433 27 | * ``` 28 | * 29 | * @module 30 | */ 31 | 32 | export { default as ASN1Element } from "./asn1.mjs"; 33 | export { default as BERElement } from "./codecs/ber.mjs"; 34 | export { default as CERElement } from "./codecs/cer.mjs"; 35 | export { default as DERElement } from "./codecs/der.mjs"; 36 | export { default as sortCanonically } from "./utils/sortCanonically.mjs"; 37 | export { default as compareSetOfElementsCanonically } from "./utils/compareSetOfElementsCanonically.mjs"; 38 | export * from "./classes/index.mjs"; 39 | export * from "./errors.mjs"; 40 | export * from "./interfaces/index.mjs"; 41 | export * from "./macros.mjs"; 42 | export * from "./types/index.mjs"; 43 | export * from "./validators/index.mjs"; 44 | export * from "./values.mjs"; 45 | export * from "./utils/index.mjs"; 46 | -------------------------------------------------------------------------------- /source/utils/encodeSignedBigEndianInteger.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../errors.mjs"; 2 | import { MIN_SINT_32, MAX_SINT_32 } from "../values.mjs"; 3 | 4 | /** 5 | * @summary Encodes a number as a signed big-endian integer 6 | * @description 7 | * Throws if the value is out of the 32-bit signed integer range. 8 | * @param {number} value - The signed integer to encode. 9 | * @returns {Uint8Array} The encoded big-endian bytes. 10 | * @throws {ASN1OverflowError} If the value is out of range for a 32-bit signed integer. 11 | * @function 12 | */ 13 | export default 14 | function encodeBigEndianSignedInteger (value: number): Uint8Array { 15 | if (value < MIN_SINT_32) { 16 | throw new errors.ASN1OverflowError( 17 | `Number ${value} too small to be encoded as a big-endian signed integer.`, 18 | ); 19 | } 20 | if (value > MAX_SINT_32) { 21 | throw new errors.ASN1OverflowError( 22 | `Number ${value} too big to be encoded as a big-endian signed integer.`, 23 | ); 24 | } 25 | 26 | if (value <= 127 && value >= -128) { 27 | return new Uint8Array([ 28 | (value & 255), 29 | ]); 30 | } else if (value <= 32767 && value >= -32768) { 31 | return new Uint8Array([ 32 | ((value >> 8) & 255), 33 | (value & 255), 34 | ]); 35 | } else if (value <= 8388607 && value >= -8388608) { 36 | return new Uint8Array([ 37 | ((value >> 16) & 255), 38 | ((value >> 8) & 255), 39 | (value & 255), 40 | ]); 41 | } else { 42 | return new Uint8Array([ 43 | ((value >> 24) & 255), 44 | ((value >> 16) & 255), 45 | ((value >> 8) & 255), 46 | (value & 255), 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /source/codecs/der/decoders/decodeUTCTime.mts: -------------------------------------------------------------------------------- 1 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 2 | import * as errors from "../../../errors.mjs"; 3 | import validateDateTime from "../../../validators/validateDateTime.mjs"; 4 | import type { UTCTime } from "../../../macros.mjs"; 5 | 6 | const DER_UTC_TIME_LENGTH = "920521000000Z".length; 7 | 8 | export default 9 | function decodeUTCTime (value: Uint8Array): UTCTime { 10 | if (value.length !== DER_UTC_TIME_LENGTH) { 11 | throw new errors.ASN1Error("Malformed DER UTCTime string: not a valid length"); 12 | } 13 | const dateString: string = convertBytesToText(value); 14 | if (!dateString.endsWith("Z")) { 15 | throw new errors.ASN1Error("Malformed DER UTCTime string: not UTC timezone"); 16 | } 17 | let year: number = Number(dateString.slice(0, 2)); 18 | const month: number = (Number(dateString.slice(2, 4)) - 1); 19 | const date: number = Number(dateString.slice(4, 6)); 20 | const hours: number = Number(dateString.slice(6, 8)); 21 | const minutes: number = Number(dateString.slice(8, 10)); 22 | const seconds: number = Number(dateString.slice(10, 12)); 23 | /** 24 | * The ITU specifications for ASN.1 and related codecs do not specify what 25 | * century the year digits of a UTCTime value refers to. However, ITU 26 | * Recommendation X.509 (2019), Section 7.2, states that the `utcTime` 27 | * alternative of the `Time` type shall be interpreted as being year 20XX if 28 | * XX is between 0 and 49 inclusive, and 19XX otherwise. 29 | */ 30 | year = (year <= 49) 31 | ? (2000 + year) 32 | : (1900 + year); 33 | validateDateTime("UTCTime", year, month, date, hours, minutes, seconds); 34 | return new Date(Date.UTC(year, month, date, hours, minutes, seconds)); 35 | } 36 | -------------------------------------------------------------------------------- /source/types/CharacterString.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | 3 | /** 4 | * A `CharacterString`, is a constructed data type, defined 5 | * in the [International Telecommunications Union](https://www.itu.int)'s 6 | * [X.680](https://www.itu.int/rec/T-REC-X.680/en). 7 | * The specification defines `CharacterString` as: 8 | * 9 | * ```asn1 10 | * CHARACTER STRING ::= [UNIVERSAL 29] SEQUENCE { 11 | * identification CHOICE { 12 | * syntaxes SEQUENCE { 13 | * abstract OBJECT IDENTIFIER, 14 | * transfer OBJECT IDENTIFIER }, 15 | * syntax OBJECT IDENTIFIER, 16 | * presentation-context-id INTEGER, 17 | * context-negotiation SEQUENCE { 18 | * presentation-context-id INTEGER, 19 | * transfer-syntax OBJECT IDENTIFIER }, 20 | * transfer-syntax OBJECT IDENTIFIER, 21 | * fixed NULL }, 22 | * string-value OCTET STRING } 23 | * ``` 24 | * 25 | * This assumes `AUTOMATIC TAGS`, so all of the `identification` 26 | * choices will be `CONTEXT-SPECIFIC` and numbered from 0 to 5. 27 | */ 28 | export default 29 | class CharacterString { 30 | constructor ( 31 | readonly identification: ASN1Element, 32 | readonly stringValue: Uint8Array, 33 | ) {} 34 | 35 | public toString (): string { 36 | return ( 37 | "CHARACTER STRING { " 38 | + `identification ${this.identification.toString()} ` 39 | + `dataValue ${Array.from(this.stringValue).map((byte) => byte.toString(16)).join("")} ` 40 | + "}" 41 | ); 42 | } 43 | 44 | public toJSON (): unknown { 45 | return { 46 | identification: this.identification.toJSON(), 47 | dataValue: Array.from(this.stringValue).map((byte) => byte.toString(16)).join(""), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/x690/invalid/empty.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(CodecElement.constructor.name, () => { 11 | it("throws when decoding a zero-byte BOOLEAN", () => { 12 | const el = new CodecElement(); 13 | el.value = new Uint8Array(0); 14 | assert.throws(() => el.boolean); 15 | }); 16 | 17 | it("throws when decoding a zero-byte INTEGER", () => { 18 | const el = new CodecElement(); 19 | el.value = new Uint8Array(0); 20 | assert.throws(() => el.integer); 21 | }); 22 | 23 | it("throws when decoding a zero-byte BIT STRING", () => { 24 | const el = new CodecElement(); 25 | el.value = new Uint8Array(0); 26 | assert.throws(() => el.bitString); 27 | }); 28 | 29 | it("throws when decoding a zero-byte OBJECT IDENTIFIER", () => { 30 | const el = new CodecElement(); 31 | el.value = new Uint8Array(0); 32 | assert.throws(() => el.objectIdentifier); 33 | }); 34 | 35 | it("throws when decoding a zero-byte ENUMERATED", () => { 36 | const el = new CodecElement(); 37 | el.value = new Uint8Array(0); 38 | assert.throws(() => el.enumerated); 39 | }); 40 | 41 | it("throws when decoding a zero-byte UTCTime", () => { 42 | const el = new CodecElement(); 43 | el.value = new Uint8Array(0); 44 | assert.throws(() => el.utcTime); 45 | }); 46 | 47 | it("throws when decoding a zero-byte GeneralizedTime", () => { 48 | const el = new CodecElement(); 49 | el.value = new Uint8Array(0); 50 | assert.throws(() => el.generalizedTime); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/ber/utc.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { test } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | const validTests = [ 6 | [ "0102030405Z", "2001-02-03T04:05:00.000Z" ], 7 | [ "010203040506Z", "2001-02-03T04:05:06.000Z" ], 8 | [ "0102030405-0400", "2001-02-03T08:05:00.000Z" ], 9 | [ "010203040506-0400", "2001-02-03T08:05:06.000Z" ], 10 | [ "0102030405+0400", "2001-02-03T00:05:00.000Z" ], 11 | [ "010203040506+0400", "2001-02-03T00:05:06.000Z" ], 12 | // Minute-specific timezone offsets 13 | [ "0102030405-0415", "2001-02-03T08:20:00.000Z" ], 14 | [ "010203040506-0415", "2001-02-03T08:20:06.000Z" ], 15 | [ "0102030405+0415", "2001-02-02T23:50:00.000Z" ], 16 | [ "010203040506+0415", "2001-02-02T23:50:06.000Z" ], 17 | ]; 18 | 19 | const invalidTests = [ 20 | [ "21030303030303003030030030300303003003030303330033" ], 21 | [ "21150204Z" ], 22 | [ "21016204Z" ], 23 | [ "21020329Z" ], 24 | [ "2102032067Z" ], 25 | [ "210203205967Z" ], 26 | [ "21" ], 27 | [ "2102" ], 28 | [ "210203" ], 29 | [ "21020304.05.06" ], 30 | [ "21020304,05,06" ], 31 | [ "21020304,05,06" ], 32 | [ "21020320.25-0" ], 33 | [ "21020320.25+0" ], 34 | [ "21020320.25-081" ], 35 | [ "21020320.25+081" ], 36 | [ "21020320.25-08105" ], 37 | [ "21020320.25+08105" ], 38 | [ "21020320.25-0810Z" ], 39 | [ "21020320.25+0810Z" ], 40 | ]; 41 | 42 | for (const [ gt, isot ] of validTests) { 43 | test(gt + " should decode correctly as a UTCTime", () => { 44 | const el = new asn1.BERElement(); 45 | el.value = Buffer.from(gt); 46 | const g = el.utcTime; 47 | assert.equal(g.toISOString(), isot); 48 | }); 49 | } 50 | 51 | for (const gt of invalidTests) { 52 | test(gt + " should fail to decode as a UTCTime", () => { 53 | const el = new asn1.BERElement(); 54 | el.value = Buffer.from(gt); 55 | assert.throws(() => el.utcTime.toISOString()); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /source/utils/index.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * Barrel file exporting ASN.1 utility functions for encoding, decoding, and manipulating ASN.1 data. 3 | * Includes helpers for integer, real, bit, and byte operations as per ITU X.690. 4 | */ 5 | export { default as base128Length } from "./base128Length.mjs"; 6 | export { default as decodeIEEE754DoublePrecisionFloat } from "./decodeIEEE754DoublePrecisionFloat.mjs"; 7 | export { default as decodeIEEE754SinglePrecisionFloat } from "./decodeIEEE754SinglePrecisionFloat.mjs"; 8 | export { default as decodeSignedBigEndianInteger } from "./decodeSignedBigEndianInteger.mjs"; 9 | export { default as decodeUnsignedBigEndianInteger } from "./decodeUnsignedBigEndianInteger.mjs"; 10 | export { default as decodeX690RealNumber } from "./decodeX690RealNumber.mjs"; 11 | export { default as dissectFloat } from "./dissectFloat.mjs"; 12 | export { default as encodeIEEE754DoublePrecisionFloat } from "./encodeIEEE754DoublePrecisionFloat.mjs"; 13 | export { default as encodeIEEE754SinglePrecisionFloat } from "./encodeIEEE754SinglePrecisionFloat.mjs"; 14 | export { default as encodeSignedBigEndianInteger } from "./encodeSignedBigEndianInteger.mjs"; 15 | export { default as encodeUnsignedBigEndianInteger } from "./encodeUnsignedBigEndianInteger.mjs"; 16 | export { default as encodeX690Base10RealNumber } from "./encodeX690Base10RealNumber.mjs"; 17 | export { default as encodeX690BinaryRealNumber } from "./encodeX690BinaryRealNumber.mjs"; 18 | export { default as getBitFromBase128 } from "./getBitFromBase128.mjs"; 19 | export { default as getBitFromBase256 } from "./getBitFromBase256.mjs"; 20 | export { default as isInCanonicalOrder } from "./isInCanonicalOrder.mjs"; 21 | export { default as isUniquelyTagged } from "./isUniquelyTagged.mjs"; 22 | export { default as packBits } from "./packBits.mjs"; 23 | export { default as setBitInBase128 } from "./setBitInBase128.mjs"; 24 | export { default as setBitInBase256 } from "./setBitInBase256.mjs"; 25 | export { default as trimLeadingPaddingBytes } from "./trimLeadingPaddingBytes.mjs"; 26 | export { default as unpackBits } from "./unpackBits.mjs"; 27 | -------------------------------------------------------------------------------- /test/x690/time.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(CodecElement.constructor.name, () => { 11 | it("Encodes a DATE correctly", () => { 12 | const el = new CodecElement(); 13 | el.date = new Date(2020, 3, 7); 14 | assert.deepEqual(el.value, new Uint8Array([ 15 | 0x32, 0x30, 0x32, 0x30, 0x30, 0x34, 0x30, 0x37, 16 | ])); 17 | const output = el.date; 18 | assert.equal(output.getFullYear(), 2020); 19 | assert.equal(output.getMonth(), 3); 20 | assert.equal(output.getDate(), 7); 21 | }); 22 | 23 | it("Encodes a TIME-OF-DAY correctly", () => { 24 | const el = new CodecElement(); 25 | el.timeOfDay = new Date(2020, 3, 7, 15, 58, 23); 26 | assert.deepEqual(el.value, new Uint8Array([ 27 | 0x31, 0x35, 0x35, 0x38, 0x32, 0x33, 28 | ])); 29 | const output = el.timeOfDay; 30 | assert.equal(output.getHours(), 15); 31 | assert.equal(output.getMinutes(), 58); 32 | assert.equal(output.getSeconds(), 23); 33 | }); 34 | 35 | it("Encodes a DATE-TIME correctly", () => { 36 | const el = new CodecElement(); 37 | el.dateTime = new Date(2020, 3, 7, 15, 58, 23); 38 | assert.deepEqual(el.value, new Uint8Array([ 39 | 0x32, 0x30, 0x32, 0x30, 0x30, 0x34, 0x30, 0x37, 40 | 0x31, 0x35, 0x35, 0x38, 0x32, 0x33, 41 | ])); 42 | const output = el.dateTime; 43 | assert.equal(output.getFullYear(), 2020); 44 | assert.equal(output.getMonth(), 3); 45 | assert.equal(output.getDate(), 7); 46 | assert.equal(output.getHours(), 15); 47 | assert.equal(output.getMinutes(), 58); 48 | assert.equal(output.getSeconds(), 23); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2020", 5 | "module": "NodeNext", 6 | "allowJs": false, 7 | "declaration": true, 8 | "declarationMap": false, 9 | "sourceMap": false, 10 | "outDir": "./dist", 11 | // "rootDir": "./", 12 | "removeComments": true, 13 | // "noEmit": true, 14 | // "downlevelIteration": true, 15 | "isolatedModules": true, 16 | "strict": true, 17 | "noImplicitAny": true, 18 | "strictNullChecks": true, 19 | "strictFunctionTypes": true, 20 | "strictPropertyInitialization": true, 21 | "noImplicitThis": true, 22 | "alwaysStrict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noImplicitReturns": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "strictBindCallApply": true, 28 | "noImplicitOverride": true, 29 | "moduleResolution": "nodenext", 30 | "exactOptionalPropertyTypes": true, 31 | "noUncheckedIndexedAccess": false, 32 | "forceConsistentCasingInFileNames": true, 33 | "skipLibCheck": true, 34 | "useUnknownInCatchVariables": true, 35 | // "baseUrl": "./", 36 | // "paths": {}, 37 | // "rootDirs": [], 38 | // "typeRoots": [], 39 | // "types": [], 40 | // "allowSyntheticDefaultImports": true, 41 | "esModuleInterop": false, 42 | // "preserveSymlinks": true, 43 | // "sourceRoot": "", 44 | // "mapRoot": "", 45 | // "inlineSourceMap": true, 46 | // "inlineSources": true, 47 | // "experimentalDecorators": true, 48 | // "emitDecoratorMetadata": true, 49 | "noPropertyAccessFromIndexSignature": true, 50 | "allowUnreachableCode": false, 51 | "allowUnusedLabels": false, 52 | "noEmitOnError": true, 53 | "preserveConstEnums": true, 54 | "resolveJsonModule": true, 55 | "checkJs": false, 56 | "verbatimModuleSyntax": true 57 | }, 58 | "include": [ 59 | "source/**/*.mts" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /test/x690/fromSeqOrSet.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(`${CodecElement.constructor.name}.fromSequence()`, () => { 11 | it("encodes a SEQUENCE correctly", () => { 12 | const el = CodecElement.fromSequence([ 13 | new CodecElement( 14 | asn1.ASN1TagClass.universal, 15 | asn1.ASN1Construction.primitive, 16 | asn1.ASN1UniversalType.boolean, 17 | false, 18 | ), 19 | null, 20 | new CodecElement( 21 | asn1.ASN1TagClass.universal, 22 | asn1.ASN1Construction.primitive, 23 | asn1.ASN1UniversalType.boolean, 24 | true, 25 | ), 26 | undefined, 27 | ]); 28 | assert.equal(el.sequence.length, 2); 29 | assert.equal(el.sequence[0].boolean, false); 30 | assert.equal(el.sequence[1].boolean, true); 31 | }); 32 | }); 33 | 34 | describe(`${CodecElement.constructor.name}.fromSet()`, () => { 35 | it("encodes a SET correctly", () => { 36 | const el = CodecElement.fromSetOf([ 37 | new CodecElement( 38 | asn1.ASN1TagClass.universal, 39 | asn1.ASN1Construction.primitive, 40 | asn1.ASN1UniversalType.boolean, 41 | false, 42 | ), 43 | null, 44 | new CodecElement( 45 | asn1.ASN1TagClass.universal, 46 | asn1.ASN1Construction.primitive, 47 | asn1.ASN1UniversalType.boolean, 48 | true, 49 | ), 50 | undefined, 51 | ]); 52 | assert.equal(el.setOf.length, 2); 53 | assert.equal(el.setOf[0].boolean, false); 54 | assert.equal(el.setOf[1].boolean, true); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/cer/cer.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | describe("Canonical Encoding Rules", function () { 6 | it("encodes and decodes long BIT STRINGs correctly", () => { 7 | const longOddBits = new Array(4053); 8 | const longEvenBits = new Array(4000); 9 | 10 | const el = new asn1.CERElement(); 11 | [ 12 | longOddBits, 13 | longEvenBits, 14 | ].forEach((bits) => { 15 | for (let i = 0; i < bits.length; i++) { 16 | bits[i] = (i % 2); 17 | } 18 | 19 | el.bitString = new Uint8ClampedArray(bits); 20 | assert.deepEqual(el.bitString, new Uint8ClampedArray(bits)); 21 | }); 22 | }); 23 | 24 | it("encodes and decodes long OCTET STRINGs correctly", () => { 25 | const longOddBytes = new Uint8Array(4053); 26 | const longEvenBytes = new Uint8Array(4000); 27 | 28 | const el = new asn1.CERElement(); 29 | [ 30 | longOddBytes, 31 | longEvenBytes, 32 | ].forEach((bytes) => { 33 | el.octetString = bytes; 34 | assert.deepEqual(el.octetString, Buffer.from(bytes)); 35 | }); 36 | }); 37 | 38 | it("encodes and decodes long strings correctly", () => { 39 | // An odd length deliberately chosen so that it is not evenly 40 | // divisible by 1000. 41 | const longOddString = "1234567".repeat(453); 42 | 43 | // Intentionally evenly divisible by 1000. 44 | const longEvenString = "1234567890".repeat(400); 45 | 46 | const el = new asn1.CERElement(); 47 | [ 48 | longOddString, 49 | longEvenString, 50 | ].forEach((str) => { 51 | el.objectDescriptor = str; 52 | assert.equal(el.objectDescriptor, str); 53 | 54 | el.utf8String = str; 55 | assert.equal(el.utf8String, str); 56 | 57 | el.universalString = str; 58 | assert.equal(el.universalString, str); 59 | 60 | el.bmpString = str; 61 | assert.equal(el.bmpString, str); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/der/invalid.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | describe("Distinguished Encoding Rules", () => { 6 | it("throws an exception when decoding a length that could have been encoded on fewer octets", () => { 7 | const el = new asn1.DERElement(); 8 | const data = new Uint8Array([ 0x05, 0x83, 0x00, 0x00, 0x01, 0xFF ]); 9 | assert.throws(() => el.fromBytes(data)); 10 | }); 11 | 12 | it("throws an exception when decoding a multi-byte BOOLEAN", () => { 13 | const el = new asn1.DERElement(); 14 | el.value = new Uint8Array([ 0x01, 0x01 ]); 15 | assert.throws(() => el.boolean); 16 | }); 17 | 18 | it("throws an exception when decoding a BOOLEAN that is not 0x00 or 0xFF", () => { 19 | const el = new asn1.DERElement(); 20 | el.value = new Uint8Array([ 0x38 ]); 21 | assert.throws(() => el.boolean); 22 | }); 23 | 24 | it("throws an exception when decoding a BIT STRING with a deceptive first byte", () => { 25 | const el = new asn1.DERElement(); 26 | el.value = new Uint8Array([ 0x05 ]); 27 | assert.throws(() => el.bitString); 28 | }); 29 | 30 | it("throws an exception when decoding a BIT STRING with trailing set bits", () => { 31 | const el = new asn1.DERElement(); 32 | el.value = new Uint8Array([ 0x03, 0x02 ]); 33 | assert.throws(() => el.bitString); 34 | }); 35 | 36 | it("throws an exception when decoding a BIT STRING with a first byte greater than 7", () => { 37 | const el = new asn1.DERElement(); 38 | el.value = new Uint8Array([ 0x08, 0x0F, 0xF0 ]); 39 | assert.throws(() => el.bitString); 40 | }); 41 | 42 | it("throws an exception when decoding a constructed BIT STRING whose non-terminal subcomponents start with non-zero value bytes", () => { 43 | const data = [ 44 | 0x23, 0x0E, 45 | 0x03, 0x02, 0x03, 0x0F, // The 0x03 is what should cause this to throw. 46 | 0x23, 0x04, 47 | 0x03, 0x02, 0x00, 0x0F, 48 | 0x03, 0x02, 0x05, 0xF0, 49 | ]; 50 | const element = new asn1.DERElement(); 51 | element.fromBytes(data); 52 | assert.throws(() => element.bitString); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /source/codecs/der/decoders/decodeGeneralizedTime.mts: -------------------------------------------------------------------------------- 1 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 2 | import * as errors from "../../../errors.mjs"; 3 | import validateDateTime from "../../../validators/validateDateTime.mjs"; 4 | import type { GeneralizedTime } from "../../../macros.mjs"; 5 | 6 | export default 7 | function decodeGeneralizedTime (value: Uint8Array): GeneralizedTime { 8 | const dateString: string = convertBytesToText(value); 9 | if (!dateString.endsWith("Z")) { 10 | throw new errors.ASN1Error("Malformed DER GeneralizedTime string: must use UTC timezone"); 11 | } 12 | const year: number = Number(dateString.slice(0, 4)); 13 | const month: number = (Number(dateString.slice(4, 6)) - 1); 14 | const date: number = Number(dateString.slice(6, 8)); 15 | const hours: number = Number(dateString.slice(8, 10)); 16 | const minutes: number = Number(dateString.slice(10, 12)); 17 | const seconds: number = Number(dateString.slice(12, 14)); 18 | if (dateString[14] === '.') { 19 | let i = 15; 20 | while (value[i] && value[i] >= 0x30 && value[i] <= 0x39) 21 | i++; 22 | if (i === 15) { 23 | throw new errors.ASN1Error("Malformed DER GeneralizedTime string: trailing stop character"); 24 | } 25 | if (dateString[i] === 'Z') { 26 | i++; 27 | } 28 | if (dateString[i] !== undefined) { 29 | throw new errors.ASN1Error("Malformed DER GeneralizedTime string: trailing data"); 30 | } 31 | const fractionString = `0.${dateString.slice(15, i)}`; 32 | if (fractionString.endsWith("0")) { 33 | throw new errors.ASN1Error("Malformed DER GeneralizedTime string: trailing 0 in milliseconds"); 34 | } 35 | const fraction = Number.parseFloat(fractionString); 36 | const milliseconds = Math.floor(1000 * fraction); 37 | validateDateTime("GeneralizedTime", year, month, date, hours, minutes, seconds); 38 | return new Date(Date.UTC(year, month, date, hours, minutes, seconds, milliseconds)); 39 | } else if (dateString[14] !== 'Z') { 40 | throw new errors.ASN1Error("Malformed DER GeneralizedTime string: trailing data"); 41 | } 42 | validateDateTime("GeneralizedTime", year, month, date, hours, minutes, seconds); 43 | return new Date(Date.UTC(year, month, date, hours, minutes, seconds)); 44 | } 45 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeExternal.mts: -------------------------------------------------------------------------------- 1 | import type { EXTERNAL } from "../../../macros.mjs"; 2 | import DERElement from "../../../codecs/der.mjs"; 3 | import { ASN1TagClass, ASN1UniversalType, ASN1Construction } from "../../../values.mjs"; 4 | import ASN1Element from "../../../asn1.mjs"; 5 | 6 | export default 7 | function encodeExternal (value: EXTERNAL): Uint8Array { 8 | let directReferenceElement: DERElement | undefined = undefined; 9 | if (value.directReference) { 10 | directReferenceElement = new DERElement( 11 | ASN1TagClass.universal, 12 | ASN1Construction.primitive, 13 | ASN1UniversalType.objectIdentifier, 14 | value.directReference, 15 | ); 16 | } 17 | 18 | let indirectReferenceElement: DERElement | undefined = undefined; 19 | if (value.indirectReference) { 20 | indirectReferenceElement = new DERElement( 21 | ASN1TagClass.universal, 22 | ASN1Construction.primitive, 23 | ASN1UniversalType.integer, 24 | value.indirectReference, 25 | ); 26 | } 27 | 28 | let dataValueDescriptorElement: DERElement | undefined = undefined; 29 | if (value.dataValueDescriptor) { 30 | dataValueDescriptorElement = new DERElement( 31 | ASN1TagClass.universal, 32 | ASN1Construction.primitive, 33 | ASN1UniversalType.objectDescriptor, 34 | ); 35 | dataValueDescriptorElement.objectDescriptor = value.dataValueDescriptor; 36 | } 37 | 38 | let encodingElement: DERElement | undefined = undefined; 39 | if (value.encoding instanceof ASN1Element) { 40 | encodingElement = new DERElement( 41 | ASN1TagClass.context, 42 | ASN1Construction.constructed, 43 | 0, 44 | value.encoding, 45 | ); 46 | } else if (value.encoding instanceof Uint8Array) { 47 | encodingElement = new DERElement( 48 | ASN1TagClass.context, 49 | ASN1Construction.primitive, 50 | 1, 51 | value.encoding, 52 | ); 53 | } else { 54 | encodingElement = new DERElement( 55 | ASN1TagClass.context, 56 | ASN1Construction.primitive, 57 | 2, 58 | ); 59 | encodingElement.bitString = value.encoding; 60 | } 61 | 62 | const ret: DERElement = DERElement.fromSequence([ 63 | directReferenceElement, 64 | indirectReferenceElement, 65 | dataValueDescriptorElement, 66 | encodingElement, 67 | ]); 68 | return ret.value; 69 | } 70 | -------------------------------------------------------------------------------- /source/codecs/x690/decoders/decodeExternal.mts: -------------------------------------------------------------------------------- 1 | import External from "../../../types/External.mjs"; 2 | import { ASN1TagClass, ASN1UniversalType } from "../../../values.mjs"; 3 | import ASN1Element from "../../../asn1.mjs"; 4 | import * as errors from "../../../errors.mjs"; 5 | import decodeSequence from "../../der/decoders/decodeSequence.mjs"; 6 | import type { 7 | BIT_STRING, 8 | EXTERNAL, 9 | ObjectDescriptor, 10 | OPTIONAL, 11 | INTEGER, 12 | OBJECT_IDENTIFIER, 13 | } from "../../../macros.mjs"; 14 | 15 | export default 16 | function decodeExternal (value: Uint8Array): EXTERNAL { 17 | const components = decodeSequence(value); 18 | let i: number = 0; 19 | const directReference: OPTIONAL = ( 20 | (components[i]?.tagNumber === ASN1UniversalType.objectIdentifier) 21 | && (components[i].tagClass === ASN1TagClass.universal) 22 | ) 23 | ? components[i++].objectIdentifier 24 | : undefined; 25 | const indirectReference: OPTIONAL = ( 26 | (components[i]?.tagNumber === ASN1UniversalType.integer) 27 | && (components[i].tagClass === ASN1TagClass.universal) 28 | ) 29 | ? components[i++].integer 30 | : undefined; 31 | const dataValueDescriptor: OPTIONAL = ( 32 | (components[i]?.tagNumber === ASN1UniversalType.objectDescriptor) 33 | && (components[i].tagClass === ASN1TagClass.universal) 34 | ) 35 | ? components[i++].objectDescriptor 36 | : undefined; 37 | if (!directReference && !indirectReference) { 38 | throw new errors.ASN1ConstructionError("EXTERNAL must have direct or indirect reference."); 39 | } 40 | const encodingElement = components[i]; 41 | if (!encodingElement || encodingElement.tagClass !== ASN1TagClass.context) { 42 | throw new errors.ASN1ConstructionError("EXTERNAL missing 'encoding' component."); 43 | } 44 | const encoding = ((): ASN1Element | Uint8Array | BIT_STRING => { 45 | switch (encodingElement.tagNumber) { 46 | case (0): return encodingElement.inner; 47 | case (1): return encodingElement.octetString 48 | case (2): return encodingElement.bitString 49 | default: { 50 | throw new errors.ASN1UndefinedError( 51 | "EXTERNAL does not know of an encoding option " 52 | + `having tag number ${encodingElement.tagNumber}.`, 53 | ); 54 | } 55 | } 56 | })(); 57 | return new External( 58 | directReference, 59 | indirectReference, 60 | dataValueDescriptor, 61 | encoding, 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /source/utils/encodeX690BinaryRealNumber.mts: -------------------------------------------------------------------------------- 1 | import dissectFloat from "./dissectFloat.mjs"; 2 | import encodeUnsignedBigEndianInteger from "./encodeUnsignedBigEndianInteger.mjs"; 3 | import encodeSignedBigEndianInteger from "./encodeSignedBigEndianInteger.mjs"; 4 | import { ASN1SpecialRealValue } from "../values.mjs"; 5 | import * as errors from "../errors.mjs"; 6 | 7 | /** 8 | * @summary Encodes a JavaScript number as an ASN.1 `REAL` value using X.690 binary encoding 9 | * @description 10 | * Handles special values and normalization as per ITU X.690. 11 | * @param {number} value - The number to encode. 12 | * @returns {Uint8Array} The encoded REAL value bytes. 13 | * @throws {ASN1OverflowError} If the value is too precise to encode. 14 | * @function 15 | */ 16 | export default 17 | function encodeX690BinaryRealNumber (value: number): Uint8Array { 18 | if (value === 0.0) { 19 | return new Uint8Array(0); 20 | } else if (Number.isNaN(value)) { 21 | return new Uint8Array([ ASN1SpecialRealValue.notANumber ]); 22 | } else if (value === -0.0) { 23 | return new Uint8Array([ ASN1SpecialRealValue.minusZero ]); 24 | } else if (value === Infinity) { 25 | return new Uint8Array([ ASN1SpecialRealValue.plusInfinity ]); 26 | } else if (value === -Infinity) { 27 | return new Uint8Array([ ASN1SpecialRealValue.minusInfinity ]); 28 | } 29 | const floatComponents = dissectFloat(value); 30 | while (floatComponents.mantissa !== 0 && (floatComponents.mantissa % 2) === 0) { 31 | floatComponents.mantissa = floatComponents.mantissa >>> 1; 32 | floatComponents.exponent++; 33 | } 34 | if (floatComponents.exponent <= -1020) { 35 | throw new errors.ASN1OverflowError( 36 | `REAL number ${value} (having exponent ${floatComponents.exponent}) ` 37 | + "is too precise to encode.", 38 | ); 39 | } 40 | const singleByteExponent: boolean = ( 41 | (floatComponents.exponent <= 127) 42 | && (floatComponents.exponent >= -128) 43 | ); 44 | const firstByte: number = ( 45 | 0b1000_0000 46 | | (value >= 0 ? 0b0000_0000 : 0b0100_0000) 47 | | (singleByteExponent ? 0b0000_0000 : 0b0000_0001) 48 | ); 49 | const exponentBytes: Uint8Array = encodeSignedBigEndianInteger(floatComponents.exponent); 50 | const mantissaBytes: Uint8Array = encodeUnsignedBigEndianInteger(floatComponents.mantissa); 51 | const ret: Uint8Array = new Uint8Array(1 + exponentBytes.length + mantissaBytes.length); 52 | ret[0] = firstByte; 53 | ret.set(exponentBytes, 1); 54 | ret.set(mantissaBytes, (1 + exponentBytes.length)); 55 | return ret; 56 | } 57 | -------------------------------------------------------------------------------- /source/validators/validateDate.mts: -------------------------------------------------------------------------------- 1 | import * as errors from "../errors.mjs"; 2 | 3 | /** 4 | * @summary Validates an ASN.1 date (year, month, day) for correctness and range. 5 | * @param {string} dataType - The ASN.1 type being validated (for error messages). 6 | * @param {number} year - The year value. 7 | * @param {number} month - The month value (0-based: 0=Jan, 1=Feb, ... 11=Dec). 8 | * @param {number} date - The day of the month (1-based). 9 | * @returns {void} 10 | * @throws {ASN1Error} if the date is invalid, out of range, or not a valid calendar date. 11 | * @function 12 | */ 13 | export default 14 | function validateDate ( 15 | dataType: string, 16 | year: number, 17 | month: number, 18 | date: number, 19 | ): void { 20 | if (!Number.isSafeInteger(year)) { 21 | throw new errors.ASN1Error(`Invalid year in ${dataType}`); 22 | } 23 | if (!Number.isSafeInteger(month)) { 24 | throw new errors.ASN1Error(`Invalid month in ${dataType}`); 25 | } 26 | if (!Number.isSafeInteger(date) || (date < 1)) { 27 | throw new errors.ASN1Error(`Invalid day in ${dataType}`); 28 | } 29 | switch (month) { 30 | // 31-day months 31 | case 0: // January 32 | case 2: // March 33 | case 4: // May 34 | case 6: // July 35 | case 7: // August 36 | case 9: // October 37 | case 11: { // December 38 | if (date > 31) { 39 | throw new errors.ASN1Error(`Day > 31 encountered in ${dataType} with 31-day month.`); 40 | } 41 | break; 42 | } 43 | // 30-day months 44 | case 3: // April 45 | case 5: // June 46 | case 8: // September 47 | case 10: { // November 48 | if (date > 30) { 49 | throw new errors.ASN1Error(`Day > 31 encountered in ${dataType} with 30-day month.`); 50 | } 51 | break; 52 | } 53 | // 28/29-day month 54 | case 1: { // Feburary 55 | // Source: https://stackoverflow.com/questions/16353211/check-if-year-is-leap-year-in-javascript#16353241 56 | const isLeapYear: boolean = ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0); 57 | if (isLeapYear) { 58 | if (date > 29) { 59 | throw new errors.ASN1Error( 60 | `Day > 29 encountered in ${dataType} with month of February in leap year.`, 61 | ); 62 | } 63 | } else if (date > 28) { 64 | throw new errors.ASN1Error( 65 | `Day > 28 encountered in ${dataType} with month of February and non leap year.`, 66 | ); 67 | } 68 | break; 69 | } 70 | default: 71 | throw new errors.ASN1Error(`Invalid month in ${dataType}`); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/der/gt.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { test } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | const validTests = [ 6 | [ "20210203040506Z", "2021-02-03T04:05:06.000Z" ], 7 | // With fractional seconds 8 | [ "20210203040506.3334Z", "2021-02-03T04:05:06.333Z" ], 9 | ]; 10 | 11 | const invalidTests = [ 12 | [ "2021030303030303003030030030300303003003030303330033" ], 13 | [ "2021150204Z" ], 14 | [ "2021016204Z" ], 15 | [ "2021020329Z" ], 16 | [ "202102032067Z" ], 17 | [ "20210203205967Z" ], 18 | [ "2021" ], 19 | [ "202102" ], 20 | [ "20210203" ], 21 | [ "2021020304.05.06" ], 22 | [ "2021020304,05,06" ], 23 | [ "2021020304,05,06" ], 24 | [ "2021020320.25-0" ], 25 | [ "2021020320.25+0" ], 26 | [ "2021020320.25-081" ], 27 | [ "2021020320.25+081" ], 28 | [ "2021020320.25-08105" ], 29 | [ "2021020320.25+08105" ], 30 | [ "2021020320.25-0810Z" ], 31 | [ "2021020320.25+0810Z" ], 32 | [ "2021020304Z" ], 33 | // With fractional hours 34 | [ "2021020304.3334Z" ], 35 | [ "2021020304,3334Z" ], 36 | [ "2021020304.50Z" ], 37 | [ "2021020304.333333334Z" ], 38 | // With fractional minutes 39 | [ "202102030405.3334Z" ], 40 | [ "202102030405,3334Z" ], 41 | [ "20210203040506,3334Z" ], 42 | // The most complicated examples 43 | [ "20210203040607.32895292-0503" ], 44 | [ "20210203040607,32895292+0304" ], 45 | // Simple timezone offset 46 | [ "20210203040000-05" ], 47 | [ "20210203040000-0500" ], 48 | // Time offset causes carry over into the next or previous day 49 | [ "2021020304+0800" ], 50 | [ "2021020320-0800" ], 51 | // Carry over, but with fractional hours this time 52 | [ "2021020304.25+0800" ], 53 | [ "2021020320.25-0800" ], 54 | // Carry over, but with offset minutes and fractional hours this time 55 | [ "2021020304.25+0815" ], 56 | [ "2021020320.25-0815" ], 57 | // Minutes with timezone offset 58 | [ "202102030406-0500" ], 59 | // Seconds with timezone offset 60 | [ "20210203040607-0500" ], 61 | ]; 62 | 63 | for (const [ gt, isot ] of validTests) { 64 | test("DER-encoded GeneralizedTime " + gt + " should decode correctly to " + isot, () => { 65 | const el = new asn1.DERElement(); 66 | el.value = Buffer.from(gt); 67 | const g = el.generalizedTime; 68 | assert.equal(g.toISOString(), isot); 69 | }); 70 | } 71 | 72 | for (const gt of invalidTests) { 73 | test("Invalid DER-encoded GeneralizedTime " + gt + " should throw when decoding", () => { 74 | const el = new asn1.DERElement(); 75 | el.value = Buffer.from(gt); 76 | assert.throws(() => g.generalizedTime); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /source/codecs/ber/decoders/decodeUTCTime.mts: -------------------------------------------------------------------------------- 1 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 2 | import * as errors from "../../../errors.mjs"; 3 | import validateDateTime from "../../../validators/validateDateTime.mjs"; 4 | import type { UTCTime } from "../../../macros.mjs"; 5 | 6 | export default 7 | function decodeUTCTime (value: Uint8Array): UTCTime { 8 | const dateString: string = convertBytesToText(value); 9 | let year: number = Number(dateString.slice(0, 2)); 10 | /** 11 | * The ITU specifications for ASN.1 and related codecs do not specify what 12 | * century the year digits of a UTCTime value refers to. However, ITU 13 | * Recommendation X.509 (2019), Section 7.2, states that the `utcTime` 14 | * alternative of the `Time` type shall be interpreted as being year 20XX if 15 | * XX is between 0 and 49 inclusive, and 19XX otherwise. 16 | */ 17 | year = (year <= 49) 18 | ? (2000 + year) 19 | : (1900 + year); 20 | const month: number = (Number(dateString.slice(2, 4)) - 1); 21 | const date: number = Number(dateString.slice(4, 6)); 22 | const hours: number = Number(dateString.slice(6, 8)); 23 | const minutes: number = Number(dateString.slice(8, 10)); 24 | const char10 = dateString.charCodeAt(10); 25 | const secondsFieldPresent = (char10 >= 0x30 && char10 <= 0x39); // Is it a digit? 26 | const seconds: number = secondsFieldPresent 27 | ? Number(dateString.slice(10, 12)) 28 | : 0; 29 | let i = secondsFieldPresent ? 12 : 10; 30 | if (dateString[i] === 'Z') { 31 | validateDateTime("UTCTime", year, month, date, hours, minutes, seconds); 32 | return new Date(Date.UTC(year, month, date, hours, minutes, seconds)); 33 | } 34 | if ((dateString[i] !== '+') && (dateString[i] !== '-')) { 35 | throw new errors.ASN1Error(`Malformed BER UTCTime: non +/- offset: ${dateString[i]}`); 36 | } 37 | const isPositive: boolean = dateString[i] === '+'; 38 | i++; 39 | let j = 0; 40 | while (value[i + j] && value[i + j] >= 0x30 && value[i + j] <= 0x39) 41 | j++; 42 | if (j !== 4) { 43 | throw new errors.ASN1Error("Malformed BER UTCTime: non four-digit offset"); 44 | } 45 | i += j; 46 | if (dateString[i] !== undefined) { 47 | throw new errors.ASN1Error("Malformed BER UTCTime: trailing data"); 48 | } 49 | const offsetHour = Number.parseInt(dateString.slice(i - 4, i - 2), 10); 50 | const offsetMinute = Number.parseInt(dateString.slice(i - 2, i), 10); 51 | // You do not need to validate the offset. -99 hours still makes sense, although it is weird. 52 | let epochTimeInMS = Date.UTC(year, month, date, hours, minutes, seconds); 53 | const sign = isPositive ? -1 : 1; // You have to reverse the sign to get back to UTC time. 54 | epochTimeInMS += sign * ((offsetHour * 60 * 60 * 1000) + (offsetMinute * 60 * 1000)); 55 | return new Date(epochTimeInMS); 56 | } 57 | -------------------------------------------------------------------------------- /source/types/External.mts: -------------------------------------------------------------------------------- 1 | import type { 2 | BIT_STRING, 3 | INTEGER, 4 | OBJECT_IDENTIFIER, 5 | OCTET_STRING, 6 | ObjectDescriptor, 7 | } from "../macros.mjs"; 8 | import type ASN1Element from "../asn1.mjs"; 9 | import packBits from "../utils/packBits.mjs"; 10 | 11 | /** 12 | * How `EXTERNAL` is to be encoded, per X.690: 13 | * 14 | * ```asn1 15 | * EXTERNAL ::= [UNIVERSAL 8] IMPLICIT SEQUENCE { 16 | * direct-reference OBJECT IDENTIFIER OPTIONAL, 17 | * indirect-reference INTEGER OPTIONAL, 18 | * data-value-descriptor ObjectDescriptor OPTIONAL, 19 | * encoding CHOICE { 20 | * single-ASN1-type [0] ABSTRACT-SYNTAX.&Type, 21 | * octet-aligned [1] IMPLICIT OCTET STRING, 22 | * arbitrary [2] IMPLICIT BIT STRING } } 23 | * ``` 24 | */ 25 | export default 26 | class External { 27 | constructor ( 28 | readonly directReference: OBJECT_IDENTIFIER | undefined, 29 | readonly indirectReference: INTEGER | undefined, 30 | readonly dataValueDescriptor: ObjectDescriptor | undefined, 31 | readonly encoding: ASN1Element | OCTET_STRING | BIT_STRING, 32 | ) {} 33 | 34 | public toString (): string { 35 | let ret: string = "EXTERNAL { "; 36 | if (this.directReference) { 37 | ret += `directReference ${this.directReference.toString()} `; 38 | } 39 | if (this.indirectReference) { 40 | ret += `indirectReference ${this.indirectReference.toString()} `; 41 | } 42 | if (this.dataValueDescriptor) { 43 | ret += `dataValueDescriptor "${this.dataValueDescriptor}"`; 44 | } 45 | if (this.encoding instanceof Uint8Array) { 46 | ret += `octet-aligned ${Array.from(this.encoding).map((byte) => byte.toString(16)).join("")} `; 47 | } else if (this.encoding instanceof Uint8ClampedArray) { 48 | ret += `arbitrary ${this.encoding.toString()} `; 49 | } else { 50 | ret += `single-ASN1-type ${this.encoding.toString()} `; 51 | } 52 | ret += "}"; 53 | return ret; 54 | } 55 | 56 | public toJSON (): unknown { 57 | return { 58 | directReference: this.directReference, 59 | indirectReference: this.indirectReference, 60 | dataValueDescriptor: this.dataValueDescriptor, 61 | encoding: ((): unknown => { 62 | if (this.encoding instanceof Uint8Array) { 63 | return Array.from(this.encoding).map((byte) => byte.toString(16)).join(""); 64 | } else if (this.encoding instanceof Uint8ClampedArray) { 65 | const bits = this.encoding; 66 | return { 67 | length: bits.length, 68 | value: Array.from(packBits(bits)).map((byte) => byte.toString(16)).join(""), 69 | }; 70 | } else { 71 | return this.encoding.toJSON(); 72 | } 73 | })(), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/misc.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | const MAX_SINT_32 = asn1.MAX_SINT_32; 6 | const MIN_SINT_32 = asn1.MIN_SINT_32; 7 | const MAX_UINT_32 = asn1.MAX_UINT_32; 8 | const MIN_UINT_32 = asn1.MIN_UINT_32; 9 | 10 | describe("The unsigned big-endian integer decoder", () => { 11 | it("decodes MIN_UINT_32 correctly", () => { 12 | const data = new Uint8Array([ 13 | 0x00, 0x00, 0x00, 0x00, 14 | ]); 15 | assert.equal(asn1.decodeUnsignedBigEndianInteger(data), MIN_UINT_32); 16 | }); 17 | 18 | it("decodes 65535 correctly", () => { 19 | const data = new Uint8Array([ 20 | 0xFF, 0xFF, 21 | ]); 22 | assert.equal(asn1.decodeUnsignedBigEndianInteger(data), 65535); 23 | }); 24 | 25 | it("decodes MAX_UINT_32 correctly", () => { 26 | const data = new Uint8Array([ 27 | 0xFF, 0xFF, 0xFF, 0xFF, 28 | ]); 29 | assert.equal(asn1.decodeUnsignedBigEndianInteger(data), MAX_UINT_32); 30 | }); 31 | }); 32 | 33 | describe("The signed big-endian integer decoder", () => { 34 | it("decodes zero correctly", () => { 35 | const data = new Uint8Array([ 36 | 0x00, 0x00, 0x00, 0x00, 37 | ]); 38 | assert.equal(asn1.decodeSignedBigEndianInteger(data), 0); 39 | }); 40 | 41 | it("decodes 65535 correctly", () => { 42 | const data = new Uint8Array([ 43 | 0xFF, 0xFF, 44 | ]); 45 | assert.equal(asn1.decodeSignedBigEndianInteger(data), -1); 46 | }); 47 | 48 | it("decodes MIN_SINT_32 correctly", () => { 49 | const data = new Uint8Array([ 50 | 0x80, 0x00, 0x00, 0x00, 51 | ]); 52 | assert.equal(asn1.decodeSignedBigEndianInteger(data), MIN_SINT_32); 53 | }); 54 | 55 | it("decodes MAX_SINT_32 correctly", () => { 56 | const data = new Uint8Array([ 57 | 0x7F, 0xFF, 0xFF, 0xFF, 58 | ]); 59 | assert.equal(asn1.decodeSignedBigEndianInteger(data), MAX_SINT_32); 60 | }); 61 | }); 62 | 63 | describe("ObjectIdentifier with a prefix", () => { 64 | it("correctly uses the nodes from the prefix", () => { 65 | const ds = asn1.ObjectIdentifier.fromParts([ 2, 5 ]); 66 | const attributeTypes = asn1.ObjectIdentifier.fromParts([ 4 ], ds); 67 | assert.deepEqual(attributeTypes.nodes, [ 2, 5, 4 ]); 68 | }); 69 | }); 70 | 71 | describe("ObjectIdentifier", () => { 72 | it("compares correctly", () => { 73 | const oid1 = asn1.ObjectIdentifier.fromParts([ 2, 5, 4, 3 ]); 74 | const oid2 = asn1.ObjectIdentifier.fromParts([ 2, 5, 4, 3 ]); 75 | const oid3 = asn1.ObjectIdentifier.fromParts([ 2, 5, 4, 5 ]); 76 | const oid4 = asn1.ObjectIdentifier.fromParts([ 1, 5, 4, 3 ]); 77 | assert(asn1.ObjectIdentifier.compare(oid1, oid2)); 78 | assert(!asn1.ObjectIdentifier.compare(oid1, oid3)); 79 | assert(!asn1.ObjectIdentifier.compare(oid1, oid4)); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /source/types/time/DURATION-INTERVAL-ENCODING.mts: -------------------------------------------------------------------------------- 1 | import type { 2 | INTEGER, 3 | OPTIONAL, 4 | } from "../../macros.mjs"; 5 | import * as errors from "../../errors.mjs"; 6 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 7 | 8 | /** 9 | * Defined in ITU Recommendation X.696:2015, Section 29: 10 | * 11 | * `DURATION-INTERVAL-ENCODING ::= SEQUENCE { 12 | * years INTEGER (0..MAX) OPTIONAL, 13 | * months INTEGER (0..MAX) OPTIONAL, 14 | * weeks INTEGER (0..MAX) OPTIONAL, 15 | * days INTEGER (0..MAX) OPTIONAL, 16 | * hours INTEGER (0..MAX) OPTIONAL, 17 | * minutes INTEGER (0..MAX) OPTIONAL, 18 | * seconds INTEGER (0..MAX) OPTIONAL, 19 | * fractional-part SEQUENCE { 20 | * number-of-digits INTEGER (0..MAX), 21 | * fractional-value INTEGER (0..MAX) 22 | * } OPTIONAL 23 | * }` 24 | * 25 | * Note that, in addition to the above, several additional restrictions are 26 | * applied to this structure, which as specified in ITU recommendation X.696. 27 | */ 28 | export default 29 | class DURATION_INTERVAL_ENCODING { 30 | constructor ( 31 | readonly years: OPTIONAL, 32 | readonly months: OPTIONAL, 33 | readonly weeks: OPTIONAL, 34 | readonly days: OPTIONAL, 35 | readonly hours: OPTIONAL, 36 | readonly minutes: OPTIONAL, 37 | readonly seconds: OPTIONAL, 38 | readonly fractional_part: OPTIONAL<{ 39 | number_of_digits: INTEGER; 40 | fractional_value: INTEGER; 41 | }>, 42 | ) { 43 | if ( 44 | typeof weeks !== "undefined" 45 | && (years || months || days || hours || minutes || seconds) 46 | ) { 47 | throw new errors.ASN1Error( 48 | "DURATION-INTERVAL-ENCODING may not combine week components and date-time components.", 49 | ); 50 | } 51 | if (years) { 52 | datetimeComponentValidator("year", 0, Number.MAX_SAFE_INTEGER)("DURATION-INTERVAL-ENCODING", years); 53 | } 54 | if (months) { 55 | datetimeComponentValidator("month", 0, Number.MAX_SAFE_INTEGER)("DURATION-INTERVAL-ENCODING", months); 56 | } 57 | if (weeks) { 58 | datetimeComponentValidator("week", 0, Number.MAX_SAFE_INTEGER)("DURATION-INTERVAL-ENCODING", weeks); 59 | } 60 | if (days) { 61 | datetimeComponentValidator("day", 0, Number.MAX_SAFE_INTEGER)("DURATION-INTERVAL-ENCODING", days); 62 | } 63 | if (hours) { 64 | datetimeComponentValidator("hour", 0, Number.MAX_SAFE_INTEGER)("DURATION-INTERVAL-ENCODING", hours); 65 | } 66 | if (minutes) { 67 | datetimeComponentValidator("minute", 0, Number.MAX_SAFE_INTEGER)("DURATION-INTERVAL-ENCODING", minutes); 68 | } 69 | if (seconds) { 70 | datetimeComponentValidator("second", 0, Number.MAX_SAFE_INTEGER)("DURATION-INTERVAL-ENCODING", seconds); 71 | } 72 | if (fractional_part && !Number.isSafeInteger(fractional_part.fractional_value)) { 73 | throw new errors.ASN1Error("Malformed DURATION-INTERVAL-ENCODING fractional part."); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/ber/gt.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, test } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | const validTests = [ 6 | // LOCAL TIME: This only works for me in Florida. This should be commented out. 7 | // [ "2021020304", "2021-02-03T09:00:00.000Z" ], 8 | // The (second) smallest and simplest time. 9 | [ "2021020304Z", "2021-02-03T04:00:00.000Z" ], 10 | // With fractional hours 11 | [ "2021020304.3334Z", "2021-02-03T04:20:00.000Z" ], 12 | [ "2021020304,3334Z", "2021-02-03T04:20:00.000Z" ], 13 | [ "2021020304.50Z", "2021-02-03T04:30:00.000Z" ], 14 | [ "2021020304.333333334Z", "2021-02-03T04:20:00.000Z" ], 15 | // With fractional minutes 16 | [ "202102030405.3334Z", "2021-02-03T04:05:20.000Z" ], 17 | [ "202102030405,3334Z", "2021-02-03T04:05:20.000Z" ], 18 | // With fractional seconds 19 | [ "20210203040506.3334Z", "2021-02-03T04:05:06.333Z" ], 20 | [ "20210203040506,3334Z", "2021-02-03T04:05:06.333Z" ], 21 | // Simple timezone offset 22 | [ "2021020304-05", "2021-02-03T09:00:00.000Z" ], 23 | [ "2021020304-0500", "2021-02-03T09:00:00.000Z" ], 24 | // Time offset causes carry over into the next or previous day 25 | [ "2021020304+0800", "2021-02-02T20:00:00.000Z" ], 26 | [ "2021020320-0800", "2021-02-04T04:00:00.000Z" ], 27 | // Carry over, but with fractional hours this time 28 | [ "2021020304.25+0800", "2021-02-02T20:15:00.000Z" ], 29 | [ "2021020320.25-0800", "2021-02-04T04:15:00.000Z" ], 30 | // Carry over, but with offset minutes and fractional hours this time 31 | [ "2021020304.25+0815", "2021-02-02T20:00:00.000Z" ], 32 | [ "2021020320.25-0815", "2021-02-04T04:30:00.000Z" ], 33 | // Minutes with timezone offset 34 | [ "202102030406-0500", "2021-02-03T09:06:00.000Z" ], 35 | // Seconds with timezone offset 36 | [ "20210203040607-0500", "2021-02-03T09:06:07.000Z" ], 37 | // The most complicated examples 38 | [ "20210203040607.32895292-0503", "2021-02-03T09:09:07.328Z" ], 39 | [ "20210203040607,32895292+0304", "2021-02-03T01:02:07.328Z" ], 40 | ]; 41 | 42 | const invalidTests = [ 43 | [ "2021030303030303003030030030300303003003030303330033" ], 44 | [ "2021150204Z" ], 45 | [ "2021016204Z" ], 46 | [ "2021020329Z" ], 47 | [ "202102032067Z" ], 48 | [ "20210203205967Z" ], 49 | [ "2021" ], 50 | [ "202102" ], 51 | [ "20210203" ], 52 | [ "2021020304.05.06" ], 53 | [ "2021020304,05,06" ], 54 | [ "2021020304,05,06" ], 55 | [ "2021020320.25-0" ], 56 | [ "2021020320.25+0" ], 57 | [ "2021020320.25-081" ], 58 | [ "2021020320.25+081" ], 59 | [ "2021020320.25-08105" ], 60 | [ "2021020320.25+08105" ], 61 | [ "2021020320.25-0810Z" ], 62 | [ "2021020320.25+0810Z" ], 63 | ]; 64 | 65 | for (const [ gt, isot ] of validTests) { 66 | test(gt + " should decode correctly as a GeneralizedTime", () => { 67 | const el = new asn1.BERElement(); 68 | el.value = Buffer.from(gt); 69 | const g = el.generalizedTime; 70 | assert.equal(g.toISOString(), isot); 71 | }); 72 | } 73 | 74 | for (const gt of invalidTests) { 75 | test(gt + " should fail to decode as a GeneralizedTime", () => { 76 | const el = new asn1.BERElement(); 77 | el.value = Buffer.from(gt); 78 | assert.throws(() => el.generalizedTime); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /source/codecs/x690/encoders/encodeDuration.mts: -------------------------------------------------------------------------------- 1 | import type { DURATION, INTEGER, OPTIONAL } from "../../../macros.mjs"; 2 | import convertTextToBytes from "../../../utils/convertTextToBytes.mjs"; 3 | 4 | export default 5 | function encodeDuration (value: DURATION): Uint8Array { 6 | if (value.weeks) { 7 | if (!value.fractional_part) { 8 | return convertTextToBytes(`${value.weeks}W`); 9 | } else { 10 | const integralAmount: INTEGER = value.weeks; 11 | const fractional_value: number = (typeof value.fractional_part.fractional_value === "bigint") 12 | ? Number(value.fractional_part.fractional_value) 13 | : value.fractional_part.fractional_value; 14 | const number_of_digits: number = (typeof value.fractional_part.number_of_digits === "bigint") 15 | ? Number(value.fractional_part.number_of_digits) 16 | : value.fractional_part.number_of_digits; 17 | const fraction: INTEGER = (fractional_value / Math.pow(10, number_of_digits)); 18 | return convertTextToBytes( 19 | integralAmount.toString() 20 | + fraction.toString().slice(1) // slice(1) gets rid of the leading 0. 21 | + "W", 22 | ); 23 | } 24 | } 25 | 26 | let years: OPTIONAL = (typeof value.years === "bigint") 27 | ? Number(value.years) 28 | : value.years; 29 | let months: OPTIONAL = (typeof value.months === "bigint") 30 | ? Number(value.months) 31 | : value.months; 32 | let days: OPTIONAL = (typeof value.days === "bigint") 33 | ? Number(value.days) 34 | : value.days; 35 | let hours: OPTIONAL = (typeof value.hours === "bigint") 36 | ? Number(value.hours) 37 | : value.hours; 38 | let minutes: OPTIONAL = (typeof value.minutes === "bigint") 39 | ? Number(value.minutes) 40 | : value.minutes; 41 | let seconds: OPTIONAL = (typeof value.seconds === "bigint") 42 | ? Number(value.seconds) 43 | : value.seconds; 44 | 45 | if (value.fractional_part) { 46 | const fractional_value: number = (typeof value.fractional_part.fractional_value === "bigint") 47 | ? Number(value.fractional_part.fractional_value) 48 | : value.fractional_part.fractional_value; 49 | const number_of_digits: number = (typeof value.fractional_part.number_of_digits === "bigint") 50 | ? Number(value.fractional_part.number_of_digits) 51 | : value.fractional_part.number_of_digits; 52 | const fraction: number = fractional_value / Math.pow(10, number_of_digits); 53 | if (seconds !== undefined) { 54 | seconds += fraction; 55 | } else if (minutes !== undefined) { 56 | minutes += fraction; 57 | } else if (hours !== undefined) { 58 | hours += fraction; 59 | } else if (days !== undefined) { 60 | days += fraction; 61 | } else if (months !== undefined) { 62 | months += fraction; 63 | } else if (years !== undefined) { 64 | years += fraction; 65 | } 66 | } 67 | 68 | return convertTextToBytes( 69 | (years ? `${years}Y` : "") 70 | + (months ? `${months}M` : "") 71 | + (days ? `${days}D` : "") 72 | + ((hours || minutes || seconds) ? "T" : "") 73 | + (hours ? `${hours}H` : "") 74 | + (minutes ? `${minutes}M` : "") 75 | + (seconds ? `${seconds}S` : ""), 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /documentation/library.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Terminology 4 | 5 | * This library uses the term "element" to refer to a datum encoded using one of the ASN.1 codecs. 6 | * This library uses the term "mantissa" instead of "significand," because "mantissa" is what's used in the specification. 7 | 8 | ## Structure 9 | 10 | This library begins with `source/asn1.ts`, whose flagship item is `ASN1Element`, 11 | the abstract class from which all other codecs must inherit. An `ASN1Element` 12 | represents a single encoded value (although it could be a single `SEQUENCE` 13 | or `SET`). In the `source/asn1/codecs` directory, you will find all of the codecs that 14 | inherit from `ASN1Element`. The `BERElement` class, for instance, can be found in 15 | `ber.ts`, and it represents a ASN.1 value, encoded via the Basic Encoding Rules 16 | (BER) specified in the 17 | [International Telecommunications Union](https://www.itu.int/en/pages/default.aspx)'s 18 | [X.690 - ASN.1 encoding rules](https://www.itu.int/rec/T-REC-X.690/en). 19 | 20 | ## Usage 21 | 22 | For each codec in the library, usage entails instantiating the class, 23 | then using that class' properties to get and set the encoded value. 24 | For all classes, the empty constructor creates an `END OF CONTENT` 25 | element. The remaining constructors will be codec-specific. 26 | 27 | Here is an example of encoding with Basic Encoding Rules, using the 28 | `BERElement` class. 29 | 30 | ```typescript 31 | let el : BERElement = new BERElement(); 32 | el.typeTag = ASN1UniversalType.integer; 33 | el.integer = 1433; // Now the data is encoded. 34 | console.log(el.integer); // Logs '1433' 35 | ``` 36 | 37 | ... and here is how you would decode that same element: 38 | 39 | ```typescript 40 | let encodedData : Uint8Array = el.toBytes(); 41 | let el2 : BERElement = new BERElement(); 42 | el2.fromBytes(encodedData); 43 | console.log(el2.integer); // Logs '1433' 44 | ``` 45 | 46 | Each codec contains accessors and mutators for each ASN.1 data type. Each property 47 | takes or returns the data type you would expect it to return (for instance, the 48 | `integer` property returns a signed integral type), and each property is given 49 | the unabbreviated name of the data type it encodes, with aliases mapping the 50 | abbreviated names to the unabbreviated names. There are a few exceptions: 51 | 52 | * Properties do not exist for these types: 53 | * `END OF CONTENT` 54 | * `NULL` 55 | * `EXTERNAL` 56 | * `EmbeddedPDV` 57 | * `CharacterString` 58 | 59 | ### Encoding Selected Types 60 | 61 | #### `END OF CONTENT` 62 | 63 | Just calling the default constructor produces an `END OF CONTENT` element. For 64 | example: 65 | 66 | ```typescript 67 | let ber : BERElement = new BERElement(); 68 | ``` 69 | 70 | However, you should not need to do this. When you encode data in indefinite- 71 | length form, the codec should append the `END OF CONTENT` octets for you. 72 | 73 | #### `ANY` 74 | 75 | You just encode any data type like normal, then call `toBytes()`. 76 | The returned value can be inserted where `ANY` goes. 77 | 78 | #### `CHOICE` 79 | 80 | Mind [CVE-2011-1142](https://nvd.nist.gov/vuln/detail/CVE-2011-1142). 81 | 82 | You just encode any data type like normal, then call `toBytes()`. 83 | The returned value can be inserted where `CHOICE` goes. 84 | 85 | #### `SET OF` 86 | 87 | This is encoded the same way that `SET` is encoded. 88 | 89 | #### `SEQUENCE OF` 90 | 91 | This is encoded the same way that `SEQUENCE` is encoded. 92 | 93 | ### Error Handling 94 | 95 | At the moment there are only two exceptions: `ASN1Error` and 96 | `ASN1NotImplementedException`, the latter of which means that this library 97 | has something that has not been implemented. Everything else will be an 98 | `ASN1Error`. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## RFC 2119 Disclosure 4 | 5 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL 6 | NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and 7 | "OPTIONAL" in this document are to be interpreted as described in 8 | [RFC 2119](https://tools.ietf.org/html/rfc2119). 9 | 10 | ## Developer's Certificate of Origin 1.1 11 | 12 | By making a contribution to this project, I certify that: 13 | 14 | * (a) The contribution was created in whole or in part by me and I 15 | have the right to submit it under the open source license 16 | indicated in the file; or 17 | 18 | * (b) The contribution is based upon previous work that, to the best 19 | of my knowledge, is covered under an appropriate open source 20 | license and I have the right under that license to submit that 21 | work with modifications, whether created in whole or in part 22 | by me, under the same open source license (unless I am 23 | permitted to submit under a different license), as indicated 24 | in the file; or 25 | 26 | * (c) The contribution was provided directly to me by some other 27 | person who certified (a), (b) or (c) and I have not modified 28 | it. 29 | 30 | * (d) I understand and agree that this project and the contribution 31 | are public and that a record of the contribution (including all 32 | personal information I submit with it, including my sign-off) is 33 | maintained indefinitely and may be redistributed consistent with 34 | this project or the open source license(s) involved. 35 | 36 | ## Versioning 37 | 38 | This project uses 39 | [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). 40 | 41 | ## Git Workflow 42 | 43 | ### Below Version 1.0.0 44 | 45 | Up until, but not including, Version 1.0.0, pushes SHALL be made directly to 46 | the `master` branch. This is done for the expedience of development, and 47 | because development is typically done by a single person and because what 48 | comes before 1.0.0 is no longer supported anyway, so maintainability does 49 | not matter for pre-1.0.0 versions. Tagging is OPTIONAL. Commit messages are 50 | OPTIONAL. 51 | 52 | ### Version 1.0.0 and higher 53 | 54 | #### Version Branches 55 | 56 | Once Version 1.0.0 is published, pushes SHALL be made to version-specific 57 | branches. These version-specific branches SHALL have a name starting with a 58 | lowercase `v`, followed by the 59 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html) number. 60 | 61 | Commits made to the version-specific branches SHALL leave the software in a 62 | usable state; in other words, commits SHALL NOT be merged into 63 | version branches unless the software is believed to be usable after these 64 | commits are applied. 65 | 66 | #### Dev Branch 67 | 68 | A separate `dev` branch MAY be used for expedient development, then changes 69 | merged into the appropriate version branches as necessary. If this branch is 70 | not present, simply fork this repository, update your fork, then submit pull 71 | requests as appropriate. 72 | 73 | #### Master Branch 74 | 75 | The `master` branch SHALL be protected from pushed commits. No other requirements 76 | shall be imposed by this standard on the `master` branch. The `master` branch 77 | SHOULD not be expected to be the most up-to-date version. The `master` branch 78 | MAY be entirely unmaintained. If this is the case, the most up-to-date branch 79 | MUST be configured to be the default branch. 80 | 81 | #### Tags 82 | 83 | Git tags SHALL indicate semantic versions. For example, 84 | a commit tagged `v1.2.3` should be the last commit of all of the changes 85 | that make version 1.2.2 into 1.2.3. Release candidate tags SHOULD be used for 86 | the last commit that goes into a pull request. For instance, if you submit 87 | a pull request that you hope becomes version 1.2.4, and there have been no 88 | other pull requests to produce a potential version 1.2.4, you MAY tag your 89 | pull request's final commit as `v1.2.4-rc1`. 90 | -------------------------------------------------------------------------------- /test/x690/invalid/date.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.CERElement, 8 | asn1.DERElement, 9 | ].forEach((CodecElement) => { 10 | describe(`${CodecElement.constructor.name} UTCTime decoder`, () => { 11 | it("throws if a month greater than 12 is encountered", () => { 12 | const el = new CodecElement(); 13 | el.utf8String = "751308132656Z"; 14 | assert.throws(() => el.utcTime); 15 | }); 16 | 17 | it("throws if a day greater than 31 is encountered", () => { 18 | const el = new CodecElement(); 19 | el.utf8String = "751235132656Z"; 20 | assert.throws(() => el.utcTime); 21 | }); 22 | 23 | it("throws if a day greater than 30 is encountered on a 30-day month", () => { 24 | const el = new CodecElement(); 25 | el.utf8String = "751131132656Z"; 26 | assert.throws(() => el.utcTime); 27 | }); 28 | 29 | it("throws if a day greater than 29 is encountered on a February", () => { 30 | const el = new CodecElement(); 31 | el.utf8String = "750230132656Z"; 32 | assert.throws(() => el.utcTime); 33 | }); 34 | 35 | it("throws if an hour greater than 23 is encountered", () => { 36 | const el = new CodecElement(); 37 | el.utf8String = "751230272656Z"; 38 | assert.throws(() => el.utcTime); 39 | }); 40 | 41 | it("throws if a minute greater than 59 is encountered", () => { 42 | const el = new CodecElement(); 43 | el.utf8String = "751230219956Z"; 44 | assert.throws(() => el.utcTime); 45 | }); 46 | 47 | it("throws if a second greater than 59 is encountered", () => { 48 | const el = new CodecElement(); 49 | el.utf8String = "751230212699Z"; 50 | assert.throws(() => el.utcTime); 51 | }); 52 | }); 53 | 54 | describe(`${CodecElement.constructor.name} GeneralizedTime decoder`, () => { 55 | it("throws if a month greater than 12 is encountered", () => { 56 | const el = new CodecElement(); 57 | el.utf8String = "19751308132656Z"; 58 | assert.throws(() => el.generalizedTime); 59 | }); 60 | 61 | it("throws if a day greater than 31 is encountered", () => { 62 | const el = new CodecElement(); 63 | el.utf8String = "19751235132656Z"; 64 | assert.throws(() => el.generalizedTime); 65 | }); 66 | 67 | it("throws if a day greater than 30 is encountered on a 30-day month", () => { 68 | const el = new CodecElement(); 69 | el.utf8String = "19751131132656Z"; 70 | assert.throws(() => el.generalizedTime); 71 | }); 72 | 73 | it("throws if a day greater than 29 is encountered on a February", () => { 74 | const el = new CodecElement(); 75 | el.utf8String = "19750230132656Z"; 76 | assert.throws(() => el.generalizedTime); 77 | }); 78 | 79 | it("throws if an hour greater than 23 is encountered", () => { 80 | const el = new CodecElement(); 81 | el.utf8String = "19751230272656Z"; 82 | assert.throws(() => el.generalizedTime); 83 | }); 84 | 85 | it("throws if a minute greater than 59 is encountered", () => { 86 | const el = new CodecElement(); 87 | el.utf8String = "19751230219956Z"; 88 | assert.throws(() => el.generalizedTime); 89 | }); 90 | 91 | it("throws if a second greater than 59 is encountered", () => { 92 | const el = new CodecElement(); 93 | el.utf8String = "19751230212699Z"; 94 | assert.throws(() => el.generalizedTime); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /benchmark/oids.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../dist/index.mjs"; 2 | const CodecElement = asn1.DERElement; 3 | import { strict as assert } from "node:assert"; 4 | 5 | const sensitiveValues = [ 6 | 0, 7 | 1, 8 | 2, 9 | 3, 10 | 7, 11 | 8, 12 | 126, 13 | 127, 14 | 128, 15 | 254, 16 | 255, 17 | 256, 18 | 32766, 19 | 32767, 20 | 32768, 21 | 65534, 22 | 65535, 23 | 65536, 24 | // 2147483646, 25 | // 2147483647, 26 | ]; 27 | 28 | console.time("oids"); 29 | let i = 0; 30 | while (i < 100) { 31 | i++; 32 | const el = new CodecElement(); 33 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ 1, 3, 4, 6, 65, 90 ]); 34 | const oid = el.objectIdentifier; 35 | assert(oid.isEqualTo(asn1.ObjectIdentifier.fromParts([ 1, 3, 4, 6, 65, 90 ]))); 36 | 37 | for (let x = 0; x < 2; x++) { 38 | for (let y = 0; y < 40; y++) { 39 | sensitiveValues.forEach((z) => { 40 | // console.log(x, y, z); 41 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z ]); 42 | assert((el.objectIdentifier).isEqualTo(asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z ]))); 43 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z, 0 ]); 44 | assert(el.objectIdentifier.isEqualTo(asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z, 0 ]))); 45 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z, 1 ]); 46 | assert(el.objectIdentifier.isEqualTo(asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z, 1 ]))); 47 | }); 48 | } 49 | } 50 | 51 | for (let y = 0; y < 175; y++) { 52 | sensitiveValues.forEach((z) => { 53 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z ]); 54 | assert((el.objectIdentifier).isEqualTo(asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z ]))); 55 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z, 0 ]); 56 | assert(el.objectIdentifier.isEqualTo(asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z, 0 ]))); 57 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z, 1 ]); 58 | assert(el.objectIdentifier.isEqualTo(asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z, 1 ]))); 59 | }); 60 | } 61 | } 62 | console.timeEnd("oids"); 63 | 64 | console.time("oids_to_string"); 65 | i = 0; 66 | while (i < 100) { 67 | i++; 68 | const el = new CodecElement(); 69 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ 1, 3, 4, 6, 65, 90 ]); 70 | el.objectIdentifier.toString(); 71 | 72 | for (let x = 0; x < 2; x++) { 73 | for (let y = 0; y < 40; y++) { 74 | sensitiveValues.forEach((z) => { 75 | // console.log(x, y, z); 76 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z ]); 77 | asn1.ObjectIdentifier.fromString(el.objectIdentifier.toString()); 78 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z, 0 ]); 79 | asn1.ObjectIdentifier.fromString(el.objectIdentifier.toString()); 80 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ x, y, 6, 4, z, 1 ]); 81 | asn1.ObjectIdentifier.fromString(el.objectIdentifier.toString()); 82 | }); 83 | } 84 | } 85 | 86 | for (let y = 0; y < 175; y++) { 87 | sensitiveValues.forEach((z) => { 88 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z ]); 89 | asn1.ObjectIdentifier.fromString(el.objectIdentifier.toString()); 90 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z, 0 ]); 91 | asn1.ObjectIdentifier.fromString(el.objectIdentifier.toString()); 92 | el.objectIdentifier = asn1.ObjectIdentifier.fromParts([ 2, y, 6, 4, z, 1 ]); 93 | asn1.ObjectIdentifier.fromString(el.objectIdentifier.toString()); 94 | }); 95 | } 96 | } 97 | console.timeEnd("oids_to_string"); 98 | -------------------------------------------------------------------------------- /source/codecs/ber/decoders/decodeDuration.mts: -------------------------------------------------------------------------------- 1 | import type { DURATION, INTEGER, OPTIONAL } from "../../../macros.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import * as errors from "../../../errors.mjs"; 4 | import { DURATION_EQUIVALENT } from "../../../types/index.mjs"; 5 | import { durationRegex } from "../../../values.mjs"; 6 | 7 | export default 8 | function decodeDuration (bytes: Uint8Array): DURATION { 9 | const str: string = convertBytesToText(bytes).replace(/,/g, "."); 10 | 11 | // If there is a W, it is a weeks designation. 12 | if (str.indexOf("W") === (str.length - 1)) { 13 | const weekString: string = str.slice(0, -1); 14 | const indexOfDecimalSeparator: number = weekString.indexOf("."); 15 | const weeks: number = indexOfDecimalSeparator !== -1 16 | ? parseInt(weekString.slice(0, indexOfDecimalSeparator), 10) 17 | : parseInt(weekString, 10); 18 | if (Number.isNaN(weeks)) { 19 | throw new errors.ASN1Error(`Could not decode a real number of weeks from DURATION ${str}.`); 20 | } 21 | let fractional_part: { number_of_digits: number; fractional_value: number } | undefined = undefined; 22 | if (indexOfDecimalSeparator !== -1) { 23 | const fractionString: string = weekString.slice(indexOfDecimalSeparator + 1); 24 | const fractionValue: number = parseInt(fractionString, 10); 25 | if (Number.isNaN(fractionValue)) { 26 | throw new errors.ASN1Error(`Could not decode a fractional number of weeks from DURATION ${str}.`); 27 | } 28 | fractional_part = { 29 | number_of_digits: fractionString.length, 30 | fractional_value: fractionValue, 31 | }; 32 | } 33 | return new DURATION_EQUIVALENT( 34 | undefined, 35 | undefined, 36 | weeks, 37 | undefined, 38 | undefined, 39 | undefined, 40 | undefined, 41 | fractional_part, 42 | ); 43 | } 44 | 45 | /** 46 | * I don't like the idea of using regular expressions for this, but it was 47 | * the best solution that ensures correct ordering of DURATION components. 48 | */ 49 | const match: RegExpExecArray | null = durationRegex.exec(str); 50 | if (!match) { 51 | throw new errors.ASN1Error(`Malformed DURATION ${str}.`); 52 | } 53 | 54 | let fractional_part: OPTIONAL<{ 55 | number_of_digits: INTEGER; 56 | fractional_value: INTEGER; 57 | }> = undefined; 58 | 59 | // This sets the fractional component and checks that it is not double-set. 60 | [ 61 | match[1], 62 | match[2], 63 | match[3], 64 | match[4], 65 | match[5], 66 | match[6], 67 | ].forEach((component: string): void => { 68 | if (!component) { 69 | return; 70 | } 71 | if (fractional_part) { 72 | throw new errors.ASN1Error( 73 | `No smaller components permitted after fractional component in DURATION ${str}.`, 74 | ); 75 | } 76 | const indexOfFractionalSeparator: number = component.indexOf("."); 77 | if (indexOfFractionalSeparator !== -1) { // It is a real number. 78 | fractional_part = { 79 | number_of_digits: (component.length - 1 - indexOfFractionalSeparator), 80 | fractional_value: Number.parseInt(component.slice(indexOfFractionalSeparator + 1), 10), 81 | }; 82 | } 83 | }); 84 | 85 | const years: OPTIONAL = match[1] ? Number.parseInt(match[1], 10) : undefined; 86 | const months: OPTIONAL = match[2] ? Number.parseInt(match[2], 10) : undefined; 87 | const days: OPTIONAL = match[3] ? Number.parseInt(match[3], 10) : undefined; 88 | const hours: OPTIONAL = match[4] ? Number.parseInt(match[4], 10) : undefined; 89 | const minutes: OPTIONAL = match[5] ? Number.parseInt(match[5], 10) : undefined; 90 | const seconds: OPTIONAL = match[6] ? Number.parseInt(match[6], 10) : undefined; 91 | return new DURATION_EQUIVALENT(years, months, undefined, days, hours, minutes, seconds, fractional_part); 92 | } 93 | -------------------------------------------------------------------------------- /source/macros.mts: -------------------------------------------------------------------------------- 1 | import type ObjectIdentifier from "./types/ObjectIdentifier.mjs"; 2 | import type EmbeddedPDV from "./types/EmbeddedPDV.mjs"; 3 | import type External from "./types/External.mjs"; 4 | import type DURATION_EQUIVALENT from "./types/time/DURATION-EQUIVALENT.mjs"; 5 | 6 | export type COMPONENTS_OF = T; 7 | export type OPTIONAL = T | undefined; 8 | export type BOOLEAN = boolean; 9 | export type INTEGER = number | bigint; 10 | export type BIT_STRING = Uint8ClampedArray; 11 | export type OCTET_STRING = Uint8Array; 12 | export type NULL = null; 13 | export type OBJECT_IDENTIFIER = ObjectIdentifier; 14 | export type ObjectDescriptor = string; 15 | export type EXTERNAL = External; 16 | export type REAL = number; 17 | export type INSTANCE_OF = External; 18 | export type ENUMERATED = number; 19 | export type EMBEDDED_PDV = EmbeddedPDV; 20 | export type UTF8String = string; 21 | export type RELATIVE_OID = number[]; 22 | export type SEQUENCE = T[]; 23 | export type SEQUENCE_OF = T[]; 24 | export type SET = T[]; 25 | export type SET_OF = T[]; 26 | export type GraphicString = string; 27 | export type NumericString = string; 28 | export type VisibleString = string; 29 | export type PrintableString = string; 30 | export type ISO646String = string; 31 | export type TeletexString = Uint8Array; 32 | export type GeneralString = string; 33 | export type T61String = Uint8Array; 34 | export type UniversalString = string; 35 | export type VideotexString = Uint8Array; 36 | export type BMPString = string; 37 | export type IA5String = string; 38 | // export type CharacterString = CharacterString; 39 | export { default as CharacterString } from "./types/CharacterString.mjs"; 40 | export type UTCTime = Date; 41 | export type GeneralizedTime = Date; 42 | 43 | /** 44 | * It might be tempting to represent these as dates, and that might be fine 45 | * when it comes to mutating, but when accessing, the wide variety of 46 | * abstract value representations would entail _months_ of additional 47 | * programming to decode. 48 | */ 49 | 50 | /** 51 | * A string is used to represent the Time type, because it can take on so 52 | * many different forms. 53 | */ 54 | export type TIME = string; 55 | 56 | /** 57 | * `DATE ::= [UNIVERSAL 31] IMPLICIT TIME (SETTINGS "Basic=Date Date=YMD Year=Basic")` 58 | * 59 | * This looks like `YYYY-MM-DD`, where `YYYY` is 1582 to 9999. 60 | * 61 | * The time will be set to local 00:00:00. 62 | */ 63 | export type DATE = Date; 64 | 65 | /** 66 | * `TIME-OF-DAY ::= [UNIVERSAL 32] IMPLICIT TIME (SETTINGS "Basic=Time Time=HMS Local-or-UTC=L")` 67 | * 68 | * This looks like `hh:mm:ss`. 69 | * 70 | * The date will be set to today's local date. 71 | */ 72 | export type TIME_OF_DAY = Date; 73 | 74 | /** 75 | * `DATE-TIME ::= [UNIVERSAL 33] IMPLICIT TIME (SETTINGS "Basic=Date-Time Date=YMD Year=Basic Time=HMS Local-or-UTC=L")` 76 | * 77 | * `YYYY-MM-DDThh:mm:ss` 78 | */ 79 | export type DATE_TIME = Date; 80 | 81 | /** 82 | * `DURATION ::= [UNIVERSAL 34] IMPLICIT TIME (SETTINGS "Basic=Interval Interval-type=D")` 83 | * 84 | * This must begin with a P, then a sequence of numbers (floating-point 85 | * tolerated), each of which is followed by a letter, indicating the unit. 86 | * The only exception to this is that an hours-minutes-seconds designation 87 | * shall be preceded by a T. 88 | * The syntax is really more complicated than this, but that is a good summary. 89 | * 90 | * This library will use a `number` to represent the value, with 1 in this 91 | * scale representing one second, and with the fractional component 92 | * representing fractions of a second. 93 | */ 94 | export type DURATION = DURATION_EQUIVALENT; 95 | 96 | export type OID_IRI = string; 97 | export type RELATIVE_OID_IRI = string; 98 | 99 | export const TRUE = true; 100 | export const FALSE = false; 101 | export const TRUE_BIT = 1; 102 | export const FALSE_BIT = 0; 103 | export const PLUS_INFINITY = Infinity; 104 | export const MINUS_INFINITY = -Infinity; 105 | export const NOT_A_NUMBER = NaN; 106 | 107 | export const itu_t = 0; 108 | export const ccitt = 0; 109 | export const itu_r = 0; 110 | export const iso = 1; 111 | export const joint_iso_itu_t = 2; 112 | export const joint_iso_ccitt = 2; 113 | -------------------------------------------------------------------------------- /source/codecs/der/decoders/decodeDuration.mts: -------------------------------------------------------------------------------- 1 | import type { DURATION, INTEGER, OPTIONAL } from "../../../macros.mjs"; 2 | import convertBytesToText from "../../../utils/convertBytesToText.mjs"; 3 | import * as errors from "../../../errors.mjs"; 4 | import { DURATION_EQUIVALENT } from "../../../types/index.mjs"; 5 | import { durationRegex } from "../../../values.mjs"; 6 | 7 | export default 8 | function decodeDuration (bytes: Uint8Array): DURATION { 9 | const str: string = convertBytesToText(bytes); 10 | if (str.indexOf(",") !== -1) { 11 | throw new errors.ASN1Error( 12 | "Comma prohibited in DURATION when using the Distinguished or Canonical Encoding Rules.", 13 | ); 14 | } 15 | 16 | // If there is a W, it is a weeks designation. 17 | if (str.indexOf("W") === (str.length - 1)) { 18 | const weekString: string = str.slice(0, -1); 19 | const indexOfDecimalSeparator: number = weekString.indexOf("."); 20 | const weeks: number = indexOfDecimalSeparator !== -1 21 | ? parseInt(weekString.slice(0, indexOfDecimalSeparator), 10) 22 | : parseInt(weekString, 10); 23 | if (Number.isNaN(weeks)) { 24 | throw new errors.ASN1Error(`Could not decode a real number of weeks from DURATION ${str}.`); 25 | } 26 | let fractional_part: { number_of_digits: number; fractional_value: number } | undefined = undefined; 27 | if (indexOfDecimalSeparator !== -1) { 28 | const fractionString: string = weekString.slice(indexOfDecimalSeparator + 1); 29 | const fractionValue: number = parseInt(fractionString, 10); 30 | if (Number.isNaN(fractionValue)) { 31 | throw new errors.ASN1Error(`Could not decode a fractional number of weeks from DURATION ${str}.`); 32 | } 33 | fractional_part = { 34 | number_of_digits: fractionString.length, 35 | fractional_value: fractionValue, 36 | }; 37 | } 38 | return new DURATION_EQUIVALENT( 39 | undefined, 40 | undefined, 41 | weeks, 42 | undefined, 43 | undefined, 44 | undefined, 45 | undefined, 46 | fractional_part, 47 | ); 48 | } 49 | 50 | /** 51 | * I don't like the idea of using regular expressions for this, but it was 52 | * the best solution that ensures correct ordering of DURATION components. 53 | */ 54 | const match: RegExpExecArray | null = durationRegex.exec(str); 55 | if (!match) { 56 | throw new errors.ASN1Error(`Malformed DURATION ${str}.`); 57 | } 58 | 59 | let fractional_part: OPTIONAL<{ 60 | number_of_digits: INTEGER; 61 | fractional_value: INTEGER; 62 | }> = undefined; 63 | 64 | // This sets the fractional component and checks that it is not double-set. 65 | [ 66 | match[1], 67 | match[2], 68 | match[3], 69 | match[4], 70 | match[5], 71 | match[6], 72 | ].forEach((component: string): void => { 73 | if (!component) { 74 | return; 75 | } 76 | if (fractional_part) { 77 | throw new errors.ASN1Error( 78 | `No smaller components permitted after fractional component in DURATION ${str}.`, 79 | ); 80 | } 81 | const indexOfFractionalSeparator: number = component.indexOf("."); 82 | if (indexOfFractionalSeparator !== -1) { // It is a real number. 83 | fractional_part = { 84 | number_of_digits: (component.length - 1 - indexOfFractionalSeparator), 85 | fractional_value: Number.parseInt(component.slice(indexOfFractionalSeparator + 1), 10), 86 | }; 87 | } 88 | }); 89 | 90 | const years: OPTIONAL = match[1] ? Number.parseInt(match[1], 10) : undefined; 91 | const months: OPTIONAL = match[2] ? Number.parseInt(match[2], 10) : undefined; 92 | const days: OPTIONAL = match[3] ? Number.parseInt(match[3], 10) : undefined; 93 | const hours: OPTIONAL = match[4] ? Number.parseInt(match[4], 10) : undefined; 94 | const minutes: OPTIONAL = match[5] ? Number.parseInt(match[5], 10) : undefined; 95 | const seconds: OPTIONAL = match[6] ? Number.parseInt(match[6], 10) : undefined; 96 | return new DURATION_EQUIVALENT(years, months, undefined, days, hours, minutes, seconds, fractional_part); 97 | } 98 | -------------------------------------------------------------------------------- /source/types/EmbeddedPDV.mts: -------------------------------------------------------------------------------- 1 | import type ASN1Element from "../asn1.mjs"; 2 | 3 | /** 4 | * An `EmbeddedPDV` is a constructed data type, defined in 5 | * the [International Telecommunications Union](https://www.itu.int)'s 6 | * [X.680](https://www.itu.int/rec/T-REC-X.680/en). 7 | * 8 | * The specification defines `EmbeddedPDV` as: 9 | * 10 | * ```asn1 11 | * EmbeddedPDV ::= [UNIVERSAL 11] IMPLICIT SEQUENCE { 12 | * identification CHOICE { 13 | * syntaxes SEQUENCE { 14 | * abstract OBJECT IDENTIFIER, 15 | * transfer OBJECT IDENTIFIER }, 16 | * syntax OBJECT IDENTIFIER, 17 | * presentation-context-id INTEGER, 18 | * context-negotiation SEQUENCE { 19 | * presentation-context-id INTEGER, 20 | * transfer-syntax OBJECT IDENTIFIER }, 21 | * transfer-syntax OBJECT IDENTIFIER, 22 | * fixed NULL }, 23 | * data-value-descriptor ObjectDescriptor OPTIONAL, 24 | * data-value OCTET STRING } 25 | * (WITH COMPONENTS { ... , data-value-descriptor ABSENT }) 26 | * ``` 27 | * 28 | * This assumes `AUTOMATIC TAGS`, so all of the `identification` 29 | * choices will be `CONTEXT-SPECIFIC` and numbered from 0 to 5. 30 | * 31 | * The following additional constraints are applied to the abstract syntax 32 | * when using Canonical Encoding Rules or Distinguished Encoding Rules, 33 | * which are also defined in the 34 | * [International Telecommunications Union](https://www.itu.int/en/pages/default.aspx)'s 35 | * [X.690 - ASN.1 encoding rules](http://www.itu.int/rec/T-REC-X.690/en): 36 | * 37 | * ```asn1 38 | * EmbeddedPDV ( WITH COMPONENTS { 39 | * ... , 40 | * identification ( WITH COMPONENTS { 41 | * ... , 42 | * presentation-context-id ABSENT, 43 | * context-negotiation ABSENT } ) } ) 44 | * ``` 45 | * 46 | * The stated purpose of the constraints shown above is to restrict the use of 47 | * the `presentation-context-id`, either by itself or within the 48 | * context-negotiation, which makes the following the effective abstract 49 | * syntax of `EmbeddedPDV` when using Canonical Encoding Rules or 50 | * Distinguished Encoding Rules: 51 | * 52 | * ```asn1 53 | * EmbeddedPDV ::= [UNIVERSAL 11] IMPLICIT SEQUENCE { 54 | * identification CHOICE { 55 | * syntaxes SEQUENCE { 56 | * abstract OBJECT IDENTIFIER, 57 | * transfer OBJECT IDENTIFIER }, 58 | * syntax OBJECT IDENTIFIER, 59 | * presentation-context-id INTEGER, 60 | * context-negotiation SEQUENCE { 61 | * presentation-context-id INTEGER, 62 | * transfer-syntax OBJECT IDENTIFIER }, 63 | * transfer-syntax OBJECT IDENTIFIER, 64 | * fixed NULL }, 65 | * data-value-descriptor ObjectDescriptor OPTIONAL, 66 | * data-value OCTET STRING } 67 | * ( WITH COMPONENTS { 68 | * ... , 69 | * identification ( WITH COMPONENTS { 70 | * ... , 71 | * presentation-context-id ABSENT, 72 | * context-negotiation ABSENT } ) } ) 73 | * ``` 74 | * 75 | * With the constraints applied, the abstract syntax for `EmbeddedPDV`s encoded 76 | * using Canonical Encoding Rules or Distinguished Encoding Rules becomes: 77 | * 78 | * ```asn1 79 | * EmbeddedPDV ::= [UNIVERSAL 11] IMPLICIT SEQUENCE { 80 | * identification CHOICE { 81 | * syntaxes SEQUENCE { 82 | * abstract OBJECT IDENTIFIER, 83 | * transfer OBJECT IDENTIFIER }, 84 | * syntax OBJECT IDENTIFIER, 85 | * transfer-syntax OBJECT IDENTIFIER, 86 | * fixed NULL }, 87 | * data-value-descriptor ObjectDescriptor OPTIONAL, 88 | * data-value OCTET STRING } 89 | * ``` 90 | */ 91 | export default 92 | class EmbeddedPDV { 93 | constructor ( 94 | readonly identification: ASN1Element, 95 | readonly dataValue: Uint8Array, 96 | ) {} 97 | 98 | public toString (): string { 99 | return ( 100 | "EMBEDDED PDV { " 101 | + `identification ${this.identification.toString()} ` 102 | + `dataValue ${Array.from(this.dataValue).map((byte) => byte.toString(16)).join("")} ` 103 | + "}" 104 | ); 105 | } 106 | 107 | public toJSON (): unknown { 108 | return { 109 | identification: this.identification.toJSON(), 110 | dataValue: Array.from(this.dataValue).map((byte) => byte.toString(16)).join(""), 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/asn1/constraint.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | [ 6 | asn1.BERElement, 7 | asn1.DERElement, 8 | ].forEach((CodecElement) => { 9 | describe(`${CodecElement.constructor.name} size-constraint validator`, () => { 10 | it("validates a BIT STRING", () => { 11 | const el = new CodecElement(); 12 | el.bitString = [ true, false, true, false, true ]; 13 | assert.doesNotThrow(() => { 14 | el.sizeConstrainedBitString(1, 5); 15 | }); 16 | assert.throws(() => { 17 | el.sizeConstrainedBitString(0, 4); 18 | }); 19 | assert.throws(() => { 20 | el.sizeConstrainedBitString(6, 8); 21 | }); 22 | }); 23 | 24 | it("validates an OCTET STRING", () => { 25 | const el = new CodecElement(); 26 | el.octetString = new Uint8Array([ 1, 2, 3, 4, 5 ]); 27 | assert.doesNotThrow(() => { 28 | el.sizeConstrainedOctetString(1, 5); 29 | }); 30 | assert.throws(() => { 31 | el.sizeConstrainedOctetString(0, 4); 32 | }); 33 | assert.throws(() => { 34 | el.sizeConstrainedOctetString(6, 8); 35 | }); 36 | }); 37 | 38 | // I am not going to test all of the string types. 39 | it("validates an ObjectDescriptor", () => { 40 | const el = new CodecElement(); 41 | el.objectDescriptor = "bloop"; 42 | assert.doesNotThrow(() => { 43 | el.sizeConstrainedObjectDescriptor(1, 5); 44 | }); 45 | assert.throws(() => { 46 | el.sizeConstrainedObjectDescriptor(0, 4); 47 | }); 48 | assert.throws(() => { 49 | el.sizeConstrainedObjectDescriptor(6, 8); 50 | }); 51 | }); 52 | 53 | it("validates a SEQUENCE OF", () => { 54 | const el = new CodecElement(); 55 | el.sequence = [ 56 | new CodecElement(), 57 | new CodecElement(), 58 | new CodecElement(), 59 | new CodecElement(), 60 | new CodecElement(), 61 | ]; 62 | assert.doesNotThrow(() => { 63 | el.sizeConstrainedSequenceOf(1, 5); 64 | }); 65 | assert.throws(() => { 66 | el.sizeConstrainedSequenceOf(0, 4); 67 | }); 68 | assert.throws(() => { 69 | el.sizeConstrainedSequenceOf(6, 8); 70 | }); 71 | }); 72 | 73 | it("validates a SET OF", () => { 74 | const el = new CodecElement(); 75 | el.setOf = [ 76 | new CodecElement(), 77 | new CodecElement(), 78 | new CodecElement(), 79 | new CodecElement(), 80 | new CodecElement(), 81 | ]; 82 | assert.doesNotThrow(() => { 83 | el.sizeConstrainedSetOf(1, 5); 84 | }); 85 | assert.throws(() => { 86 | el.sizeConstrainedSetOf(0, 4); 87 | }); 88 | assert.throws(() => { 89 | el.sizeConstrainedSetOf(6, 8); 90 | }); 91 | }); 92 | }); 93 | 94 | describe(`${CodecElement.constructor.name} range-constraint validator`, () => { 95 | it("validates an INTEGER", () => { 96 | const el = new CodecElement(); 97 | el.integer = 5; 98 | assert.doesNotThrow(() => { 99 | el.rangeConstrainedInteger(1, 5); 100 | }); 101 | assert.throws(() => { 102 | el.rangeConstrainedInteger(0, 4); 103 | }); 104 | assert.throws(() => { 105 | el.rangeConstrainedInteger(6, 8); 106 | }); 107 | }); 108 | 109 | it("validates a REAL", () => { 110 | const el = new CodecElement(); 111 | el.real = 5.5; 112 | assert.doesNotThrow(() => { 113 | el.rangeConstrainedReal(1, 5.5); 114 | }); 115 | assert.throws(() => { 116 | el.rangeConstrainedReal(0, 4); 117 | }); 118 | assert.throws(() => { 119 | el.rangeConstrainedReal(6, 8); 120 | }); 121 | }); 122 | }); 123 | }); 124 | 125 | -------------------------------------------------------------------------------- /source/types/time/DURATION-EQUIVALENT.mts: -------------------------------------------------------------------------------- 1 | import type { 2 | INTEGER, 3 | OPTIONAL, 4 | } from "../../macros.mjs"; 5 | import * as errors from "../../errors.mjs"; 6 | import datetimeComponentValidator from "../../validators/datetimeComponentValidator.mjs"; 7 | 8 | /** 9 | * Note that this is equivalent to `DURATION-INTERVAL-ENCODING` defined in 10 | * ITU X.696. 11 | * 12 | * `DURATION-EQUIVALENT ::= SEQUENCE { 13 | * years INTEGER (0..MAX) OPTIONAL, 14 | * months INTEGER (0..MAX) OPTIONAL, 15 | * weeks INTEGER (0..MAX) OPTIONAL, 16 | * days INTEGER (0..MAX) OPTIONAL, 17 | * hours INTEGER (0..MAX) OPTIONAL, 18 | * minutes INTEGER (0..MAX) OPTIONAL, 19 | * seconds INTEGER (0..MAX) OPTIONAL, 20 | * fractional-part SEQUENCE { 21 | * number-of-digits INTEGER(1..MAX), 22 | * fractional-value INTEGER(0..MAX) } OPTIONAL 23 | * }` 24 | */ 25 | export default 26 | class DURATION_EQUIVALENT { 27 | constructor ( 28 | readonly years: OPTIONAL, 29 | readonly months: OPTIONAL, 30 | readonly weeks: OPTIONAL, 31 | readonly days: OPTIONAL, 32 | readonly hours: OPTIONAL, 33 | readonly minutes: OPTIONAL, 34 | readonly seconds: OPTIONAL, 35 | readonly fractional_part: OPTIONAL<{ 36 | number_of_digits: INTEGER; 37 | fractional_value: INTEGER; 38 | }>, 39 | ) { 40 | if ( 41 | typeof weeks !== "undefined" 42 | && (years || months || days || hours || minutes || seconds) 43 | ) { 44 | throw new errors.ASN1Error( 45 | "DURATION-EQUIVALENT may not combine week components and date-time components.", 46 | ); 47 | } 48 | if (years) { 49 | datetimeComponentValidator("year", 0, Number.MAX_SAFE_INTEGER)("DURATION-EQUIVALENT", years); 50 | } 51 | if (months) { 52 | datetimeComponentValidator("month", 0, Number.MAX_SAFE_INTEGER)("DURATION-EQUIVALENT", months); 53 | } 54 | if (weeks) { 55 | datetimeComponentValidator("week", 0, Number.MAX_SAFE_INTEGER)("DURATION-EQUIVALENT", weeks); 56 | } 57 | if (days) { 58 | datetimeComponentValidator("day", 0, Number.MAX_SAFE_INTEGER)("DURATION-EQUIVALENT", days); 59 | } 60 | if (hours) { 61 | datetimeComponentValidator("hour", 0, Number.MAX_SAFE_INTEGER)("DURATION-EQUIVALENT", hours); 62 | } 63 | if (minutes) { 64 | datetimeComponentValidator("minute", 0, Number.MAX_SAFE_INTEGER)("DURATION-EQUIVALENT", minutes); 65 | } 66 | if (seconds) { 67 | datetimeComponentValidator("second", 0, Number.MAX_SAFE_INTEGER)("DURATION-EQUIVALENT", seconds); 68 | } 69 | if (fractional_part && !Number.isSafeInteger(fractional_part.fractional_value)) { 70 | throw new errors.ASN1Error("Malformed DURATION-EQUIVALENT fractional part."); 71 | } 72 | } 73 | 74 | public toString (): string { 75 | let ret: string = "DURATION { "; 76 | if (this.years !== undefined) { 77 | ret += `years ${this.years}`; 78 | } 79 | if (this.months !== undefined) { 80 | ret += `months ${this.months}`; 81 | } 82 | if (this.weeks !== undefined) { 83 | ret += `weeks ${this.weeks}`; 84 | } 85 | if (this.days !== undefined) { 86 | ret += `days ${this.days}`; 87 | } 88 | if (this.hours !== undefined) { 89 | ret += `hours ${this.hours}`; 90 | } 91 | if (this.minutes !== undefined) { 92 | ret += `minutes ${this.minutes}`; 93 | } 94 | if (this.seconds !== undefined) { 95 | ret += `seconds ${this.seconds}`; 96 | } 97 | ret += "}"; 98 | return ret; 99 | } 100 | 101 | public toJSON (): unknown { 102 | return { 103 | years: this.years, 104 | months: this.months, 105 | weeks: this.weeks, 106 | days: this.days, 107 | hours: this.hours, 108 | minutes: this.minutes, 109 | seconds: this.seconds, 110 | fractional_part: this.fractional_part 111 | ? { 112 | number_of_digits: this.fractional_part.number_of_digits, 113 | fractional_value: this.fractional_part.fractional_value, 114 | } 115 | : undefined, 116 | }; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/x690/real.test.mjs: -------------------------------------------------------------------------------- 1 | import * as asn1 from "../../dist/index.mjs"; 2 | import { describe, it } from "node:test"; 3 | import { strict as assert } from "node:assert"; 4 | 5 | // Thanks, ChatGPT 4o. 6 | function areFloatsEqual(a, b) { 7 | // return Math.abs(a - b) < epsilon; 8 | return a.toFixed(8) === b.toFixed(8); 9 | } 10 | 11 | [ 12 | asn1.BERElement, 13 | asn1.CERElement, 14 | asn1.DERElement, 15 | ].forEach((CodecElement) => { 16 | describe(`${CodecElement.constructor.name} Base-10 REAL decoder`, () => { 17 | // These examples are taken directly from ISO 6093 18 | it("decodes all valid NR1 expressions correctly", () => { 19 | const regex = asn1.nr1Regex; 20 | 21 | // Unsigned NR1 22 | assert.equal(regex.test("0004902"), true); 23 | assert.equal(regex.test(" 04902"), true); 24 | assert.equal(regex.test(" 4902"), true); 25 | assert.equal(regex.test("0001234"), true); 26 | assert.equal(regex.test(" 1234"), true); 27 | assert.equal(regex.test("0000000"), true); 28 | assert.equal(regex.test(" 0"), true); 29 | assert.equal(regex.test("1234567"), true); 30 | 31 | // Signed NR1 32 | assert.equal(regex.test("+004902"), true); 33 | assert.equal(regex.test(" +04902"), true); 34 | assert.equal(regex.test(" +4902"), true); 35 | assert.equal(regex.test(" 4902"), true); 36 | assert.equal(regex.test("+001234"), true); 37 | assert.equal(regex.test(" +1234"), true); 38 | assert.equal(regex.test(" 1234"), true); 39 | assert.equal(regex.test("-56780"), true); 40 | assert.equal(regex.test(" -56780"), true); 41 | assert.equal(regex.test("+000000"), true); 42 | assert.equal(regex.test(" +0"), true); 43 | assert.equal(regex.test(" 0"), true); 44 | }); 45 | 46 | // These examples are taken directly from ISO 6093 47 | it("decodes all valid NR2 expressions correctly", () => { 48 | const regex = asn1.nr2Regex; 49 | 50 | // Unsigned NR2 51 | assert.equal(regex.test("1327.000"), true); 52 | assert.equal(regex.test("0001327."), true); 53 | assert.equal(regex.test(" 1327."), true); 54 | assert.equal(regex.test("00123,45"), true); 55 | assert.equal(regex.test(" 123,45"), true); 56 | assert.equal(regex.test(" 1237,0"), true); 57 | assert.equal(regex.test("00.00001"), true); 58 | assert.equal(regex.test("1234,567"), true); 59 | assert.equal(regex.test("000,0000"), true); 60 | assert.equal(regex.test(" 0,0"), true); 61 | 62 | // Signed NR2 63 | assert.equal(regex.test("+1327.00"), true); 64 | assert.equal(regex.test(" +1327."), true); 65 | assert.equal(regex.test(" 1327."), true); 66 | assert.equal(regex.test("0+123,45"), true); 67 | assert.equal(regex.test(" 123,45"), true); 68 | assert.equal(regex.test(" +1237,0"), true); 69 | assert.equal(regex.test(" 1237,0"), true); 70 | assert.equal(regex.test("+0.00001"), true); 71 | assert.equal(regex.test("-5,67800"), true); 72 | assert.equal(regex.test("-05,6780"), true); 73 | assert.equal(regex.test("1234,567"), true); 74 | assert.equal(regex.test("+0,00000"), true); 75 | assert.equal(regex.test(" +0,0"), true); 76 | assert.equal(regex.test(" 0,0"), true); 77 | assert.equal(regex.test(" 0,"), true); 78 | }); 79 | 80 | // These examples are taken directly from ISO 6093 81 | it("decodes all valid NR3 expressions correctly", () => { 82 | const regex = asn1.nr3Regex; 83 | 84 | // Signed NR3 85 | assert.equal(regex.test("+0,56E+4"), true); 86 | assert.equal(regex.test("+5.6e+03"), true); 87 | assert.equal(regex.test("+0,3E-04"), true); 88 | assert.equal(regex.test(" 0,3e-04"), true); 89 | assert.equal(regex.test("-2,8E+00"), true); 90 | assert.equal(regex.test("+0,0E+00"), true); 91 | assert.equal(regex.test(" 0.e+0"), true); 92 | }); 93 | }); 94 | 95 | 96 | describe(`${CodecElement.constructor.name} Base-2 REAL decoder`, () => { 97 | it("decodes all valid Base-2 REALs correctly", () => { 98 | const el = new CodecElement(); 99 | el.value = new Uint8Array([ 0x80, 0xFB, 0x05 ]); 100 | assert(areFloatsEqual(el.real, 0.15625)); 101 | }); 102 | }); 103 | }); 104 | --------------------------------------------------------------------------------