From 735d6d088f96419ae016adf84c50f843f6d2deb3 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 3 Jun 2019 16:46:26 +0200 Subject: [PATCH] Implement V5 signatures --- src/cleartext.js | 4 +-- src/key.js | 5 ++-- src/message.js | 27 +++++++++-------- src/packet/literal.js | 19 +++++++++--- src/packet/one_pass_signature.js | 16 ++++------ src/packet/signature.js | 51 ++++++++++++++++++++++---------- test/general/openpgp.js | 36 ++++++++++++++++++++++ 7 files changed, 112 insertions(+), 46 deletions(-) diff --git a/src/cleartext.js b/src/cleartext.js index 93850a54..4f7c73c9 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -89,7 +89,7 @@ CleartextMessage.prototype.signDetached = async function(privateKeys, signature= const literalDataPacket = new packet.Literal(); literalDataPacket.setText(this.text); - return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds)); + return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true)); }; /** @@ -115,7 +115,7 @@ CleartextMessage.prototype.verifyDetached = function(signature, keys, date=new D const literalDataPacket = new packet.Literal(); // we assume that cleartext signature is generated based on UTF8 cleartext literalDataPacket.setText(this.text); - return createVerificationObjects(signatureList, [literalDataPacket], keys, date); + return createVerificationObjects(signatureList, [literalDataPacket], keys, date, true); }; /** diff --git a/src/key.js b/src/key.js index 26033bfd..a783352f 100644 --- a/src/key.js +++ b/src/key.js @@ -922,9 +922,10 @@ User.prototype.isRevoked = async function(primaryKey, certificate, key, date=new * @param {Object} signatureProperties (optional) properties to write on the signature packet before signing * @param {Date} date (optional) override the creationtime of the signature * @param {Object} userId (optional) user ID + * @param {Object} detached (optional) whether to create a detached signature packet * @returns {module:packet/signature} signature packet */ -export async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, signatureProperties, date, userId) { +export async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, signatureProperties, date, userId, detached=false) { if (!signingKeyPacket.isDecrypted()) { throw new Error('Private key is not decrypted.'); } @@ -932,7 +933,7 @@ export async function createSignaturePacket(dataToSign, privateKey, signingKeyPa Object.assign(signaturePacket, signatureProperties); signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKeyPacket, date, userId); - await signaturePacket.sign(signingKeyPacket, dataToSign); + await signaturePacket.sign(signingKeyPacket, dataToSign, detached); return signaturePacket; } diff --git a/src/message.js b/src/message.js index 30f4e2e6..72d82482 100644 --- a/src/message.js +++ b/src/message.js @@ -474,7 +474,7 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new }); packetlist.push(literalDataPacket); - packetlist.concat(await createSignaturePackets(literalDataPacket, privateKeys, signature, date)); + packetlist.concat(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, false)); return new Message(packetlist); }; @@ -513,7 +513,7 @@ Message.prototype.signDetached = async function(privateKeys=[], signature=null, if (!literalDataPacket) { throw new Error('No literal data packet to sign.'); } - return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds)); + return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true)); }; /** @@ -523,10 +523,11 @@ Message.prototype.signDetached = async function(privateKeys=[], signature=null, * @param {Signature} signature (optional) any existing detached signature to append * @param {Date} date (optional) override the creationtime of the signature * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Boolean} detached (optional) whether to create detached signature packets * @returns {Promise} list of signature packets * @async */ -export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date(), userIds=[]) { +export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date(), userIds=[], detached=false) { const packetlist = new packet.List(); // If data packet was created from Uint8Array, use binary, otherwise use text @@ -543,7 +544,7 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig throw new Error(`Could not find valid signing key packet in key ${ privateKey.getKeyId().toHex()}`); } - return createSignaturePacket(literalDataPacket, privateKey, signingKey.keyPacket, { signatureType }, date, userId); + return createSignaturePacket(literalDataPacket, privateKey, signingKey.keyPacket, { signatureType }, date, userId, detached); })).then(signatureList => { signatureList.forEach(signaturePacket => packetlist.push(signaturePacket)); }); @@ -578,7 +579,7 @@ Message.prototype.verify = async function(keys, date=new Date(), streaming) { onePassSig.correspondingSigReject = reject; }); onePassSig.signatureData = stream.fromAsync(async () => (await onePassSig.correspondingSig).signatureData); - onePassSig.hashed = await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, streaming); + onePassSig.hashed = await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, false, streaming); })); msg.packets.stream = stream.transformPair(msg.packets.stream, async (readable, writable) => { const reader = stream.getReader(readable); @@ -598,9 +599,9 @@ Message.prototype.verify = async function(keys, date=new Date(), streaming) { await writer.abort(e); } }); - return createVerificationObjects(onePassSigList, literalDataList, keys, date); + return createVerificationObjects(onePassSigList, literalDataList, keys, date, false); } - return createVerificationObjects(signatureList, literalDataList, keys, date); + return createVerificationObjects(signatureList, literalDataList, keys, date, false); }; /** @@ -618,7 +619,7 @@ Message.prototype.verifyDetached = function(signature, keys, date=new Date()) { throw new Error('Can only verify message with one literal data packet.'); } const signatureList = signature.packets; - return createVerificationObjects(signatureList, literalDataList, keys, date); + return createVerificationObjects(signatureList, literalDataList, keys, date, true); }; /** @@ -628,11 +629,12 @@ Message.prototype.verifyDetached = function(signature, keys, date=new Date()) { * @param {Array} keys array of keys to verify signatures * @param {Date} date Verify the signature against the given date, * i.e. check signature creation time < date < expiration time + * @param {Boolean} detached (optional) whether to verify detached signature packets * @returns {Promise>} list of signer's keyid and validity of signature * @async */ -async function createVerificationObject(signature, literalDataList, keys, date=new Date()) { +async function createVerificationObject(signature, literalDataList, keys, date=new Date(), detached=false) { let primaryKey = null; let signingKey = null; await Promise.all(keys.map(async function(key) { @@ -651,7 +653,7 @@ async function createVerificationObject(signature, literalDataList, keys, date=n if (!signingKey) { return null; } - const verified = await signature.verify(signingKey.keyPacket, signature.signatureType, literalDataList[0]); + const verified = await signature.verify(signingKey.keyPacket, signature.signatureType, literalDataList[0], detached); const sig = await signaturePacket; if (sig.isExpired(date) || !( sig.created >= signingKey.getCreationTime() && @@ -689,15 +691,16 @@ async function createVerificationObject(signature, literalDataList, keys, date=n * @param {Array} keys array of keys to verify signatures * @param {Date} date Verify the signature against the given date, * i.e. check signature creation time < date < expiration time + * @param {Boolean} detached (optional) whether to verify detached signature packets * @returns {Promise>} list of signer's keyid and validity of signature * @async */ -export async function createVerificationObjects(signatureList, literalDataList, keys, date=new Date()) { +export async function createVerificationObjects(signatureList, literalDataList, keys, date=new Date(), detached=false) { return Promise.all(signatureList.filter(function(signature) { return ['text', 'binary'].includes(enums.read(enums.signature, signature.signatureType)); }).map(async function(signature) { - return createVerificationObject(signature, literalDataList, keys, date); + return createVerificationObject(signature, literalDataList, keys, date, detached); })); } diff --git a/src/packet/literal.js b/src/packet/literal.js index b2586615..6750da4a 100644 --- a/src/packet/literal.js +++ b/src/packet/literal.js @@ -139,19 +139,30 @@ Literal.prototype.read = async function(bytes) { }; /** - * Creates a string representation of the packet + * Creates a Uint8Array representation of the packet, excluding the data * - * @returns {Uint8Array | ReadableStream} Uint8Array representation of the packet + * @returns {Uint8Array} Uint8Array representation of the packet */ -Literal.prototype.write = function() { +Literal.prototype.writeHeader = function() { const filename = util.encode_utf8(this.filename); const filename_length = new Uint8Array([filename.length]); const format = new Uint8Array([enums.write(enums.literal, this.format)]); const date = util.writeDate(this.date); + + return util.concatUint8Array([format, filename_length, filename, date]); +}; + +/** + * Creates a Uint8Array representation of the packet + * + * @returns {Uint8Array | ReadableStream} Uint8Array representation of the packet + */ +Literal.prototype.write = function() { + const header = this.writeHeader(); const data = this.getBytes(); - return util.concat([format, filename_length, filename, date, data]); + return util.concat([header, data]); }; export default Literal; diff --git a/src/packet/one_pass_signature.js b/src/packet/one_pass_signature.js index 91b607ad..d7a79d27 100644 --- a/src/packet/one_pass_signature.js +++ b/src/packet/one_pass_signature.js @@ -16,12 +16,14 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA /** + * @requires web-stream-tools * @requires packet/signature * @requires type/keyid * @requires enums * @requires util */ +import stream from 'web-stream-tools'; import Signature from './signature'; import type_keyid from '../type/keyid'; import enums from '../enums'; @@ -127,18 +129,12 @@ OnePassSignature.prototype.postCloneTypeFix = function() { 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.hash = Signature.prototype.hash; OnePassSignature.prototype.toHash = Signature.prototype.toHash; OnePassSignature.prototype.toSign = Signature.prototype.toSign; -OnePassSignature.prototype.calculateTrailer = Signature.prototype.calculateTrailer; +OnePassSignature.prototype.calculateTrailer = function(...args) { + return stream.fromAsync(async () => (await this.correspondingSig).calculateTrailer(...args)); +}; OnePassSignature.prototype.verify = async function() { const correspondingSig = await this.correspondingSig; diff --git a/src/packet/signature.js b/src/packet/signature.js index 5b01da2a..45cb2261 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -47,7 +47,7 @@ import config from '../config'; */ function Signature(date=new Date()) { this.tag = enums.packet.signature; - this.version = 4; + this.version = 4; // This is set to 5 below if we sign with a V5 key. this.signatureType = null; this.hashAlgorithm = null; this.publicKeyAlgorithm = null; @@ -106,7 +106,7 @@ Signature.prototype.read = function (bytes) { let i = 0; this.version = bytes[i++]; - if (this.version !== 4) { + if (this.version !== 4 && this.version !== 5) { throw new Error('Version ' + this.version + ' of the signature is unsupported.'); } @@ -148,15 +148,19 @@ Signature.prototype.write = function () { * Signs provided data. This needs to be done prior to serialization. * @param {module:packet.SecretKey} key private key used to sign the message. * @param {Object} data Contains packets to be signed. + * @param {Boolean} detached (optional) whether to create a detached signature * @returns {Promise} * @async */ -Signature.prototype.sign = async function (key, data) { +Signature.prototype.sign = async function (key, data, detached=false) { 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 arr = [new Uint8Array([4, signatureType, publicKeyAlgorithm, hashAlgorithm])]; + if (key.version === 5) { + this.version = 5; + } + const arr = [new Uint8Array([this.version, signatureType, publicKeyAlgorithm, hashAlgorithm])]; if (key.version === 5) { // We could also generate this subpacket for version 4 keys, but for @@ -172,8 +176,8 @@ Signature.prototype.sign = async function (key, data) { this.signatureData = util.concat(arr); - const toHash = this.toHash(signatureType, data); - const hash = await this.hash(signatureType, data, toHash); + const toHash = this.toHash(signatureType, data, detached); + const hash = await this.hash(signatureType, data, toHash, detached); this.signedHashValue = stream.slice(stream.clone(hash), 0, 2); @@ -628,28 +632,42 @@ Signature.prototype.toSign = function (type, data) { }; -Signature.prototype.calculateTrailer = function () { +Signature.prototype.calculateTrailer = function (data, detached) { 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)]); + const arr = []; + if (this.version === 5 && (this.signatureType === enums.signature.binary || this.signatureType === enums.signature.text)) { + if (detached) { + arr.push(new Uint8Array(6)); + } else { + arr.push(data.writeHeader()); + } + } + arr.push(new Uint8Array([this.version, 0xFF])); + if (this.version === 5) { + arr.push(new Uint8Array(4)); + } + arr.push(util.writeNumber(length, 4)); + // For v5, this should really be writeNumber(length, 8) rather than the + // hardcoded 4 zero bytes above + return util.concat(arr); }); }; -Signature.prototype.toHash = function(signatureType, data) { +Signature.prototype.toHash = function(signatureType, data, detached=false) { const bytes = this.toSign(signatureType, data); - return util.concat([bytes, this.signatureData, this.calculateTrailer()]); + return util.concat([bytes, this.signatureData, this.calculateTrailer(data, detached)]); }; -Signature.prototype.hash = async function(signatureType, data, toHash, streaming=true) { +Signature.prototype.hash = async function(signatureType, data, toHash, detached=false, streaming=true) { const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); - if (!toHash) toHash = this.toHash(signatureType, data); + if (!toHash) toHash = this.toHash(signatureType, data, detached); if (!streaming && util.isStream(toHash)) { - return stream.fromAsync(async () => this.hash(signatureType, data, await stream.readToEnd(toHash))); + return stream.fromAsync(async () => this.hash(signatureType, data, await stream.readToEnd(toHash), detached)); } return crypto.hash.digest(hashAlgorithm, toHash); }; @@ -661,10 +679,11 @@ Signature.prototype.hash = async function(signatureType, data, toHash, streaming * module:packet.SecretSubkey|module:packet.SecretKey} key the public key to verify the signature * @param {module:enums.signature} signatureType expected signature type * @param {String|Object} data data which on the signature applies + * @param {Boolean} detached (optional) whether to verify a detached signature * @returns {Promise} True if message is verified, else false. * @async */ -Signature.prototype.verify = async function (key, signatureType, data) { +Signature.prototype.verify = async function (key, signatureType, data, detached=false) { const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm); const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); @@ -677,7 +696,7 @@ Signature.prototype.verify = async function (key, signatureType, data) { if (this.hashed) { hash = this.hashed; } else { - toHash = this.toHash(signatureType, data); + toHash = this.toHash(signatureType, data, detached); hash = await this.hash(signatureType, data, toHash); } hash = await stream.readToEnd(hash); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index faa44e28..65146234 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -1305,6 +1305,42 @@ describe('[Sauce Labs Group 2] OpenPGP.js public api tests', function() { }); }); + it('should encrypt/sign and decrypt/verify with generated key and detached signatures', function () { + const genOpt = { + userIds: [{ name: 'Test User', email: 'text@example.com' }], + numBits: 512 + }; + if (openpgp.util.getWebCryptoAll()) { genOpt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + + return openpgp.generateKey(genOpt).then(async function(newKey) { + const newPublicKey = await openpgp.key.readArmored(newKey.publicKeyArmored); + const newPrivateKey = await openpgp.key.readArmored(newKey.privateKeyArmored); + + const encOpt = { + message: openpgp.message.fromText(plaintext), + publicKeys: newPublicKey.keys, + privateKeys: newPrivateKey.keys, + detached: true + }; + const decOpt = { + privateKeys: newPrivateKey.keys[0], + publicKeys: newPublicKey.keys + }; + return openpgp.encrypt(encOpt).then(async function (encrypted) { + decOpt.message = await openpgp.message.readArmored(encrypted.data); + decOpt.signature = await openpgp.signature.readArmored(encrypted.signature); + expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect); + return openpgp.decrypt(decOpt); + }).then(async function (decrypted) { + expect(decrypted.data).to.equal(plaintext); + expect(decrypted.signatures[0].valid).to.be.true; + const signingKey = await newPrivateKey.keys[0].getSigningKey(); + expect(decrypted.signatures[0].keyid.toHex()).to.equal(signingKey.getKeyId().toHex()); + expect(decrypted.signatures[0].signature.packets.length).to.equal(1); + }); + }); + }); + it('should encrypt/sign and decrypt/verify with null string input', function () { const encOpt = { message: openpgp.message.fromText(''),