-
Notifications
You must be signed in to change notification settings - Fork 397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ECDSA signature verification error due to leading zero #322
Conversation
this solution appears breaking for existing apps using elliptic, which is very unfortunate since ethers 5 is still widely used but not being updated. Of course, should check if |
@@ -97,7 +98,7 @@ EC.prototype.sign = function sign(msg, key, enc, options) { | |||
options = {}; | |||
|
|||
key = this.keyFromPrivate(key, enc); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we just did this
var msgBitSize = typeof msg === 'string' ? msg.replace(/\s+/g,'').length * 4 : 64;
leave the public function prototypes as they are?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems like a semver-major breaking api change
can this be patched without api changes?
Can someone please merge this pr @Markus-MS |
wow, ok. |
@hunkydoryrepair @ChALkeR you are correct that the current changes require the hash size as a parameter, which means that a code update downstream would be necessary. Requiring the hash size is arguably the most secure option (and, as far as I can tell, is the standard in most crypto libraries), but in this particular case, it is certainly not the most convenient option when it comes to backward compatibility. From my perspective, there are two alternatives (though slightly less secure) that have already been discussed by @hunkydoryrepair which would avoid any breaking API changes:
The proposed changes by @hunkydoryrepair are a good start, but going this route requires checking the hash size for all supported input types (not just strings). For example, if one provides the hash as an Array (by first converting it using the console.log('Valid signature: ' + pubKey.verify(toArray(hashStr, 'hex'), sig)); Option 1 might be a reasonable middle ground, although you lose some security. |
@avembankottu I do not have the necessary permissions to merge any changes directly. |
my concern is the ethers 5 package. It is not being updated, but dozens of packages are built on ethers 5, and ethers 6 did a HUGE number of breaking changes so things built on ethers 5 are not migrating quickly to 6 (if ever). |
Cant we do this instead breaking api @Markus-MS |
I tend to agree, although I don't know that this PR and Issue submitter is the same one that submitted the CVE. The CVE is just quoted here in the description. I'm unclear on how breaking this is. CVE claims it could validate invalid signatures, but I'm not sure how that is possible. Sure, trimming a leading zero on a valid signature would still pass, but you'd need a valid signature to come up with the invalid one, so does not seem exploitable. |
Any idea when will it get fixed ? |
It would get fixed when someone fixes it. There is no need to ask these questions. |
I think the correct API for implementations that accept prehashes is to define the hash and digest length when constructing the signer and verifier and any digest that does not have the correct length should be rejected. It feels very strange when the caller must know the length of the digest, but the signer and verfier do not have this information. Accepting, arbitrary digest lengths just makes the implementation difficult to analyze. I.e. the question now becomes: "Is there a set of arbitrarily long digests, claimed digest lengths so that receiving their signatures either computed with the broken or the fixed version of the code leaks the private key? As far as I can determine this is luckily not the case, and it appears that indeed some luck was needed here. |
@bleichenbacher-daniel RFC6979 and FIPS 186-4 4.6 suggest the leftmost |
Hello everyone! Sorry for the delay on my end with the original report and this open Pull Request. I'm evaluating options for applying this patch since as y'all have pointed out doing it as is means cutting a major release which we won't be helpful to anyone since people won't auto-upgrade. My current thinking is along the lines of what have been suggested here:
Both are breaking changes regardless of how you look at them... |
@indutny bn messages sound weird. I think changing behavior for them is fine. Auto-computing bit length for strings / arrays seems easy and simple. |
According to FIPS 186-5, section 6.4.2 ECDSA Signature Verification Algorithm, the hash of the message must be adjusted based on the order n of the base point of the elliptic curve: If log2(n) ≥ hashlen, set E = H. Otherwise, set E equal to the leftmost log2(n) bits of H. Unfortunately because elliptic converts messages to BN instances the reported `byteLength()` for the message can be incorrect if the message has 8 or more leading zero bits. Here we fix it by: 1. Counting leading zeroes in hex strings provided as messages 2. Counting all array entries in Array-like (e.g. Buffer) messages 3. Providing an `msgBitLength` option to both `.sign`/`.verify` to let user override the behavior Original PR: #322 Credit: @Markus-MS
Sounds good. Here is the proposed alternative PR #326 . I'd appreciate any feedback, and plan to merge it this evening unless someone has significant objections. Thank y'all! |
According to FIPS 186-5, section 6.4.2 ECDSA Signature Verification Algorithm, the hash of the message must be adjusted based on the order n of the base point of the elliptic curve: If log2(n) ≥ hashlen, set E = H. Otherwise, set E equal to the leftmost log2(n) bits of H. Unfortunately because elliptic converts messages to BN instances the reported `byteLength()` for the message can be incorrect if the message has 8 or more leading zero bits. Here we fix it by: 1. Counting leading zeroes in hex strings provided as messages 2. Counting all array entries in Array-like (e.g. Buffer) messages 3. Providing an `msgBitLength` option to both `.sign`/`.verify` to let user override the behavior Original PR: #322 Credit: @Markus-MS
Published similar fix in 6.6.0. Thank you! |
Hi, |
I've been running some small experiments to determine the severity of this bug. |
@bleichenbacher-daniel is this also related to bn.js? |
@paulmillr this is still the same issue. I'm running tests against my test vectors. The test itself signs valid hashes represented as Uint8Array with the elliptic library. The interface of the library is indeed quite uncommon. It makes the implementation of ECDSA more brittle and the analysis of the implementation more complex as there are more potential attack vectors to cover. I haven't looked at bn.js. There may be timing based attacks, but this is a whole other story. I'm currently, generating more test vectors, since it is still unclear how serious this issue really is. |
I've done some more testing. About 1 out of every 256 signatures with vulnerable curves are incorrect leading to the problem above. Again, this uses version 6.6.0. I haven not tested previous versions, hence I don't know if the problem is better or worse now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!!!
@bleichenbacher-daniel is there a Proof-of-Concept code that you could share with me? The original issue is public so I don't think there's much harm in doing it here. |
An example that fails is the following: curve = secp521r1 Shifting the incorrect k by 7 bits to the right gives the correct k. |
Also, just to make sure: updating from the broken version to a correct implementation requires a key revocation, since by doing so users risk to generate both correct and broken signatures. |
According to FIPS 186-5, section 6.4.2 ECDSA Signature Verification Algorithm, the hash of the message must be adjusted based on the order n of the base point of the elliptic curve: If log2(n) ≥ hashlen, set E = H. Otherwise, set E equal to the leftmost log2(n) bits of H. Unfortunately because elliptic converts messages to BN instances the reported `byteLength()` for the message can be incorrect if the message has 8 or more leading zero bits. Here we fix it by: 1. Counting leading zeroes in hex strings provided as messages 2. Counting all array entries in Array-like (e.g. Buffer) messages 3. Providing an `msgBitLength` option to both `.sign`/`.verify` to let user override the behavior Original PR: indutny#322 Credit: @Markus-MS
Summary
The Elliptic package 6.5.7 for Node.js ECDSA implementation does not correctly verify valid signatures if the hash contains at least 4 leading 0 bytes and when the order of the elliptic curve's base point is smaller than the hash. This leads to valid signatures being rejected.
This vulnerability was privately disclosed via the GitHub security advisory feature on July 17, 2024, with a public disclosure deadline of October 15, 2024. Unfortunately, we received no communication and are now publicly disclosing the issue. We reserved CVE-2024-48948 to track this vulnerability. This vulnerability has since also been discussed in Issue #321.
Tested Version
v6.5.7
Details
The ECDSA implementation contains an issue where valid signatures fail the validation check.
Hashes containing at least four leading zero bytes fail to validate even if they are correct if the hash size is greater than the order
n
of the base point of the elliptic curve.An example of a problematic hash stems from hashing the hex string
343236343739373234
using SHA256, resulting in:According to FIPS 186-5, section 6.4.2 ECDSA Signature Verification Algorithm, the hash of the message must be adjusted based on the order
n
of the base point of the elliptic curve:To achieve this, the
_truncateToN
function is called, performing the necessary adjustment.Before this function is called, the hashed message
msg
is converted from a hex string or array into a number object usingnew BN(msg, 16)
.File: lib/elliptic/ec/index.js
The issue arises due to
new BN(msg, 16)
removing leading zeros (if there are at least 4), reducing the size of the original hash.Removing the leading zeros results in a smaller hash, which takes up fewer bytes.
To illustrate the issue, we will use
secp192r1
, which uses 192 bits, andSHA256
as an example of an elliptic curve and hash.The hash should be shifted by 64 bits to the right to retain the leftmost 192 bits.
However, during the delta calculation,
msg.byteLength()
returns 28 bytes instead of 32.Consequently, the hashed message is not shifted correctly, causing verification to fail.
PoC
First, the library in question must be installed:
npm install elliptic@6.5.7
and then the POC can be executed using this commands:
node poc.js
Impact
This issue prevents the successful validation of certain valid ECDSA signatures.
As a result, legitimate transactions or communications may be incorrectly flagged as invalid, leading to potential disruptions in service and a loss of trust in the system's reliability and security.
Furthermore, this vulnerability could cause a split in consensus among nodes in distributed systems, as some nodes might accept transactions that others reject, potentially leading to network fragmentation and inconsistent state across the system.
Remediation
We propose to remedy this issue by adding the bit size of the message as a required argument.
File: elliptic/lib/elliptic/ec/index.js
Since the msgBitSize argument would be provided by the user of the library, this requires changes to the function signature of:
File: lib/elliptic/ec/index.js
as well as the usages of
_truncateToN
within these functionsFurthermore, the usage of EC.prototype.verify and EC.prototype.sign would need to be changed inside the corresponding KeyPair.prototype.sign and KeyPair.prototype.verify functions as well as their signatures.
File: lib/elliptic/ec/key.js
Subsequently, the ECDSA tests
test/ecdsa-test.js
must be adjusted to provide the now-required msgBitSize to pass.Discovered by
Markus Schiffermüller at Trail of Bits
Scott Arciszewski at Trail of Bits
using wycheproof