bug 1304188 - introduce X509.jsm r=Cykesiopka,jcj
authorDavid Keeler <dkeeler@mozilla.com>
Tue, 20 Sep 2016 15:36:25 -0700
changeset 421223 2bf6541e14fcb918cbbc0e6ecce836af612dea10
parent 421222 d4ff44952f725161d76fa7e7be32b3be153652fe
child 421224 54474ed8388b6f677bb35ab251df6f147c13ec24
push id31424
push userbmo:afarre@mozilla.com
push dateWed, 05 Oct 2016 16:04:50 +0000
reviewersCykesiopka, jcj
bugs1304188
milestone52.0a1
bug 1304188 - introduce X509.jsm r=Cykesiopka,jcj This is mostly a preliminary review request, although I think everything that should be done in this bug is present. This intentionally does not include support for decoding extensions or subject public keys. MozReview-Commit-ID: 4ewu66Xx411
security/manager/ssl/X509.jsm
security/manager/ssl/moz.build
security/manager/ssl/tests/unit/test_x509.js
security/manager/ssl/tests/unit/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/X509.jsm
@@ -0,0 +1,632 @@
+/* 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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+var { DER } = Cu.import("resource://gre/modules/psm/DER.jsm", {});
+
+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
+ * @return {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
+ * @return {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 > 0) {
+      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.
+   * @return {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.DER(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
+ * @return {DER} the contents of the SEQUENCE
+ */
+function readSEQUENCEAndMakeDER(der) {
+  return new DER.DER(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
+ * @return {DER} the contents of the tag
+ */
+function readTagAndMakeDER(der, tag) {
+  return new DER.DER(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} the digit in question (as ASCII in the range ["0", "9"])
+   * @return {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();
+  }
+}
+
+this.X509 = { Certificate };
+this.EXPORTED_SYMBOLS = ["X509"];
--- a/security/manager/ssl/moz.build
+++ b/security/manager/ssl/moz.build
@@ -50,16 +50,17 @@ if CONFIG['MOZ_XUL']:
     XPIDL_SOURCES += [
         'nsICertTree.idl',
     ]
 
 XPIDL_MODULE = 'pipnss'
 
 EXTRA_JS_MODULES.psm += [
     'DER.jsm',
+    'X509.jsm',
 ]
 
 EXPORTS += [
     'CryptoTask.h',
     'nsClientAuthRemember.h',
     'nsCrypto.h',
     'nsNSSCallbacks.h',
     'nsNSSCertificate.h',
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_x509.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests X509.jsm functionality.
+
+var { X509 } = Cu.import("resource://gre/modules/psm/X509.jsm", {});
+
+function stringToBytes(s) {
+  let b = [];
+  for (let i = 0; i < s.length; i++) {
+    b.push(s.charCodeAt(i));
+  }
+  return b;
+}
+
+function readPEMToBytes(filename) {
+  return stringToBytes(atob(pemToBase64(readFile(do_get_file(filename)))));
+}
+
+function run_test() {
+  let certificate = new X509.Certificate();
+  certificate.parse(readPEMToBytes("bad_certs/default-ee.pem"));
+
+  equal(certificate.tbsCertificate.version, 3,
+        "default-ee.pem should be x509v3");
+
+  // serialNumber
+  deepEqual(certificate.tbsCertificate.serialNumber,
+            [ 0x1b, 0x27, 0x62, 0x4d, 0xc3, 0x70, 0xbf, 0x3d, 0xb6, 0x66,
+              0x98, 0x33, 0xd8, 0x3c, 0x74, 0xd9, 0xee, 0x2c, 0x56, 0xc1 ],
+            "default-ee.pem should have expected serialNumber");
+
+  deepEqual(certificate.tbsCertificate.signature.algorithm._values,
+            [ 1, 2, 840, 113549, 1, 1, 11 ], // sha256WithRSAEncryption
+            "default-ee.pem should have sha256WithRSAEncryption signature");
+  // TODO: there should actually be an explicit encoded NULL here, but it looks
+  // like pycert doesn't include it.
+  deepEqual(certificate.tbsCertificate.signature.parameters, null,
+            "default-ee.pem should have NULL parameters for signature");
+
+  equal(certificate.tbsCertificate.issuer.rdns.length, 1,
+        "default-ee.pem should have one RDN in issuer");
+  equal(certificate.tbsCertificate.issuer.rdns[0].avas.length, 1,
+        "default-ee.pem should have one AVA in RDN in issuer");
+  deepEqual(certificate.tbsCertificate.issuer.rdns[0].avas[0].value.value,
+            stringToBytes("Test CA"),
+            "default-ee.pem should have issuer 'Test CA'");
+
+  equal(certificate.tbsCertificate.validity.notBefore.time.getTime(),
+        Date.parse("2014-11-27T00:00:00.000Z"),
+        "default-ee.pem should have the correct value for notBefore");
+  equal(certificate.tbsCertificate.validity.notAfter.time.getTime(),
+        Date.parse("2017-02-04T00:00:00.000Z"),
+        "default-ee.pem should have the correct value for notAfter");
+
+  equal(certificate.tbsCertificate.subject.rdns.length, 1,
+        "default-ee.pem should have one RDN in subject");
+  equal(certificate.tbsCertificate.subject.rdns[0].avas.length, 1,
+        "default-ee.pem should have one AVA in RDN in subject");
+  deepEqual(certificate.tbsCertificate.subject.rdns[0].avas[0].value.value,
+            stringToBytes("Test End-entity"),
+            "default-ee.pem should have subject 'Test End-entity'");
+
+  deepEqual(certificate.tbsCertificate.subjectPublicKeyInfo.algorithm.algorithm._values,
+            [ 1, 2, 840, 113549, 1, 1, 1 ], // rsaEncryption
+            "default-ee.pem should have a spki algorithm of rsaEncryption");
+
+  equal(certificate.tbsCertificate.extensions.length, 2,
+        "default-ee.pem should have two extensions");
+
+  deepEqual(certificate.signatureAlgorithm.algorithm._values,
+            [ 1, 2, 840, 113549, 1, 1, 11 ], // sha256WithRSAEncryption
+            "default-ee.pem should have sha256WithRSAEncryption signatureAlgorithm");
+  // TODO: there should actually be an explicit encoded NULL here, but it looks
+  // like pycert doesn't include it.
+  deepEqual(certificate.signatureAlgorithm.parameters, null,
+            "default-ee.pem should have NULL parameters for signatureAlgorithm");
+
+  equal(certificate.signatureValue.length, 2048 / 8,
+        "length of signature on default-ee.pem should be 2048 bits");
+}
--- a/security/manager/ssl/tests/unit/xpcshell.ini
+++ b/security/manager/ssl/tests/unit/xpcshell.ini
@@ -146,11 +146,12 @@ skip-if = toolkit == 'android' || toolki
 [test_sss_savestate.js]
 [test_sts_fqdn.js]
 [test_sts_holepunch.js]
 [test_sts_ipv4_ipv6.js]
 [test_sts_preloadlist_perwindowpb.js]
 [test_sts_preloadlist_selfdestruct.js]
 [test_validity.js]
 run-sequentially = hardcoded ports
+[test_x509.js]
 
 # The TLS error reporting functionality lives in /toolkit but needs tlsserver
 [test_toolkit_securityreporter.js]