Merge pull request #528 from openpgpjs/detached

Detached Signatures
This commit is contained in:
Bart Butler 2017-03-14 18:15:24 -07:00 committed by GitHub
commit e925331a99
10 changed files with 490 additions and 61 deletions

View File

@ -29,23 +29,26 @@ import config from './config';
import packet from './packet'; import packet from './packet';
import enums from './enums.js'; import enums from './enums.js';
import armor from './encoding/armor.js'; import armor from './encoding/armor.js';
import * as sigModule from './signature.js';
/** /**
* @class * @class
* @classdesc Class that represents an OpenPGP cleartext signed message. * @classdesc Class that represents an OpenPGP cleartext signed message.
* See {@link http://tools.ietf.org/html/rfc4880#section-7} * See {@link http://tools.ietf.org/html/rfc4880#section-7}
* @param {String} text The cleartext of the signed message * @param {String} text The cleartext of the signed message
* @param {module:packet/packetlist} packetlist The packetlist with signature packets or undefined * @param {module:Signature} signature The detached signature or an empty signature if message not yet signed
* if message not yet signed
*/ */
export function CleartextMessage(text, packetlist) { export function CleartextMessage(text, signature) {
if (!(this instanceof CleartextMessage)) { if (!(this instanceof CleartextMessage)) {
return new CleartextMessage(text, packetlist); return new CleartextMessage(text, signature);
} }
// normalize EOL to canonical form <CR><LF> // normalize EOL to canonical form <CR><LF>
this.text = text.replace(/\r/g, '').replace(/[\t ]+\n/g, "\n").replace(/\n/g,"\r\n"); this.text = text.replace(/\r/g, '').replace(/[\t ]+\n/g, "\n").replace(/\n/g,"\r\n");
this.packets = packetlist || new packet.List(); if (signature && !(signature instanceof sigModule.Signature)) {
throw new Error('Invalid signature input');
}
this.signature = signature || new sigModule.Signature(new packet.List());
} }
/** /**
@ -54,7 +57,7 @@ export function CleartextMessage(text, packetlist) {
*/ */
CleartextMessage.prototype.getSigningKeyIds = function() { CleartextMessage.prototype.getSigningKeyIds = function() {
var keyIds = []; var keyIds = [];
var signatureList = this.packets.filterByTag(enums.packet.signature); var signatureList = this.signature.packets;
signatureList.forEach(function(packet) { signatureList.forEach(function(packet) {
keyIds.push(packet.issuerKeyId); keyIds.push(packet.issuerKeyId);
}); });
@ -66,6 +69,15 @@ CleartextMessage.prototype.getSigningKeyIds = function() {
* @param {Array<module:key~Key>} privateKeys private keys with decrypted secret key data for signing * @param {Array<module:key~Key>} privateKeys private keys with decrypted secret key data for signing
*/ */
CleartextMessage.prototype.sign = function(privateKeys) { CleartextMessage.prototype.sign = function(privateKeys) {
this.signature = this.signDetached(privateKeys);
};
/**
* Sign the cleartext message
* @param {Array<module:key~Key>} privateKeys private keys with decrypted secret key data for signing
* @return {module:signature~Signature} new detached signature of message content
*/
CleartextMessage.prototype.signDetached = function(privateKeys) {
var packetlist = new packet.List(); var packetlist = new packet.List();
var literalDataPacket = new packet.Literal(); var literalDataPacket = new packet.Literal();
literalDataPacket.setText(this.text); literalDataPacket.setText(this.text);
@ -84,7 +96,7 @@ CleartextMessage.prototype.sign = function(privateKeys) {
signaturePacket.sign(signingKeyPacket, literalDataPacket); signaturePacket.sign(signingKeyPacket, literalDataPacket);
packetlist.push(signaturePacket); packetlist.push(signaturePacket);
} }
this.packets = packetlist; return new sigModule.Signature(packetlist);
}; };
/** /**
@ -93,8 +105,17 @@ CleartextMessage.prototype.sign = function(privateKeys) {
* @return {Array<{keyid: module:type/keyid, valid: Boolean}>} list of signer's keyid and validity of signature * @return {Array<{keyid: module:type/keyid, valid: Boolean}>} list of signer's keyid and validity of signature
*/ */
CleartextMessage.prototype.verify = function(keys) { CleartextMessage.prototype.verify = function(keys) {
return this.verifyDetached(this.signature, keys);
};
/**
* Verify signatures of cleartext signed message
* @param {Array<module:key~Key>} keys array of keys to verify signatures
* @return {Array<{keyid: module:type/keyid, valid: Boolean}>} list of signer's keyid and validity of signature
*/
CleartextMessage.prototype.verifyDetached = function(signature, keys) {
var result = []; var result = [];
var signatureList = this.packets.filterByTag(enums.packet.signature); var signatureList = signature.packets;
var literalDataPacket = new packet.Literal(); var literalDataPacket = new packet.Literal();
// we assume that cleartext signature is generated based on UTF8 cleartext // we assume that cleartext signature is generated based on UTF8 cleartext
literalDataPacket.setText(this.text); literalDataPacket.setText(this.text);
@ -115,6 +136,8 @@ CleartextMessage.prototype.verify = function(keys) {
verifiedSig.keyid = signatureList[i].issuerKeyId; verifiedSig.keyid = signatureList[i].issuerKeyId;
verifiedSig.valid = null; verifiedSig.valid = null;
} }
verifiedSig.signature = new sigModule.Signature([signatureList[i]]);
result.push(verifiedSig); result.push(verifiedSig);
} }
return result; return result;
@ -137,7 +160,7 @@ CleartextMessage.prototype.armor = function() {
var body = { var body = {
hash: enums.read(enums.hash, config.prefer_hash_algorithm).toUpperCase(), hash: enums.read(enums.hash, config.prefer_hash_algorithm).toUpperCase(),
text: this.text, text: this.text,
data: this.packets.write() data: this.signature.packets.write()
}; };
return armor.encode(enums.armor.signed, body); return armor.encode(enums.armor.signed, body);
}; };
@ -157,7 +180,8 @@ export function readArmored(armoredText) {
var packetlist = new packet.List(); var packetlist = new packet.List();
packetlist.read(input.data); packetlist.read(input.data);
verifyHeaders(input.headers, packetlist); verifyHeaders(input.headers, packetlist);
var newMessage = new CleartextMessage(input.text, packetlist); var signature = new sigModule.Signature(packetlist);
var newMessage = new CleartextMessage(input.text, signature);
return newMessage; return newMessage;
} }

