Skip to content

Commit 3f0b49a

Browse files
committed
Fix signature verification issues.
**SECURITY**: Three RSA PKCS#1 v1.5 signature verification issues were reported by Moosa Yahyazadeh ([email protected]): - Leniency in checking `digestAlgorithm` structure can lead to signature forgery. - The code is lenient in checking the digest algorithm structure. This can allow a crafted structure that steals padding bytes and uses unchecked portion of the PKCS#1 encoded message to forge a signature when a low public exponent is being used. - Failing to check tailing garbage bytes can lead to signature forgery. - The code does not check for tailing garbage bytes after decoding a `DigestInfo` ASN.1 structure. This can allow padding bytes to be removed and garbage data added to forge a signature when a low public exponent is being used. - Leniency in checking type octet. - `DigestInfo` is not properly checked for proper ASN.1 structure. This can lead to successful verification with signatures that contain invalid structures but a valid digest. For more information, please see "Bleichenbacher's RSA signature forgery based on implementation error" by Hal Finney: https://mailarchive.ietf.org/arch/msg/openpgp/5rnE9ZRN1AokBVj3VqblGlP63QE/ Fixed with the following: - [asn1] `fromDer` is now more strict and will default to ensuring all input bytes are parsed or throw an error. A new option `parseAllBytes` can disable this behavior. - **NOTE**: The previous behavior is being changed since it can lead to security issues with crafted inputs. It is possible that code doing custom DER parsing may need to adapt to this new behavior and optional flag. - [rsa] Add and use a validator to check for proper structure of parsed ASN.1 `RSASSA-PKCS-v1_5` `DigestInfo` data. Additionally check that the hash algorithm identifier is a known value. An invalid `DigestInfo` or algorithm identifier will now cause an error to be thrown. - [oid] Added `1.2.840.113549.2.2` / `md2` for hash algorithm checking. - [tests] Tests were added for all of the reported issues. A private verify option was added to assist in checking multiple possible failures in the test data.
1 parent c20f309 commit 3f0b49a

File tree

5 files changed

+284
-4
lines changed

5 files changed

+284
-4
lines changed

CHANGELOG.md

+38
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,46 @@ Forge ChangeLog
33

44
## 1.3.0 - 2022-XXX
55

