diff --git a/README.md b/README.md index a62a6641..92bbfb61 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,30 @@ var options = { openpgp.generateKey(options).then(function(key) { var privkey = key.privateKeyArmored; // '-----BEGIN PGP PRIVATE KEY BLOCK ... ' var pubkey = key.publicKeyArmored; // '-----BEGIN PGP PUBLIC KEY BLOCK ... ' + var revocationSignature = key.revocationSignature; // '-----BEGIN PGP PUBLIC KEY BLOCK ... ' +}); +``` + +#### Revoke a key + +Using a revocation signature: +```js +var options = { + key: openpgp.key.readArmored(pubkey).keys[0], + revocationSignature: revocationSignature +}; +``` + +Using the private key: +```js +var options = { + key: openpgp.key.readArmored(privkey).keys[0] +}; +``` + +```js +openpgp.revokeKey(options).then(function(key) { + var pubkey = key.publicKeyArmored; // '-----BEGIN PGP PUBLIC KEY BLOCK ... ' }); ``` diff --git a/src/encoding/armor.js b/src/encoding/armor.js index 24298528..c18d818c 100644 --- a/src/encoding/armor.js +++ b/src/encoding/armor.js @@ -95,9 +95,10 @@ function getType(text) { * packet block. * @author Alex * @version 2011-12-16 + * @param {String} customComment (optional) additional comment to add to the armored string * @returns {String} The header information */ -function addheader() { +function addheader(customComment) { let result = ""; if (config.show_version) { result += "Version: " + config.versionstring + '\r\n'; @@ -105,6 +106,9 @@ function addheader() { if (config.show_comment) { result += "Comment: " + config.commentstring + '\r\n'; } + if (customComment) { + result += "Comment: " + customComment + '\r\n'; + } result += '\r\n'; return result; } @@ -326,22 +330,23 @@ function dearmor(text) { * @param body * @param {Integer} partindex * @param {Integer} parttotal + * @param {String} customComment (optional) additional comment to add to the armored string * @returns {String} Armored text * @static */ -function armor(messagetype, body, partindex, parttotal) { +function armor(messagetype, body, partindex, parttotal, customComment) { const result = []; switch (messagetype) { case enums.armor.multipart_section: result.push("-----BEGIN PGP MESSAGE, PART " + partindex + "/" + parttotal + "-----\r\n"); - result.push(addheader()); + result.push(addheader(customComment)); result.push(base64.encode(body)); result.push("\r\n=" + getCheckSum(body) + "\r\n"); result.push("-----END PGP MESSAGE, PART " + partindex + "/" + parttotal + "-----\r\n"); break; case enums.armor.multipart_last: result.push("-----BEGIN PGP MESSAGE, PART " + partindex + "-----\r\n"); - result.push(addheader()); + result.push(addheader(customComment)); result.push(base64.encode(body)); result.push("\r\n=" + getCheckSum(body) + "\r\n"); result.push("-----END PGP MESSAGE, PART " + partindex + "-----\r\n"); @@ -351,35 +356,35 @@ function armor(messagetype, body, partindex, parttotal) { result.push("Hash: " + body.hash + "\r\n\r\n"); result.push(body.text.replace(/^-/mg, "- -")); result.push("\r\n-----BEGIN PGP SIGNATURE-----\r\n"); - result.push(addheader()); + result.push(addheader(customComment)); result.push(base64.encode(body.data)); result.push("\r\n=" + getCheckSum(body.data) + "\r\n"); result.push("-----END PGP SIGNATURE-----\r\n"); break; case enums.armor.message: result.push("-----BEGIN PGP MESSAGE-----\r\n"); - result.push(addheader()); + result.push(addheader(customComment)); result.push(base64.encode(body)); result.push("\r\n=" + getCheckSum(body) + "\r\n"); result.push("-----END PGP MESSAGE-----\r\n"); break; case enums.armor.public_key: result.push("-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n"); - result.push(addheader()); + result.push(addheader(customComment)); result.push(base64.encode(body)); result.push("\r\n=" + getCheckSum(body) + "\r\n"); result.push("-----END PGP PUBLIC KEY BLOCK-----\r\n\r\n"); break; case enums.armor.private_key: result.push("-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n"); - result.push(addheader()); + result.push(addheader(customComment)); result.push(base64.encode(body)); 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(addheader(customComment)); result.push(base64.encode(body)); result.push("\r\n=" + getCheckSum(body) + "\r\n"); result.push("-----END PGP SIGNATURE-----\r\n"); diff --git a/src/enums.js b/src/enums.js index 8e152b08..f7ef4652 100644 --- a/src/enums.js +++ b/src/enums.js @@ -428,6 +428,23 @@ export default { signature: 6 }, + /** {@link https://tools.ietf.org/html/rfc4880#section-5.2.3.23|RFC4880, section 5.2.3.23} + * @enum {Integer} + * @readonly + */ + reasonForRevocation: { + /** No reason specified (key revocations or cert revocations) */ + no_reason: 0, + /** Key is superseded (key revocations) */ + key_superseded: 1, + /** Key material has been compromised (key revocations) */ + key_compromised: 2, + /** Key is retired and no longer used (key revocations) */ + key_retired: 3, + /** User ID information is no longer valid (cert revocations) */ + userid_invalid: 32 + }, + /** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.2.3.25|RFC4880bis-04, section 5.2.3.25} * @enum {Integer} * @readonly diff --git a/src/index.js b/src/index.js index 7b2b7f14..e0cd2f3c 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,7 @@ export default openpgp; */ export { encrypt, decrypt, sign, verify, - generateKey, reformatKey, decryptKey, + generateKey, reformatKey, revokeKey, decryptKey, encryptSessionKey, decryptSessionKeys, initWorker, getWorker, destroyWorker } from './openpgp'; diff --git a/src/key.js b/src/key.js index 243698b1..dce81e77 100644 --- a/src/key.js +++ b/src/key.js @@ -625,9 +625,71 @@ async function mergeSignatures(source, dest, attr, checkFn) { } } -// TODO -Key.prototype.revoke = function() { +/** + * Revokes the key + * @param {Object} reasonForRevocation optional, object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation + * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation + * @param {Date} date optional, override the creationtime of the revocation signature + * @return {module:key~Key} new key with revocation signature + */ +Key.prototype.revoke = async function({ + flag: reasonForRevocationFlag=enums.reasonForRevocation.no_reason, + string: reasonForRevocationString='' +} = {}, date=new Date()) { + if (this.isPublic()) { + throw new Error('Need private key for revoking'); + } + const dataToSign = { key: this.primaryKey }; + const key = new Key(this.toPacketlist()); + key.revocationSignatures.push(await createSignaturePacket(dataToSign, null, this.primaryKey, { + signatureType: enums.signature.key_revocation, + reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), + reasonForRevocationString + }, date)); + return key; +}; +/** + * Get revocation certificate from a revoked key. + * (To get a revocation certificate for an unrevoked key, call revoke() first.) + * @return {String} armored revocation certificate + */ +Key.prototype.getRevocationCertificate = function() { + if (this.revocationSignatures.length) { + const packetlist = new packet.List(); + packetlist.push(getLatestSignature(this.revocationSignatures)); + return armor.encode(enums.armor.public_key, packetlist.write(), null, null, 'This is a revocation certificate'); + } +}; + +/** + * Applies a revocation certificate to a key + * This adds the first signature packet in the armored text to the key, + * if it is a valid revocation signature. + * @param {String} revocationCertificate armored revocation certificate + * @return {module:key~Key} new revoked key + */ +Key.prototype.applyRevocationCertificate = async function(revocationCertificate) { + const input = armor.decode(revocationCertificate); + const packetlist = new packet.List(); + packetlist.read(input.data); + const revocationSignature = packetlist.findPacket(enums.packet.signature); + if (!revocationSignature || revocationSignature.signatureType !== enums.signature.key_revocation) { + throw new Error('Could not find revocation signature packet'); + } + if (!revocationSignature.issuerKeyId.equals(this.primaryKey.getKeyId())) { + throw new Error('Revocation signature does not match key'); + } + if (revocationSignature.isExpired()) { + throw new Error('Revocation signature is expired'); + } + if (!await revocationSignature.verify(this.primaryKey, { key: this.primaryKey })) { + throw new Error('Could not verify revocation signature'); + } + const key = new Key(this.toPacketlist()); + key.revocationSignatures.push(revocationSignature); + return key; }; /** @@ -763,15 +825,11 @@ User.prototype.sign = async function(primaryKey, privateKeys) { if (!signingKeyPacket.isDecrypted) { throw new Error('Private key is not decrypted.'); } - const signaturePacket = new packet.Signature(); - // Most OpenPGP implementations use generic certification (0x10) - signaturePacket.signatureType = enums.write(enums.signature, enums.signature.cert_generic); - signaturePacket.keyFlags = [enums.keyFlags.certify_keys | enums.keyFlags.sign_data]; - signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; - signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey); - signaturePacket.signingKeyId = signingKeyPacket.getKeyId(); - signaturePacket.sign(signingKeyPacket, dataToSign); - return signaturePacket; + return createSignaturePacket(dataToSign, privateKey, signingKeyPacket, { + // Most OpenPGP implementations use generic certification (0x10) + signatureType: enums.signature.cert_generic, + keyFlags: [enums.keyFlags.certify_keys | enums.keyFlags.sign_data] + }); })); await user.update(this, primaryKey); return user; @@ -799,6 +857,28 @@ User.prototype.isRevoked = async function(primaryKey, certificate, key, date=new ); }; +/** + * Create signature packet + * @param {Object} dataToSign Contains packets to be signed + * @param {module:packet.SecretKey| + * module:packet.SecretSubkey} signingKeyPacket secret key packet for signing + * @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 + * @return {module:packet/signature} signature packet + */ +export async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, signatureProperties, date, userId) { + if (!signingKeyPacket.isDecrypted) { + throw new Error('Private key is not decrypted.'); + } + const signaturePacket = new packet.Signature(date); + Object.assign(signaturePacket, signatureProperties); + signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; + signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKeyPacket, date, userId); + await signaturePacket.sign(signingKeyPacket, dataToSign); + return signaturePacket; +} + /** * Verifies the user certificate * @param {module:packet.SecretKey| @@ -1036,6 +1116,30 @@ SubKey.prototype.update = async function(subKey, primaryKey) { }); }; +/** + * Revokes the subkey + * @param {module:packet.SecretKey} primaryKey decrypted private primary key for revocation + * @param {Object} reasonForRevocation optional, object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation + * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation + * @param {Date} date optional, override the creationtime of the revocation signature + * @return {module:key~SubKey} new subkey with revocation signature + */ +SubKey.prototype.revoke = async function(primaryKey, { + flag: reasonForRevocationFlag=enums.reasonForRevocation.no_reason, + string: reasonForRevocationString='' +} = {}, date=new Date()) { + const dataToSign = { key: primaryKey, bind: this.subKey }; + const subKey = new SubKey(this.subKey); + subKey.revocationSignatures.push(await createSignaturePacket(dataToSign, null, primaryKey, { + signatureType: enums.signature.subkey_revocation, + reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), + reasonForRevocationString + }, date)); + await subKey.update(this, primaryKey); + return subKey; +}; + /** * Reads an unarmored OpenPGP key list and returns one or multiple key objects * @param {Uint8Array} data to be parsed @@ -1189,6 +1293,7 @@ export async function generate(options) { * @param {Date} date Override the creation date of the key and the key signatures * @param {Array} subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}] * + * @param {Boolean} [options.revoked=false] Whether the key should include a revocation signature * @returns {Promise} * @async * @static @@ -1267,7 +1372,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { const signaturePacket = new packet.Signature(options.date); signaturePacket.signatureType = enums.signature.cert_generic; signaturePacket.publicKeyAlgorithm = secretKeyPacket.algorithm; - signaturePacket.hashAlgorithm = await getPreferredHashAlgo(secretKeyPacket); + signaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, secretKeyPacket); signaturePacket.keyFlags = [enums.keyFlags.certify_keys | enums.keyFlags.sign_data]; signaturePacket.preferredSymmetricAlgorithms = []; // prefer aes256, aes128, then aes192 (no WebCrypto support: https://www.chromium.org/blink/webcrypto#TOC-AES-support) @@ -1323,7 +1428,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { const subkeySignaturePacket = new packet.Signature(subkeyOptions.date); subkeySignaturePacket.signatureType = enums.signature.subkey_binding; subkeySignaturePacket.publicKeyAlgorithm = secretKeyPacket.algorithm; - subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(secretSubkeyPacket); + subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, secretSubkeyPacket); subkeySignaturePacket.keyFlags = subkeyOptions.sign ? enums.keyFlags.sign_data : [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage]; if (subkeyOptions.keyExpirationTime > 0) { subkeySignaturePacket.keyExpirationTime = subkeyOptions.keyExpirationTime; @@ -1339,6 +1444,15 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { }); }); + // Add revocation signature packet for creating a revocation certificate. + // This packet should be removed before returning the key. + const dataToSign = { key: secretKeyPacket }; + packetlist.push(await createSignaturePacket(dataToSign, null, secretKeyPacket, { + signatureType: enums.signature.key_revocation, + reasonForRevocationFlag: enums.reasonForRevocation.no_reason, + reasonForRevocationString: '' + }, options.date)); + // set passphrase protection if (options.passphrase) { secretKeyPacket.clearPrivateParams(); @@ -1416,13 +1530,14 @@ function getExpirationTime(keyPacket, signature) { /** * Returns the preferred signature hash algorithm of a key - * @param {object} key + * @param {module:key.Key} key (optional) the key to get preferences from + * @param {module:packet.SecretKey|module:packet.SecretSubkey} keyPacket key packet used for signing * @param {Date} date (optional) use the given date for verification instead of the current time * @param {Object} userId (optional) user ID * @returns {Promise} * @async */ -export async function getPreferredHashAlgo(key, date=new Date(), userId={}) { +export async function getPreferredHashAlgo(key, keyPacket, date=new Date(), userId={}) { let hash_algo = config.prefer_hash_algorithm; let pref_algo = hash_algo; if (key instanceof Key) { @@ -1432,19 +1547,17 @@ export async function getPreferredHashAlgo(key, date=new Date(), userId={}) { hash_algo = crypto.hash.getHashByteLength(hash_algo) <= crypto.hash.getHashByteLength(pref_algo) ? pref_algo : hash_algo; } - // disable expiration checks - key = key.getSigningKeyPacket(undefined, null, userId); } - switch (Object.getPrototypeOf(key)) { + switch (Object.getPrototypeOf(keyPacket)) { case packet.SecretKey.prototype: case packet.PublicKey.prototype: case packet.SecretSubkey.prototype: case packet.PublicSubkey.prototype: - switch (key.algorithm) { + switch (keyPacket.algorithm) { case 'ecdh': case 'ecdsa': case 'eddsa': - pref_algo = crypto.publicKey.elliptic.getPreferredHashAlgo(key.params[0]); + pref_algo = crypto.publicKey.elliptic.getPreferredHashAlgo(keyPacket.params[0]); } } return crypto.hash.getHashByteLength(hash_algo) <= crypto.hash.getHashByteLength(pref_algo) ? diff --git a/src/message.js b/src/message.js index 2c3826db..cdeb888f 100644 --- a/src/message.js +++ b/src/message.js @@ -36,7 +36,7 @@ import enums from './enums'; import util from './util'; import packet from './packet'; import { Signature } from './signature'; -import { getPreferredHashAlgo, getPreferredAlgo, isAeadSupported } from './key'; +import { getPreferredHashAlgo, getPreferredAlgo, isAeadSupported, createSignaturePacket } from './key'; /** @@ -428,7 +428,7 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new } const onePassSig = new packet.OnePassSignature(); onePassSig.type = signatureType; - onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, date, userId); + onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKeyPacket, date, userId); onePassSig.publicKeyAlgorithm = signingKeyPacket.algorithm; onePassSig.signingKeyId = signingKeyPacket.getKeyId(); if (i === privateKeys.length - 1) { @@ -499,24 +499,16 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig const signatureType = literalDataPacket.text === null ? enums.signature.binary : enums.signature.text; - await Promise.all(privateKeys.map(async function(privateKey) { + await Promise.all(privateKeys.map(async privateKey => { if (privateKey.isPublic()) { throw new Error('Need private key for signing'); } const signingKeyPacket = await privateKey.getSigningKeyPacket(undefined, date, userId); if (!signingKeyPacket) { - throw new Error('Could not find valid key packet for signing in key ' + - privateKey.primaryKey.getKeyId().toHex()); + throw new Error(`Could not find valid signing key packet in key ${ + privateKey.primaryKey.getKeyId().toHex()}`); } - if (!signingKeyPacket.isDecrypted) { - throw new Error('Private key is not decrypted.'); - } - const signaturePacket = new packet.Signature(date); - signaturePacket.signatureType = signatureType; - signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; - signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, date, userId); - await signaturePacket.sign(signingKeyPacket, literalDataPacket); - return signaturePacket; + return createSignaturePacket(literalDataPacket, privateKey, signingKeyPacket, {signatureType}, date, userId); })).then(signatureList => { signatureList.forEach(signaturePacket => packetlist.push(signaturePacket)); }); diff --git a/src/openpgp.js b/src/openpgp.js index 7bc4706e..9517837e 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -109,7 +109,7 @@ export function destroyWorker() { * @param {Array} subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}] * sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt * @returns {Promise} The generated key object in the form: - * { key:Key, privateKeyArmored:String, publicKeyArmored:String } + * { key:Key, privateKeyArmored:String, publicKeyArmored:String, revocationCertificate:String } * @async * @static */ @@ -125,13 +125,19 @@ export function generateKey({ userIds=[], passphrase="", numBits=2048, keyExpira return asyncProxy.delegate('generateKey', options); } - return generate(options).then(key => ({ + return generate(options).then(key => { + const revocationCertificate = key.getRevocationCertificate(); + key.revocationSignatures = []; - key: key, - privateKeyArmored: key.armor(), - publicKeyArmored: key.toPublic().armor() + return { - })).catch(onError.bind(null, 'Error generating keypair')); + key: key, + privateKeyArmored: key.armor(), + publicKeyArmored: key.toPublic().armor(), + revocationCertificate: revocationCertificate + + }; + }).catch(onError.bind(null, 'Error generating keypair')); } /** @@ -140,25 +146,81 @@ export function generateKey({ userIds=[], passphrase="", numBits=2048, keyExpira * @param {Array} userIds array of user IDs e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }] * @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key * @param {Number} keyExpirationTime (optional) The number of seconds after the key creation time that the key expires + * @param {Boolean} revocationCertificate (optional) Whether the returned object should include a revocation certificate to revoke the public key * @returns {Promise} The generated key object in the form: - * { key:Key, privateKeyArmored:String, publicKeyArmored:String } + * { key:Key, privateKeyArmored:String, publicKeyArmored:String, revocationCertificate:String } * @async * @static */ -export function reformatKey({privateKey, userIds=[], passphrase="", keyExpirationTime=0, date}) { +export function reformatKey({privateKey, userIds=[], passphrase="", keyExpirationTime=0, date, revocationCertificate=true}) { userIds = toArray(userIds); - const options = { privateKey, userIds, passphrase, keyExpirationTime, date}; + const options = { privateKey, userIds, passphrase, keyExpirationTime, date, revocationCertificate }; if (asyncProxy) { return asyncProxy.delegate('reformatKey', options); } - return reformat(options).then(key => ({ + options.revoked = options.revocationCertificate; - key: key, - privateKeyArmored: key.armor(), - publicKeyArmored: key.toPublic().armor() + return reformat(options).then(key => { + const revocationCertificate = key.getRevocationCertificate(); + key.revocationSignatures = []; - })).catch(onError.bind(null, 'Error reformatting keypair')); + return { + + key: key, + privateKeyArmored: key.armor(), + publicKeyArmored: key.toPublic().armor(), + revocationCertificate: revocationCertificate + + }; + }).catch(onError.bind(null, 'Error reformatting keypair')); +} + +/** + * Revokes a key. Requires either a private key or a revocation certificate. + * If a revocation certificate is passed, the reasonForRevocation parameters will be ignored. + * @param {Key} key (optional) public or private key to revoke + * @param {String} revocationCertificate (optional) revocation certificate to revoke the key with + * @param {Object} reasonForRevocation (optional) object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} reasonForRevocation.flag (optional) flag indicating the reason for revocation + * @param {String} reasonForRevocation.string (optional) string explaining the reason for revocation + * @return {Promise} The revoked key object in the form: + * { privateKey:Key, privateKeyArmored:String, publicKey:Key, publicKeyArmored:String } + * (if private key is passed) or { publicKey:Key, publicKeyArmored:String } (otherwise) + * @static + */ +export function revokeKey({ + key, revocationCertificate, reasonForRevocation +} = {}) { + const options = { + key, revocationCertificate, reasonForRevocation + }; + + if (!util.getWebCryptoAll() && asyncProxy) { // use web worker if web crypto apis are not supported + return asyncProxy.delegate('revokeKey', options); + } + + return Promise.resolve().then(() => { + if (revocationCertificate) { + return key.applyRevocationCertificate(revocationCertificate); + } else { + return key.revoke(reasonForRevocation); + } + }).then(key => { + if (key.isPrivate()) { + const publicKey = key.toPublic(); + return { + privateKey: key, + privateKeyArmored: key.armor(), + publicKey: publicKey, + publicKeyArmored: publicKey.armor() + }; + } + return { + publicKey: key, + publicKeyArmored: key.armor() + }; + }).catch(onError.bind(null, 'Error revoking key')); } /** diff --git a/test/general/key.js b/test/general/key.js index 196819b2..1e7991be 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -50,6 +50,162 @@ describe('Key', function() { }); function tests() { + const priv_key_arm2 = + ['-----BEGIN PGP PRIVATE KEY BLOCK-----', + 'Version: GnuPG v2.0.19 (GNU/Linux)', + '', + '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', + '=lw5e', + '-----END PGP PRIVATE KEY BLOCK-----'].join('\n'); + + const pub_key_arm2 = + ['-----BEGIN PGP PUBLIC KEY BLOCK-----', + 'Version: GnuPG v2.0.19 (GNU/Linux)', + '', + 'mI0EUmEvTgEEANyWtQQMOybQ9JltDqmaX0WnNPJeLILIM36sw6zL0nfTQ5zXSS3+', + 'fIF6P29lJFxpblWk02PSID5zX/DYU9/zjM2xPO8Oa4xo0cVTOTLj++Ri5mtr//f5', + 'GLsIXxFrBJhD/ghFsL3Op0GXOeLJ9A5bsOn8th7x6JucNKuaRB6bQbSPABEBAAG0', + 'JFRlc3QgTWNUZXN0aW5ndG9uIDx0ZXN0QGV4YW1wbGUuY29tPoi5BBMBAgAjBQJS', + 'YS9OAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQSmNhOk1uQJQwDAP6', + 'AgrTyqkRlJVqz2pb46TfbDM2TDF7o9CBnBzIGoxBhlRwpqALz7z2kxBDmwpQa+ki', + 'Bq3jZN/UosY9y8bhwMAlnrDY9jP1gdCo+H0sD48CdXybblNwaYpwqC8VSpDdTndf', + '9j2wE/weihGp/DAdy/2kyBCaiOY1sjhUfJ1GogF49rC4jQRSYS9OAQQA6R/PtBFa', + 'JaT4jq10yqASk4sqwVMsc6HcifM5lSdxzExFP74naUMMyEsKHP53QxTF0Grqusag', + 'Qg/ZtgT0CN1HUM152y7ACOdp1giKjpMzOTQClqCoclyvWOFB+L/SwGEIJf7LSCEr', + 'woBuJifJc8xAVr0XX0JthoW+uP91eTQ3XpsAEQEAAYkBPQQYAQIACQUCUmEvTgIb', + 'LgCoCRBKY2E6TW5AlJ0gBBkBAgAGBQJSYS9OAAoJEOCE90RsICyXuqIEANmmiRCA', + 'SF7YK7PvFkieJNwzeK0V3F2lGX+uu6Y3Q/Zxdtwc4xR+me/CSBmsURyXTO29OWhP', + 'GLszPH9zSJU9BdDi6v0yNprmFPX/1Ng0Abn/sCkwetvjxC1YIvTLFwtUL/7v6NS2', + 'bZpsUxRTg9+cSrMWWSNjiY9qUKajm1tuzPDZXAUEAMNmAN3xXN/Kjyvj2OK2ck0X', + 'W748sl/tc3qiKPMJ+0AkMF7Pjhmh9nxqE9+QCEl7qinFqqBLjuzgUhBU4QlwX1GD', + 'AtNTq6ihLMD5v1d82ZC7tNatdlDMGWnIdvEMCv2GZcuIqDQ9rXWs49e7tq1NncLY', + 'hz3tYjKhoFTKEIq3y3Pp', + '=h/aX', + '-----END PGP PUBLIC KEY BLOCK-----'].join('\n'); + + const pub_key_arm4 = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG 2.1.15 (GNU/Linux) + +mI0EWpoNLAEEAMoO8dfnLvvCze1hjWcr8t1fMdndFQc1fAM7dm6sbqrdlaAz+Dab +zF3F9UhIOCcABRm+QHyZlgEsoQpHF/7sWflUK1FpoxdORINtIDilukUkMZ0NnIaD ++8pRutdSczPNFvSImSzZNCyLzvDCGMO3+Xeaa6pViSPEeBwhXWJUuHYtABEBAAG0 +IkpvZSBVc2VyIDxqb2UudXNlckBwcm90b25tYWlsLmNvbT6ItwQTAQgAIQUCWpoN +LAIbAwULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRBYtrN4WUnCtoUjBACi6qVb +noQJ1aOaOyoBZscDIO5XZHZK4L9V9uJJhD9v1qRF5s0PRiG969EwFlvjqIlLiRej +KSxvE/1rcym4GwBUndku1fMM+weTWHNtRn9+BPzN/4eKZbUbY3fHtlk+Lde3N+CZ +vrGKS/ICtbtuAfZL0LdzzqnNuBUXlO6EpG5C3riNBFqaDSwBBADDURzGkpTn/FTT +xHyheai+zTOUmy7N1ViCRPkErIeD606tZf/sKqAnEChfECeZJReYydN1B3O8QOyI +Ly/rH0DS2bt/6juhknPVGHPUAyNxHmiHYXTUgGPEX1QfusjzBcfIk6vHjYBiRm/I +u9iwrzCwypA4dWDZSTZuFrVsf4n+twARAQABiJ8EGAEIAAkFAlqaDSwCGwwACgkQ +WLazeFlJwrZQEAQAuUrp9Qp76CnKqUsUjcVxq7DJBi/lewyGGYSVAFt6/0Xyg/8Y +TEa/c4Dri/HMOtrfbgjp/doIVaZLOXZYfqRcpy3z0M6BierOPB3D+fdaTfd7gIrQ +nGHIp2NmbJZnYgl8Ps23qF+LKTa1eE+AmMQYzUHSGuka2lp6OglwWzg/dEw= +=/vbH +-----END PGP PUBLIC KEY BLOCK-----`; + + const priv_key_arm4 = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG 2.1.15 (GNU/Linux) + +lQIGBFqaDSwBBADKDvHX5y77ws3tYY1nK/LdXzHZ3RUHNXwDO3ZurG6q3ZWgM/g2 +m8xdxfVISDgnAAUZvkB8mZYBLKEKRxf+7Fn5VCtRaaMXTkSDbSA4pbpFJDGdDZyG +g/vKUbrXUnMzzRb0iJks2TQsi87wwhjDt/l3mmuqVYkjxHgcIV1iVLh2LQARAQAB +/gcDAoZ8RULY7umS4fVGPmTuETCnOOTGancXT5r7chKyfFXlyVU4ULvTdLwdFtqx +Vl9tNyED31nIiRP1CTmZLeaVScNGfVLjo8nvpMZUVopw5UdaFADeVTpwVdtp7ru+ +IgH4ynrRMgMGh7/dgBzIP8WN4w8uBPK5G4bS34NNiREkVoZ3oh4dA/6aeYfW7lVV +cYRl2F7++AGfqS+FpLsE8KjFU2z8POJjWMN1nYKwjNa+beEO0BFYdUFvMzU7eUHA +/G0xWAhYvNyuJHE4imgYmCy1OZeawc9h8YGeaQJCh2NTVzaD9HRu0xmz93bNF19q +bfUZJC7mC6WzKsRXHX0JmzH+9DShUqGnkRl5fMo2UhQMpSxsMT4dU/Ji4q+t96oy +K6g3DMJr5OtZML3XKxGmdy0CgepkG1aikrC9qLfBgxjqi+uTewcbrS9lAOfrZg4N +jwt1FLEK8gu7aOeczdW/pFOHOCrX1DnpF81JKJ1a7hz5JRP1m+ffqwm0IkpvZSBV +c2VyIDxqb2UudXNlckBwcm90b25tYWlsLmNvbT6ItwQTAQgAIQUCWpoNLAIbAwUL +CQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRBYtrN4WUnCtoUjBACi6qVbnoQJ1aOa +OyoBZscDIO5XZHZK4L9V9uJJhD9v1qRF5s0PRiG969EwFlvjqIlLiRejKSxvE/1r +cym4GwBUndku1fMM+weTWHNtRn9+BPzN/4eKZbUbY3fHtlk+Lde3N+CZvrGKS/IC +tbtuAfZL0LdzzqnNuBUXlO6EpG5C3p0CBgRamg0sAQQAw1EcxpKU5/xU08R8oXmo +vs0zlJsuzdVYgkT5BKyHg+tOrWX/7CqgJxAoXxAnmSUXmMnTdQdzvEDsiC8v6x9A +0tm7f+o7oZJz1Rhz1AMjcR5oh2F01IBjxF9UH7rI8wXHyJOrx42AYkZvyLvYsK8w +sMqQOHVg2Uk2bha1bH+J/rcAEQEAAf4HAwLwNvRIoBFS3OHTIYirkr4sHzSkWFJx +xDPozovXgCq7BoCXDMaSIQLwZqEfb+SabYtk7nLSnG2Y2mgwb9swZuBZEWuQjZk7 +lX1MvuZ0Ih2QdQSMEJk8sEsMoBGHHdHh/MZO4a27+5B9OceDfnEZZcGSOweUuu1n +IlgWcgrM40q4S3Mt39FXFgdJWnpd93hAokKDHklUGMdMLw/02dGVRkJmvUp9qdhe +c2njq9HSeYwqbY2rYgcNsF2ZcCLt9UXA2dOG4X2c2mPfjKuTRZUPxNKh6JfL3mlu +rBdd/z8gQHoKObyaarVwN3HAbtP0+6Z8a9/wDYj1K9ZCoHuEtKq1qq5J2Ec8+Yzl +K0Zlcs760LiYUr69CninMrnbDNnAhrYAcyJS42viUADPv9g+CBbyanB4KyE4UNrZ +BCB296lOEW4v1IZVNrNvqrbka3/p0qqBJiFTh7eT3zXpRNArFZDmLCUEEm53qT1a +PO/MyYUGTTMRAzTmNTiPiJ8EGAEIAAkFAlqaDSwCGwwACgkQWLazeFlJwrZQEAQA +uUrp9Qp76CnKqUsUjcVxq7DJBi/lewyGGYSVAFt6/0Xyg/8YTEa/c4Dri/HMOtrf +bgjp/doIVaZLOXZYfqRcpy3z0M6BierOPB3D+fdaTfd7gIrQnGHIp2NmbJZnYgl8 +Ps23qF+LKTa1eE+AmMQYzUHSGuka2lp6OglwWzg/dEw= +=mr3M +-----END PGP PRIVATE KEY BLOCK-----`; + + const revocation_certificate_arm4 = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG 2.1.15 (GNU/Linux) +Comment: This is a revocation certificate + +iJ8EIAEIAAkFAlqaDT0CHQAACgkQWLazeFlJwrbOaAP/V38FhBrUy4XYgt8ZX22G +ov6IFDNoyRKafSuz7Rg+8K8cf+0MAsSi52ueKfsbPxQ+I1vPeaEuEYbwTjtbvM+M +vZcX+VNYdsc1iZeNaT4ayA+2LrCN/xgFj0nrExHqcZAjgBZ9pvKghAqdK4Zb2Ghb +7chPiLLNWJCMtL4bo7a1X84= +=HcWg +-----END PGP PUBLIC KEY BLOCK-----`; + + const revoked_key_arm4 = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG 2.1.15 (GNU/Linux) + +mI0EWpoNLAEEAMoO8dfnLvvCze1hjWcr8t1fMdndFQc1fAM7dm6sbqrdlaAz+Dab +zF3F9UhIOCcABRm+QHyZlgEsoQpHF/7sWflUK1FpoxdORINtIDilukUkMZ0NnIaD ++8pRutdSczPNFvSImSzZNCyLzvDCGMO3+Xeaa6pViSPEeBwhXWJUuHYtABEBAAGI +nwQgAQgACQUCWpoNPQIdAAAKCRBYtrN4WUnCts5oA/9XfwWEGtTLhdiC3xlfbYai +/ogUM2jJEpp9K7PtGD7wrxx/7QwCxKLna54p+xs/FD4jW895oS4RhvBOO1u8z4y9 +lxf5U1h2xzWJl41pPhrID7YusI3/GAWPSesTEepxkCOAFn2m8qCECp0rhlvYaFvt +yE+Iss1YkIy0vhujtrVfzrQiSm9lIFVzZXIgPGpvZS51c2VyQHByb3Rvbm1haWwu +Y29tPoi3BBMBCAAhBQJamg0sAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJ +EFi2s3hZScK2hSMEAKLqpVuehAnVo5o7KgFmxwMg7ldkdkrgv1X24kmEP2/WpEXm +zQ9GIb3r0TAWW+OoiUuJF6MpLG8T/WtzKbgbAFSd2S7V8wz7B5NYc21Gf34E/M3/ +h4pltRtjd8e2WT4t17c34Jm+sYpL8gK1u24B9kvQt3POqc24FReU7oSkbkLeuI0E +WpoNLAEEAMNRHMaSlOf8VNPEfKF5qL7NM5SbLs3VWIJE+QSsh4PrTq1l/+wqoCcQ +KF8QJ5klF5jJ03UHc7xA7IgvL+sfQNLZu3/qO6GSc9UYc9QDI3EeaIdhdNSAY8Rf +VB+6yPMFx8iTq8eNgGJGb8i72LCvMLDKkDh1YNlJNm4WtWx/if63ABEBAAGInwQY +AQgACQUCWpoNLAIbDAAKCRBYtrN4WUnCtlAQBAC5Sun1CnvoKcqpSxSNxXGrsMkG +L+V7DIYZhJUAW3r/RfKD/xhMRr9zgOuL8cw62t9uCOn92ghVpks5dlh+pFynLfPQ +zoGJ6s48HcP591pN93uAitCcYcinY2ZslmdiCXw+zbeoX4spNrV4T4CYxBjNQdIa +6RraWno6CXBbOD90TA== +=8d2d +-----END PGP PUBLIC KEY BLOCK-----`; + const twoKeys = ['-----BEGIN PGP PUBLIC KEY BLOCK-----', 'Version: GnuPG v2.0.19 (GNU/Linux)', @@ -1172,6 +1328,11 @@ const mergeKey2 = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + done(); }); + it('Verify status of revoked primary key', function(done) { + const pubKey = openpgp.key.readArmored(pub_revoked_subkeys).keys[0]; + expect(pubKey.verifyPrimaryKey()).to.eventually.equal(openpgp.enums.keyStatus.revoked).notify(done); + }); + it('Verify status of revoked subkey', function(done) { const pubKeys = openpgp.key.readArmored(pub_sig_test); expect(pubKeys).to.exist; @@ -1359,6 +1520,71 @@ const mergeKey2 = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + }); }); + it('revoke() - primary key', async function() { + const privKey = openpgp.key.readArmored(priv_key_arm2).keys[0]; + await privKey.decrypt('hello world'); + + await privKey.revoke({ + flag: openpgp.enums.reasonForRevocation.key_retired, + string: 'Testing key revocation' + }).then(async revKey => { + expect(revKey.revocationSignatures).to.exist.and.have.length(1); + expect(revKey.revocationSignatures[0].signatureType).to.equal(openpgp.enums.signature.key_revocation); + expect(revKey.revocationSignatures[0].reasonForRevocationFlag).to.equal(openpgp.enums.reasonForRevocation.key_retired); + expect(revKey.revocationSignatures[0].reasonForRevocationString).to.equal('Testing key revocation'); + + expect(await privKey.verifyPrimaryKey()).to.equal(openpgp.enums.keyStatus.valid); + expect(await revKey.verifyPrimaryKey()).to.equal(openpgp.enums.keyStatus.revoked); + }); + }); + + it('revoke() - subkey', async function() { + const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0]; + const privKey = openpgp.key.readArmored(priv_key_arm2).keys[0]; + await privKey.decrypt('hello world'); + + const subKey = pubKey.subKeys[0]; + await subKey.revoke(privKey.primaryKey, { + flag: openpgp.enums.reasonForRevocation.key_superseded + }).then(async revKey => { + expect(revKey.revocationSignatures).to.exist.and.have.length(1); + expect(revKey.revocationSignatures[0].signatureType).to.equal(openpgp.enums.signature.subkey_revocation); + expect(revKey.revocationSignatures[0].reasonForRevocationFlag).to.equal(openpgp.enums.reasonForRevocation.key_superseded); + expect(revKey.revocationSignatures[0].reasonForRevocationString).to.equal(''); + + expect(await subKey.verify(pubKey.primaryKey)).to.equal(openpgp.enums.keyStatus.valid); + expect(await revKey.verify(pubKey.primaryKey)).to.equal(openpgp.enums.keyStatus.revoked); + }); + }); + + it('applyRevocationCertificate() should produce the same revoked key as GnuPG', function() { + const pubKey = openpgp.key.readArmored(pub_key_arm4).keys[0]; + + return pubKey.applyRevocationCertificate(revocation_certificate_arm4).then(revKey => { + expect(revKey.armor()).to.equal(openpgp.key.readArmored(revoked_key_arm4).keys[0].armor()); + }); + }); + + it('getRevocationCertificate() should produce the same revocation certificate as GnuPG', function() { + const revKey = openpgp.key.readArmored(revoked_key_arm4).keys[0]; + const revocationCertificate = revKey.getRevocationCertificate(); + + const input = openpgp.armor.decode(revocation_certificate_arm4); + const packetlist = new openpgp.packet.List(); + packetlist.read(input.data); + const armored = openpgp.armor.encode(openpgp.enums.armor.public_key, packetlist.write()); + + expect(revocationCertificate.replace(/^Comment: .*$\r\n/mg, '')).to.equal(armored.replace(/^Comment: .*$\r\n/mg, '')); + }); + + it('getRevocationCertificate() should have an appropriate comment', function() { + const revKey = openpgp.key.readArmored(revoked_key_arm4).keys[0]; + const revocationCertificate = revKey.getRevocationCertificate(); + + expect(revocationCertificate).to.match(/Comment: This is a revocation certificate/); + expect(revKey.armor()).not.to.match(/Comment: This is a revocation certificate/); + }); + it("getPreferredAlgo('symmetric') - one key - AES256", async function() { const key1 = openpgp.key.readArmored(twoKeys).keys[0]; const prefAlgo = await openpgp.key.getPreferredAlgo('symmetric', [key1]); @@ -1922,6 +2148,37 @@ VYGdb3eNlV8CfoEC }); }); + it('Revoke generated key with revocation certificate', function() { + const opt = {numBits: 512, userIds: 'test1 ', passphrase: '1234'}; + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + return openpgp.generateKey(opt).then(function(original) { + return openpgp.revokeKey({key: original.key.toPublic(), revocationCertificate: original.revocationCertificate}).then(function(revKey) { + revKey = revKey.publicKey; + expect(revKey.revocationSignatures[0].reasonForRevocationFlag).to.equal(openpgp.enums.reasonForRevocation.no_reason); + expect(revKey.revocationSignatures[0].reasonForRevocationString).to.equal(''); + return revKey.verifyPrimaryKey().then(function(status) { + expect(status).to.equal(openpgp.enums.keyStatus.revoked); + }); + }); + }); + }); + + it('Revoke generated key with private key', function() { + const opt = {numBits: 512, userIds: 'test1 ', passphrase: '1234'}; + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + return openpgp.generateKey(opt).then(async function(original) { + await original.key.decrypt('1234'); + return openpgp.revokeKey({key: original.key, reasonForRevocation: {string: 'Testing key revocation'}}).then(function(revKey) { + revKey = revKey.publicKey; + expect(revKey.revocationSignatures[0].reasonForRevocationFlag).to.equal(openpgp.enums.reasonForRevocation.no_reason); + expect(revKey.revocationSignatures[0].reasonForRevocationString).to.equal('Testing key revocation'); + return revKey.verifyPrimaryKey().then(function(status) { + expect(status).to.equal(openpgp.enums.keyStatus.revoked); + }); + }); + }); + }); + it('Merge key with another key with non-ID user attributes', function(done) { const key = openpgp.key.readArmored(mergeKey1).keys[0]; const updateKey = openpgp.key.readArmored(mergeKey2).keys[0]; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index a94c676c..536a12be 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -484,7 +484,8 @@ describe('OpenPGP.js public api tests', function() { return 'pub_key'; } }; - } + }, + getRevocationCertificate: function() {} }; keyGenStub = stub(openpgp.key, 'generate'); keyGenStub.returns(resolves(keyObjStub)); @@ -513,7 +514,7 @@ describe('OpenPGP.js public api tests', function() { keyExpirationTime: 0, curve: "", date: now, - subkeys: [], + subkeys: [] }).calledOnce).to.be.true; expect(newKey.key).to.exist; expect(newKey.privateKeyArmored).to.exist; @@ -1855,6 +1856,38 @@ describe('OpenPGP.js public api tests', function() { expect(signatures[0].signature.packets.length).to.equal(1); }); }); + + it('should fail to encrypt with revoked key', function() { + return openpgp.revokeKey({ + key: privateKey.keys[0] + }).then(function(revKey) { + return openpgp.encrypt({ + data: plaintext, + publicKeys: revKey.publicKey + }).then(function(encrypted) { + throw new Error('Should not encrypt with revoked key'); + }).catch(function(error) { + expect(error.message).to.match(/Could not find valid key packet for encryption/); + }); + }); + }); + + it('should fail to encrypt with revoked subkey', async function() { + const pubKeyDE = openpgp.key.readArmored(pub_key_de).keys[0]; + const privKeyDE = openpgp.key.readArmored(priv_key_de).keys[0]; + await privKeyDE.decrypt(passphrase); + return privKeyDE.subKeys[0].revoke(privKeyDE.primaryKey).then(function(revSubKey) { + pubKeyDE.subKeys[0] = revSubKey; + return openpgp.encrypt({ + data: plaintext, + publicKeys: pubKeyDE + }).then(function(encrypted) { + throw new Error('Should not encrypt with revoked subkey'); + }).catch(function(error) { + expect(error.message).to.match(/Could not find valid key packet for encryption/); + }); + }); + }); }); describe('ELG / DSA encrypt, decrypt, sign, verify', function() {