Implement V5 signatures

This commit is contained in:
Daniel Huigens 2019-06-03 16:46:26 +02:00
parent f629ddcb31
commit 735d6d088f
7 changed files with 112 additions and 46 deletions

View File

@ -89,7 +89,7 @@ CleartextMessage.prototype.signDetached = async function(privateKeys, signature=
const literalDataPacket = new packet.Literal();
literalDataPacket.setText(this.text);
return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds));
return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true));
};
/**
@ -115,7 +115,7 @@ CleartextMessage.prototype.verifyDetached = function(signature, keys, date=new D
const literalDataPacket = new packet.Literal();
// we assume that cleartext signature is generated based on UTF8 cleartext
literalDataPacket.setText(this.text);
return createVerificationObjects(signatureList, [literalDataPacket], keys, date);
return createVerificationObjects(signatureList, [literalDataPacket], keys, date, true);
};
/**

View File

@ -922,9 +922,10 @@ User.prototype.isRevoked = async function(primaryKey, certificate, key, date=new
* @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
* @param {Object} detached (optional) whether to create a detached signature packet
* @returns {module:packet/signature} signature packet
*/
export async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, signatureProperties, date, userId) {
export async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, signatureProperties, date, userId, detached=false) {
if (!signingKeyPacket.isDecrypted()) {
throw new Error('Private key is not decrypted.');
}
@ -932,7 +933,7 @@ export async function createSignaturePacket(dataToSign, privateKey, signingKeyPa
Object.assign(signaturePacket, signatureProperties);
signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm;
signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKeyPacket, date, userId);
await signaturePacket.sign(signingKeyPacket, dataToSign);
await signaturePacket.sign(signingKeyPacket, dataToSign, detached);
return signaturePacket;
}

View File

