diff --git a/src/key.js b/src/key.js index 7e98f33b..b669d2db 100644 --- a/src/key.js +++ b/src/key.js @@ -261,32 +261,28 @@ Key.prototype.armor = function() { }; /** - * Returns the signature that has the latest creation date, while ignoring signatures created in the future. + * Returns the valid and non-expired signature that has the latest creation date, while ignoring signatures created in the future. * @param {Array} signatures List of signatures * @param {Date} date Use the given date instead of the current time - * @returns {module:packet.Signature} The latest signature + * @returns {Promise} The latest valid signature + * @async */ -function getLatestSignature(signatures, date=new Date()) { - let signature = signatures[0]; - for (let i = 1; i < signatures.length; i++) { - if (signatures[i].created >= signature.created && - (signatures[i].created <= date || date === null)) { +async function getLatestValidSignature(signatures, primaryKey, dataToVerify, date=new Date()) { + let signature; + for (let i = signatures.length - 1; i >= 0; i--) { + if ( + (!signature || signatures[i].created >= signature.created) && + // check binding signature is not expired (ie, check for V4 expiration time) + !signatures[i].isExpired(date) && + // check binding signature is verified + (signatures[i].verified || await signatures[i].verify(primaryKey, dataToVerify)) + ) { signature = signatures[i]; } } return signature; } -function isValidSigningKeyPacket(keyPacket, signature, date=new Date()) { - return keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.rsa_encrypt) && - keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.elgamal) && - keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.ecdh) && - (!signature.keyFlags || - (signature.keyFlags[0] & enums.keyFlags.sign_data) !== 0) && - signature.verified && !signature.revoked && !signature.isExpired(date) && - !isDataExpired(keyPacket, signature, date); -} - /** * Returns last created key or key by given keyId that is available for signing and verification * @param {module:type/keyid} keyId, optional @@ -302,8 +298,9 @@ Key.prototype.getSigningKey = async function (keyId=null, date=new Date(), userI for (let i = 0; i < subKeys.length; i++) { if (!keyId || subKeys[i].getKeyId().equals(keyId)) { if (await subKeys[i].verify(primaryKey, date) === enums.keyStatus.valid) { - const bindingSignature = getLatestSignature(subKeys[i].bindingSignatures, date); - if (isValidSigningKeyPacket(subKeys[i].keyPacket, bindingSignature, date)) { + const dataToVerify = { key: primaryKey, bind: subKeys[i].keyPacket }; + const bindingSignature = await getLatestValidSignature(subKeys[i].bindingSignatures, primaryKey, dataToVerify, date); + if (bindingSignature && isValidSigningKeyPacket(subKeys[i].keyPacket, bindingSignature)) { return subKeys[i]; } } @@ -311,24 +308,23 @@ Key.prototype.getSigningKey = async function (keyId=null, date=new Date(), userI } const primaryUser = await this.getPrimaryUser(date, userId); if (primaryUser && (!keyId || primaryKey.getKeyId().equals(keyId)) && - isValidSigningKeyPacket(primaryKey, primaryUser.selfCertification, date)) { + isValidSigningKeyPacket(primaryKey, primaryUser.selfCertification)) { return this; } } return null; -}; -function isValidEncryptionKeyPacket(keyPacket, signature, date=new Date()) { - return keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.dsa) && - keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.rsa_sign) && - keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.ecdsa) && - keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.eddsa) && - (!signature.keyFlags || - (signature.keyFlags[0] & enums.keyFlags.encrypt_communication) !== 0 || - (signature.keyFlags[0] & enums.keyFlags.encrypt_storage) !== 0) && - signature.verified && !signature.revoked && !signature.isExpired(date) && - !isDataExpired(keyPacket, signature, date); -} + function isValidSigningKeyPacket(keyPacket, signature) { + if (!signature.verified || signature.revoked !== false) { // Sanity check + throw new Error('Signature not verified'); + } + return keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.rsa_encrypt) && + keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.elgamal) && + keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.ecdh) && + (!signature.keyFlags || + (signature.keyFlags[0] & enums.keyFlags.sign_data) !== 0); + } +}; /** * Returns last created key or key by given keyId that is available for encryption or decryption @@ -346,8 +342,9 @@ Key.prototype.getEncryptionKey = async function(keyId, date=new Date(), userId={ for (let i = 0; i < subKeys.length; i++) { if (!keyId || subKeys[i].getKeyId().equals(keyId)) { if (await subKeys[i].verify(primaryKey, date) === enums.keyStatus.valid) { - const bindingSignature = getLatestSignature(subKeys[i].bindingSignatures, date); - if (isValidEncryptionKeyPacket(subKeys[i].keyPacket, bindingSignature, date)) { + const dataToVerify = { key: primaryKey, bind: subKeys[i].keyPacket }; + const bindingSignature = await getLatestValidSignature(subKeys[i].bindingSignatures, primaryKey, dataToVerify, date); + if (bindingSignature && isValidEncryptionKeyPacket(subKeys[i].keyPacket, bindingSignature)) { return subKeys[i]; } } @@ -356,11 +353,24 @@ Key.prototype.getEncryptionKey = async function(keyId, date=new Date(), userId={ // if no valid subkey for encryption, evaluate primary key const primaryUser = await this.getPrimaryUser(date, userId); if (primaryUser && (!keyId || primaryKey.getKeyId().equals(keyId)) && - isValidEncryptionKeyPacket(primaryKey, primaryUser.selfCertification, date)) { + isValidEncryptionKeyPacket(primaryKey, primaryUser.selfCertification)) { return this; } } return null; + + function isValidEncryptionKeyPacket(keyPacket, signature) { + if (!signature.verified || signature.revoked !== false) { // Sanity check + throw new Error('Signature not verified'); + } + return keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.dsa) && + keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.rsa_sign) && + keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.ecdsa) && + keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.eddsa) && + (!signature.keyFlags || + (signature.keyFlags[0] & enums.keyFlags.encrypt_communication) !== 0 || + (signature.keyFlags[0] & enums.keyFlags.encrypt_storage) !== 0); + } }; /** @@ -473,11 +483,12 @@ Key.prototype.verifyPrimaryKey = async function(date=new Date(), userId={}) { /** * Returns the latest date when the key can be used for encrypting, signing, or both, depending on the `capabilities` paramater. * When `capabilities` is null, defaults to returning the expiry date of the primary key. + * Returns null if `capabilities` is passed and the key does not have the specified capabilities or is revoked or invalid. * Returns Infinity if the key doesn't expire. * @param {encrypt|sign|encrypt_sign} capabilities, optional * @param {module:type/keyid} keyId, optional * @param {Object} userId, optional user ID - * @returns {Promise} + * @returns {Promise} * @async */ Key.prototype.getExpirationTime = async function(capabilities, keyId, userId) { @@ -492,13 +503,13 @@ Key.prototype.getExpirationTime = async function(capabilities, keyId, userId) { if (capabilities === 'encrypt' || capabilities === 'encrypt_sign') { const encryptKey = await this.getEncryptionKey(keyId, null, userId); if (!encryptKey) return null; - const encryptExpiry = encryptKey.getExpirationTime(); + const encryptExpiry = await encryptKey.getExpirationTime(this.keyPacket); if (encryptExpiry < expiry) expiry = encryptExpiry; } if (capabilities === 'sign' || capabilities === 'encrypt_sign') { const signKey = await this.getSigningKey(keyId, null, userId); if (!signKey) return null; - const signExpiry = signKey.getExpirationTime(); + const signExpiry = await signKey.getExpirationTime(this.keyPacket); if (signExpiry < expiry) expiry = signExpiry; } return expiry; @@ -515,16 +526,20 @@ Key.prototype.getExpirationTime = async function(capabilities, keyId, userId) { * @async */ Key.prototype.getPrimaryUser = async function(date=new Date(), userId={}) { - const users = this.users.map(function(user, index) { - const selfCertification = getLatestSignature(user.selfCertifications, date); - return { index, user, selfCertification }; - }).filter(({ user, selfCertification }) => { - return user.userId && selfCertification && ( + const primaryKey = this.keyPacket; + const users = []; + for (let i = 0; i < this.users.length; i++) { + const user = this.users[i]; + if (!user.userId || !( (userId.name === undefined || user.userId.name === userId.name) && (userId.email === undefined || user.userId.email === userId.email) && (userId.comment === undefined || user.userId.comment === userId.comment) - ); - }); + )) continue; + const dataToVerify = { userId: user.userId, key: primaryKey }; + const selfCertification = await getLatestValidSignature(user.selfCertifications, primaryKey, dataToVerify, date); + if (!selfCertification) continue; + users.push({ index: i, user, selfCertification }); + } if (!users.length) { if (userId.name !== undefined || userId.email !== undefined || userId.comment !== undefined) { @@ -539,18 +554,9 @@ Key.prototype.getPrimaryUser = async function(date=new Date(), userId={}) { return A.isPrimaryUserID - B.isPrimaryUserID || A.created - B.created; }).pop(); const { user, selfCertification: cert } = primaryUser; - const primaryKey = this.keyPacket; - const dataToVerify = { userId: user.userId, key: primaryKey }; - // skip if certificates is invalid, revoked, or expired - if (!(cert.verified || await cert.verify(primaryKey, dataToVerify))) { - return null; - } if (cert.revoked || await user.isRevoked(primaryKey, cert, null, date)) { return null; } - if (cert.isExpired(date)) { - return null; - } return primaryUser; }; @@ -678,12 +684,15 @@ Key.prototype.revoke = async function({ /** * Get revocation certificate from a revoked key. * (To get a revocation certificate for an unrevoked key, call revoke() first.) - * @returns {String} armored revocation certificate + * @returns {Promise} armored revocation certificate + * @async */ -Key.prototype.getRevocationCertificate = function() { - if (this.revocationSignatures.length) { +Key.prototype.getRevocationCertificate = async function() { + const dataToVerify = { key: this.keyPacket }; + const revocationSignature = await getLatestValidSignature(this.revocationSignatures, this.keyPacket, dataToVerify); + if (revocationSignature) { const packetlist = new packet.List(); - packetlist.push(getLatestSignature(this.revocationSignatures)); + packetlist.push(revocationSignature); return armor.encode(enums.armor.public_key, packetlist.write(), null, null, 'This is a revocation certificate'); } }; @@ -1090,17 +1099,17 @@ SubKey.prototype.verify = async function(primaryKey, date=new Date()) { const that = this; const dataToVerify = { key: primaryKey, bind: this.keyPacket }; // check subkey binding signatures - const bindingSignature = getLatestSignature(this.bindingSignatures, date); + const bindingSignature = await getLatestValidSignature(this.bindingSignatures, primaryKey, dataToVerify, date); // check binding signature is verified - if (!(bindingSignature.verified || await bindingSignature.verify(primaryKey, dataToVerify))) { + if (!bindingSignature) { return enums.keyStatus.invalid; } // check binding signature is not revoked if (bindingSignature.revoked || await that.isRevoked(primaryKey, bindingSignature, null, date)) { return enums.keyStatus.revoked; } - // check binding signature is not expired (ie, check for V4 expiration time) - if (bindingSignature.isExpired(date)) { + // check for expiration time + if (isDataExpired(this.keyPacket, bindingSignature, date)) { return enums.keyStatus.expired; } return enums.keyStatus.valid; // binding signature passed all checks @@ -1108,11 +1117,17 @@ SubKey.prototype.verify = async function(primaryKey, date=new Date()) { /** * Returns the expiration time of the subkey or Infinity if key does not expire + * Returns null if the subkey is invalid. + * @param {module:packet.SecretKey| + * module:packet.PublicKey} primaryKey The primary key packet * @param {Date} date Use the given date instead of the current time - * @returns {Date} + * @returns {Promise} + * @async */ -SubKey.prototype.getExpirationTime = function(date=new Date()) { - const bindingSignature = getLatestSignature(this.bindingSignatures, date); +SubKey.prototype.getExpirationTime = async function(primaryKey, date=new Date()) { + const dataToVerify = { key: primaryKey, bind: this.keyPacket }; + const bindingSignature = await getLatestValidSignature(this.bindingSignatures, primaryKey, dataToVerify, date); + if (!bindingSignature) return null; const keyExpiry = getExpirationTime(this.keyPacket, bindingSignature); const sigExpiry = bindingSignature.getExpirationTime(); return keyExpiry < sigExpiry ? keyExpiry : sigExpiry; @@ -1186,8 +1201,6 @@ SubKey.prototype.revoke = async function(primaryKey, { return subKey; }; -/** - */ ['getKeyId', 'getFingerprint', 'getAlgorithmInfo', 'getCreationTime', 'isDecrypted'].forEach(name => { Key.prototype[name] = SubKey.prototype[name] = @@ -1207,9 +1220,16 @@ SubKey.prototype.revoke = async function(primaryKey, { export async function read(data) { const result = {}; result.keys = []; + const err = []; try { const packetlist = new packet.List(); await packetlist.read(data); + if (packetlist.filterByTag(enums.packet.signature).some( + signature => signature.revocationKeyClass !== null + )) { + // Indicate an error, but still parse the key. + err.push(new Error('This key is intended to be revoked with an authorized key, which OpenPGP.js does not support.')); + } const keyIndex = packetlist.indexOfTag(enums.packet.publicKey, enums.packet.secretKey); if (keyIndex.length === 0) { throw new Error('No key packet found'); @@ -1220,13 +1240,14 @@ export async function read(data) { const newKey = new Key(oneKeyList); result.keys.push(newKey); } catch (e) { - result.err = result.err || []; - result.err.push(e); + err.push(e); } } } catch (e) { - result.err = result.err || []; - result.err.push(e); + err.push(e); + } + if (err.length) { + result.err = err; } return result; } @@ -1545,8 +1566,19 @@ async function isDataRevoked(primaryKey, dataToVerify, revocations, signature, k const normDate = util.normalizeDate(date); const revocationKeyIds = []; await Promise.all(revocations.map(async function(revocationSignature) { - if (!(config.revocations_expire && revocationSignature.isExpired(normDate)) && - (revocationSignature.verified || await revocationSignature.verify(key, dataToVerify))) { + if ( + // Note: a third-party revocation signature could legitimately revoke a + // self-signature if the signature has an authorized revocation key. + // However, we don't support passing authorized revocation keys, nor + // verifying such revocation signatures. Instead, we indicate an error + // when parsing a key with an authorized revocation key, and ignore + // third-party revocation signatures here. (It could also be revoking a + // third-party key certification, which should only affect + // `verifyAllCertifications`.) + (!signature || revocationSignature.issuerKeyId.equals(signature.issuerKeyId)) && + !(config.revocations_expire && revocationSignature.isExpired(normDate)) && + (revocationSignature.verified || await revocationSignature.verify(key, dataToVerify)) + ) { // TODO get an identifier of the revoked object instead revocationKeyIds.push(revocationSignature.issuerKeyId); return true; @@ -1556,7 +1588,7 @@ async function isDataRevoked(primaryKey, dataToVerify, revocations, signature, k // TODO further verify that this is the signature that should be revoked if (signature) { signature.revoked = revocationKeyIds.some(keyId => keyId.equals(signature.issuerKeyId)) ? true : - signature.revoked; + signature.revoked || false; return signature.revoked; } return revocationKeyIds.length > 0; diff --git a/src/openpgp.js b/src/openpgp.js index e9540a55..8fb07ef8 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -124,7 +124,7 @@ export function generateKey({ userIds=[], passphrase="", numBits=2048, keyExpira } return generate(options).then(async key => { - const revocationCertificate = key.getRevocationCertificate(); + const revocationCertificate = await key.getRevocationCertificate(); key.revocationSignatures = []; return convertStreams({ @@ -159,8 +159,8 @@ export function reformatKey({privateKey, userIds=[], passphrase="", keyExpiratio options.revoked = options.revocationCertificate; - return reformat(options).then(key => { - const revocationCertificate = key.getRevocationCertificate(); + return reformat(options).then(async key => { + const revocationCertificate = await key.getRevocationCertificate(); key.revocationSignatures = []; return { @@ -344,7 +344,7 @@ export function encrypt({ message, publicKeys, privateKeys, passwords, sessionKe * @param {String|Array} passwords (optional) passwords to decrypt the message * @param {Object|Array} sessionKeys (optional) session keys in the form: { data:Uint8Array, algorithm:String } * @param {Key|Array} publicKeys (optional) array of public keys or single key, to verify signatures - * @param {String} format (optional) return data format either as 'utf8' or 'binary' + * @param {'utf8'|'binary'} format (optional) whether to return data as a string(Stream) or Uint8Array(Stream). If 'utf8' (the default), also normalize newlines. * @param {'web'|'node'|false} streaming (optional) whether to return data as a stream. Defaults to the type of stream `message` was created from, if any. * @param {Signature} signature (optional) detached signature for verification * @param {Date} date (optional) use the given date for verification instead of the current time diff --git a/src/packet/packetlist.js b/src/packet/packetlist.js index 12b09754..f1394017 100644 --- a/src/packet/packetlist.js +++ b/src/packet/packetlist.js @@ -21,6 +21,7 @@ import util from '../util'; * are stored as numerical indices. * @memberof module:packet * @constructor + * @extends Array */ function List() { /** @@ -31,6 +32,8 @@ function List() { this.length = 0; } +List.prototype = []; + /** * Reads a stream of binary data and interprents it as a list of packets. * @param {Uint8Array | ReadableStream} A Uint8Array of bytes. @@ -145,37 +148,6 @@ List.prototype.push = function (packet) { this.length++; }; -/** - * Remove a packet from the list and return it. - * @returns {Object} The packet that was removed - */ -List.prototype.pop = function() { - if (this.length === 0) { - return; - } - - const packet = this[this.length - 1]; - delete this[this.length - 1]; - this.length--; - - return packet; -}; - -/** - * Creates a new PacketList with all packets that pass the test implemented by the provided function. - */ -List.prototype.filter = function (callback) { - const filtered = new List(); - - for (let i = 0; i < this.length; i++) { - if (callback(this[i], i, this)) { - filtered.push(this[i]); - } - } - - return filtered; -}; - /** * Creates a new PacketList with all packets from the given types */ @@ -193,58 +165,6 @@ List.prototype.filterByTag = function (...args) { return filtered; }; -/** - * Executes the provided callback once for each element - */ -List.prototype.forEach = function (callback) { - for (let i = 0; i < this.length; i++) { - callback(this[i], i, this); - } -}; - -/** - * Returns an array containing return values of callback - * on each element - */ -List.prototype.map = function (callback) { - const packetArray = []; - - for (let i = 0; i < this.length; i++) { - packetArray.push(callback(this[i], i, this)); - } - - return packetArray; -}; - -/** - * Executes the callback function once for each element - * until it finds one where callback returns a truthy value - * @param {Function} callback - * @returns {Promise} - * @async - */ -List.prototype.some = async function (callback) { - for (let i = 0; i < this.length; i++) { - if (await callback(this[i], i, this)) { - return true; - } - } - return false; -}; - -/** - * Executes the callback function once for each element, - * returns true if all callbacks returns a truthy value - */ -List.prototype.every = function (callback) { - for (let i = 0; i < this.length; i++) { - if (!callback(this[i], i, this)) { - return false; - } - } - return true; -}; - /** * Traverses packet tree and returns first matching packet * @param {module:enums.packet} type The packet type @@ -285,20 +205,6 @@ List.prototype.indexOfTag = function (...args) { return tagIndex; }; -/** - * Returns slice of packetlist - */ -List.prototype.slice = function (begin, end) { - if (!end) { - end = this.length; - } - const part = new List(); - for (let i = begin; i < end; i++) { - part.push(this[i]); - } - return part; -}; - /** * Concatenates packetlist or array of packets */ diff --git a/src/packet/signature.js b/src/packet/signature.js index 0121cfd0..25464690 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -201,6 +201,12 @@ Signature.prototype.sign = async function (key, data) { this.signature = stream.fromAsync(async () => crypto.signature.sign( publicKeyAlgorithm, hashAlgorithm, params, toHash, await stream.readToEnd(hash) )); + + // Store the fact that this signature is valid, e.g. for when we call `await + // getLatestValidSignature(this.revocationSignatures, key, data)` later. Note + // that this only holds up if the key and data passed to verify are the same + // as the ones passed to sign. + this.verified = true; return true; }; diff --git a/test/general/key.js b/test/general/key.js index 1275f2f3..eb21125f 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1223,6 +1223,111 @@ t/ia1kMpSEiOVLlX5dfHZzhR3WNtBqU= =C0fJ -----END PGP PRIVATE KEY BLOCK-----`; +const key_with_authorized_revocation_key = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: OpenPGP.js VERSION +Comment: https://openpgpjs.org + +xsBNBFujnwwBCADK1xX03tSCmktPDS9Ncij3O5wG+F5/5Zm7QJDc39Wt1t/K +szCSobWtm/UObQVZjsTGwg0ZUPgepgWGGDBL0dlc1NObwUiOhGYnJnd4V25P +iU5Mg3+DhRq+LzNK+oGlpVPDpwQ48S8HOphbswKUpuaDcEQ2f+NKIc0eXe5m +ut5x9uoVj8jneUNsHYq6FIlxh4knzpJWFj5+LNi7plMCwKip6srVNf8He/q0 +0xA/4vjSIOfGIE7TCBH33CbHEr98p81Cf4g0E+kswEz5iWE2SDCyYgQkMrkz +H9mtVqk3nFT8NR0USxKqH9bGhaTx1AWq/HDgsphayPEWQ0usjQDrbQUnABEB +AAHNDnRlc3QgPGFAYi5jb20+wsCNBBABCABBBQJbo58MBgsJBwgDAhcMgAH0 +cOUNyxrV8eZOCGRKY2E6TW5AlAkQWICi/ReDcrkEFQgKAgMWAgECGQECGwMC +HgEAADgHB/0WIHh6maX2LZ0u5ujk1tZxWMrCycccopdQNKN0RGD98X4fyY6J +wfmKb107gcidJBFct0sVWFW8GU42w9pVMU5qWD6kyFkgcmov319UL+7aZ19b +HOWVKUTb6rFG8/qAbq3BF7YB/cZIBWMFKAS3BRJ4Kz23GheAB2A9oVLmuq5o +gW5c2R1YC0T0XyXEFiw9uZ+AS6kEZymFPRQfPUIbJs1ct/lAN+mC9Qp0Y6CL +60Hd6jlKUb6TgljaQ6CtLfT9v72GeKznapKr9IEtsgYv69j0c/MRM2nmu50c +g+fICiiHrTbNS6jkUz0pZLe7hdhWHeEiqcA9+GC1DxOQCRCS/YNfzsBNBFuj +nwwBCADK1xX03tSCmktPDS9Ncij3O5wG+F5/5Zm7QJDc39Wt1t/KszCSobWt +m/UObQVZjsTGwg0ZUPgepgWGGDBL0dlc1NObwUiOhGYnJnd4V25PiU5Mg3+D +hRq+LzNK+oGlpVPDpwQ48S8HOphbswKUpuaDcEQ2f+NKIc0eXe5mut5x9uoV +j8jneUNsHYq6FIlxh4knzpJWFj5+LNi7plMCwKip6srVNf8He/q00xA/4vjS +IOfGIE7TCBH33CbHEr98p81Cf4g0E+kswEz5iWE2SDCyYgQkMrkzH9mtVqk3 +nFT8NR0USxKqH9bGhaTx1AWq/HDgsphayPEWQ0usjQDrbQUnABEBAAHCwF8E +GAEIABMFAlujnwwJEFiAov0Xg3K5AhsMAACI/QgArvTcutod+7n1D8wCwM50 +jo3x4KPuQw+NwbOnMbFwv0R8i8NqtSFf2bYkkZ7RLViTmphvSon4h2WgfczL +SBulZ1QZF7zCKXmXDg8/HZgRUflC1XMixpB8Hqouin5AVgMbsbHg30V2uPco +V3DeFQ8HWxQC9symaMW/20MkqNXgCjM0us7kVwTEJQqZ6KYrFVjKyprSQRyP +rfckEBuZnj91OS+kAHlZ+ScZIuV4QVF0e2U6oEuB+qFXppR030PJoO6WDOzm +67hkzfrc3VpKw/I+vVJnZb4GOhNTSpa+p8i4gTyWmrOZ0G85QoldHWlWaRBg +lbjwPj3QUTbLFvHisYzXEQ== +=aT8U +-----END PGP PUBLIC KEY BLOCK----- +`; + +const key_with_revoked_third_party_cert = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: GPGTools - https://gpgtools.org + +mQENBFS2KSEBCACzz8KtwE5ualmgF+rKo8aPQ9inTQWCNzCuTs3HaSe0D5heGoSh +mJWl9B5zvXN78L3yzmtWQV92CXOkCRWezIY8y+aN+aJZ6PzPE5Yy74404v3yG9ZK +jGlAWC7Wgkx+YR2vbzj7hDqi5e6TpDGsFkH3OsI3nY7FIvXWbz9Ih4/s/nBPuF0v +sBZ0n97ItszhnrXvvrF1fQvEviB0+xF5DfUURWP45EA+NWnBl7HFzY4FeN5ImYZK +Nt6A88i9SIB3MiwRSUy1UwJjL2L8l+rLbr20JbnIUuJN3h/dY10igxyOh5gsXtr1 +fabsm6s2AacrCjQqLkXSnB8Ucu+Enz5R1s0dABEBAAG0KVBhc3N3b3J0ICDDpMOE +Pz9eXjEyIDIgwrUgIDxwd2RAdGVzdC5jb20+iFoEMBEKABoFAlYvilYTHSBJY2gg +d2VpcyBlcyBuaWNodAAKCRDheQpvjBNXTQ5jAKC0AMb5Ivoy0DKNI8Hjus72ob3u +TACg32AGuCernx1Wt7/5oi4KdjxjjxeJATIEEAEIACYFAlS2KSIGCwkIBwMCCRBj +ZJ9T2gveXAQVCAIKAxYCAQIbAwIeAQAA/c4H/i/NgI36q/2lwcRkt5rsVBUlx+Ho ++iKIEh1+XKfDq4A8DTjeYCCOg/k3hDm2LpmmclwRc2X9CMwraSoFTEN6Em78Kd5a +DFaNPbWGP0RCW5zqPGAoZSvOlZYsLMaswFMBD93wf3XwHK8HxTJhTmQC1kGSplO1 +GMWkTh6B3tqiy/Jk7Hp5mPASQBid+E9rjr8CgOPF26hjTw+UUBs2ZWO9U9PyhBYH +xLIwtUDoZvqhTdXD0aV7vhRQw6p3UEzxa8t/1iGogHe2CfcMgq5jYmHLTZN3VGE3 +djwLQIikRRig7dTBD9BgeG6a+22XRbvpOsHBzsAH4UC97nS+wzkgkyvqfOKIRgQQ +EQoABgUCVi+JYAAKCRDheQpvjBNXTTNIAJwJacb4mKUPrRnnNcGXC6hjizuJOACg +zVFVnWA/A+GrHBogUD780vcJwMG5AQ0EVLYpIQEIANNCJ5sUKv6YDWRToF/tG6ik +LjTAcNelr5LCXLT3Y7CAmk7y88vzCaTLZZUWgwyK8lYGZ3x2icoc4fJeo5BhHNJz +TSL239cTsAugNoVMJFG2xm1TEzBsBCNPOOVpS5cArt6mmhxozwkafawtgA+5z0zB +vQm0AHPudSAJp3Gx69meRzAJgdFVgljZVyCUmQizJqJ1dQPPgarpJKJy3f0+g0ec +yx+gTA4nj+zfqjrXM4O1Ok/Di+8mneA3bhadiYU1VjkqY+1UqkQOU0UFdDlBppRj +xr6h00xECoayyPXr/U+gFSgZHO1mKk3meCyNVKLGAajQxWVWfBwoPixfqOXlBh0A +EQEAAYkBHwQYAQgAEwUCVLYpIwkQY2SfU9oL3lwCGwwAAMkDB/9QeLRyEOX2LWdZ +UkxSldMklAvMBjqY27cC1Qn8wiWzHNJKVEnQ9MiKn0mVRohkRKgsiWfSugDVyVov +eTM7PSjDlAYALCSxSYStykordUSf/5WYb9Mmea4J/WXBQCvwJKFU47ZDl2Tg+HdS +SVznLTt/ASxd2Nap2mUveC4ivGdSo1KOq3+t95xGC7dh4U3JwPanBZ6cfBJbnSEs +QWLUAPnlfn37Ff14haRETX3ND82bkXKEGEk6O5HJ0PWuUtl+TFIkYUGh4zFOZZWq +VHwaffAHDrmTZt/pOXg9l/VFnzfxrId33Tog3Zvejm2+8d2LhSCtfdrdJ/Dx2CZM +Yzxp9Mp9 +=uVX6 +-----END PGP PUBLIC KEY BLOCK----- +`; + +const certifying_key = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: GPGTools - https://gpgtools.org + +mQGiBEOd+/gRBACfqCfgQCmUzOr7iA1CerGVmFm8HcN+NVGSpkwF6pmPJh1XVGEA +Nz9Aok6Vx4MQ+QCKo9dTXMZWDE4W/vzaKaEmsirsxGgn7JhK0t/9VeXXieWiJirA +5iTQMsRjfnS6MLLUr56E7HmDZftiOcpJu81S943r+oeINhq37SlJM7Q47wCg8miR +egss26IZfW3RvBuNW1KEDh0D/195DH6sl+/qmgUAj3M7ai1iKOqkILdNuIkYRc18 +bsBYIAOjY81guhlEabYEqv8FUKPh2A7dAe/4og89HrUsVxOKJ9EGyrcqyJj676gK +BL383t1dQvyJyWfV5z4umUMaF/xE46go3Trdyu86aJDe57B74RYbWL2aaGLtkPJ2 +rHOnBACG5PmLM6EXWJQSfbRqJHlRysc3MOAXMnxu8DVz+Tm0nPcArUG+Mf3GmxT+ +537nHqF2xEgVvwfkvGB51ideRRQMhHUzy583zkuPYV1cJ6ykfeA6EHzVbT4vRzO8 +AW9ELBKTK9F4N4gGTOdAATcaMC0gwzCz+fofJEJqC/9CS2pYvrQlVGhvbWFzIE9i +ZXJuZMO2cmZlciA8dG9iZXJuZG9AZ214LmRlPohdBBMRAgAdBQJDnfv4BgsJCAcD +AgQVAggDBBYCAwECHgECF4AACgkQ4XkKb4wTV02nkACfWvWnRawPI9AmgXpg6jg1 +/22exKkAoMJ+yFhjtuGobOrIAPcEYrwlTQXBiGsEEBECACsFAksJKNQFgwHihQAe +Gmh0dHA6Ly93d3cuY2FjZXJ0Lm9yZy9jcHMucGhwAAoJENK7DQFl0P1Y4VoAmgMc +2qWuBtOBb6Uj6DskTtXORlPgAKCB3Hqp8dJ3dbQh5m0Ebf8F3P71WrkCDQRDnfw1 +EAgAkp+0jMpfKKF6Gxy63fP3tCFzn9vq3GBtbNnvp8b0dx6nf+ZxELt/q9K4Yz9g ++sXq0RFQGV+YwS2BGoogzRcT4PHmUBcEAbjZIs9lZdZDEF0/68d+32mHSkLZJxGI +ezXJK3+MpGPnCMbQ63UYpcY1BvL7Vbj6P4X75dJJReGIHQMBA0FEYB5AVm6HrWU5 +eDvOZ2w8QAAUluFnD9/xNRqBpcwm5uoox7zq60W5coK6p6WX8t5+WMMrRKF2A1Ru +aTxYQKo3f8XQA4e6tEcdGFlk1K9W8Ov1xVRQa6EqQYZFesbuoo8HHuSNsJ7PQrP+ +vyYcafohlO/q4QtJXoUimsrEywADBQf7BQWrEx9YlNNsUD2yuov8pYCymxfUVTzK +huxGHmNj1htXfWWScA2uqD97HOdFu5nvoL2tdaO1RQR/OXKRBcUg6FhOQqqxQSxi +Vcsoy3aofGi3CWVXgn7KlSopkhlb4ELjzt5H+BMneXdgowO4MimXAfivI7OZl2fN +ut7emyN9qaeY/e25UKmCYhmhE5hM2+lV8wEmmu/qTCPiZ2u0zH/PE9AAwRz/6X+p +gsW0WIQpI6iQSSq4KyJxebtJFmCSTFawuXB6rCGovDXo/BkRsDEj1rpZnkwKJPa0 +dEhKK4EzNrUzpWHeE3gKPjFXVmcjIPWVAC3BJoJRHOHg8wqLKcX5MYhGBBgRAgAG +BQJDnfw1AAoJEOF5Cm+ME1dNChoAoMKa/qx/RKlu3iQPtN6p4NlhRA9IAJ94F/7l +cKFQz1DDfFCfVpSIJRGozQ== +=EYzO +-----END PGP PUBLIC KEY BLOCK----- +`; + function versionSpecificTests() { it('Preferences of generated key', function() { const testPref = function(key) { @@ -1389,7 +1494,7 @@ function versionSpecificTests() { const actual_delta = (new Date(expiration) - new Date()) / 1000; expect(Math.abs(actual_delta - expect_delta)).to.be.below(60); - const subKeyExpiration = await key.subKeys[0].getExpirationTime(); + const subKeyExpiration = await key.subKeys[0].getExpirationTime(key.primaryKey); expect(subKeyExpiration).to.exist; const actual_subKeyDelta = (new Date(subKeyExpiration) - new Date()) / 1000; @@ -1688,6 +1793,15 @@ describe('Key', function() { expect(pubKeys.keys[1].getKeyId().toHex()).to.equal('dbf223e870534df4'); }); + it('Parsing armored key with an authorized revocation key', async function() { + const pubKeys = await openpgp.key.readArmored(key_with_authorized_revocation_key); + expect(pubKeys).to.exist; + expect(pubKeys.err).to.exist.and.have.length(1); + expect(pubKeys.err[0].message).to.equal('This key is intended to be revoked with an authorized key, which OpenPGP.js does not support.'); + expect(pubKeys.keys).to.have.length(1); + expect(pubKeys.keys[0].getKeyId().toHex()).to.equal('5880a2fd178372b9'); + }); + it('Parsing V5 public key packet', async function() { // Manually modified from https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b2092/back.mkd#sample-eddsa-key let packetBytes = openpgp.util.hex_to_Uint8Array(` @@ -1765,6 +1879,26 @@ describe('Key', function() { )).to.eventually.equal(openpgp.enums.keyStatus.revoked); }); + it('Verify status of key with non-self revocation signature', async function() { + const { keys: [pubKey] } = await openpgp.key.readArmored(key_with_revoked_third_party_cert); + const [selfCertification] = await pubKey.verifyPrimaryUser(); + const publicSigningKey = await pubKey.getSigningKey(); + expect(selfCertification.keyid.toHex()).to.equal(publicSigningKey.getKeyId().toHex()); + expect(selfCertification.valid).to.be.true; + + const { keys: [certifyingKey] } = await openpgp.key.readArmored(certifying_key); + const certifyingSigningKey = await certifyingKey.getSigningKey(); + const signatures = await pubKey.verifyPrimaryUser([certifyingKey]); + expect(signatures.length).to.equal(2); + expect(signatures[0].keyid.toHex()).to.equal(publicSigningKey.getKeyId().toHex()); + expect(signatures[0].valid).to.be.null; + expect(signatures[1].keyid.toHex()).to.equal(certifyingSigningKey.getKeyId().toHex()); + expect(signatures[1].valid).to.be.false; + + const { user } = await pubKey.getPrimaryUser(); + expect(await user.verifyCertificate(pubKey.primaryKey, user.otherCertifications[0], [certifyingKey])).to.equal(openpgp.enums.keyStatus.revoked); + }); + it('Evaluate key flags to find valid encryption key packet', async function() { const pubKeys = await openpgp.key.readArmored(pub_sig_test); expect(pubKeys).to.exist; @@ -1799,7 +1933,7 @@ describe('Key', function() { const pubKey = (await openpgp.key.readArmored(twoKeys)).keys[1]; expect(pubKey).to.exist; expect(pubKey).to.be.an.instanceof(openpgp.key.Key); - const expirationTime = await pubKey.subKeys[0].getExpirationTime(); + const expirationTime = await pubKey.subKeys[0].getExpirationTime(pubKey.primaryKey); expect(expirationTime.toISOString()).to.be.equal('2018-11-26T10:58:29.000Z'); }); @@ -1990,7 +2124,7 @@ describe('Key', function() { it('getRevocationCertificate() should produce the same revocation certificate as GnuPG', async function() { const revKey = (await openpgp.key.readArmored(revoked_key_arm4)).keys[0]; - const revocationCertificate = revKey.getRevocationCertificate(); + const revocationCertificate = await revKey.getRevocationCertificate(); const input = await openpgp.armor.decode(revocation_certificate_arm4); const packetlist = new openpgp.packet.List(); @@ -2002,7 +2136,7 @@ describe('Key', function() { it('getRevocationCertificate() should have an appropriate comment', async function() { const revKey = (await openpgp.key.readArmored(revoked_key_arm4)).keys[0]; - const revocationCertificate = revKey.getRevocationCertificate(); + const revocationCertificate = await revKey.getRevocationCertificate(); expect(revocationCertificate).to.match(/Comment: This is a revocation certificate/); expect(revKey.armor()).not.to.match(/Comment: This is a revocation certificate/); @@ -2170,7 +2304,27 @@ VYGdb3eNlV8CfoEC it('Selects the most recent subkey binding signature', async function() { const key = (await openpgp.key.readArmored(multipleBindingSignatures)).keys[0]; - expect(key.subKeys[0].getExpirationTime().toISOString()).to.equal('2015-10-18T07:41:30.000Z'); + expect((await key.subKeys[0].getExpirationTime(key.primaryKey)).toISOString()).to.equal('2015-10-18T07:41:30.000Z'); + }); + + it('Selects the most recent non-expired subkey binding signature', async function() { + const key = (await openpgp.key.readArmored(multipleBindingSignatures)).keys[0]; + key.subKeys[0].bindingSignatures[1].signatureNeverExpires = false; + key.subKeys[0].bindingSignatures[1].signatureExpirationTime = 0; + expect((await key.subKeys[0].getExpirationTime(key.primaryKey)).toISOString()).to.equal('2018-09-07T06:03:37.000Z'); + }); + + it('Selects the most recent valid subkey binding signature', async function() { + const key = (await openpgp.key.readArmored(multipleBindingSignatures)).keys[0]; + key.subKeys[0].bindingSignatures[1].signatureData[0]++; + expect((await key.subKeys[0].getExpirationTime(key.primaryKey)).toISOString()).to.equal('2018-09-07T06:03:37.000Z'); + }); + + it('Handles a key with no valid subkey binding signatures gracefully', async function() { + const key = (await openpgp.key.readArmored(multipleBindingSignatures)).keys[0]; + key.subKeys[0].bindingSignatures[0].signatureData[0]++; + key.subKeys[0].bindingSignatures[1].signatureData[0]++; + expect(await key.subKeys[0].getExpirationTime(key.primaryKey)).to.be.null; }); it('Reject encryption with revoked subkey', async function() { diff --git a/test/serviceworker.js b/test/serviceworker.js deleted file mode 100644 index 157fd0f5..00000000 --- a/test/serviceworker.js +++ /dev/null @@ -1,47 +0,0 @@ - - - - -// addEventListener('fetch', event => { -// console.log(event); -// const url = new URL(event.request.url); -// console.log(url); -// if (url.pathname === '/test/somedata') { -// let plaintext = []; -// let i = 0; -// let canceled = false; -// const data = new ReadableStream({ -// /*start(_controller) { -// controller = _controller; -// },*/ -// async pull(controller) { -// await new Promise(resolve => setTimeout(resolve, 1000)); -// console.log(i); -// if (i++ < 10) { -// let randomBytes = new Uint8Array(1000); -// randomBytes.fill(i); -// controller.enqueue(randomBytes); -// plaintext.push(randomBytes); -// } else { -// controller.close(); -// } -// }, -// cancel() { -// console.log('canceled!'); -// } -// }); - - -// const response = new Response(data, { -// headers: { -// 'Content-Type': 'application/octet-stream; charset=utf-8', -// 'Content-Disposition': 'Content-Disposition: attachment; filename=data.bin;' -// } -// }); - -// event.respondWith(response); -// } - -// }); - - \ No newline at end of file