View File

@ -38,6 +38,7 @@ import config from '../config';
* 3 = PGP MESSAGE * 3 = PGP MESSAGE
* 4 = PUBLIC KEY BLOCK * 4 = PUBLIC KEY BLOCK
* 5 = PRIVATE KEY BLOCK * 5 = PRIVATE KEY BLOCK
* 6 = SIGNATURE
*/ */
function getType(text) { function getType(text) {
var reHeader = /^-----BEGIN PGP (MESSAGE, PART \d+\/\d+|MESSAGE, PART \d+|SIGNED MESSAGE|MESSAGE|PUBLIC KEY BLOCK|PRIVATE KEY BLOCK|SIGNATURE)-----$\n/m; var reHeader = /^-----BEGIN PGP (MESSAGE, PART \d+\/\d+|MESSAGE, PART \d+|SIGNED MESSAGE|MESSAGE|PUBLIC KEY BLOCK|PRIVATE KEY BLOCK|SIGNATURE)-----$\n/m;
@ -62,10 +63,7 @@ function getType(text) {
return enums.armor.multipart_last; return enums.armor.multipart_last;
} else } else
// BEGIN PGP SIGNATURE // BEGIN PGP SIGNED MESSAGE
// Used for detached signatures, OpenPGP/MIME signatures, and
// cleartext signatures. Note that PGP 2.x uses BEGIN PGP MESSAGE
// for detached signatures.
if (/SIGNED MESSAGE/.test(header[1])) { if (/SIGNED MESSAGE/.test(header[1])) {
return enums.armor.signed; return enums.armor.signed;
@ -86,6 +84,14 @@ function getType(text) {
// Used for armoring private keys. // Used for armoring private keys.
if (/PRIVATE KEY BLOCK/.test(header[1])) { if (/PRIVATE KEY BLOCK/.test(header[1])) {
return enums.armor.private_key; return enums.armor.private_key;
} else
// BEGIN PGP SIGNATURE
// Used for detached signatures, OpenPGP/MIME signatures, and
// cleartext signatures. Note that PGP 2.x uses BEGIN PGP MESSAGE
// for detached signatures.
if (/SIGNATURE/.test(header[1])) {
return enums.armor.signature;
} }
} }
@ -397,6 +403,13 @@ function armor(messagetype, body, partindex, parttotal) {
result.push("\r\n=" + getCheckSum(body) + "\r\n"); result.push("\r\n=" + getCheckSum(body) + "\r\n");
result.push("-----END PGP PRIVATE KEY BLOCK-----\r\n"); result.push("-----END PGP PRIVATE KEY BLOCK-----\r\n");
break; break;
case enums.armor.signature:
result.push("-----BEGIN PGP SIGNATURE-----\r\n");
result.push(addheader());
result.push(base64.encode(body));
result.push("\r\n=" + getCheckSum(body) + "\r\n");
result.push("-----END PGP SIGNATURE-----\r\n");
break;
} }
return result.join(''); return result.join('');

View File

@ -297,7 +297,8 @@ export default {
signed: 2, signed: 2,
message: 3, message: 3,
public_key: 4, public_key: 4,
private_key: 5 private_key: 5,
signature: 6
}, },
/** Asserts validity and converts from string/integer to integer. */ /** Asserts validity and converts from string/integer to integer. */

View File

