diff --git a/src/cleartext.js b/src/cleartext.js index d84da8d5..21a91a7f 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -29,23 +29,26 @@ import config from './config'; import packet from './packet'; import enums from './enums.js'; import armor from './encoding/armor.js'; +import * as sigModule from './signature.js'; /** * @class * @classdesc Class that represents an OpenPGP cleartext signed message. * See {@link http://tools.ietf.org/html/rfc4880#section-7} * @param {String} text The cleartext of the signed message - * @param {module:packet/packetlist} packetlist The packetlist with signature packets or undefined - * if message not yet signed + * @param {module:Signature} signature The detached signature or an empty signature if message not yet signed */ -export function CleartextMessage(text, packetlist) { +export function CleartextMessage(text, signature) { if (!(this instanceof CleartextMessage)) { - return new CleartextMessage(text, packetlist); + return new CleartextMessage(text, signature); } // normalize EOL to canonical form this.text = text.replace(/\r/g, '').replace(/[\t ]+\n/g, "\n").replace(/\n/g,"\r\n"); - this.packets = packetlist || new packet.List(); + if (signature && !(signature instanceof sigModule.Signature)) { + throw new Error('Invalid signature input'); + } + this.signature = signature || new sigModule.Signature(new packet.List()); } /** @@ -54,7 +57,7 @@ export function CleartextMessage(text, packetlist) { */ CleartextMessage.prototype.getSigningKeyIds = function() { var keyIds = []; - var signatureList = this.packets.filterByTag(enums.packet.signature); + var signatureList = this.signature.packets; signatureList.forEach(function(packet) { keyIds.push(packet.issuerKeyId); }); @@ -66,6 +69,15 @@ CleartextMessage.prototype.getSigningKeyIds = function() { * @param {Array} privateKeys private keys with decrypted secret key data for signing */ CleartextMessage.prototype.sign = function(privateKeys) { + this.signature = this.signDetached(privateKeys); +}; + +/** + * Sign the cleartext message + * @param {Array} privateKeys private keys with decrypted secret key data for signing + * @return {module:signature~Signature} new detached signature of message content + */ +CleartextMessage.prototype.signDetached = function(privateKeys) { var packetlist = new packet.List(); var literalDataPacket = new packet.Literal(); literalDataPacket.setText(this.text); @@ -84,7 +96,7 @@ CleartextMessage.prototype.sign = function(privateKeys) { signaturePacket.sign(signingKeyPacket, literalDataPacket); packetlist.push(signaturePacket); } - this.packets = packetlist; + return new sigModule.Signature(packetlist); }; /** @@ -93,8 +105,17 @@ CleartextMessage.prototype.sign = function(privateKeys) { * @return {Array<{keyid: module:type/keyid, valid: Boolean}>} list of signer's keyid and validity of signature */ CleartextMessage.prototype.verify = function(keys) { + return this.verifyDetached(this.signature, keys); +}; + +/** + * Verify signatures of cleartext signed message + * @param {Array} keys array of keys to verify signatures + * @return {Array<{keyid: module:type/keyid, valid: Boolean}>} list of signer's keyid and validity of signature + */ +CleartextMessage.prototype.verifyDetached = function(signature, keys) { var result = []; - var signatureList = this.packets.filterByTag(enums.packet.signature); + var signatureList = signature.packets; var literalDataPacket = new packet.Literal(); // we assume that cleartext signature is generated based on UTF8 cleartext literalDataPacket.setText(this.text); @@ -115,6 +136,8 @@ CleartextMessage.prototype.verify = function(keys) { verifiedSig.keyid = signatureList[i].issuerKeyId; verifiedSig.valid = null; } + verifiedSig.signature = new sigModule.Signature([signatureList[i]]); + result.push(verifiedSig); } return result; @@ -137,7 +160,7 @@ CleartextMessage.prototype.armor = function() { var body = { hash: enums.read(enums.hash, config.prefer_hash_algorithm).toUpperCase(), text: this.text, - data: this.packets.write() + data: this.signature.packets.write() }; return armor.encode(enums.armor.signed, body); }; @@ -157,7 +180,8 @@ export function readArmored(armoredText) { var packetlist = new packet.List(); packetlist.read(input.data); verifyHeaders(input.headers, packetlist); - var newMessage = new CleartextMessage(input.text, packetlist); + var signature = new sigModule.Signature(packetlist); + var newMessage = new CleartextMessage(input.text, signature); return newMessage; } diff --git a/src/encoding/armor.js b/src/encoding/armor.js index 70af9a78..68607308 100644 --- a/src/encoding/armor.js +++ b/src/encoding/armor.js @@ -38,6 +38,7 @@ import config from '../config'; * 3 = PGP MESSAGE * 4 = PUBLIC KEY BLOCK * 5 = PRIVATE KEY BLOCK + * 6 = SIGNATURE */ function getType(text) { var reHeader = /^-----BEGIN PGP (MESSAGE, PART \d+\/\d+|MESSAGE, PART \d+|SIGNED MESSAGE|MESSAGE|PUBLIC KEY BLOCK|PRIVATE KEY BLOCK|SIGNATURE)-----$\n/m; @@ -62,10 +63,7 @@ function getType(text) { return enums.armor.multipart_last; } else - // BEGIN PGP SIGNATURE - // Used for detached signatures, OpenPGP/MIME signatures, and - // cleartext signatures. Note that PGP 2.x uses BEGIN PGP MESSAGE - // for detached signatures. + // BEGIN PGP SIGNED MESSAGE if (/SIGNED MESSAGE/.test(header[1])) { return enums.armor.signed; @@ -86,6 +84,14 @@ function getType(text) { // Used for armoring private keys. if (/PRIVATE KEY BLOCK/.test(header[1])) { return enums.armor.private_key; + + } else + // BEGIN PGP SIGNATURE + // Used for detached signatures, OpenPGP/MIME signatures, and + // cleartext signatures. Note that PGP 2.x uses BEGIN PGP MESSAGE + // for detached signatures. + if (/SIGNATURE/.test(header[1])) { + return enums.armor.signature; } } @@ -397,6 +403,13 @@ function armor(messagetype, body, partindex, parttotal) { result.push("\r\n=" + getCheckSum(body) + "\r\n"); result.push("-----END PGP PRIVATE KEY BLOCK-----\r\n"); break; + case enums.armor.signature: + result.push("-----BEGIN PGP SIGNATURE-----\r\n"); + result.push(addheader()); + result.push(base64.encode(body)); + result.push("\r\n=" + getCheckSum(body) + "\r\n"); + result.push("-----END PGP SIGNATURE-----\r\n"); + break; } return result.join(''); diff --git a/src/enums.js b/src/enums.js index 837c6649..861f2f01 100644 --- a/src/enums.js +++ b/src/enums.js @@ -297,7 +297,8 @@ export default { signed: 2, message: 3, public_key: 4, - private_key: 5 + private_key: 5, + signature: 6 }, /** Asserts validity and converts from string/integer to integer. */ diff --git a/src/index.js b/src/index.js index 32832af5..7472326b 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,13 @@ export * from './openpgp'; import * as keyMod from './key'; export const key = keyMod; +/** + * @see module:signature + * @name module:openpgp.signature + */ +import * as signatureMod from './signature'; +export const signature = signatureMod; + /** * @see module:message * @name module:openpgp.message diff --git a/src/message.js b/src/message.js index 31b09c75..8580fd36 100644 --- a/src/message.js +++ b/src/message.js @@ -32,6 +32,7 @@ import enums from './enums.js'; import armor from './encoding/armor.js'; import config from './config'; import crypto from './crypto'; +import * as sigModule from './signature.js'; import * as keyModule from './key.js'; /** @@ -344,19 +345,81 @@ Message.prototype.sign = function(privateKeys) { return new Message(packetlist); }; +/** + * Create a detached signature for the message (the literal data packet of the message) + * @param {Array} privateKey private keys with decrypted secret key data for signing + * @return {module:signature~Signature} new detached signature of message content + */ +Message.prototype.signDetached = function(privateKeys) { + + var packetlist = new packet.List(); + + var literalDataPacket = this.packets.findPacket(enums.packet.literal); + if (!literalDataPacket) { + throw new Error('No literal data packet to sign.'); + } + + var literalFormat = enums.write(enums.literal, literalDataPacket.format); + var signatureType = literalFormat === enums.literal.binary ? + enums.signature.binary : enums.signature.text; + + for (var i = 0; i < privateKeys.length; i++) { + var signingKeyPacket = privateKeys[i].getSigningKeyPacket(); + var signaturePacket = new packet.Signature(); + signaturePacket.signatureType = signatureType; + signaturePacket.hashAlgorithm = config.prefer_hash_algorithm; + signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; + if (!signingKeyPacket.isDecrypted) { + throw new Error('Private key is not decrypted.'); + } + signaturePacket.sign(signingKeyPacket, literalDataPacket); + packetlist.push(signaturePacket); + } + + return new sigModule.Signature(packetlist); +}; + + /** * Verify message signatures * @param {Array} keys array of keys to verify signatures * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature */ Message.prototype.verify = function(keys) { - var result = []; var msg = this.unwrapCompressed(); var literalDataList = msg.packets.filterByTag(enums.packet.literal); if (literalDataList.length !== 1) { throw new Error('Can only verify message with one literal data packet.'); } var signatureList = msg.packets.filterByTag(enums.packet.signature); + return createVerificationObjects(signatureList, literalDataList, keys); +}; + +/** + * Verify detached message signature + * @param {Array} keys array of keys to verify signatures + * @param {Signature} + * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature + */ +Message.prototype.verifyDetached = function(signature, keys) { + var msg = this.unwrapCompressed(); + var literalDataList = msg.packets.filterByTag(enums.packet.literal); + if (literalDataList.length !== 1) { + throw new Error('Can only verify message with one literal data packet.'); + } + var signatureList = signature.packets; + return createVerificationObjects(signatureList, literalDataList, keys); +}; + +/** + * Create list of objects containing signer's keyid and validity of signature + * @param {Array} signatureList array of signature packets + * @param {Array} literalDataList array of literal data packets + * @param {Array} keys array of keys to verify signatures + * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature + */ +function createVerificationObjects(signatureList, literalDataList, keys) { + var result = []; for (var i = 0; i < signatureList.length; i++) { var keyPacket = null; for (var j = 0; j < keys.length; j++) { @@ -368,16 +431,19 @@ Message.prototype.verify = function(keys) { var verifiedSig = {}; if (keyPacket) { + //found a key packet that matches keyId of signature verifiedSig.keyid = signatureList[i].issuerKeyId; verifiedSig.valid = signatureList[i].verify(keyPacket, literalDataList[0]); } else { verifiedSig.keyid = signatureList[i].issuerKeyId; verifiedSig.valid = null; } + verifiedSig.signature = new sigModule.Signature([signatureList[i]]); + result.push(verifiedSig); } return result; -}; +} /** * Unwrap compressed message diff --git a/src/openpgp.js b/src/openpgp.js index b027991e..58656bf8 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -178,36 +178,44 @@ export function decryptKey({ privateKey, passphrase }) { * @param {Key|Array} privateKeys (optional) private keys for signing. If omitted message will not be signed * @param {String|Array} passwords (optional) array of passwords or a single password to encrypt the message * @param {String} filename (optional) a filename for the literal data packet - * @param {Boolean} armor (optional) if the return value should be ascii armored or the message object - * @return {Promise} encrypted ASCII armored message, or the full Message object if 'armor' is false + * @param {Boolean} armor (optional) if the return values should be ascii armored or the message/signature objects + * @param {Boolean} detached (optional) if the signature should be detached (if true, signature will be added to returned object) + * @return {Promise} encrypted (and optionally signed message) in the form: + * {data: ASCII armored message if 'armor' is true, + * message: full Message object if 'armor' is false, signature: detached signature if 'detached' is true} * @static */ -export function encrypt({ data, publicKeys, privateKeys, passwords, filename, armor=true }) { +export function encrypt({ data, publicKeys, privateKeys, passwords, filename, armor=true, detached=false }) { checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported - return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor }); + return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor, detached }); } - + var result = {}; return Promise.resolve().then(() => { let message = createMessage(data, filename); if (privateKeys) { // sign the message only if private keys are specified - message = message.sign(privateKeys); + if (detached) { + var signature = message.signDetached(privateKeys); + if (armor) { + result.signature = signature.armor(); + } else { + result.signature = signature; + } + } else { + message = message.sign(privateKeys); + } } return message.encrypt(publicKeys, passwords); }).then(message => { - - if(armor) { - return { - data: message.armor() - }; + if (armor) { + result.data = message.armor(); + } else { + result.message = message; } - return { - message: message - }; - + return result; }).catch(onError.bind(null, 'Error encrypting message')); } @@ -220,22 +228,28 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar * @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String } * @param {String} password (optional) single password to decrypt the message * @param {String} format (optional) return data format either as 'utf8' or 'binary' + * @param {Signature} signature (optional) detached signature for verification * @return {Promise} decrypted and verified message in the form: * { data:Uint8Array|String, filename:String, signatures:[{ keyid:String, valid:Boolean }] } * @static */ -export function decrypt({ message, privateKey, publicKeys, sessionKey, password, format='utf8' }) { +export function decrypt({ message, privateKey, publicKeys, sessionKey, password, format='utf8', signature=null }) { checkMessage(message); publicKeys = toArray(publicKeys); if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported - return asyncProxy.delegate('decrypt', { message, privateKey, publicKeys, sessionKey, password, format }); + return asyncProxy.delegate('decrypt', { message, privateKey, publicKeys, sessionKey, password, format, signature }); } return message.decrypt(privateKey, sessionKey, password).then(message => { const result = parseMessage(message, format); - if (publicKeys && result.data) { // verify only if publicKeys are specified - result.signatures = message.verify(publicKeys); + if (result.data) { // verify + if (signature) { + //detached signature + result.signatures = message.verifyDetached(signature, publicKeys); + } else { + result.signatures = message.verify(publicKeys); + } } return result; @@ -255,30 +269,41 @@ export function decrypt({ message, privateKey, publicKeys, sessionKey, password, * @param {String} data cleartext input to be signed * @param {Key|Array} privateKeys array of keys or single key with decrypted secret key data to sign cleartext * @param {Boolean} armor (optional) if the return value should be ascii armored or the message object - * @return {Promise} ASCII armored message or the message of type CleartextMessage + * @return {Promise} signed cleartext in the form: + * {data: ASCII armored message if 'armor' is true, + * message: full Message object if 'armor' is false, signature: detached signature if 'detached' is true} * @static */ -export function sign({ data, privateKeys, armor=true }) { +export function sign({ data, privateKeys, armor=true, detached=false}) { checkString(data); privateKeys = toArray(privateKeys); if (asyncProxy) { // use web worker if available - return asyncProxy.delegate('sign', { data, privateKeys, armor }); + return asyncProxy.delegate('sign', { data, privateKeys, armor, detached }); } + var result = {}; return execute(() => { const cleartextMessage = new cleartext.CleartextMessage(data); - cleartextMessage.sign(privateKeys); - if(armor) { - return { - data: cleartextMessage.armor() - }; + if (detached) { + var signature = cleartextMessage.signDetached(privateKeys); + if (armor) { + result.signature = signature.armor(); + } else { + result.signature = signature; + } + } else { + cleartextMessage.sign(privateKeys); } - return { - message: cleartextMessage - }; + + if (armor) { + result.data = cleartextMessage.armor(); + } else { + result.message = cleartextMessage; + } + return result; }, 'Error signing cleartext message'); } @@ -287,24 +312,32 @@ export function sign({ data, privateKeys, armor=true }) { * Verifies signatures of cleartext signed message * @param {Key|Array} publicKeys array of publicKeys or single key, to verify signatures * @param {CleartextMessage} message cleartext message object with signatures + * @param {Signature} signature (optional) detached signature for verification * @return {Promise} cleartext with status of verified signatures in the form of: * { data:String, signatures: [{ keyid:String, valid:Boolean }] } * @static */ -export function verify({ message, publicKeys }) { +export function verify({ message, publicKeys, signature=null }) { checkCleartextMessage(message); publicKeys = toArray(publicKeys); if (asyncProxy) { // use web worker if available - return asyncProxy.delegate('verify', { message, publicKeys }); + return asyncProxy.delegate('verify', { message, publicKeys, signature }); } - return execute(() => ({ + var result = {}; + return execute(() => { + result.data = message.getText(); - data: message.getText(), - signatures: message.verify(publicKeys) + if (signature) { + //detached signature + result.signatures = message.verifyDetached(signature, publicKeys); + } else { + result.signatures = message.verify(publicKeys); + } + return result; - }), 'Error verifying cleartext signed message'); + }, 'Error verifying cleartext signed message'); } diff --git a/src/packet/clone.js b/src/packet/clone.js index 0fefe29f..17c89867 100644 --- a/src/packet/clone.js +++ b/src/packet/clone.js @@ -26,6 +26,7 @@ import * as key from '../key.js'; import * as message from '../message.js'; import * as cleartext from '../cleartext.js'; +import * as signature from '../signature.js' import Packetlist from './packetlist.js'; import type_keyid from '../type/keyid.js'; @@ -55,9 +56,27 @@ export function clonePackets(options) { if (options.key) { options.key = options.key.toPacketlist(); } + if (options.message) { + //could be either a Message or CleartextMessage object + if (options.message instanceof message.Message) { + options.message = options.message.packets; + } else if (options.message instanceof cleartext.CleartextMessage) { + options.message.signature = options.message.signature.packets; + } + } + if (options.signature && (options.signature instanceof signature.Signature)) { + options.signature = options.signature.packets; + } + if (options.signatures) { + options.signatures = options.signatures.map(sig => verificationObjectToClone(sig)); + } return options; } +function verificationObjectToClone(verObject) { + verObject.signature = verObject.signature.packets; + return verObject; +} ////////////////////////////// // // @@ -91,7 +110,10 @@ export function parseClonedPackets(options, method) { options.message = packetlistCloneToMessage(options.message); } if (options.signatures) { - options.signatures = options.signatures.map(packetlistCloneToSignature); + options.signatures = options.signatures.map(packetlistCloneToSignatures); + } + if (options.signature) { + options.signature = packetlistCloneToSignature(options.signature); } return options; } @@ -102,16 +124,27 @@ function packetlistCloneToKey(clone) { } function packetlistCloneToMessage(clone) { - const packetlist = Packetlist.fromStructuredClone(clone.packets); + const packetlist = Packetlist.fromStructuredClone(clone); return new message.Message(packetlist); } function packetlistCloneToCleartextMessage(clone) { - var packetlist = Packetlist.fromStructuredClone(clone.packets); - return new cleartext.CleartextMessage(clone.text, packetlist); + var packetlist = Packetlist.fromStructuredClone(clone.signature); + return new cleartext.CleartextMessage(clone.text, new signature.Signature(packetlist)); +} + +//verification objects +function packetlistCloneToSignatures(clone) { + clone.keyid = type_keyid.fromClone(clone.keyid); + clone.signature = new signature.Signature(clone.signature); + return clone; } function packetlistCloneToSignature(clone) { - clone.keyid = type_keyid.fromClone(clone.keyid); - return clone; + if (typeof clone === "string") { + //signature is armored + return clone; + } + var packetlist = Packetlist.fromStructuredClone(clone); + return new signature.Signature(packetlist); } diff --git a/src/signature.js b/src/signature.js new file mode 100644 index 00000000..69ccf58d --- /dev/null +++ b/src/signature.js @@ -0,0 +1,76 @@ +// GPG4Browsers - An OpenPGP implementation in javascript +// Copyright (C) 2011 Recurity Labs GmbH +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 3.0 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +/** + * @requires config + * @requires crypto + * @requires encoding/armor + * @requires enums + * @requires packet + * @module signature + */ + +'use strict'; + +import packet from './packet'; +import enums from './enums.js'; +import armor from './encoding/armor.js'; + +/** + * @class + * @classdesc Class that represents an OpenPGP signature. + * @param {module:packet/packetlist} packetlist The signature packets + */ + +export function Signature(packetlist) { + if (!(this instanceof Signature)) { + return new Signature(packetlist); + } + this.packets = packetlist || new packet.List(); +} + + +/** + * Returns ASCII armored text of signature + * @return {String} ASCII armor + */ +Signature.prototype.armor = function() { + return armor.encode(enums.armor.signature, this.packets.write()); +}; + +/** + * reads an OpenPGP armored signature and returns a signature object + * @param {String} armoredText text to be parsed + * @return {module:signature~Signature} new signature object + * @static + */ +export function readArmored(armoredText) { + var input = armor.decode(armoredText).data; + return read(input); +} + +/** + * reads an OpenPGP signature as byte array and returns a signature object + * @param {Uint8Array} input binary signature + * @return {Signature} new signature object + * @static + */ +export function read(input) { + var packetlist = new packet.List(); + packetlist.read(input); + return new Signature(packetlist); +} diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 9cde0211..917a0aae 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -637,7 +637,8 @@ describe('OpenPGP.js public api tests', function() { return openpgp.decrypt(decOpt); }).then(function(decrypted) { expect(decrypted.data).to.equal(plaintext); - expect(decrypted.signatures).to.not.exist; + expect(decrypted.signatures).to.exist; + expect(decrypted.signatures.length).to.equal(0); done(); }); }); @@ -659,6 +660,31 @@ describe('OpenPGP.js public api tests', function() { expect(decrypted.data).to.equal(plaintext); expect(decrypted.signatures[0].valid).to.be.true; expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(decrypted.signatures[0].signature.packets.length).to.equal(1); + done(); + }); + }); + + it('should encrypt/sign and decrypt/verify with detached signatures', function(done) { + var encOpt = { + data: plaintext, + publicKeys: publicKey.keys, + privateKeys: privateKey.keys, + detached: true + }; + var decOpt = { + privateKey: privateKey.keys[0], + publicKeys: publicKey.keys + }; + openpgp.encrypt(encOpt).then(function(encrypted) { + decOpt.message = openpgp.message.readArmored(encrypted.data); + decOpt.signature = openpgp.signature.readArmored(encrypted.signature); + return openpgp.decrypt(decOpt); + }).then(function(decrypted) { + expect(decrypted.data).to.equal(plaintext); + expect(decrypted.signatures[0].valid).to.be.true; + expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(decrypted.signatures[0].signature.packets.length).to.equal(1); done(); }); }); @@ -680,6 +706,31 @@ describe('OpenPGP.js public api tests', function() { expect(decrypted.data).to.equal(plaintext); expect(decrypted.signatures[0].valid).to.be.null; expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(decrypted.signatures[0].signature.packets.length).to.equal(1); + done(); + }); + }); + + it('should fail to verify decrypted data with wrong public pgp key with detached signatures', function(done) { + var encOpt = { + data: plaintext, + publicKeys: publicKey.keys, + privateKeys: privateKey.keys, + detached: true + }; + var decOpt = { + privateKey: privateKey.keys[0], + publicKeys: openpgp.key.readArmored(wrong_pubkey).keys + }; + openpgp.encrypt(encOpt).then(function(encrypted) { + decOpt.message = openpgp.message.readArmored(encrypted.data); + decOpt.signature = openpgp.signature.readArmored(encrypted.signature); + return openpgp.decrypt(decOpt); + }).then(function(decrypted) { + expect(decrypted.data).to.equal(plaintext); + expect(decrypted.signatures[0].valid).to.be.null; + expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(decrypted.signatures[0].signature.packets.length).to.equal(1); done(); }); }); @@ -700,6 +751,29 @@ describe('OpenPGP.js public api tests', function() { expect(verified.data).to.equal(plaintext); expect(verified.signatures[0].valid).to.be.true; expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(verified.signatures[0].signature.packets.length).to.equal(1); + done(); + }); + }); + + it('should sign and verify cleartext data with detached signatures', function(done) { + var signOpt = { + data: plaintext, + privateKeys: privateKey.keys, + detached: true + }; + var verifyOpt = { + publicKeys: publicKey.keys + }; + openpgp.sign(signOpt).then(function(signed) { + verifyOpt.message = openpgp.cleartext.readArmored(signed.data); + verifyOpt.signature = openpgp.signature.readArmored(signed.signature); + return openpgp.verify(verifyOpt); + }).then(function(verified) { + expect(verified.data).to.equal(plaintext); + expect(verified.signatures[0].valid).to.be.true; + expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(verified.signatures[0].signature.packets.length).to.equal(1); done(); }); }); @@ -719,6 +793,29 @@ describe('OpenPGP.js public api tests', function() { expect(verified.data).to.equal(plaintext); expect(verified.signatures[0].valid).to.be.null; expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(verified.signatures[0].signature.packets.length).to.equal(1); + done(); + }); + }); + + it('should sign and fail to verify cleartext data with wrong public pgp key with detached signature', function(done) { + var signOpt = { + data: plaintext, + privateKeys: privateKey.keys, + detached: true + }; + var verifyOpt = { + publicKeys: openpgp.key.readArmored(wrong_pubkey).keys + }; + openpgp.sign(signOpt).then(function(signed) { + verifyOpt.message = openpgp.cleartext.readArmored(signed.data); + verifyOpt.signature = openpgp.signature.readArmored(signed.signature); + return openpgp.verify(verifyOpt); + }).then(function(verified) { + expect(verified.data).to.equal(plaintext); + expect(verified.signatures[0].valid).to.be.null; + expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(verified.signatures[0].signature.packets.length).to.equal(1); done(); }); }); @@ -739,6 +836,30 @@ describe('OpenPGP.js public api tests', function() { expect(verified.data).to.equal(plaintext); expect(verified.signatures[0].valid).to.be.true; expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(verified.signatures[0].signature.packets.length).to.equal(1); + done(); + }); + }); + + it('should sign and verify cleartext data and not armor with detached signatures', function(done) { + var signOpt = { + data: plaintext, + privateKeys: privateKey.keys, + detached: true, + armor: false + }; + var verifyOpt = { + publicKeys: publicKey.keys + }; + openpgp.sign(signOpt).then(function(signed) { + verifyOpt.message = signed.message; + verifyOpt.signature = signed.signature; + return openpgp.verify(verifyOpt); + }).then(function(verified) { + expect(verified.data).to.equal(plaintext); + expect(verified.signatures[0].valid).to.be.true; + expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + expect(verified.signatures[0].signature.packets.length).to.equal(1); done(); }); }); @@ -762,6 +883,9 @@ describe('OpenPGP.js public api tests', function() { }).then(function(encrypted) { expect(encrypted.data).to.exist; expect(encrypted.data).to.equal(plaintext); + expect(encrypted.signatures[0].valid).to.be.true; + expect(encrypted.signatures[0].keyid.toHex()).to.equal(privKeyDE.getSigningKeyPacket().getKeyId().toHex()); + expect(encrypted.signatures[0].signature.packets.length).to.equal(1); done(); }); }); @@ -828,6 +952,7 @@ describe('OpenPGP.js public api tests', function() { openpgp.decrypt({ privateKey:privKey, message:message }).then(function(decrypted) { expect(decrypted.data).to.equal('hello 3des\n'); + expect(decrypted.signatures.length).to.equal(0); done(); }); }); @@ -847,6 +972,7 @@ describe('OpenPGP.js public api tests', function() { return openpgp.decrypt(decOpt); }).then(function(decrypted) { expect(decrypted.data).to.equal(plaintext); + expect(decrypted.signatures.length).to.equal(0); done(); }); }); @@ -864,6 +990,7 @@ describe('OpenPGP.js public api tests', function() { return openpgp.decrypt(decOpt); }).then(function(decrypted) { expect(decrypted.data).to.equal(plaintext); + expect(decrypted.signatures.length).to.equal(0); done(); }); }); @@ -882,6 +1009,7 @@ describe('OpenPGP.js public api tests', function() { return openpgp.decrypt(decOpt); }).then(function(decrypted) { expect(decrypted.data).to.equal(plaintext); + expect(decrypted.signatures.length).to.equal(0); done(); }); }); @@ -905,6 +1033,7 @@ describe('OpenPGP.js public api tests', function() { expect(encOpt.data.byteLength).to.equal(0); // transfered buffer should be empty } expect(decrypted.data).to.deep.equal(new Uint8Array([0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01])); + expect(decrypted.signatures.length).to.equal(0); done(); }); }); diff --git a/test/general/signature.js b/test/general/signature.js index 15c72784..052126ee 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -265,6 +265,7 @@ describe("Signature", function() { openpgp.decrypt({ privateKey: priv_key, publicKeys:[pub_key], message:msg }).then(function(decrypted) { expect(decrypted.data).to.exist; expect(decrypted.signatures[0].valid).to.be.true; + expect(decrypted.signatures[0].signature.packets.length).to.equal(1); done(); }); }); @@ -309,6 +310,7 @@ describe("Signature", function() { expect(verified).to.exist; expect(verified).to.have.length(1); expect(verified[0].valid).to.be.true; + expect(verified[0].signature.packets.length).to.equal(1); done(); }); }); @@ -333,6 +335,7 @@ describe("Signature", function() { expect(verified).to.exist; expect(verified).to.have.length(1); expect(verified[0].valid).to.be.true; + expect(verified[0].signature.packets.length).to.equal(1); done(); }); @@ -356,6 +359,7 @@ describe("Signature", function() { expect(verified).to.exist; expect(verified).to.have.length(1); expect(verified[0].valid).to.be.true; + expect(verified[0].signature.packets.length).to.equal(1); done(); }); @@ -390,6 +394,7 @@ describe("Signature", function() { expect(decrypted.data).to.equal(plaintext); expect(decrypted.signatures).to.have.length(1); expect(decrypted.signatures[0].valid).to.be.true; + expect(decrypted.signatures[0].signature.packets.length).to.equal(1); done(); }); }); @@ -426,6 +431,7 @@ describe("Signature", function() { expect(decrypted.data).to.equal(plaintext); expect(decrypted.signatures).to.have.length(1); expect(decrypted.signatures[0].valid).to.be.true; + expect(decrypted.signatures[0].signature.packets.length).to.equal(1); done(); }); @@ -469,6 +475,8 @@ describe("Signature", function() { expect(verifiedSig).to.have.length(2); expect(verifiedSig[0].valid).to.be.true; expect(verifiedSig[1].valid).to.be.true; + expect(verifiedSig[0].signature.packets.length).to.equal(1); + expect(verifiedSig[1].signature.packets.length).to.equal(1); done(); }); @@ -513,6 +521,8 @@ describe("Signature", function() { expect(cleartextSig.signatures).to.have.length(2); expect(cleartextSig.signatures[0].valid).to.be.true; expect(cleartextSig.signatures[1].valid).to.be.true; + expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1); + expect(cleartextSig.signatures[1].signature.packets.length).to.equal(1); done(); }); }); @@ -533,6 +543,7 @@ describe("Signature", function() { expect(cleartextSig.data).to.equal(plaintext.replace(/\r/g,'')); 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); done(); }); @@ -595,7 +606,7 @@ describe("Signature", function() { expect(pubKey.users[0].selfCertifications).to.eql(pubKey2.users[0].selfCertifications); }); - it('Verify a detached signature', function() { + it('Verify a detached signature using readSignedContent', function() { var detachedSig = ['-----BEGIN PGP SIGNATURE-----', 'Version: GnuPG v1.4.13 (Darwin)', 'Comment: GPGTools - https://gpgtools.org', @@ -641,6 +652,42 @@ describe("Signature", function() { expect(result[0].valid).to.be.true; }); + it('Detached signature signing and verification cleartext', function () { + var msg = openpgp.message.fromText('hello'); + var pubKey2 = openpgp.key.readArmored(pub_key_arm2).keys[0]; + var privKey2 = openpgp.key.readArmored(priv_key_arm2).keys[0]; + privKey2.decrypt('hello world'); + + var opt = {numBits: 512, userIds: { name:'test', email:'a@b.com' }, passphrase: null}; + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + openpgp.generateKey(opt).then(function(gen) { + var generatedKey = gen.key; + var detachedSig = msg.signDetached([generatedKey, privKey2]); + var result = msg.verifyDetached(detachedSig, [generatedKey.toPublic(), pubKey2]); + expect(result[0].valid).to.be.true; + expect(result[1].valid).to.be.true; + }); + }); + + it('Detached signature signing and verification encrypted', function () { + var msg = openpgp.message.fromText('hello'); + var pubKey2 = openpgp.key.readArmored(pub_key_arm2).keys[0]; + var privKey2 = openpgp.key.readArmored(priv_key_arm2).keys[0]; + privKey2.decrypt('hello world'); + msg.encrypt({keys: [pubKey2] }); + + var detachedSig = msg.signDetached([privKey2]); + + var opt = {numBits: 512, userIds: { name:'test', email:'a@b.com' }, passphrase: null}; + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + openpgp.generateKey(opt).then(function(gen) { + var key = gen.key; + var result = msg.verifyDetached(detachedSig, [pubKey2, key.toPublic()]); + expect(result[0].valid).to.be.true; + expect(result[0].valid).to.be.false; + }); + }); + it('Sign message with key without password', function(done) { var opt = {numBits: 512, userIds: { name:'test', email:'a@b.com' }, passphrase: null}; if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys