Warm Champagne Manatee
High
A malicious btc delegator can DOS covenant members via a specially crafted BTC delegation transaction due to a reliably triggerable panic
in BIP340PubKey
parsing.
BTCSlashingTx.ParseEncVerifyAdaptorSignatures()
uses BIP340PubKey.MustToBTCPK()
which will panic if any errors are returned from schnorr.ParsePubKey()
. Since this function and its downstream calls bubble up errors from calls to the btcec
and secp256k1
packages, there are multiple ways to format the delegation target public key (a hex string with a 32 byte length that the attacker controls) that will trigger the panic. An example Public Key that would bring down the covenant committee members' babylon nodes is 0x3132303132303930303030313230303130303030313130323030324230303031 since the x coordinate of this key it is just outside of the secp256k1 curve.
An attacker needs to make a BTCDelegation
with at least one of the FpBtcPkList
(delegation targets' public keys) being a public key that will trigger any of the errors downstream of the BIP340PubKey.MustToBTCPK()
calls. They will be able to undelegate the BTC later on so the cost of the attack is having BTC stuck in delegation for some time.
None.
An attacker makes a BTCDelegation
delegating to an invalid pubkey (eg. 0x3132303132303930303030313230303130303030313130323030324230303031) as an entry into the FpBtcPkList
.
All nodes running as covenant members have a loop waiting for delegation transactions via CovenantEmulator.covenantSigSubmissionLoop()
. This loop appends pending delegations into batches and eventually calls CovenantEmulator.AddCovenantSignatures()
on the batch.
After various checks on the delegation objects, none of which will catch public key coordinate issues (eg. outside of the prime field, outside of the secp256k1 curve, etc.), the covenant emulator will request that their babylon nodes sign the transaction. It will call SubmitCovenantSigs() which will generate and submit a btcstakingtypes.MsgAddCovenantSigs
message to the applicable grpc message handler in the babylon node service.
This message will be processed by msgServer.AddCovenantSigs()
which will eventually call BTCSlashingTx.ParseEncVerifyAdaptorSignatures
with the list of target public keys to delegate to. A loop will iterate through the public keys calling bbn.BIP340PubKey.MustToBTCPK()
on each key until the malicious pubkey payload triggers a panic that will bring down the node entirely (eg. panic: invalid public key: x coordinate 3137303230303132303041303730303030303230303130303031303130303039 is not on the secp256k1 curve
).
All nodes running as covenant members will panic
and shut down. If any of the the nodes are configured as a system service or in some other way that will restart the node, then they will panic again in a loop each time they attempt to process and sign the malicious delegation message.
This will DOS the covenant committee for the duration of time it takes for the issue to be discovered, the root cause triaged, a work around discovered and the issue patched, a new release cut and announced, a quorum threshold of covenant committee members updated, the quorum resynced and processed all transactions to the current head. It is unreasonable to expect that this process could be reliably done in under 24 hours, therefore I am assigning this issue as high
severity per the scoping impact guidelines for high severity issues: "Inability for a non-malicious covenant to submit signatures for valid staking registrations for more than 24h."
I tested this locally with a modified node that allowed me to fuzz babylon's various grpc methods but this setup would be very time consuming to upload and make easily useable for others. It would also not fit here.
It is also possible to modify a babylon node such that you tell it to process a malicious delegation message but doing so requires setting up various services (covenant, babylon, lightclient, etc.) since the data is passed between various go routines via multiple go channels and grpc messages. This would also be very time consuming and would not fit in the submission upload space.
It is possible to follow the code path from the beginning of btc delegation message parsing to verify that code identified in the root cause is the first place that the delegation targets' pub keys are processed in a way that would catch their x coordinate issues. Therefore I am providing a POC showing that a valid 32 byte buffer for the public key (which is entirely controlled by the generator of the btc delegation message) can trigger one of the panic's in question.
package main
import (
"fmt"
bbn "github.com/babylonlabs-io/babylon/types"
)
func main() {
// create a public key where the x coordinate is not on the secp256k1 curve
// eg. 0x3132303132303930303030313230303130303030313130323030324230303031
d1 := []byte{49, 50, 48, 49, 50, 48, 57, 48, 48, 48, 48, 49, 50, 48, 48, 49, 48, 48, 48, 48, 49, 49, 48, 50, 48, 48, 50, 66, 48, 48, 48, 49}
pk, err := bbn.NewBIP340PubKey(d1)
// note no error
if err != nil {
fmt.Println(err)
return
}
// vulnerable code panic
// this is the same function that is called here:
// https://github.com/babylonlabs-io/babylon/blob/04d2861834bcf886eba08269631284ccb1c4e1d9/x/btcstaking/types/btc_slashing_tx.go#L210
pk.MustToBTCPK()
// If the program makes it here then it is still running
// if not then the node is DOS'd
fmt.Println("Program still running, everything is okay")
}
Example run:
user@shredder:~/audits/babylon/poc$ go run poc.go
panic: invalid public key: x coordinate 3132303132303930303030313230303130303030313130323030324230303031 is not on the secp256k1 curve
goroutine 1 [running]:
github.com/babylonlabs-io/babylon/types.BIP340PubKey.MustToBTCPK(...)
/home/user/go/pkg/mod/github.com/babylonlabs-io/[email protected]/types/btc_schnorr_pk.go:43
main.main()
/home/user/audits/babylon/poc/poc.go:24 +0x114
exit status 2
Handle panics gracefully with recover()
at the service level, although the tainted delegation would prevent all batched transactions from processing until a fix was made in this case. Do not use "must" (eg. panic on error) style helper routines on externally controlled buffers (or even outside of tests to be safe). Change this instance of MustToBTCPK()
to a function that returns errors instead of panicing with them.