Security vulnerability: nacl.sign.detached accepts invalid keys
See original GitHub issueSee https://github.com/MystenLabs/ed25519-unsafe-libs
Proof: nacl.sign.detached(new Uint8Array(1), new Uint8Array(64))
Solution: validate the right 32 bytes of 64-byte array in detached
People are using the method in the wild: https://github.com/search?l=JavaScript&q=nacl.sign.detached&type=Code
Issue Analytics
- State:
- Created a year ago
- Comments:8 (6 by maintainers)
Top Results From Across the Web
tweetnacl-js/README.md at master - GitHub
Signs the message using the secret key and returns a signature. nacl.sign.detached.verify(message, signature, publicKey). Verifies the signature for the message ...
Read more >How to use the tweetnacl.sign function in tweetnacl | Snyk
To help you get started, we've selected a few tweetnacl.sign examples, based on popular ways it is used in public projects.
Read more >AWS Foundational Security Best Practices controls
View details about the available controls for the AWS Foundational Security Best Practices standard.
Read more >Modern PHP data Encryption/Decryption with Sodium ...
An in-depth guide on public-key and secret-key cryptography with Sodium extension in PHP.
Read more >node-forge - npm
509 certificate support, ED25519 key generation and signing/verifying, and RSA public and private key encoding, decoding, encryption/decryption, ...
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Let’s try to clearly define the problem.
Signing
nacl.sign.keyPair
generates a key pair consisting of a 32-byte public key and a 64-byte secret key. Example:nacl.sign
(andnacl.sign.detached
) sign a message with a 64-byte secret key.nacl.sign
or provided separately in the case ofnacl.sign.detached
) can be verified with the corresponding 32-byte public key.Claim
Internally, the 64-byte secret key actually embeds both 32-byte private and 32-byte public key. Due to a quirk of ed25519 construction, if a different public key part is used for signing another message with the same 32-byte private part, it is possible to reveal the original 32-byte private part.
Attacks
1. Exploiting design vulnerability
The programmer may mistakely use a different 32-byte public key part of the secret key along with the correct 32-byte secret part. If an attacker can provide a different public key part for signing, and the user signs a message with such secret key, the attacker can learn the secret key.
2. Malicious corruption
An attacker gets a write-only access to the system that stores the secret key and modifies the public key part of it. The next time the user signs a message with such secret key, the attacker can learn the secret key.
3. Accidental corruption
Cosmic rays or bugs in software modify the public key part, but not the secret key part, thus allowing anyone know has different messages signed under different public key parts to learn the secret key.
What is a secret key?
According to Wikipedia, “A key in cryptography is a piece of information, … when processed through a cryptographic algorithm, can encode or decode cryptographic data. Based on the used method, the key can be different sizes and varieties, but in all cases, the strength of the encryption relies on the security of the key being maintained” (emphasis mine).
Indeed, if we suppose that an attacker (be it an actual attacker or a cosmic ray) can modify the secret key, to various degrees, most of the cryptosystems are not secure.
How attacks affect NaCl/TweetNaCl/TweetNaCl.js
1. Exploiting design vulnerability
NaCl uses 64-byte secret keys. Its API does not allow providing the public key part of it separately for the signing operation. Some other libraries, as listed in https://github.com/MystenLabs/ed25519-unsafe-libs (earlier thread: https://github.com/jedisct1/libsodium/issues/170), allow providing the two 32-byte parts separately, making it more likely that the programmer creates a design vulnerability.
No such mistake can happen with NaCl unless the programmer knowingly and deliberately modifies the secret key. Thus, the attack 1 is not valid for NaCl/TweetNaCl/TweetNaCl.js.
2. Malicious corruption
…where we have an attacker that cannot read the secret key, but can modify it.
If we consider this a valid attack, then the attack is valid even without the design quirk of ed25519. Similar to the timing attack, the attacker can modify the 32-byte secret part of the key, zeroing out some bytes, and leaving some of the original bytes, receiving signed messages, bruteforcing the non-zeroed secret key bytes by signing the same message with guesses, and repeating until learning the whole secret key.
If this is a valid attack, it is also a valid attack on AES.
Here’s an example of the attack on AES, where an attacker can modify the secret key and sees two encrypted messages:
Again, this is a version of the common timing attacks, and can be scaled for any capability of the attacker down to bit-by-bit corruption - this would require up to 128 messages to reveal the whole key with a capability of bruteforcing only 2^1 operations.
Referring back to the definition of the secret key, we don’t consider attacks that compromise the security of the secret key valid. Thus, either the attack 2 is not valid or it is valid for all cryptosystems.
3. Accidental corruption
Same argument as for 2 — the cryptosystem relies of the security of the secret key. Additionally, if we consider accidental corruption of the secret key a valid attack, we should not only consider it valid for inputs/outputs, but also for the internal workings of the algorithm — the corruption can happen during any stage of it.
I realize that they are various degrees to corruption. Is it more likely to happen to the permanent storage than memory? Maybe. It’s definitely more damaging to the secret key of ed25519 due to the mentioned quirk.
The attack is outside the scope of TweetNaCl.js. The library provides functions to sign messages with the secret key. It doesn’t provide a way to safely store the secret key so that it cannot be corrupted.
Defenses
1. Exploiting design vulnerability
The library already doesn’t allow providing separate parts of the secret key. The secret key is a 64-byte Uint8Array.
2. Malicious corruption
Potential defence: when provided with a secret key, validate that the public key embedded in it is actually the key that was derived from the secret material (or just re-derive for every signing.) Or verify the signature after signing (caveat).
This defense doesn’t work against this attack. See the attack in which the attacker just modifies parts of the 32-byte secret part.
3. Accidental corruption
Same potential defense as for 2: we can detect if a part of the second 32-byte part of the secret key was corrupted. Only applies partially — doesn’t (and cannot) fix accidental corruption elsewhere.
Conclusions
TweetNaCl.js provides a way to sign messages with a 64-byte secret key. Handling of the secret key (specifically, making sure it is stored securely, not modified, and not corrupted) is a responsibility of the programmer when designing and coding a cryptographic protocol and is out of the scope for this library.
If we consider the above attacks valid, they apply to all cryptosystems and all cryptographic libraries, to a degree. In fact, all of the above also applies to secret keys and nonces in
nacl.box
andnacl.secretbox
(e.g. an attacker modifying a nonce so that it repeats) and is not unique to ed25519.Comment
In general, NaCl/TweetNaCl/TweetNaCl.js have an API that is pretty robust against misuse — they don’t allow many of the common pitfalls. However, they still depend on the correct handling of nonces and secret keys.
Some libraries, such as Google’s Tink are more high-level and better in this regard: no worry about nonces and some higher-level key management.
Am I missing something else?
“Mr. Babbage, if you put into the machine wrong figures, will the right answers come out?”
Indeed, a lot of effort in computing goes into protecting against corruption: ECC memory, rad-hardened CPUs, storage checksums, network packet checksums, database page checksums, authenticated encryption, memory-safe languages. Should we put a (literally) half-working solution to protect against a minuscule chance of accidental corruption at the cost of halving the performance by default? I don’t think so. In fact, if our goal is to protect against accidental corruption, adding a simple checksum byte at the end of the secret key is a much better solution: it’s faster and more reliable.