@ -26,6 +26,13 @@ export * from './openpgp';
import * as keyMod from './key'; import * as keyMod from './key';
export const key = keyMod; export const key = keyMod;
/**
* @see module:signature
* @name module:openpgp.signature
*/
import * as signatureMod from './signature';
export const signature = signatureMod;
/** /**
* @see module:message * @see module:message
* @name module:openpgp.message * @name module:openpgp.message

View File

@ -32,6 +32,7 @@ import enums from './enums.js';
import armor from './encoding/armor.js'; import armor from './encoding/armor.js';
import config from './config'; import config from './config';
import crypto from './crypto'; import crypto from './crypto';
import * as sigModule from './signature.js';
import * as keyModule from './key.js'; import * as keyModule from './key.js';
/** /**
@ -344,19 +345,81 @@ Message.prototype.sign = function(privateKeys) {
return new Message(packetlist); return new Message(packetlist);
}; };
/**
* Create a detached signature for the message (the literal data packet of the message)
* @param {Array<module:key~Key>} privateKey private keys with decrypted secret key data for signing
* @return {module:signature~Signature} new detached signature of message content
*/
Message.prototype.signDetached = function(privateKeys) {
var packetlist = new packet.List();
var literalDataPacket = this.packets.findPacket(enums.packet.literal);
if (!literalDataPacket) {
throw new Error('No literal data packet to sign.');
}
var literalFormat = enums.write(enums.literal, literalDataPacket.format);
var signatureType = literalFormat === enums.literal.binary ?
enums.signature.binary : enums.signature.text;
for (var i = 0; i < privateKeys.length; i++) {
var signingKeyPacket = privateKeys[i].getSigningKeyPacket();
var signaturePacket = new packet.Signature();
signaturePacket.signatureType = signatureType;
signaturePacket.hashAlgorithm = config.prefer_hash_algorithm;
signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm;
if (!signingKeyPacket.isDecrypted) {
throw new Error('Private key is not decrypted.');
}
signaturePacket.sign(signingKeyPacket, literalDataPacket);
packetlist.push(signaturePacket);
}
return new sigModule.Signature(packetlist);
};
/** /**
* Verify message signatures * Verify message signatures
* @param {Array<module:key~Key>} keys array of keys to verify signatures * @param {Array<module:key~Key>} keys array of keys to verify signatures
* @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature
*/ */
Message.prototype.verify = function(keys) { Message.prototype.verify = function(keys) {
var result = [];
var msg = this.unwrapCompressed(); var msg = this.unwrapCompressed();
var literalDataList = msg.packets.filterByTag(enums.packet.literal); var literalDataList = msg.packets.filterByTag(enums.packet.literal);
if (literalDataList.length !== 1) { if (literalDataList.length !== 1) {
throw new Error('Can only verify message with one literal data packet.'); throw new Error('Can only verify message with one literal data packet.');
} }
var signatureList = msg.packets.filterByTag(enums.packet.signature); var signatureList = msg.packets.filterByTag(enums.packet.signature);
return createVerificationObjects(signatureList, literalDataList, keys);
};
/**
* Verify detached message signature
* @param {Array<module:key~Key>} keys array of keys to verify signatures
* @param {Signature}
* @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature
*/
Message.prototype.verifyDetached = function(signature, keys) {
var msg = this.unwrapCompressed();
var literalDataList = msg.packets.filterByTag(enums.packet.literal);
if (literalDataList.length !== 1) {
throw new Error('Can only verify message with one literal data packet.');
}
var signatureList = signature.packets;
return createVerificationObjects(signatureList, literalDataList, keys);
};
/**
* Create list of objects containing signer's keyid and validity of signature
* @param {Array<module:packet/signature>} signatureList array of signature packets
* @param {Array<module:packet/literal>} literalDataList array of literal data packets
* @param {Array<module:key~Key>} keys array of keys to verify signatures
* @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature
*/
function createVerificationObjects(signatureList, literalDataList, keys) {
var result = [];
for (var i = 0; i < signatureList.length; i++) { for (var i = 0; i < signatureList.length; i++) {
var keyPacket = null; var keyPacket = null;
for (var j = 0; j < keys.length; j++) { for (var j = 0; j < keys.length; j++) {
@ -368,16 +431,19 @@ Message.prototype.verify = function(keys) {
var verifiedSig = {}; var verifiedSig = {};
if (keyPacket) { if (keyPacket) {
//found a key packet that matches keyId of signature
verifiedSig.keyid = signatureList[i].issuerKeyId; verifiedSig.keyid = signatureList[i].issuerKeyId;
verifiedSig.valid = signatureList[i].verify(keyPacket, literalDataList[0]); verifiedSig.valid = signatureList[i].verify(keyPacket, literalDataList[0]);
} else { } else {
verifiedSig.keyid = signatureList[i].issuerKeyId; verifiedSig.keyid = signatureList[i].issuerKeyId;
verifiedSig.valid = null; verifiedSig.valid = null;
} }
verifiedSig.signature = new sigModule.Signature([signatureList[i]]);
result.push(verifiedSig); result.push(verifiedSig);
} }
return result; return result;
}; }
/** /**
* Unwrap compressed message * Unwrap compressed message

View File

@ -178,36 +178,44 @@ export function decryptKey({ privateKey, passphrase }) {
* @param {Key|Array<Key>} privateKeys (optional) private keys for signing. If omitted message will not be signed * @param {Key|Array<Key>} privateKeys (optional) private keys for signing. If omitted message will not be signed
* @param {String|Array<String>} passwords (optional) array of passwords or a single password to encrypt the message * @param {String|Array<String>} passwords (optional) array of passwords or a single password to encrypt the message
* @param {String} filename (optional) a filename for the literal data packet * @param {String} filename (optional) a filename for the literal data packet
* @param {Boolean} armor (optional) if the return value should be ascii armored or the message object * @param {Boolean} armor (optional) if the return values should be ascii armored or the message/signature objects
* @return {Promise<String|Message>} encrypted ASCII armored message, or the full Message object if 'armor' is false * @param {Boolean} detached (optional) if the signature should be detached (if true, signature will be added to returned object)
* @return {Promise<Object>} encrypted (and optionally signed message) in the form:
* {data: ASCII armored message if 'armor' is true,
* message: full Message object if 'armor' is false, signature: detached signature if 'detached' is true}
* @static * @static
*/ */
export function encrypt({ data, publicKeys, privateKeys, passwords, filename, armor=true }) { export function encrypt({ data, publicKeys, privateKeys, passwords, filename, armor=true, detached=false }) {
checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords);
if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported
return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor }); return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor, detached });
} }
var result = {};
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
let message = createMessage(data, filename); let message = createMessage(data, filename);
if (privateKeys) { // sign the message only if private keys are specified if (privateKeys) { // sign the message only if private keys are specified
message = message.sign(privateKeys); if (detached) {
var signature = message.signDetached(privateKeys);
if (armor) {
result.signature = signature.armor();
} else {
result.signature = signature;
}
} else {
message = message.sign(privateKeys);
}
} }
return message.encrypt(publicKeys, passwords); return message.encrypt(publicKeys, passwords);
}).then(message => { }).then(message => {
if (armor) {
if(armor) { result.data = message.armor();
return { } else {
data: message.armor() result.message = message;
};
} }
return { return result;
message: message
};
}).catch(onError.bind(null, 'Error encrypting message')); }).catch(onError.bind(null, 'Error encrypting message'));
} }
@ -220,22 +228,28 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar
* @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String } * @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String }
* @param {String} password (optional) single password to decrypt the message * @param {String} password (optional) single password to decrypt the message
* @param {String} format (optional) return data format either as 'utf8' or 'binary' * @param {String} format (optional) return data format either as 'utf8' or 'binary'
* @param {Signature} signature (optional) detached signature for verification
* @return {Promise<Object>} decrypted and verified message in the form: * @return {Promise<Object>} decrypted and verified message in the form:
* { data:Uint8Array|String, filename:String, signatures:[{ keyid:String, valid:Boolean }] } * { data:Uint8Array|String, filename:String, signatures:[{ keyid:String, valid:Boolean }] }
* @static * @static
*/ */
export function decrypt({ message, privateKey, publicKeys, sessionKey, password, format='utf8' }) { export function decrypt({ message, privateKey, publicKeys, sessionKey, password, format='utf8', signature=null }) {
checkMessage(message); publicKeys = toArray(publicKeys); checkMessage(message); publicKeys = toArray(publicKeys);
if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported
return asyncProxy.delegate('decrypt', { message, privateKey, publicKeys, sessionKey, password, format }); return asyncProxy.delegate('decrypt', { message, privateKey, publicKeys, sessionKey, password, format, signature });
} }
return message.decrypt(privateKey, sessionKey, password).then(message => { return message.decrypt(privateKey, sessionKey, password).then(message => {
const result = parseMessage(message, format); const result = parseMessage(message, format);
if (publicKeys && result.data) { // verify only if publicKeys are specified if (result.data) { // verify
result.signatures = message.verify(publicKeys); if (signature) {
//detached signature
result.signatures = message.verifyDetached(signature, publicKeys);
} else {
result.signatures = message.verify(publicKeys);
}
} }
return result; return result;
@ -255,30 +269,41 @@ export function decrypt({ message, privateKey, publicKeys, sessionKey, password,
* @param {String} data cleartext input to be signed * @param {String} data cleartext input to be signed
* @param {Key|Array<Key>} privateKeys array of keys or single key with decrypted secret key data to sign cleartext * @param {Key|Array<Key>} privateKeys array of keys or single key with decrypted secret key data to sign cleartext
* @param {Boolean} armor (optional) if the return value should be ascii armored or the message object * @param {Boolean} armor (optional) if the return value should be ascii armored or the message object
* @return {Promise<String|CleartextMessage>} ASCII armored message or the message of type CleartextMessage * @return {Promise<Object>} signed cleartext in the form:
* {data: ASCII armored message if 'armor' is true,
* message: full Message object if 'armor' is false, signature: detached signature if 'detached' is true}
* @static * @static
*/ */
export function sign({ data, privateKeys, armor=true }) { export function sign({ data, privateKeys, armor=true, detached=false}) {
checkString(data); checkString(data);
privateKeys = toArray(privateKeys); privateKeys = toArray(privateKeys);
if (asyncProxy) { // use web worker if available if (asyncProxy) { // use web worker if available
return asyncProxy.delegate('sign', { data, privateKeys, armor }); return asyncProxy.delegate('sign', { data, privateKeys, armor, detached });
} }
var result = {};
return execute(() => { return execute(() => {
const cleartextMessage = new cleartext.CleartextMessage(data); const cleartextMessage = new cleartext.CleartextMessage(data);
cleartextMessage.sign(privateKeys);
if(armor) { if (detached) {
return { var signature = cleartextMessage.signDetached(privateKeys);
data: cleartextMessage.armor() if (armor) {
}; result.signature = signature.armor();
} else {
result.signature = signature;
}
} else {
cleartextMessage.sign(privateKeys);
} }
return {
message: cleartextMessage if (armor) {
}; result.data = cleartextMessage.armor();
} else {
result.message = cleartextMessage;
}
return result;
}, 'Error signing cleartext message'); }, 'Error signing cleartext message');
} }
@ -287,24 +312,32 @@ export function sign({ data, privateKeys, armor=true }) {
* Verifies signatures of cleartext signed message * Verifies signatures of cleartext signed message
* @param {Key|Array<Key>} publicKeys array of publicKeys or single key, to verify signatures * @param {Key|Array<Key>} publicKeys array of publicKeys or single key, to verify signatures
* @param {CleartextMessage} message cleartext message object with signatures * @param {CleartextMessage} message cleartext message object with signatures
* @param {Signature} signature (optional) detached signature for verification
* @return {Promise<Object>} cleartext with status of verified signatures in the form of: * @return {Promise<Object>} cleartext with status of verified signatures in the form of:
* { data:String, signatures: [{ keyid:String, valid:Boolean }] } * { data:String, signatures: [{ keyid:String, valid:Boolean }] }
* @static * @static
*/ */
export function verify({ message, publicKeys }) { export function verify({ message, publicKeys, signature=null }) {
checkCleartextMessage(message); checkCleartextMessage(message);
publicKeys = toArray(publicKeys); publicKeys = toArray(publicKeys);
if (asyncProxy) { // use web worker if available if (asyncProxy) { // use web worker if available
return asyncProxy.delegate('verify', { message, publicKeys }); return asyncProxy.delegate('verify', { message, publicKeys, signature });
} }
return execute(() => ({ var result = {};
return execute(() => {
result.data = message.getText();
data: message.getText(), if (signature) {
signatures: message.verify(publicKeys) //detached signature
result.signatures = message.verifyDetached(signature, publicKeys);
} else {
result.signatures = message.verify(publicKeys);
}
return result;
}), 'Error verifying cleartext signed message'); }, 'Error verifying cleartext signed message');
} }

View File

@ -26,6 +26,7 @@
import * as key from '../key.js'; import * as key from '../key.js';
import * as message from '../message.js'; import * as message from '../message.js';
import * as cleartext from '../cleartext.js'; import * as cleartext from '../cleartext.js';
import * as signature from '../signature.js'
import Packetlist from './packetlist.js'; import Packetlist from './packetlist.js';
import type_keyid from '../type/keyid.js'; import type_keyid from '../type/keyid.js';
@ -55,9 +56,27 @@ export function clonePackets(options) {
if (options.key) { if (options.key) {
options.key = options.key.toPacketlist(); options.key = options.key.toPacketlist();
} }
if (options.message) {
//could be either a Message or CleartextMessage object
if (options.message instanceof message.Message) {
options.message = options.message.packets;
} else if (options.message instanceof cleartext.CleartextMessage) {
options.message.signature = options.message.signature.packets;
}
}
if (options.signature && (options.signature instanceof signature.Signature)) {
options.signature = options.signature.packets;
}
if (options.signatures) {
options.signatures = options.signatures.map(sig => verificationObjectToClone(sig));
}
return options; return options;
} }
function verificationObjectToClone(verObject) {
verObject.signature = verObject.signature.packets;
return verObject;
}
////////////////////////////// //////////////////////////////
// // // //
@ -91,7 +110,10 @@ export function parseClonedPackets(options, method) {
options.message = packetlistCloneToMessage(options.message); options.message = packetlistCloneToMessage(options.message);
} }
if (options.signatures) { if (options.signatures) {
options.signatures = options.signatures.map(packetlistCloneToSignature); options.signatures = options.signatures.map(packetlistCloneToSignatures);
}
if (options.signature) {
options.signature = packetlistCloneToSignature(options.signature);
} }
return options; return options;
} }
@ -102,16 +124,27 @@ function packetlistCloneToKey(clone) {
} }
function packetlistCloneToMessage(clone) { function packetlistCloneToMessage(clone) {
const packetlist = Packetlist.fromStructuredClone(clone.packets); const packetlist = Packetlist.fromStructuredClone(clone);
return new message.Message(packetlist); return new message.Message(packetlist);
} }
function packetlistCloneToCleartextMessage(clone) { function packetlistCloneToCleartextMessage(clone) {
var packetlist = Packetlist.fromStructuredClone(clone.packets); var packetlist = Packetlist.fromStructuredClone(clone.signature);
return new cleartext.CleartextMessage(clone.text, packetlist); return new cleartext.CleartextMessage(clone.text, new signature.Signature(packetlist));
}
//verification objects
function packetlistCloneToSignatures(clone) {
clone.keyid = type_keyid.fromClone(clone.keyid);
clone.signature = new signature.Signature(clone.signature);
return clone;
} }
function packetlistCloneToSignature(clone) { function packetlistCloneToSignature(clone) {
clone.keyid = type_keyid.fromClone(clone.keyid); if (typeof clone === "string") {
return clone; //signature is armored
return clone;
}
var packetlist = Packetlist.fromStructuredClone(clone);
return new signature.Signature(packetlist);
} }

