diff --git a/src/message.js b/src/message.js index 2ce9f627..bea828bf 100644 --- a/src/message.js +++ b/src/message.js @@ -24,7 +24,7 @@ import crypto from './crypto'; import enums from './enums'; import util from './util'; import { Signature } from './signature'; -import { getPreferredHashAlgo, getPreferredCipherSuite, createSignaturePacket } from './key'; +import { getPreferredCipherSuite, createSignaturePacket } from './key'; import { PacketList, LiteralDataPacket, @@ -514,49 +514,14 @@ export class Message { throw new Error('No literal data packet to sign.'); } - let i; - let existingSigPacketlist; - // If data packet was created from Uint8Array, use binary, otherwise use text - const signatureType = literalDataPacket.text === null ? - enums.signature.binary : enums.signature.text; - - if (signature) { - existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature); - for (i = existingSigPacketlist.length - 1; i >= 0; i--) { - const signaturePacket = existingSigPacketlist[i]; - const onePassSig = new OnePassSignaturePacket(); - onePassSig.signatureType = signaturePacket.signatureType; - onePassSig.hashAlgorithm = signaturePacket.hashAlgorithm; - onePassSig.publicKeyAlgorithm = signaturePacket.publicKeyAlgorithm; - onePassSig.issuerKeyID = signaturePacket.issuerKeyID; - if (!signingKeys.length && i === 0) { - onePassSig.flags = 1; - } - packetlist.push(onePassSig); - } - } - - await Promise.all(Array.from(signingKeys).reverse().map(async function (primaryKey, i) { - if (!primaryKey.isPrivate()) { - throw new Error('Need private key for signing'); - } - const signingKeyID = signingKeyIDs[signingKeys.length - 1 - i]; - const signingKey = await primaryKey.getSigningKey(signingKeyID, date, userIDs, config); - const onePassSig = new OnePassSignaturePacket(); - onePassSig.signatureType = signatureType; - onePassSig.hashAlgorithm = await getPreferredHashAlgo(primaryKey, signingKey.keyPacket, date, userIDs, config); - onePassSig.publicKeyAlgorithm = signingKey.keyPacket.algorithm; - onePassSig.issuerKeyID = signingKey.getKeyID(); - if (i === signingKeys.length - 1) { - onePassSig.flags = 1; - } - return onePassSig; - })).then(onePassSignatureList => { - onePassSignatureList.forEach(onePassSig => packetlist.push(onePassSig)); - }); + const signaturePackets = await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, false, config); // this returns the existing signature packets as well + const onePassSignaturePackets = signaturePackets.map( + (signaturePacket, i) => OnePassSignaturePacket.fromSignaturePacket(signaturePacket, i === 0)) + .reverse(); // innermost OPS refers to the first signature packet + packetlist.push(...onePassSignaturePackets); packetlist.push(literalDataPacket); - packetlist.push(...(await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, false, config))); + packetlist.push(...signaturePackets); return new Message(packetlist); } @@ -733,6 +698,7 @@ export class Message { * @param {Date} [date] - Override the creationtime of the signature * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] + * @param {Array} [signatureSalts] - A list of signature salts matching the number of signingKeys that should be used for v6 signatures * @param {Boolean} [detached] - Whether to create detached signature packets * @param {Object} [config] - Full configuration, defaults to openpgp.config * @returns {Promise<PacketList>} List of signature packets. diff --git a/src/packet/one_pass_signature.js b/src/packet/one_pass_signature.js index 54e8697d..9018e79f 100644 --- a/src/packet/one_pass_signature.js +++ b/src/packet/one_pass_signature.js @@ -16,14 +16,12 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import * as stream from '@openpgp/web-stream-tools'; -import SignaturePacket from './signature'; +import SignaturePacket, { saltLengthForHash } from './signature'; import KeyID from '../type/keyid'; import enums from '../enums'; import util from '../util'; import { UnsupportedError } from './packet'; -const VERSION = 3; - /** * Implementation of the One-Pass Signature Packets (Tag 4) * @@ -39,8 +37,22 @@ class OnePassSignaturePacket { return enums.packet.onePassSignature; } + static fromSignaturePacket(signaturePacket, isLast) { + const onePassSig = new OnePassSignaturePacket(); + onePassSig.version = signaturePacket.version === 6 ? 6 : 3; + onePassSig.signatureType = signaturePacket.signatureType; + onePassSig.hashAlgorithm = signaturePacket.hashAlgorithm; + onePassSig.publicKeyAlgorithm = signaturePacket.publicKeyAlgorithm; + onePassSig.issuerKeyID = signaturePacket.issuerKeyID; + onePassSig.salt = signaturePacket.salt; // v6 only + onePassSig.issuerFingerprint = signaturePacket.issuerFingerprint; // v6 only + + onePassSig.flags = isLast ? 1 : 0; + return onePassSig; + } + constructor() { - /** A one-octet version number. The current version is 3. */ + /** A one-octet version number. The current versions are 3 and 6. */ this.version = null; /** * A one-octet signature type. @@ -62,8 +74,12 @@ class OnePassSignaturePacket { * @type {enums.publicKey} */ this.publicKeyAlgorithm = null; - /** An eight-octet number holding the Key ID of the signing key. */ + /** Only for v6, a variable-length field containing the salt. */ + this.salt = null; + /** Only for v3 packets, an eight-octet number holding the Key ID of the signing key. */ this.issuerKeyID = null; + /** Only for v6 packets, 32 octets of the fingerprint of the signing key. */ + this.issuerFingerprint = 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 @@ -79,9 +95,9 @@ class OnePassSignaturePacket { */ read(bytes) { let mypos = 0; - // A one-octet version number. The current version is 3. + // A one-octet version number. The current versions are 3 or 6. this.version = bytes[mypos++]; - if (this.version !== VERSION) { + if (this.version !== 3 && this.version !== 6) { throw new UnsupportedError(`Version ${this.version} of the one-pass signature packet is unsupported.`); } @@ -95,10 +111,32 @@ class OnePassSignaturePacket { // A one-octet number describing the public-key algorithm used. this.publicKeyAlgorithm = bytes[mypos++]; - // An eight-octet number holding the Key ID of the signing key. - this.issuerKeyID = new KeyID(); - this.issuerKeyID.read(bytes.subarray(mypos, mypos + 8)); - mypos += 8; + if (this.version === 6) { + // Only for v6 signatures, a variable-length field containing: + + // A one-octet salt size. The value MUST match the value defined + // for the hash algorithm as specified in Table 23 (Hash algorithm registry). + const saltLength = bytes[mypos++]; + if (saltLength !== saltLengthForHash(this.hashAlgorithm)) { + throw new Error('Unexpected salt size for the hash algorithm'); + } + + // The salt; a random value value of the specified size. + this.salt = bytes.subarray(mypos, mypos + saltLength); + mypos += saltLength; + + // Only for v6 packets, 32 octets of the fingerprint of the signing key. + this.issuerFingerprint = bytes.subarray(mypos, mypos + 32); + mypos += 32; + this.issuerKeyID = new KeyID(); + // For v6 the Key ID is the high-order 64 bits of the fingerprint. + this.issuerKeyID.read(this.issuerFingerprint); + } else { + // Only for v3 packets, an eight-octet number holding the Key ID of the signing key. + this.issuerKeyID = new KeyID(); + this.issuerKeyID.read(bytes.subarray(mypos, mypos + 8)); + mypos += 8; + } // A one-octet number holding a flag showing whether the signature // is nested. A zero value indicates that the next packet is @@ -113,11 +151,23 @@ class OnePassSignaturePacket { * @returns {Uint8Array} A Uint8Array representation of a one-pass signature packet. */ write() { - const start = new Uint8Array([VERSION, this.signatureType, this.hashAlgorithm, this.publicKeyAlgorithm]); - - const end = new Uint8Array([this.flags]); - - return util.concatUint8Array([start, this.issuerKeyID.write(), end]); + const arr = [new Uint8Array([ + this.version, + this.signatureType, + this.hashAlgorithm, + this.publicKeyAlgorithm + ])]; + if (this.version === 6) { + arr.push( + new Uint8Array([this.salt.length]), + this.salt, + this.issuerFingerprint + ); + } else { + arr.push(this.issuerKeyID.write()); + } + arr.push(new Uint8Array([this.flags])); + return util.concatUint8Array(arr); } calculateTrailer(...args) { @@ -133,7 +183,11 @@ class OnePassSignaturePacket { correspondingSig.signatureType !== this.signatureType || correspondingSig.hashAlgorithm !== this.hashAlgorithm || correspondingSig.publicKeyAlgorithm !== this.publicKeyAlgorithm || - !correspondingSig.issuerKeyID.equals(this.issuerKeyID) + !correspondingSig.issuerKeyID.equals(this.issuerKeyID) || + (this.version === 3 && correspondingSig.version === 6) || + (this.version === 6 && correspondingSig.version !== 6) || + (this.version === 6 && !util.equalsUint8Array(correspondingSig.issuerFingerprint, this.issuerFingerprint)) || + (this.version === 6 && !util.equalsUint8Array(correspondingSig.salt, this.salt)) ) { throw new Error('Corresponding signature packet does not match one-pass signature packet'); } diff --git a/src/packet/signature.js b/src/packet/signature.js index eb7f8062..6e0c0dc1 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -214,7 +214,11 @@ class SignaturePacket { if (this.version === 6) { const saltLength = saltLengthForHash(this.hashAlgorithm); - this.salt = await crypto.random.getRandomBytes(saltLength); + if (this.salt === null) { + this.salt = crypto.random.getRandomBytes(saltLength); + } else if (saltLength !== this.salt.length) { + throw new Error('Provided salt does not have the required length'); + } } const toHash = this.toHash(this.signatureType, data, detached); const hash = await this.hash(this.signatureType, data, toHash, detached); @@ -823,7 +827,7 @@ function writeSubPacket(type, critical, data) { * @returns {Integer} Salt length. * @private */ -function saltLengthForHash(hashAlgorithm) { +export function saltLengthForHash(hashAlgorithm) { switch (hashAlgorithm) { case enums.hash.sha256: return 16; case enums.hash.sha384: return 24; diff --git a/test/general/signature.js b/test/general/signature.js index 1c123f75..73e1425e 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -710,6 +710,68 @@ hUhMKMuiM3pRwdIyDOItkUWQmjEEw7/XmhgInkXsCw== expect(signature.getSigningKeyIDs().map(x => x.toHex())).to.include(publicKey.getKeyID().toHex()); }); + it('Generates valid one-pass signature packets (v6 keys)', async function () { + const v6PrivateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB +exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ +BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh +RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe +7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/ +LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG +GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE +M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr +k0mXubZvyl4GBg== +-----END PGP PRIVATE KEY BLOCK-----` }); + const v4PrivateKey = await openpgp.readKey({ + armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZOjwQBYJKwYBBAHaRw8BAQdAIzsYHb7T7NhSFmkWKSk5ItaBYv2HET7u +IFXhGdvOpogAAQDMk8SQlysNkLe7VRwXedX63St1a2V6/dzDG926oPyW3hJ6 +zQ48dGVzdEB0ZXN0Lml0PsKMBBAWCgA+BYJk6PBABAsJBwgJkFMSwQwprjrH +AxUICgQWAAIBAhkBApsDAh4BFiEEwKKS1V/wldBNfwpjUxLBDCmuOscAAONc +AQDU1gi9moPtQzh6rrpKPWZ8nMSiZzLb3LYmpqz9Tn9YFAEAmEC2uHET/Oj6 +VVxvMwvGytr5y0nuc6ZV5D0I2qoMcAHHXQRk6PBAEgorBgEEAZdVAQUBAQdA +xeBFSxzKOtLEQ6mVO2mnxvDiiMKmq7NbOezoWkbeSjgDAQgHAAD/YMvH+eDP +h1RplHOiaAMwhpTRGSGOaC6+pu6AMWiRLWAQ88J4BBgWCAAqBYJk6PBACZBT +EsEMKa46xwKbDBYhBMCiktVf8JXQTX8KY1MSwQwprjrHAAD4igEAzoExR6VX +EY9xxFKtAVdtYOT61d0FZibs0TD0VjbqzdkBAK60jT9jKefpnZ9sGv7bhSRX +2hrIPyCF2f0R5Js3LCML +=xyQ6 +-----END PGP PRIVATE KEY BLOCK-----` + }); + const armoredSignedMessage = await openpgp.sign({ + message: await openpgp.createMessage({ text: 'test' }), + signingKeys: [v6PrivateKey, v4PrivateKey, v6PrivateKey] // get multiple signature packets + }); + const signedMessage = await openpgp.readMessage({ armoredMessage: armoredSignedMessage }); + // read signature packet stream + signedMessage.packets.push(...await stream.readToEnd(signedMessage.packets.stream, _ => _)); + const signature1 = signedMessage.packets[4]; + const signature2 = signedMessage.packets[5]; // v4 sig + const signature3 = signedMessage.packets[6]; + const opsSignature1 = signedMessage.packets[2]; + const opsSignature2 = signedMessage.packets[1]; + const opsSignature3 = signedMessage.packets[0]; + expect(opsSignature1).to.be.instanceOf(openpgp.OnePassSignaturePacket); + expect(signature1).to.be.instanceOf(openpgp.SignaturePacket); + expect(opsSignature2).to.be.instanceOf(openpgp.OnePassSignaturePacket); + expect(signature2).to.be.instanceOf(openpgp.SignaturePacket); + expect(opsSignature3).to.be.instanceOf(openpgp.OnePassSignaturePacket); + expect(signature3).to.be.instanceOf(openpgp.SignaturePacket); + expect(opsSignature1.version).to.equal(6); + expect(opsSignature2.version).to.equal(3); + expect(opsSignature3.version).to.equal(6); + expect(util.uint8ArrayToHex(opsSignature1.issuerFingerprint)).to.equal(v6PrivateKey.getFingerprint()); + expect(util.uint8ArrayToHex(opsSignature3.issuerFingerprint)).to.equal(v6PrivateKey.getFingerprint()); + expect(opsSignature1.salt).to.deep.equal(signature1.salt); + expect(opsSignature2.salt).to.be.null; + expect(opsSignature3.salt).to.deep.equal(signature3.salt); + expect(opsSignature1.salt).to.not.deep.equal(opsSignature3.salt); // sanity check + }); + it('Throws when reading a signature missing the creation time', async function () { const armoredSignature = `-----BEGIN PGP SIGNATURE----- @@ -1109,7 +1171,92 @@ Fk7EflUZzngwY4lBzYAfnNBjEjc30xD/ddo+rwE= }); }); - it('Verify signed message with two one pass signatures', async function() { + + it('Verify signed message with a v6 one pass signature', async function() { + // test vector from https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-10.html#appendix-A.7 + const message = await openpgp.readMessage({ armoredMessage: `-----BEGIN PGP MESSAGE----- + +xEYGAQobIHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usyxhsTwYJppfk +1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkBy0p1AAAAAABXaGF0IHdlIG5lZWQgZnJv +bSB0aGUgZ3JvY2VyeSBzdG9yZToKCi0gdG9mdQotIHZlZ2V0YWJsZXMKLSBub29k +bGVzCsKYBgEbCgAAACkFgmOYo2MiIQbLGGxPBgmml+TVLfpscisMHx4nwYpWcI9l +JewnutmsyQAAAABpNiB2SV9QIYiQ9/Xi7jwYIlFPcFAPVR2G5ckh5ATjSlP7rCfQ +b7gKqPxbyxbhljGygHQPnqau1eBzrQD5QVplPEDnemrnfmkrpx0GmhCfokxYz9jj +FtCgazStmsuOXF9SFQE= +-----END PGP MESSAGE-----` }); + const key = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf +GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy +KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw +gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE +QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn ++eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh +BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8 +j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805 +I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== +-----END PGP PUBLIC KEY BLOCK-----` }); + + const signingKeyIDs = message.getSigningKeyIDs(); + expect(key.getKeys(signingKeyIDs[0])).to.not.be.empty; + + const { data, signatures } = await openpgp.verify({ message, verificationKeys: key }); + expect(signatures).to.have.length(1); + expect(await signatures[0].verified).to.be.true; + expect((await signatures[0].signature).packets.length).to.equal(1); + expect(data.includes('What we need from the grocery store')).to.be.true; + }); + + it('Verify signed message with two one pass signatures (v3 and v6)', async function() { + // test vector from gopenpgp + const message = await openpgp.readMessage({ armoredMessage: `-----BEGIN PGP MESSAGE----- + +xEYGAAobIBEtnAy3EeX8aDFd2bf/A1Xfi7C/D3mLIkGEwVmD0fecyxhsTwYJppfk +1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkAxA0DAAoW8jFVDE9H444ByxRiAAAAAABI +ZWxsbyBXb3JsZCA6KcJ1BAAWCgAnBQJk52D2CRDyMVUMT0fjjhYhBOuFu1+jOnXh +XpROY/IxVQxPR+OOAABOSwEA2nLYxa9ELDxwuPskKCUVs8noyT4fVEScWlWZAKqP +ykIBAJEhqqNHFP4Gok1Ss9mvf6fE25tJkdJKD+7PIH0mCJgAwpgGABsKAAAAKQUC +ZOdg9iKhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAA2MIBEt +nAy3EeX8aDFd2bf/A1Xfi7C/D3mLIkGEwVmD0fecr0oTPTcMNupiH7SCY4THe3ue +7PlRvF2WoeWKkNZT1uwcb+ilW5h9eBpp0I4Xj98C0hMA5+rHsTsME2EZM8HADA== +-----END PGP MESSAGE-----` }); + const v6Key = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf +GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy +KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw +gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE +QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn ++eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh +BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8 +j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805 +I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== +-----END PGP PUBLIC KEY BLOCK-----` }); + const v4Key = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: Alice's OpenPGP certificate + +mDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U +b7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE +ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy +MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO +dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4 +OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s +E9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb +DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn +0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE= +=iIGO +-----END PGP PUBLIC KEY BLOCK-----` }); + + const { data, signatures } = await openpgp.verify({ message, verificationKeys: [v4Key, v6Key] }); + expect(signatures).to.have.length(2); + expect(await signatures[0].verified).to.be.true; + expect((await signatures[0].signature).packets.length).to.equal(1); + expect(await signatures[1].verified).to.be.true; + expect((await signatures[1].signature).packets.length).to.equal(1); + expect(data).to.equal('Hello World :)'); + }); + + it('Verify signed message with two one pass signatures (both v3)', async function() { const msg_armor = ['-----BEGIN PGP MESSAGE-----', 'Version: GnuPG v2.0.19 (GNU/Linux)',