// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * ASN.1 parser, in the manner of BoringSSL's CBS (crypto byte string) lib. * * A |ByteString| is a buffer of DER-encoded bytes. To decode the buffer, you * must know something about the expected sequence of tags, which allows you to * call getASN1() and friends with the right arguments and in the right order. * * https://commondatastorage.googleapis.com/chromium-boringssl-docs/bytestring.h.html * is the canonical API reference. */ const ByteString = class { /** * Creates a new ASN.1 parser. * @param {!Uint8Array} buffer DER-encoded ASN.1 bytes. */ constructor(buffer) { /** @private {!Uint8Array} */ this.slice_ = buffer; } /** * @return {!Uint8Array} The DER-encoded bytes remaining in the buffer. */ get data() { return this.slice_; } /** * @return {number} The number of DER-encoded bytes remaining in the buffer. */ get length() { return this.slice_.length; } /** * @return {boolean} True if the buffer is empty. */ get empty() { return this.slice_.length == 0; } /** * Pops a byte from the start of the buffer. * @return {number} A byte. * @throws {Error} if the buffer is empty. * @private */ getU8_() { if (this.empty) { throw Error('getU8_: slice empty'); } const b = this.slice_[0]; this.slice_ = this.slice_.subarray(1); return b; } /** * Pops |n| bytes from the buffer. * @param {number} n The number of bytes to pop. * @throws {Error} * @private */ skip_(n) { if (this.slice_.length < n) { throw Error('skip_: too few bytes in input'); } this.slice_ = this.slice_.subarray(n); } /** * @param {number} n The number of bytes to read from the buffer. * @return {!Uint8Array} an array of |n| bytes. * @throws {Error} */ getBytes(n) { if (this.slice_.length < n) { throw Error('getBytes: too few bytes in input'); } const prefix = this.slice_.subarray(0, n); this.slice_ = this.slice_.subarray(n); return prefix; } /** * Returns a value of the specified type. * @param {number} expectedTag The expected tag, e.g. |SEQUENCE|, of the next * value in the buffer. * @param {boolean=} opt_includeHeader If true, include header bytes in the * buffer. * @return {!ByteString} The DER-encoded value bytes. * @throws {Error} * @private */ getASN1_(expectedTag, opt_includeHeader) { if (this.empty) { throw Error('getASN1: empty slice, expected tag ' + expectedTag); } const v = this.getAnyASN1(); if (v.tag != expectedTag) { throw Error('getASN1: got tag ' + v.tag + ', want ' + expectedTag); } if (!opt_includeHeader) { v.val.skip_(v.headerLen); } return v.val; } /** * Returns a value of the specified type. * @param {number} expectedTag The expected tag, e.g. |SEQUENCE|, of the next * value in the buffer. * @return {!ByteString} The DER-encoded value bytes. * @throws {Error} */ getASN1(expectedTag) { return this.getASN1_(expectedTag, false); } /** * Returns a base128-encoded integer. * @return {number} an int32. * @private */ getBase128Int_() { var lookahead = this.slice_.length; if (lookahead > 4) { lookahead = 4; } var len = 0; for (var i = 0; i < lookahead; i++) { if (!(this.data[i] & 0x80)) { len = i + 1; break; } } if (len == 0) { throw Error('terminating byte not found'); } var n = 0; var octets = this.getBytes(len); for (var i = 0; i < len; i++) { n |= (octets[i] & 0x7f) << 7 * (len - i - 1); } return n; } /** * Returns an OBJECT IDENTIFIER. * @return {Array} */ getASN1ObjectIdentifier() { var b = this.getASN1(Tag.OBJECT); var result = []; var first = b.getBase128Int_(); result[1] = first % 40; result[0] = (first - result[1]) / 40; var n = 2; while (!b.empty) { result[n++] = b.getBase128Int_(); } return result; } /** * Returns a value of the specified type, with its header. * @param {number} expectedTag The expected tag, e.g. |SEQUENCE|, of the next * value in the buffer. * @return {!ByteString} The DER-encoded header and value bytes. * @throws {Error} */ getASN1Element(expectedTag) { return this.getASN1_(expectedTag, true); } /** * Returns an optional value of the specified type. * @param {number} expectedTag The expected tag, e.g. |SEQUENCE|, of the next * value in the buffer. * @return {ByteString} * */ getOptionalASN1(expectedTag) { if (this.slice_.length < 1 || this.slice_[0] != expectedTag) { return null; } return this.getASN1(expectedTag); } /** * Matches and returns any ASN.1 type. * @return {{tag: number, headerLen: number, val: !ByteString}} An ASN.1 * value. The returned |ByteString| includes the DER header bytes. * @throws {Error} */ getAnyASN1() { const header = new ByteString(this.slice_); const tag = header.getU8_(); const lengthByte = header.getU8_(); if ((tag & 0x1f) == 0x1f) { throw Error('getAnyASN1: long-form tag found'); } var len = 0; var headerLen = 0; if ((lengthByte & 0x80) == 0) { // Short form length. len = lengthByte + 2; headerLen = 2; } else { // The high bit indicates that this is the long form, while the next 7 // bits encode the number of subsequent octets used to encode the length // (ITU-T X.690 clause 8.1.3.5.b). const numBytes = lengthByte & 0x7f; // Bitwise operations are always on signed 32-bit two's complement // numbers. This check ensures that we stay under this limit. We could // do this in a better way, but there's no need to process very large // objects. if (numBytes == 0 || numBytes > 3) { throw Error('getAnyASN1: bad ASN.1 long-form length'); } const lengthBytes = header.getBytes(numBytes); for (var i = 0; i < numBytes; i++) { len <<= 8; len |= lengthBytes[i]; } if (len < 128 || (len >> ((numBytes - 1) * 8)) == 0) { throw Error('getAnyASN1: incorrectly encoded ASN.1 length'); } headerLen = 2 + numBytes; len += headerLen; } if (this.slice_.length < len) { throw Error('getAnyASN1: too few bytes in input'); } const prefix = this.slice_.subarray(0, len); this.slice_ = this.slice_.subarray(len); return {tag: tag, headerLen: headerLen, val: new ByteString(prefix)}; } }; /** * Tag is a container for ASN.1 tag values, like |SEQUENCE|. These values * are arguments to e.g. getASN1(). */ const Tag = class { /** @return {number} */ static get BOOLEAN() { return 1; } /** @return {number} */ static get INTEGER() { return 2; } /** @return {number} */ static get BITSTRING() { return 3; } /** @return {number} */ static get OCTETSTRING() { return 4; } /** @return {number} */ static get NULL() { return 5; } /** @return {number} */ static get OBJECT() { return 6; } /** @return {number} */ static get UTF8String() { return 12; } /** @return {number} */ static get PrintableString() { return 19; } /** @return {number} */ static get UTCTime() { return 23; } /** @return {number} */ static get GeneralizedTime() { return 24; } /** @return {number} */ static get CONSTRUCTED() { return 0x20; } /** @return {number} */ static get SEQUENCE() { return 0x30; } /** @return {number} */ static get SET() { return 0x31; } /** @return {number} */ static get CONTEXT_SPECIFIC() { return 0x80; } }; /** * ASN.1 builder, in the manner of BoringSSL's CBB (crypto byte builder). * * A |ByteBuilder| maintains a |Uint8Array| slice and appends to it on demand. * After appending all the necessary values, the |data| property returns a * slice containing the result. Utility functions are provided for appending * ASN.1 DER-formatted values. * * Several of the functions take a "continuation" parameter. This is a function * that makes calls to its argument in order to lay down the contents of a * value. Once the continuation returns, the length prefix will be serialised. * It is illegal to call methods on a parent ByteBuilder while a continuation * function is running. */ const ByteBuilder = class { constructor() { /** @private {?Uint8Array} */ this.slice_ = null; /** @private {number} */ this.len_ = 0; /** @private {?ByteBuilder} */ this.child_ = null; } /** * @return {!Uint8Array} The constructed bytes */ get data() { if (this.child_ != null) { throw Error('data access while child is pending'); } if (this.slice_ === null) { return new Uint8Array(0); } return this.slice_.subarray(0, this.len_); } /** * Reallocates the slice to at least a given size. * @param {number} minNewSize The minimum resulting size of the slice. * @private */ realloc_(minNewSize) { var newSize = 0; if (minNewSize > Number.MAX_SAFE_INTEGER - minNewSize) { // Cannot grow exponentially without overflow. newSize = minNewSize; } else { newSize = minNewSize * 2; } if (this.slice_ === null) { if (newSize < 128) { newSize = 128; } this.slice_ = new Uint8Array(newSize); return; } const newSlice = new Uint8Array(newSize); for (var i = 0; i < this.len_; i++) { newSlice[i] = this.slice_[i]; } this.slice_ = newSlice; } /** * Extends the current slice by the given number of bytes. * @param {number} n The number of extra bytes needed in the slice. * @return {number} The offset of the new bytes. * @throws {Error} * @private */ extend_(n) { if (this.child_ != null) { throw Error('write while child pending'); } if (this.len_ > Number.MAX_SAFE_INTEGER - n) { throw Error('length overflow'); } if (this.slice_ === null || this.len_ + n > this.slice_.length) { this.realloc_(this.len_ + n); } const offset = this.len_; this.len_ += n; return offset; } /** * Appends a uint8 to the slice. * @param {number} b The byte to append. * @throws {Error} * @private */ addU8_(b) { const offset = this.extend_(1); this.slice_[offset] = b; } /** * Appends a length prefixed value to the slice. * @param {number} lenLen The number of length-prefix bytes. * @param {boolean} isASN1 True iff an ASN.1 length should be prefixed. * @param {function(ByteBuilder)} k A function to construct the contents. * @throws {Error} * @private */ addLengthPrefixed_(lenLen, isASN1, k) { var offset = this.extend_(lenLen); var child = new ByteBuilder(); child.slice_ = this.slice_; child.len_ = this.len_; this.child_ = child; k(child); var length = child.len_ - lenLen - offset; if (length > 0x7fffffff) { // If a number larger than this is used with a shift operation in // Javascript, the result is incorrect. throw Error('length too large'); } if (isASN1) { // In the case of ASN.1 a single byte was reserved for // the length. The contents of the array may need to be // shifted along if the length needs more than that. if (lenLen != 1) { throw Error('internal error'); } var lenByte = 0; if (length > 0xffffff) { lenLen = 5; lenByte = 0x80 | 4; } else if (length > 0xffff) { lenLen = 4; lenByte = 0x80 | 3; } else if (length > 0xff) { lenLen = 3; lenByte = 0x80 | 2; } else if (length > 0x7f) { lenLen = 2; lenByte = 0x80 | 1; } else { lenLen = 1; lenByte = length; length = 0; } child.slice_[offset] = lenByte; const extraBytesNeeded = lenLen - 1; if (extraBytesNeeded > 0) { child.extend_(extraBytesNeeded); child.slice_.copyWithin(offset + lenLen, offset + 1, child.len_); } offset++; lenLen = extraBytesNeeded; } var l = length; for (var i = lenLen - 1; i >= 0; i--) { child.slice_[offset + i] = l; l >>= 8; } if (l != 0) { throw Error('pending child length exceeds reserved space'); } this.slice_ = child.slice_; this.len_ = child.len_; this.child_ = null; } /** * Appends an ASN.1 element to the slice. * @param {number} tag The ASN.1 tag value (must be < 31). * @param {function(ByteBuilder)} k A function to construct the contents. * @throws {Error} */ addASN1(tag, k) { if (tag > 255) { throw Error('high-tag values not supported'); } this.addU8_(tag); this.addLengthPrefixed_(1, true, k); } /** * Appends an ASN.1 INTEGER to the slice. * @param {number} n The value of the integer. Must be within the range of an * int32. * @throws {Error} */ addASN1Int(n) { if (n < (0x80000000 << 0) || n > 0x7fffffff) { // Numbers this large (or small) cannot be correctly shifted in // Javascript. throw Error('integer out of encodable range'); } var length = 1; for (var nn = n; nn >= 0x80 || nn <= -0x80; nn >>= 8) { length++; } this.addASN1(Tag.INTEGER, (b) => { for (var i = length - 1; i >= 0; i--) { b.addU8_((n >> (8 * i)) & 0xff); } }); } /** * Appends a non-negative ASN.1 INTEGER to the slice given its big-endian * encoding. This can be useful when interacting with the WebCrypto API. * @param {!Uint8Array} bytes The big-endian encoding of the integer. * @throws {Error} */ addASN1BigInt(bytes) { // Zero is representated as a single zero byte, rather than no bytes. if (bytes.length == 0) { bytes = new Uint8Array(1); } // Leading zero bytes need to be removed, unless that would make the number // negative. while (bytes.length >= 2 && bytes[0] == 0 && (bytes[1] & 0x80) == 0) { bytes = bytes.slice(1); } // If the MSB is set, the number will be considered to be negative. Thus // a zero prefix is needed in that case. if (bytes.length > 0 && (bytes[0] & 0x80) == 0x80) { if (bytes.length > Number.MAX_SAFE_INTEGER - 1) { throw Error('bigint array too long'); } var newBytes = new Uint8Array(bytes.length + 1); newBytes.set(bytes, 1); bytes = newBytes; } this.addASN1(Tag.INTEGER, (b) => b.addBytes(bytes)); } /** * Appends a base128-encoded integer to the slice. * @param {number} n The value of the integer. Must be non-negative and within * the range of an int32. * @throws {Error} * @private */ addBase128Int_(n) { if (n < 0 || n > 0x7fffffff) { // Cannot encode negative numbers and large numbers cannot be shifted in // Javascript. throw Error('integer out of encodable range'); } var length = 0; if (n == 0) { length = 1; } else { for (var i = n; i > 0; i >>= 7) { length++; } } for (var i = length - 1; i >= 0; i--) { var octet = 0x7f & (n >> (7 * i)); if (i != 0) { octet |= 0x80; } this.addU8_(octet); } } /** * Appends an OBJECT IDENTIFIER to the slice. * @param {Array} oid The OID as a list of integer elements. * @throws {Error} */ addASN1ObjectIdentifier(oid) { if (oid.length < 2 || oid[0] > 2 || (oid[0] <= 1 && oid[1] >= 40)) { throw Error('invalid OID'); } this.addASN1(Tag.OBJECT, (b) => { b.addBase128Int_(oid[0] * 40 + oid[1]); for (var i = 2; i < oid.length; i++) { b.addBase128Int_(oid[i]); } }); } /** * Appends an ASN.1 NULL to the slice. * @throws {Error} */ addASN1Null() { const offset = this.extend_(2); this.slice_[offset] = Tag.NULL; this.slice_[offset + 1] = 0; } /** * Appends an ASN.1 PrintableString to the slice. * @param {string} s The contents of the string. * @throws {Error} */ addASN1PrintableString(s) { var buf = new Uint8Array(s.length); for (var i = 0; i < s.length; i++) { const code = s.charCodeAt(i); if ((code < 97 && code > 122) && // a-z (code < 65 && code > 90) && // A-Z ' \'()+,-/:=?'.indexOf(String.fromCharCode(code)) == -1) { throw Error( 'cannot encode \'' + String.fromCharCode(code) + '\' in' + ' PrintableString'); } buf[i] = code; } this.addASN1(Tag.PrintableString, (b) => { b.addBytes(buf); }); } /** * Appends an ASN.1 UTF8String to the slice. * @param {string} s The contents of the string. * @throws {Error} */ addASN1UTF8String(s) { this.addASN1(Tag.UTF8String, (b) => { b.addBytes((new TextEncoder()).encode(s)); }); } /** * Appends an ASN.1 BIT STRING to the slice. * @param {!Uint8Array} bytes The contents, which must be a whole number of * bytes. * @throws {Error} */ addASN1BitString(bytes) { this.addASN1(Tag.BITSTRING, (b) => { b.addU8_(0); // no superfluous bits in encoding. b.addBytes(bytes); }); } /** * Appends raw data to the slice. * @param {string} s The contents to append. All character values must * be < 256. * @throws {Error} */ addBytesFromString(s) { const buf = new Uint8Array(s.length); for (var i = 0; i < s.length; i++) { const code = s.charCodeAt(i); if (code > 255) { throw Error('out-of-range character in string of bytes'); } buf[i] = code; } this.addBytes(buf); } /** * Appends raw bytes to the slice. * @param {!Array|!Uint8Array} bytes Data to append. * @throws {Error} */ addBytes(bytes) { const offset = this.extend_(bytes.length); for (var i = 0; i < bytes.length; i++) { this.slice_[offset + i] = bytes[i]; } } };