76
src/signature.js Normal file
View File

@ -0,0 +1,76 @@
// GPG4Browsers - An OpenPGP implementation in javascript
// Copyright (C) 2011 Recurity Labs GmbH
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
/**
* @requires config
* @requires crypto
* @requires encoding/armor
* @requires enums
* @requires packet
* @module signature
*/
'use strict';
import packet from './packet';
import enums from './enums.js';
import armor from './encoding/armor.js';
/**
* @class
* @classdesc Class that represents an OpenPGP signature.
* @param {module:packet/packetlist} packetlist The signature packets
*/
export function Signature(packetlist) {
if (!(this instanceof Signature)) {
return new Signature(packetlist);
}
this.packets = packetlist || new packet.List();
}
/**
* Returns ASCII armored text of signature
* @return {String} ASCII armor
*/
Signature.prototype.armor = function() {
return armor.encode(enums.armor.signature, this.packets.write());
};
/**
* reads an OpenPGP armored signature and returns a signature object
* @param {String} armoredText text to be parsed
* @return {module:signature~Signature} new signature object
* @static
*/
export function readArmored(armoredText) {
var input = armor.decode(armoredText).data;
return read(input);
}
/**
* reads an OpenPGP signature as byte array and returns a signature object
* @param {Uint8Array} input binary signature
* @return {Signature} new signature object
* @static
*/
export function read(input) {
var packetlist = new packet.List();
packetlist.read(input);
return new Signature(packetlist);
}