6+
### Security
7+
- **SECURITY**: Three RSA PKCS#1 v1.5 signature verification issues were
8+
reported by Moosa Yahyazadeh ([email protected]).
9+
- Leniency in checking `digestAlgorithm` structure can lead to signature
10+
forgery.
11+
- The code is lenient in checking the digest algorithm structure. This can
12+
allow a crafted structure that steals padding bytes and uses unchecked
13+
portion of the PKCS#1 encoded message to forge a signature when a low
14+
public exponent is being used. For more information, please see
15+
["Bleichenbacher's RSA signature forgery based on implementation
16+
error"](https://mailarchive.ietf.org/arch/msg/openpgp/5rnE9ZRN1AokBVj3VqblGlP63QE/)
17+
by Hal Finney.
18+
- Failing to check tailing garbage bytes can lead to signature forgery.
19+
- The code does not check for tailing garbage bytes after decoding a
20+
`DigestInfo` ASN.1 structure. This can allow padding bytes to be removed
21+
and garbage data added to forge a signature when a low public exponent is
22+
being used. For more information, please see ["Bleichenbacher's RSA
23+
signature forgery based on implementation
24+
error"](https://mailarchive.ietf.org/arch/msg/openpgp/5rnE9ZRN1AokBVj3VqblGlP63QE/)
25+
by Hal Finney.
26+
- Leniency in checking type octet.
27+
- `DigestInfo` is not properly checked for proper ASN.1 structure. This can
28+
lead to successful verification with signatures that contain invalid
29+
structures but a valid digest.
30+
631
### Fixed
732
- [asn1] Add fallback to pretty print invalid UTF8 data.
33+
- [asn1] `fromDer` is now more strict and will default to ensuring all input
34+
bytes are parsed or throw an error. A new option `parseAllBytes` can disable
35+
this behavior.
36+
- **NOTE**: The previous behavior is being changed since it can lead to
37+
security issues with crafted inputs. It is possible that code doing custom
38+
DER parsing may need to adapt to this new behavior and optional flag.
39+
- [rsa] Add and use a validator to check for proper structure of parsed ASN.1
40+
`RSASSA-PKCS-v1_5` `DigestInfo` data. Additionally check that the hash
41+
algorithm identifier is a known value. An invalid `DigestInfo` or algorithm
42+
identifier will now cause an error to be thrown.
43+
44+
### Added
45+
- [oid] Added `1.2.840.113549.2.2` / `md2` for hash algorithm checking.
846

947
## 1.2.1 - 2022-01-11
1048

lib/asn1.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -411,31 +411,40 @@ var _getValueLength = function(bytes, remaining) {
411411
* @param [options] object with options or boolean strict flag
412412
* [strict] true to be strict when checking value lengths, false to
413413
* allow truncated values (default: true).
414+
* [parseAllBytes] true to ensure all bytes are parsed
415+
* (default: true)
414416
* [decodeBitStrings] true to attempt to decode the content of
415417
* BIT STRINGs (not OCTET STRINGs) using strict mode. Note that
416418
* without schema support to understand the data context this can
417419
* erroneously decode values that happen to be valid ASN.1. This
418420
* flag will be deprecated or removed as soon as schema support is
419421
* available. (default: true)
420422
*
423+
* @throws Will throw an error for various malformed input conditions.
424+
*
421425
* @return the parsed asn1 object.
422426
*/
423427
asn1.fromDer = function(bytes, options) {
424428
if(options === undefined) {
425429
options = {
426430
strict: true,
431+
parseAllBytes: true,
427432
decodeBitStrings: true
428433
};
429434
}
430435
if(typeof options === 'boolean') {
431436
options = {
432437
strict: options,
438+
parseAllBytes: true,
433439
decodeBitStrings: true
434440
};
435441
}
436442
if(!('strict' in options)) {
437443
options.strict = true;
438444
}
445+
if(!('parseAllBytes' in options)) {
446+
options.parseAllBytes = true;
447+
}
439448
if(!('decodeBitStrings' in options)) {
440449
options.decodeBitStrings = true;
441450
}
@@ -445,7 +454,15 @@ asn1.fromDer = function(bytes, options) {
445454
bytes = forge.util.createBuffer(bytes);
446455
}
447456

448-
return _fromDer(bytes, bytes.length(), 0, options);
457+
var byteCount = bytes.length();
458+
var value = _fromDer(bytes, bytes.length(), 0, options);
459+
if(options.parseAllBytes && bytes.length() !== 0) {
460+
var error = new Error('Unparsed DER bytes remain after ASN.1 parsing.');
461+
error.byteCount = byteCount;
462+
error.remaining = bytes.length();
463+
throw error;
464+
}
465+
return value;
449466
};
450467

451468
/**

lib/oids.js

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ _IN('1.3.14.3.2.29', 'sha1WithRSASignature');
4747
_IN('2.16.840.1.101.3.4.2.1', 'sha256');
4848
_IN('2.16.840.1.101.3.4.2.2', 'sha384');
4949
_IN('2.16.840.1.101.3.4.2.3', 'sha512');
50+
_IN('1.2.840.113549.2.2', 'md2');
5051
_IN('1.2.840.113549.2.5', 'md5');
5152

5253
// pkcs#7 content types

lib/rsa.js

+77-3
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,40 @@ var publicKeyValidator = forge.pki.rsa.publicKeyValidator = {
264264
}]
265265
};
266266

267+
// validator for a DigestInfo structure
268+
var digestInfoValidator = {
269+
name: 'DigestInfo',
270+
tagClass: asn1.Class.UNIVERSAL,
271+
type: asn1.Type.SEQUENCE,
272+
constructed: true,
273+
value: [{
274+
name: 'DigestInfo.DigestAlgorithm',
275+
tagClass: asn1.Class.UNIVERSAL,
276+
type: asn1.Type.SEQUENCE,
277+
constructed: true,
278+
value: [{
279+
name: 'DigestInfo.DigestAlgorithm.algorithmIdentifier',
280+
tagClass: asn1.Class.UNIVERSAL,
281+
type: asn1.Type.OID,
282+
constructed: false,
283+
capture: 'algorithmIdentifier'
284+
}, {
285+
// NULL paramters
286+
name: 'DigestInfo.DigestAlgorithm.parameters',
287+
tagClass: asn1.Class.UNIVERSAL,
288+
type: asn1.Type.NULL,
289+
constructed: false
290+
}]
291+
}, {
292+
// digest
293+
name: 'DigestInfo.digest',
294+
tagClass: asn1.Class.UNIVERSAL,
295+
type: asn1.Type.OCTETSTRING,
296+
constructed: false,
297+
capture: 'digest'
298+
}]
299+
};
300+
267301
/**
268302
* Wrap digest in DigestInfo object.
269303
*
@@ -1092,25 +1126,65 @@ pki.setRsaPublicKey = pki.rsa.setPublicKey = function(n, e) {
10921126
* a Forge PSS object for RSASSA-PSS,
10931127
* 'NONE' or null for none, DigestInfo will not be expected, but
10941128
* PKCS#1 v1.5 padding will still be used.
1129+
* @param options optional verify options
1130+
* _parseAllDigestBytes testing flag to control parsing of all
1131+
* digest bytes. Unsupported and not for general usage.
1132+
* (default: true)
10951133
*
10961134
* @return true if the signature was verified, false if not.
10971135
*/
1098-
key.verify = function(digest, signature, scheme) {
1136+
key.verify = function(digest, signature, scheme, options) {
10991137
if(typeof scheme === 'string') {
11001138
scheme = scheme.toUpperCase();
11011139
} else if(scheme === undefined) {
11021140
scheme = 'RSASSA-PKCS1-V1_5';
11031141
}
1142+
if(options === undefined) {
1143+
options = {
1144+
_parseAllDigestBytes: true
1145+
};
1146+
}
1147+
if(!('_parseAllDigestBytes' in options)) {
1148+
options._parseAllDigestBytes = true;
1149+
}
11041150

11051151
if(scheme === 'RSASSA-PKCS1-V1_5') {
11061152
scheme = {
11071153
verify: function(digest, d) {
11081154
// remove padding
11091155
d = _decodePkcs1_v1_5(d, key, true);
11101156
// d is ASN.1 BER-encoded DigestInfo
1111-
var obj = asn1.fromDer(d);
1157+
var obj = asn1.fromDer(d, {
1158+
parseAllBytes: options._parseAllDigestBytes
1159+
});
1160+
1161+
// validate DigestInfo
1162+
var capture = {};
1163+
var errors = [];
1164+
if(!asn1.validate(obj, digestInfoValidator, capture, errors)) {
1165+
var error = new Error(
1166+
'ASN.1 object does not contain a valid RSASSA-PKCS1-v1_5 ' +
1167+
'DigestInfo value.');
1168+
error.errors = errors;
1169+
throw error;
1170+
}
1171+
// check hash algorithm identifier
1172+
// FIXME: add support to vaidator for strict value choices
1173+
var oid = asn1.derToOid(capture.algorithmIdentifier);
1174+
if(!(oid === forge.oids.md2 ||
1175+
oid === forge.oids.md5 ||
1176+
oid === forge.oids.sha1 ||
1177+
oid === forge.oids.sha256 ||
1178+
oid === forge.oids.sha384 ||
1179+
oid === forge.oids.sha512)) {
1180+
var error = new Error(
1181+
'Unknown RSASSA-PKCS1-v1_5 DigestAlgorithm identifier.');
1182+
error.oid = oid;
1183+
throw error;
1184+
}
1185+
11121186
// compare the given digest to the decrypted one
1113-
return digest === obj.value[1].value;
1187+
return digest === capture.digest;
11141188
}
11151189
};
11161190
} else if(scheme === 'NONE' || scheme === 'NULL' || scheme === null) {

0 commit comments

Comments
 (0)