fix: reject PSBT inputs with non-standard sighash types before signing (#844)
What changed, and why it matters
Krux is a small, open-source Bitcoin signing device (hardware wallet). This commit fixes a security flaw where the device would sign transactions even if the sender asked it to use unusual Bitcoin signature modes—specifically SIGHASH_NONE, SIGHASH_SINGLE, or ANYONECANPAY. Those modes can let someone else change where the money goes after the device has already signed, which could be abused to steal funds. The fix makes the device refuse to sign any PSBT (the file format used to pass a transaction around) that contains those non-standard modes. The project labels this as a security fix for an external audit finding.
Users should upgrade to a firmware release containing this commit and avoid signing PSBTs produced by untrusted software until patched. Developers integrating Krux should ensure any custom PSBT workflows only request SIGHASH_ALL or SIGHASH_DEFAULT.
Security signals we found
Explicit security fix for externally-reported audit finding (#843 C2)
Pre-sign validation added to reject non-standard sighash types
Relevant to transaction-replacement / fund-redirect attacks via SIGHASH_NONE/SINGLE/ANYONECANPAY
Changelog categorized as "Security Fixes"
Unit tests added for both rejection and acceptance cases
Evidence from the diff
The patch adds a new PSBTSigner.check_sighash() method in src/krux/psbt.py that iterates over all PSBT inputs and rejects any input whose sighash_type is not in the safe set {None, SIGHASH.DEFAULT, SIGHASH.ALL}. It is called immediately before add_signatures() invokes psbt.sign_with(). The commit also adds unit tests covering SIGHASH_NONE (0x02), SIGHASH_SINGLE (0x03), SIGHASH.ALL|ANYONECANPAY (0x81), plus acceptance tests for DEFAULT, ALL, and unset (None), and a test verifying the reported input index. The changelog explicitly lists this under “Security Fixes.”
Changed components
src/krux/psbt.py - PSBTSigner.add_signatures() and new check_sighash()Krux firmware PSBT signing flowCHANGELOG.mdInspect captured patch +115 / −0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 004cf71..a52da09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# Changelog 26.03.1 - March 2025
+
+### Security Fixes
+Reject PSBT inputs with non-standard sighash types before signing
+
# Changelog 26.03.0 - March 2025
### New Device Support: Embed Fire
diff --git a/src/krux/psbt.py b/src/krux/psbt.py
index df9566c..7dbf6f1 100644
--- a/src/krux/psbt.py
+++ b/src/krux/psbt.py
@@ -431,8 +431,26 @@ class PSBTSigner:
return messages, fee_percent
+ def check_sighash(self):
+ """Check that all inputs use SIGHASH_ALL (or DEFAULT for taproot).
+
+ Refuse to sign if any input requests a non-standard sighash type
+ (SIGHASH_NONE, SIGHASH_SINGLE, ANYONECANPAY), as these can allow
+ an attacker to redirect funds after signing.
+ """
+ from embit.transaction import SIGHASH
+
+ safe_sighash = {None, SIGHASH.DEFAULT, SIGHASH.ALL}
+ for i, inp in enumerate(self.psbt.inputs):
+ if inp.sighash_type not in safe_sighash:
+ sighash_val = inp.sighash_type
+ raise ValueError(
+ "Input %d has non-standard sighash type: 0x%02x" % (i, sighash_val)
+ )
+
def add_signatures(self):
"""Add signatures to PSBT"""
+ self.check_sighash()
sigs_added = self.psbt.sign_with(self.wallet.key.root)
if sigs_added == 0:
raise ValueError("cannot sign")
diff --git a/tests/test_psbt.py b/tests/test_psbt.py
index 6252154..56738f8 100644
--- a/tests/test_psbt.py
+++ b/tests/test_psbt.py
@@ -1522,6 +1522,98 @@ def test_sign_fails_with_0_sigs_added(mocker, m5stickv, tdata):
signer.psbt.sign_with.assert_called_with(wallet.key.root)
+def test_check_sighash_rejects_sighash_none(mocker, m5stickv, tdata):
+ from embit.networks import NETWORKS
+ from embit.transaction import SIGHASH
+ from krux.psbt import PSBTSigner
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.qr import FORMAT_NONE
+
+ wallet = Wallet(Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG, NETWORKS["test"]))
+ signer = PSBTSigner(wallet, tdata.P2WPKH_PSBT, FORMAT_NONE)
+ # Inject SIGHASH_NONE into the first input
+ signer.psbt.inputs[0].sighash_type = SIGHASH.NONE
+
+ with pytest.raises(ValueError, match="non-standard sighash type: 0x02"):
+ signer.sign()
+
+
+def test_check_sighash_rejects_sighash_single(mocker, m5stickv, tdata):
+ from embit.networks import NETWORKS
+ from embit.transaction import SIGHASH
+ from krux.psbt import PSBTSigner
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.qr import FORMAT_NONE
+
+ wallet = Wallet(Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG, NETWORKS["test"]))
+ signer = PSBTSigner(wallet, tdata.P2WPKH_PSBT, FORMAT_NONE)
+ signer.psbt.inputs[0].sighash_type = SIGHASH.SINGLE
+
+ with pytest.raises(ValueError, match="non-standard sighash type: 0x03"):
+ signer.sign()
+
+
+def test_check_sighash_rejects_anyonecanpay(mocker, m5stickv, tdata):
+ from embit.networks import NETWORKS
+ from embit.transaction import SIGHASH
+ from krux.psbt import PSBTSigner
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.qr import FORMAT_NONE
+
+ wallet = Wallet(Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG, NETWORKS["test"]))
+ signer = PSBTSigner(wallet, tdata.P2WPKH_PSBT, FORMAT_NONE)
+ signer.psbt.inputs[0].sighash_type = SIGHASH.ALL | SIGHASH.ANYONECANPAY
+
+ with pytest.raises(ValueError, match="non-standard sighash type: 0x81"):
+ signer.sign()
+
+
+def test_check_sighash_allows_default_and_all(mocker, m5stickv, tdata):
+ from embit.networks import NETWORKS
+ from embit.transaction import SIGHASH
+ from krux.psbt import PSBTSigner
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.qr import FORMAT_NONE
+
+ wallet = Wallet(Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG, NETWORKS["test"]))
+
+ # SIGHASH_ALL should be accepted (signs successfully)
+ signer = PSBTSigner(wallet, tdata.P2WPKH_PSBT, FORMAT_NONE)
+ signer.psbt.inputs[0].sighash_type = SIGHASH.ALL
+ signer.sign() # Should not raise
+
+ # SIGHASH_DEFAULT should be accepted
+ signer = PSBTSigner(wallet, tdata.P2WPKH_PSBT, FORMAT_NONE)
+ signer.psbt.inputs[0].sighash_type = SIGHASH.DEFAULT
+ signer.sign() # Should not raise
+
+ # None (unset) should be accepted
+ signer = PSBTSigner(wallet, tdata.P2WPKH_PSBT, FORMAT_NONE)
+ signer.psbt.inputs[0].sighash_type = None
+ signer.sign() # Should not raise
+
+
+def test_check_sighash_reports_correct_input_index(mocker, m5stickv, tdata):
+ from embit.networks import NETWORKS
+ from embit.transaction import SIGHASH
+ from krux.psbt import PSBTSigner
+ from krux.key import Key, TYPE_SINGLESIG
+ from krux.wallet import Wallet
+ from krux.qr import FORMAT_NONE
+
+ wallet = Wallet(Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG, NETWORKS["test"]))
+ signer = PSBTSigner(wallet, tdata.P2PKH_PSBT, FORMAT_NONE)
+ # P2PKH_PSBT has 3 inputs; set non-standard sighash on the third one
+ if len(signer.psbt.inputs) >= 3:
+ signer.psbt.inputs[2].sighash_type = SIGHASH.NONE
+ with pytest.raises(ValueError, match="Input 2"):
+ signer.sign()
+
+
def test_outputs_singlesig(mocker, m5stickv, tdata):
from embit.networks import NETWORKS
from krux.psbt import PSBTSigner
Why this scored 78/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.