Merge pull request #570 from LedgerHQ/python_bip322
What changed, and why it matters
This commit adds support in the Ledger Bitcoin app's Python client for a new PSBT global field called PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE, defined by Bitcoin improvement proposal BIP-322. It lets a PSBT carry a signed message alongside transaction data. The change is a feature addition to serialization/deserialization code, with tests that check the field survives round-trips and that malformed inputs (duplicate keys, extra key data) are rejected. There is no indication in the commit that this fixes a security bug or is being released as a security patch.
No immediate security action required. Treat as a normal feature update. If using the Python client to parse untrusted PSBTs, ensure the broader BIP-322 signing flow is reviewed for message-signing security, but that is outside the scope of this commit.
Security signals we found
New PSBT field parsing added with duplicate-key and key-length validation
No buffer overflow or memory-safety issue evident in Python client code
No mention of vulnerability, CVE, security fix, or attacker scenario in commit or changelog
Change is limited to Python client library; no device firmware code modified
Evidence from the diff
The patch extends the bitcoin_client Python library’s PSBT class to recognize and handle key type 0x09 (PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE) per BIP-322. It adds a class constant, an instance attribute, deserialization logic that rejects duplicate keys and keys with extra data, and serialization logic that emits the field when set. Unit tests cover round-tripping in PSBT v0 and v2, absence of the field, and error cases. The CHANGELOG describes this as an ‘Added’ item, not a fix.
Changed components
bitcoin_client/ledger_bitcoin/psbt.pybitcoin_client/tests/test_psbt_generic_signed_message.pybitcoin_client/CHANGELOG.mdInspect captured patch +83 / −2
### bitcoin_client/CHANGELOG.md
@@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Dates are in `dd-mm-yyyy` format.
-## [0.4.2] - 18-09-2026
+## [0.4.2] - 29-09-2026
+
+### Added
+
+- `PSBT.generic_signed_message`: support for the `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` global field (0x09) of [BIP-0322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki), in both PSBT versions.
### Fixed
### bitcoin_client/ledger_bitcoin/psbt.py
@@ -772,6 +772,7 @@ class PSBT(object):
PSBT_GLOBAL_INPUT_COUNT = 0x04
PSBT_GLOBAL_OUTPUT_COUNT = 0x05
PSBT_GLOBAL_TX_MODIFIABLE = 0x06
+ PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = 0x09
PSBT_GLOBAL_VERSION = 0xFB
def __init__(self, tx: Optional[CTransaction] = None) -> None:
@@ -789,6 +790,8 @@ def __init__(self, tx: Optional[CTransaction] = None) -> None:
self.tx_version: Optional[int] = None
self.fallback_locktime: Optional[int] = None
self.tx_modifiable: Optional[int] = None
+ # BIP-322: the message of a generic signed message request (allowed in any PSBT version)
+ self.generic_signed_message: Optional[bytes] = None
# Assume version 0 PSBT
self.version = 0
@@ -888,6 +891,12 @@ def deserialize(self, psbt: str) -> None:
if len(v) != 1:
raise PSBTSerializationError("Global tx modifiable flags is not 1 bytes")
self.tx_modifiable = struct.unpack("<B", v)[0]
+ elif key_type == PSBT.PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE:
+ if key in key_lookup:
+ raise PSBTSerializationError("Duplicate key, global generic signed message is already provided")
+ elif len(key) > 1:
+ raise PSBTSerializationError("Global generic signed message key is more than one byte type")
+ self.generic_signed_message = deser_string(f)
elif key_type == PSBT.PSBT_GLOBAL_VERSION:
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, global PSBT version is already provided")
@@ -1018,6 +1027,10 @@ def serialize(self) -> str:
r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_TX_MODIFIABLE))
r += ser_string(struct.pack("<B", self.tx_modifiable))
+ if self.generic_signed_message is not None:
+ r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE))
+ r += ser_string(self.generic_signed_message)
+
if self.version > 0 or self.explicit_version:
r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_VERSION))
r += ser_string(struct.pack("<I", self.version))
### bitcoin_client/tests/README.md
@@ -1 +1 @@
-These tests verify the compatibility layer of the bitcoin_client when ran against the legacy app (v1.6.5) using the LegacyClient.
+These tests verify the compatibility layer of the bitcoin_client when ran against the legacy app (v1.6.5) using the LegacyClient, plus some pure-Python unit tests.
### bitcoin_client/tests/test_psbt_generic_signed_message.py
@@ -0,0 +1,64 @@
+import pytest
+
+from bitcoin_client.ledger_bitcoin.errors import PSBTSerializationError
+from bitcoin_client.ledger_bitcoin.psbt import PSBT, PartiallySignedInput, PartiallySignedOutput
+from bitcoin_client.ledger_bitcoin.tx import CTransaction, CTxIn, CTxOut, COutPoint
+
+
+def make_psbt(message=None) -> PSBT:
+ """A PSBTv0 with one input and one bare OP_RETURN output, like a BIP-322 to_sign."""
+ tx = CTransaction()
+ tx.nVersion = 0
+ tx.vin = [CTxIn(COutPoint(1, 0), b"", 0)]
+ tx.vout = [CTxOut(0, b"\x6a")]
+
+ psbt = PSBT(tx)
+ psbt.inputs = [PartiallySignedInput(0)]
+ psbt.outputs = [PartiallySignedOutput(0)]
+ psbt.generic_signed_message = message
+ return psbt
+
+
+def roundtrip(psbt: PSBT) -> PSBT:
+ result = PSBT()
+ result.deserialize(psbt.serialize())
+ return result
+
+
+@pytest.mark.parametrize("message", [b"Hello World", b""])
+def test_generic_signed_message_roundtrip(message: bytes):
+ """PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (BIP-322) is kept in both PSBT versions."""
+ psbt = make_psbt(message)
+
+ psbt_v0 = roundtrip(psbt)
+ assert psbt_v0.version == 0
+ assert psbt_v0.generic_signed_message == message
+ assert psbt_v0.unknown == {}
+
+ psbt_v0.convert_to_v2()
+ psbt_v2 = roundtrip(psbt_v0)
+ assert psbt_v2.version == 2
+ assert psbt_v2.generic_signed_message == message
+
+ psbt_v2.convert_to_v0()
+ assert roundtrip(psbt_v2).generic_signed_message == message
+
+
+def test_generic_signed_message_absent():
+ psbt = roundtrip(make_psbt())
+ assert psbt.generic_signed_message is None
+
+
+def test_generic_signed_message_key_with_keydata():
+ # the field has no keydata
+ psbt = make_psbt()
+ psbt.unknown[b"\x09\x00"] = b"Hello World"
+ with pytest.raises(PSBTSerializationError):
+ roundtrip(psbt)
+
+
+def test_generic_signed_message_duplicate_key():
+ psbt = make_psbt(b"Hello World")
+ psbt.unknown[b"\x09"] = b"x"
+ with pytest.raises(PSBTSerializationError):
+ roundtrip(psbt)Why this scored 21/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.