ccssh 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccssh/__init__.py +21 -0
- ccssh/__main__.py +5 -0
- ccssh/algs.py +290 -0
- ccssh/async_client.py +419 -0
- ccssh/auth.py +170 -0
- ccssh/certs.py +283 -0
- ccssh/chacha.py +117 -0
- ccssh/channel.py +124 -0
- ccssh/chansock.py +227 -0
- ccssh/cli.py +889 -0
- ccssh/client.py +1274 -0
- ccssh/crypto.py +316 -0
- ccssh/dh_groups.py +92 -0
- ccssh/forward.py +872 -0
- ccssh/gcm.py +87 -0
- ccssh/hostcheck.py +217 -0
- ccssh/hostkeys.py +250 -0
- ccssh/kex.py +222 -0
- ccssh/kex_ext.py +429 -0
- ccssh/mldsa.py +210 -0
- ccssh/mlkem.py +367 -0
- ccssh/mux.py +301 -0
- ccssh/sftp.py +270 -0
- ccssh/shell.py +257 -0
- ccssh/sk.py +224 -0
- ccssh/sntrup.py +325 -0
- ccssh/stub.py +710 -0
- ccssh/transport.py +737 -0
- ccssh/umac.py +262 -0
- ccssh/wire.py +199 -0
- ccssh-1.0.0.dist-info/METADATA +442 -0
- ccssh-1.0.0.dist-info/RECORD +36 -0
- ccssh-1.0.0.dist-info/WHEEL +5 -0
- ccssh-1.0.0.dist-info/entry_points.txt +2 -0
- ccssh-1.0.0.dist-info/licenses/LICENSE +21 -0
- ccssh-1.0.0.dist-info/top_level.txt +1 -0
ccssh/umac.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UMAC message authentication (RFC 4418) + SSH wire mapping
|
|
3
|
+
(draft-miller-secsh-umac-01: mac = UMAC(key, seq-as-uint64, packet)).
|
|
4
|
+
|
|
5
|
+
Pure Python over AES-128-ECB from `cryptography` (correctness-first;
|
|
6
|
+
SSH packets are small). Verified against every RFC Appendix test
|
|
7
|
+
vector (32/64/96-bit tags plus all intermediate keys and L1/L2/L3
|
|
8
|
+
outputs) AND live against OpenSSH (umac-64/128, plain and -etm).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import struct
|
|
13
|
+
|
|
14
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
15
|
+
|
|
16
|
+
BLOCKLEN = 16
|
|
17
|
+
KEYLEN = 16
|
|
18
|
+
|
|
19
|
+
P36 = 2**36 - 5
|
|
20
|
+
P64 = 2**64 - 59
|
|
21
|
+
P128 = 2**128 - 159
|
|
22
|
+
M64 = (1 << 64) - 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _enc(key: bytes, block: bytes) -> bytes:
|
|
26
|
+
e = Cipher(algorithms.AES(key), modes.ECB()).encryptor()
|
|
27
|
+
return e.update(block) + e.finalize()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _enc_with(cipher: Cipher, block: bytes) -> bytes:
|
|
31
|
+
e = cipher.encryptor()
|
|
32
|
+
return e.update(block) + e.finalize()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _kdf_with(cipher: Cipher, index: int, numbytes: int) -> bytes:
|
|
36
|
+
out = b""
|
|
37
|
+
n = (numbytes + BLOCKLEN - 1) // BLOCKLEN
|
|
38
|
+
for i in range(1, n + 1):
|
|
39
|
+
out += _enc_with(cipher, index.to_bytes(BLOCKLEN - 8, "big") + i.to_bytes(8, "big"))
|
|
40
|
+
return out[:numbytes]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _uhash_with_keys(l1key: bytes, l2key: bytes, l3k1: bytes, l3k2: bytes,
|
|
44
|
+
msg: bytes, taglen: int) -> bytes:
|
|
45
|
+
"""UHASH with pre-derived L1/L2/L3 subkeys (same math as uhash)."""
|
|
46
|
+
iters = taglen // 4
|
|
47
|
+
out = b""
|
|
48
|
+
for i in range(iters):
|
|
49
|
+
k1i = l1key[i * 16:i * 16 + 1024]
|
|
50
|
+
k2i = l2key[i * 24:(i + 1) * 24]
|
|
51
|
+
k3a = l3k1[i * 64:(i + 1) * 64]
|
|
52
|
+
k3b = l3k2[i * 4:(i + 1) * 4]
|
|
53
|
+
a = _l1(k1i, msg)
|
|
54
|
+
b = b"\x00" * 8 + a if len(msg) <= 1024 else _l2(k2i, a)
|
|
55
|
+
out += _l3(k3a, k3b, b)
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _pdf_with(pdf_cipher: Cipher, nonce: bytes, taglen: int) -> bytes:
|
|
60
|
+
if taglen not in (4, 8, 12, 16):
|
|
61
|
+
raise ValueError("taglen must be 4/8/12/16")
|
|
62
|
+
if not 1 <= len(nonce) <= BLOCKLEN:
|
|
63
|
+
raise ValueError("nonce must be 1..16 bytes")
|
|
64
|
+
index = 0
|
|
65
|
+
if taglen in (4, 8):
|
|
66
|
+
index = int.from_bytes(nonce, "big") % (BLOCKLEN // taglen)
|
|
67
|
+
nonce = bytes(a ^ b for a, b in zip(nonce, index.to_bytes(len(nonce), "big")))
|
|
68
|
+
nonce = nonce + b"\x00" * (BLOCKLEN - len(nonce))
|
|
69
|
+
# pdf encrypts with KDF(key, 0): derive the pad key once per context.
|
|
70
|
+
t = _enc_with(pdf_cipher, nonce)
|
|
71
|
+
if taglen in (4, 8):
|
|
72
|
+
return t[index * taglen:(index + 1) * taglen]
|
|
73
|
+
return t[:taglen]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _u32(b: bytes) -> int:
|
|
77
|
+
return int.from_bytes(b, "big")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _u64(b: bytes) -> int:
|
|
81
|
+
return int.from_bytes(b, "big")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _pack32(n: int) -> bytes:
|
|
85
|
+
return (n & 0xFFFFFFFF).to_bytes(4, "big")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _pack64(n: int) -> bytes:
|
|
89
|
+
return (n & M64).to_bytes(8, "big")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _pack128(n: int) -> bytes:
|
|
93
|
+
return (n & ((1 << 128) - 1)).to_bytes(16, "big")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _endian_swap(s: bytes) -> bytes:
|
|
97
|
+
return b"".join(s[i:i + 4][::-1] for i in range(0, len(s), 4))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def kdf(key: bytes, index: int, numbytes: int) -> bytes:
|
|
101
|
+
# Test/reference path (RFC vectors): production tags go through
|
|
102
|
+
# UMacCtx with precomputed subkeys below.
|
|
103
|
+
out = b""
|
|
104
|
+
n = (numbytes + BLOCKLEN - 1) // BLOCKLEN
|
|
105
|
+
for i in range(1, n + 1):
|
|
106
|
+
out += _enc(key, index.to_bytes(BLOCKLEN - 8, "big") + i.to_bytes(8, "big"))
|
|
107
|
+
return out[:numbytes]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def pdf(key: bytes, nonce: bytes, taglen: int) -> bytes:
|
|
111
|
+
if taglen not in (4, 8, 12, 16):
|
|
112
|
+
raise ValueError("taglen must be 4/8/12/16")
|
|
113
|
+
if not 1 <= len(nonce) <= BLOCKLEN:
|
|
114
|
+
raise ValueError("nonce must be 1..16 bytes")
|
|
115
|
+
index = 0
|
|
116
|
+
if taglen in (4, 8):
|
|
117
|
+
index = int.from_bytes(nonce, "big") % (BLOCKLEN // taglen)
|
|
118
|
+
nonce = bytes(a ^ b for a, b in zip(nonce, index.to_bytes(len(nonce), "big")))
|
|
119
|
+
nonce = nonce + b"\x00" * (BLOCKLEN - len(nonce))
|
|
120
|
+
t = _enc(kdf(key, 0, KEYLEN), nonce)
|
|
121
|
+
if taglen in (4, 8):
|
|
122
|
+
return t[index * taglen:(index + 1) * taglen]
|
|
123
|
+
return t[:taglen]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _nh(key: bytes, msg: bytes) -> bytes:
|
|
127
|
+
"""
|
|
128
|
+
NH over a 32-byte-multiple message with a 1024-byte key prefix.
|
|
129
|
+
"""
|
|
130
|
+
t = len(msg) // 4
|
|
131
|
+
y = 0
|
|
132
|
+
m32 = 0xFFFFFFFF
|
|
133
|
+
for i in range(0, t, 8):
|
|
134
|
+
for j in range(4):
|
|
135
|
+
a = (_u32(msg[4 * (i + j):4 * (i + j) + 4])
|
|
136
|
+
+ _u32(key[4 * (i + j):4 * (i + j) + 4])) & m32
|
|
137
|
+
b = (_u32(msg[4 * (i + 4 + j):4 * (i + 4 + j) + 4])
|
|
138
|
+
+ _u32(key[4 * (i + 4 + j):4 * (i + 4 + j) + 4])) & m32
|
|
139
|
+
y += a * b
|
|
140
|
+
return _pack64(y & M64)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _l1(key: bytes, msg: bytes) -> bytes:
|
|
144
|
+
t = max((len(msg) * 8 + 8191) // 8192, 1)
|
|
145
|
+
out = b""
|
|
146
|
+
for i in range(t - 1):
|
|
147
|
+
chunk = _endian_swap(msg[i * 1024:(i + 1) * 1024])
|
|
148
|
+
out += _pack64((_u64(_nh(key, chunk)) + 1024 * 8) & M64)
|
|
149
|
+
last = msg[(t - 1) * 1024:]
|
|
150
|
+
bitlen = len(last) * 8
|
|
151
|
+
pad = (-len(last)) % 32
|
|
152
|
+
if not last:
|
|
153
|
+
pad = 32 # zeropad() targets a *positive* multiple: empty -> 32 zeros
|
|
154
|
+
last += b"\x00" * pad
|
|
155
|
+
out += _pack64((_u64(_nh(key, _endian_swap(last))) + bitlen) & M64)
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _poly(wordbits: int, maxwordrange: int, k: int, msg: bytes) -> int:
|
|
160
|
+
wb = wordbits // 8
|
|
161
|
+
p = P64 if wordbits == 64 else P128
|
|
162
|
+
offset = 2**wordbits - p
|
|
163
|
+
marker = p - 1
|
|
164
|
+
n = len(msg) // wb
|
|
165
|
+
y = 1
|
|
166
|
+
for i in range(n):
|
|
167
|
+
m = int.from_bytes(msg[i * wb:(i + 1) * wb], "big")
|
|
168
|
+
if m >= maxwordrange:
|
|
169
|
+
y = (k * y + marker) % p
|
|
170
|
+
y = (k * y + (m - offset)) % p
|
|
171
|
+
else:
|
|
172
|
+
y = (k * y + m) % p
|
|
173
|
+
return y
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _l2(key: bytes, msg: bytes) -> bytes:
|
|
177
|
+
mask64 = 0x01FFFFFF01FFFFFF
|
|
178
|
+
mask128 = 0x01FFFFFF01FFFFFF01FFFFFF01FFFFFF
|
|
179
|
+
k64 = _u64(key[:8]) & mask64
|
|
180
|
+
k128 = int.from_bytes(key[8:24], "big") & mask128
|
|
181
|
+
if len(msg) <= 2**17:
|
|
182
|
+
y = _poly(64, 2**64 - 2**32, k64, msg)
|
|
183
|
+
else:
|
|
184
|
+
m1, m2 = msg[:2**17], msg[2**17:]
|
|
185
|
+
m2 = m2 + b"\x80"
|
|
186
|
+
m2 += b"\x00" * ((-len(m2)) % 16)
|
|
187
|
+
y = _poly(64, 2**64 - 2**32, k64, m1)
|
|
188
|
+
y = _poly(128, 2**128 - 2**96, k128, y.to_bytes(16, "big") + m2)
|
|
189
|
+
return _pack128(y)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _l3(k1: bytes, k2: bytes, msg: bytes) -> bytes:
|
|
193
|
+
y = 0
|
|
194
|
+
for i in range(8):
|
|
195
|
+
m = int.from_bytes(msg[2 * i:2 * i + 2], "big")
|
|
196
|
+
k = int.from_bytes(k1[8 * i:8 * i + 8], "big") % P36
|
|
197
|
+
y += m * k
|
|
198
|
+
y %= P36
|
|
199
|
+
y %= 2**32
|
|
200
|
+
return bytes(a ^ b for a, b in zip(_pack32(y), k2))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def uhash(key: bytes, msg: bytes, taglen: int) -> bytes:
|
|
204
|
+
if taglen not in (4, 8, 12, 16):
|
|
205
|
+
raise ValueError("taglen must be 4/8/12/16")
|
|
206
|
+
iters = taglen // 4
|
|
207
|
+
l1key = kdf(key, 1, 1024 + (iters - 1) * 16)
|
|
208
|
+
l2key = kdf(key, 2, iters * 24)
|
|
209
|
+
l3k1 = kdf(key, 3, iters * 64)
|
|
210
|
+
l3k2 = kdf(key, 4, iters * 4)
|
|
211
|
+
out = b""
|
|
212
|
+
for i in range(iters):
|
|
213
|
+
k1i = l1key[i * 16:i * 16 + 1024]
|
|
214
|
+
k2i = l2key[i * 24:(i + 1) * 24]
|
|
215
|
+
k3a = l3k1[i * 64:(i + 1) * 64]
|
|
216
|
+
k3b = l3k2[i * 4:(i + 1) * 4]
|
|
217
|
+
a = _l1(k1i, msg)
|
|
218
|
+
b = b"\x00" * 8 + a if len(msg) <= 1024 else _l2(k2i, a)
|
|
219
|
+
out += _l3(k3a, k3b, b)
|
|
220
|
+
return out
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def umac(key: bytes, msg: bytes, nonce: bytes, taglen: int) -> bytes:
|
|
224
|
+
# Test/reference path: production tags go through UMacCtx.
|
|
225
|
+
if len(key) != KEYLEN:
|
|
226
|
+
raise ValueError("UMAC key must be 16 bytes")
|
|
227
|
+
hashed = uhash(key, msg, taglen)
|
|
228
|
+
pad = pdf(key, nonce, taglen)
|
|
229
|
+
return bytes(a ^ b for a, b in zip(hashed, pad))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class UMacCtx:
|
|
233
|
+
"""
|
|
234
|
+
SSH UMAC (draft-miller-secsh-umac-01): the packet sequence number,
|
|
235
|
+
encoded as uint64, is the nonce. One direction per instance.
|
|
236
|
+
|
|
237
|
+
The L1/L2/L3 subkeys depend only on the connection key, so they are
|
|
238
|
+
derived once here instead of on every packet (dozens of AES ops per
|
|
239
|
+
tag before). The AES key schedules are cached as Cipher objects.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
def __init__(self, key: bytes, out_len: int):
|
|
243
|
+
if len(key) != KEYLEN or out_len not in (8, 16):
|
|
244
|
+
raise ValueError("UMAC needs a 16-byte key and 8/16-byte tags")
|
|
245
|
+
self._key = key
|
|
246
|
+
self._out_len = out_len
|
|
247
|
+
iters = out_len // 4
|
|
248
|
+
kdf_cipher = Cipher(algorithms.AES(key), modes.ECB())
|
|
249
|
+
self._l1key = _kdf_with(kdf_cipher, 1, 1024 + (iters - 1) * 16)
|
|
250
|
+
self._l2key = _kdf_with(kdf_cipher, 2, iters * 24)
|
|
251
|
+
self._l3k1 = _kdf_with(kdf_cipher, 3, iters * 64)
|
|
252
|
+
self._l3k2 = _kdf_with(kdf_cipher, 4, iters * 4)
|
|
253
|
+
pdf_key = _kdf_with(kdf_cipher, 0, KEYLEN)
|
|
254
|
+
self._pdf_cipher = Cipher(algorithms.AES(pdf_key), modes.ECB())
|
|
255
|
+
|
|
256
|
+
def tag(self, seq: int, data: bytes) -> bytes:
|
|
257
|
+
hashed = _uhash_with_keys(self._l1key, self._l2key, self._l3k1,
|
|
258
|
+
self._l3k2, data, self._out_len)
|
|
259
|
+
pad = _pdf_with(self._pdf_cipher,
|
|
260
|
+
struct.pack(">Q", seq & 0xFFFFFFFFFFFFFFFF),
|
|
261
|
+
self._out_len)
|
|
262
|
+
return bytes(a ^ b for a, b in zip(hashed, pad))
|
ccssh/wire.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SSH wire encoding, RFC 4251 section 5: byte/boolean/uint32/uint64/string/mpint/namelist.
|
|
3
|
+
|
|
4
|
+
Dependency-free. Byte-for-byte compatible with OpenSSH on the wire.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import struct
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WireError(ValueError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def dec_byte(buf: bytes, off: int = 0) -> tuple[int, int]:
|
|
15
|
+
if off + 1 > len(buf):
|
|
16
|
+
raise WireError("buffer too short for byte")
|
|
17
|
+
return buf[off], off + 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def enc_bool(v: bool) -> bytes:
|
|
21
|
+
return b"\x01" if v else b"\x00"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def dec_bool(buf: bytes, off: int = 0) -> tuple[bool, int]:
|
|
25
|
+
b, off2 = dec_byte(buf, off)
|
|
26
|
+
if b not in (0, 1):
|
|
27
|
+
raise WireError("boolean must be 0 or 1")
|
|
28
|
+
return bool(b), off2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def enc_u32(v: int) -> bytes:
|
|
32
|
+
if not 0 <= v < 2**32:
|
|
33
|
+
raise WireError("uint32 out of range")
|
|
34
|
+
return struct.pack(">I", v)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dec_u32(buf: bytes, off: int = 0) -> tuple[int, int]:
|
|
38
|
+
if off + 4 > len(buf):
|
|
39
|
+
raise WireError("buffer too short for uint32")
|
|
40
|
+
return struct.unpack_from(">I", buf, off)[0], off + 4
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def enc_u64(v: int) -> bytes:
|
|
44
|
+
if not 0 <= v < 2**64:
|
|
45
|
+
raise WireError("uint64 out of range")
|
|
46
|
+
return struct.pack(">Q", v)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dec_u64(buf: bytes, off: int = 0) -> tuple[int, int]:
|
|
50
|
+
if off + 8 > len(buf):
|
|
51
|
+
raise WireError("buffer too short for uint64")
|
|
52
|
+
return struct.unpack_from(">Q", buf, off)[0], off + 8
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def enc_string(data: bytes) -> bytes:
|
|
56
|
+
return enc_u32(len(data)) + data
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def dec_string(buf: bytes, off: int = 0) -> tuple[bytes, int]:
|
|
60
|
+
ln, off2 = dec_u32(buf, off)
|
|
61
|
+
end = off2 + ln
|
|
62
|
+
if end > len(buf):
|
|
63
|
+
raise WireError(f"string declares len={ln}, only {len(buf) - off2} left")
|
|
64
|
+
return buf[off2:end], end
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def enc_namelist(names: list[str]) -> bytes:
|
|
68
|
+
for n in names:
|
|
69
|
+
if not n or "," in n:
|
|
70
|
+
raise WireError(f"bad namelist entry {n!r}")
|
|
71
|
+
try:
|
|
72
|
+
n.encode("ascii")
|
|
73
|
+
except UnicodeEncodeError as e:
|
|
74
|
+
raise WireError(f"namelist must be ascii: {n!r}") from e
|
|
75
|
+
try:
|
|
76
|
+
return enc_string(",".join(names).encode("ascii"))
|
|
77
|
+
except UnicodeEncodeError as e:
|
|
78
|
+
raise WireError(f"namelist must be ascii: {e}") from e
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def dec_namelist(buf: bytes, off: int = 0) -> tuple[list[str], int]:
|
|
82
|
+
raw, off2 = dec_string(buf, off)
|
|
83
|
+
if not raw:
|
|
84
|
+
return [], off2
|
|
85
|
+
if len(raw) > 32768 or raw.count(b",") > 512:
|
|
86
|
+
raise WireError("namelist too large")
|
|
87
|
+
# Lenient like OpenSSH: empty items (trailing/double commas) are kept
|
|
88
|
+
# and simply never match anything in negotiation.
|
|
89
|
+
try:
|
|
90
|
+
return raw.decode("ascii").split(","), off2
|
|
91
|
+
except UnicodeDecodeError as e:
|
|
92
|
+
raise WireError(f"namelist must be ascii: {e}") from e
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def enc_mpint(value: int) -> bytes:
|
|
96
|
+
"""mpint per RFC 4251: uint32 length + minimal big-endian two's complement."""
|
|
97
|
+
if value == 0:
|
|
98
|
+
return enc_u32(0)
|
|
99
|
+
if value < 0:
|
|
100
|
+
# Negative values never occur on the wire, but encode them correctly.
|
|
101
|
+
nbytes = (value.bit_length() + 8) // 8
|
|
102
|
+
raw = value.to_bytes(nbytes, "big", signed=True)
|
|
103
|
+
else:
|
|
104
|
+
nbytes = (value.bit_length() + 7) // 8
|
|
105
|
+
raw = value.to_bytes(nbytes, "big", signed=False)
|
|
106
|
+
if raw[0] & 0x80:
|
|
107
|
+
raw = b"\x00" + raw
|
|
108
|
+
return enc_u32(len(raw)) + raw
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def dec_mpint(buf: bytes, off: int = 0) -> tuple[int, int]:
|
|
112
|
+
raw, off2 = dec_string(buf, off)
|
|
113
|
+
if not raw:
|
|
114
|
+
return 0, off2
|
|
115
|
+
# RFC 4251 section 5: zero is the empty string; a lone 0x00 byte is
|
|
116
|
+
# non-minimal and must be rejected.
|
|
117
|
+
if len(raw) == 1 and raw[0] == 0:
|
|
118
|
+
raise WireError("non-minimal mpint encoding of zero")
|
|
119
|
+
# Minimal two's complement: no redundant leading 0x00 (when the next
|
|
120
|
+
# byte's high bit is clear) or 0xFF (when it is set) bytes.
|
|
121
|
+
if len(raw) > 1 and raw[0] == 0x00 and not (raw[1] & 0x80):
|
|
122
|
+
raise WireError("non-minimal mpint: redundant leading zero")
|
|
123
|
+
if len(raw) > 1 and raw[0] == 0xFF and (raw[1] & 0x80):
|
|
124
|
+
raise WireError("non-minimal mpint: redundant leading 0xFF")
|
|
125
|
+
return int.from_bytes(raw, "big", signed=True), off2
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def mpint_from_bytes(b: bytes) -> bytes:
|
|
129
|
+
"""
|
|
130
|
+
Encode a raw big-endian shared secret as an mpint field.
|
|
131
|
+
|
|
132
|
+
Adds a 0x00 prefix when the top bit is set and strips redundant
|
|
133
|
+
leading zeroes (keeping one if needed for the sign bit).
|
|
134
|
+
"""
|
|
135
|
+
if not b:
|
|
136
|
+
return enc_u32(0)
|
|
137
|
+
if b[0] & 0x80:
|
|
138
|
+
b = b"\x00" + b
|
|
139
|
+
# Strip redundant leading zeroes in one pass (no quadratic slicing).
|
|
140
|
+
i = 0
|
|
141
|
+
while len(b) - i > 1 and b[i] == 0 and not (b[i + 1] & 0x80):
|
|
142
|
+
i += 1
|
|
143
|
+
if i:
|
|
144
|
+
b = b[i:]
|
|
145
|
+
return enc_u32(len(b)) + b
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Reader:
|
|
149
|
+
"""Sequential cursor reader over a payload buffer."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, buf: bytes):
|
|
152
|
+
self._b = buf
|
|
153
|
+
self._o = 0
|
|
154
|
+
|
|
155
|
+
def rest(self) -> int:
|
|
156
|
+
return len(self._b) - self._o
|
|
157
|
+
|
|
158
|
+
def tell(self) -> int:
|
|
159
|
+
return self._o
|
|
160
|
+
|
|
161
|
+
def byte(self) -> int:
|
|
162
|
+
v, self._o = dec_byte(self._b, self._o)
|
|
163
|
+
return v
|
|
164
|
+
|
|
165
|
+
def bool(self) -> bool:
|
|
166
|
+
v, self._o = dec_bool(self._b, self._o)
|
|
167
|
+
return v
|
|
168
|
+
|
|
169
|
+
def u32(self) -> int:
|
|
170
|
+
v, self._o = dec_u32(self._b, self._o)
|
|
171
|
+
return v
|
|
172
|
+
|
|
173
|
+
def u64(self) -> int:
|
|
174
|
+
v, self._o = dec_u64(self._b, self._o)
|
|
175
|
+
return v
|
|
176
|
+
|
|
177
|
+
def string(self) -> bytes:
|
|
178
|
+
v, self._o = dec_string(self._b, self._o)
|
|
179
|
+
return v
|
|
180
|
+
|
|
181
|
+
def namelist(self) -> list[str]:
|
|
182
|
+
v, self._o = dec_namelist(self._b, self._o)
|
|
183
|
+
return v
|
|
184
|
+
|
|
185
|
+
def mpint(self) -> int:
|
|
186
|
+
v, self._o = dec_mpint(self._b, self._o)
|
|
187
|
+
return v
|
|
188
|
+
|
|
189
|
+
def raw(self, n: int) -> bytes:
|
|
190
|
+
if n < 0 or self._o + n > len(self._b):
|
|
191
|
+
raise WireError("raw read out of bounds")
|
|
192
|
+
v = self._b[self._o:self._o + n]
|
|
193
|
+
self._o += n
|
|
194
|
+
return v
|
|
195
|
+
|
|
196
|
+
def assert_end(self) -> None:
|
|
197
|
+
"""Reject trailing garbage after a fixed-layout message."""
|
|
198
|
+
if self.rest() != 0:
|
|
199
|
+
raise WireError(f"trailing bytes in message ({self.rest()} left)")
|