matrixscroll 0.1.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.
@@ -0,0 +1,62 @@
1
+ """Matrix Scroll — open protocol for hardware-signed AI-assisted code.
2
+
3
+ This package is the Python reference implementation of the Matrix Scroll
4
+ protocol. It exposes an Ed25519 root-of-trust abstraction with a software
5
+ emulator (default) and a hardware provider stub for the SSX360 reference
6
+ device (NXP SE050). Private keys never leave the provider.
7
+
8
+ Quickstart:
9
+
10
+ >>> import matrixscroll
11
+ >>> info = matrixscroll.identity_info()
12
+ >>> signed = matrixscroll.sign_manifest({"release": "v1.0.0"})
13
+ >>> matrixscroll.verify_manifest(signed)
14
+ True
15
+
16
+ See SPEC.md for the wire format and canonical encoding rules.
17
+ """
18
+
19
+ from ._core import (
20
+ ALGORITHM,
21
+ DEVICE_FILE,
22
+ SCHEMA,
23
+ SIGNATURE_SCHEMA,
24
+ EmulatedProvider,
25
+ HardwareProvider,
26
+ IdentityError,
27
+ IdentityProvider,
28
+ device_id,
29
+ get_provider,
30
+ identity_info,
31
+ public_key_b64,
32
+ sign,
33
+ sign_manifest,
34
+ status,
35
+ store_dir,
36
+ verify,
37
+ verify_manifest,
38
+ )
39
+
40
+ __version__ = "0.1.0"
41
+
42
+ __all__ = [
43
+ "ALGORITHM",
44
+ "DEVICE_FILE",
45
+ "EmulatedProvider",
46
+ "HardwareProvider",
47
+ "IdentityError",
48
+ "IdentityProvider",
49
+ "SCHEMA",
50
+ "SIGNATURE_SCHEMA",
51
+ "__version__",
52
+ "device_id",
53
+ "get_provider",
54
+ "identity_info",
55
+ "public_key_b64",
56
+ "sign",
57
+ "sign_manifest",
58
+ "status",
59
+ "store_dir",
60
+ "verify",
61
+ "verify_manifest",
62
+ ]
matrixscroll/_core.py ADDED
@@ -0,0 +1,360 @@
1
+ """Matrix Scroll root of trust — Ed25519 device identity and signing.
2
+
3
+ This is the "software emulation first" layer for the Matrix Scroll hardware key.
4
+ The same API serves the local emulator today and the physical NXP SE050 device
5
+ later, selected via the MATRIXSCROLL_MODE environment variable.
6
+
7
+ Security contract:
8
+ - Private keys never leave the provider. Public callers only ever see the
9
+ public key, the derived device id, and signatures.
10
+ - In emulated mode the private seed is stored locally under
11
+ ~/.matrixscroll/device.json. The directory is created 0700 and the file is
12
+ opened 0600 at creation time (never write-then-chmod), so the seed is never
13
+ momentarily world-readable. A corrupt store fails loud rather than silently
14
+ re-minting identity. On real hardware the seed is sealed in the secure
15
+ element and this file holds only public material.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import binascii
22
+ import copy
23
+ import hashlib
24
+ import json
25
+ import os
26
+ import time
27
+ from abc import ABC, abstractmethod
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ from cryptography.exceptions import InvalidSignature
32
+ from cryptography.hazmat.primitives import serialization
33
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
34
+ Ed25519PrivateKey,
35
+ Ed25519PublicKey,
36
+ )
37
+
38
+ SCHEMA = "matrixscroll.identity.v1"
39
+ SIGNATURE_SCHEMA = "matrixscroll.signature.v1"
40
+ ALGORITHM = "ed25519"
41
+ DEVICE_FILE = "device.json"
42
+
43
+ _RAW = serialization.Encoding.Raw
44
+ _PRIV_RAW = serialization.PrivateFormat.Raw
45
+ _PUB_RAW = serialization.PublicFormat.Raw
46
+ _NOENC = serialization.NoEncryption()
47
+
48
+ SEED_LEN = 32
49
+ DIR_MODE = 0o700
50
+ FILE_MODE = 0o600
51
+
52
+
53
+ class IdentityError(Exception):
54
+ """Raised when the device key store cannot be read or is untrustworthy."""
55
+
56
+
57
+ def store_dir() -> Path:
58
+ """Resolve the device store directory (override via MATRIXSCROLL_HOME)."""
59
+ env = os.environ.get("MATRIXSCROLL_HOME", "").strip()
60
+ base = Path(env).expanduser() if env else (Path.home() / ".matrixscroll")
61
+ return base
62
+
63
+
64
+ def _b64(data: bytes) -> str:
65
+ return base64.b64encode(data).decode("ascii")
66
+
67
+
68
+ def _unb64(value: str) -> bytes:
69
+ return base64.b64decode(value.encode("ascii"), validate=True)
70
+
71
+
72
+ def _write_secret(path: Path, text: str) -> None:
73
+ """Write ``text`` to ``path`` created with owner-only (0o600) permissions.
74
+
75
+ The file is opened with O_CREAT|O_EXCL via os.open so the private seed is
76
+ never momentarily world-readable (which a write-then-chmod sequence allows).
77
+ On Windows the POSIX mode is advisory, but O_EXCL still prevents clobbering.
78
+ """
79
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
80
+ fd = os.open(str(path), flags, FILE_MODE)
81
+ try:
82
+ os.write(fd, text.encode("utf-8"))
83
+ finally:
84
+ os.close(fd)
85
+ try:
86
+ os.chmod(path, FILE_MODE)
87
+ except OSError:
88
+ pass
89
+
90
+
91
+ def device_id(public_key: bytes) -> str:
92
+ """Derive a stable, human-readable id (MS-XXXX-XXXX) from a public key."""
93
+ digest = hashlib.sha256(public_key).hexdigest().upper()
94
+ return f"MS-{digest[:4]}-{digest[4:8]}"
95
+
96
+
97
+ class IdentityProvider(ABC):
98
+ """A root-of-trust provider. Signing happens here; keys never escape."""
99
+
100
+ mode: str = "unknown"
101
+
102
+ @abstractmethod
103
+ def public_key_bytes(self) -> bytes:
104
+ ...
105
+
106
+ @abstractmethod
107
+ def sign(self, data: bytes) -> bytes:
108
+ ...
109
+
110
+ @property
111
+ def created_at(self) -> str:
112
+ return ""
113
+
114
+ def is_available(self) -> tuple[bool, str | None]:
115
+ """Whether this provider can actually serve keys/signatures right now.
116
+
117
+ Returns ``(True, None)`` when usable; ``(False, reason)`` when the
118
+ provider is selected but not yet operational (e.g. the hardware stub).
119
+ Soft status surfaces use this to avoid crashing read-only endpoints
120
+ when the SE050 path is not yet wired; signing paths still raise loud.
121
+ """
122
+ return True, None
123
+
124
+
125
+ class EmulatedProvider(IdentityProvider):
126
+ """Software Matrix Scroll. Holds an Ed25519 key in process memory."""
127
+
128
+ mode = "emulated"
129
+
130
+ def __init__(self, private_key: Ed25519PrivateKey, created_at: str) -> None:
131
+ self._key = private_key
132
+ self._created_at = created_at
133
+
134
+ @classmethod
135
+ def load_or_create(cls, directory: Path | None = None) -> "EmulatedProvider":
136
+ directory = directory or store_dir()
137
+ path = directory / DEVICE_FILE
138
+ if path.is_file():
139
+ return cls._load(path)
140
+
141
+ key = Ed25519PrivateKey.generate()
142
+ created = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
143
+ pub = key.public_key().public_bytes(_RAW, _PUB_RAW)
144
+ doc = {
145
+ "schema": SCHEMA,
146
+ "mode": cls.mode,
147
+ "created_at": created,
148
+ "device_id": device_id(pub),
149
+ "public_key": _b64(pub),
150
+ "private_key": _b64(key.private_bytes(_RAW, _PRIV_RAW, _NOENC)),
151
+ }
152
+ directory.mkdir(parents=True, exist_ok=True)
153
+ try:
154
+ os.chmod(directory, DIR_MODE)
155
+ except OSError:
156
+ pass
157
+ _write_secret(path, json.dumps(doc, indent=2) + "\n")
158
+ return cls(key, created)
159
+
160
+ @classmethod
161
+ def _load(cls, path: Path) -> "EmulatedProvider":
162
+ """Load an existing key store, failing loud (never re-minting) if it is
163
+ corrupt so a tampered or truncated file cannot silently rotate identity."""
164
+ try:
165
+ doc = json.loads(path.read_text(encoding="utf-8"))
166
+ seed = _unb64(doc["private_key"])
167
+ except (OSError, ValueError, KeyError, AttributeError, binascii.Error) as exc:
168
+ raise IdentityError(f"device key store at {path} is unreadable: {exc}")
169
+ if len(seed) != SEED_LEN:
170
+ raise IdentityError(
171
+ f"device key store at {path} has an invalid {len(seed)}-byte seed"
172
+ )
173
+ try:
174
+ key = Ed25519PrivateKey.from_private_bytes(seed)
175
+ except ValueError as exc:
176
+ raise IdentityError(f"device key store at {path} is corrupt: {exc}")
177
+ return cls(key, doc.get("created_at", ""))
178
+
179
+ def public_key_bytes(self) -> bytes:
180
+ return self._key.public_key().public_bytes(_RAW, _PUB_RAW)
181
+
182
+ def sign(self, data: bytes) -> bytes:
183
+ return self._key.sign(data)
184
+
185
+ @property
186
+ def created_at(self) -> str:
187
+ return self._created_at
188
+
189
+
190
+ class HardwareProvider(IdentityProvider):
191
+ """Stub for the physical NXP SE050 device (not yet wired)."""
192
+
193
+ mode = "hardware"
194
+ UNAVAILABLE_REASON = (
195
+ "Matrix Scroll hardware provider is not available yet. "
196
+ "Use MATRIXSCROLL_MODE=emulated (default) for the software key."
197
+ )
198
+
199
+ def is_available(self) -> tuple[bool, str | None]:
200
+ return False, self.UNAVAILABLE_REASON
201
+
202
+ def public_key_bytes(self) -> bytes: # pragma: no cover - hardware path
203
+ raise NotImplementedError(self.UNAVAILABLE_REASON)
204
+
205
+ def sign(self, data: bytes) -> bytes: # pragma: no cover - hardware path
206
+ raise NotImplementedError(self.UNAVAILABLE_REASON)
207
+
208
+
209
+ _PROVIDER: IdentityProvider | None = None
210
+
211
+
212
+ def get_provider(*, refresh: bool = False) -> IdentityProvider:
213
+ """Return the active root-of-trust provider (cached).
214
+
215
+ Mode is chosen by MATRIXSCROLL_MODE (default "emulated"). The hardware path
216
+ is reserved for the physical device.
217
+ """
218
+ global _PROVIDER
219
+ if _PROVIDER is not None and not refresh:
220
+ return _PROVIDER
221
+ mode = os.environ.get("MATRIXSCROLL_MODE", "emulated").strip().lower()
222
+ if mode == "hardware":
223
+ _PROVIDER = HardwareProvider()
224
+ else:
225
+ _PROVIDER = EmulatedProvider.load_or_create()
226
+ return _PROVIDER
227
+
228
+
229
+ def public_key_b64(provider: IdentityProvider | None = None) -> str:
230
+ provider = provider or get_provider()
231
+ return _b64(provider.public_key_bytes())
232
+
233
+
234
+ def identity_info(provider: IdentityProvider | None = None) -> dict[str, Any]:
235
+ """Public-only identity material. Never includes the private key."""
236
+ provider = provider or get_provider()
237
+ pub = provider.public_key_bytes()
238
+ return {
239
+ "schema": SCHEMA,
240
+ "device_id": device_id(pub),
241
+ "public_key": _b64(pub),
242
+ "algorithm": ALGORITHM,
243
+ "mode": provider.mode,
244
+ "created_at": provider.created_at,
245
+ }
246
+
247
+
248
+ def status(provider: IdentityProvider | None = None) -> dict[str, Any]:
249
+ """Soft identity status for read-only surfaces (e.g. an HTTP API).
250
+
251
+ Mirrors :func:`identity_info` when the provider is available and adds an
252
+ ``available`` flag. When the provider is selected but not yet operational
253
+ (the hardware stub today), returns ``available=False`` with a ``reason``
254
+ and no key material instead of raising — so dashboards stay green when the
255
+ SSX360 device path is configured before the SE050 transport is wired.
256
+ """
257
+ provider = provider or get_provider()
258
+ available, reason = provider.is_available()
259
+ base: dict[str, Any] = {
260
+ "schema": SCHEMA,
261
+ "available": available,
262
+ "algorithm": ALGORITHM,
263
+ "mode": provider.mode,
264
+ "created_at": provider.created_at,
265
+ }
266
+ if not available:
267
+ base["reason"] = reason
268
+ return base
269
+ pub = provider.public_key_bytes()
270
+ base["device_id"] = device_id(pub)
271
+ base["public_key"] = _b64(pub)
272
+ return base
273
+
274
+
275
+ def sign(data: bytes, provider: IdentityProvider | None = None) -> bytes:
276
+ provider = provider or get_provider()
277
+ return provider.sign(data)
278
+
279
+
280
+ def verify(public_key: str | bytes, data: bytes, signature: str | bytes) -> bool:
281
+ """Verify a signature against a public key. Returns False on any mismatch."""
282
+ try:
283
+ pub = public_key if isinstance(public_key, bytes) else _unb64(public_key)
284
+ sig = signature if isinstance(signature, bytes) else _unb64(signature)
285
+ Ed25519PublicKey.from_public_bytes(pub).verify(sig, data)
286
+ return True
287
+ except (InvalidSignature, ValueError, TypeError, AttributeError, binascii.Error):
288
+ return False
289
+
290
+
291
+ def _canonical(payload: dict[str, Any]) -> bytes:
292
+ """Deterministic bytes for signing, identical across platforms and runs.
293
+
294
+ Contract: the top-level ``signature`` block is excluded; keys are sorted
295
+ recursively; whitespace is stripped; non-ASCII is escaped (``ensure_ascii``)
296
+ so byte output never depends on locale or terminal encoding; and NaN/Infinity
297
+ are rejected (``allow_nan=False``) because they have no portable JSON form and
298
+ would otherwise produce signatures other verifiers cannot reproduce.
299
+ """
300
+ body = {k: v for k, v in payload.items() if k != "signature"}
301
+ return json.dumps(
302
+ body,
303
+ sort_keys=True,
304
+ ensure_ascii=True,
305
+ allow_nan=False,
306
+ separators=(",", ":"),
307
+ ).encode("utf-8")
308
+
309
+
310
+ def sign_manifest(
311
+ manifest: dict[str, Any], provider: IdentityProvider | None = None
312
+ ) -> dict[str, Any]:
313
+ """Return a copy of ``manifest`` with a Matrix Scroll signature block attached."""
314
+ provider = provider or get_provider()
315
+ info = identity_info(provider)
316
+ signed = copy.deepcopy(manifest)
317
+ signed.pop("signature", None)
318
+ signature_value = _b64(provider.sign(_canonical(signed)))
319
+ signed["signature"] = {
320
+ "schema": SIGNATURE_SCHEMA,
321
+ "algorithm": ALGORITHM,
322
+ "device_id": info["device_id"],
323
+ "public_key": info["public_key"],
324
+ "mode": info["mode"],
325
+ "signed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
326
+ "value": signature_value,
327
+ }
328
+ return signed
329
+
330
+
331
+ def verify_manifest(manifest: dict[str, Any]) -> bool:
332
+ """Verify a manifest produced by :func:`sign_manifest`.
333
+
334
+ Malformed signature blocks return ``False`` instead of raising. Version and
335
+ algorithm checks intentionally happen before the cryptographic verify so a
336
+ future schema cannot be accidentally accepted under v1 rules.
337
+ """
338
+ if not isinstance(manifest, dict):
339
+ return False
340
+ block = manifest.get("signature")
341
+ if not isinstance(block, dict):
342
+ return False
343
+ if block.get("schema") != SIGNATURE_SCHEMA or block.get("algorithm") != ALGORITHM:
344
+ return False
345
+ public_key = block.get("public_key")
346
+ signature = block.get("value")
347
+ if not isinstance(public_key, str) or not isinstance(signature, str):
348
+ return False
349
+ try:
350
+ public_key_bytes = _unb64(public_key)
351
+ except (ValueError, binascii.Error):
352
+ return False
353
+ if block.get("device_id") != device_id(public_key_bytes):
354
+ return False
355
+ try:
356
+ signing_input = _canonical(manifest)
357
+ except (TypeError, ValueError):
358
+ return False
359
+ return verify(public_key_bytes, signing_input, signature)
360
+
matrixscroll/cli.py ADDED
@@ -0,0 +1,94 @@
1
+ """Command-line interface for Matrix Scroll.
2
+
3
+ Exposed as the ``matrixscroll`` console script. Kept dependency-free
4
+ (argparse + json) so it works without spinning up a host application —
5
+ useful for support sessions and release-evidence verification.
6
+
7
+ Subcommands:
8
+ status Print the active provider status as JSON.
9
+ verify <manifest.json> Verify a signed manifest; exits 0 on pass, 2 on fail.
10
+ sign <manifest.json> Sign a manifest from disk; prints the signed JSON.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ from ._core import sign_manifest, status, verify_manifest
21
+
22
+
23
+ def _cmd_status(_args: argparse.Namespace) -> int:
24
+ print(json.dumps(status(), indent=2, sort_keys=True))
25
+ return 0
26
+
27
+
28
+ def _cmd_verify(args: argparse.Namespace) -> int:
29
+ path = Path(args.manifest)
30
+ try:
31
+ manifest = json.loads(path.read_text(encoding="utf-8-sig"))
32
+ except (OSError, ValueError) as exc:
33
+ print(json.dumps({"ok": False, "error": f"cannot read manifest: {exc}"}))
34
+ return 2
35
+ ok = verify_manifest(manifest)
36
+ block = manifest.get("signature") or {}
37
+ print(json.dumps({
38
+ "ok": ok,
39
+ "device_id": block.get("device_id"),
40
+ "mode": block.get("mode"),
41
+ "signed_at": block.get("signed_at"),
42
+ }, sort_keys=True))
43
+ return 0 if ok else 2
44
+
45
+
46
+ def _cmd_sign(args: argparse.Namespace) -> int:
47
+ path = Path(args.manifest)
48
+ try:
49
+ manifest = json.loads(path.read_text(encoding="utf-8-sig"))
50
+ except (OSError, ValueError) as exc:
51
+ print(json.dumps({"ok": False, "error": f"cannot read manifest: {exc}"}))
52
+ return 2
53
+ signed = sign_manifest(manifest)
54
+ print(json.dumps(signed, indent=2, sort_keys=True))
55
+ return 0
56
+
57
+
58
+ def build_parser() -> argparse.ArgumentParser:
59
+ parser = argparse.ArgumentParser(
60
+ prog="matrixscroll",
61
+ description="Matrix Scroll / SSX360 root-of-trust CLI",
62
+ )
63
+ sub = parser.add_subparsers(dest="command")
64
+
65
+ sub.add_parser("status", help="Print active provider status as JSON")
66
+
67
+ verify_p = sub.add_parser("verify", help="Verify a signed manifest JSON file")
68
+ verify_p.add_argument("manifest", help="Path to a signed manifest produced by sign_manifest")
69
+
70
+ sign_p = sub.add_parser("sign", help="Sign a manifest JSON file with the active provider")
71
+ sign_p.add_argument("manifest", help="Path to a manifest JSON file to sign")
72
+
73
+ return parser
74
+
75
+
76
+ def main(argv: list[str] | None = None) -> int:
77
+ parser = build_parser()
78
+ args = parser.parse_args(argv)
79
+
80
+ handlers = {
81
+ None: _cmd_status,
82
+ "status": _cmd_status,
83
+ "verify": _cmd_verify,
84
+ "sign": _cmd_sign,
85
+ }
86
+ handler = handlers.get(args.command)
87
+ if handler is None:
88
+ parser.print_help()
89
+ return 1
90
+ return handler(args)
91
+
92
+
93
+ if __name__ == "__main__":
94
+ sys.exit(main())
matrixscroll/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: matrixscroll
3
+ Version: 0.1.0
4
+ Summary: Open protocol for hardware-signed AI-assisted code (Ed25519 root of trust, software emulator + SSX360 reference device).
5
+ Project-URL: Homepage, https://matrixscroll.com
6
+ Project-URL: Documentation, https://matrixscroll.com/docs
7
+ Project-URL: Source, https://github.com/SSX360/matrixscroll
8
+ Project-URL: Issues, https://github.com/SSX360/matrixscroll/issues
9
+ Project-URL: Specification, https://github.com/SSX360/matrixscroll/blob/main/SPEC.md
10
+ Project-URL: Reference Device, https://matrixscroll.com/device
11
+ Author-email: SSX360 <security@matrixscroll.com>
12
+ License-Expression: Apache-2.0
13
+ License-File: LICENSE
14
+ Keywords: ai,ed25519,matrixscroll,provenance,root-of-trust,secure-element,signing,ssx360,supply-chain
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Information Technology
18
+ Classifier: License :: OSI Approved :: Apache Software License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Topic :: Security :: Cryptography
27
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: cryptography>=41.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.0; extra == 'dev'
33
+ Requires-Dist: pytest>=7.4; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Matrix Scroll
37
+
38
+ **Open protocol for hardware-signed AI-assisted code.**
39
+
40
+ Every AI-generated change in your IDE gets cryptographically signed by an
41
+ Ed25519 key sealed in a hardware root of trust. Anyone can verify the result
42
+ offline with a public key and one command.
43
+
44
+ - 📜 **Spec:** [`SPEC.md`](SPEC.md) — wire format, canonical encoding, schemas.
45
+ - 🛡 **Agentic AI controls:** [`docs/AGENTIC_AI_SECURITY.md`](docs/AGENTIC_AI_SECURITY.md)
46
+ maps Matrix Scroll to the joint *Careful Adoption of Agentic AI Services* guidance.
47
+ - 🔐 **Algorithm:** Ed25519 (RFC 8032). Keys never leave the provider.
48
+ - 🧪 **Conformance vectors:** [`vectors/`](vectors/) — for non-Python implementations.
49
+ - 🌐 **Site:** <https://matrixscroll.com>
50
+ - 🔧 **Reference device:** [SSX360](https://matrixscroll.com/device) (NXP SE050).
51
+
52
+ ```bash
53
+ pip install matrixscroll
54
+ ```
55
+
56
+ ## Quickstart
57
+
58
+ ```python
59
+ import matrixscroll
60
+
61
+ # What identity is active on this machine?
62
+ print(matrixscroll.status())
63
+ # {'schema': 'matrixscroll.identity.v1', 'available': True,
64
+ # 'mode': 'emulated', 'device_id': 'MS-A3F2-9C81', ...}
65
+
66
+ # Sign anything (a release manifest, a commit envelope, a SBOM, an evidence pack)
67
+ signed = matrixscroll.sign_manifest({"release": "v1.0.0", "artifacts": [...]})
68
+
69
+ # Verify, anywhere, offline
70
+ assert matrixscroll.verify_manifest(signed)
71
+ ```
72
+
73
+ ## CLI
74
+
75
+ ```bash
76
+ $ matrixscroll status
77
+ {
78
+ "available": true,
79
+ "device_id": "MS-A3F2-9C81",
80
+ "mode": "emulated",
81
+ "public_key": "...",
82
+ "schema": "matrixscroll.identity.v1"
83
+ }
84
+
85
+ $ matrixscroll sign release.json > release.signed.json
86
+ $ matrixscroll verify release.signed.json
87
+ {"device_id": "MS-A3F2-9C81", "mode": "emulated", "ok": true, "signed_at": "..."}
88
+ ```
89
+
90
+ `matrixscroll verify` exits **0** on a valid signature, **2** on any failure
91
+ (tampered manifest, missing signature block, wrong schema/algorithm, mismatched
92
+ device id, malformed public key, unreadable file). Pipe it from CI without
93
+ parsing the output.
94
+
95
+ ## How it works
96
+
97
+ ```
98
+ your IDE / agent / CI
99
+
100
+ │ manifest (release, commit, evidence pack, SBOM, anything)
101
+
102
+ matrixscroll.sign_manifest(...)
103
+
104
+ │ canonical JSON (sorted keys, ASCII-escaped, no NaN,
105
+ │ signature block excluded from input)
106
+
107
+ IdentityProvider ──► Ed25519 signature
108
+ (Emulated today,
109
+ SSX360 / SE050 tomorrow)
110
+
111
+
112
+ signed manifest ──► matrixscroll.verify_manifest(...)
113
+ (anyone, anywhere, offline)
114
+ ```
115
+
116
+ The same Python API serves the local software emulator and the physical
117
+ SSX360 device. Switch with the `MATRIXSCROLL_MODE` environment variable.
118
+
119
+ ## Compliance levels
120
+
121
+ | Level | Provider | Backed by | Status |
122
+ | ----- | -------- | --------- | ------ |
123
+ | **L1** Emulated | `EmulatedProvider` | Software key, file-backed (0600) | ✅ Shipping |
124
+ | **L2** Hardware | `HardwareProvider` | NXP SE050 secure element (SSX360) | 🛠 Stage-0 prototype |
125
+ | **L3** Attested | future | L2 + remote attestation | 🗺 Roadmap |
126
+
127
+ `status()` exposes the active level via the `mode` and `available` fields so
128
+ read-only dashboards can render before the hardware path is wired.
129
+
130
+ ## Storage and trust boundaries
131
+
132
+ - Emulated key store: `~/.matrixscroll/device.json`
133
+ (override with `MATRIXSCROLL_HOME`).
134
+ - The directory is created `0700`; the seed file is opened `0600` with
135
+ `O_CREAT|O_EXCL` so the private seed is never momentarily world-readable and
136
+ a race cannot silently clobber an existing key store.
137
+ - A corrupt or truncated store **fails loud** (`IdentityError`) rather than
138
+ silently minting a fresh identity. Identity rotation is an explicit operation.
139
+ - The hardware path holds nothing private on disk — the seed is sealed in the
140
+ secure element.
141
+
142
+ ## Reference implementation, not the only one
143
+
144
+ Matrix Scroll is a protocol. This Python package is the reference. We welcome
145
+ implementations in Rust, Go, TypeScript, and embedded C — run them against
146
+ [`vectors/`](vectors/) to self-certify. See `CONTRIBUTING.md`.
147
+
148
+ ## Agentic AI guidance proof
149
+
150
+ The repo includes a machine-readable control matrix at
151
+ [`controls/agentic_ai_controls.json`](controls/agentic_ai_controls.json), an
152
+ example bounded-agent evidence manifest at
153
+ [`examples/agentic_ai_evidence_manifest.json`](examples/agentic_ai_evidence_manifest.json),
154
+ and executable checks in `tests/test_agentic_guidance.py`. These prove each
155
+ claim maps to repo evidence and that signed agent scope changes fail verify.
156
+
157
+ ## License
158
+
159
+ - Code: **Apache-2.0** (`LICENSE`).
160
+ - Specification text (`SPEC.md`, `vectors/`): **CC0 1.0** — public domain.
161
+
162
+ ## Security
163
+
164
+ See [`SECURITY.md`](SECURITY.md). Report vulnerabilities privately to
165
+ **security@matrixscroll.com** or via a GitHub Security Advisory.
@@ -0,0 +1,9 @@
1
+ matrixscroll/__init__.py,sha256=xdcP2oVLj8Gjp4vJsNFn6_XkvaYqWI5FWyX1DGqryok,1368
2
+ matrixscroll/_core.py,sha256=UpBpNn36ntJDdRsEGXk8OMGY2On8PaeD3nHJoW0VSR8,12635
3
+ matrixscroll/cli.py,sha256=V9lZqfAFo7xuUA-PIb9E8bX3GV3uhhtt-QGAbXVAleI,2899
4
+ matrixscroll/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
+ matrixscroll-0.1.0.dist-info/METADATA,sha256=9-ZAjP6Vou5BTx3EJ0Y0X3jgOXRsZA0pQ8xNce5J-3s,6498
6
+ matrixscroll-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ matrixscroll-0.1.0.dist-info/entry_points.txt,sha256=QNuWZkuBxauvxVoTNjmTCpeTErw974lJ8WyyrzFa7JI,55
8
+ matrixscroll-0.1.0.dist-info/licenses/LICENSE,sha256=tEVbK2foaOz1nfkv3jaom6_ojcEDJIKoSzfRfAZDELQ,11342
9
+ matrixscroll-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ matrixscroll = matrixscroll.cli:main
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, notice, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for describing the origin of the Work and
142
+ reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 SSX360 / Matrix Scroll contributors
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.