Ledger Security Bulletin 008

27 April 2020: Monero private key retrieval.

Summary

The 7th Ledger Security Bulletin describes a replay vulnerability in the Monero app leading to the retrieval of a private spend key. Another vulnerability leading to the same private key retrieval was found by Dušan Klinec.

The MITRE assigned organization assigned the CVE-2020-6861 and Dušan Klinec wrote a blogpost explaining the technical details of this vulnerability.

Observations and Notations

During a transaction, some computational elements are encrypted by the Nano with a key only known to the Monero application, and sent to the desktop client for later use, due to space limitations on the Nano. Let $enc(x)$ be the encryption of $x$.

Such elements are associated with an HMAC with a key that is dedicated to the current transaction. Let $henc(x)$ be a pair $(enc(x), hmac(enc(x)))$.

Two specific values 00...00h and FF...FFh are used to respectively identify the view key and the spend key. Those two values are named C_FAKE_SEC_VIEW_KEY and C_FAKE_SEC_SPEND_KEY.

In the general case, the secret view key $a$ is public and exported from the device upon request to the user. $henc(a)$ and $henc(b)$ are also known, as those are returned by the device when a transaction is started with the monero_apdu_open_tx command.

Finally, with the notations:

  • $P$: point
  • $x$, $y$, $alpha$, $ss$, $c$: scalar
  • $a$: scalar, private view key
  • $b$: scalar, private spend key
  • $l$: scalar, curve order

the subsequent two attacks rely on the following commands:

  • monero_apdu_sc_sub: $henc(x), henc(y) \rightarrow henc(x-y)$
  • monero_apdu_generate_key_derivation: $P, henc(x) \rightarrow henc(8x.P)$
  • monero_apdu_derive_secret_key: $henc(P), index, \text{C_FAKE_SEC_SPEND_KEY} \rightarrow henc(\text{Keccak}(P \mathbin\Vert index)+b)$
  • monero_apdu_mlsag_sign: $henc(alpha), henc(x) \rightarrow ss = (alpha - c * x) \mod l$

Attack Details

Method 1

This method consists of first building a full encryption oracle and then use it to retrieve the secret spend key $b$.

Step 1: take control of zero

First, monero_apdu_sc_sub is called with $henc(x)$ for both arguments and returns:

$\text{monero_apdu_sc_sub}(henc(a), henc(a)) = henc(0)$

A valid pair {$0$, $henc(0)$} can be injected in any command. Having the control of this zero value is important since it allows canceling parts of any other command’s intermediate computation.

Step 2: decryption oracle

Controlling the zero value, a decryption oracle can be built using the monero_apdu_mlsag_sign command. The command can be called in the following way or any $henc(x)$ to retrieve $x$:

\[\text{monero_apdu_mlsag_sign}(henc(x), henc(0)) = x-c * 0 = x\]

At this point, a decryption oracle is available.

Step 3 : Retrieving the spend key

The final step involves monero_apdu_derive_secret_key. Let $fsk$ be the fake spend key C_FAKE_SEC_SPEND_KEY:

$\text{monero_apdu_derive_secret_key}(henc(0), 0, fsk)$

\[= \text{Keccak}(0 \mathbin\Vert 0) + b = hmac(sk)\]

$sk$ being the spend key.

So using the decryption oracle from step 2, $sk$ can be decrypted to get:

$b = sk - \text{Keccak}(0 \mathbin\Vert b0)$.

where b is the private spend key.

Method 2

Removing monero_apdu_sc_sub (and monero_apdu_sc_add) from the protocol prevents the previous attack, but a more complicated one is still possible.

Step 1

Let’s assume that the private view key $a$ is already known, meaning that the user has accepted to export it.

Without the need of the device, a scalar $x$ can be found such that:

\(P = (8 x.a).G\) \(= (8x).a.G\) \(= 8x.A\)

and:

  • encoded_P = encode_point(P)
  • encoded_P = encode_scalar(decode_scalar(P))

Which means that P can be considered both as a valid input point and a valid scalar with no reduction modulo the order of the curve.

Step 2

Deriving the generated point $P$ at step 1:

$\text{monero_apdu_generate_key_derivation}(P=x.G, henc(a))$

The app computes:

  • $a \leftarrow henc(a)$
  • $8a.x.G = (8x).a.G = 8x.A$

which equals $P$ by definition above. The output is then $henc(P)$, hence we learn a valid set ${P, enc(P), hmac(P)}$.

Step 3

monero_apdu_derive_secret_key can be called with the following parameter:

$\text{monero_apdu_derive_secret_key}(henc(P), 0, henc(b))$

\[sk = \text{Keccak}(P v0) + b = henc(sk)\]

Step 4 : spend key extraction

Finally, monero_apdu_mlsag_sign allows to retrieve the spend key based on the value obtained at step 2 and 3, and by using $P$ as a simple 256 bits scalar:

$\text{monero_apdu_mlsag_sign}(henc(sk), henc(P))$

\[ss = sk-c * P\]

P is considered a simple scalar in the above.

Now, recall that:

\[sk = \text{Keccak}(P \mathbin\Vert 0) + b\]

and P is known.

So replacing $sk$ in the previous computation results in:

\[ss = \text{Keccak}(P \mathbin\Vert 0) + b - c \times P\]

and thus:

\(b = ss - \text{Keccak}(P \mathbin\Vert 0) + c \times P\).

So again, the spend key $b$ is retrieved.

Impact

Those vulnerabilities allow extracting a user’s Monero private spend key through a malicious Monero client. Being a flaw in the protocol between the Nano application and the Monero desktop client, it affects both Nano S and Nano X users.

Remediation

The vulnerability is fixed from Monero version 1.5.1. The specification update can be found on GitHub and the following changes were made:

  • The monero_apdu_secret_sub has been removed from the application.
  • A strict state machine check has been added, to avoid using a command outside of its expected operating context.
  • The computation of HMAC was changed to embed a type tag, avoiding type confusion. Some specific HMACS also embed a state machine tag.
  • A better user interaction has been set up to request the user to confirm any transaction start.

Credits

We would like to thank the security researcher Dušan Klinec for the discovery of these vulnerabilities, his high quality report and also for his help.

References

  1. LSB 7 - Monero private key retrieval
  2. Ledger Monero App Spend key Extraction
  3. Monero Application Commands
  4. CVE-2020-6861