зеркало из https://github.com/mozilla/gecko-dev.git
635 строки
18 KiB
JavaScript
635 строки
18 KiB
JavaScript
/* 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;
|
|
// Until DER.jsm is actually used in production code, this is where we have to
|
|
// import it from.
|
|
var { DER } = Cu.import("resource://testing-common/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} d 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"];
|