summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/X509.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'security/manager/ssl/X509.sys.mjs')
-rw-r--r--security/manager/ssl/X509.sys.mjs641
1 files changed, 641 insertions, 0 deletions
diff --git a/security/manager/ssl/X509.sys.mjs b/security/manager/ssl/X509.sys.mjs
new file mode 100644
index 0000000000..0d85ca5730
--- /dev/null
+++ b/security/manager/ssl/X509.sys.mjs
@@ -0,0 +1,641 @@
+/* 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 };