From ee56723139c603b26c887c74f9b4ee0563b80f47 Mon Sep 17 00:00:00 2001 From: David Keeler Date: Tue, 20 Sep 2016 15:36:25 -0700 Subject: [PATCH] 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 --HG-- extra : rebase_source : 6105cf16e46d5d2cc9355cf38f8d0098a8a40462 --- security/manager/ssl/X509.jsm | 632 +++++++++++++++++++ security/manager/ssl/moz.build | 1 + security/manager/ssl/tests/unit/test_x509.js | 83 +++ security/manager/ssl/tests/unit/xpcshell.ini | 1 + 4 files changed, 717 insertions(+) create mode 100644 security/manager/ssl/X509.jsm create mode 100644 security/manager/ssl/tests/unit/test_x509.js diff --git a/security/manager/ssl/X509.jsm b/security/manager/ssl/X509.jsm new file mode 100644 index 000000000000..be107454e115 --- /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"]; diff --git a/security/manager/ssl/moz.build b/security/manager/ssl/moz.build index bf88ef5d59e8..19e1b4f4b141 100644 --- a/security/manager/ssl/moz.build +++ b/security/manager/ssl/moz.build @@ -55,6 +55,7 @@ XPIDL_MODULE = 'pipnss' EXTRA_JS_MODULES.psm += [ 'DER.jsm', + 'X509.jsm', ] EXPORTS += [ diff --git a/security/manager/ssl/tests/unit/test_x509.js b/security/manager/ssl/tests/unit/test_x509.js new file mode 100644 index 000000000000..a7ece856a1c1 --- /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"); +} diff --git a/security/manager/ssl/tests/unit/xpcshell.ini b/security/manager/ssl/tests/unit/xpcshell.ini index 6ed01a50cf66..ad3da3d264de 100644 --- a/security/manager/ssl/tests/unit/xpcshell.ini +++ b/security/manager/ssl/tests/unit/xpcshell.ini @@ -151,6 +151,7 @@ skip-if = toolkit == 'android' || toolkit == 'gonk' [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]