Skip to content

Commit 47138ee

Browse files
committed
Don't trust unhashed signature subpackets
Also, export packet.Signature.prototype.read_sub_packets.
1 parent 327d3e5 commit 47138ee

3 files changed

Lines changed: 129 additions & 22 deletions

File tree

src/packet/signature.js

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -109,31 +109,12 @@ Signature.prototype.read = function (bytes) {
109109
throw new Error('Version ' + this.version + ' of the signature is unsupported.');
110110
}
111111

112-
const subpackets = bytes => {
113-
// Two-octet scalar octet count for following subpacket data.
114-
const subpacket_length = util.readNumber(bytes.subarray(0, 2));
115-
116-
let i = 2;
117-
118-
// subpacket data set (zero or more subpackets)
119-
while (i < 2 + subpacket_length) {
120-
const len = packet.readSimpleLength(bytes.subarray(i, bytes.length));
121-
i += len.offset;
122-
123-
this.read_sub_packet(bytes.subarray(i, i + len.len));
124-
125-
i += len.len;
126-
}
127-
128-
return i;
129-
};
130-
131112
this.signatureType = bytes[i++];
132113
this.publicKeyAlgorithm = bytes[i++];
133114
this.hashAlgorithm = bytes[i++];
134115

135116
// hashed subpackets
136-
i += subpackets(bytes.subarray(i, bytes.length), true);
117+
i += this.read_sub_packets(bytes.subarray(i, bytes.length), true);
137118

