diff --git a/bower.json b/bower.json index 12641816f..ca7f91904 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,5 @@ { "name": "forge", - "version": "0.6.47-dev", "description": "JavaScript implementations of network transports, cryptography, ciphers, PKI, message digests, and various utilities.", "moduleType": ["amd"], "authors": [ diff --git a/js/asn1.js b/js/asn1.js index e07e45c49..326b579dd 100644 --- a/js/asn1.js +++ b/js/asn1.js @@ -185,10 +185,13 @@ asn1.Type = { * @param type the data type (tag number) for the object. * @param constructed true if the asn1 object is in constructed form. * @param value the value for the object, if it is not constructed. + * @param [options] the options to use: + * [bitStringContents] the plain BIT STRING content including padding + * byte. * * @return the asn1 object. */ -asn1.create = function(tagClass, type, constructed, value) { +asn1.create = function(tagClass, type, constructed, value, options) { /* An asn1 object has a tagClass, a type, a constructed flag, and a value. The value's type depends on the constructed flag. If constructed, it will contain a list of other asn1 objects. If not, @@ -206,13 +209,108 @@ asn1.create = function(tagClass, type, constructed, value) { value = tmp; } - return { + var obj = { tagClass: tagClass, type: type, constructed: constructed, composed: constructed || forge.util.isArray(value), value: value }; + if(options && 'bitStringContents' in options) { + // TODO: copy byte buffer if it's a buffer not a string + obj.bitStringContents = options.bitStringContents; + // TODO: add readonly flag to avoid this overhead + // save copy to detect changes + obj.original = asn1.copy(obj); + } + return obj; +}; + +/** + * Copies an asn1 object. + * + * @param obj the asn1 object. + * @param [options] copy options: + * [excludeBitStringContents] true to not copy bitStringContents + * + * @return the a copy of the asn1 object. + */ +asn1.copy = function(obj, options) { + var copy; + + if(forge.util.isArray(obj)) { + copy = []; + for(var i = 0; i < obj.length; ++i) { + copy.push(asn1.copy(obj[i], options)); + } + return copy; + } + + if(typeof obj === 'string') { + // TODO: copy byte buffer if it's a buffer not a string + return obj; + } + + copy = { + tagClass: obj.tagClass, + type: obj.type, + constructed: obj.constructed, + composed: obj.composed, + value: asn1.copy(obj.value, options) + }; + if(options && !options.excludeBitStringContents) { + // TODO: copy byte buffer if it's a buffer not a string + copy.bitStringContents = obj.bitStringContents; + } + return copy; +}; + +/** + * Compares asn1 objects for equality. + * + * Note this function does not run in constant time. + * + * @param obj1 the first asn1 object. + * @param obj2 the second asn1 object. + * @param [options] compare options: + * [includeBitStringContents] true to compare bitStringContents + * + * @return true if the asn1 objects are equal. + */ +asn1.equals = function(obj1, obj2, options) { + if(forge.util.isArray(obj1)) { + if(!forge.util.isArray(obj2)) { + return false; + } + if(obj1.length !== obj2.length) { + return false; + } + for(var i = 0; i < obj1.length; ++i) { + if(!asn1.equals(obj1[i], obj2[i])) { + return false; + } + return true; + } + } + + if(typeof obj1 !== typeof obj2) { + return false; + } + + if(typeof obj1 === 'string') { + return obj1 === obj2; + } + + var equal = obj1.tagClass === obj2.tagClass && + obj1.type === obj2.type && + obj1.constructed === obj2.constructed && + obj1.composed === obj2.composed && + asn1.equals(obj1.value, obj2.value); + if(options && options.includeBitStringContents) { + equal = equal && (obj1.bitStringContents === obj2.bitStringContents); + } + + return equal; }; /** @@ -225,7 +323,7 @@ asn1.create = function(tagClass, type, constructed, value) { * * @return the length of the BER-encoded ASN.1 value or undefined. */ -var _getValueLength = asn1.getBerValueLength = function(b) { +asn1.getBerValueLength = function(b) { // TODO: move this function and related DER/BER functions to a der.js // file; better abstract ASN.1 away from der/ber. var b2 = b.getByte(); @@ -247,18 +345,99 @@ var _getValueLength = asn1.getBerValueLength = function(b) { return length; }; +/** + * Check if the byte buffer has enough bytes. Throws an Error if not. + * + * @param bytes the byte buffer to parse from. + * @param remaining the bytes remaining in the current parsing state. + * @param n the number of bytes the buffer must have. + */ +function _checkBufferLength(bytes, remaining, n) { + if(n > remaining) { + var error = new Error('Too few bytes to parse DER.'); + error.available = bytes.length(); + error.remaining = remaining; + error.requested = n; + throw error; + } +} + +/** + * Gets the length of a BER-encoded ASN.1 value. + * + * In case the length is not specified, undefined is returned. + * + * @param bytes the byte buffer to parse from. + * @param remaining the bytes remaining in the current parsing state. + * + * @return the length of the BER-encoded ASN.1 value or undefined. + */ +var _getValueLength = function(bytes, remaining) { + // TODO: move this function and related DER/BER functions to a der.js + // file; better abstract ASN.1 away from der/ber. + // fromDer already checked that this byte exists + var b2 = bytes.getByte(); + remaining--; + if(b2 === 0x80) { + return undefined; + } + + // see if the length is "short form" or "long form" (bit 8 set) + var length; + var longForm = b2 & 0x80; + if(!longForm) { + // length is just the first byte + length = b2; + } else { + // the number of bytes the length is specified in bits 7 through 1 + // and each length byte is in big-endian base-256 + var longFormBytes = b2 & 0x7F; + _checkBufferLength(bytes, remaining, longFormBytes); + length = bytes.getInt(longFormBytes << 3); + } + // FIXME: this will only happen for 32 bit getInt with high bit set + if(length < 0) { + throw new Error('Negative length: ' + length); + } + return length; +}; + /** * Parses an asn1 object from a byte buffer in DER format. * * @param bytes the byte buffer to parse from. - * @param strict true to be strict when checking value lengths, false to + * @param [strict] true to be strict when checking value lengths, false to * allow truncated values (default: true). + * @param [options] object with options or boolean strict flag + * [strict] true to be strict when checking value lengths, false to + * allow truncated values (default: true). + * [decodeBitStrings] true to attempt to decode the content of + * BIT STRINGs (not OCTET STRINGs) using strict mode. Note that + * without schema support to understand the data context this can + * erroneously decode values that happen to be valid ASN.1. This + * flag will be deprecated or removed as soon as schema support is + * available. (default: true) * * @return the parsed asn1 object. */ -asn1.fromDer = function(bytes, strict) { - if(strict === undefined) { - strict = true; +asn1.fromDer = function(bytes, options) { + if(options === undefined) { + options = { + strict: true, + decodeBitStrings: true + }; + } + if(typeof options === 'boolean') { + options = { + strict: options, + decodeBitStrings: true + }; + } + if(!('strict' in options)) { + options.strict = true; + } + if(!('decodeBitStrings' in options)) { + options.decodeBitStrings = true; } // wrap in buffer if needed @@ -266,15 +445,30 @@ asn1.fromDer = function(bytes, strict) { bytes = forge.util.createBuffer(bytes); } + return _fromDer(bytes, bytes.length(), 0, options); +} + +/** + * Internal function to parse an asn1 object from a byte buffer in DER format. + * + * @param bytes the byte buffer to parse from. + * @param remaining the number of bytes remaining for this chunk. + * @param depth the current parsing depth. + * @param options object with same options as fromDer(). + * + * @return the parsed asn1 object. + */ +function _fromDer(bytes, remaining, depth, options) { + // temporary storage for consumption calculations + var start; + // minimum length for ASN.1 DER structure is 2 - if(bytes.length() < 2) { - var error = new Error('Too few bytes to parse DER.'); - error.bytes = bytes.length(); - throw error; - } + _checkBufferLength(bytes, remaining, 2); // get the first byte var b1 = bytes.getByte(); + // consumed one byte + remaining--; // get the tag class var tagClass = (b1 & 0xC0); @@ -282,107 +476,156 @@ asn1.fromDer = function(bytes, strict) { // get the type (bits 1-5) var type = b1 & 0x1F; - // get the value length - var length = _getValueLength(bytes); + // get the variable value length and adjust remaining bytes + start = bytes.length(); + var length = _getValueLength(bytes, remaining); + remaining -= start - bytes.length(); // ensure there are enough bytes to get the value - if(bytes.length() < length) { - if(strict) { + if(length !== undefined && length > remaining) { + if(options.strict) { var error = new Error('Too few bytes to read ASN.1 value.'); - error.detail = bytes.length() + ' < ' + length; + error.available = bytes.length(); + error.remaining = remaining; + error.requested = length; throw error; } - // Note: be lenient with truncated values - length = bytes.length(); + // Note: be lenient with truncated values and use remaining state bytes + length = remaining; } - // prepare to get value + // value storage var value; + // possible BIT STRING contents storage + var bitStringContents; // constructed flag is bit 6 (32 = 0x20) of the first byte var constructed = ((b1 & 0x20) === 0x20); - - // determine if the value is composed of other ASN.1 objects (if its - // constructed it will be and if its a BITSTRING it may be) - var composed = constructed; - if(!composed && tagClass === asn1.Class.UNIVERSAL && - type === asn1.Type.BITSTRING && length > 1) { - /* The first octet gives the number of bits by which the length of the - bit string is less than the next multiple of eight (this is called - the "number of unused bits"). - - The second and following octets give the value of the bit string - converted to an octet string. */ - // if there are no unused bits, maybe the bitstring holds ASN.1 objs - var read = bytes.read; - var unused = bytes.getByte(); - if(unused === 0) { - // if the first byte indicates UNIVERSAL or CONTEXT_SPECIFIC, - // and the length is valid, assume we've got an ASN.1 object - b1 = bytes.getByte(); - var tc = (b1 & 0xC0); - if(tc === asn1.Class.UNIVERSAL || tc === asn1.Class.CONTEXT_SPECIFIC) { - try { - var len = _getValueLength(bytes); - composed = (len === length - (bytes.read - read)); - if(composed) { - // adjust read/length to account for unused bits byte - ++read; - --length; - } - } catch(ex) {} - } - } - // restore read pointer - bytes.read = read; - } - - if(composed) { + if(constructed) { // parse child asn1 objects from the value value = []; if(length === undefined) { // asn1 object of indefinite length, read until end tag for(;;) { + _checkBufferLength(bytes, remaining, 2); if(bytes.bytes(2) === String.fromCharCode(0, 0)) { bytes.getBytes(2); + remaining -= 2; break; } - value.push(asn1.fromDer(bytes, strict)); + start = bytes.length(); + value.push(_fromDer(bytes, remaining, depth + 1, options)); + remaining -= start - bytes.length(); } } else { // parsing asn1 object of definite length - var start = bytes.length(); while(length > 0) { - value.push(asn1.fromDer(bytes, strict)); + start = bytes.length(); + value.push(_fromDer(bytes, length, depth + 1, options)); + remaining -= start - bytes.length(); length -= start - bytes.length(); + } + } + } + + // if a BIT STRING, save the contents including padding + if(value === undefined && tagClass === asn1.Class.UNIVERSAL && + type === asn1.Type.BITSTRING) { + bitStringContents = bytes.bytes(length); + } + + // determine if a non-constructed value should be decoded as a composed + // value that contains other ASN.1 objects. BIT STRINGs (and OCTET STRINGs) + // can be used this way. + if(value === undefined && options.decodeBitStrings && + tagClass === asn1.Class.UNIVERSAL && + // FIXME: OCTET STRINGs not yet supported here + // .. other parts of forge expect to decode OCTET STRINGs manually + (type === asn1.Type.BITSTRING /*|| type === asn1.Type.OCTETSTRING*/) && + length > 1) { + // save read position + var savedRead = bytes.read; + var savedRemaining = remaining; + var unused = 0; + if(type === asn1.Type.BITSTRING) { + /* The first octet gives the number of bits by which the length of the + bit string is less than the next multiple of eight (this is called + the "number of unused bits"). + + The second and following octets give the value of the bit string + converted to an octet string. */ + _checkBufferLength(bytes, remaining, 1); + unused = bytes.getByte(); + remaining--; + } + // if all bits are used, maybe the BIT/OCTET STRING holds ASN.1 objs + if(unused === 0) { + try { + // attempt to parse child asn1 object from the value + // (stored in array to signal composed value) start = bytes.length(); + var subOptions = { + // enforce strict mode to avoid parsing ASN.1 from plain data + verbose: options.verbose, + strict: true, + decodeBitStrings: true + }; + var composed = _fromDer(bytes, remaining, depth + 1, subOptions); + var used = start - bytes.length(); + remaining -= used; + if(type == asn1.Type.BITSTRING) { + used++; + } + + // if the data all decoded and the class indicates UNIVERSAL or + // CONTEXT_SPECIFIC then assume we've got an encapsulated ASN.1 object + var tc = composed.tagClass; + if(used === length && + (tc === asn1.Class.UNIVERSAL || tc === asn1.Class.CONTEXT_SPECIFIC)) { + value = [composed]; + } + } catch(ex) { } } - } else { - // asn1 not composed, get raw value + if(value === undefined) { + // restore read position + bytes.read = savedRead; + remaining = savedRemaining; + } + } + + if(value === undefined) { + // asn1 not constructed or composed, get raw value // TODO: do DER to OID conversion and vice-versa in .toDer? if(length === undefined) { - if(strict) { + if(options.strict) { throw new Error('Non-constructed ASN.1 object of indefinite length.'); } - // be lenient and use remaining bytes - length = bytes.length(); + // be lenient and use remaining state bytes + length = remaining; } if(type === asn1.Type.BMPSTRING) { value = ''; - for(var i = 0; i < length; i += 2) { + for(; length > 0; length -= 2) { + _checkBufferLength(bytes, remaining, 2); value += String.fromCharCode(bytes.getInt16()); + remaining -= 2; } } else { value = bytes.getBytes(length); } } + // add BIT STRING contents if available + var asn1Options = bitStringContents === undefined ? null : { + bitStringContents: bitStringContents + }; + // create and return asn1 object - return asn1.create(tagClass, type, constructed, value); -}; + return asn1.create(tagClass, type, constructed, value, asn1Options); +} /** * Converts the given asn1 object to a buffer of bytes in DER format. @@ -400,8 +643,19 @@ asn1.toDer = function(obj) { // for storing the ASN.1 value var value = forge.util.createBuffer(); - // if composed, use each child asn1 object's DER bytes as value - if(obj.composed) { + // use BIT STRING contents if available and data not changed + var useBitStringContents = false; + if('bitStringContents' in obj) { + useBitStringContents = true; + if(obj.original) { + useBitStringContents = asn1.equals(obj, obj.original); + } + } + + if(useBitStringContents) { + value.putBytes(obj.bitStringContents); + } else if(obj.composed) { + // if composed, use each child asn1 object's DER bytes as value // turn on 6th bit (0x20 = 32) to indicate asn1 is constructed // from other asn1 objects if(obj.constructed) { @@ -425,6 +679,8 @@ asn1.toDer = function(obj) { } } else { // ensure integer is minimally-encoded + // TODO: should all leading bytes be stripped vs just one? + // .. ex '00 00 01' => '01'? if(obj.type === asn1.Type.INTEGER && obj.value.length > 1 && // leading 0x00 for positive integer @@ -862,7 +1118,10 @@ asn1.derToInteger = function(bytes) { * * To capture an ASN.1 value, set an object in the validator's 'capture' * parameter to the key to use in the capture map. To capture the full - * ASN.1 object, specify 'captureAsn1'. + * ASN.1 object, specify 'captureAsn1'. To capture BIT STRING bytes, including + * the leading unused bits counter byte, specify 'captureBitStringContents'. + * To capture BIT STRING bytes, without the leading unused bits counter byte, + * specify 'captureBitStringValue'. * * Objects in the validator may set a field 'optional' to true to indicate * that it isn't necessary to pass validation. @@ -916,6 +1175,23 @@ asn1.validate = function(obj, v, capture, errors) { if(v.captureAsn1) { capture[v.captureAsn1] = obj; } + if(v.captureBitStringContents && 'bitStringContents' in obj) { + capture[v.captureBitStringContents] = obj.bitStringContents; + } + if(v.captureBitStringValue && 'bitStringContents' in obj) { + var value; + if(obj.bitStringContents.length < 2) { + capture[v.captureBitStringValue] = ''; + } else { + // FIXME: support unused bits with data shifting + var unused = obj.bitStringContents.charCodeAt(0); + if(unused !== 0) { + throw new Error( + 'captureBitStringValue only supported for zero unused bits'); + } + capture[v.captureBitStringValue] = obj.bitStringContents.slice(1); + } + } } } else if(errors) { errors.push( @@ -997,12 +1273,12 @@ asn1.prettyPrint = function(obj, level, indentation) { case asn1.Type.BOOLEAN: rval += ' (Boolean)'; break; - case asn1.Type.BITSTRING: - rval += ' (Bit string)'; - break; case asn1.Type.INTEGER: rval += ' (Integer)'; break; + case asn1.Type.BITSTRING: + rval += ' (Bit string)'; + break; case asn1.Type.OCTETSTRING: rval += ' (Octet string)'; break; @@ -1092,6 +1368,23 @@ asn1.prettyPrint = function(obj, level, indentation) { } catch(ex) { rval += '0x' + forge.util.bytesToHex(obj.value); } + } else if(obj.type === asn1.Type.BITSTRING) { + // TODO: shift bits as needed to display without padding + if(obj.value.length > 1) { + // remove unused bits field + rval += '0x' + forge.util.bytesToHex(obj.value.slice(1)); + } else { + rval += '(none)'; + } + // show unused bit count + if(obj.value.length > 0) { + var unused = obj.value.charCodeAt(0); + if(unused == 1) { + rval += ' (1 unused bit shown)'; + } else if(unused > 1) { + rval += ' (' + unused + ' unused bits shown)'; + } + } } else if(obj.type === asn1.Type.OCTETSTRING) { if(!_nonLatinRegex.test(obj.value)) { rval += '(' + obj.value + ') '; diff --git a/js/pbkdf2.js b/js/pbkdf2.js index 63612e762..49c23b51f 100644 --- a/js/pbkdf2.js +++ b/js/pbkdf2.js @@ -51,6 +51,7 @@ forge.pbkdf2 = pkcs5.pbkdf2 = function(p, s, c, dkLen, md, callback) { // default prf to SHA-1 md = 'sha1'; } + p = new Buffer(p, 'binary'); s = new Buffer(s, 'binary'); if(!callback) { if(crypto.pbkdf2Sync.length === 4) { diff --git a/js/util.js b/js/util.js index f1e73b463..612976443 100644 --- a/js/util.js +++ b/js/util.js @@ -120,6 +120,21 @@ util.isArrayBufferView = function(x) { return x && util.isArrayBuffer(x.buffer) && x.byteLength !== undefined; }; +/** + * Ensure a bits param is 8, 16, 24, or 32. Used to validate input for + * algorithms where bit manipulation, JavaScript limitations, and/or algorithm + * design only allow for byte operations of a limited size. + * + * @param n number of bits. + * + * Throw Error if n invalid. + */ +function _checkBitsParam(n) { + if(!(n === 8 || n === 16 || n === 24 || n === 32)) { + throw new Error('Only 8, 16, 24, or 32 bits supported: ' + n); + } +} + // TODO: set ByteBuffer to best available backing util.ByteBuffer = ByteStringBuffer; @@ -351,11 +366,12 @@ util.ByteStringBuffer.prototype.putInt32Le = function(i) { * Puts an n-bit integer in this buffer in big-endian order. * * @param i the n-bit integer. - * @param n the number of bits in the integer. + * @param n the number of bits in the integer (8, 16, 24, or 32). * * @return this buffer. */ util.ByteStringBuffer.prototype.putInt = function(i, n) { + _checkBitsParam(n); var bytes = ''; do { n -= 8; @@ -369,11 +385,12 @@ util.ByteStringBuffer.prototype.putInt = function(i, n) { * complement representation is used. * * @param i the n-bit integer. - * @param n the number of bits in the integer. + * @param n the number of bits in the integer (8, 16, 24, or 32). * * @return this buffer. */ util.ByteStringBuffer.prototype.putSignedInt = function(i, n) { + // putInt checks n if(i < 0) { i += 2 << (n - 1); } @@ -492,15 +509,17 @@ util.ByteStringBuffer.prototype.getInt32Le = function() { /** * Gets an n-bit integer from this buffer in big-endian order and advances the - * read pointer by n/8. + * read pointer by ceil(n/8). * - * @param n the number of bits in the integer. + * @param n the number of bits in the integer (8, 16, 24, or 32). * * @return the integer. */ util.ByteStringBuffer.prototype.getInt = function(n) { + _checkBitsParam(n); var rval = 0; do { + // TODO: Use (rval * 0x100) if adding support for 33 to 53 bits. rval = (rval << 8) + this.data.charCodeAt(this.read++); n -= 8; } while(n > 0); @@ -511,11 +530,12 @@ util.ByteStringBuffer.prototype.getInt = function(n) { * Gets a signed n-bit integer from this buffer in big-endian order, using * two's complement, and advances the read pointer by n/8. * - * @param n the number of bits in the integer. + * @param n the number of bits in the integer (8, 16, 24, or 32). * * @return the integer. */ util.ByteStringBuffer.prototype.getSignedInt = function(n) { + // getInt checks n var x = this.getInt(n); var max = 2 << (n - 2); if(x >= max) { @@ -1032,11 +1052,12 @@ util.DataBuffer.prototype.putInt32Le = function(i) { * Puts an n-bit integer in this buffer in big-endian order. * * @param i the n-bit integer. - * @param n the number of bits in the integer. + * @param n the number of bits in the integer (8, 16, 24, or 32). * * @return this buffer. */ util.DataBuffer.prototype.putInt = function(i, n) { + _checkBitsParam(n); this.accommodate(n / 8); do { n -= 8; @@ -1055,6 +1076,7 @@ util.DataBuffer.prototype.putInt = function(i, n) { * @return this buffer. */ util.DataBuffer.prototype.putSignedInt = function(i, n) { + _checkBitsParam(n); this.accommodate(n / 8); if(i < 0) { i += 2 << (n - 1); @@ -1151,13 +1173,15 @@ util.DataBuffer.prototype.getInt32Le = function() { * Gets an n-bit integer from this buffer in big-endian order and advances the * read pointer by n/8. * - * @param n the number of bits in the integer. + * @param n the number of bits in the integer (8, 16, 24, or 32). * * @return the integer. */ util.DataBuffer.prototype.getInt = function(n) { + _checkBitsParam(n); var rval = 0; do { + // TODO: Use (rval * 0x100) if adding support for 33 to 53 bits. rval = (rval << 8) + this.data.getInt8(this.read++); n -= 8; } while(n > 0); @@ -1168,11 +1192,12 @@ util.DataBuffer.prototype.getInt = function(n) { * Gets a signed n-bit integer from this buffer in big-endian order, using * two's complement, and advances the read pointer by n/8. * - * @param n the number of bits in the integer. + * @param n the number of bits in the integer (8, 16, 24, or 32). * * @return the integer. */ util.DataBuffer.prototype.getSignedInt = function(n) { + // getInt checks n var x = this.getInt(n); var max = 2 << (n - 2); if(x >= max) { diff --git a/js/x509.js b/js/x509.js index 1f161387c..be90819b1 100644 --- a/js/x509.js +++ b/js/x509.js @@ -257,7 +257,8 @@ var x509CertificateValidator = { tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.BITSTRING, constructed: false, - capture: 'certIssuerUniqueId' + // TODO: support arbitrary bit length ids + captureBitStringValue: 'certIssuerUniqueId' }] }, { // subjectUniqueID (optional) @@ -271,7 +272,8 @@ var x509CertificateValidator = { tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.BITSTRING, constructed: false, - capture: 'certSubjectUniqueId' + // TODO: support arbitrary bit length ids + captureBitStringValue: 'certSubjectUniqueId' }] }, { // Extensions (optional) @@ -307,7 +309,7 @@ var x509CertificateValidator = { tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.BITSTRING, constructed: false, - capture: 'certSignature' + captureBitStringValue: 'certSignature' }] }; @@ -478,7 +480,7 @@ var certificationRequestValidator = { tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.BITSTRING, constructed: false, - capture: 'csrSignature' + captureBitStringValue: 'csrSignature' }] }; @@ -947,6 +949,7 @@ pki.createCertificate = function() { cert.subject.attributes = attrs; delete cert.subject.uniqueId; if(uniqueId) { + // TODO: support arbitrary bit length ids cert.subject.uniqueId = uniqueId; } cert.subject.hash = null; @@ -964,6 +967,7 @@ pki.createCertificate = function() { cert.issuer.attributes = attrs; delete cert.issuer.uniqueId; if(uniqueId) { + // TODO: support arbitrary bit length ids cert.issuer.uniqueId = uniqueId; } cert.issuer.hash = null; @@ -1264,18 +1268,9 @@ pki.certificateFromAsn1 = function(obj, computeHash) { throw error; } - // ensure signature is not interpreted as an embedded ASN.1 object - if(typeof capture.certSignature !== 'string') { - var certSignature = '\x00'; - for(var i = 0; i < capture.certSignature.length; ++i) { - certSignature += asn1.toDer(capture.certSignature[i]).getBytes(); - } - capture.certSignature = certSignature; - } - // get oid var oid = asn1.derToOid(capture.publicKeyOid); - if(oid !== pki.oids['rsaEncryption']) { + if(oid !== pki.oids.rsaEncryption) { throw new Error('Cannot read public key. OID is not RSA.'); } @@ -1291,10 +1286,7 @@ pki.certificateFromAsn1 = function(obj, computeHash) { cert.siginfo.algorithmOid = forge.asn1.derToOid(capture.certinfoSignatureOid); cert.siginfo.parameters = _readSignatureParameters(cert.siginfo.algorithmOid, capture.certinfoSignatureParams, false); - // skip "unused bits" in signature value BITSTRING - var signature = forge.util.createBuffer(capture.certSignature); - ++signature.read; - cert.signature = signature.getBytes(); + cert.signature = capture.certSignature; var validity = []; if(capture.certValidity1UTCTime !== undefined) { @@ -1646,15 +1638,6 @@ pki.certificationRequestFromAsn1 = function(obj, computeHash) { throw error; } - // ensure signature is not interpreted as an embedded ASN.1 object - if(typeof capture.csrSignature !== 'string') { - var csrSignature = '\x00'; - for(var i = 0; i < capture.csrSignature.length; ++i) { - csrSignature += asn1.toDer(capture.csrSignature[i]).getBytes(); - } - capture.csrSignature = csrSignature; - } - // get oid var oid = asn1.derToOid(capture.publicKeyOid); if(oid !== pki.oids.rsaEncryption) { @@ -1670,10 +1653,7 @@ pki.certificationRequestFromAsn1 = function(obj, computeHash) { csr.siginfo.algorithmOid = forge.asn1.derToOid(capture.csrSignatureOid); csr.siginfo.parameters = _readSignatureParameters( csr.siginfo.algorithmOid, capture.csrSignatureParams, false); - // skip "unused bits" in signature value BITSTRING - var signature = forge.util.createBuffer(capture.csrSignature); - ++signature.read; - csr.signature = signature.getBytes(); + csr.signature = capture.csrSignature; // keep CertificationRequestInfo to preserve signature when exporting csr.certificationRequestInfo = capture.certificationRequestInfo; @@ -2515,6 +2495,7 @@ pki.getTBSCertificate = function(cert) { tbs.value.push( asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ asn1.create(asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, + // TODO: support arbitrary bit length ids String.fromCharCode(0x00) + cert.issuer.uniqueId ) @@ -2526,6 +2507,7 @@ pki.getTBSCertificate = function(cert) { tbs.value.push( asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ asn1.create(asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, + // TODO: support arbitrary bit length ids String.fromCharCode(0x00) + cert.subject.uniqueId ) diff --git a/nodejs/test/asn1.js b/nodejs/test/asn1.js index f5febc64f..e251775f9 100644 --- a/nodejs/test/asn1.js +++ b/nodejs/test/asn1.js @@ -268,6 +268,1028 @@ function Tests(ASSERT, ASN1, UTIL) { }); } })(); + + (function() { + // use function to avoid calling apis during setup + function _asn1(str) { + return function() { + return ASN1.fromDer(UTIL.hexToBytes(str.replace(/ /g, ''))); + }; + } + var tests = [{ + name: 'empty strings', + obj1: '', + obj2: '', + equal: true + }, { + name: 'simple strings', + obj1: '\u0001', + obj2: '\u0001', + equal: true + }, { + name: 'simple strings', + obj1: '\u0000', + obj2: '\u0001', + equal: false + }, { + name: 'simple arrays', + obj1: ['', ''], + obj2: ['', ''], + equal: true + }, { + name: 'simple arrays', + obj1: ['', ''], + obj2: [''], + equal: false + }, { + name: 'INTEGERs', + obj1: _asn1('02 01 00'), + obj2: _asn1('02 01 00'), + equal: true + }, { + name: 'BER INTEGERs', + obj1: _asn1('02 01 01'), + obj2: _asn1('02 02 00 01'), + equal: false + }, { + name: 'BIT STRINGs', + obj1: _asn1('03 02 00 01'), + obj2: _asn1('03 02 00 01'), + equal: true + }, { + name: 'BIT STRINGs', + obj1: _asn1('03 02 00 01'), + obj2: _asn1('03 02 00 02'), + equal: false + }, { + name: 'BIT STRINGs sub INTEGER', + obj1: _asn1('03 04 00 02 01 01'), + obj2: _asn1('03 04 00 02 01 01'), + equal: true + }, { + name: 'mutated BIT STRINGs', + obj1: _asn1('03 04 00 02 01 01'), + obj2: _asn1('03 04 00 02 01 01'), + mutate: function(obj1, obj2) { + obj2.value[0].value = '\u0002'; + }, + equal: false + }]; + tests.forEach(function(test, index) { + var name = 'should check ASN.1 ' + + (test.equal ? '' : 'not ') + 'equal: ' + + (test.name || '#' + index); + it(name, function() { + var obj1 = typeof test.obj1 === 'function' ? test.obj1() : test.obj1; + var obj2 = typeof test.obj2 === 'function' ? test.obj2() : test.obj2; + if(test.mutate) { + test.mutate(obj1, obj2); + } + ASSERT.equal(ASN1.equals(obj1, obj2), test.equal); + }); + }); + })(); + + (function() { + // use function to avoid calling apis during setup + function _asn1(str) { + return function() { + return ASN1.fromDer(UTIL.hexToBytes(str.replace(/ /g, ''))); + }; + } + var tests = [{ + name: 'empty string', + obj: '' + }, { + name: 'simple string', + obj: '\u0001' + }, { + name: 'simple array', + obj: ['', ''] + }, { + name: 'INTEGER', + obj: _asn1('02 01 00') + }, { + name: 'BER INTEGER', + obj: _asn1('02 01 01') + }, { + name: 'BIT STRING', + obj: _asn1('03 02 00 01') + }, { + name: 'BIT STRING sub INTEGER', + obj: _asn1('03 04 00 02 01 01') + }]; + tests.forEach(function(test, index) { + var name = 'should check ASN.1 copy: ' + (test.name || '#' + index); + it(name, function() { + var obj = typeof test.obj === 'function' ? test.obj() : test.obj; + ASSERT.equal(ASN1.equals(ASN1.copy(obj), obj), true); + }); + }); + })(); + + (function() { + function _h2b(str) { + return UTIL.hexToBytes(str.replace(/ /g, '')); + } + function _add(b, str) { + b.putBytes(_h2b(str)); + } + function _asn1dump(asn1) { + console.log(ASN1.prettyPrint(asn1)); + console.log(JSON.stringify(asn1, null, 2)); + } + function _asn1TestOne(strict, throws, options) { + options = options || {}; + if(!('decodeBitStrings' in options)) { + options.decodeBitStrings = true; + } + // buffer strict test + var b = UTIL.createBuffer(); + // init + options.init(b); + // bytes for round-trip comparison + var bytes = b.copy().bytes(); + // copy for non-strict test + var bns = b.copy(); + // create strict and non-strict asn1 + var asn1assert = throws ? ASSERT.throws : function(f) { f(); }; + var asn1; + var der; + asn1assert(function() { + asn1 = ASN1.fromDer(b, { + strict: strict, + decodeBitStrings: options.decodeBitStrings + }); + }); + // debug + if(options.dump && asn1) { + console.log('=== ' + (strict ? 'Strict' : 'Non Strict') + ' ==='); + _asn1dump(asn1); + } + // basic check + if(!throws) { + ASSERT.ok(asn1); + } + + // round-trip(ish) check + if(!throws) { + der = ASN1.toDer(asn1); + if(options.roundtrip) { + // byte comparisons for round-trip testing can fail due to + // symantically safe changes such as changing the length encoding. + // test a roundtrip for data where it makes sense. + ASSERT.equal( + UTIL.bytesToHex(bytes), + UTIL.bytesToHex(der.bytes())); + } + } + + // validator check + if(!throws && options.v) { + var capture = {}; + var errors = [] + var asn1ok = ASN1.validate(asn1, options.v, capture, errors); + ASSERT.deepEqual(errors, []); + if(options.captured) { + ASSERT.deepEqual(capture, options.captured); + } else { + ASSERT.deepEqual(capture, {}); + } + ASSERT.ok(asn1ok); + } + + return { + asn1: asn1, + der: der + }; + } + function _asn1Test(options) { + var s = _asn1TestOne(true, options.strictThrows, options); + var ns = _asn1TestOne(false, options.nonStrictThrows, options); + + // check asn1 equality + if(s.asn1 && ns.asn1) { + ASSERT.deepEqual(s.asn1, ns.asn1); + } + + // check der equality + if(s.der && ns.der) { + ASSERT.equal( + UTIL.bytesToHex(s.der.bytes()), + UTIL.bytesToHex(ns.der.bytes())); + } + + if(options.done) { + options.done({ + strict: s, + nonStrict: ns + }); + } + } + + it('should convert BIT STRING from DER (short,empty)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING value=none + _add(b, '03 00'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits' + }, + captured: { + bits: '' + } + }); + }); + + it('should convert BIT STRING from DER (short,empty2)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING value=none + _add(b, '03 01 00'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bitsC', + captureBitStringValue: 'bitsV' + }, + captured: { + bitsC: _h2b('00'), + bitsV: '' + } + }); + }); + + it('should convert BIT STRING from BER (short,invalid)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING value=partial + // invalid in strict mode, non-strict will read 1 of 2 bytes + _add(b, '03 02 00'); + }, + dump: false, + roundtrip: false, + strictThrows: true, + nonStrictThrows: false, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits' + }, + captured: { + // only for non-strict mode, truncated value + bits: _h2b('00') + } + }); + }); + + it('should convert BIT STRING from DER (short)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING value=0110111001011101 + _add(b, '03 03 00 6e 5d'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits' + }, + captured: { + bits: _h2b('00 6e 5d') + } + }); + }); + + it('should convert BIT STRING from DER (short2)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING value=0110111001011101 + // contains an INTEGER=0x12 + _add(b, '03 04 00 02 01 12'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + // note captureBitStringContents used to get all bytes + // 'capture' would get the value structure + // 'captureAsn1' would get the value and sub-value structures + captureBitStringContents: 'bits', + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int0' + }] + }, + captured: { + bits: _h2b('00 02 01 12'), + int0: _h2b('12') + } + }); + }); + + it('should convert BIT STRING from DER (short,unused1z)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING value=01101110010111011010111, unused=0 + _add(b, '03 04 01 6e 5d ae'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits' + }, + captured: { + bits: _h2b('01 6e 5d ae') + } + }); + }); + + it('should convert BIT STRING from DER (short,unused6z)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING short len, value=011011100101110111, unused=000000 + _add(b, '03 04 06 6e 5d c0'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits' + }, + captured: { + bits: _h2b('06 6e 5d c0') + } + }); + }); + + it('should convert BIT STRING from BER (short,unused6d)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING short len, value=011011100101110111, unused=100000 + _add(b, '03 04 06 6e 5d e0'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits' + }, + captured: { + bits: _h2b('06 6e 5d e0') + } + }); + }); + + it('should convert BIT STRING from BER (long,unused6z)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING long len, value=011011100101110111, unused=000000 + _add(b, '03 81 04 06 6e 5d c0'); + }, + dump: false, + // length is compressed + roundtrip: false, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits' + }, + captured: { + bits: _h2b('06 6e 5d c0') + } + }); + }); + + it('should convert BIT STRING from BER (unused6z)', function() { + _asn1Test({ + init: function(b) { + // BIT STRING constructed, value=0110111001011101+11, unused=000000 + _add(b, '23 09'); + _add(b, '03 03 00 6e 5d'); + _add(b, '03 02 06 c0'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: true, + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + capture: 'bits0' + }, { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + capture: 'bits1' + }] + }, + captured: { + bits0: _h2b('00 6e 5d'), + bits1: _h2b('06 c0') + } + }); + }); + + it('should convert BIT STRING from BER (decode)', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data that includes encapsulated + // data. often used to store SEQUENCE of INTEGERs. + // add bit stream of bytes using long length + _add(b, '03 82 00 10'); + // no padding + _add(b, '00'); + // sequence of two ints + _add(b, '30 0D'); + // add test int, long len + _add(b, '02 81 04 12 34 56 78'); + // add test int, short len + _add(b, '02 04 87 65 43 21'); + }, + dump: false, + decodeBitStrings: true, + // long len will compress to short len + roundtrip: false, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits', + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.SEQUENCE, + constructed: true, + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int0' + }, { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int1' + }] + }] + }, + captured: { + bits: _h2b( + '00' + + '30 0D' + + '02 81 04 12 34 56 78' + + '02 04 87 65 43 21'), + int0: _h2b('12 34 56 78'), + int1: _h2b('87 65 43 21') + } + }); + }); + + it('should convert BIT STRING from BER (no decode)', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data that includes encapsulated + // data. often used to store SEQUENCE of INTEGERs. + // add bit stream + _add(b, '03 82 00 10'); + // no padding + _add(b, '00'); + // sequence of two ints + _add(b, '30 0D'); + // add test int, long len + _add(b, '02 81 04 12 34 56 78'); + // add test int, short len + _add(b, '02 04 87 65 43 21'); + }, + dump: false, + decodeBitStrings: false, + // long length is compressed + roundtrip: false, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits' + }, + captured: { + bits: _h2b( + '00' + + '30 0D' + + '02 81 04 12 34 56 78' + + '02 04 87 65 43 21') + } + }); + }); + + it('should convert BIT STRING from DER (decode2)', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data that includes encapsulated + // data. often used to store SEQUENCE of INTEGERs. + // bit stream + _add(b, '03 81 8D'); + // no padding + _add(b, '00'); + // sequence + _add(b, '30 81 89'); + // int header and leading 0 + _add(b, '02 81 81 00'); + // 1024 bit int + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F1'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F2'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F3'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F4'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F5'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F6'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F7'); + // int header and 3 byte int + _add(b, '02 03 01 00 01'); + }, + dump: false, + decodeBitStrings: true, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.SEQUENCE, + constructed: true, + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int0' + }, { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int1' + }] + }] + }, + captured: { + int0: _h2b('00' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F1' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F2' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F3' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F4' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F5' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F6' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F7'), + int1: _h2b('01 00 01') + } + }); + }); + + it('should convert BIT STRING from DER (sig)', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data similar to a signature that + // could be interpreted incorrectly as encapsulated data. + // add bit stream of 257 bytes + _add(b, '03 82 01 01'); + // no unused + _add(b, '00'); + // signature bits + _add(b, '25 81 FD 6E D3 AB 34 45 DE AE F1 5B EC 6A FB 79'); + _add(b, '14 CD 7B B2 8E 48 59 AE 89 B1 55 60 11 AB BC 7F'); + _add(b, '6D 6D FE 16 22 42 AC 57 CC E9 C0 3A 8D 1E F3 C3'); + _add(b, '97 C8 23 53 DE E0 34 C3 A9 43 8B 2B D9 C0 24 FF'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F4'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F5'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F6'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F7'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F8'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F9'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FA'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FB'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FE'); + _add(b, 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + // captureBitStringContents not used to check if valude decoded + capture: 'sig' + }, + captured: { + sig: _h2b( + '00' + + '25 81 FD 6E D3 AB 34 45 DE AE F1 5B EC 6A FB 79' + + '14 CD 7B B2 8E 48 59 AE 89 B1 55 60 11 AB BC 7F' + + '6D 6D FE 16 22 42 AC 57 CC E9 C0 3A 8D 1E F3 C3' + + '97 C8 23 53 DE E0 34 C3 A9 43 8B 2B D9 C0 24 FF' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F4' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F5' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F6' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F7' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F8' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F9' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FA' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FB' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FE' + + 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF') + } + }); + }); + + it('should convert BIT STRING from DER (sig2)', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data similar to a signature that + // could be interpreted incorrectly as encapsulated data. + // add bit stream of 257 bytes + _add(b, '03 82 01 01'); + // no unused + _add(b, '00'); + // signature bits + _add(b, '2B 05 9D 81 FB 07 2C CE 15 0A 39 CD D3 89 A7 83'); + _add(b, '5C 99 5E B2 0D A4 E0 26 81 20 EF 5A 0F 23 46 E0'); + _add(b, '46 4A 5D 7B 6A C9 4F B1 38 D5 FC 71 6A 32 06 6C'); + _add(b, '68 15 9E F2 13 DB 2A 36 41 93 51 4C 98 EB 9F 32'); + _add(b, '28 54 07 CE B2 05 92 A7 C8 DF 2F A1 E3 C9 9C 0A'); + _add(b, 'E4 BE B3 88 17 CF 62 70 80 CD 10 B8 9B 08 E0 47'); + _add(b, '61 24 12 16 C0 FC 70 D9 0A 4A 39 09 F4 51 F1 62'); + _add(b, '0A 56 6B 46 C1 E2 0B FF 92 3E F5 A5 06 EE 55 0A'); + _add(b, '6D FD DA 18 B9 C1 30 6E 98 CD 38 4D 9C C5 B5 6B'); + _add(b, '81 19 B7 B1 19 52 5C F8 99 9D C2 EC A1 F5 96 A7'); + _add(b, '66 79 A6 53 F8 17 67 64 52 F6 32 37 F4 CD 74 5A'); + _add(b, '2F 59 35 06 90 6B CC F7 E6 7D 67 C4 FA 0C 7B 10'); + _add(b, '05 85 E8 4F E2 0E EF A0 D4 F8 57 EB BF 2F 14 42'); + _add(b, '62 01 09 08 35 5C 24 8C 0D 5D FD FA 52 58 D8 C9'); + _add(b, '10 45 4F AE 15 B0 9A 82 B9 FB 17 CC E6 A0 BD BA'); + _add(b, '76 BD 05 F1 70 69 43 9D 60 31 F9 F4 13 7A 8C 71'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + // captureBitStringContents not used to check if valude decoded + captureBitStringValue: 'bits' + }, + captured: { + bits: _h2b( + '2B 05 9D 81 FB 07 2C CE 15 0A 39 CD D3 89 A7 83' + + '5C 99 5E B2 0D A4 E0 26 81 20 EF 5A 0F 23 46 E0' + + '46 4A 5D 7B 6A C9 4F B1 38 D5 FC 71 6A 32 06 6C' + + '68 15 9E F2 13 DB 2A 36 41 93 51 4C 98 EB 9F 32' + + '28 54 07 CE B2 05 92 A7 C8 DF 2F A1 E3 C9 9C 0A' + + 'E4 BE B3 88 17 CF 62 70 80 CD 10 B8 9B 08 E0 47' + + '61 24 12 16 C0 FC 70 D9 0A 4A 39 09 F4 51 F1 62' + + '0A 56 6B 46 C1 E2 0B FF 92 3E F5 A5 06 EE 55 0A' + + '6D FD DA 18 B9 C1 30 6E 98 CD 38 4D 9C C5 B5 6B' + + '81 19 B7 B1 19 52 5C F8 99 9D C2 EC A1 F5 96 A7' + + '66 79 A6 53 F8 17 67 64 52 F6 32 37 F4 CD 74 5A' + + '2F 59 35 06 90 6B CC F7 E6 7D 67 C4 FA 0C 7B 10' + + '05 85 E8 4F E2 0E EF A0 D4 F8 57 EB BF 2F 14 42' + + '62 01 09 08 35 5C 24 8C 0D 5D FD FA 52 58 D8 C9' + + '10 45 4F AE 15 B0 9A 82 B9 FB 17 CC E6 A0 BD BA' + + '76 BD 05 F1 70 69 43 9D 60 31 F9 F4 13 7A 8C 71') + } + }); + }); + + it('should convert BIT STRING from DER (sig3)', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data similar to a signature that + // could be interpreted incorrectly as encapsulated data. + _add(b, '03 0B'); + // no unused + _add(b, '00'); + // signature bits with structure with bad type and length + _add(b, '2B 05 9D 05 F0 F1 F2 F3 F4 F5'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + // captureBitStringContents not used to check if value decoded + capture: 'sig' + }, + captured: { + sig: _h2b( + '00' + + '2B 05 9D 05 F0 F1 F2 F3 F4 F5') + } + }); + }); + + it('should convert BIT STRING from BER (decodable sig)', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data similar to a signature that + // could be interpreted as encapsulated data. data is such that + // a round trip process could change the data due to INTEGER + // optimization (removal of leading bytes) or length structure + // compression (long to short). + // add a basic bit stream "signature" with test data + _add(b, '03 22'); + // no unused + _add(b, '00'); + // everything after this point might be important bits, not ASN.1 + // SEQUENCE of tests + _add(b, '30 1E'); + // signature bits + // '02 02' prefix will be cause parsing as an integer + // toDer will try to remove the extra 00. + // tests the BIT STRING content/value saving feature + _add(b, '02 02 00 7F'); + // similar example for -1: + _add(b, '02 02 FF FF'); + // could extend out to any structure size: + _add(b, '02 06 FF FF FF FF FF FF'); + // the roundtrip issue can exist for long lengths that could + // compress to short lenghts, this could be output as '02 02 01 23': + _add(b, '02 81 02 01 23'); + // also an issue for indefinite length structures that will + // have a known length later: + _add(b, '30 80'); + // a few INTEGERs + _add(b, '02 01 00'); + _add(b, '02 01 01'); + // done + _add(b, '00 00'); + // other examples may exist + }, + dump: false, + // NOTE: arbitrary data can be recompacted, check saved data worked + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringValue: 'sig' + }, + captured: { + sig: _h2b( + '30 1E' + + '02 02 00 7F' + + '02 02 FF FF' + + '02 06 FF FF FF FF FF FF' + + '02 81 02 01 23' + + '30 80' + + '02 01 00' + + '02 01 01' + + '00 00') + } + }); + }); + + it('should convert BIT STRING from strict DER', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data that includes encapsulated + // data. include valid data that would only parse in strict + // mode. + _add(b, '03 06'); + // no padding + _add(b, '00'); + // sub-BIT STRING with valid length + _add(b, '03 03 00 01 02'); + }, + dump: false, + decodeBitStrings: true, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + // default capture value has structural data + // check contents and value + captureBitStringContents: 'bitsC', + captureBitStringValue: 'bitsV' + }, + captured: { + bitsC: _h2b('00 03 03 00 01 02'), + bitsV: _h2b('03 03 00 01 02') + } + }); + }); + it('should convert BIT STRING from non-strict DER', function() { + _asn1Test({ + init: function(b) { + // create crafted DER BIT STRING data that includes encapsulated + // data. include invalid data that would only parse in non-strict + // mode. ensure it is never parsed. + _add(b, '03 05'); + // no padding + _add(b, '00'); + // sub-BIT STRING with invalid length, missing a byte + _add(b, '03 03 00 01'); + }, + dump: false, + decodeBitStrings: true, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + // ensure default captures contents vs decoded structure + capture: 'bits0', + // check contents and value + captureBitStringContents: 'bitsC', + captureBitStringValue: 'bitsV' + }, + captured: { + bits0: _h2b('00 03 03 00 01'), + bitsC: _h2b('00 03 03 00 01'), + bitsV: _h2b('03 03 00 01') + } + }); + }); + + it('should convert indefinite length seq from BER', function() { + _asn1Test({ + init: function(b) { + // SEQUENCE + _add(b, '30 80'); + // a few INTEGERs + _add(b, '02 01 00'); + _add(b, '02 01 01'); + _add(b, '02 01 02'); + // done + _add(b, '00 00'); + }, + dump: false, + // roundtrip will know the sequence length + roundtrip: false, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.SEQUENCE, + constructed: true, + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int0' + }, { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int1' + }, { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int2' + }] + }, + captured: { + int0: _h2b('00'), + int1: _h2b('01'), + int2: _h2b('02') + } + }); + }); + + it('should handle ASN.1 mutations', function() { + _asn1Test({ + init: function(b) { + // BIT STRING + _add(b, '03 09 00'); + // SEQUENCE + _add(b, '30 06'); + // a few INTEGERs + _add(b, '02 01 00'); + _add(b, '02 01 01'); + }, + dump: false, + // roundtrip will know the sequence length + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BITSTRING, + constructed: false, + captureBitStringContents: 'bits', + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.SEQUENCE, + constructed: true, + value: [{ + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int0' + }, { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.INTEGER, + constructed: false, + capture: 'int1' + }] + }] + }, + captured: { + bits: _h2b('00 30 06 02 01 00 02 01 01'), + int0: _h2b('00'), + int1: _h2b('01') + }, + done: function(data) { + var asn1 = data.strict.asn1; + // mutate + asn1.value[0].value[0].value = _h2b('02'); + asn1.value[0].value[1].value = _h2b('03'); + // convert + // must use new data vs saved BIT STRING data + var der = ASN1.toDer(asn1); + var expect = _h2b('03 09 00 30 06 02 01 02 02 01 03'); + // compare + ASSERT.equal(UTIL.bytesToHex(der), UTIL.bytesToHex(expect)); + } + }); + }); + + it('should convert BMP STRING from DER', function() { + _asn1Test({ + init: function(b) { + // BPMSTRING + _add(b, '1e 08'); + _add(b, '01 02 03 04 05 06 07 08'); + }, + dump: false, + roundtrip: true, + v: { + tagClass: ASN1.Class.UNIVERSAL, + type: ASN1.Type.BPMSTRING, + constructed: false, + capture: 'bits' + }, + captured: { + bits: '\u0102\u0304\u0506\u0708' + } + }); + }); + + // TODO: how minimal should INTEGERs be encoded? + // .. fromDer will create the full integer + // .. toDer will remove only first byte if possible + it('should minimally encode INTEGERs', function() { + function _test(hin, hout) { + var derIn = _h2b(hin); + var derOut = ASN1.toDer(ASN1.fromDer(derIn)); + ASSERT.equal( + UTIL.bytesToHex(derOut), + UTIL.bytesToHex(_h2b(hout))); + } + // optimial + _test('02 01 01', '02 01 01'); + _test('02 01 FF', '02 01 FF'); + _test('02 02 00 FF', '02 02 00 FF'); + + // remove leading 00s before a 0b0xxxxxxx + _test('02 04 00 00 00 01', '02 03 00 00 01'); + // this would be more optimal + //_test('02 04 00 00 00 01', '02 01 01'); + + // remove leading FFs before a 0b1xxxxxxx + _test('02 04 FF FF FF FF', '02 03 FF FF FF'); + // this would be more optimal + //_test('02 04 FF FF FF FF', '02 01 FF'); + }); + })(); }); } diff --git a/nodejs/test/pbkdf2.js b/nodejs/test/pbkdf2.js index 90e36f0ba..d307457cf 100644 --- a/nodejs/test/pbkdf2.js +++ b/nodejs/test/pbkdf2.js @@ -18,6 +18,11 @@ function Tests(ASSERT, PBKDF2, MD, UTIL) { ASSERT.equal(dkHex, 'd1daa78615f287e6'); }); + it('should derive a utf8 password with hmac-sha-1 c=1 keylen=16', function() { + var dkHex = UTIL.bytesToHex(PBKDF2('δΈ­', 'salt', 1, 16)); + ASSERT.equal(dkHex, '5f719aa196edc4df6b1556de503faaf3'); + }); + it('should derive a password with hmac-sha-1 c=4096', function() { // Note: might be too slow on old browsers var dkHex = UTIL.bytesToHex(PBKDF2('password', 'salt', 4096, 20)); diff --git a/nodejs/test/util.js b/nodejs/test/util.js index ecf6a34ab..5e88676d6 100644 --- a/nodejs/test/util.js +++ b/nodejs/test/util.js @@ -109,7 +109,7 @@ function Tests(ASSERT, UTIL) { var n = 8; var b = UTIL.createBuffer(); b.putSignedInt(x, n); - ASSERT.equal(b.getSignedInt(x, n), x); + ASSERT.equal(b.getSignedInt(n), x); }); it('should get 127 from a buffer using two\'s complement', function() { @@ -152,6 +152,75 @@ function Tests(ASSERT, UTIL) { ASSERT.equal(b.getSignedInt(n), x); }); + it('should getInt(8) from buffer', function() { + var b = UTIL.createBuffer(UTIL.hexToBytes('12')); + ASSERT.equal(b.getInt(8), 0x12); + ASSERT.equal(b.length(), 0); + }); + + it('should getInt(8)x2 from buffer', function() { + var b = UTIL.createBuffer(UTIL.hexToBytes('1221')); + ASSERT.equal(b.getInt(8), 0x12); + ASSERT.equal(b.getInt(8), 0x21); + ASSERT.equal(b.length(), 0); + }); + + it('should getInt(16) from buffer', function() { + var b = UTIL.createBuffer(UTIL.hexToBytes('1234')); + ASSERT.equal(b.getInt(16), 0x1234); + ASSERT.equal(b.length(), 0); + }); + + it('should getInt(16)x2 from buffer', function() { + var b = UTIL.createBuffer(UTIL.hexToBytes('12344321')); + ASSERT.equal(b.getInt(16), 0x1234); + ASSERT.equal(b.getInt(16), 0x4321); + ASSERT.equal(b.length(), 0); + }); + + it('should getInt(24) from buffer', function() { + var b = UTIL.createBuffer(UTIL.hexToBytes('123456')); + ASSERT.equal(b.getInt(24), 0x123456); + ASSERT.equal(b.length(), 0); + }); + + it('should getInt(24)x2 from buffer', function() { + var b = UTIL.createBuffer(UTIL.hexToBytes('123456654321')); + ASSERT.equal(b.getInt(24), 0x123456); + ASSERT.equal(b.getInt(24), 0x654321); + ASSERT.equal(b.length(), 0); + }); + + it('should getInt(32) from buffer', function() { + var b = UTIL.createBuffer(UTIL.hexToBytes('12345678')); + ASSERT.equal(b.getInt(32), 0x12345678); + ASSERT.equal(b.length(), 0); + }); + + it('should getInt(32)x2 from buffer', function() { + var b = UTIL.createBuffer(UTIL.hexToBytes('1234567887654321')); + ASSERT.equal(b.getInt(32), 0x12345678); + // FIXME: getInt bit shifts create signed int + ASSERT.equal(b.getInt(32), 0x87654321<<0); + ASSERT.equal(b.length(), 0); + }); + + it('should throw for getInt(1) from buffer', function() { + var b = UTIL.createBuffer(); + ASSERT.throws(function() { + b.getInt(1); + }); + }); + + it('should throw for getInt(33) from buffer', function() { + var b = UTIL.createBuffer(); + ASSERT.throws(function() { + b.getInt(33); + }); + }); + + // TODO: add get/put tests at limits of signed/unsigned types + it('should base64 encode some bytes', function() { var s1 = '00010203050607080A0B0C0D0F1011121415161719'; var s2 = 'MDAwMTAyMDMwNTA2MDcwODBBMEIwQzBEMEYxMDExMTIxNDE1MTYxNzE5'; diff --git a/nodejs/test/x509.js b/nodejs/test/x509.js index 57f4493f7..0b1394bb4 100644 --- a/nodejs/test/x509.js +++ b/nodejs/test/x509.js @@ -1,6 +1,6 @@ (function() { -function Tests(ASSERT, PKI, MD, UTIL) { +function Tests(ASSERT, ASN1, PKI, MD, UTIL) { var _pem = { privateKey: '-----BEGIN RSA PRIVATE KEY-----\r\n' + 'MIICXQIBAAKBgQDL0EugUiNGMWscLAVM0VoMdhDZEJOqdsUMpx9U0YZI7szokJqQ\r\n' + @@ -853,6 +853,44 @@ function Tests(ASSERT, PKI, MD, UTIL) { var issuer = PKI.certificateFromPem(issuerPem); ASSERT.ok(issuer.verify(cert)); }); + + it('should parse certificate with ASN.1 signature data', function() { + // signature has bytes that could look like ASN.1 data + // TODO: add more similar tests with possible ASN.1 data in BIT + // .. STRINGS that are just data. + var certPem = + '-----BEGIN CERTIFICATE-----\r\n' + + 'MIIDsjCCApqgAwIBAgIUR+JnrOX42d+AXaXwLDWYsxmr+DwwDQYJKoZIhvcNAQEL\r\n' + + 'BQAwgYgxJjAkBgNVBAMMHVRlc3QgSW50ZXJtZWRpYXRlIENlcnRpZmljYXRlMSEw\r\n' + + 'HwYDVQQLDBhUZXN0IE9yZ2FuaXphdGlvbmFsIFVuaXQxGjAYBgNVBAoMEVRlc3Qg\r\n' + + 'T3JnYW5pemF0aW9uMRIwEAYDVQQHDAlUZXN0IENpdHkxCzAJBgNVBAYTAlVTMB4X\r\n' + + 'DTE3MDEzMDE1NTk0MloXDTIyMDEzMDE1NTk0MlowezEZMBcGA1UEAwwQVGVzdCBD\r\n' + + 'ZXJ0aWZpY2F0ZTEhMB8GA1UECwwYVGVzdCBPcmdhbml6YXRpb25hbCBVbml0MRow\r\n' + + 'GAYDVQQKDBFUZXN0IE9yZ2FuaXphdGlvbjESMBAGA1UEBwwJVGVzdCBDaXR5MQsw\r\n' + + 'CQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMEBoBE9\r\n' + + 'Kprzj7LRbK0JHKSNI93eKFDgkRc1lQWVf2cVyU6bncBDdxrCFLG6vadpQzPQDCJs\r\n' + + 'ePlzM4J2TueTtbNTgLj3EBwwB1Ju9kOidxn5hCASWR0EObLiHSq3zlM/ABZOEOtz\r\n' + + 'hPTGWnp7jfoDeTgK/zNnCsqdT1w1nzsz1u8zZ+96dEul6RC313CxV5Xq+Qacky8f\r\n' + + '2tug0wgvmcYqfd6AIg0btIjhREsulJK0QqSjqmzkLiDBJRsOHzi9zAusYBkvqUMj\r\n' + + '9ae2j4adIHKNzzgRAif8Bu9yXZ0iK3im6BQhBiC+unwMRAjbHdPI73ASJFdqjHb0\r\n' + + 'uAkno0WIORHnQvECAwEAAaMgMB4wDgYDVR0PAQH/BAQDAgbAMAwGA1UdEwEB/wQC\r\n' + + 'MAAwDQYJKoZIhvcNAQELBQADggEBACsFnYH7ByzOFQo5zdOJp4NcmV6yDaTgJoEg\r\n' + + '71oPI0bgRkpde2rJT7E41fxxajIGbGgVnvIT2yo2QZNRTJjrnzIoVAfOsgWSp8jf\r\n' + + 'L6HjyZwK5L6ziBfPYnCAzRC4mwjgR2EkEhbA/HDZCko5CfRR8WIKVmtGweIL/5I+\r\n' + + '9aUG7lUKbf3aGLnBMG6YzThNnMW1a4EZt7EZUlz4mZ3C7KH1lqdmeaZT+BdnZFL2\r\n' + + 'Mjf0zXRaL1k1BpBrzPfmfWfE+gx7EAWF6E/iDu+g1PhX678vFEJiAQkINVwkjA1d\r\n' + + '/fpSWNjJEEVPrhWwmoK5+xfM5qC9una9BfFwaUOdYDH59BN6jHE=\r\n' + + '-----END CERTIFICATE-----\r\n'; + // round trip pem -> cert -> asn1 -> der -> asn1 -> cert -> pem + var inCert = PKI.certificateFromPem(certPem); + var inAsn1 = PKI.certificateToAsn1(inCert); + var inDer = ASN1.toDer(inAsn1); + var outAsn1 = ASN1.fromDer(inDer); + var outCert = PKI.certificateFromAsn1(outAsn1); + var outPem = PKI.certificateToPem(outCert); + ASSERT.equal(certPem, outPem); + }); + }); // TODO: add sha-512 and sha-256 fingerprint tests @@ -1222,13 +1260,15 @@ function Tests(ASSERT, PKI, MD, UTIL) { // check for AMD if(typeof define === 'function') { define([ + 'forge/asn1', 'forge/pki', 'forge/md', 'forge/util' - ], function(PKI, MD, UTIL) { + ], function(ASN1, PKI, MD, UTIL) { Tests( // Global provided by test harness ASSERT, + ASN1(), PKI(), MD(), UTIL() @@ -1238,6 +1278,7 @@ if(typeof define === 'function') { // assume NodeJS Tests( require('assert'), + require('../../js/asn1')(), require('../../js/pki')(), require('../../js/md')(), require('../../js/util')()); diff --git a/package.json b/package.json index 860afa8e5..3e2a356e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-forge", - "version": "0.6.47-dev", + "version": "0.6.50-dev", "description": "JavaScript implementations of network transports, cryptography, ciphers, PKI, message digests, and various utilities.", "homepage": "https://github.com/digitalbazaar/forge", "author": { @@ -37,6 +37,13 @@ }, "license": "(BSD-3-Clause OR GPL-2.0)", "main": "js/forge.js", + "files": [ + "js/*.js", + "swf/*.swf", + "minify.js", + "start.frag", + "end.frag" + ], "engines": { "node": "*" },