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.
- matrixscroll/__init__.py +62 -0
- matrixscroll/_core.py +360 -0
- matrixscroll/cli.py +94 -0
- matrixscroll/py.typed +1 -0
- matrixscroll-0.1.0.dist-info/METADATA +165 -0
- matrixscroll-0.1.0.dist-info/RECORD +9 -0
- matrixscroll-0.1.0.dist-info/WHEEL +4 -0
- matrixscroll-0.1.0.dist-info/entry_points.txt +2 -0
- matrixscroll-0.1.0.dist-info/licenses/LICENSE +202 -0
matrixscroll/__init__.py
ADDED
|
@@ -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,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.
|