138119
// A V4 signature hashes the packet body
139120
// starting from its first field, the version number, through the end
@@ -145,7 +126,7 @@ Signature.prototype.read = function (bytes) {
145126
const sigDataLength = i;
146127

147128
// unhashed subpackets
148-
i += subpackets(bytes.subarray(i, bytes.length), false);
129+
i += this.read_sub_packets(bytes.subarray(i, bytes.length), false);
149130
this.unhashedSubpackets = bytes.subarray(sigDataLength, i);
150131

151132
// Two-octet field holding left 16 bits of signed hash value.
@@ -346,7 +327,7 @@ function write_sub_packet(type, data) {
346327

347328
// V4 signature sub packets
348329

349-
Signature.prototype.read_sub_packet = function (bytes) {
330+
Signature.prototype.read_sub_packet = function (bytes, trusted=true) {
350331
let mypos = 0;
351332

352333
const read_array = (prop, bytes) => {
@@ -359,6 +340,17 @@ Signature.prototype.read_sub_packet = function (bytes) {
359340

360341
// The leftwost bit denotes a "critical" packet, but we ignore it.
361342
const type = bytes[mypos++] & 0x7F;
343+
344+
// GPG puts the Issuer and Signature subpackets in the unhashed area.
345+
// Tampering with those invalidates the signature, so we can trust them.
346+
// Ignore all other unhashed subpackets.
347+
if (!trusted && ![
348+
enums.signatureSubpacket.issuer,
349+
enums.signatureSubpacket.embedded_signature
350+
].includes(type)) {
351+
return;
352+
}
353+
362354
let seconds;
363355

364356
// subpacket type
@@ -515,6 +507,25 @@ Signature.prototype.read_sub_packet = function (bytes) {
515507
}
516508
};
517509

510+
Signature.prototype.read_sub_packets = function(bytes, trusted=true) {
511+
// Two-octet scalar octet count for following subpacket data.
512+
const subpacket_length = util.readNumber(bytes.subarray(0, 2));
513+
514+
let i = 2;
515+
516+
// subpacket data set (zero or more subpackets)
517+
while (i < 2 + subpacket_length) {
518+
const len = packet.readSimpleLength(bytes.subarray(i, bytes.length));
519+
i += len.offset;
520+
521+
this.read_sub_packet(bytes.subarray(i, i + len.len), trusted);
522+
523+
i += len.len;
524+
}
525+
526+
return i;
527+
};
528+
518529
// Produces data to produce signature on
519530
Signature.prototype.toSign = function (type, data) {
520531
const t = enums.signature;

test/security/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
describe('Security', function () {
22
require('./message_signature_bypass');
3+
require('./unsigned_subpackets');
34
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp');
2+
3+
const { key, message, enums, packet: { List, Signature } } = openpgp;
4+
5+
const chai = require('chai');
6+
chai.use(require('chai-as-promised'));
7+
8+
const expect = chai.expect;
9+
10+
/*
11+
* This key is long expired and cannot be used for encryption.
12+
*/
13+
const INVALID_KEY = `
14+
-----BEGIN PGP PRIVATE KEY BLOCK-----
15+
Version: OpenPGP.js VERSION
16+
Comment: https://openpgpjs.org
17+
18+
xcMGBDhtQ4ABB/9uAfnjiE8HLfFrk4AzYIoxISvIbqDlItn3Mk2RK4iGTaAL
19+
h+hN8BrqOopgdHj5c3pTo6VDvJLieHwymdZ3d296L55zt2ichhVIgRxh20Tv
20+
j0dYLKGIEWDMBKvQNoDi83eGrIeHGNjRDOipr/PD251LzwaeiNVyw8ce2Fpd
21+
1ORbC2MJU57C2appZqeMJsWPCnsHNkhxPyRGdp+vifgizi/lt2DcQ6C6EiJx
22+
HV0jFDPJnb69LxKLUelRH+l/b2ZHTONu2pZwUXcFpjA5yTrSzO/kaUtGu/Cz
23+
3euQ3scEtvMXgO2R9H7halxYwyXL/PPLmgaUt1RNXGC7BZjkUW4n8qd/ABEB
24+
AAH+CQMITYNkFGQHMiJgt2s69CHTfwUUZg1Yfcq8alY7GpqeH4CayWCMPI+v
25+
l7kIJdl2b9N/xGnpaUMmaXJts6AtlIBLwzxg0syIfgRv4/wfrVeruJ9TfCFC
26+
NbKP3lk3FZCGF0I4T1FSNvyPJ//ee1cX7U/gM7A2g5xyBFnH5d8LTUDlQjXb
27+
a+BwYN2TZaFrvlWwMIU+NQa+EOiyAwXsgyQbVn2d7JsUUs/lyEG2xkWNTeqe
28+
FWKJJvyDwixsxd7oobBSM6Krt2TreuelPBFQxKyaYyv81gASga9wxyfbIuTG
29+
7wAKW9i4pFMgrrIABcnNKOyeAgMDcAYXAW3eNbYDCIDL9/AuOUotPR+0pEun
30+
WssAZGBM78ZlJZ1Qnbg9nT0rn4pHrFQHnBxlWyPEqj1mZ0Svc0vXHVH+8JgN
31+
pwOGxo7DiF5lL/bphdFVMF2e+UPoc1efO4cpW+ZH/BOug14dJROfkrPhrUTp
32+
nYu6VF9N723YVT9PDTg79E4kIzjMDvhV1odHSaxfl4VtgueYv+Bt3n2nXdME
33+
XZVBXbp7jO7pTS5HsOBcModos8ZYS5RcaHPJ6H8807hFyva4GThZ744ryV8b
34+
XnROoC+d/xR4ShA4f/f9QszMXZ+Xlh4IU3Ccz5PF5UiZ/nC5ho5KzJphBB53
35+
c78gjRIXeUK1Rgj2AquF3KDOjCm60oazKzXv8316ZODNJr+HVvGSKeq85z9Z
36+
z/BfXUtn+PrmzHxegusZfFCpB6YAJCILsHgJ2gT8v26QF+1CJ3ngHVnSkghR
37+
z64zJexeqA8ChTZnhPbHVhh5qx2hlNTofBV29LJGa/EpMykO5pZiuaSEkmZx
38+
RpU+iKNYKa3U516O8f9yj+UZ5/t2SJRpT+9fro3RB4lUnt/RdkY8q2R+3owo
39+
xr4sYaInfvrs3eCsmh5UtygUVARKrK84zR1UZXN0aSBUZXN0IDx0ZXN0QGV4
40+
YW1wbGUuY29tPsLAewQQAQgALwUCOG1DgAUJAAAACgYLCQcIAwIJEMwSTBo3
41+
j0N5BBUICgIDFgIBAhkBAhsDAh4BAAD2TQf+KQbrX2zO9SL5ffCK5qu2VigM
42+
0E3uF763II9vRYfXHdZtXY/8K/uBLbu2rdZHwwb/jAHEe60Qf5VjcbIMtCfA
43+
khPB5JuCvW+JEsYhXplNxYka27svfWI75/cYVc/0OharKEaaPOv2F8C1k2jL
44+
Sk7Az01IAJkdwmBkG6fUwupevuvpO+kUQjsHg35q8Lm7G8roCYiK7K7/JQi3
45+
K+e0ovVFvunFSORaG8jR37uT7X7KA0LHD3S7XYO0o2OJi0QKB1wN3H3FEll0
46+
bFznfdIzKKIDzGwC/zhpUMGMwsqVLb8sw/H9cr82yPoM6pXVUqnstKDlLce8
47+
Dc2vwS83Aja9iWrIEg==
48+
=dvRO
49+
-----END PGP PRIVATE KEY BLOCK-----`;
50+
51+
async function getInvalidKey() {
52+
return (await key.readArmored(INVALID_KEY)).keys[0];
53+
}
54+
async function makeKeyValid() {
55+
/**
56+
* Checks if a key can be used for encryption.
57+
*/
58+
async function encryptFails(k) {
59+
try {
60+
await openpgp.encrypt({
61+
message: message.fromText('Hello', 'hello.txt'),
62+
publicKeys: k
63+
});
64+
return false;
65+
} catch (e) {
66+
return true;
67+
}
68+
}
69+
const invalidkey = await getInvalidKey();
70+
// deconstruct invalid key
71+
const [pubkey, puser, pusersig] = invalidkey.toPacketlist().map(i => i);
72+
// create a fake signature
73+
const fake = new Signature();
74+
Object.assign(fake, pusersig);
75+
// extend expiration times
76+
fake.keyExpirationTime = 0x7FFFFFFF;
77+
fake.signatureExpirationTime = 0x7FFFFFFF;
78+
// add key capability
79+
fake.keyFlags[0] |= enums.keyFlags.encrypt_communication;
80+
// create modified subpacket data
81+
pusersig.unhashedSubpackets = fake.write_all_sub_packets();
82+
// reconstruct the modified key
83+
const newlist = new List();
84+
newlist.concat([pubkey, puser, pusersig]);
85+
let modifiedkey = new key.Key(newlist);
86+
// re-read the message to eliminate any
87+
// behaviour due to cached values.
88+
modifiedkey = (await key.readArmored(
89+
await modifiedkey.armor())).keys[0];
90+
91+
expect(await encryptFails(invalidkey)).to.be.true;
92+
expect(await encryptFails(modifiedkey)).to.be.true;
93+
}
94+
95+
it('Does not accept unsigned subpackets', makeKeyValid);

0 commit comments

Comments
 (0)