Skip to content

Commit e474ac8

Browse files
authored
Merge pull request #94 from LedgerHQ/slip21-stableprotobuf
Add optional paths_slip21 field to manifest, and (de)serialization (stable_protobuf_version branch)
2 parents 73535f5 + 0f963a2 commit e474ac8

4 files changed

Lines changed: 92 additions & 7 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ repos:
3333
- types-toml
3434

3535
- repo: https://github.com/PyCQA/bandit
36-
rev: 1.7.4
36+
rev: 1.8.6
3737
hooks:
3838
- id: bandit
3939
args:

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.5.4] - 2025-11-25
9+
10+
### Added
11+
12+
- Support for `path_slip21` in the manifest.
13+
814
## [0.5.3] - 2025-08-27
915

1016
### Added
1117

1218
- Support for Apex P and Apex M devices
1319

14-
1520
## [0.5.2] - 2024-12-09
1621

1722
### Fixed

ledgerwallet/manifest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ def assert_compatible_device(self, device_id: int):
260260
def serialize_derivation_path(self, value):
261261
derivation_paths: Dict[str, Optional[int]] = {
262262
"paths": None,
263+
"paths_slip21": None,
263264
"curve": None,
264265
}
265266
for derivation_entry in value:
@@ -272,9 +273,13 @@ def serialize_derivation_path(self, value):
272273
curves |= params.CURVE_PRIME256R1
273274
elif curve == "ed25519":
274275
curves |= params.CURVE_ED25519
276+
elif curve == "slip21":
277+
curves |= params.CURVE_SLIP21
275278
elif curve == "bls12381g1":
276279
curves |= params.CURVE_BLS12381G1
277280
derivation_paths["curve"] = curves
278281
elif derivation_entry == "paths":
279282
derivation_paths["paths"] = value["paths"]
283+
elif derivation_entry == "paths_slip21":
284+
derivation_paths["paths_slip21"] = value["paths_slip21"]
280285
return derivation_paths

ledgerwallet/params.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,69 @@ def _emitprimitivetype(self, ksy, bitwise):
6666
return "asn1_der_len"
6767

6868

69+
# A byte with value < 0x80
70+
@singleton
71+
class LowByte(Construct):
72+
def _parse(self, stream, context, path):
73+
b = byte2int(stream_read(stream, 1, path))
74+
if b >= 0x80:
75+
raise IntegerError("BIP32 path length prefix must be < 0x80")
76+
return b
77+
78+
def _build(self, obj, stream, context, path):
79+
if not isinstance(obj, int):
80+
raise IntegerError("length must be an integer")
81+
if not (0 <= obj < 0x80):
82+
raise IntegerError("BIP32 path length must be < 0x80")
83+
stream_write(stream, int2byte(obj), 1, path)
84+
return obj
85+
86+
87+
# A byte with value >= 0x80, decoded as (byte - 0x80)
88+
# This is used to encode the length of SLIP-21 paths
89+
@singleton
90+
class Slip21LenByte(Construct):
91+
def _parse(self, stream, context, path):
92+
b = byte2int(stream_read(stream, 1, path))
93+
if b < 0x80:
94+
raise IntegerError("SLIP-21 length prefix must be >= 0x80")
95+
return b - 0x80
96+
97+
def _build(self, obj, stream, context, path):
98+
if not isinstance(obj, int):
99+
raise IntegerError("length must be an integer")
100+
if not (0 <= obj <= 0x7F):
101+
raise IntegerError("SLIP-21 decoded length must be in [0, 0x7F]")
102+
stream_write(stream, int2byte(0x80 + obj), 1, path)
103+
return obj
104+
105+
106+
class Slip21PathAdapter(Adapter):
107+
def _decode(self, obj, context, path):
108+
# obj is now a list/array of bytes (ints 0–255)
109+
if not obj:
110+
return str()
111+
112+
# First byte must be zero prefix
113+
if obj[0] != 0x00:
114+
raise IntegerError("invalid SLIP-21 path prefix")
115+
116+
# Remaining bytes are UTF-8 chars
117+
return bytes(obj[1:]).decode("utf-8")
118+
119+
def _encode(self, obj, context, path):
120+
if not isinstance(obj, str):
121+
raise IntegerError("SLIP-21 path must be a string")
122+
123+
payload = b"\0" + obj.encode("utf-8")
124+
length = len(payload)
125+
if length > 0x7F:
126+
raise IntegerError("SLIP-21 path too long")
127+
128+
# Return list of ints so PrefixedArray(Slip21LenByte, Byte) can handle it
129+
return list(payload)
130+
131+
69132
# noinspection PyAbstractClass
70133
class Bip32PathAdapter(Adapter):
71134
def _decode(self, obj, context, path):
@@ -90,7 +153,8 @@ def _encode(self, obj, context, path):
90153
return out
91154

