Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
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 };