fork-openpgpjs/src/packet/signature.js
Thomas Oberndörfer b0ea97ec28 Make packet tests run. Rename test files, move signature tests to general folder.
Split keyring tests in own file. Add tests for V3 signatures and fix verification.
2013-11-26 18:30:17 +01:00

623 lines
21 KiB
JavaScript

// 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 2.1 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
var util = require('../util'),
packet = require('./packet.js'),
enums = require('../enums.js'),
crypto = require('../crypto'),
type_mpi = require('../type/mpi.js'),
type_keyid = require('../type/keyid.js');
/**
* @class
* @classdesc Implementation of the Signature Packet (Tag 2)
*
* RFC4480 5.2:
* A Signature packet describes a binding between some public key and
* some data. The most common signatures are a signature of a file or a
* block of text, and a signature that is a certification of a User ID.
*/
module.exports = function packet_signature() {
this.version = null;
this.signatureType = null;
this.hashAlgorithm = null;
this.publicKeyAlgorithm = null;
this.signatureData = null;
this.signedHashValue = null;
this.mpi = null;
this.created = new Date();
this.signatureExpirationTime = null;
this.signatureNeverExpires = null;
this.exportable = null;
this.trustLevel = null;
this.trustAmount = null;
this.regularExpression = null;
this.revocable = null;
this.keyExpirationTime = null;
this.keyNeverExpires = null;
this.preferredSymmetricAlgorithms = null;
this.revocationKeyClass = null;
this.revocationKeyAlgorithm = null;
this.revocationKeyFingerprint = null;
this.issuerKeyId = new type_keyid();
this.notation = null;
this.preferredHashAlgorithms = null;
this.preferredCompressionAlgorithms = null;
this.keyServerPreferences = null;
this.preferredKeyServer = null;
this.isPrimaryUserID = null;
this.policyURI = null;
this.keyFlags = null;
this.signersUserId = null;
this.reasonForRevocationFlag = null;
this.reasonForRevocationString = null;
this.features = null;
this.signatureTargetPublicKeyAlgorithm = null;
this.signatureTargetHashAlgorithm = null;
this.signatureTargetHash = null;
this.embeddedSignature = null;
this.verified = false;
/**
* parsing function for a signature packet (tag 2).
* @param {String} bytes payload of a tag 2 packet
* @param {Integer} position position to start reading from the bytes string
* @param {Integer} len length of the packet or the remaining length of bytes at position
* @return {openpgp_packet_encrypteddata} object representation
*/
this.read = function(bytes) {
var i = 0;
this.version = bytes[i++].charCodeAt();
// switch on version (3 and 4)
switch (this.version) {
case 3:
// One-octet length of following hashed material. MUST be 5.
if (bytes[i++].charCodeAt() != 5)
util.print_debug("openpgp.packet.signature.js\n" +
'invalid One-octet length of following hashed material.' +
'MUST be 5. @:' + (i - 1));
var sigpos = i;
// One-octet signature type.
this.signatureType = bytes[i++].charCodeAt();
// Four-octet creation time.
this.created = util.readDate(bytes.substr(i, 4));
i += 4;
// storing data appended to data which gets verified
this.signatureData = bytes.substring(sigpos, i);
// Eight-octet Key ID of signer.
this.issuerKeyId.read(bytes.substring(i, i + 8));
i += 8;
// One-octet public-key algorithm.
this.publicKeyAlgorithm = bytes[i++].charCodeAt();
// One-octet hash algorithm.
this.hashAlgorithm = bytes[i++].charCodeAt();
break;
case 4:
this.signatureType = bytes[i++].charCodeAt();
this.publicKeyAlgorithm = bytes[i++].charCodeAt();
this.hashAlgorithm = bytes[i++].charCodeAt();
function subpackets(bytes, signed) {
// Two-octet scalar octet count for following hashed subpacket
// data.
var subpacket_length = util.readNumber(
bytes.substr(0, 2));
var i = 2;
// Hashed subpacket data set (zero or more subpackets)
var subpacked_read = 0;
while (i < 2 + subpacket_length) {
var len = packet.readSimpleLength(bytes.substr(i));
i += len.offset;
// Since it is trivial to add data to the unhashed portion of
// the packet we simply ignore all unauthenticated data.
if (signed)
this.read_sub_packet(bytes.substr(i, len.len));
i += len.len;
}
return i;
}
i += subpackets.call(this, bytes.substr(i), true);
// A V4 signature hashes the packet body
// starting from its first field, the version number, through the end
// of the hashed subpacket data. Thus, the fields hashed are the
// signature version, the signature type, the public-key algorithm, the
// hash algorithm, the hashed subpacket length, and the hashed
// subpacket body.
this.signatureData = bytes.substr(0, i);
i += subpackets.call(this, bytes.substr(i), false);
break;
default:
throw new Error('Version ' + version + ' of the signature is unsupported.');
break;
}
// Two-octet field holding left 16 bits of signed hash value.
this.signedHashValue = bytes.substr(i, 2);
i += 2;
this.signature = bytes.substr(i);
};
this.write = function() {
return this.signatureData +
util.writeNumber(0, 2) + // Number of unsigned subpackets.
this.signedHashValue +
this.signature;
};
/**
* Signs provided data. This needs to be done prior to serialization.
* @param {Object} data Contains packets to be signed.
* @param {openpgp_msg_privatekey} privatekey private key used to sign the message.
*/
this.sign = function(key, data) {
var signatureType = enums.write(enums.signature, this.signatureType),
publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm),
hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm);
var result = String.fromCharCode(4);
result += String.fromCharCode(signatureType);
result += String.fromCharCode(publicKeyAlgorithm);
result += String.fromCharCode(hashAlgorithm);
this.issuerKeyId = key.getKeyId();
// Add hashed subpackets
result += this.write_all_sub_packets();
this.signatureData = result;
var trailer = this.calculateTrailer();
var toHash = this.toSign(signatureType, data) +
this.signatureData + trailer;
var hash = crypto.hash.digest(hashAlgorithm, toHash);
this.signedHashValue = hash.substr(0, 2);
this.signature = crypto.signature.sign(hashAlgorithm,
publicKeyAlgorithm, key.mpi, toHash);
};
/**
* Creates string of bytes with all subpacket data
* @return {String} a string-representation of a all subpacket data
*/
this.write_all_sub_packets = function() {
var sub = enums.signatureSubpacket;
var result = '';
var bytes = '';
if (this.created !== null) {
result += write_sub_packet(sub.signature_creation_time, util.writeDate(this.created));
}
if (this.signatureExpirationTime !== null) {
result += write_sub_packet(sub.signature_expiration_time, util.writeDate(this.signatureExpirationTime));
}
if (this.exportable !== null) {
result += write_sub_packet(sub.exportable_certification, String.fromCharCode(this.exportable ? 1 : 0));
}
if (this.trustLevel !== null) {
bytes = String.fromCharCode(this.trustLevel) + String.fromCharCode(this.trustAmount);
result += write_sub_packet(sub.trust_signature, bytes);
}
if (this.regularExpression !== null) {
result += write_sub_packet(sub.regular_expression, this.regularExpression);
}
if (this.revocable !== null) {
result += write_sub_packet(sub.revocable, String.fromCharCode(this.revocable ? 1 : 0));
}
if (this.keyExpirationTime !== null) {
result += write_sub_packet(sub.key_expiration_time, util.writeDate(this.keyExpirationTime));
}
if (this.preferredSymmetricAlgorithms !== null) {
bytes = util.bin2str(this.preferredSymmetricAlgorithms);
result += write_sub_packet(sub.preferred_symmetric_algorithms, bytes);
}
if (this.revocationKeyClass !== null) {
bytes = String.fromCharCode(this.revocationKeyClass);
bytes += String.fromCharCode(this.revocationKeyAlgorithm);
bytes += this.revocationKeyFingerprint;
result += write_sub_packet(sub.revocation_key, bytes);
}
if (!this.issuerKeyId.isNull()) {
result += write_sub_packet(sub.issuer, this.issuerKeyId.write());
}
if (this.notation !== null) {
for (var name in this.notation) {
if (this.notation.hasOwnProperty(name)) {
var value = this.notation[name];
bytes = String.fromCharCode(0x80);
bytes += String.fromCharCode(0);
bytes += String.fromCharCode(0);
bytes += String.fromCharCode(0);
// 2 octets of name length
bytes += util.writeNumber(name.length, 2);
// 2 octets of value length
bytes += util.writeNumber(value.length, 2);
bytes += name + value;
result += write_sub_packet(sub.notation_data, bytes);
}
}
}
if (this.preferredHashAlgorithms !== null) {
bytes = util.bin2str(this.preferredHashAlgorithms);
result += write_sub_packet(sub.preferred_hash_algorithms, bytes);
}
if (this.preferredCompressionAlgorithms !== null) {
bytes = util.bin2str(this.preferredCompressionAlgorithms);
result += write_sub_packet(sub.preferred_hash_algorithms, bytes);
}
if (this.keyServerPreferences !== null) {
bytes = util.bin2str(this.keyServerPreferences);
result += write_sub_packet(sub.key_server_preferences, bytes);
}
if (this.preferredKeyServer !== null) {
result += write_sub_packet(sub.preferred_key_server, this.preferredKeyServer);
}
if (this.isPrimaryUserID !== null) {
result += write_sub_packet(sub.primary_user_id, String.fromCharCode(this.isPrimaryUserID ? 1 : 0));
}
if (this.policyURI !== null) {
result += write_sub_packet(sub.policy_uri, this.policyURI);
}
if (this.keyFlags !== null) {
bytes = util.bin2str(this.keyFlags);
result += write_sub_packet(sub.key_flags, bytes);
}
if (this.signersUserId !== null) {
result += write_sub_packet(sub.signers_user_id, this.signersUserId);
}
if (this.reasonForRevocationFlag !== null) {
bytes = String.fromCharCode(this.reasonForRevocationFlag);
bytes += this.reasonForRevocationString;
result += write_sub_packet(sub.reason_for_revocation, bytes);
}
if (this.features !== null) {
bytes = util.bin2str(this.features);
result += write_sub_packet(sub.features, bytes);
}
if (this.signatureTargetPublicKeyAlgorithm !== null) {
bytes = String.fromCharCode(this.signatureTargetPublicKeyAlgorithm);
bytes += String.fromCharCode(this.signatureTargetHashAlgorithm);
bytes += this.signatureTargetHash;
result += write_sub_packet(sub.signature_target, bytes);
}
if (this.embeddedSignature !== null) {
result += write_sub_packet(sub.embedded_signature, this.embeddedSignature.write());
}
result = util.writeNumber(result.length, 2) + result;
return result;
};
/**
* creates a string representation of a sub signature packet (See RFC 4880 5.2.3.1)
* @param {Integer} type subpacket signature type. Signature types as described
* in RFC4880 Section 5.2.3.2
* @param {String} data data to be included
* @return {String} a string-representation of a sub signature packet (See RFC 4880 5.2.3.1)
*/
function write_sub_packet(type, data) {
var result = "";
result += packet.writeSimpleLength(data.length + 1);
result += String.fromCharCode(type);
result += data;
return result;
}
// V4 signature sub packets
this.read_sub_packet = function(bytes) {
var mypos = 0;
function read_array(prop, bytes) {
this[prop] = [];
for (var i = 0; i < bytes.length; i++) {
this[prop].push(bytes[i].charCodeAt());
}
}
// The leftwost bit denotes a "critical" packet, but we ignore it.
var type = bytes[mypos++].charCodeAt() & 0x7F;
// subpacket type
switch (type) {
case 2:
// Signature Creation Time
this.created = util.readDate(bytes.substr(mypos));
break;
case 3:
// Signature Expiration Time
var time = util.readDate(bytes.substr(mypos));
this.signatureNeverExpires = time.getTime() == 0;
this.signatureExpirationTime = time;
break;
case 4:
// Exportable Certification
this.exportable = bytes[mypos++].charCodeAt() == 1;
break;
case 5:
// Trust Signature
this.trustLevel = bytes[mypos++].charCodeAt();
this.trustAmount = bytes[mypos++].charCodeAt();
break;
case 6:
// Regular Expression
this.regularExpression = bytes.substr(mypos);
break;
case 7:
// Revocable
this.revocable = bytes[mypos++].charCodeAt() == 1;
break;
case 9:
// Key Expiration Time
var time = util.readDate(bytes.substr(mypos));
this.keyExpirationTime = time;
this.keyNeverExpires = time.getTime() == 0;
break;
case 11:
// Preferred Symmetric Algorithms
this.preferredSymmetricAlgorithms = [];
while (mypos != bytes.length) {
this.preferredSymmetricAlgorithms.push(bytes[mypos++].charCodeAt());
}
break;
case 12:
// Revocation Key
// (1 octet of class, 1 octet of public-key algorithm ID, 20
// octets of
// fingerprint)
this.revocationKeyClass = bytes[mypos++].charCodeAt();
this.revocationKeyAlgorithm = bytes[mypos++].charCodeAt();
this.revocationKeyFingerprint = bytes.substr(mypos, 20);
break;
case 16:
// Issuer
this.issuerKeyId.read(bytes.substr(mypos));
break;
case 20:
// Notation Data
// We don't know how to handle anything but a text flagged data.
if (bytes[mypos].charCodeAt() == 0x80) {
// We extract key/value tuple from the byte stream.
mypos += 4;
var m = util.readNumber(bytes.substr(mypos, 2));
mypos += 2
var n = util.readNumber(bytes.substr(mypos, 2));
mypos += 2
var name = bytes.substr(mypos, m),
value = bytes.substr(mypos + m, n);
this.notation = this.notation || {};
this.notation[name] = value;
} else throw new Error("Unsupported notation flag.");
break;
case 21:
// Preferred Hash Algorithms
read_array.call(this, 'preferredHashAlgorithms', bytes.substr(mypos));
break;
case 22:
// Preferred Compression Algorithms
read_array.call(this, 'preferredCompressionAlgorithms ', bytes.substr(mypos));
break;
case 23:
// Key Server Preferences
read_array.call(this, 'keyServerPreferencess', bytes.substr(mypos));
break;
case 24:
// Preferred Key Server
this.preferredKeyServer = bytes.substr(mypos);
break;
case 25:
// Primary User ID
this.isPrimaryUserID = bytes[mypos++] != 0;
break;
case 26:
// Policy URI
this.policyURI = bytes.substr(mypos);
break;
case 27:
// Key Flags
read_array.call(this, 'keyFlags', bytes.substr(mypos));
break;
case 28:
// Signer's User ID
this.signersUserId += bytes.substr(mypos);
break;
case 29:
// Reason for Revocation
this.reasonForRevocationFlag = bytes[mypos++].charCodeAt();
this.reasonForRevocationString = bytes.substr(mypos);
break;
case 30:
// Features
read_array.call(this, 'features', bytes.substr(mypos));
break;
case 31:
// Signature Target
// (1 octet public-key algorithm, 1 octet hash algorithm, N octets hash)
this.signatureTargetPublicKeyAlgorithm = bytes[mypos++].charCodeAt();
this.signatureTargetHashAlgorithm = bytes[mypos++].charCodeAt();
var len = crypto.getHashByteLength(this.signatureTargetHashAlgorithm);
this.signatureTargetHash = bytes.substr(mypos, len);
break;
case 32:
// Embedded Signature
this.embeddedSignature = new packet_signature();
this.embeddedSignature.read(bytes.substr(mypos));
break;
default:
util.print_error("openpgp.packet.signature.js\n" +
'unknown signature subpacket type ' + type + " @:" + mypos +
" subplen:" + subplen + " len:" + len);
break;
}
};
// Produces data to produce signature on
this.toSign = function(type, data) {
var t = enums.signature;
switch (type) {
case t.binary:
// conversion to CRLF line endings done in literal data packet
case t.text:
return data.getBytes();
case t.standalone:
return '';
case t.cert_generic:
case t.cert_persona:
case t.cert_casual:
case t.cert_positive:
case t.cert_revocation:
{
var packet, tag;
if (data.userid !== undefined) {
tag = 0xB4;
packet = data.userid;
} else if (data.userattribute !== undefined) {
tag = 0xD1;
packet = data.userattribute;
} else throw new Error('Either a userid or userattribute packet needs to be ' +
'supplied for certification.');
var bytes = packet.write();
return this.toSign(t.key, data) +
String.fromCharCode(tag) +
util.writeNumber(bytes.length, 4) +
bytes;
}
case t.subkey_binding:
case t.key_binding:
{
return this.toSign(t.key, data) + this.toSign(t.key, {
key: data.bind
});
}
case t.key:
{
if (data.key == undefined)
throw new Error('Key packet is required for this sigtature.');
return data.key.writeOld();
}
case t.key_revocation:
case t.subkey_revocation:
return this.toSign(t.key, data);
case t.timestamp:
return '';
case t.third_party:
throw new Error('Not implemented');
break;
default:
throw new Error('Unknown signature type.')
}
}
this.calculateTrailer = function() {
// calculating the trailer
var trailer = '';
// V3 signatures don't have a trailer
if (this.version == 3) return trailer;
trailer += String.fromCharCode(4); // Version
trailer += String.fromCharCode(0xFF);
trailer += util.writeNumber(this.signatureData.length, 4);
return trailer
}
/**
* verifys the signature packet. Note: not signature types are implemented
* @param {String} data data which on the signature applies
* @param {openpgp_msg_privatekey} key the public key to verify the signature
* @return {boolean} True if message is verified, else false.
*/
this.verify = function(key, data) {
var signatureType = enums.write(enums.signature, this.signatureType),
publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm),
hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm);
var bytes = this.toSign(signatureType, data),
trailer = this.calculateTrailer();
var mpicount = 0;
// Algorithm-Specific Fields for RSA signatures:
// - multiprecision number (MPI) of RSA signature value m**d mod n.
if (publicKeyAlgorithm > 0 && publicKeyAlgorithm < 4)
mpicount = 1;
// Algorithm-Specific Fields for DSA signatures:
// - MPI of DSA value r.
// - MPI of DSA value s.
else if (publicKeyAlgorithm == 17)
mpicount = 2;
var mpi = [],
i = 0;
for (var j = 0; j < mpicount; j++) {
mpi[j] = new type_mpi();
i += mpi[j].read(this.signature.substr(i));
}
this.verified = crypto.signature.verify(publicKeyAlgorithm,
hashAlgorithm, mpi, key.mpi,
bytes + this.signatureData + trailer);
return this.verified;
}
}