packet-tracer-skill 0.2.2 → 0.3.0

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +424 -42
  2. package/README.md +535 -250
  3. package/SKILL.md +337 -262
  4. package/bin/packet-tracer-skill.js +29 -2
  5. package/docs/automation-controller-proof.md +35 -0
  6. package/docs/curated-donor-registry.md +11 -0
  7. package/docs/generate-ready-pilot-design.md +30 -0
  8. package/docs/github-launch-ops-0.2.3.md +37 -0
  9. package/docs/github-metadata.md +6 -4
  10. package/docs/hero-demo-plan.md +1 -1
  11. package/docs/home-iot-donor-proof.md +4 -4
  12. package/docs/industrial-programming-proof.md +48 -0
  13. package/docs/ipv4-routing-management-proof.md +37 -0
  14. package/docs/l2-resiliency-bgp-proof.md +60 -0
  15. package/docs/l2-security-qos-proof.md +59 -0
  16. package/docs/packet-tracer-feature-gap-atlas.md +174 -17
  17. package/docs/post-launch-follow-up.md +9 -5
  18. package/docs/proof-readiness-dashboard.md +69 -0
  19. package/docs/publish-preview-roadmap.md +6 -5
  20. package/docs/release-checklist.md +27 -13
  21. package/docs/release-notes-0.2.2.md +1 -1
  22. package/docs/release-notes-0.2.3.md +59 -0
  23. package/docs/release-notes-0.2.4.md +20 -0
  24. package/docs/runtime-truth.md +33 -8
  25. package/docs/security-edge-deepening-proof.md +65 -0
  26. package/docs/voice-collaboration-proof.md +38 -0
  27. package/docs/wan-security-donor-proof.md +20 -3
  28. package/examples/README.md +98 -69
  29. package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
  30. package/examples/gallery.md +94 -6
  31. package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
  32. package/examples/index.json +932 -4
  33. package/examples/local-sample-evidence.json +24 -0
  34. package/examples/proof-cards.json +117 -0
  35. package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
  36. package/package.json +60 -44
  37. package/pytest.ini +9 -0
  38. package/references/packettracer-feature-atlas.json +67 -17
  39. package/references/packettracer-sample-catalog.json +45287 -4525
  40. package/references/packettracer-sample-catalog.md +599 -259
  41. package/references/proof-readiness-candidates.json +352 -0
  42. package/scripts/build_examples_index.py +228 -35
  43. package/scripts/build_sample_catalog.py +24 -44
  44. package/scripts/corpus_runner.py +430 -0
  45. package/scripts/coverage_matrix.py +1842 -1319
  46. package/scripts/donor_cache.py +354 -0
  47. package/scripts/donor_diagnostics.py +3 -1
  48. package/scripts/feature_atlas.py +65 -1
  49. package/scripts/generate_pkt.py +8762 -4070
  50. package/scripts/intent_parser.py +2242 -1138
  51. package/scripts/local_donors.py +340 -0
  52. package/scripts/packet_tracer_env.py +846 -391
  53. package/scripts/pkt_annotate.py +218 -0
  54. package/scripts/pkt_codec.py +420 -181
  55. package/scripts/pkt_editor.py +2405 -1226
  56. package/scripts/pkt_transformer.py +1072 -727
  57. package/scripts/pkt_verify.py +461 -0
  58. package/scripts/remote_search.py +197 -21
  59. package/scripts/runtime_doctor.py +80 -29
  60. package/scripts/sample_catalog.py +1372 -1195
  61. package/scripts/twofish_diagnostics.py +48 -31
  62. package/scripts/usage_ledger.py +218 -0
  63. package/scripts/vendor/README.md +44 -37
  64. package/scripts/vendor/twofish_pure.py +321 -0
  65. package/scripts/workspace_repair.py +548 -508
  66. package/templates/pt900/donors/README.md +15 -0