92155

93-
Bip32Path = Bip32PathAdapter(PrefixedArray(Byte, Int32ub))
156+
Bip32Path = Bip32PathAdapter(PrefixedArray(LowByte, Int32ub))
157+
Slip21Path = Slip21PathAdapter(PrefixedArray(Slip21LenByte, Byte))
94158

95159
PrefixedString = PascalString(Asn1Length, "utf8")
96160

@@ -101,18 +165,27 @@ def _encode(self, obj, context, path):
101165
CURVE_SECP256K1 = 1
102166
CURVE_PRIME256R1 = 2
103167
CURVE_ED25519 = 4
168+
CURVE_SLIP21 = (
169+
8 # not really a curve, but used to indicate the presence of SLIP-21 paths
170+
)
104171
CURVE_BLS12381G1 = 16
105172

106173
Curve = FlagsEnum(
107174
Byte,
108175
secp256k1=CURVE_SECP256K1,
109176
prime256r1=CURVE_PRIME256R1,
110177
ed25519=CURVE_ED25519,
178+
slip21=CURVE_SLIP21,
111179
bls12381g1=CURVE_BLS12381G1,
112180
)
113181

114182
DerivationPath = Prefixed(
115-
Asn1Length, Struct(curve=Curve, paths=Optional(GreedyRange(Bip32Path)))
183+
Asn1Length,
184+
Struct(
185+
curve=Curve,
186+
paths=Optional(GreedyRange(Bip32Path)),
187+
paths_slip21=Optional(GreedyRange(Slip21Path)),
188+
),
116189
)
117190

118191
Dependency = Prefixed(
@@ -163,8 +236,9 @@ def main():
163236
{
164237
"type_": "BOLOS_TAG_DERIVEPATH",
165238
"value": {
166-
"curve": Curve.prime256r1 | Curve.ed25519,
239+
"curve": Curve.prime256r1 | Curve.ed25519 | Curve.slip21,
167240
"paths": ["44'/535348'", "13'", "17'"],
241+
"paths_slip21": ["MYPATH"],
168242
},
169243
},
170244
]
@@ -175,8 +249,9 @@ def main():
175249
b"\x02\x05\x30\x2E\x30\x2E\x34\x03\x29\x01\x00\x00\x00\x00\xFF"
176250
b"\xFF\xFF\x00\x00\x18\xFC\x24\x02\x24\x0A\x24\x1A\x7E\x32\x66"
177251
b"\x62\x6E\x62\x7E\x32\x00\x1A\x40\x0A\x5F\x02\x5F\x02\x40\x02"
178-
b"\x40\xFE\x7F\x00\x00\x04\x14\x06\x02\x80\x00\x00\x2C\x80\x08"
179-
b"\x2B\x34\x01\x80\x00\x00\x0D\x01\x80\x00\x00\x11"
252+
b"\x40\xFE\x7F\x00\x00\x04\x1c\x0E\x02\x80\x00\x00\x2C\x80\x08"
253+
b"\x2B\x34\x01\x80\x00\x00\x0D\x01\x80\x00\x00\x11\x87\x00\x4D"
254+
b"\x59\x50\x41\x54\x48"
180255
)
181256
assert params1 == params2
182257

0 commit comments

Comments
 (0)