From ade2627bca62a7dcbfc56a9e31931e582205ddf0 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 25 May 2018 19:24:33 +0200 Subject: [PATCH] Streaming verify one-pass signatures --- src/crypto/hash/index.js | 3 +- src/crypto/pkcs1.js | 7 +- src/crypto/public_key/dsa.js | 16 +-- src/crypto/public_key/elliptic/ecdsa.js | 8 +- src/crypto/public_key/elliptic/eddsa.js | 8 +- src/crypto/public_key/elliptic/key.js | 58 +++++---- src/crypto/signature.js | 25 ++-- src/message.js | 37 +++++- src/openpgp.js | 4 +- src/packet/compressed.js | 7 +- src/packet/one_pass_signature.js | 33 +++-- src/packet/packetlist.js | 3 +- src/packet/signature.js | 157 +++++++++++++----------- src/stream.js | 48 ++++++-- src/util.js | 18 +-- test/crypto/crypto.js | 8 +- test/crypto/elliptic.js | 3 +- test/general/packet.js | 1 + test/general/signature.js | 37 ++++++ 19 files changed, 296 insertions(+), 185 deletions(-) diff --git a/src/crypto/hash/index.js b/src/crypto/hash/index.js index 97c37478..b8ef17e1 100644 --- a/src/crypto/hash/index.js +++ b/src/crypto/hash/index.js @@ -16,6 +16,7 @@ import Rusha from 'rusha'; import { SHA256 } from 'asmcrypto.js/src/hash/sha256/exports'; import sha1 from 'hash.js/lib/hash/sha/1'; import sha224 from 'hash.js/lib/hash/sha/224'; +import sha256 from 'hash.js/lib/hash/sha/256'; import sha384 from 'hash.js/lib/hash/sha/384'; import sha512 from 'hash.js/lib/hash/sha/512'; import { ripemd160 } from 'hash.js/lib/hash/ripemd'; @@ -64,7 +65,7 @@ if (nodeCrypto) { // Use Node native crypto for all hash functions return util.hex_to_Uint8Array(rusha.digest(data)); },*/ sha224: hashjs_hash(sha224), - sha256: SHA256.bytes, + sha256: hashjs_hash(sha256), sha384: hashjs_hash(sha384), // TODO, benchmark this vs asmCrypto's SHA512 sha512: hashjs_hash(sha512), diff --git a/src/crypto/pkcs1.js b/src/crypto/pkcs1.js index 886f2755..eb088357 100644 --- a/src/crypto/pkcs1.js +++ b/src/crypto/pkcs1.js @@ -128,14 +128,13 @@ eme.decode = function(EM) { * Create a EMSA-PKCS1-v1_5 padded message * @see {@link https://tools.ietf.org/html/rfc4880#section-13.1.3|RFC 4880 13.1.3} * @param {Integer} algo Hash algorithm type used - * @param {String} M message to be encoded + * @param {Uint8Array} hashed message to be encoded * @param {Integer} emLen intended length in octets of the encoded message * @returns {String} encoded message */ -emsa.encode = function(algo, M, emLen) { +emsa.encode = async function(algo, hashed, emLen) { let i; - // Apply the hash function to the message M to produce a hash value H - const H = util.Uint8Array_to_str(hash.digest(algo, util.str_to_Uint8Array(M))); + const H = util.Uint8Array_to_str(hashed); if (H.length !== hash.getHashByteLength(algo)) { throw new Error('Invalid hash length'); } diff --git a/src/crypto/public_key/dsa.js b/src/crypto/public_key/dsa.js index bd5e7b23..a53697df 100644 --- a/src/crypto/public_key/dsa.js +++ b/src/crypto/public_key/dsa.js @@ -18,14 +18,12 @@ /** * @fileoverview A Digital signature algorithm implementation * @requires bn.js - * @requires crypto/hash * @requires crypto/random * @requires util * @module crypto/public_key/dsa */ import BN from 'bn.js'; -import hash from '../hash'; import random from '../random'; import util from '../../util'; @@ -42,7 +40,7 @@ export default { /** * DSA Sign function * @param {Integer} hash_algo - * @param {String} m + * @param {Uint8Array} hashed * @param {BN} g * @param {BN} p * @param {BN} q @@ -50,7 +48,7 @@ export default { * @returns {{ r: BN, s: BN }} * @async */ - sign: async function(hash_algo, m, g, p, q, x) { + sign: async function(hash_algo, hashed, g, p, q, x) { let k; let r; let s; @@ -65,8 +63,7 @@ export default { // truncated) hash function result is treated as a number and used // directly in the DSA signature algorithm. const h = new BN( - util.getLeftNBits( - hash.digest(hash_algo, m), q.bitLength())) + util.getLeftNBits(hashed, q.bitLength())) .toRed(redq); // FIPS-186-4, section 4.6: // The values of r and s shall be checked to determine if r = 0 or s = 0. @@ -96,7 +93,7 @@ export default { * @param {Integer} hash_algo * @param {BN} r * @param {BN} s - * @param {String} m + * @param {Uint8Array} hashed * @param {BN} g * @param {BN} p * @param {BN} q @@ -104,7 +101,7 @@ export default { * @returns BN * @async */ - verify: async function(hash_algo, r, s, m, g, p, q, y) { + verify: async function(hash_algo, r, s, hashed, g, p, q, y) { if (zero.ucmp(r) >= 0 || r.ucmp(q) >= 0 || zero.ucmp(s) >= 0 || s.ucmp(q) >= 0) { util.print_debug("invalid DSA Signature"); @@ -113,8 +110,7 @@ export default { const redp = new BN.red(p); const redq = new BN.red(q); const h = new BN( - util.getLeftNBits( - hash.digest(hash_algo, m), q.bitLength())); + util.getLeftNBits(hashed, q.bitLength())); const w = s.toRed(redq).redInvm(); // s**-1 mod q if (zero.cmp(w) === 0) { util.print_debug("invalid DSA Signature"); diff --git a/src/crypto/public_key/elliptic/ecdsa.js b/src/crypto/public_key/elliptic/ecdsa.js index 9bcae5ef..8330da9f 100644 --- a/src/crypto/public_key/elliptic/ecdsa.js +++ b/src/crypto/public_key/elliptic/ecdsa.js @@ -33,10 +33,10 @@ import Curve from './curves'; * s: Uint8Array}} Signature of the message * @async */ -async function sign(oid, hash_algo, m, d) { +async function sign(oid, hash_algo, m, d, hashed) { const curve = new Curve(oid); const key = curve.keyFromPrivate(d); - const signature = await key.sign(m, hash_algo); + const signature = await key.sign(m, hash_algo, hashed); return { r: signature.r.toArrayLike(Uint8Array), s: signature.s.toArrayLike(Uint8Array) }; } @@ -52,10 +52,10 @@ async function sign(oid, hash_algo, m, d) { * @returns {Boolean} * @async */ -async function verify(oid, hash_algo, signature, m, Q) { +async function verify(oid, hash_algo, signature, m, Q, hashed) { const curve = new Curve(oid); const key = curve.keyFromPublic(Q); - return key.verify(m, signature, hash_algo); + return key.verify(m, signature, hash_algo, hashed); } export default { sign, verify }; diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index a3c59b16..4e99f637 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -33,10 +33,10 @@ import Curve from './curves'; * S: Uint8Array}} Signature of the message * @async */ -async function sign(oid, hash_algo, m, d) { +async function sign(oid, hash_algo, m, d, hashed) { const curve = new Curve(oid); const key = curve.keyFromSecret(d); - const signature = await key.sign(m, hash_algo); + const signature = await key.sign(m, hash_algo, hashed); // EdDSA signature params are returned in little-endian format return { R: new Uint8Array(signature.Rencoded()), S: new Uint8Array(signature.Sencoded()) }; @@ -53,10 +53,10 @@ async function sign(oid, hash_algo, m, d) { * @returns {Boolean} * @async */ -async function verify(oid, hash_algo, signature, m, Q) { +async function verify(oid, hash_algo, signature, m, Q, hashed) { const curve = new Curve(oid); const key = curve.keyFromPublic(Q); - return key.verify(m, signature, hash_algo); + return key.verify(m, signature, hash_algo, hashed); } export default { sign, verify }; diff --git a/src/crypto/public_key/elliptic/key.js b/src/crypto/public_key/elliptic/key.js index 3d0e0f78..08c485df 100644 --- a/src/crypto/public_key/elliptic/key.js +++ b/src/crypto/public_key/elliptic/key.js @@ -19,7 +19,7 @@ * @fileoverview Wrapper for a KeyPair of an Elliptic Curve * @requires bn.js * @requires crypto/public_key/elliptic/curves - * @requires crypto/hash + * @requires stream * @requires util * @requires enums * @requires asn1.js @@ -28,7 +28,7 @@ import BN from 'bn.js'; import { webCurves } from './curves'; -import hash from '../../hash'; +import stream from '../../../stream'; import util from '../../../util'; import enums from '../../../enums'; @@ -44,37 +44,43 @@ function KeyPair(curve, options) { this.keyPair = this.curve.curve.keyPair(options); } -KeyPair.prototype.sign = async function (message, hash_algo) { - if (this.curve.web && util.getWebCrypto()) { - // If browser doesn't support a curve, we'll catch it - try { - // need to await to make sure browser succeeds - const signature = await webSign(this.curve, hash_algo, message, this.keyPair); - return signature; - } catch (err) { - util.print_debug("Browser did not support signing: " + err.message); +KeyPair.prototype.sign = async function (message, hash_algo, hashed) { + if (!message.locked) { + message = await stream.readToEnd(message); + if (this.curve.web && util.getWebCrypto()) { + // If browser doesn't support a curve, we'll catch it + try { + // need to await to make sure browser succeeds + const signature = await webSign(this.curve, hash_algo, message, this.keyPair); + return signature; + } catch (err) { + util.print_debug("Browser did not support signing: " + err.message); + } + } else if (this.curve.node && util.getNodeCrypto()) { + return nodeSign(this.curve, hash_algo, message, this.keyPair); } - } else if (this.curve.node && util.getNodeCrypto()) { - return nodeSign(this.curve, hash_algo, message, this.keyPair); } - const digest = (typeof hash_algo === 'undefined') ? message : hash.digest(hash_algo, message); + const digest = (typeof hash_algo === 'undefined') ? message : hashed; return this.keyPair.sign(digest); }; -KeyPair.prototype.verify = async function (message, signature, hash_algo) { - if (this.curve.web && util.getWebCrypto()) { - // If browser doesn't support a curve, we'll catch it - try { - // need to await to make sure browser succeeds - const result = await webVerify(this.curve, hash_algo, signature, message, this.keyPair.getPublic()); - return result; - } catch (err) { - util.print_debug("Browser did not support signing: " + err.message); +KeyPair.prototype.verify = async function (message, signature, hash_algo, hashed) { + if (!message.locked) { + message = await stream.readToEnd(message); + if (this.curve.web && util.getWebCrypto()) { + // If browser doesn't support a curve, we'll catch it + try { + // need to await to make sure browser succeeds + const result = await webVerify(this.curve, hash_algo, signature, message, this.keyPair.getPublic()); + return result; + } catch (err) { + util.print_debug("Browser did not support signing: " + err.message); + } + } else if (this.curve.node && util.getNodeCrypto()) { + return nodeVerify(this.curve, hash_algo, signature, message, this.keyPair.getPublic()); } - } else if (this.curve.node && util.getNodeCrypto()) { - return nodeVerify(this.curve, hash_algo, signature, message, this.keyPair.getPublic()); } - const digest = (typeof hash_algo === 'undefined') ? message : hash.digest(hash_algo, message); + const digest = (typeof hash_algo === 'undefined') ? message : hashed; return this.keyPair.verify(digest, signature); }; diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 5b7f166f..5e825e28 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -4,7 +4,6 @@ * @requires crypto/public_key * @requires crypto/pkcs1 * @requires enums - * @requires stream * @requires util * @module crypto/signature */ @@ -13,7 +12,6 @@ import BN from 'bn.js'; import publicKey from './public_key'; import pkcs1 from './pkcs1'; import enums from '../enums'; -import stream from '../stream'; import util from '../util'; export default { @@ -30,8 +28,7 @@ export default { * @returns {Boolean} True if signature is valid * @async */ - verify: async function(algo, hash_algo, msg_MPIs, pub_MPIs, data) { - data = await stream.readToEnd(data); + verify: async function(algo, hash_algo, msg_MPIs, pub_MPIs, data, hashed) { switch (algo) { case enums.publicKey.rsa_encrypt_sign: case enums.publicKey.rsa_encrypt: @@ -40,7 +37,7 @@ export default { const n = pub_MPIs[0].toBN(); const e = pub_MPIs[1].toBN(); const EM = await publicKey.rsa.verify(m, n, e); - const EM2 = pkcs1.emsa.encode(hash_algo, util.Uint8Array_to_str(data), n.byteLength()); + const EM2 = await pkcs1.emsa.encode(hash_algo, hashed, n.byteLength()); return util.Uint8Array_to_hex(EM) === EM2; } case enums.publicKey.dsa: { @@ -50,13 +47,13 @@ export default { const q = pub_MPIs[1].toBN(); const g = pub_MPIs[2].toBN(); const y = pub_MPIs[3].toBN(); - return publicKey.dsa.verify(hash_algo, r, s, data, g, p, q, y); + return publicKey.dsa.verify(hash_algo, r, s, hashed, g, p, q, y); } case enums.publicKey.ecdsa: { const oid = pub_MPIs[0]; const signature = { r: msg_MPIs[0].toUint8Array(), s: msg_MPIs[1].toUint8Array() }; const Q = pub_MPIs[1].toUint8Array(); - return publicKey.elliptic.ecdsa.verify(oid, hash_algo, signature, data, Q); + return publicKey.elliptic.ecdsa.verify(oid, hash_algo, signature, data, Q, hashed); } case enums.publicKey.eddsa: { const oid = pub_MPIs[0]; @@ -65,7 +62,7 @@ export default { const signature = { R: Array.from(msg_MPIs[0].toUint8Array('le', 32)), S: Array.from(msg_MPIs[1].toUint8Array('le', 32)) }; const Q = Array.from(pub_MPIs[1].toUint8Array('be', 33)); - return publicKey.elliptic.eddsa.verify(oid, hash_algo, signature, data, Q); + return publicKey.elliptic.eddsa.verify(oid, hash_algo, signature, data, Q, hashed); } default: throw new Error('Invalid signature algorithm.'); @@ -84,8 +81,7 @@ export default { * @returns {Uint8Array} Signature * @async */ - sign: async function(algo, hash_algo, key_params, data) { - data = await stream.readToEnd(data); + sign: async function(algo, hash_algo, key_params, data, hashed) { switch (algo) { case enums.publicKey.rsa_encrypt_sign: case enums.publicKey.rsa_encrypt: @@ -93,8 +89,7 @@ export default { const n = key_params[0].toBN(); const e = key_params[1].toBN(); const d = key_params[2].toBN(); - data = util.Uint8Array_to_str(data); - const m = new BN(pkcs1.emsa.encode(hash_algo, data, n.byteLength()), 16); + const m = new BN(await pkcs1.emsa.encode(hash_algo, hashed, n.byteLength()), 16); const signature = await publicKey.rsa.sign(m, n, e, d); return util.Uint8Array_to_MPI(signature); } @@ -103,7 +98,7 @@ export default { const q = key_params[1].toBN(); const g = key_params[2].toBN(); const x = key_params[4].toBN(); - const signature = await publicKey.dsa.sign(hash_algo, data, g, p, q, x); + const signature = await publicKey.dsa.sign(hash_algo, hashed, g, p, q, x); return util.concatUint8Array([ util.Uint8Array_to_MPI(signature.r), util.Uint8Array_to_MPI(signature.s) @@ -115,7 +110,7 @@ export default { case enums.publicKey.ecdsa: { const oid = key_params[0]; const d = key_params[2].toUint8Array(); - const signature = await publicKey.elliptic.ecdsa.sign(oid, hash_algo, data, d); + const signature = await publicKey.elliptic.ecdsa.sign(oid, hash_algo, data, d, hashed); return util.concatUint8Array([ util.Uint8Array_to_MPI(signature.r), util.Uint8Array_to_MPI(signature.s) @@ -124,7 +119,7 @@ export default { case enums.publicKey.eddsa: { const oid = key_params[0]; const d = Array.from(key_params[2].toUint8Array('be', 32)); - const signature = await publicKey.elliptic.eddsa.sign(oid, hash_algo, data, d); + const signature = await publicKey.elliptic.eddsa.sign(oid, hash_algo, data, d, hashed); return util.concatUint8Array([ util.Uint8Array_to_MPI(signature.R), util.Uint8Array_to_MPI(signature.S) diff --git a/src/message.js b/src/message.js index aa4acbf3..401f50dc 100644 --- a/src/message.js +++ b/src/message.js @@ -21,6 +21,7 @@ * @requires config * @requires crypto * @requires enums + * @requires stream * @requires util * @requires packet * @requires signature @@ -33,6 +34,7 @@ import type_keyid from './type/keyid'; import config from './config'; import crypto from './crypto'; import enums from './enums'; +import stream from './stream'; import util from './util'; import packet from './packet'; import { Signature } from './signature'; @@ -77,7 +79,7 @@ Message.prototype.getSigningKeyIds = function() { // search for one pass signatures const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature); onePassSigList.forEach(function(packet) { - keyIds.push(packet.signingKeyId); + keyIds.push(packet.issuerKeyId); }); // if nothing found look for signature packets if (!keyIds.length) { @@ -406,10 +408,10 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new for (i = existingSigPacketlist.length - 1; i >= 0; i--) { const signaturePacket = existingSigPacketlist[i]; const onePassSig = new packet.OnePassSignature(); - onePassSig.type = signatureType; + onePassSig.signatureType = signatureType; onePassSig.hashAlgorithm = signaturePacket.hashAlgorithm; onePassSig.publicKeyAlgorithm = signaturePacket.publicKeyAlgorithm; - onePassSig.signingKeyId = signaturePacket.issuerKeyId; + onePassSig.issuerKeyId = signaturePacket.issuerKeyId; if (!privateKeys.length && i === 0) { onePassSig.flags = 1; } @@ -427,10 +429,10 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new privateKey.getKeyId().toHex()); } const onePassSig = new packet.OnePassSignature(); - onePassSig.type = signatureType; + onePassSig.signatureType = signatureType; onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKey.keyPacket, date, userId); onePassSig.publicKeyAlgorithm = signingKey.keyPacket.algorithm; - onePassSig.signingKeyId = signingKey.getKeyId(); + onePassSig.issuerKeyId = signingKey.getKeyId(); if (i === privateKeys.length - 1) { onePassSig.flags = 1; } @@ -527,12 +529,35 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig * @returns {Promise>} list of signer's keyid and validity of signature * @async */ -Message.prototype.verify = function(keys, date=new Date()) { +Message.prototype.verify = async function(keys, date=new Date()) { const msg = this.unwrapCompressed(); const literalDataList = msg.packets.filterByTag(enums.packet.literal); if (literalDataList.length !== 1) { throw new Error('Can only verify message with one literal data packet.'); } + if (msg.packets.stream) { + let onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature); + onePassSigList = Array.from(onePassSigList).reverse(); + if (onePassSigList.length) { + onePassSigList.forEach(onePassSig => { + onePassSig.signatureData = stream.fromAsync(() => new Promise(resolve => { + onePassSig.signatureDataResolve = resolve; + })); + onePassSig.hash(literalDataList[0]); + }); + const reader = stream.getReader(msg.packets.stream); + for (let i = 0; ; i++) { + const { done, value } = await reader.read(); + if (done) { + break; + } + onePassSigList[i].signatureDataResolve(value.signatureData); + value.hashed = onePassSigList[i].hashed; + value.hashedData = onePassSigList[i].hashedData; + msg.packets.push(value); + } + } + } const signatureList = msg.packets.filterByTag(enums.packet.signature); return createVerificationObjects(signatureList, literalDataList, keys, date); }; diff --git a/src/openpgp.js b/src/openpgp.js index 5132565f..be8f87c8 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -455,11 +455,11 @@ export function verify({ message, publicKeys, asStream, signature=null, date=new return Promise.resolve().then(async function() { const result = {}; - result.data = message instanceof CleartextMessage ? message.getText() : message.getLiteralData(); - result.data = await convertStream(result.data, asStream); result.signatures = signature ? await message.verifyDetached(signature, publicKeys, date) : await message.verify(publicKeys, date); + result.data = message instanceof CleartextMessage ? message.getText() : message.getLiteralData(); + result.data = await convertStream(result.data, asStream); return result; }).catch(onError.bind(null, 'Error verifying cleartext signed message')); } diff --git a/src/packet/compressed.js b/src/packet/compressed.js index 83e9996c..f20c9493 100644 --- a/src/packet/compressed.js +++ b/src/packet/compressed.js @@ -149,12 +149,7 @@ function pako_zlib(constructor, options = {}) { function bzip2(func) { return function(data) { - return new ReadableStream({ - async start(controller) { - controller.enqueue(func(await stream.readToEnd(data))); - controller.close(); - } - }); + return stream.fromAsync(async () => func(await stream.readToEnd(data))); }; } diff --git a/src/packet/one_pass_signature.js b/src/packet/one_pass_signature.js index 2381c245..92a3cf9f 100644 --- a/src/packet/one_pass_signature.js +++ b/src/packet/one_pass_signature.js @@ -16,11 +16,13 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA /** + * @requires packet/signature * @requires type/keyid * @requires enums * @requires util -*/ + */ +import Signature from './signature'; import type_keyid from '../type/keyid'; import enums from '../enums'; import util from '../util'; @@ -50,7 +52,7 @@ function OnePassSignature() { * Signature types are described in * {@link https://tools.ietf.org/html/rfc4880#section-5.2.1|RFC4880 Section 5.2.1}. */ - this.type = null; + this.signatureType = null; /** * A one-octet number describing the hash algorithm used. * @see {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC4880 9.4} @@ -62,7 +64,7 @@ function OnePassSignature() { */ this.publicKeyAlgorithm = null; /** An eight-octet number holding the Key ID of the signing key. */ - this.signingKeyId = null; + this.issuerKeyId = null; /** * A one-octet number holding a flag showing whether the signature is nested. * A zero value indicates that the next packet is another One-Pass Signature packet @@ -83,7 +85,7 @@ OnePassSignature.prototype.read = function (bytes) { // A one-octet signature type. Signature types are described in // Section 5.2.1. - this.type = enums.read(enums.signature, bytes[mypos++]); + this.signatureType = enums.read(enums.signature, bytes[mypos++]); // A one-octet number describing the hash algorithm used. this.hashAlgorithm = enums.read(enums.hash, bytes[mypos++]); @@ -92,8 +94,8 @@ OnePassSignature.prototype.read = function (bytes) { this.publicKeyAlgorithm = enums.read(enums.publicKey, bytes[mypos++]); // An eight-octet number holding the Key ID of the signing key. - this.signingKeyId = new type_keyid(); - this.signingKeyId.read(bytes.subarray(mypos, mypos + 8)); + this.issuerKeyId = new type_keyid(); + this.issuerKeyId.read(bytes.subarray(mypos, mypos + 8)); mypos += 8; // A one-octet number holding a flag showing whether the signature @@ -109,20 +111,33 @@ OnePassSignature.prototype.read = function (bytes) { * @returns {Uint8Array} a Uint8Array representation of a one-pass signature packet */ OnePassSignature.prototype.write = function () { - const start = new Uint8Array([3, enums.write(enums.signature, this.type), + const start = new Uint8Array([3, enums.write(enums.signature, this.signatureType), enums.write(enums.hash, this.hashAlgorithm), enums.write(enums.publicKey, this.publicKeyAlgorithm)]); const end = new Uint8Array([this.flags]); - return util.concatUint8Array([start, this.signingKeyId.write(), end]); + return util.concatUint8Array([start, this.issuerKeyId.write(), end]); }; /** * Fix custom types after cloning */ OnePassSignature.prototype.postCloneTypeFix = function() { - this.signingKeyId = type_keyid.fromClone(this.signingKeyId); + this.issuerKeyId = type_keyid.fromClone(this.issuerKeyId); }; +OnePassSignature.prototype.hash = function() { + const version = this.version; + this.version = 4; + try { + return Signature.prototype.hash.apply(this, arguments); + } finally { + this.version = version; + } +}; +OnePassSignature.prototype.toHash = Signature.prototype.toHash; +OnePassSignature.prototype.toSign = Signature.prototype.toSign; +OnePassSignature.prototype.calculateTrailer = Signature.prototype.calculateTrailer; + export default OnePassSignature; diff --git a/src/packet/packetlist.js b/src/packet/packetlist.js index 51dcc7d4..825c7db7 100644 --- a/src/packet/packetlist.js +++ b/src/packet/packetlist.js @@ -62,7 +62,7 @@ List.prototype.read = async function (bytes) { }); // Wait until first few packets have been read - const reader = stream.getReader(stream.clone(this.stream)); + const reader = stream.getReader(this.stream); while (true) { const { done, value } = await reader.read(); if (!done) { @@ -72,6 +72,7 @@ List.prototype.read = async function (bytes) { break; } } + reader.releaseLock(); }; /** diff --git a/src/packet/signature.js b/src/packet/signature.js index c430a637..490da2fc 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -21,6 +21,7 @@ * @requires type/mpi * @requires crypto * @requires enums + * @requires stream * @requires util */ @@ -29,6 +30,7 @@ import type_keyid from '../type/keyid.js'; import type_mpi from '../type/mpi.js'; import crypto from '../crypto'; import enums from '../enums'; +import stream from '../stream'; import util from '../util'; /** @@ -124,7 +126,7 @@ Signature.prototype.read = function (bytes) { // switch on version (3 and 4) switch (this.version) { - case 3: { + case 3: // One-octet length of following hashed material. MUST be 5. if (bytes[i++] !== 5) { util.print_debug("packet/signature.js\n" + @@ -132,7 +134,6 @@ Signature.prototype.read = function (bytes) { 'MUST be 5. @:' + (i - 1)); } - const sigpos = i; // One-octet signature type. this.signatureType = bytes[i++]; @@ -140,9 +141,6 @@ Signature.prototype.read = function (bytes) { this.created = util.readDate(bytes.subarray(i, i + 4)); i += 4; - // storing data appended to data which gets verified - this.signatureData = bytes.subarray(sigpos, i); - // Eight-octet Key ID of signer. this.issuerKeyId.read(bytes.subarray(i, i + 8)); i += 8; @@ -153,7 +151,6 @@ Signature.prototype.read = function (bytes) { // One-octet hash algorithm. this.hashAlgorithm = bytes[i++]; break; - } case 4: { this.signatureType = bytes[i++]; this.publicKeyAlgorithm = bytes[i++]; @@ -223,42 +220,31 @@ Signature.prototype.sign = async function (key, data) { const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm); const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); - const arr = [new Uint8Array([4, signatureType, publicKeyAlgorithm, hashAlgorithm])]; + if (this.version === 4) { + const arr = [new Uint8Array([4, signatureType, publicKeyAlgorithm, hashAlgorithm])]; - if (key.version === 5) { - // We could also generate this subpacket for version 4 keys, but for - // now we don't. - this.issuerKeyVersion = key.version; - this.issuerFingerprint = key.getFingerprintBytes(); + if (key.version === 5) { + // We could also generate this subpacket for version 4 keys, but for + // now we don't. + this.issuerKeyVersion = key.version; + this.issuerFingerprint = key.getFingerprintBytes(); + } + + this.issuerKeyId = key.getKeyId(); + + // Add hashed subpackets + arr.push(this.write_all_sub_packets()); + + this.signatureData = util.concat(arr); } - this.issuerKeyId = key.getKeyId(); - - // Add hashed subpackets - arr.push(this.write_all_sub_packets()); - - this.signatureData = util.concat(arr); - - const trailer = this.calculateTrailer(); - - let toHash = null; - - switch (this.version) { - case 3: - toHash = util.concat([this.toSign(signatureType, data), new Uint8Array([signatureType]), util.writeDate(this.created)]); - break; - case 4: - toHash = util.concat([this.toSign(signatureType, data), this.signatureData, trailer]); - break; - default: throw new Error('Version ' + this.version + ' of the signature is unsupported.'); - } - - const hash = crypto.hash.digest(hashAlgorithm, toHash); + const toHash = this.toHash(data); + const hash = await stream.readToEnd(this.hash(data, toHash)); this.signedHashValue = hash.subarray(0, 2); this.signature = await crypto.signature.sign( - publicKeyAlgorithm, hashAlgorithm, key.params, toHash + publicKeyAlgorithm, hashAlgorithm, key.params, toHash, hash ); return true; }; @@ -647,13 +633,37 @@ Signature.prototype.toSign = function (type, data) { Signature.prototype.calculateTrailer = function () { - // calculating the trailer - // V3 signatures don't have a trailer - if (this.version === 3) { - return new Uint8Array(0); + let length = 0; + return stream.transform(stream.clone(this.signatureData), value => { + length += value.length; + }, () => { + const first = new Uint8Array([4, 0xFF]); //Version, ? + return util.concat([first, util.writeNumber(length, 4)]); + }); +}; + + +Signature.prototype.toHash = function(data) { + const signatureType = enums.write(enums.signature, this.signatureType); + + const bytes = this.toSign(signatureType, data); + + switch (this.version) { + case 3: + return util.concat([bytes, new Uint8Array([signatureType]), util.writeDate(this.created)]); + case 4: + return util.concat([bytes, this.signatureData, this.calculateTrailer()]); + default: + throw new Error('Version ' + this.version + ' of the signature is unsupported.'); } - const first = new Uint8Array([4, 0xFF]); //Version, ? - return util.concat([first, util.writeNumber(this.signatureData.length, 4)]); +}; + +Signature.prototype.hash = function(data, toHash) { + if (!this.hashed) { + const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); + this.hashed = crypto.hash.digest(hashAlgorithm, toHash || this.toHash(data)); + } + return this.hashed; }; @@ -666,43 +676,46 @@ Signature.prototype.calculateTrailer = function () { * @async */ Signature.prototype.verify = async function (key, data) { - const signatureType = enums.write(enums.signature, this.signatureType); const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm); const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); - const bytes = this.toSign(signatureType, data); - const trailer = this.calculateTrailer(); + const toHash = this.toHash(data); + const hash = await stream.readToEnd(this.hash(data, toHash)); - let mpicount = 0; - // Algorithm-Specific Fields for RSA signatures: - // - multiprecision number (MPI) of RSA signature value m**d mod n. - if (publicKeyAlgorithm > 0 && publicKeyAlgorithm < 4) { - mpicount = 1; + if (this.signedHashValue[0] !== hash[0] || + this.signedHashValue[1] !== hash[1]) { + this.verified = false; + } else { + let mpicount = 0; + // Algorithm-Specific Fields for RSA signatures: + // - multiprecision number (MPI) of RSA signature value m**d mod n. + if (publicKeyAlgorithm > 0 && publicKeyAlgorithm < 4) { + mpicount = 1; - // Algorithm-Specific Fields for DSA, ECDSA, and EdDSA signatures: - // - MPI of DSA value r. - // - MPI of DSA value s. - } else if (publicKeyAlgorithm === enums.publicKey.dsa || - publicKeyAlgorithm === enums.publicKey.ecdsa || - publicKeyAlgorithm === enums.publicKey.eddsa) { - mpicount = 2; + // Algorithm-Specific Fields for DSA, ECDSA, and EdDSA signatures: + // - MPI of DSA value r. + // - MPI of DSA value s. + } else if (publicKeyAlgorithm === enums.publicKey.dsa || + publicKeyAlgorithm === enums.publicKey.ecdsa || + publicKeyAlgorithm === enums.publicKey.eddsa) { + mpicount = 2; + } + + // EdDSA signature parameters are encoded in little-endian format + // https://tools.ietf.org/html/rfc8032#section-5.1.2 + const endian = publicKeyAlgorithm === enums.publicKey.eddsa ? 'le' : 'be'; + const mpi = []; + let i = 0; + for (let j = 0; j < mpicount; j++) { + mpi[j] = new type_mpi(); + i += mpi[j].read(this.signature.subarray(i, this.signature.length), endian); + } + + this.verified = await crypto.signature.verify( + publicKeyAlgorithm, hashAlgorithm, mpi, key.params, + toHash, hash + ); } - - // EdDSA signature parameters are encoded in little-endian format - // https://tools.ietf.org/html/rfc8032#section-5.1.2 - const endian = publicKeyAlgorithm === enums.publicKey.eddsa ? 'le' : 'be'; - const mpi = []; - let i = 0; - for (let j = 0; j < mpicount; j++) { - mpi[j] = new type_mpi(); - i += mpi[j].read(this.signature.subarray(i, this.signature.length), endian); - } - - this.verified = await crypto.signature.verify( - publicKeyAlgorithm, hashAlgorithm, mpi, key.params, - util.concat([bytes, this.signatureData, trailer]) - ); - return this.verified; }; diff --git a/src/stream.js b/src/stream.js index 41d0efc8..c243b14e 100644 --- a/src/stream.js +++ b/src/stream.js @@ -56,7 +56,7 @@ function tee(input) { teed[0].externalBuffer = teed[1].externalBuffer = input.externalBuffer; return teed; } - return [input, input]; + return [subarray(input), subarray(input)]; } function clone(input) { @@ -66,7 +66,7 @@ function clone(input) { input.tee = teed[0].tee.bind(teed[0]); return teed[1]; } - return input; + return subarray(input); } function subarray(input, begin=0, end=Infinity) { @@ -89,13 +89,14 @@ function subarray(input, begin=0, end=Infinity) { } }); } - return new ReadableStream({ - pull: async controller => { - // TODO: Don't read entire stream into memory here. - controller.enqueue((await readToEnd(input)).subarray(begin, end)); - controller.close(); - } - }); + // TODO: Don't read entire stream into memory here. + return fromAsync(async () => (await readToEnd(input)).subarray(begin, end)); + } + if (util.isString(input)) { + return input.substr(begin, end); + } + if (input.externalBuffer) { + input = util.concat(input.externalBuffer.concat([input])); } return input.subarray(begin, end); } @@ -107,6 +108,15 @@ async function readToEnd(input, join) { return input; } +function fromAsync(fn) { + return new ReadableStream({ + pull: async controller => { + controller.enqueue(await fn()); + controller.close(); + } + }); +} + /** * Web / node stream conversion functions @@ -177,7 +187,7 @@ if (nodeStream) { } -export default { concat, getReader, transform, clone, subarray, readToEnd, nodeToWeb, webToNode }; +export default { concat, getReader, transform, clone, subarray, readToEnd, nodeToWeb, webToNode, fromAsync }; /*const readerAcquiredMap = new Map(); @@ -189,7 +199,14 @@ ReadableStream.prototype.getReader = function() { } else { readerAcquiredMap.set(this, new Error('Reader for this ReadableStream already acquired here.')); } - return _getReader.apply(this, arguments); + const _this = this; + const reader = _getReader.apply(this, arguments); + const _releaseLock = reader.releaseLock; + reader.releaseLock = function() { + readerAcquiredMap.delete(_this); + return _releaseLock.apply(this, arguments); + }; + return reader; }; const _tee = ReadableStream.prototype.tee; @@ -203,6 +220,7 @@ ReadableStream.prototype.tee = function() { };*/ +const doneReadingSet = new WeakSet(); function Reader(input) { this.stream = input; if (input.externalBuffer) { @@ -216,13 +234,17 @@ function Reader(input) { } let doneReading = false; this._read = async () => { - if (doneReading) { + if (doneReading || doneReadingSet.has(input)) { return { value: undefined, done: true }; } doneReading = true; return { value: input, done: false }; }; - this._releaseLock = () => {}; + this._releaseLock = () => { + if (doneReading) { + doneReadingSet.add(input); + } + }; } Reader.prototype.read = async function() { diff --git a/src/util.js b/src/util.js index fb29fa94..c8db4648 100644 --- a/src/util.js +++ b/src/util.js @@ -81,13 +81,17 @@ export default { if (Object.prototype.isPrototypeOf(obj)) { Object.entries(obj).forEach(([key, value]) => { // recursively search all children if (util.isStream(value)) { - const reader = stream.getReader(value); - const { port1, port2 } = new MessageChannel(); - port1.onmessage = async function() { - port1.postMessage(await reader.read()); - }; - obj[key] = port2; - collection.push(port2); + if (value.locked) { + obj[key] = null; + } else { + const reader = stream.getReader(value); + const { port1, port2 } = new MessageChannel(); + port1.onmessage = async function() { + port1.postMessage(await reader.read()); + }; + obj[key] = port2; + collection.push(port2); + } return; } util.collectTransferables(value, collection); diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 7f9e7f43..081ac5c3 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -239,12 +239,12 @@ describe('API functional testing', function() { //Originally we passed public and secret MPI separately, now they are joined. Is this what we want to do long term? // RSA return crypto.signature.sign( - 1, 2, RSApubMPIs.concat(RSAsecMPIs), data + 1, 2, RSApubMPIs.concat(RSAsecMPIs), data, crypto.hash.digest(2, data) ).then(RSAsignedData => { const RSAsignedDataMPI = new openpgp.MPI(); RSAsignedDataMPI.read(RSAsignedData); return crypto.signature.verify( - 1, 2, [RSAsignedDataMPI], RSApubMPIs, data + 1, 2, [RSAsignedDataMPI], RSApubMPIs, data, crypto.hash.digest(2, data) ).then(success => { return expect(success).to.be.true; }); @@ -254,7 +254,7 @@ describe('API functional testing', function() { it('DSA', function () { // DSA return crypto.signature.sign( - 17, 2, DSApubMPIs.concat(DSAsecMPIs), data + 17, 2, DSApubMPIs.concat(DSAsecMPIs), data, crypto.hash.digest(2, data) ).then(DSAsignedData => { DSAsignedData = util.Uint8Array_to_str(DSAsignedData); const DSAmsgMPIs = []; @@ -263,7 +263,7 @@ describe('API functional testing', function() { DSAmsgMPIs[0].read(DSAsignedData.substring(0,34)); DSAmsgMPIs[1].read(DSAsignedData.substring(34,68)); return crypto.signature.verify( - 17, 2, DSAmsgMPIs, DSApubMPIs, data + 17, 2, DSAmsgMPIs, DSApubMPIs, data, crypto.hash.digest(2, data) ).then(success => { return expect(success).to.be.true; }); diff --git a/test/crypto/elliptic.js b/test/crypto/elliptic.js index ed41edc8..a399542f 100644 --- a/test/crypto/elliptic.js +++ b/test/crypto/elliptic.js @@ -184,7 +184,8 @@ describe('Elliptic Curve Cryptography', function () { it('Signature generation', function () { const curve = new elliptic_curves.Curve('p256'); let key = curve.keyFromPrivate(key_data.p256.priv); - return key.sign(signature_data.message, 8).then(async signature => { + return key.sign(signature_data.message, 8).then(async ({ r, s }) => { + const signature = { r: new Uint8Array(r.toArray()), s: new Uint8Array(s.toArray()) }; key = curve.keyFromPublic(key_data.p256.pub); await expect( key.verify(signature_data.message, signature, 8) diff --git a/test/general/packet.js b/test/general/packet.js index 6459dd01..a2983219 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -705,6 +705,7 @@ describe("Packet", function() { await msg[1].decrypt(msg[0].sessionKeyAlgorithm, msg[0].sessionKey); const payload = msg[1].packets[0].packets; + await openpgp.stream.readToEnd(payload.stream, packets => packets.forEach(payload.push.bind(payload))); await expect(payload[2].verify( key[0], payload[1] diff --git a/test/general/signature.js b/test/general/signature.js index 51b5c7fe..08e13175 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -649,6 +649,43 @@ yYDnCgA= }); }); + it('Streaming verify signed message with trailing spaces from GPG', async function() { + const msg_armor = + `-----BEGIN PGP MESSAGE----- +Version: GnuPG v1 + +owGbwMvMyMT4oOW7S46CznTG01El3MUFicmpxbolqcUlUTev14K5Vgq8XGCGQmJe +ikJJYpKVAicvV16+QklRYmZOZl66AliWl0sBqBAkzQmmwKohBnAqdMxhYWRkYmBj +ZQIZy8DFKQCztusM8z+Vt/svG80IS/etn90utv/T16jquk69zPvp6t9F16ryrwpb +kfVlS5Xl38KnVYxWvIor0nao6WUczA4vvZX9TXPWnnW3tt1vbZoiqWUjYjjjhuKG +4DtmMTuL3TW6/zNzVfWp/Q11+71O8RGnXMsBvWM6mSqX75uLiPo6HRaUDHnvrfCP +yYDnCgA= +=15ki +-----END PGP MESSAGE-----`.split(''); + + const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t '; + const sMsg = await openpgp.message.readArmored(new ReadableStream({ + async pull(controller) { + await new Promise(setTimeout); + controller.enqueue(msg_armor.shift()); + if (!msg_armor.length) controller.close(); + } + })); + const pubKey = (await openpgp.key.readArmored(pub_key_arm2)).keys[0]; + + const keyids = sMsg.getSigningKeyIds(); + + expect(pubKey.getKeys(keyids[0])).to.not.be.empty; + + return openpgp.verify({ publicKeys:[pubKey], message:sMsg }).then(async function(cleartextSig) { + expect(cleartextSig).to.exist; + expect(openpgp.util.nativeEOL(openpgp.util.Uint8Array_to_str(await openpgp.stream.readToEnd(cleartextSig.data)))).to.equal(plaintext); + expect(cleartextSig.signatures).to.have.length(1); + expect(cleartextSig.signatures[0].valid).to.be.true; + expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1); + }); + }); + it('Sign text with openpgp.sign and verify with openpgp.verify leads to same string cleartext and valid signatures', async function() { const plaintext = 'short message\nnext line\n한국어/조선말'; const pubKey = (await openpgp.key.readArmored(pub_key_arm2)).keys[0];