View File

@ -637,7 +637,8 @@ describe('OpenPGP.js public api tests', function() {
return openpgp.decrypt(decOpt); return openpgp.decrypt(decOpt);
}).then(function(decrypted) { }).then(function(decrypted) {
expect(decrypted.data).to.equal(plaintext); expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures).to.not.exist; expect(decrypted.signatures).to.exist;
expect(decrypted.signatures.length).to.equal(0);
done(); done();
}); });
}); });
@ -659,6 +660,31 @@ describe('OpenPGP.js public api tests', function() {
expect(decrypted.data).to.equal(plaintext); expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures[0].valid).to.be.true; expect(decrypted.signatures[0].valid).to.be.true;
expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
done();
});
});
it('should encrypt/sign and decrypt/verify with detached signatures', function(done) {
var encOpt = {
data: plaintext,
publicKeys: publicKey.keys,
privateKeys: privateKey.keys,
detached: true
};
var decOpt = {
privateKey: privateKey.keys[0],
publicKeys: publicKey.keys
};
openpgp.encrypt(encOpt).then(function(encrypted) {
decOpt.message = openpgp.message.readArmored(encrypted.data);
decOpt.signature = openpgp.signature.readArmored(encrypted.signature);
return openpgp.decrypt(decOpt);
}).then(function(decrypted) {
expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures[0].valid).to.be.true;
expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -680,6 +706,31 @@ describe('OpenPGP.js public api tests', function() {
expect(decrypted.data).to.equal(plaintext); expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures[0].valid).to.be.null; expect(decrypted.signatures[0].valid).to.be.null;
expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
done();
});
});
it('should fail to verify decrypted data with wrong public pgp key with detached signatures', function(done) {
var encOpt = {
data: plaintext,
publicKeys: publicKey.keys,
privateKeys: privateKey.keys,
detached: true
};
var decOpt = {
privateKey: privateKey.keys[0],
publicKeys: openpgp.key.readArmored(wrong_pubkey).keys
};
openpgp.encrypt(encOpt).then(function(encrypted) {
decOpt.message = openpgp.message.readArmored(encrypted.data);
decOpt.signature = openpgp.signature.readArmored(encrypted.signature);
return openpgp.decrypt(decOpt);
}).then(function(decrypted) {
expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures[0].valid).to.be.null;
expect(decrypted.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -700,6 +751,29 @@ describe('OpenPGP.js public api tests', function() {
expect(verified.data).to.equal(plaintext); expect(verified.data).to.equal(plaintext);
expect(verified.signatures[0].valid).to.be.true; expect(verified.signatures[0].valid).to.be.true;
expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(verified.signatures[0].signature.packets.length).to.equal(1);
done();
});
});
it('should sign and verify cleartext data with detached signatures', function(done) {
var signOpt = {
data: plaintext,
privateKeys: privateKey.keys,
detached: true
};
var verifyOpt = {
publicKeys: publicKey.keys
};
openpgp.sign(signOpt).then(function(signed) {
verifyOpt.message = openpgp.cleartext.readArmored(signed.data);
verifyOpt.signature = openpgp.signature.readArmored(signed.signature);
return openpgp.verify(verifyOpt);
}).then(function(verified) {
expect(verified.data).to.equal(plaintext);
expect(verified.signatures[0].valid).to.be.true;
expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(verified.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -719,6 +793,29 @@ describe('OpenPGP.js public api tests', function() {
expect(verified.data).to.equal(plaintext); expect(verified.data).to.equal(plaintext);
expect(verified.signatures[0].valid).to.be.null; expect(verified.signatures[0].valid).to.be.null;
expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(verified.signatures[0].signature.packets.length).to.equal(1);
done();
});
});
it('should sign and fail to verify cleartext data with wrong public pgp key with detached signature', function(done) {
var signOpt = {
data: plaintext,
privateKeys: privateKey.keys,
detached: true
};
var verifyOpt = {
publicKeys: openpgp.key.readArmored(wrong_pubkey).keys
};
openpgp.sign(signOpt).then(function(signed) {
verifyOpt.message = openpgp.cleartext.readArmored(signed.data);
verifyOpt.signature = openpgp.signature.readArmored(signed.signature);
return openpgp.verify(verifyOpt);
}).then(function(verified) {
expect(verified.data).to.equal(plaintext);
expect(verified.signatures[0].valid).to.be.null;
expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(verified.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -739,6 +836,30 @@ describe('OpenPGP.js public api tests', function() {
expect(verified.data).to.equal(plaintext); expect(verified.data).to.equal(plaintext);
expect(verified.signatures[0].valid).to.be.true; expect(verified.signatures[0].valid).to.be.true;
expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(verified.signatures[0].signature.packets.length).to.equal(1);
done();
});
});
it('should sign and verify cleartext data and not armor with detached signatures', function(done) {
var signOpt = {
data: plaintext,
privateKeys: privateKey.keys,
detached: true,
armor: false
};
var verifyOpt = {
publicKeys: publicKey.keys
};
openpgp.sign(signOpt).then(function(signed) {
verifyOpt.message = signed.message;
verifyOpt.signature = signed.signature;
return openpgp.verify(verifyOpt);
}).then(function(verified) {
expect(verified.data).to.equal(plaintext);
expect(verified.signatures[0].valid).to.be.true;
expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex());
expect(verified.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -762,6 +883,9 @@ describe('OpenPGP.js public api tests', function() {
}).then(function(encrypted) { }).then(function(encrypted) {
expect(encrypted.data).to.exist; expect(encrypted.data).to.exist;
expect(encrypted.data).to.equal(plaintext); expect(encrypted.data).to.equal(plaintext);
expect(encrypted.signatures[0].valid).to.be.true;
expect(encrypted.signatures[0].keyid.toHex()).to.equal(privKeyDE.getSigningKeyPacket().getKeyId().toHex());
expect(encrypted.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -828,6 +952,7 @@ describe('OpenPGP.js public api tests', function() {
openpgp.decrypt({ privateKey:privKey, message:message }).then(function(decrypted) { openpgp.decrypt({ privateKey:privKey, message:message }).then(function(decrypted) {
expect(decrypted.data).to.equal('hello 3des\n'); expect(decrypted.data).to.equal('hello 3des\n');
expect(decrypted.signatures.length).to.equal(0);
done(); done();
}); });
}); });
@ -847,6 +972,7 @@ describe('OpenPGP.js public api tests', function() {
return openpgp.decrypt(decOpt); return openpgp.decrypt(decOpt);
}).then(function(decrypted) { }).then(function(decrypted) {
expect(decrypted.data).to.equal(plaintext); expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures.length).to.equal(0);
done(); done();
}); });
}); });
@ -864,6 +990,7 @@ describe('OpenPGP.js public api tests', function() {
return openpgp.decrypt(decOpt); return openpgp.decrypt(decOpt);
}).then(function(decrypted) { }).then(function(decrypted) {
expect(decrypted.data).to.equal(plaintext); expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures.length).to.equal(0);
done(); done();
}); });
}); });
@ -882,6 +1009,7 @@ describe('OpenPGP.js public api tests', function() {
return openpgp.decrypt(decOpt); return openpgp.decrypt(decOpt);
}).then(function(decrypted) { }).then(function(decrypted) {
expect(decrypted.data).to.equal(plaintext); expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures.length).to.equal(0);
done(); done();
}); });
}); });
@ -905,6 +1033,7 @@ describe('OpenPGP.js public api tests', function() {
expect(encOpt.data.byteLength).to.equal(0); // transfered buffer should be empty expect(encOpt.data.byteLength).to.equal(0); // transfered buffer should be empty
} }
expect(decrypted.data).to.deep.equal(new Uint8Array([0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01])); expect(decrypted.data).to.deep.equal(new Uint8Array([0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01]));
expect(decrypted.signatures.length).to.equal(0);
done(); done();
}); });
}); });