@@ -0,0 +1,321 @@
1
+ """
2
+ Pure-Python Twofish block cipher.
3
+
4
+ Twofish was designed by Bruce Schneier, John Kelsey, Doug Whiting, David Wagner,
5
+ Chris Hall and Niels Ferguson. It is unpatented, uncopyrighted, and free for all
6
+ uses, so it can be vendored directly instead of binding to a compiled bridge.
7
+
8
+ This module exists so that `pkt_codec` has a runtime that works on a clean
9
+ checkout with no compiled artifacts and no environment variables. The ctypes
10
+ bridge in `twofish.py` remains supported as an optional accelerator.
11
+
12
+ Reference: "Twofish: A 128-Bit Block Cipher" (Schneier et al., 1998), and the
13
+ official test vectors reproduced in `self_test()` below.
14
+
15
+ The implementation uses the standard "full keying" optimisation: the key-dependent
16
+ S-box and the MDS matrix multiply are folded into four 256-entry word tables at
17
+ key-schedule time, so the hot loop is four table lookups and three XORs per g().
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ MASK32 = 0xFFFFFFFF
23
+ BLOCK_SIZE = 16
24
+ ROUNDS = 16
25
+
26
+ # --- q permutation construction (spec section 4.3.5) ------------------------
27
+
28
+ _Q0_T = (
29
+ (0x8, 0x1, 0x7, 0xD, 0x6, 0xF, 0x3, 0x2, 0x0, 0xB, 0x5, 0x9, 0xE, 0xC, 0xA, 0x4),
30
+ (0xE, 0xC, 0xB, 0x8, 0x1, 0x2, 0x3, 0x5, 0xF, 0x4, 0xA, 0x6, 0x7, 0x0, 0x9, 0xD),
31
+ (0xB, 0xA, 0x5, 0xE, 0x6, 0xD, 0x9, 0x0, 0xC, 0x8, 0xF, 0x3, 0x2, 0x4, 0x7, 0x1),
32
+ (0xD, 0x7, 0xF, 0x4, 0x1, 0x2, 0x6, 0xE, 0x9, 0xB, 0x3, 0x0, 0x8, 0x5, 0xC, 0xA),
33
+ )
34
+
35
+ _Q1_T = (
36
+ (0x2, 0x8, 0xB, 0xD, 0xF, 0x7, 0x6, 0xE, 0x3, 0x1, 0x9, 0x4, 0x0, 0xA, 0xC, 0x5),
37
+ (0x1, 0xE, 0x2, 0xB, 0x4, 0xC, 0x3, 0x7, 0x6, 0xD, 0xA, 0x5, 0xF, 0x9, 0x0, 0x8),
38
+ (0x4, 0xC, 0x7, 0x5, 0x1, 0x6, 0x9, 0xA, 0x0, 0xE, 0xD, 0x8, 0x2, 0xB, 0x3, 0xF),
39
+ (0xB, 0x9, 0x5, 0x1, 0xC, 0x3, 0xD, 0xE, 0x6, 0x4, 0x7, 0xF, 0x2, 0x0, 0x8, 0xA),
40
+ )
41
+
42
+
43
+ def _build_q(t: tuple[tuple[int, ...], ...]) -> tuple[int, ...]:
44
+ table = []
45
+ for x in range(256):
46
+ a0, b0 = x >> 4, x & 0xF
47
+ a1 = a0 ^ b0
48
+ b1 = (a0 ^ ((b0 >> 1) | ((b0 & 1) << 3)) ^ (a0 << 3)) & 0xF
49
+ a2, b2 = t[0][a1], t[1][b1]
50
+ a3 = a2 ^ b2
51
+ b3 = (a2 ^ ((b2 >> 1) | ((b2 & 1) << 3)) ^ (a2 << 3)) & 0xF
52
+ a4, b4 = t[2][a3], t[3][b3]
53
+ table.append((b4 << 4) | a4)
54
+ return tuple(table)
55
+
56
+
57
+ Q0 = _build_q(_Q0_T)
58
+ Q1 = _build_q(_Q1_T)
59
+
60
+ # --- GF(2^8) helpers --------------------------------------------------------
61
+
62
+ MDS_POLY = 0x169 # x^8 + x^6 + x^5 + x^3 + 1
63
+ RS_POLY = 0x14D # x^8 + x^6 + x^3 + x^2 + 1
64
+
65
+
66
+ def _gf_mul(a: int, b: int, poly: int) -> int:
67
+ result = 0
68
+ while b:
69
+ if b & 1:
70
+ result ^= a
71
+ b >>= 1
72
+ a <<= 1
73
+ if a & 0x100:
74
+ a ^= poly
75
+ return result & 0xFF
76
+
77
+
78
+ # MDS matrix, by column: column i scaled by y_i contributes to z0..z3.
79
+ _MDS_COLUMNS = (
80
+ (0x01, 0x5B, 0xEF, 0xEF),
81
+ (0xEF, 0xEF, 0x5B, 0x01),
82
+ (0x5B, 0xEF, 0x01, 0xEF),
83
+ (0x5B, 0x01, 0xEF, 0x5B),
84
+ )
85
+
86
+ # mds_column_word[i][b] == the 32-bit little-endian word contributed by byte b
87
+ # sitting in position i of the MDS input vector.
88
+ _MDS_COLUMN_WORD: tuple[tuple[int, ...], ...] = tuple(
89
+ tuple(
90
+ (_gf_mul(column[0], b, MDS_POLY))
91
+ | (_gf_mul(column[1], b, MDS_POLY) << 8)
92
+ | (_gf_mul(column[2], b, MDS_POLY) << 16)
93
+ | (_gf_mul(column[3], b, MDS_POLY) << 24)
94
+ for b in range(256)
95
+ )
96
+ for column in _MDS_COLUMNS
97
+ )
98
+
99
+ _RS_MATRIX = (
100
+ (0x01, 0xA4, 0x55, 0x87, 0x5A, 0x58, 0xDB, 0x9E),
101
+ (0xA4, 0x56, 0x82, 0xF3, 0x1E, 0xC6, 0x68, 0xE5),
102
+ (0x02, 0xA1, 0xFC, 0xC1, 0x47, 0xAE, 0x3D, 0x19),
103
+ (0xA4, 0x55, 0x87, 0x5A, 0x58, 0xDB, 0x9E, 0x03),
104
+ )
105
+
106
+
107
+ def _rs_encode(chunk: bytes) -> int:
108
+ """Map 8 key bytes to one 32-bit S-vector word via the RS matrix."""
109
+ word = 0
110
+ for row_index, row in enumerate(_RS_MATRIX):
111
+ acc = 0
112
+ for coefficient, byte in zip(row, chunk):
113
+ acc ^= _gf_mul(coefficient, byte, RS_POLY)
114
+ word |= acc << (8 * row_index)
115
+ return word
116
+
117
+
118
+ # --- rotations --------------------------------------------------------------
119
+
120
+
121
+ def _rol32(value: int, count: int) -> int:
122
+ count &= 31
123
+ return ((value << count) | (value >> (32 - count))) & MASK32
124
+
125
+
126
+ def _ror32(value: int, count: int) -> int:
127
+ count &= 31
128
+ return ((value >> count) | (value << (32 - count))) & MASK32
129
+
130
+
131
+ # --- h function -------------------------------------------------------------
132
+
133
+
134
+ def _h_byte(position: int, byte: int, l_bytes: list[tuple[int, int, int, int]], k: int) -> int:
135
+ """The q-permutation cascade of h() for one byte position, before MDS."""
136
+ y = byte
137
+ if k == 4:
138
+ y = (Q1, Q0, Q0, Q1)[position][y] ^ l_bytes[3][position]
139
+ if k >= 3:
140
+ y = (Q1, Q1, Q0, Q0)[position][y] ^ l_bytes[2][position]
141
+ y = (Q0, Q1, Q0, Q1)[position][y] ^ l_bytes[1][position]
142
+ y = (Q0, Q0, Q1, Q1)[position][y] ^ l_bytes[0][position]
143
+ return (Q1, Q0, Q1, Q0)[position][y]
144
+
145
+
146
+ def _h(x: int, l_words: list[int], k: int) -> int:
147
+ l_bytes = [
148
+ (word & 0xFF, (word >> 8) & 0xFF, (word >> 16) & 0xFF, (word >> 24) & 0xFF)
149
+ for word in l_words
150
+ ]
151
+ result = 0
152
+ for position in range(4):
153
+ byte = (x >> (8 * position)) & 0xFF
154
+ result ^= _MDS_COLUMN_WORD[position][_h_byte(position, byte, l_bytes, k)]
155
+ return result
156
+
157
+
158
+ class Twofish:
159
+ """Twofish in ECB single-block form, API-compatible with the ctypes bridge."""
160
+
161
+ def __init__(self, key: bytes) -> None:
162
+ if not isinstance(key, (bytes, bytearray)):
163
+ raise TypeError("Twofish key must be bytes")
164
+ key = bytes(key)
165
+ if not 0 < len(key) <= 32:
166
+ raise ValueError("invalid Twofish key length")
167
+
168
+ # Twofish is defined for 128/192/256-bit keys; shorter keys are
169
+ # zero-padded up to the next legal size, matching the reference library.
170
+ for legal in (16, 24, 32):
171
+ if len(key) <= legal:
172
+ key = key.ljust(legal, b"\x00")
173
+ break
174
+
175
+ k = len(key) // 8
176
+ words = [int.from_bytes(key[i : i + 4], "little") for i in range(0, len(key), 4)]
177
+ me = [words[i] for i in range(0, 2 * k, 2)]
178
+ mo = [words[i] for i in range(1, 2 * k, 2)]
179
+ s_words = [_rs_encode(key[8 * i : 8 * (i + 1)]) for i in range(k)]
180
+ s_words.reverse()
181
+
182
+ subkeys: list[int] = []
183
+ for i in range(20):
184
+ a = _h((2 * i) * 0x01010101, me, k)
185
+ b = _rol32(_h((2 * i + 1) * 0x01010101, mo, k), 8)
186
+ subkeys.append((a + b) & MASK32)
187
+ subkeys.append(_rol32((a + 2 * b) & MASK32, 9))
188
+ self._k = subkeys
189
+
190
+ # Fold the key-dependent S-box and the MDS multiply into four word
191
+ # tables, so g() costs four lookups instead of a full h() evaluation.
192
+ s_bytes = [
193
+ (word & 0xFF, (word >> 8) & 0xFF, (word >> 16) & 0xFF, (word >> 24) & 0xFF)
194
+ for word in s_words
195
+ ]
196
+ self._sbox: list[tuple[int, ...]] = [
197
+ tuple(
198
+ _MDS_COLUMN_WORD[position][_h_byte(position, byte, s_bytes, k)]
199
+ for byte in range(256)
200
+ )
201
+ for position in range(4)
202
+ ]
203
+
204
+ def _g(self, x: int) -> int:
205
+ sbox = self._sbox
206
+ return (
207
+ sbox[0][x & 0xFF]
208
+ ^ sbox[1][(x >> 8) & 0xFF]
209
+ ^ sbox[2][(x >> 16) & 0xFF]
210
+ ^ sbox[3][(x >> 24) & 0xFF]
211
+ )
212
+
213
+ # The round function is written out longhand rather than calling `_g`,
214
+ # `_rol32` and `_ror32`. In CPython the call overhead dominates: inlining
215
+ # is worth roughly 2x, and these two methods are the whole cost of
216
+ # encoding or decoding a `.pkt`.
217
+ #
218
+ # `g(rol32(r1, 8))` is folded into the lookups: rotating left by 8 permutes
219
+ # the byte positions to (b3, b0, b1, b2), so the rotate never has to happen.
220
+
221
+ def encrypt(self, block: bytes) -> bytes:
222
+ if not isinstance(block, (bytes, bytearray)):
223
+ raise TypeError("block must be bytes")
224
+ if len(block) != BLOCK_SIZE:
225
+ raise ValueError("Twofish encrypt expects a 16-byte block")
226
+
227
+ k = self._k
228
+ s0, s1, s2, s3 = self._sbox
229
+ r0 = int.from_bytes(block[0:4], "little") ^ k[0]
230
+ r1 = int.from_bytes(block[4:8], "little") ^ k[1]
231
+ r2 = int.from_bytes(block[8:12], "little") ^ k[2]
232
+ r3 = int.from_bytes(block[12:16], "little") ^ k[3]
233
+
234
+ for index in range(8, 40, 2):
235
+ t0 = s0[r0 & 0xFF] ^ s1[(r0 >> 8) & 0xFF] ^ s2[(r0 >> 16) & 0xFF] ^ s3[r0 >> 24]
236
+ t1 = s0[r1 >> 24] ^ s1[r1 & 0xFF] ^ s2[(r1 >> 8) & 0xFF] ^ s3[(r1 >> 16) & 0xFF]
237
+ x = r2 ^ ((t0 + t1 + k[index]) & MASK32)
238
+ r0, r1, r2, r3 = (
239
+ (x >> 1) | ((x & 1) << 31),
240
+ (((r3 << 1) & MASK32) | (r3 >> 31)) ^ ((t0 + 2 * t1 + k[index + 1]) & MASK32),
241
+ r0,
242
+ r1,
243
+ )
244
+
245
+ # The final swap is undone by reading the registers back in 2,3,0,1 order.
246
+ return (
247
+ (r2 ^ k[4]).to_bytes(4, "little")
248
+ + (r3 ^ k[5]).to_bytes(4, "little")
249
+ + (r0 ^ k[6]).to_bytes(4, "little")
250
+ + (r1 ^ k[7]).to_bytes(4, "little")
251
+ )
252
+
253
+ def decrypt(self, block: bytes) -> bytes:
254
+ if not isinstance(block, (bytes, bytearray)):
255
+ raise TypeError("block must be bytes")
256
+ if len(block) != BLOCK_SIZE:
257
+ raise ValueError("Twofish decrypt expects a 16-byte block")
258
+
259
+ k = self._k
260
+ s0, s1, s2, s3 = self._sbox
261
+ r0 = int.from_bytes(block[0:4], "little") ^ k[4]
262
+ r1 = int.from_bytes(block[4:8], "little") ^ k[5]
263
+ r2 = int.from_bytes(block[8:12], "little") ^ k[6]
264
+ r3 = int.from_bytes(block[12:16], "little") ^ k[7]
265
+
266
+ for index in range(38, 6, -2):
267
+ t0 = s0[r0 & 0xFF] ^ s1[(r0 >> 8) & 0xFF] ^ s2[(r0 >> 16) & 0xFF] ^ s3[r0 >> 24]
268
+ t1 = s0[r1 >> 24] ^ s1[r1 & 0xFF] ^ s2[(r1 >> 8) & 0xFF] ^ s3[(r1 >> 16) & 0xFF]
269
+ x = r3 ^ ((t0 + 2 * t1 + k[index + 1]) & MASK32)
270
+ r0, r1, r2, r3 = (
271
+ (((r2 << 1) & MASK32) | (r2 >> 31)) ^ ((t0 + t1 + k[index]) & MASK32),
272
+ (x >> 1) | ((x & 1) << 31),
273
+ r0,
274
+ r1,
275
+ )
276
+
277
+ return (
278
+ (r2 ^ k[0]).to_bytes(4, "little")
279
+ + (r3 ^ k[1]).to_bytes(4, "little")
280
+ + (r0 ^ k[2]).to_bytes(4, "little")
281
+ + (r1 ^ k[3]).to_bytes(4, "little")
282
+ )
283
+
284
+
285
+ # Official Twofish test vectors: the I=3 (128-bit) and I=4 (192/256-bit) cases
286
+ # from section B.2 of the Twofish book.
287
+ TEST_VECTORS = (
288
+ (
289
+ "9F589F5CF6122C32B6BFEC2F2AE8C35A",
290
+ "D491DB16E7B1C39E86CB086B789F5419",
291
+ "019F9809DE1711858FAAC3A3BA20FBC3",
292
+ ),
293
+ (
294
+ "88B2B2706B105E36B446BB6D731A1E88EFA71F788965BD44",
295
+ "39DA69D6BA4997D585B6DC073CA341B2",
296
+ "182B02D81497EA45F9DAACDC29193A65",
297
+ ),
298
+ (
299
+ "D43BB7556EA32E46F2A282B7D45B4E0D57FF739D4DC92C1BD7FC01700CC8216F",
300
+ "90AFE91BB288544F2C32DC239B2635E6",
301
+ "6CB4561C40BF0A9705931CB6D408E7FA",
302
+ ),
303
+ )
304
+
305
+
306
+ def self_test() -> None:
307
+ """Verify against the official vectors. Raises on any mismatch."""
308
+ for key_hex, plain_hex, cipher_hex in TEST_VECTORS:
309
+ key = bytes.fromhex(key_hex)
310
+ plain = bytes.fromhex(plain_hex)
311
+ cipher = bytes.fromhex(cipher_hex)
312
+ engine = Twofish(key)
313
+ if engine.encrypt(plain) != cipher:
314
+ raise ValueError(f"Twofish encrypt vector failed for {len(key) * 8}-bit key")
315
+ if engine.decrypt(cipher) != plain:
316
+ raise ValueError(f"Twofish decrypt vector failed for {len(key) * 8}-bit key")
317
+
318
+
319
+ if __name__ == "__main__":
320
+ self_test()
321
+ print("pure-python twofish: all official vectors pass")