In this article, we explore the compatibility issue when signing messages on Ledger devices, that are relayed via the MetaMask extension into web apps with libraries that doesn’t alter the last byte of the signature.
The descriptions here are specifically related to the use of canonical {v} values in vrs signatures produced by Ledger. We discuss the problem's impact on user experience and provide a practical solution for developers to implement, ensuring a seamless and secure interaction between these popular tools in the web3 ecosystem.
🔐 🦊 If you're facing issues with validating signatures from Ledger devices, where the user connects with MetaMask, this article might hold the key to solving your problem.
— "I’m in a hurry – just give me the fix so I can make it work."
You’ll find an example of 5 lines of JS that fixes the signatures if you scroll down.
In the world of decentralized applications (dApps) and web3, ensuring a seamless user experience is crucial for adoption and usability. One challenge that developers sometimes face is the compatibility between hardware wallets like Ledger and browser extensions such as MetaMask. Specifically, an issue arises with validating message signatures, which affects a subset of users who use MetaMask in combination with a Ledger device. This article discusses the nature of the issue, its impact on the user experience, and provides a solution to address it. Additionally, we will explore common symptoms to watch out for and what developers should consider when building dApps that rely on signature authentication for various actions.
The issue with signatures might show up in other combinations than just on the discussed case of Ledger + MetaMask. You could also see the same problems with validating signatures from a few other wallets / providers that relay raw signatures.
Developers need to be aware of the potential issues that may arise when users sign messages with a Ledger device connected through MetaMask. Some common symptoms of the issue include:
Validation step failure: The signing step may appear successful for the user, but the validation step fails when verifying that the signature originates from a specific address. This can lead to untested behaviour in the frontend app.
Stuck on the login screen: Users may find themselves stuck on the login screen, even after signing the message, because the frontend app does not proceed further due to the validation failure.
Cryptic error messages: In some cases, users might encounter confusing JSON errors that leak through the backend service to the user interface, providing little information about the actual problem.
Silent failures: In certain situations, no error message is displayed, and the issue can only be detected by inspecting the developer console or network tab in the browser.
As a developer, it is essential to be vigilant about these symptoms and consider implementing the provided solution to ensure a smooth experience for all users. By addressing this issue, you can create a more inclusive and user-friendly environment for Ledger users on your web app, enabling seamless authentication for logins, minting, settings changes, and other crucial functions.
While this article highlights an issue which you may encounter when validating Ledger signatures, it is important to recognize the numerous benefits of using hardware wallets like Ledger. These devices offer a secure and convenient way to store and manage digital assets, making them an attractive option for many users in the web3 space. 🌟🙏
The kind of signatures we're looking at are vrs signatures. In an execution context, we expect these signature to be a hexadecimal string that may or may not be prepended by 0x. A signature value of exactly 130 hexadecimal characters (0-9, a-f) can also be represented as 65 pairs of two's complemented hexadecimal values, or more commonly, 65 full bytes. A 65 byte signature, or vrs signature consists of three components: {r, s, v}.
{r} and {s} are outputs of an ECDSA signature. Together they add up to the first 64 bytes.
{v} is the last byte in the signature, and nowadays for signature validadtion has to be either 27 (0x1b) or 28 (0x1c).
The {v} identifier is important because since we are working with elliptic curves, multiple points on the curve can be calculated from r and s alone. This would result in two different public keys (thus addresses) that can be recovered. The {v} simply indicates which one of these points to use.
The alignment done towards the current standard {27, 28} {v} values for signatures in validation ensures that not several different signatures for the same message and address can be created. Thus, {v} values of {0, 1} should be denied within general signature validation.
This GitHub comment on an issue about “inconsistant usage of {v} values in signatures” higlights what’s happening in our case.
Signatures with v = {0, 1} and signatures with v = {27, 28} are both kinda valid - and some software will pass validation for the same address using both {v} values of 0 and 27, or similar {v} values of 1 and 28.
Many tools and contracts will fail signature validation of signatures ending with 0x00 or 0x01.
When signing a message on a Ledger and then relaying the signature to MetaMask, the {v} byte is still going to be 0 or 1 when it is sent to the dapp, instead of the expected 27 or 28. The invalid last byte will cause validation of the signature to fail as it is nowadays not an expected value.
This is inline documentation from ECDSA.sol
noting that OpenZeppelin's ECDSA implementation will not accept signatures where the {v} value isn't {27, 28}.
/*
* The `ecrecover` EVM opcode allows for malleable (non-unique)
* signatures: this function rejects them by requiring the `s`
* value to be in the lower half order, and the `v` value to be
* either 27 or 28.
*
* If your library generates signatures with 0/1 for v instead of
* 27/28, add 27 to v to accept these malleable signatures as well.
*/
0x00
or 0x01
For any modification of the signature's {v} value all of the following checks must pass.
In order to not accidentally cause surprise errors caused by library updates, we verify that the signature is a string.
The signature is allowed to be prepended with 0x or not. Besides any potential 0x prefix, the string must be 130 chars.
130 hexadecimal characters can better be represented as 65 pairs of two's complemented values, or 65 full bytes.
By ensuring the signature is 130 hexadecimal chars (65 byte) we can be sure that it has each {r, s, v} component set.
Also - no changes of the {v} value of the signature will be altered unless the final two chars is either exactly 00 or 01.
Use a fix such as this if the web3 library you use for signing doesn't update the last byte on its own – for example if using web3.eth.personal.sign
.
Add the following code block or something similar to where the signature value first is relayed into the web app.
Test that it works and deploy the fix.
Great! All Ledger users are now happy – and can use your app.
Find the full block including the comments on the GitHub Gist.Here also as a condensed statement without the comments for any quick copy-paste.
if (typeof signature === "string" &&
/(^0[xX]|^)[0-9a-fA-F]{128}(00|01)$/.test(signature)) {
const sigV = (parseInt(signature.slice(-2), 16) + 27).toString(16);
signature = signature.slice(0, -2) + sigV;
}
Applying the if statement above within the callback argument for web3.eth.personal.sign
, where we'll receive the signature from Ledger -> MetaMask.
The last byte of the signature will be corrected, so that any upstream validation can correctly validate the signature / address.
Many other tools used for signatures already fixes these kinds of issues in signatures. There’s also libs that fixes signatures before exposing it to function callbacks, although some popular JS libs are still lacking, which makes the issue more widespread.
The cast
CLI tool from Foundry also outputs correct signatures, which can be used to compare or test message signatures.
Ledger connections through Wallet Connect, etc. seems to relay valid signatures, which on some web apps can work as a temporary workaround until their devs work it out.
In conclusion, the compatibility issue between Ledger devices and MetaMask extension, specifically with vrs signatures, can lead to a less than ideal user experience for a subset of users. This article sheds light on the nature of the problem, its impact on user experience, and provides a practical solution to address it. Developers should be mindful of the common symptoms associated with this issue and ensure their dApps can handle such situations gracefully.
Despite the issue highlighted in this article, Ledger devices remain a valuable tool for users seeking additional security and convenience. By implementing the proposed fix, developers can ensure that their dApps provide a seamless experience for Ledger users, further promoting the adoption of secure hardware wallets in the web3 space.
GitHub: https://github.com/kalaspuff
Twitter: https://twitter.com/carloscaraaro
Keybase: https://keybase.io/carloscar
ETH: coa.eth (0x39be...a5a9)