From 0712e8af2ddb733afd1d0d8889cfc87588c835eb Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Tue, 18 Aug 2020 11:07:58 +0200 Subject: [PATCH] Support non-human-readable notation values (#983) This change adds support for binary (non-human-readable) values in signature notations through `rawNotations` property on signature objects. Human-readable notations will additionally appear in `notations` object where the value of the notation will be deserialized into a string. Additionally the check for human-readable flag was modified to check the existence of the flag instead of comparison with the whole value. --- src/packet/signature.js | 46 +++++++++++++++++++++------------------ test/general/packet.js | 25 ++++++++++++++++----- test/general/signature.js | 24 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 27 deletions(-) diff --git a/src/packet/signature.js b/src/packet/signature.js index 68b436cc..1e04357f 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -71,7 +71,8 @@ function Signature(date = new Date()) { this.revocationKeyAlgorithm = null; this.revocationKeyFingerprint = null; this.issuerKeyId = new type_keyid(); - this.notations = []; + this.rawNotations = []; + this.notations = {}; this.preferredHashAlgorithms = null; this.preferredCompressionAlgorithms = null; this.keyServerPreferences = null; @@ -233,13 +234,14 @@ Signature.prototype.write_hashed_sub_packets = function () { bytes = util.concat([bytes, this.revocationKeyFingerprint]); arr.push(write_sub_packet(sub.revocation_key, bytes)); } - this.notations.forEach(([name, value]) => { - bytes = [new Uint8Array([0x80, 0, 0, 0])]; + this.rawNotations.forEach(([{ name, value, humanReadable }]) => { + bytes = [new Uint8Array([humanReadable ? 0x80 : 0, 0, 0, 0])]; // 2 octets of name length bytes.push(util.writeNumber(name.length, 2)); // 2 octets of value length bytes.push(util.writeNumber(value.length, 2)); - bytes.push(util.str_to_Uint8Array(name + value)); + bytes.push(util.str_to_Uint8Array(name)); + bytes.push(value); bytes = util.concat(bytes); arr.push(write_sub_packet(sub.notation_data, bytes)); }); @@ -436,29 +438,31 @@ Signature.prototype.read_sub_packet = function (bytes, trusted = true) { this.issuerKeyId.read(bytes.subarray(mypos, bytes.length)); break; - case 20: + case 20: { // Notation Data - // We don't know how to handle anything but a text flagged data. - if (bytes[mypos] === 0x80) { - // We extract key/value tuple from the byte stream. - mypos += 4; - const m = util.readNumber(bytes.subarray(mypos, mypos + 2)); - mypos += 2; - const n = util.readNumber(bytes.subarray(mypos, mypos + 2)); - mypos += 2; + const humanReadable = !!(bytes[mypos] & 0x80); - const name = util.Uint8Array_to_str(bytes.subarray(mypos, mypos + m)); - const value = util.Uint8Array_to_str(bytes.subarray(mypos + m, mypos + m + n)); + // We extract key/value tuple from the byte stream. + mypos += 4; + const m = util.readNumber(bytes.subarray(mypos, mypos + 2)); + mypos += 2; + const n = util.readNumber(bytes.subarray(mypos, mypos + 2)); + mypos += 2; - this.notations.push([name, value]); + const name = util.Uint8Array_to_str(bytes.subarray(mypos, mypos + m)); + const value = bytes.subarray(mypos + m, mypos + m + n); - if (critical && (config.known_notations.indexOf(name) === -1)) { - throw new Error("Unknown critical notation: " + name); - } - } else { - util.print_debug("Unsupported notation flag " + bytes[mypos]); + this.rawNotations.push({ name, humanReadable, value }); + + if (humanReadable) { + this.notations[name] = util.Uint8Array_to_str(value); + } + + if (critical && (config.known_notations.indexOf(name) === -1)) { + throw new Error("Unknown critical notation: " + name); } break; + } case 21: // Preferred Hash Algorithms read_array('preferredHashAlgorithms', bytes.subarray(mypos, bytes.length)); diff --git a/test/general/packet.js b/test/general/packet.js index 3a1f4294..034a38c8 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -819,13 +819,26 @@ V+HOQJQxXJkVRYa3QrFUehiMzTeqqMdgC6ZqJy7+ const key = (await openpgp.key.readArmored(pubkey)).keys[0]; - const notations = key.users[0].selfCertifications[0].notations; + const { notations, rawNotations } = key.users[0].selfCertifications[0]; - expect(notations.length).to.equal(2); - expect(notations[0][0]).to.equal('test@example.com'); - expect(notations[0][1]).to.equal('2'); - expect(notations[1][0]).to.equal('test@example.com'); - expect(notations[1][1]).to.equal('3'); + // Even though there are two notations with the same keys + // the `notations` property reads only the single one: + // the last one encountered during parse + expect(Object.keys(notations).length).to.equal(1); + expect(notations['test@example.com']).to.equal('3'); + + // On the other hand `rawNotations` property provides access to all + // notations, even non human-readable. The values are not deserialized + // and they are byte-arrays. + expect(rawNotations.length).to.equal(2); + + expect(rawNotations[0].name).to.equal('test@example.com'); + expect(rawNotations[0].value).to.deep.equal(Uint8Array.from(['2'.charCodeAt(0)])); + expect(rawNotations[0].humanReadable).to.equal(true); + + expect(rawNotations[1].name).to.equal('test@example.com'); + expect(rawNotations[1].value).to.deep.equal(Uint8Array.from(['3'.charCodeAt(0)])); + expect(rawNotations[1].humanReadable).to.equal(true); }); it('Writing and encryption of a secret key packet.', function() { diff --git a/test/general/signature.js b/test/general/signature.js index 17de1de9..8c19b963 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -831,6 +831,15 @@ vwjE8mqJXetNMfj8r2SCyvkEnlVRYR+/mnge+ib56FdJ8uKtqSxyvgA= =fRXs -----END PGP MESSAGE-----`; +const signature_with_non_human_readable_notations = `-----BEGIN PGP SIGNATURE----- + +wncEARYKAB8FAl2TS9MYFAAAAAAADAADdGVzdEBrZXkuY29tAQIDAAoJEGZ9 +gtV/iL8hrhMBAOQ/UgqRTbx1Z8inGmRdUx1cJU1SR4Pnq/eJNH/CFk5DAP0Q +hUhMKMuiM3pRwdIyDOItkUWQmjEEw7/XmhgInkXsCw== +=ZGXr +-----END PGP SIGNATURE----- +`; + it('Testing signature checking on CAST5-enciphered message', async function() { const { reject_message_hash_algorithms } = openpgp.config; Object.assign(openpgp.config, { reject_message_hash_algorithms: new Set([openpgp.enums.hash.md5, openpgp.enums.hash.ripemd]) }); @@ -887,6 +896,21 @@ vwjE8mqJXetNMfj8r2SCyvkEnlVRYR+/mnge+ib56FdJ8uKtqSxyvgA= expect(sig.data).to.match(/-----END PGP MESSAGE-----\r\n$/); }); + it('Supports non-human-readable notations', async function() { + const { packets: [signature] } = await openpgp.message.readArmored(signature_with_non_human_readable_notations); + // There are no human-readable notations so `notations` property does not + // expose the `test@key.com` notation. + expect(Object.keys(signature.notations).length).to.equal(0); + expect(signature.notations['test@key.com']).to.equal(undefined); + + // The notation is readable through `rawNotations` property: + expect(signature.rawNotations.length).to.equal(1); + const notation = signature.rawNotations[0]; + expect(notation.name).to.equal('test@key.com'); + expect(notation.value).to.deep.equal(Uint8Array.from([0x01, 0x02, 0x03])); + expect(notation.humanReadable).to.equal(false); + }); + it('Verify V4 signature. Hash: SHA1. PK: RSA. Signature Type: 0x00 (binary document)', async function() { const { reject_message_hash_algorithms } = openpgp.config; Object.assign(openpgp.config, { reject_message_hash_algorithms: new Set([openpgp.enums.hash.md5, openpgp.enums.hash.ripemd]) });