View File

@ -265,6 +265,7 @@ describe("Signature", function() {
openpgp.decrypt({ privateKey: priv_key, publicKeys:[pub_key], message:msg }).then(function(decrypted) { openpgp.decrypt({ privateKey: priv_key, publicKeys:[pub_key], message:msg }).then(function(decrypted) {
expect(decrypted.data).to.exist; expect(decrypted.data).to.exist;
expect(decrypted.signatures[0].valid).to.be.true; expect(decrypted.signatures[0].valid).to.be.true;
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -309,6 +310,7 @@ describe("Signature", function() {
expect(verified).to.exist; expect(verified).to.exist;
expect(verified).to.have.length(1); expect(verified).to.have.length(1);
expect(verified[0].valid).to.be.true; expect(verified[0].valid).to.be.true;
expect(verified[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -333,6 +335,7 @@ describe("Signature", function() {
expect(verified).to.exist; expect(verified).to.exist;
expect(verified).to.have.length(1); expect(verified).to.have.length(1);
expect(verified[0].valid).to.be.true; expect(verified[0].valid).to.be.true;
expect(verified[0].signature.packets.length).to.equal(1);
done(); done();
}); });
@ -356,6 +359,7 @@ describe("Signature", function() {
expect(verified).to.exist; expect(verified).to.exist;
expect(verified).to.have.length(1); expect(verified).to.have.length(1);
expect(verified[0].valid).to.be.true; expect(verified[0].valid).to.be.true;
expect(verified[0].signature.packets.length).to.equal(1);
done(); done();
}); });
@ -390,6 +394,7 @@ describe("Signature", function() {
expect(decrypted.data).to.equal(plaintext); expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures).to.have.length(1); expect(decrypted.signatures).to.have.length(1);
expect(decrypted.signatures[0].valid).to.be.true; expect(decrypted.signatures[0].valid).to.be.true;
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -426,6 +431,7 @@ describe("Signature", function() {
expect(decrypted.data).to.equal(plaintext); expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures).to.have.length(1); expect(decrypted.signatures).to.have.length(1);
expect(decrypted.signatures[0].valid).to.be.true; expect(decrypted.signatures[0].valid).to.be.true;
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
@ -469,6 +475,8 @@ describe("Signature", function() {
expect(verifiedSig).to.have.length(2); expect(verifiedSig).to.have.length(2);
expect(verifiedSig[0].valid).to.be.true; expect(verifiedSig[0].valid).to.be.true;
expect(verifiedSig[1].valid).to.be.true; expect(verifiedSig[1].valid).to.be.true;
expect(verifiedSig[0].signature.packets.length).to.equal(1);
expect(verifiedSig[1].signature.packets.length).to.equal(1);
done(); done();
}); });
@ -513,6 +521,8 @@ describe("Signature", function() {
expect(cleartextSig.signatures).to.have.length(2); expect(cleartextSig.signatures).to.have.length(2);
expect(cleartextSig.signatures[0].valid).to.be.true; expect(cleartextSig.signatures[0].valid).to.be.true;
expect(cleartextSig.signatures[1].valid).to.be.true; expect(cleartextSig.signatures[1].valid).to.be.true;
expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1);
expect(cleartextSig.signatures[1].signature.packets.length).to.equal(1);
done(); done();
}); });
}); });
@ -533,6 +543,7 @@ describe("Signature", function() {
expect(cleartextSig.data).to.equal(plaintext.replace(/\r/g,'')); expect(cleartextSig.data).to.equal(plaintext.replace(/\r/g,''));
expect(cleartextSig.signatures).to.have.length(1); expect(cleartextSig.signatures).to.have.length(1);
expect(cleartextSig.signatures[0].valid).to.be.true; expect(cleartextSig.signatures[0].valid).to.be.true;
expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1);
done(); done();
}); });
@ -595,7 +606,7 @@ describe("Signature", function() {
expect(pubKey.users[0].selfCertifications).to.eql(pubKey2.users[0].selfCertifications); expect(pubKey.users[0].selfCertifications).to.eql(pubKey2.users[0].selfCertifications);
}); });
it('Verify a detached signature', function() { it('Verify a detached signature using readSignedContent', function() {
var detachedSig = ['-----BEGIN PGP SIGNATURE-----', var detachedSig = ['-----BEGIN PGP SIGNATURE-----',
'Version: GnuPG v1.4.13 (Darwin)', 'Version: GnuPG v1.4.13 (Darwin)',
'Comment: GPGTools - https://gpgtools.org', 'Comment: GPGTools - https://gpgtools.org',
@ -641,6 +652,42 @@ describe("Signature", function() {
expect(result[0].valid).to.be.true; expect(result[0].valid).to.be.true;
}); });
it('Detached signature signing and verification cleartext', function () {
var msg = openpgp.message.fromText('hello');
var pubKey2 = openpgp.key.readArmored(pub_key_arm2).keys[0];
var privKey2 = openpgp.key.readArmored(priv_key_arm2).keys[0];
privKey2.decrypt('hello world');
var opt = {numBits: 512, userIds: { name:'test', email:'a@b.com' }, passphrase: null};
if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys
openpgp.generateKey(opt).then(function(gen) {
var generatedKey = gen.key;
var detachedSig = msg.signDetached([generatedKey, privKey2]);
var result = msg.verifyDetached(detachedSig, [generatedKey.toPublic(), pubKey2]);
expect(result[0].valid).to.be.true;
expect(result[1].valid).to.be.true;
});
});
it('Detached signature signing and verification encrypted', function () {
var msg = openpgp.message.fromText('hello');
var pubKey2 = openpgp.key.readArmored(pub_key_arm2).keys[0];
var privKey2 = openpgp.key.readArmored(priv_key_arm2).keys[0];
privKey2.decrypt('hello world');
msg.encrypt({keys: [pubKey2] });
var detachedSig = msg.signDetached([privKey2]);
var opt = {numBits: 512, userIds: { name:'test', email:'a@b.com' }, passphrase: null};
if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys
openpgp.generateKey(opt).then(function(gen) {
var key = gen.key;
var result = msg.verifyDetached(detachedSig, [pubKey2, key.toPublic()]);
expect(result[0].valid).to.be.true;
expect(result[0].valid).to.be.false;
});
});
it('Sign message with key without password', function(done) { it('Sign message with key without password', function(done) {
var opt = {numBits: 512, userIds: { name:'test', email:'a@b.com' }, passphrase: null}; var opt = {numBits: 512, userIds: { name:'test', email:'a@b.com' }, passphrase: null};
if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys