Validating Ledger signatures relayed via MetaMask
Carl Oscar Aaro
0x39bE
April 15th, 2023

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.


Introduction

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.

Common Symptoms and Considerations for Developers

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

There’s lots of love for Ledger – a short note on Ledger devices in general

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. 🌟🙏

Exploring signatures – in particular “vrs signatures”

"Ledger devices produces vrs signatures with a canonical {v} value of 0 or 1."

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).

A vrs signature is 65 bytes long — {r} and {s} are each 32 bytes, followed by 1 byte for the {v} recovery id.
A vrs signature is 65 bytes long — {r} and {s} are each 32 bytes, followed by 1 byte for the {v} recovery id.

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.

https://github.com/ethereum/go-ethereum/issues/19751#issuecomment-504900739
https://github.com/ethereum/go-ethereum/issues/19751#issuecomment-504900739

What different {v} values in the signature means for validation

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.

Signing on a Ledger + relay to MetaMask extension

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.

Example – OpenZeppelin's ECDSA implementation

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.
 */

Modification of last byte in vrs signatures if 0x00 or 0x01

Safety checks before modifying signature

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.

👉 THE FIX → a few rows of JS to repair signatures

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.

  1. Add the following code block or something similar to where the signature value first is relayed into the web app.

  2. Test that it works and deploy the fix.

  3. Great! All Ledger users are now happy – and can use your app.

https://gist.github.com/kalaspuff/5b1512b9bc74bea87c4920a447d895ef
https://gist.github.com/kalaspuff/5b1512b9bc74bea87c4920a447d895ef

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; 
}

Testing with a connection to a web wallet provider

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.

Note how the last byte of the logged signature value is now changed from 0x00 to 0x1b.
Note how the last byte of the logged signature value is now changed from 0x00 to 0x1b.

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.

Summary

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)

The Fabric of Space by coa.eth – https://foundation.app/@carloscar/imagine-dreams/2
The Fabric of Space by coa.eth – https://foundation.app/@carloscar/imagine-dreams/2
Subscribe to Carl Oscar Aaro
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
Arweave Transaction
34o4Zyyv1TbxfY-…kNQ1wCF4iPQj7Qg
Author Address
0x39bEb60bc4c1b8b…c7A56e7DfB3a5A9
Content Digest
mvPbLPXvy375CXi…SKYUBHDx_R1TIgU
More from Carl Oscar Aaro
View All

Skeleton

Skeleton

Skeleton

0 Collectors