/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { DER } from "resource://gre/modules/psm/DER.sys.mjs"; const ERROR_UNSUPPORTED_ASN1 = "unsupported asn.1"; const ERROR_TIME_NOT_VALID = "Time not valid"; const ERROR_LIBRARY_FAILURE = "library failure"; const X509v3 = 2; /** * Helper function to read a NULL tag from the given DER. * * @param {DER} der a DER object to read a NULL from * @returns {null} an object representing an ASN.1 NULL */ function readNULL(der) { return new NULL(der.readTagAndGetContents(DER.NULL)); } /** * Class representing an ASN.1 NULL. When encoded as DER, the only valid value * is 05 00, and thus the contents should always be an empty array. */ class NULL { /** * @param {number[]} bytes the contents of the NULL tag (should be empty) */ constructor(bytes) { // Lint TODO: bytes should be an empty array this._contents = bytes; } } /** * Helper function to read an OBJECT IDENTIFIER from the given DER. * * @param {DER} der the DER to read an OBJECT IDENTIFIER from * @returns {OID} the value of the OBJECT IDENTIFIER */ function readOID(der) { return new OID(der.readTagAndGetContents(DER.OBJECT_IDENTIFIER)); } /** Class representing an ASN.1 OBJECT IDENTIFIER */ class OID { /** * @param {number[]} bytes the encoded contents of the OBJECT IDENTIFIER * (not including the ASN.1 tag or length bytes) */ constructor(bytes) { this._values = []; // First octet has value 40 * value1 + value2 // Lint TODO: validate that value1 is one of {0, 1, 2} // Lint TODO: validate that value2 is in [0, 39] if value1 is 0 or 1 let value1 = Math.floor(bytes[0] / 40); let value2 = bytes[0] - 40 * value1; this._values.push(value1); this._values.push(value2); bytes.shift(); let accumulator = 0; // Lint TODO: prevent overflow here while (bytes.length) { let value = bytes.shift(); accumulator *= 128; if (value > 128) { accumulator += value - 128; } else { accumulator += value; this._values.push(accumulator); accumulator = 0; } } } } /** * Class that serves as an abstract base class for more specific classes that * represent datatypes from RFC 5280 and others. Given an array of bytes * representing the DER encoding of such types, this framework simplifies the * process of making a new DER object, attempting to parse the given bytes, and * catching and stashing thrown exceptions. Subclasses are to implement * parseOverride, which should read from this._der to fill out the structure's * values. */ class DecodedDER { constructor() { this._der = null; this._error = null; } /** * Returns the first exception encountered when decoding or null if none has * been encountered. * * @returns {Error} the first exception encountered when decoding or null */ get error() { return this._error; } /** * Does the actual work of parsing the data. To be overridden by subclasses. * If an implementation of parseOverride throws an exception, parse will catch * that exception and stash it in the error property. This enables parent * levels in a nested decoding hierarchy to continue to decode as much as * possible. */ parseOverride() { throw new Error(ERROR_LIBRARY_FAILURE); } /** * Public interface to be called to parse all data. Calls parseOverride inside * a try/catch block. If an exception is thrown, stashes the error, which can * be obtained via the error getter (above). * * @param {number[]} bytes encoded DER to be decoded */ parse(bytes) { this._der = new DER.DERDecoder(bytes); try { this.parseOverride(); } catch (e) { this._error = e; } } } /** * Helper function for reading the next SEQUENCE out of a DER and creating a new * DER out of the resulting bytes. * * @param {DER} der the underlying DER object * @returns {DER} the contents of the SEQUENCE */ function readSEQUENCEAndMakeDER(der) { return new DER.DERDecoder(der.readTagAndGetContents(DER.SEQUENCE)); } /** * Helper function for reading the next item identified by tag out of a DER and * creating a new DER out of the resulting bytes. * * @param {DER} der the underlying DER object * @param {number} tag the expected next tag in the DER * @returns {DER} the contents of the tag */ function readTagAndMakeDER(der, tag) { return new DER.DERDecoder(der.readTagAndGetContents(tag)); } // Certificate ::= SEQUENCE { // tbsCertificate TBSCertificate, // signatureAlgorithm AlgorithmIdentifier, // signatureValue BIT STRING } class Certificate extends DecodedDER { constructor() { super(); this._tbsCertificate = new TBSCertificate(); this._signatureAlgorithm = new AlgorithmIdentifier(); this._signatureValue = []; } get tbsCertificate() { return this._tbsCertificate; } get signatureAlgorithm() { return this._signatureAlgorithm; } get signatureValue() { return this._signatureValue; } parseOverride() { let contents = readSEQUENCEAndMakeDER(this._der); this._tbsCertificate.parse(contents.readTLV()); this._signatureAlgorithm.parse(contents.readTLV()); let signatureValue = contents.readBIT_STRING(); if (signatureValue.unusedBits != 0) { throw new Error(ERROR_UNSUPPORTED_ASN1); } this._signatureValue = signatureValue.contents; contents.assertAtEnd(); this._der.assertAtEnd(); } } // TBSCertificate ::= SEQUENCE { // version [0] EXPLICIT Version DEFAULT v1, // serialNumber CertificateSerialNumber, // signature AlgorithmIdentifier, // issuer Name, // validity Validity, // subject Name, // subjectPublicKeyInfo SubjectPublicKeyInfo, // issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, // -- If present, version MUST be v2 or v3 // subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, // -- If present, version MUST be v2 or v3 // extensions [3] EXPLICIT Extensions OPTIONAL // -- If present, version MUST be v3 // } class TBSCertificate extends DecodedDER { constructor() { super(); this._version = null; this._serialNumber = []; this._signature = new AlgorithmIdentifier(); this._issuer = new Name(); this._validity = new Validity(); this._subject = new Name(); this._subjectPublicKeyInfo = new SubjectPublicKeyInfo(); this._extensions = []; } get version() { return this._version; } get serialNumber() { return this._serialNumber; } get signature() { return this._signature; } get issuer() { return this._issuer; } get validity() { return this._validity; } get subject() { return this._subject; } get subjectPublicKeyInfo() { return this._subjectPublicKeyInfo; } get extensions() { return this._extensions; } parseOverride() { let contents = readSEQUENCEAndMakeDER(this._der); let versionTag = DER.CONTEXT_SPECIFIC | DER.CONSTRUCTED | 0; if (!contents.peekTag(versionTag)) { this._version = 1; } else { let versionContents = readTagAndMakeDER(contents, versionTag); let versionBytes = versionContents.readTagAndGetContents(DER.INTEGER); if (versionBytes.length == 1 && versionBytes[0] == X509v3) { this._version = 3; } else { // Lint TODO: warn about non-v3 certificates (this INTEGER could take up // multiple bytes, be negative, and so on). this._version = versionBytes; } versionContents.assertAtEnd(); } let serialNumberBytes = contents.readTagAndGetContents(DER.INTEGER); this._serialNumber = serialNumberBytes; this._signature.parse(contents.readTLV()); this._issuer.parse(contents.readTLV()); this._validity.parse(contents.readTLV()); this._subject.parse(contents.readTLV()); this._subjectPublicKeyInfo.parse(contents.readTLV()); // Lint TODO: warn about unsupported features let issuerUniqueIDTag = DER.CONTEXT_SPECIFIC | DER.CONSTRUCTED | 1; if (contents.peekTag(issuerUniqueIDTag)) { contents.readTagAndGetContents(issuerUniqueIDTag); } let subjectUniqueIDTag = DER.CONTEXT_SPECIFIC | DER.CONSTRUCTED | 2; if (contents.peekTag(subjectUniqueIDTag)) { contents.readTagAndGetContents(subjectUniqueIDTag); } let extensionsTag = DER.CONTEXT_SPECIFIC | DER.CONSTRUCTED | 3; if (contents.peekTag(extensionsTag)) { let extensionsSequence = readTagAndMakeDER(contents, extensionsTag); let extensionsContents = readSEQUENCEAndMakeDER(extensionsSequence); while (!extensionsContents.atEnd()) { // TODO: parse extensions this._extensions.push(extensionsContents.readTLV()); } extensionsContents.assertAtEnd(); extensionsSequence.assertAtEnd(); } contents.assertAtEnd(); this._der.assertAtEnd(); } } // AlgorithmIdentifier ::= SEQUENCE { // algorithm OBJECT IDENTIFIER, // parameters ANY DEFINED BY algorithm OPTIONAL } class AlgorithmIdentifier extends DecodedDER { constructor() { super(); this._algorithm = null; this._parameters = null; } get algorithm() { return this._algorithm; } get parameters() { return this._parameters; } parseOverride() { let contents = readSEQUENCEAndMakeDER(this._der); this._algorithm = readOID(contents); if (!contents.atEnd()) { if (contents.peekTag(DER.NULL)) { this._parameters = readNULL(contents); } else if (contents.peekTag(DER.OBJECT_IDENTIFIER)) { this._parameters = readOID(contents); } } contents.assertAtEnd(); this._der.assertAtEnd(); } } // Name ::= CHOICE { -- only one possibility for now -- // rdnSequence RDNSequence } // // RDNSequence ::= SEQUENCE OF RelativeDistinguishedName class Name extends DecodedDER { constructor() { super(); this._rdns = []; } get rdns() { return this._rdns; } parseOverride() { let contents = readSEQUENCEAndMakeDER(this._der); while (!contents.atEnd()) { let rdn = new RelativeDistinguishedName(); rdn.parse(contents.readTLV()); this._rdns.push(rdn); } contents.assertAtEnd(); this._der.assertAtEnd(); } } // RelativeDistinguishedName ::= // SET SIZE (1..MAX) OF AttributeTypeAndValue class RelativeDistinguishedName extends DecodedDER { constructor() { super(); this._avas = []; } get avas() { return this._avas; } parseOverride() { let contents = readTagAndMakeDER(this._der, DER.SET); // Lint TODO: enforce SET SIZE restrictions while (!contents.atEnd()) { let ava = new AttributeTypeAndValue(); ava.parse(contents.readTLV()); this._avas.push(ava); } contents.assertAtEnd(); this._der.assertAtEnd(); } } // AttributeTypeAndValue ::= SEQUENCE { // type AttributeType, // value AttributeValue } // // AttributeType ::= OBJECT IDENTIFIER // // AttributeValue ::= ANY -- DEFINED BY AttributeType class AttributeTypeAndValue extends DecodedDER { constructor() { super(); this._type = null; this._value = new DirectoryString(); } get type() { return this._type; } get value() { return this._value; } parseOverride() { let contents = readSEQUENCEAndMakeDER(this._der); this._type = readOID(contents); // We don't support universalString or bmpString. // IA5String is supported because it is valid if `type == id-emailaddress`. // Lint TODO: validate that the type of string is valid given `type`. this._value.parse( contents.readTLVChoice([ DER.UTF8String, DER.PrintableString, DER.TeletexString, DER.IA5String, ]) ); contents.assertAtEnd(); this._der.assertAtEnd(); } } // DirectoryString ::= CHOICE { // teletexString TeletexString (SIZE (1..MAX)), // printableString PrintableString (SIZE (1..MAX)), // universalString UniversalString (SIZE (1..MAX)), // utf8String UTF8String (SIZE (1..MAX)), // bmpString BMPString (SIZE (1..MAX)) } class DirectoryString extends DecodedDER { constructor() { super(); this._type = null; this._value = null; } get type() { return this._type; } get value() { return this._value; } parseOverride() { if (this._der.peekTag(DER.UTF8String)) { this._type = DER.UTF8String; } else if (this._der.peekTag(DER.PrintableString)) { this._type = DER.PrintableString; } else if (this._der.peekTag(DER.TeletexString)) { this._type = DER.TeletexString; } else if (this._der.peekTag(DER.IA5String)) { this._type = DER.IA5String; } // Lint TODO: validate that the contents are actually valid for the type this._value = this._der.readTagAndGetContents(this._type); this._der.assertAtEnd(); } } // Time ::= CHOICE { // utcTime UTCTime, // generalTime GeneralizedTime } class Time extends DecodedDER { constructor() { super(); this._type = null; this._time = null; } get time() { return this._time; } parseOverride() { if (this._der.peekTag(DER.UTCTime)) { this._type = DER.UTCTime; } else if (this._der.peekTag(DER.GeneralizedTime)) { this._type = DER.GeneralizedTime; } let contents = readTagAndMakeDER(this._der, this._type); let year; // Lint TODO: validate that the appropriate one of {UTCTime,GeneralizedTime} // is used according to RFC 5280 and what the value of the date is. // TODO TODO: explain this better (just quote the rfc). if (this._type == DER.UTCTime) { // UTCTime is YYMMDDHHMMSSZ in RFC 5280. If YY is greater than or equal // to 50, the year is 19YY. Otherwise, it is 20YY. let y1 = this._validateDigit(contents.readByte()); let y2 = this._validateDigit(contents.readByte()); let yy = y1 * 10 + y2; if (yy >= 50) { year = 1900 + yy; } else { year = 2000 + yy; } } else { // GeneralizedTime is YYYYMMDDHHMMSSZ in RFC 5280. year = 0; for (let i = 0; i < 4; i++) { let y = this._validateDigit(contents.readByte()); year = year * 10 + y; } } let m1 = this._validateDigit(contents.readByte()); let m2 = this._validateDigit(contents.readByte()); let month = m1 * 10 + m2; if (month == 0 || month > 12) { throw new Error(ERROR_TIME_NOT_VALID); } let d1 = this._validateDigit(contents.readByte()); let d2 = this._validateDigit(contents.readByte()); let day = d1 * 10 + d2; if (day == 0 || day > 31) { throw new Error(ERROR_TIME_NOT_VALID); } let h1 = this._validateDigit(contents.readByte()); let h2 = this._validateDigit(contents.readByte()); let hour = h1 * 10 + h2; if (hour > 23) { throw new Error(ERROR_TIME_NOT_VALID); } let min1 = this._validateDigit(contents.readByte()); let min2 = this._validateDigit(contents.readByte()); let minute = min1 * 10 + min2; if (minute > 59) { throw new Error(ERROR_TIME_NOT_VALID); } let s1 = this._validateDigit(contents.readByte()); let s2 = this._validateDigit(contents.readByte()); let second = s1 * 10 + s2; if (second > 60) { // leap-seconds mean this can be as much as 60 throw new Error(ERROR_TIME_NOT_VALID); } let z = contents.readByte(); if (z != "Z".charCodeAt(0)) { throw new Error(ERROR_TIME_NOT_VALID); } // Lint TODO: verify that the Time doesn't specify a nonsensical // month/day/etc. // months are zero-indexed in JS this._time = new Date(Date.UTC(year, month - 1, day, hour, minute, second)); contents.assertAtEnd(); this._der.assertAtEnd(); } /** * Takes a byte that is supposed to be in the ASCII range for "0" to "9". * Validates the range and then converts it to the range 0 to 9. * * @param {number} d the digit in question (as ASCII in the range ["0", "9"]) * @returns {number} the numerical value of the digit (in the range [0, 9]) */ _validateDigit(d) { if (d < "0".charCodeAt(0) || d > "9".charCodeAt(0)) { throw new Error(ERROR_TIME_NOT_VALID); } return d - "0".charCodeAt(0); } } // Validity ::= SEQUENCE { // notBefore Time, // notAfter Time } class Validity extends DecodedDER { constructor() { super(); this._notBefore = new Time(); this._notAfter = new Time(); } get notBefore() { return this._notBefore; } get notAfter() { return this._notAfter; } parseOverride() { let contents = readSEQUENCEAndMakeDER(this._der); this._notBefore.parse( contents.readTLVChoice([DER.UTCTime, DER.GeneralizedTime]) ); this._notAfter.parse( contents.readTLVChoice([DER.UTCTime, DER.GeneralizedTime]) ); contents.assertAtEnd(); this._der.assertAtEnd(); } } // SubjectPublicKeyInfo ::= SEQUENCE { // algorithm AlgorithmIdentifier, // subjectPublicKey BIT STRING } class SubjectPublicKeyInfo extends DecodedDER { constructor() { super(); this._algorithm = new AlgorithmIdentifier(); this._subjectPublicKey = null; } get algorithm() { return this._algorithm; } get subjectPublicKey() { return this._subjectPublicKey; } parseOverride() { let contents = readSEQUENCEAndMakeDER(this._der); this._algorithm.parse(contents.readTLV()); let subjectPublicKeyBitString = contents.readBIT_STRING(); if (subjectPublicKeyBitString.unusedBits != 0) { throw new Error(ERROR_UNSUPPORTED_ASN1); } this._subjectPublicKey = subjectPublicKeyBitString.contents; contents.assertAtEnd(); this._der.assertAtEnd(); } } export var X509 = { Certificate };