@ -474,7 +474,7 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new
});
packetlist.push(literalDataPacket);
packetlist.concat(await createSignaturePackets(literalDataPacket, privateKeys, signature, date));
packetlist.concat(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, false));
return new Message(packetlist);
};
@ -513,7 +513,7 @@ Message.prototype.signDetached = async function(privateKeys=[], signature=null,
if (!literalDataPacket) {
throw new Error('No literal data packet to sign.');
}
return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds));
return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true));
};
/**
@ -523,10 +523,11 @@ Message.prototype.signDetached = async function(privateKeys=[], signature=null,
* @param {Signature} signature (optional) any existing detached signature to append
* @param {Date} date (optional) override the creationtime of the signature
* @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @param {Boolean} detached (optional) whether to create detached signature packets
* @returns {Promise<module:packet.List>} list of signature packets
* @async
*/
export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date(), userIds=[]) {
export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date(), userIds=[], detached=false) {
const packetlist = new packet.List();
// If data packet was created from Uint8Array, use binary, otherwise use text
@ -543,7 +544,7 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig
throw new Error(`Could not find valid signing key packet in key ${
privateKey.getKeyId().toHex()}`);
}
return createSignaturePacket(literalDataPacket, privateKey, signingKey.keyPacket, { signatureType }, date, userId);
return createSignaturePacket(literalDataPacket, privateKey, signingKey.keyPacket, { signatureType }, date, userId, detached);
})).then(signatureList => {
signatureList.forEach(signaturePacket => packetlist.push(signaturePacket));
});
@ -578,7 +579,7 @@ Message.prototype.verify = async function(keys, date=new Date(), streaming) {
onePassSig.correspondingSigReject = reject;
});
onePassSig.signatureData = stream.fromAsync(async () => (await onePassSig.correspondingSig).signatureData);
onePassSig.hashed = await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, streaming);
onePassSig.hashed = await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, false, streaming);
}));
msg.packets.stream = stream.transformPair(msg.packets.stream, async (readable, writable) => {
const reader = stream.getReader(readable);
@ -598,9 +599,9 @@ Message.prototype.verify = async function(keys, date=new Date(), streaming) {
await writer.abort(e);
}
});
return createVerificationObjects(onePassSigList, literalDataList, keys, date);
return createVerificationObjects(onePassSigList, literalDataList, keys, date, false);
}
return createVerificationObjects(signatureList, literalDataList, keys, date);
return createVerificationObjects(signatureList, literalDataList, keys, date, false);
};
/**
@ -618,7 +619,7 @@ Message.prototype.verifyDetached = function(signature, keys, date=new Date()) {
throw new Error('Can only verify message with one literal data packet.');
}
const signatureList = signature.packets;
return createVerificationObjects(signatureList, literalDataList, keys, date);
return createVerificationObjects(signatureList, literalDataList, keys, date, true);
};
/**
@ -628,11 +629,12 @@ Message.prototype.verifyDetached = function(signature, keys, date=new Date()) {
* @param {Array<module:key.Key>} keys array of keys to verify signatures
* @param {Date} date Verify the signature against the given date,
* i.e. check signature creation time < date < expiration time
* @param {Boolean} detached (optional) whether to verify detached signature packets
* @returns {Promise<Array<{keyid: module:type/keyid,
* valid: Boolean}>>} list of signer's keyid and validity of signature
* @async
*/
async function createVerificationObject(signature, literalDataList, keys, date=new Date()) {
async function createVerificationObject(signature, literalDataList, keys, date=new Date(), detached=false) {
let primaryKey = null;
let signingKey = null;
await Promise.all(keys.map(async function(key) {
@ -651,7 +653,7 @@ async function createVerificationObject(signature, literalDataList, keys, date=n
if (!signingKey) {
return null;
}
const verified = await signature.verify(signingKey.keyPacket, signature.signatureType, literalDataList[0]);
const verified = await signature.verify(signingKey.keyPacket, signature.signatureType, literalDataList[0], detached);
const sig = await signaturePacket;
if (sig.isExpired(date) || !(
sig.created >= signingKey.getCreationTime() &&
@ -689,15 +691,16 @@ async function createVerificationObject(signature, literalDataList, keys, date=n
* @param {Array<module:key.Key>} keys array of keys to verify signatures
* @param {Date} date Verify the signature against the given date,
* i.e. check signature creation time < date < expiration time
* @param {Boolean} detached (optional) whether to verify detached signature packets
* @returns {Promise<Array<{keyid: module:type/keyid,
* valid: Boolean}>>} list of signer's keyid and validity of signature
* @async
*/
export async function createVerificationObjects(signatureList, literalDataList, keys, date=new Date()) {
export async function createVerificationObjects(signatureList, literalDataList, keys, date=new Date(), detached=false) {
return Promise.all(signatureList.filter(function(signature) {
return ['text', 'binary'].includes(enums.read(enums.signature, signature.signatureType));
}).map(async function(signature) {
return createVerificationObject(signature, literalDataList, keys, date);
return createVerificationObject(signature, literalDataList, keys, date, detached);
}));
}

View File

@ -139,19 +139,30 @@ Literal.prototype.read = async function(bytes) {
};
/**
* Creates a string representation of the packet
* Creates a Uint8Array representation of the packet, excluding the data
*
* @returns {Uint8Array | ReadableStream<Uint8Array>} Uint8Array representation of the packet
* @returns {Uint8Array} Uint8Array representation of the packet
*/
Literal.prototype.write = function() {
Literal.prototype.writeHeader = function() {
const filename = util.encode_utf8(this.filename);
const filename_length = new Uint8Array([filename.length]);
const format = new Uint8Array([enums.write(enums.literal, this.format)]);
const date = util.writeDate(this.date);
return util.concatUint8Array([format, filename_length, filename, date]);
};
/**
* Creates a Uint8Array representation of the packet
*
* @returns {Uint8Array | ReadableStream<Uint8Array>} Uint8Array representation of the packet
*/
Literal.prototype.write = function() {
const header = this.writeHeader();
const data = this.getBytes();
return util.concat([format, filename_length, filename, date, data]);
return util.concat([header, data]);
};
export default Literal;

View File

@ -16,12 +16,14 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
/**
* @requires web-stream-tools
* @requires packet/signature
* @requires type/keyid
* @requires enums
* @requires util
*/
import stream from 'web-stream-tools';
import Signature from './signature';
import type_keyid from '../type/keyid';
import enums from '../enums';
@ -127,18 +129,12 @@ OnePassSignature.prototype.postCloneTypeFix = function() {
this.issuerKeyId = type_keyid.fromClone(this.issuerKeyId);
};
OnePassSignature.prototype.hash = function() {
const version = this.version;
this.version = 4;
try {
return Signature.prototype.hash.apply(this, arguments);
} finally {
this.version = version;
}
};
OnePassSignature.prototype.hash = Signature.prototype.hash;
OnePassSignature.prototype.toHash = Signature.prototype.toHash;
OnePassSignature.prototype.toSign = Signature.prototype.toSign;
OnePassSignature.prototype.calculateTrailer = Signature.prototype.calculateTrailer;
OnePassSignature.prototype.calculateTrailer = function(...args) {
return stream.fromAsync(async () => (await this.correspondingSig).calculateTrailer(...args));
};
OnePassSignature.prototype.verify = async function() {
const correspondingSig = await this.correspondingSig;

View File

@ -47,7 +47,7 @@ import config from '../config';
*/
function Signature(date=new Date()) {
this.tag = enums.packet.signature;
this.version = 4;
this.version = 4; // This is set to 5 below if we sign with a V5 key.
this.signatureType = null;
this.hashAlgorithm = null;
this.publicKeyAlgorithm = null;
@ -106,7 +106,7 @@ Signature.prototype.read = function (bytes) {
let i = 0;
this.version = bytes[i++];
if (this.version !== 4) {
if (this.version !== 4 && this.version !== 5) {
throw new Error('Version ' + this.version + ' of the signature is unsupported.');
}
@ -148,15 +148,19 @@ Signature.prototype.write = function () {
* Signs provided data. This needs to be done prior to serialization.
* @param {module:packet.SecretKey} key private key used to sign the message.
* @param {Object} data Contains packets to be signed.
* @param {Boolean} detached (optional) whether to create a detached signature
* @returns {Promise<Boolean>}
* @async
*/
Signature.prototype.sign = async function (key, data) {
Signature.prototype.sign = async function (key, data, detached=false) {
const signatureType = enums.write(enums.signature, this.signatureType);
const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm);
const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm);
const arr = [new Uint8Array([4, signatureType, publicKeyAlgorithm, hashAlgorithm])];
if (key.version === 5) {
this.version = 5;
}
const arr = [new Uint8Array([this.version, signatureType, publicKeyAlgorithm, hashAlgorithm])];
if (key.version === 5) {
// We could also generate this subpacket for version 4 keys, but for
@ -172,8 +176,8 @@ Signature.prototype.sign = async function (key, data) {
this.signatureData = util.concat(arr);
const toHash = this.toHash(signatureType, data);
const hash = await this.hash(signatureType, data, toHash);
const toHash = this.toHash(signatureType, data, detached);
const hash = await this.hash(signatureType, data, toHash, detached);
this.signedHashValue = stream.slice(stream.clone(hash), 0, 2);
@ -628,28 +632,42 @@ Signature.prototype.toSign = function (type, data) {
};
Signature.prototype.calculateTrailer = function () {
Signature.prototype.calculateTrailer = function (data, detached) {
let length = 0;
return stream.transform(stream.clone(this.signatureData), value => {
length += value.length;
}, () => {
const first = new Uint8Array([4, 0xFF]); //Version, ?
return util.concat([first, util.writeNumber(length, 4)]);
const arr = [];
if (this.version === 5 && (this.signatureType === enums.signature.binary || this.signatureType === enums.signature.text)) {
if (detached) {
arr.push(new Uint8Array(6));
} else {
arr.push(data.writeHeader());
}
}
arr.push(new Uint8Array([this.version, 0xFF]));
if (this.version === 5) {
arr.push(new Uint8Array(4));
}
arr.push(util.writeNumber(length, 4));
// For v5, this should really be writeNumber(length, 8) rather than the
// hardcoded 4 zero bytes above
return util.concat(arr);
});
};
Signature.prototype.toHash = function(signatureType, data) {
Signature.prototype.toHash = function(signatureType, data, detached=false) {
const bytes = this.toSign(signatureType, data);
return util.concat([bytes, this.signatureData, this.calculateTrailer()]);
return util.concat([bytes, this.signatureData, this.calculateTrailer(data, detached)]);
};
Signature.prototype.hash = async function(signatureType, data, toHash, streaming=true) {
Signature.prototype.hash = async function(signatureType, data, toHash, detached=false, streaming=true) {
const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm);
if (!toHash) toHash = this.toHash(signatureType, data);
if (!toHash) toHash = this.toHash(signatureType, data, detached);
if (!streaming && util.isStream(toHash)) {
return stream.fromAsync(async () => this.hash(signatureType, data, await stream.readToEnd(toHash)));
return stream.fromAsync(async () => this.hash(signatureType, data, await stream.readToEnd(toHash), detached));
}
return crypto.hash.digest(hashAlgorithm, toHash);
};
@ -661,10 +679,11 @@ Signature.prototype.hash = async function(signatureType, data, toHash, streaming
* module:packet.SecretSubkey|module:packet.SecretKey} key the public key to verify the signature
* @param {module:enums.signature} signatureType expected signature type
* @param {String|Object} data data which on the signature applies
* @param {Boolean} detached (optional) whether to verify a detached signature
* @returns {Promise<Boolean>} True if message is verified, else false.
* @async
*/
Signature.prototype.verify = async function (key, signatureType, data) {
Signature.prototype.verify = async function (key, signatureType, data, detached=false) {
const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm);
const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm);
@ -677,7 +696,7 @@ Signature.prototype.verify = async function (key, signatureType, data) {
if (this.hashed) {
hash = this.hashed;
} else {
toHash = this.toHash(signatureType, data);
toHash = this.toHash(signatureType, data, detached);
hash = await this.hash(signatureType, data, toHash);
}
hash = await stream.readToEnd(hash);

View File

@ -1305,6 +1305,42 @@ describe('[Sauce Labs Group 2] OpenPGP.js public api tests', function() {
});
});
it('should encrypt/sign and decrypt/verify with generated key and detached signatures', function () {
const genOpt = {
userIds: [{ name: 'Test User', email: 'text@example.com' }],
numBits: 512
};
if (openpgp.util.getWebCryptoAll()) { genOpt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys
return openpgp.generateKey(genOpt).then(async function(newKey) {
const newPublicKey = await openpgp.key.readArmored(newKey.publicKeyArmored);
const newPrivateKey = await openpgp.key.readArmored(newKey.privateKeyArmored);
const encOpt = {
message: openpgp.message.fromText(plaintext),
publicKeys: newPublicKey.keys,
privateKeys: newPrivateKey.keys,
detached: true
};
const decOpt = {
privateKeys: newPrivateKey.keys[0],
publicKeys: newPublicKey.keys
};
return openpgp.encrypt(encOpt).then(async function (encrypted) {
decOpt.message = await openpgp.message.readArmored(encrypted.data);
decOpt.signature = await openpgp.signature.readArmored(encrypted.signature);
expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect);
return openpgp.decrypt(decOpt);
}).then(async function (decrypted) {
expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures[0].valid).to.be.true;
const signingKey = await newPrivateKey.keys[0].getSigningKey();
expect(decrypted.signatures[0].keyid.toHex()).to.equal(signingKey.getKeyId().toHex());
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
});
});
});
it('should encrypt/sign and decrypt/verify with null string input', function () {
const encOpt = {
message: openpgp.message.fromText(''),