diff --git a/src/config/config.js b/src/config/config.js index 131205d3..0e9b2287 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -41,6 +41,7 @@ export default { integrity_protect: true, // use integrity protection for symmetric encryption ignore_mdc_error: false, // fail on decrypt if message is not integrity protected checksum_required: false, // do not throw error when armor is missing a checksum + verify_expired_keys: true, // allow signature verification with expired keys rsa_blinding: true, use_native: true, // use native node.js crypto and Web Crypto apis (if available) zero_copy: false, // use transferable objects between the Web Worker and main thread diff --git a/src/key.js b/src/key.js index 60a46be0..e93ffb30 100644 --- a/src/key.js +++ b/src/key.js @@ -294,18 +294,20 @@ Key.prototype.armor = function() { /** * Returns first key packet or key packet by given keyId that is available for signing or signature verification * @param {module:type/keyid} keyId, optional + * @param {Boolean} allowExpired allows signature verification with expired keys * @return {(module:packet/secret_subkey|module:packet/secret_key|null)} key packet or null if no signing key has been found */ -Key.prototype.getSigningKeyPacket = function(keyId) { - var primaryUser = this.getPrimaryUser(); +Key.prototype.getSigningKeyPacket = function(keyId, allowExpired=false) { + var primaryUser = this.getPrimaryUser(allowExpired); if (primaryUser && isValidSigningKeyPacket(this.primaryKey, primaryUser.selfCertificate) && - (!keyId || this.primaryKey.getKeyId().equals(keyId))) { + (!keyId || this.primaryKey.getKeyId().equals(keyId)) && + this.verifyPrimaryKey(allowExpired) === enums.keyStatus.valid) { return this.primaryKey; } if (this.subKeys) { for (var i = 0; i < this.subKeys.length; i++) { - if (this.subKeys[i].isValidSigningKey(this.primaryKey) && + if (this.subKeys[i].isValidSigningKey(this.primaryKey, allowExpired) && (!keyId || this.subKeys[i].subKey.getKeyId().equals(keyId))) { return this.subKeys[i].subKey; } @@ -430,9 +432,10 @@ Key.prototype.decryptKeyPacket = function(keyIds, passphrase) { /** * Verify primary key. Checks for revocation signatures, expiration time * and valid self signature + * @param {Boolean} allowExpired allows signature verification with expired keys * @return {module:enums.keyStatus} The status of the primary key */ -Key.prototype.verifyPrimaryKey = function() { +Key.prototype.verifyPrimaryKey = function(allowExpired=false) { // check revocation signature if (this.revocationSignature && !this.revocationSignature.isExpired() && (this.revocationSignature.verified || @@ -440,7 +443,7 @@ Key.prototype.verifyPrimaryKey = function() { return enums.keyStatus.revoked; } // check V3 expiration time - if (this.primaryKey.version === 3 && this.primaryKey.expirationTimeV3 !== 0 && + if (!allowExpired && this.primaryKey.version === 3 && this.primaryKey.expirationTimeV3 !== 0 && Date.now() > (this.primaryKey.created.getTime() + this.primaryKey.expirationTimeV3*24*3600*1000)) { return enums.keyStatus.expired; } @@ -461,7 +464,7 @@ Key.prototype.verifyPrimaryKey = function() { return enums.keyStatus.invalid; } // check V4 expiration time - if (this.primaryKey.version === 4 && primaryUser.selfCertificate.keyNeverExpires === false && + if (!allowExpired && this.primaryKey.version === 4 && primaryUser.selfCertificate.keyNeverExpires === false && Date.now() > (this.primaryKey.created.getTime() + primaryUser.selfCertificate.keyExpirationTime*1000)) { return enums.keyStatus.expired; } @@ -502,9 +505,10 @@ function getExpirationTime(keyPacket, selfCertificate) { * Returns primary user and most significant (latest valid) self signature * - if multiple users are marked as primary users returns the one with the latest self signature * - if no primary user is found returns the user with the latest self signature + * @param {Boolean} allowExpired allows signature verification with expired keys * @return {{user: Array, selfCertificate: Array}|null} The primary user and the self signature */ -Key.prototype.getPrimaryUser = function() { +Key.prototype.getPrimaryUser = function(allowExpired=false) { var primUser = []; for (var i = 0; i < this.users.length; i++) { if (!this.users[i].userId || !this.users[i].selfCertifications) { @@ -530,7 +534,7 @@ Key.prototype.getPrimaryUser = function() { }); // return first valid for (var k = 0; k < primUser.length; k++) { - if (primUser[k].user.isValidSelfCertificate(this.primaryKey, primUser[k].selfCertificate)) { + if (primUser[k].user.isValidSelfCertificate(this.primaryKey, primUser[k].selfCertificate, allowExpired)) { return primUser[k]; } } @@ -744,40 +748,18 @@ User.prototype.isRevoked = function(certificate, primaryKey) { } }; -/** - * Returns the most significant (latest valid) self signature of the user - * @param {module:packet/secret_key|module:packet/public_key} primaryKey The primary key packet - * @return {module:packet/signature} The self signature - */ -User.prototype.getValidSelfCertificate = function(primaryKey) { - if (!this.selfCertifications) { - return null; - } - // most recent first - var validCert = this.selfCertifications.sort(function(a, b) { - a = a.created; - b = b.created; - return a>b ? -1 : a (this.subKey.created.getTime() + this.subKey.expirationTimeV3*24*3600*1000)) { return enums.keyStatus.expired; } @@ -977,7 +962,7 @@ SubKey.prototype.verify = function(primaryKey) { var isLast = (i === this.bindingSignatures.length - 1); var sig = this.bindingSignatures[i]; // check binding signature is not expired - if(sig.isExpired()) { + if(!allowExpired && sig.isExpired()) { if(isLast) { return enums.keyStatus.expired; // last expired binding signature } else { @@ -994,7 +979,7 @@ SubKey.prototype.verify = function(primaryKey) { } // check V4 expiration time if (this.subKey.version === 4) { - if(sig.keyNeverExpires === false && Date.now() > (this.subKey.created.getTime() + sig.keyExpirationTime*1000)) { + if(!allowExpired && sig.keyNeverExpires === false && Date.now() > (this.subKey.created.getTime() + sig.keyExpirationTime*1000)) { if(isLast) { return enums.keyStatus.expired; // last V4 expired binding signature } else { diff --git a/src/message.js b/src/message.js index 9d9a8b30..139ca4de 100644 --- a/src/message.js +++ b/src/message.js @@ -467,7 +467,7 @@ function createVerificationObjects(signatureList, literalDataList, keys) { for (var i = 0; i < signatureList.length; i++) { var keyPacket = null; for (var j = 0; j < keys.length; j++) { - keyPacket = keys[j].getSigningKeyPacket(signatureList[i].issuerKeyId); + keyPacket = keys[j].getSigningKeyPacket(signatureList[i].issuerKeyId, config.verify_expired_keys); if (keyPacket) { break; } diff --git a/test/general/signature.js b/test/general/signature.js index b1b7ad52..7c287205 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -257,6 +257,83 @@ describe("Signature", function() { '-----END PGP PUBLIC KEY BLOCK-----' ].join('\n'); + var pub_expired = + [ '-----BEGIN PGP PUBLIC KEY BLOCK-----', + 'Comment: GPGTools - https://gpgtools.org', + '', + 'mQINBFpcwc8BEAC3ywtlTJ1inmifeTrC85b2j+WRySworAUKobk/jmswSoLt720R', + '1J211Uu7IW7UBReoEhfNq+M0CAaoTxT7XPvd2O8lyn/RMAlnmFC0x3pyGrRYyRFd', + 'ZuGaWsFdHT/hCOeXOHv7sV/UWjL4wfeSlGqGWzHy4QH718HOQciZ7UHcS5J9B09W', + 't4TWcY+rTwl2GoFWLBHYCZZLnsQhvJqUTEHc63j+WV5M6oPNDNzqXa545ktss4Bq', + 'L7efeMtAThDlMg4vmodNkHYu0g+RqsGb1kwBkCznrNpYNETqgalhn5fZ6uV2RaDR', + 'WwFOm7ujGwQCzLSHcoDh4zqtWKkImMBEnwTFo0GTgXTTz0T565l5uqUvZ9UkJLXc', + 'IKWpfzHPUPOCstTaVNcCiTw+nwu4BvvOVgOWridKirpxks9uvihnzcAyR5ey212q', + 'HkFW1464qss4b9b4W399/KbOQ8Ngr1kUUeAoK13x7QKTmTwjE1Qt66370nFjk9Go', + 'k0Z0Of90oxQXx2/8g4gufpoMloTNdK/pMzPd+KfePpiVKoxmFTWOqmYgiX/4YcKi', + 'nQsJf++D9xsmAN6v9Ay1RqKmxiJgcuDvqcZ+FJdGatlpKfyEEsDRjAtMXSgw3BpH', + 'xsfPViEsVblmSQBPvuloKbp8kNPsJe3MW0fLSWjSuNDppx+OJX6xq1MNkQARAQAB', + 'tBlzdW5ueSA8c3VubnlAc3Vubnkuc3Vubnk+iQJUBBMBCgA+AhsDBQsJCAcDBRUK', + 'CQgLBRYCAwEAAh4BAheAFiEE8XCJ+2Ua4cHedSGW7IB7EDeBZnQFAlpeFK8FCQAC', + 'pGAACgkQ7IB7EDeBZnToTg//eVOfzHdKvKCTanqbBNDkChtwfHABR01HvowwIdwz', + 'IXeGkAJcV2GaCFtcClYcWFPZq4PQerQc1SB62S8RkuiaWbgVET6adRqq1MVNMvrl', + '/RGJaW7JL0pG8J8cJ1l5Jq9WCdtH0TqfRG/4DkkD7Cgay4oMhPU5w4inoYbeysyH', + 'FKmFIJfbRfoWd2vM3HYi8+2c7UqDtG9R8Xe5X/skYAflAiym/TYF4+H9+siIdk3D', + '5U3WLcwrI45ZsJatK2E4mFy9ne98kYM27QB4TeIuZ+8jQWECqpc3nyQ6UjRYOpAw', + '3jdYYAECmOKjxZJy6tVksLfZin07d4Ya/vgWz27uF3vjkxYywmuvonDyzIkwayTR', + 'NZUbMXnC3yN+e8jtw/HMdQT8LYOxrW/192Y6RBJM1hCWQIaYJxDms9z4JoyYHX5c', + 'tYgEcyMDfwGcsTFLnM+JYJqkOHUfKc3JHtiZmN8QO1TBEUgx18psEBiva3ilTMUG', + 'Zr39gHRp/7fUSj3Vm+bpMOUs0vRdnd3/IGFUgZnTB5nUCCvbs4qLzi2cW9rqDliQ', + 'PyIQKcvCFXFzXsZ31DHnT4OMP9hxpAdGaRhcNySf/Stq3n6tgJSi0IxGqrdCH93y', + 'Ko9b9nRNvHHPoWnuGkAKsxDLm7V4LEGEJLTbYk1R+/M6ijaBnD9rIp3cV9RXtzcc', + 'fuW5Ag0EWlzBzwEQAMML14fV1LUhO5xb0ZRj+ouBSofOY9Zbyjq4JN8qNXjrJJGR', + 'GGWvdWUvUDTBynIm1vhIuArNvqEUdvXjSO/xISz6yXZXX/NrbD8MiCRRqYza2ONw', + '256XvxEt9H4gWnpa+cPkjzzG5tlMceMoE8UWiHC+ua3cadXdlMzxXbGBKrWWZkHv', + 'kPcXV08wuGDPDNiS6syBSfk6l9qz4sZfgt8zAiNkM32JsCu2GkuYwCMXnc28XJOY', + 'zqBIDcz7VUee41C0L5v7puSKwxvuZBVDJNVxDs/ufUFePEOhqpkTkJDroGh+3Qy0', + 'ePzL8KrRtt/Lla6Qz6MckR7myXdJeVFQyza5gjhEi/i3afI3zELdFwHn14AEGxp3', + 'FfmCM2w6Aiyy4JdBQ2ggC7rIOuElMkX7Am6lINQiIwNkYZVIL5UF7avlja4zp/Qm', + '3gyLNCANrZ+HsdQuSzOYRsyGgIM2FLqKBHKqF5VmWsHN2GdFHwnrWp7DwtPqHoat', + 'kVotP0adzOAMC3McbRibkHXOtNXNYCz7yNCn6i9IY5KGj4y3uj7curs1LkYARPg8', + 'hFrnKOFOBE/pCPUlJeaZAjJiQ6FIKrKNADlNwTVZ5puo/gCE/WxzjOA06prG62Un', + '+d5HUUmlZzjPQ44kfmUvMXyfqIiRboAtvdnZc81UlrXNmiewUY4PM3HYmmoHABEB', + 'AAGJAjwEGAEKACYWIQTxcIn7ZRrhwd51IZbsgHsQN4FmdAUCWlzBzwIbDAUJB4Yf', + 'gAAKCRDsgHsQN4FmdFQND/9/Xu0S6H7IVNPWQL7kXgh3VxAwMaS8RTueTPmTP+5A', + 'HCld/20eTsHxWhZcISyDxkKgAnm7hMlY0S4yObFlKc7FRT32W9Ep2PEMc8Qe0Z2v', + 'gqOEGWtb2iZZkLmNRFAj2PUHtOODufVqEPLx22DL+Wy3MnOU7IrxLjmMFUd91hkN', + 'JmLNlolKxRkgH8NfPrpstMUzFDcbTsqIthI3yJlUh0gQaS7zElvWBGfCG2MQFZ4q', + '1xd9rXDaFmIf6+X9k7MNRrSv/uQ7cwW/36/sXdWV4tA/lZxjh+WRkhxu3vSCyP2v', + 'kemT/cHBIcLdG7+4aTAML6Roqy/mNk1k9oO+g9yfty5RmVvROlrL7EIu4D0inC74', + '5XZ36mUR2U0jLN0IaSAmQp+Dxh87S1SxhoA6qi0mYroSSngR68y880nq5xIgBjQr', + 'uoCHfcE/toiLkT8Zyjv7AMXsfuRFsW5VGkRuOceMgK+UPijEK/yAzbGTrj8hCrMM', + 'K/M0OXg9T1W2kzILkVjpj2PyY5MQjoWQEzFRbDrjdXHBuSvyf+SI02QvG2KdOuvv', + 'G6TOw3dj+jf1VgJBkNpt6NCIfXYQczuv8HSzqwtstQoHsIpdz7FjKaXR1fqr+Ikl', + '1Ze66O/1dHY4rt0l0IoMxHL93P3plUiy7wflxuSwLthPybAu7I4QGPC8qP0vm2Cm', + '2g==', + '=X7/F', + '-----END PGP PUBLIC KEY BLOCK-----' + ].join('\n'); + + var msg_sig_expired = + [ '-----BEGIN PGP MESSAGE-----', + 'Comment: GPGTools - https://gpgtools.org', + '', + 'owEBWwKk/ZANAwAKAeyAexA3gWZ0AawUYgloZWxsby50eHRaX2WpaGVsbG+JAjME', + 'AAEKAB0WIQTxcIn7ZRrhwd51IZbsgHsQN4FmdAUCWl9lqQAKCRDsgHsQN4FmdCln', + 'D/44x1bcrOXg+DbRStSrC75wFa+cvPEmaTZyqN6d7qlQCMxOcPlq6lbZ74QWfEq7', + 'i1ZYHp4AU8jALw0QqBQQE5FvABleQKpVfY22s83Bqy+P0DB9ntpD+t+oZrxGCLmL', + 'MbZJNFnGro48gHt+/OQKLuftiVwE2opHfgogVKNL74FmYA0hMItdzpn4OPNFkP8t', + 'Iq/m0hkXlTAKqBPITVLv1FN16v+Sm1iC317eP/HOTYqVZdJN3svVF8ZBfg29a8p6', + '6nl67fZhXgrt0OB6KSNIZEwMTWjFAqi365mtTssqAA0un94+cQ/WvAC5QcMM8g5S', + 'i3G7vny9AsXor+GDU1z7UDWs3wBV4mVRdj7bBIS6PK+6oe012aNpRObcI2bU2BT/', + 'H/7uHZWfwEmpfvH9RVZgoeETA3vSx7MDrNyDt3gwv2hxOHEd7nnVQ3EKG33173o1', + '/5/oEmn2USujKGhHJ2Zo3aWNRuUWZlvBaYw+PwB2R0UiuJbi0KofNYPssNdpw4sg', + 'Qs7Nb2/Ilo1zn5bDh+WDrUrn6zHKAfBytBPpwPFWPZ8W10HUlC5vMZSKH5/UZhj5', + 'kLlUC1zKjFPpRhO27ImTJuImil4lR2/CFjB1duG3JGJQaYIq8RFJOjvTVY29wl0i', + 'pFy6y1Ofv2lLHB9K7N7dvvee2nvpUMkLEL52oFQ6Jc7sdg==', + '=Q4tk', + '-----END PGP MESSAGE-----' + ].join('\n'); + it('Testing signature checking on CAST5-enciphered message', function() { var priv_key = openpgp.key.readArmored(priv_key_arm1).keys[0]; var pub_key = openpgp.key.readArmored(pub_key_arm1).keys[0]; @@ -583,6 +660,32 @@ describe("Signature", function() { }); + it('Verify test with expired verification public key and verify_expired_keys set to false', function() { + openpgp.config.verify_expired_keys = false; + var pubKey = openpgp.key.readArmored(pub_expired).keys[0]; + var message = openpgp.message.readArmored(msg_sig_expired); + return openpgp.verify({ publicKeys:[pubKey], message:message }).then(function(verified) { + expect(verified).to.exist; + expect(verified.signatures).to.have.length(1); + expect(verified.signatures[0].valid).to.not.be.true; + expect(verified.signatures[0].signature.packets.length).to.equal(1); + }); + + }); + + it('Verify test with expired verification public key and verify_expired_keys set to true', function() { + openpgp.config.verify_expired_keys = true; + var pubKey = openpgp.key.readArmored(pub_expired).keys[0]; + var message = openpgp.message.readArmored(msg_sig_expired); + return openpgp.verify({ publicKeys:[pubKey], message:message }).then(function(verified) { + expect(verified).to.exist; + expect(verified.signatures).to.have.length(1); + expect(verified.signatures[0].valid).to.be.true; + expect(verified.signatures[0].signature.packets.length).to.equal(1); + }); + + }); + it('Verify primary key revocation signature', function(done) { var pubKey = openpgp.key.readArmored(pub_revoked).keys[0];