| // 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<number>} |
| */ |
| 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<number>} 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<number>|!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]; |
| } |
| } |
| }; |