Added ability to sign and verify public keys
This commit is contained in:
parent
3d32898250
commit
5140a946e5
84
src/key.js
84
src/key.js
|
@ -507,11 +507,11 @@ function getExpirationTime(keyPacket, selfCertificate) {
|
||||||
Key.prototype.getPrimaryUser = function() {
|
Key.prototype.getPrimaryUser = function() {
|
||||||
var primUser = [];
|
var primUser = [];
|
||||||
for (var i = 0; i < this.users.length; i++) {
|
for (var i = 0; i < this.users.length; i++) {
|
||||||
if (!this.users[i].userId || !this.users[i].selfCertifications) {
|
if ((!this.users[i].userId && !this.users[i].userAttribute) || !this.users[i].selfCertifications) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (var j = 0; j < this.users[i].selfCertifications.length; j++) {
|
for (var j = 0; j < this.users[i].selfCertifications.length; j++) {
|
||||||
primUser.push({user: this.users[i], selfCertificate: this.users[i].selfCertifications[j]});
|
primUser.push({index: i, user: this.users[i], selfCertificate: this.users[i].selfCertifications[j]});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// sort by primary user flag and signature creation time
|
// sort by primary user flag and signature creation time
|
||||||
|
@ -638,6 +638,86 @@ Key.prototype.revoke = function() {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the public key
|
||||||
|
* @param {Array<module:key~Key>} privateKey decrypted private keys for signing
|
||||||
|
* @return {module:key~Key} new public key with new certificate signature
|
||||||
|
*/
|
||||||
|
Key.prototype.sign = function(privateKeys) {
|
||||||
|
var primaryUser, primaryKey, dataToSign, signingKeyPacket, signaturePacket, user, key;
|
||||||
|
if (this.isPrivate()) {
|
||||||
|
throw new Error('Only public key can be signed');
|
||||||
|
}
|
||||||
|
primaryUser = this.getPrimaryUser();
|
||||||
|
if (!primaryUser) {
|
||||||
|
throw new Error('Could not find primary user');
|
||||||
|
}
|
||||||
|
primaryKey = this.primaryKey;
|
||||||
|
dataToSign = {};
|
||||||
|
dataToSign.userid = primaryUser.user.userId || primaryUser.user.userAttribute;
|
||||||
|
dataToSign.key = primaryKey;
|
||||||
|
user = new User(primaryUser.user.userId || primaryUser.user.userAttribute);
|
||||||
|
user.otherCertifications = [];
|
||||||
|
privateKeys.forEach(function(privateKey) {
|
||||||
|
if (privateKey.isPublic()) {
|
||||||
|
throw new Error('Need private key for signing');
|
||||||
|
}
|
||||||
|
if (privateKey.primaryKey.getKeyId().equals(primaryKey.getKeyId())) {
|
||||||
|
throw new Error('No need to self signinig');
|
||||||
|
}
|
||||||
|
signingKeyPacket = privateKey.getSigningKeyPacket();
|
||||||
|
if (!signingKeyPacket) {
|
||||||
|
throw new Error('Could not find valid signing key packet');
|
||||||
|
}
|
||||||
|
if (!signingKeyPacket.isDecrypted) {
|
||||||
|
throw new Error('Private key is not decrypted.');
|
||||||
|
}
|
||||||
|
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.hashAlgorithm = privateKey.getPreferredHashAlgorithm();
|
||||||
|
signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm;
|
||||||
|
signaturePacket.signingKeyId = signingKeyPacket.getKeyId();
|
||||||
|
signaturePacket.sign(signingKeyPacket, dataToSign);
|
||||||
|
user.otherCertifications.push(signaturePacket);
|
||||||
|
});
|
||||||
|
user.update(primaryUser.user, this.primaryKey);
|
||||||
|
key = new Key(this.toPacketlist());
|
||||||
|
key.users[primaryUser.index] = user;
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies public key
|
||||||
|
* @param {Array<module:key~Key>} keys array of keys to verify certificate signatures
|
||||||
|
* @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature
|
||||||
|
*/
|
||||||
|
Key.prototype.verify = function(keys) {
|
||||||
|
var primaryKey, primaryUser, user;
|
||||||
|
if (this.isPrivate()) {
|
||||||
|
throw new Error('Only public key can be verified');
|
||||||
|
}
|
||||||
|
primaryKey = this.primaryKey;
|
||||||
|
primaryUser = this.getPrimaryUser();
|
||||||
|
if (!primaryUser) {
|
||||||
|
throw new Error('Could not find primary user');
|
||||||
|
}
|
||||||
|
user = primaryUser.user;
|
||||||
|
if (!user.otherCertifications) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return user.otherCertifications.map(function(signaturePacket) {
|
||||||
|
var keyPacket = keys.find(function(key) {
|
||||||
|
return key.getSigningKeyPacket(signaturePacket.issuerKeyId);
|
||||||
|
}) || null;
|
||||||
|
return {
|
||||||
|
keyid: signaturePacket.issuerKeyId,
|
||||||
|
valid: keyPacket && signaturePacket.verify(keyPacket.primaryKey, {userid: user.userId || user.userAttribute, key: primaryKey})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class
|
* @class
|
||||||
* @classdesc Class that represents an user ID or attribute packet and the relevant signatures.
|
* @classdesc Class that represents an user ID or attribute packet and the relevant signatures.
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
import * as messageLib from './message.js';
|
import * as messageLib from './message.js';
|
||||||
import * as cleartext from './cleartext.js';
|
import * as cleartext from './cleartext.js';
|
||||||
import * as key from './key.js';
|
import * as keyLib from './key.js';
|
||||||
import config from './config/config.js';
|
import config from './config/config.js';
|
||||||
import util from './util';
|
import util from './util';
|
||||||
import AsyncProxy from './worker/async_proxy.js';
|
import AsyncProxy from './worker/async_proxy.js';
|
||||||
|
@ -104,7 +104,7 @@ export function generateKey({ userIds=[], passphrase, numBits=2048, unlocked=fal
|
||||||
return asyncProxy.delegate('generateKey', options);
|
return asyncProxy.delegate('generateKey', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return key.generate(options).then(newKey => ({
|
return keyLib.generate(options).then(newKey => ({
|
||||||
|
|
||||||
key: newKey,
|
key: newKey,
|
||||||
privateKeyArmored: newKey.armor(),
|
privateKeyArmored: newKey.armor(),
|
||||||
|
@ -361,6 +361,65 @@ export function decryptSessionKey({ message, privateKey, password }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// //
|
||||||
|
// Public key signing and verification //
|
||||||
|
// //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs a paublic key.
|
||||||
|
* @param {Key} publicKey public key to be signed
|
||||||
|
* @param {Key|Array<Key>} privateKeys array of keys or single key with decrypted secret key data to sign public key
|
||||||
|
* @return {Promise<String|Key>} Public key object in form:
|
||||||
|
* { publicKey:Key, publicKeyArmored:String }
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
export function signPublicKey({ publicKey, privateKeys }) {
|
||||||
|
checkKey(publicKey, 'publicKey');
|
||||||
|
privateKeys = toArray(privateKeys);
|
||||||
|
|
||||||
|
if (asyncProxy) { // use web worker if available
|
||||||
|
return asyncProxy.delegate('signPublicKey', { publicKey, privateKeys });
|
||||||
|
}
|
||||||
|
|
||||||
|
return execute(() => {
|
||||||
|
|
||||||
|
const signedPublicKey = publicKey.sign(privateKeys);
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: signedPublicKey,
|
||||||
|
publicKeyArmored: signedPublicKey.armor()
|
||||||
|
};
|
||||||
|
|
||||||
|
}, 'Error signing public key');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies public key
|
||||||
|
* @param {Key} publicKey public key object with signatures
|
||||||
|
* @param {Key|Array<Key>} publicKeys array of publicKeys or single key, to verify signatures
|
||||||
|
* @return {Promise<Object>} cleartext with status of verified signatures in the form of:
|
||||||
|
* { signatures: [{ keyid:String, valid:Boolean|null }] }
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
export function verifyPublicKey({ publicKey, publicKeys }) {
|
||||||
|
checkKey(publicKey, 'publicKey');
|
||||||
|
publicKeys = toArray(publicKeys);
|
||||||
|
|
||||||
|
if (asyncProxy) { // use web worker if available
|
||||||
|
return asyncProxy.delegate('verifyPublicKey', { publicKey, publicKeys });
|
||||||
|
}
|
||||||
|
|
||||||
|
return execute(() => ({
|
||||||
|
|
||||||
|
signatures: publicKey.verify(publicKeys)
|
||||||
|
|
||||||
|
}), 'Error verifying signed public key');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
// //
|
// //
|
||||||
// Helper functions //
|
// Helper functions //
|
||||||
|
@ -396,6 +455,11 @@ function checkCleartextMessage(message) {
|
||||||
throw new Error('Parameter [message] needs to be of type CleartextMessage');
|
throw new Error('Parameter [message] needs to be of type CleartextMessage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function checkKey(key, name) {
|
||||||
|
if (!keyLib.Key.prototype.isPrototypeOf(key)) {
|
||||||
|
throw new Error('Parameter [' + (name || 'key') + '] needs to be of type Key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format user ids for internal use.
|
* Format user ids for internal use.
|
||||||
|
|
|
@ -158,6 +158,20 @@ var priv_key_de =
|
||||||
'=kyeP',
|
'=kyeP',
|
||||||
'-----END PGP PRIVATE KEY BLOCK-----'].join('\n');
|
'-----END PGP PRIVATE KEY BLOCK-----'].join('\n');
|
||||||
|
|
||||||
|
|
||||||
|
var wrong_pubkey = [
|
||||||
|
'-----BEGIN PGP PUBLIC KEY BLOCK-----',
|
||||||
|
'Version: OpenPGP.js v0.9.0',
|
||||||
|
'Comment: Hoodiecrow - https://hoodiecrow.com',
|
||||||
|
'',
|
||||||
|
'xk0EUlhMvAEB/2MZtCUOAYvyLFjDp3OBMGn3Ev8FwjzyPbIF0JUw+L7y2XR5',
|
||||||
|
'RVGvbK88unV3cU/1tOYdNsXI6pSp/Ztjyv7vbBUAEQEAAc0pV2hpdGVvdXQg',
|
||||||
|
'VXNlciA8d2hpdGVvdXQudGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhM',
|
||||||
|
'vQkQ9vYOm0LN/0wAAAW4Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXq',
|
||||||
|
'IiN602mWrkd8jcEzLsW5IUNzVPLhrFIuKyBDTpLnC07Loce1',
|
||||||
|
'=6XMW',
|
||||||
|
'-----END PGP PUBLIC KEY BLOCK-----'].join('\n');
|
||||||
|
|
||||||
var passphrase = 'hello world';
|
var passphrase = 'hello world';
|
||||||
var plaintext = 'short message\nnext line\n한국어/조선말';
|
var plaintext = 'short message\nnext line\n한국어/조선말';
|
||||||
var password1 = 'I am a password';
|
var password1 = 'I am a password';
|
||||||
|
@ -607,18 +621,6 @@ describe('OpenPGP.js public api tests', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('AES / RSA encrypt, decrypt, sign, verify', function() {
|
describe('AES / RSA encrypt, decrypt, sign, verify', function() {
|
||||||
var wrong_pubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n' +
|
|
||||||
'Version: OpenPGP.js v0.9.0\r\n' +
|
|
||||||
'Comment: Hoodiecrow - https://hoodiecrow.com\r\n' +
|
|
||||||
'\r\n' +
|
|
||||||
'xk0EUlhMvAEB/2MZtCUOAYvyLFjDp3OBMGn3Ev8FwjzyPbIF0JUw+L7y2XR5\r\n' +
|
|
||||||
'RVGvbK88unV3cU/1tOYdNsXI6pSp/Ztjyv7vbBUAEQEAAc0pV2hpdGVvdXQg\r\n' +
|
|
||||||
'VXNlciA8d2hpdGVvdXQudGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhM\r\n' +
|
|
||||||
'vQkQ9vYOm0LN/0wAAAW4Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXq\r\n' +
|
|
||||||
'IiN602mWrkd8jcEzLsW5IUNzVPLhrFIuKyBDTpLnC07Loce1\r\n' +
|
|
||||||
'=6XMW\r\n' +
|
|
||||||
'-----END PGP PUBLIC KEY BLOCK-----\r\n\r\n';
|
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
expect(privateKey.keys[0].decrypt(passphrase)).to.be.true;
|
expect(privateKey.keys[0].decrypt(passphrase)).to.be.true;
|
||||||
});
|
});
|
||||||
|
@ -909,6 +911,48 @@ describe('OpenPGP.js public api tests', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('signPublicKey, verifyPublicKey', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
expect(privateKey.keys[0].decrypt(passphrase)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sign and verify public key', function(done) {
|
||||||
|
var signOpt = {
|
||||||
|
publicKey: openpgp.key.readArmored(pub_key_de).keys[0],
|
||||||
|
privateKeys: privateKey.keys
|
||||||
|
};
|
||||||
|
var verifyOpt = {
|
||||||
|
publicKeys: publicKey.keys
|
||||||
|
};
|
||||||
|
openpgp.signPublicKey(signOpt).then(function(signed) {
|
||||||
|
verifyOpt.publicKey = signed.publicKey;
|
||||||
|
return openpgp.verifyPublicKey(verifyOpt);
|
||||||
|
}).then(function(verified) {
|
||||||
|
expect(verified.signatures[0].valid).to.be.true;
|
||||||
|
expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sign and fail to verify public key with wrong public key', function(done) {
|
||||||
|
var signOpt = {
|
||||||
|
publicKey: openpgp.key.readArmored(pub_key_de).keys[0],
|
||||||
|
privateKeys: privateKey.keys
|
||||||
|
};
|
||||||
|
var verifyOpt = {
|
||||||
|
publicKeys: openpgp.key.readArmored(wrong_pubkey).keys
|
||||||
|
};
|
||||||
|
openpgp.signPublicKey(signOpt).then(function(signed) {
|
||||||
|
verifyOpt.publicKey = signed.publicKey;
|
||||||
|
return openpgp.verifyPublicKey(verifyOpt);
|
||||||
|
}).then(function(verified) {
|
||||||
|
expect(verified.signatures[0].valid).to.be.null;
|
||||||
|
expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -654,5 +654,42 @@ describe("Signature", function() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Verify signed public key', function(done) {
|
||||||
|
var signedArmor = [
|
||||||
|
'-----BEGIN PGP PUBLIC KEY BLOCK-----',
|
||||||
|
'Version: GnuPG v1',
|
||||||
|
'',
|
||||||
|
'mI0EUmEvTgEEANyWtQQMOybQ9JltDqmaX0WnNPJeLILIM36sw6zL0nfTQ5zXSS3+',
|
||||||
|
'fIF6P29lJFxpblWk02PSID5zX/DYU9/zjM2xPO8Oa4xo0cVTOTLj++Ri5mtr//f5',
|
||||||
|
'GLsIXxFrBJhD/ghFsL3Op0GXOeLJ9A5bsOn8th7x6JucNKuaRB6bQbSPABEBAAG0',
|
||||||
|
'JFRlc3QgTWNUZXN0aW5ndG9uIDx0ZXN0QGV4YW1wbGUuY29tPoi5BBMBAgAjBQJS',
|
||||||
|
'YS9OAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQSmNhOk1uQJQwDAP6',
|
||||||
|
'AgrTyqkRlJVqz2pb46TfbDM2TDF7o9CBnBzIGoxBhlRwpqALz7z2kxBDmwpQa+ki',
|
||||||
|
'Bq3jZN/UosY9y8bhwMAlnrDY9jP1gdCo+H0sD48CdXybblNwaYpwqC8VSpDdTndf',
|
||||||
|
'9j2wE/weihGp/DAdy/2kyBCaiOY1sjhUfJ1GogF49rCIRgQQEQIABgUCVuXBfQAK',
|
||||||
|
'CRARJ5QDyxae+O0fAJ9hUQPejXvZv6VW1Q3/Pm3+x2wfJACgwFg9NlrPPfejoC1w',
|
||||||
|
'P+z+vE5NFA24jQRSYS9OAQQA6R/PtBFaJaT4jq10yqASk4sqwVMsc6HcifM5lSdx',
|
||||||
|
'zExFP74naUMMyEsKHP53QxTF0GrqusagQg/ZtgT0CN1HUM152y7ACOdp1giKjpMz',
|
||||||
|
'OTQClqCoclyvWOFB+L/SwGEIJf7LSCErwoBuJifJc8xAVr0XX0JthoW+uP91eTQ3',
|
||||||
|
'XpsAEQEAAYkBPQQYAQIACQUCUmEvTgIbLgCoCRBKY2E6TW5AlJ0gBBkBAgAGBQJS',
|
||||||
|
'YS9OAAoJEOCE90RsICyXuqIEANmmiRCASF7YK7PvFkieJNwzeK0V3F2lGX+uu6Y3',
|
||||||
|
'Q/Zxdtwc4xR+me/CSBmsURyXTO29OWhPGLszPH9zSJU9BdDi6v0yNprmFPX/1Ng0',
|
||||||
|
'Abn/sCkwetvjxC1YIvTLFwtUL/7v6NS2bZpsUxRTg9+cSrMWWSNjiY9qUKajm1tu',
|
||||||
|
'zPDZXAUEAMNmAN3xXN/Kjyvj2OK2ck0XW748sl/tc3qiKPMJ+0AkMF7Pjhmh9nxq',
|
||||||
|
'E9+QCEl7qinFqqBLjuzgUhBU4QlwX1GDAtNTq6ihLMD5v1d82ZC7tNatdlDMGWnI',
|
||||||
|
'dvEMCv2GZcuIqDQ9rXWs49e7tq1NncLYhz3tYjKhoFTKEIq3y3Pp',
|
||||||
|
'=fvK7',
|
||||||
|
'-----END PGP PUBLIC KEY BLOCK-----'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
var sig_key = openpgp.key.readArmored(signedArmor).keys[0];
|
||||||
|
var pub_key = openpgp.key.readArmored(priv_key_arm1).keys[0].toPublic();
|
||||||
|
openpgp.verifyPublicKey({ publicKey: sig_key, publicKeys: [pub_key] }).then(function(verified) {
|
||||||
|
expect(verified.signatures[0].valid).to.be.true;
|
||||||
|
expect(verified.signatures[0].keyid.toHex()).to.equal(pub_key.primaryKey.getKeyId().toHex());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user