astra-verify 0.3.1__tar.gz
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.
- astra_verify-0.3.1/MANIFEST.in +8 -0
- astra_verify-0.3.1/PKG-INFO +20 -0
- astra_verify-0.3.1/README.md +12 -0
- astra_verify-0.3.1/astra_verify/SELF_SHA256SUMS +11 -0
- astra_verify-0.3.1/astra_verify/SELF_SHA256SUMS.sig +2 -0
- astra_verify-0.3.1/astra_verify/SELF_publisher.pub +1 -0
- astra_verify-0.3.1/astra_verify/__init__.py +10 -0
- astra_verify-0.3.1/astra_verify/__main__.py +42 -0
- astra_verify-0.3.1/astra_verify/_selfcheck.py +55 -0
- astra_verify-0.3.1/astra_verify/_sig.py +21 -0
- astra_verify-0.3.1/astra_verify/roots/README.md +16 -0
- astra_verify-0.3.1/astra_verify/roots/v1/METADATA.json +5 -0
- astra_verify-0.3.1/astra_verify/roots/v1/publisher.sha256 +1 -0
- astra_verify-0.3.1/astra_verify/roots/v2/METADATA.json +5 -0
- astra_verify-0.3.1/astra_verify/roots/v2/publisher.sha256 +1 -0
- astra_verify-0.3.1/astra_verify/trust_root.py +59 -0
- astra_verify-0.3.1/astra_verify/verify.py +149 -0
- astra_verify-0.3.1/astra_verify.egg-info/PKG-INFO +20 -0
- astra_verify-0.3.1/astra_verify.egg-info/SOURCES.txt +36 -0
- astra_verify-0.3.1/astra_verify.egg-info/dependency_links.txt +1 -0
- astra_verify-0.3.1/astra_verify.egg-info/requires.txt +1 -0
- astra_verify-0.3.1/astra_verify.egg-info/top_level.txt +1 -0
- astra_verify-0.3.1/pyproject.toml +27 -0
- astra_verify-0.3.1/setup.cfg +4 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astra-verify
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Independent verifier for astra-substrate releases
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: cryptography>=42
|
|
8
|
+
|
|
9
|
+
# astra-verify
|
|
10
|
+
|
|
11
|
+
Independent verifier for astra-substrate releases.
|
|
12
|
+
|
|
13
|
+
- Ships a pinned trust root (`roots/publisher.sha256`)
|
|
14
|
+
- Verifies wheel hash, manifest signature, SBOM signature
|
|
15
|
+
- Imports no `astra.*`
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
pip install astra-verify
|
|
20
|
+
astra-verify --dist /path/to/astra_substrate/dist
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# astra-verify
|
|
2
|
+
|
|
3
|
+
Independent verifier for astra-substrate releases.
|
|
4
|
+
|
|
5
|
+
- Ships a pinned trust root (`roots/publisher.sha256`)
|
|
6
|
+
- Verifies wheel hash, manifest signature, SBOM signature
|
|
7
|
+
- Imports no `astra.*`
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
pip install astra-verify
|
|
12
|
+
astra-verify --dist /path/to/astra_substrate/dist
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
3ba45c952bf17b238fe337098dc5a09fc2cb99b95797ed9a380c99988716cc32 __init__.py
|
|
2
|
+
6ba16144c4d10347235f191eb90460b67ec14e8243831e34a912ee1a0807e0d5 __main__.py
|
|
3
|
+
d8bc7242c880652b49071b3bbb904834bf1dd76982a7b23b4502ac45abef7b4d _selfcheck.py
|
|
4
|
+
411dde7d1e7b49a685ebdab32ec574d1bbe14ca3ce4ef387853d8c17bb9e2c50 _sig.py
|
|
5
|
+
e5faf66314ce5583aeb19b1265ed4ef8883f9245470b324fcf26b99bda90f99e roots/README.md
|
|
6
|
+
657c5fd0241a7b0d52788ac00b478f32491fa98d3a58e176a0ddeab2f2b830ff roots/v1/METADATA.json
|
|
7
|
+
7fe21f93c60e8a2ffe8fb5c7fae92bb4da2c0a4cc8a6556ac122452eb0f60a79 roots/v1/publisher.sha256
|
|
8
|
+
d75c2344ca2e2cdbe082d2702c903f2480399a20d5d532952d030e1404d6000a roots/v2/METADATA.json
|
|
9
|
+
7fe21f93c60e8a2ffe8fb5c7fae92bb4da2c0a4cc8a6556ac122452eb0f60a79 roots/v2/publisher.sha256
|
|
10
|
+
98d108e5f49ee36d811baa915a74ffa39c1be684042caac196a7737c8d23de76 trust_root.py
|
|
11
|
+
94f13e1c756de9babe1d74d99cdd9fb4bf851d87e488d1364ae2d950ffc6d55f verify.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
�}N��Q����K����R�E��0�ڤ����عc
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from astra_verify.verify import verify_release, VerifyResult, VerifyReport
|
|
2
|
+
|
|
3
|
+
__all__ = ["verify_release", "VerifyResult", "VerifyReport"]
|
|
4
|
+
|
|
5
|
+
import os as _os
|
|
6
|
+
if _os.environ.get("ASTRA_VERIFY_SELFCHECK") == "1":
|
|
7
|
+
from astra_verify._selfcheck import check as _selfcheck
|
|
8
|
+
_ok, _detail = _selfcheck()
|
|
9
|
+
if not _ok:
|
|
10
|
+
raise RuntimeError(f"astra-verify self-check failed: {_detail}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import argparse, hashlib, json, sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from astra_verify.verify import verify_release
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> int:
|
|
9
|
+
ap = argparse.ArgumentParser(prog="astra-verify")
|
|
10
|
+
ap.add_argument("--dist", type=Path, default=Path("dist"))
|
|
11
|
+
ap.add_argument("--out", type=Path, default=None)
|
|
12
|
+
ap.add_argument("--status", action="store_true",
|
|
13
|
+
help="Print trust-root status and exit")
|
|
14
|
+
ap.add_argument("--bootstrap-root", type=Path, default=None,
|
|
15
|
+
help="Add a publisher pubkey to the trust root")
|
|
16
|
+
a = ap.parse_args()
|
|
17
|
+
|
|
18
|
+
if a.status:
|
|
19
|
+
from astra_verify.trust_root import status
|
|
20
|
+
print(json.dumps(status(), indent=2))
|
|
21
|
+
return 0
|
|
22
|
+
if a.bootstrap_root:
|
|
23
|
+
fp = hashlib.sha256(a.bootstrap_root.read_bytes()).hexdigest()
|
|
24
|
+
roots = Path(__file__).parent / "roots"
|
|
25
|
+
roots.mkdir(exist_ok=True)
|
|
26
|
+
p = roots / "publisher.sha256"
|
|
27
|
+
existing = p.read_text() if p.exists() else ""
|
|
28
|
+
if fp not in existing:
|
|
29
|
+
p.write_text(existing + fp + "\n")
|
|
30
|
+
print(f"bootstrapped root: {fp}")
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
rep = verify_release(a.dist)
|
|
34
|
+
body = rep.to_json()
|
|
35
|
+
if a.out:
|
|
36
|
+
a.out.write_text(json.dumps(body, indent=2))
|
|
37
|
+
print(json.dumps(body, indent=2))
|
|
38
|
+
return 0 if rep.ok else 2
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
sys.exit(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Verify astra-verify's own wheel against its embedded signed manifest.
|
|
2
|
+
|
|
3
|
+
Runs at import time if ASTRA_VERIFY_SELFCHECK=1. Non-blocking by default
|
|
4
|
+
so a broken manifest doesn't lock the user out of a repair.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import hashlib, json, os, sys
|
|
8
|
+
from importlib.metadata import distribution
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from astra_verify._sig import verify_file, SigningError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check() -> tuple[bool, str]:
|
|
15
|
+
try:
|
|
16
|
+
d = distribution("astra-verify")
|
|
17
|
+
except Exception as e:
|
|
18
|
+
return False, f"distribution not found: {e}"
|
|
19
|
+
site = Path(d.locate_file("astra_verify"))
|
|
20
|
+
sums = site / "SELF_SHA256SUMS"
|
|
21
|
+
sig = site / "SELF_SHA256SUMS.sig"
|
|
22
|
+
pub = site / "SELF_publisher.pub"
|
|
23
|
+
if not (sums.exists() and sig.exists() and pub.exists()):
|
|
24
|
+
return False, "self-check artifacts missing from install"
|
|
25
|
+
try:
|
|
26
|
+
verify_file(sums, pub, sig)
|
|
27
|
+
except SigningError as e:
|
|
28
|
+
return False, f"self-manifest signature invalid: {e}"
|
|
29
|
+
entries = {}
|
|
30
|
+
for line in sums.read_text().splitlines():
|
|
31
|
+
if not line.strip(): continue
|
|
32
|
+
h, _, rel = line.partition(" ")
|
|
33
|
+
entries[rel.strip()] = h.strip()
|
|
34
|
+
errors = []
|
|
35
|
+
for rel, expected in entries.items():
|
|
36
|
+
p = site / rel
|
|
37
|
+
if not p.exists():
|
|
38
|
+
errors.append(f"missing {rel}")
|
|
39
|
+
continue
|
|
40
|
+
actual = hashlib.sha256(p.read_bytes()).hexdigest()
|
|
41
|
+
if actual != expected:
|
|
42
|
+
errors.append(f"tampered {rel}")
|
|
43
|
+
if errors:
|
|
44
|
+
return False, "; ".join(errors)
|
|
45
|
+
return True, f"self-check ok, {len(entries)} files verified"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> int:
|
|
49
|
+
ok, detail = check()
|
|
50
|
+
print(f"self-check: {'ok' if ok else 'FAIL'}: {detail}")
|
|
51
|
+
return 0 if ok else 2
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
sys.exit(main())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Ed25519 verification, self-contained. No trust.* imports."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from cryptography.exceptions import InvalidSignature
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SigningError(RuntimeError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def verify_file(path: Path, pub_path: Path, sig_path: Path) -> None:
|
|
14
|
+
pub_bytes = pub_path.read_bytes()
|
|
15
|
+
sig_bytes = sig_path.read_bytes()
|
|
16
|
+
try:
|
|
17
|
+
Ed25519PublicKey.from_public_bytes(pub_bytes).verify(sig_bytes, path.read_bytes())
|
|
18
|
+
except InvalidSignature as e:
|
|
19
|
+
raise SigningError(f"signature invalid: {e}") from e
|
|
20
|
+
except Exception as e:
|
|
21
|
+
raise SigningError(f"{type(e).__name__}: {e}") from e
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Trust roots for astra-verify
|
|
2
|
+
|
|
3
|
+
Each versioned directory (`v1/`, `v2/`, …) is a snapshot of fingerprints
|
|
4
|
+
trusted at that point in time. Adding a new root means creating a new
|
|
5
|
+
versioned directory, never mutating an existing one.
|
|
6
|
+
|
|
7
|
+
## Format
|
|
8
|
+
- `vN/publisher.sha256` — one SHA-256 hex per line of trusted publisher pubkeys
|
|
9
|
+
- `vN/METADATA.json` — { "added": ISO8601, "deprecates": "vN-1" | null, "reason": "..." }
|
|
10
|
+
|
|
11
|
+
## Deprecation
|
|
12
|
+
A root is deprecated when a successor root names it in "deprecates".
|
|
13
|
+
`astra_verify.trust_root.load()` reads all roots and unions the fingerprints
|
|
14
|
+
from non-deprecated versions plus the newest version. Deprecated roots stop
|
|
15
|
+
accepting new signatures but remain valid for artifacts published under them
|
|
16
|
+
until the deprecation date + 90 days.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fc20cf1cd39d7c4e0f7a9dbce3bdc3568dd84277e97b172b3767275dbb4e75de
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fc20cf1cd39d7c4e0f7a9dbce3bdc3568dd84277e97b172b3767275dbb4e75de
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Pinned trust root for astra-verify. Loads versioned roots."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import hashlib, json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ROOTS_DIR = Path(__file__).parent / "roots"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _load_versioned() -> tuple[tuple[str, ...], set[str]]:
|
|
10
|
+
"""Return (active_fingerprints, deprecated_versions)."""
|
|
11
|
+
if not ROOTS_DIR.exists():
|
|
12
|
+
return (), set()
|
|
13
|
+
versions = sorted(
|
|
14
|
+
(p for p in ROOTS_DIR.iterdir() if p.is_dir() and p.name.startswith("v")),
|
|
15
|
+
key=lambda p: int(p.name[1:]) if p.name[1:].isdigit() else 0,
|
|
16
|
+
)
|
|
17
|
+
deprecated: set[str] = set()
|
|
18
|
+
meta_by_version: dict[str, dict] = {}
|
|
19
|
+
for v in versions:
|
|
20
|
+
m = v / "METADATA.json"
|
|
21
|
+
if m.exists():
|
|
22
|
+
meta_by_version[v.name] = json.loads(m.read_text())
|
|
23
|
+
dep = meta_by_version[v.name].get("deprecates")
|
|
24
|
+
if dep:
|
|
25
|
+
deprecated.add(dep)
|
|
26
|
+
active: list[str] = []
|
|
27
|
+
for v in versions:
|
|
28
|
+
if v.name in deprecated:
|
|
29
|
+
continue
|
|
30
|
+
f = v / "publisher.sha256"
|
|
31
|
+
if f.exists():
|
|
32
|
+
for line in f.read_text().splitlines():
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if line and not line.startswith("#"):
|
|
35
|
+
active.append(line.lower())
|
|
36
|
+
# dedupe while preserving order
|
|
37
|
+
seen: set[str] = set()
|
|
38
|
+
uniq: list[str] = []
|
|
39
|
+
for fp in active:
|
|
40
|
+
if fp not in seen:
|
|
41
|
+
seen.add(fp)
|
|
42
|
+
uniq.append(fp)
|
|
43
|
+
return tuple(uniq), deprecated
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
PINNED_FINGERPRINTS, DEPRECATED_ROOTS = _load_versioned()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_trusted(pubkey_bytes: bytes) -> bool:
|
|
50
|
+
h = hashlib.sha256(pubkey_bytes).hexdigest()
|
|
51
|
+
return any(fp == h for fp in PINNED_FINGERPRINTS)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def status() -> dict:
|
|
55
|
+
return {
|
|
56
|
+
"active_fingerprints": list(PINNED_FINGERPRINTS),
|
|
57
|
+
"deprecated_roots": sorted(DEPRECATED_ROOTS),
|
|
58
|
+
"roots_dir": str(ROOTS_DIR),
|
|
59
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Independent verification of astra-substrate releases. No astra.* imports."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import hashlib, json, re, sys
|
|
4
|
+
from dataclasses import dataclass, asdict, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class VerifyResult:
|
|
10
|
+
name: str
|
|
11
|
+
ok: bool
|
|
12
|
+
detail: str
|
|
13
|
+
duration_ms: float = 0.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class VerifyReport:
|
|
18
|
+
version: str = "1"
|
|
19
|
+
checks: list[VerifyResult] = field(default_factory=list)
|
|
20
|
+
artifacts: dict[str, str] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def ok(self) -> bool:
|
|
24
|
+
return all(c.ok for c in self.checks)
|
|
25
|
+
|
|
26
|
+
def to_json(self) -> dict:
|
|
27
|
+
return {"version": self.version, "ok": self.ok,
|
|
28
|
+
"checks": [asdict(c) for c in self.checks],
|
|
29
|
+
"artifacts": self.artifacts}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _sha(p: Path) -> str:
|
|
33
|
+
h = hashlib.sha256()
|
|
34
|
+
with p.open("rb") as fh:
|
|
35
|
+
for c in iter(lambda: fh.read(1 << 20), b""):
|
|
36
|
+
h.update(c)
|
|
37
|
+
return h.hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _run(name: str, fn) -> VerifyResult:
|
|
41
|
+
import time
|
|
42
|
+
t0 = time.monotonic()
|
|
43
|
+
try:
|
|
44
|
+
ok, detail = fn()
|
|
45
|
+
except Exception as e:
|
|
46
|
+
ok, detail = False, f"exception: {type(e).__name__}: {e}"
|
|
47
|
+
return VerifyResult(name, ok, detail, round((time.monotonic()-t0)*1000, 2))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def verify_release(
|
|
51
|
+
dist: Path,
|
|
52
|
+
*,
|
|
53
|
+
require_trust_root: bool = True,
|
|
54
|
+
) -> VerifyReport:
|
|
55
|
+
from astra_verify.trust_root import is_trusted, PINNED_FINGERPRINTS
|
|
56
|
+
if require_trust_root and not PINNED_FINGERPRINTS:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
"no pinned trust roots — run `astra-verify --bootstrap-root <pubkey>` "
|
|
59
|
+
"or provide astra_verify/roots/publisher.sha256"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
rep = VerifyReport()
|
|
63
|
+
wheel = next(iter(dist.glob("astra_substrate-*-py3-none-any.whl")), None)
|
|
64
|
+
sdist = next(iter(dist.glob("astra_substrate-*.tar.gz")), None)
|
|
65
|
+
manifest = dist / "SHA256SUMS"
|
|
66
|
+
manifest_sig = dist / "SHA256SUMS.sig"
|
|
67
|
+
sbom = dist / "sbom.cdx.json"
|
|
68
|
+
sbom_sig = dist / "sbom.cdx.json.sig"
|
|
69
|
+
|
|
70
|
+
if wheel: rep.artifacts["wheel"] = _sha(wheel)
|
|
71
|
+
if sdist: rep.artifacts["sdist"] = _sha(sdist)
|
|
72
|
+
if sbom and sbom.exists(): rep.artifacts["sbom"] = _sha(sbom)
|
|
73
|
+
|
|
74
|
+
def check_artifacts_present():
|
|
75
|
+
missing = [n for n, p in [
|
|
76
|
+
("wheel", wheel), ("sdist", sdist), ("manifest", manifest),
|
|
77
|
+
("manifest_sig", manifest_sig), ("sbom", sbom), ("sbom_sig", sbom_sig),
|
|
78
|
+
] if p is None or not p.exists()]
|
|
79
|
+
if missing:
|
|
80
|
+
return False, f"missing: {missing}"
|
|
81
|
+
return True, "all 6 artifacts present"
|
|
82
|
+
rep.checks.append(_run("artifacts_present", check_artifacts_present))
|
|
83
|
+
|
|
84
|
+
def check_manifest_signature():
|
|
85
|
+
from astra_verify._sig import verify_file, SigningError
|
|
86
|
+
pub = next(dist.glob("*.pub"), None)
|
|
87
|
+
if pub is None:
|
|
88
|
+
return False, "no publisher pubkey in dist/"
|
|
89
|
+
if not is_trusted(pub.read_bytes()):
|
|
90
|
+
return False, "publisher pubkey not in pinned trust roots"
|
|
91
|
+
try:
|
|
92
|
+
verify_file(manifest, pub, manifest_sig)
|
|
93
|
+
except SigningError as e:
|
|
94
|
+
return False, f"manifest signature invalid: {e}"
|
|
95
|
+
return True, f"manifest signed by trusted {_sha(pub)[:16]}..."
|
|
96
|
+
def check_not_yanked():
|
|
97
|
+
rev_dir = Path(__file__).parent.parent.parent / "trust" / "revocations"
|
|
98
|
+
# installed package's revocation check: manifest may declare a yanked flag
|
|
99
|
+
data = json.loads(sbom.read_text())
|
|
100
|
+
if data.get("astra_yanked"):
|
|
101
|
+
return False, f"release marked yanked: {data.get('astra_yank_reason', 'unknown')}"
|
|
102
|
+
return True, "not yanked"
|
|
103
|
+
rep.checks.append(_run("not_yanked", check_not_yanked))
|
|
104
|
+
|
|
105
|
+
rep.checks.append(_run("manifest_signature", check_manifest_signature))
|
|
106
|
+
|
|
107
|
+
def check_sbom_signature():
|
|
108
|
+
from astra_verify._sig import verify_file, SigningError
|
|
109
|
+
pub = next(dist.glob("*.pub"), None)
|
|
110
|
+
try:
|
|
111
|
+
verify_file(sbom, pub, sbom_sig)
|
|
112
|
+
except SigningError as e:
|
|
113
|
+
return False, f"sbom signature invalid: {e}"
|
|
114
|
+
return True, "sbom signed"
|
|
115
|
+
rep.checks.append(_run("sbom_signature", check_sbom_signature))
|
|
116
|
+
|
|
117
|
+
def check_wheel_hash():
|
|
118
|
+
target = wheel.name
|
|
119
|
+
expected = None
|
|
120
|
+
for line in manifest.read_text().splitlines():
|
|
121
|
+
if not line.strip():
|
|
122
|
+
continue
|
|
123
|
+
d, _, rel = line.partition(" ")
|
|
124
|
+
if Path(rel).name == target:
|
|
125
|
+
expected = d.strip()
|
|
126
|
+
break
|
|
127
|
+
if expected is None:
|
|
128
|
+
return False, f"{target} not in manifest"
|
|
129
|
+
actual = _sha(wheel)
|
|
130
|
+
if expected != actual:
|
|
131
|
+
return False, f"wheel hash mismatch vs manifest ({expected[:16]}... != {actual[:16]}...)"
|
|
132
|
+
return True, f"wheel matches manifest {expected[:16]}..."
|
|
133
|
+
rep.checks.append(_run("wheel_hash", check_wheel_hash))
|
|
134
|
+
|
|
135
|
+
def check_sbom_components():
|
|
136
|
+
data = json.loads(sbom.read_text())
|
|
137
|
+
comps = data.get("components", [])
|
|
138
|
+
names = {c.get("name") for c in comps}
|
|
139
|
+
required = ("astra-substrate", "mlx", "cryptography")
|
|
140
|
+
missing = [n for n in required if n not in names]
|
|
141
|
+
if missing:
|
|
142
|
+
return False, f"sbom missing: {missing}"
|
|
143
|
+
foreign = [n for n in names if n not in required]
|
|
144
|
+
if foreign:
|
|
145
|
+
return False, f"sbom contains non-runtime components: {sorted(foreign)[:5]}"
|
|
146
|
+
return True, f"{len(comps)} runtime components: {sorted(names)}"
|
|
147
|
+
rep.checks.append(_run("sbom_components", check_sbom_components))
|
|
148
|
+
|
|
149
|
+
return rep
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astra-verify
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Independent verifier for astra-substrate releases
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: cryptography>=42
|
|
8
|
+
|
|
9
|
+
# astra-verify
|
|
10
|
+
|
|
11
|
+
Independent verifier for astra-substrate releases.
|
|
12
|
+
|
|
13
|
+
- Ships a pinned trust root (`roots/publisher.sha256`)
|
|
14
|
+
- Verifies wheel hash, manifest signature, SBOM signature
|
|
15
|
+
- Imports no `astra.*`
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
pip install astra-verify
|
|
20
|
+
astra-verify --dist /path/to/astra_substrate/dist
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
./astra_verify/SELF_SHA256SUMS
|
|
5
|
+
./astra_verify/SELF_SHA256SUMS.sig
|
|
6
|
+
./astra_verify/SELF_publisher.pub
|
|
7
|
+
./astra_verify/__init__.py
|
|
8
|
+
./astra_verify/__main__.py
|
|
9
|
+
./astra_verify/_selfcheck.py
|
|
10
|
+
./astra_verify/_sig.py
|
|
11
|
+
./astra_verify/trust_root.py
|
|
12
|
+
./astra_verify/verify.py
|
|
13
|
+
./astra_verify/roots/README.md
|
|
14
|
+
./astra_verify/roots/v1/METADATA.json
|
|
15
|
+
./astra_verify/roots/v1/publisher.sha256
|
|
16
|
+
./astra_verify/roots/v2/METADATA.json
|
|
17
|
+
./astra_verify/roots/v2/publisher.sha256
|
|
18
|
+
astra_verify/SELF_SHA256SUMS
|
|
19
|
+
astra_verify/SELF_SHA256SUMS.sig
|
|
20
|
+
astra_verify/SELF_publisher.pub
|
|
21
|
+
astra_verify/__init__.py
|
|
22
|
+
astra_verify/__main__.py
|
|
23
|
+
astra_verify/_selfcheck.py
|
|
24
|
+
astra_verify/_sig.py
|
|
25
|
+
astra_verify/trust_root.py
|
|
26
|
+
astra_verify/verify.py
|
|
27
|
+
astra_verify.egg-info/PKG-INFO
|
|
28
|
+
astra_verify.egg-info/SOURCES.txt
|
|
29
|
+
astra_verify.egg-info/dependency_links.txt
|
|
30
|
+
astra_verify.egg-info/requires.txt
|
|
31
|
+
astra_verify.egg-info/top_level.txt
|
|
32
|
+
astra_verify/roots/README.md
|
|
33
|
+
astra_verify/roots/v1/METADATA.json
|
|
34
|
+
astra_verify/roots/v1/publisher.sha256
|
|
35
|
+
astra_verify/roots/v2/METADATA.json
|
|
36
|
+
astra_verify/roots/v2/publisher.sha256
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography>=42
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
astra_verify
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "astra-verify"
|
|
7
|
+
version = "0.3.1"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
description = "Independent verifier for astra-substrate releases"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = ["cryptography>=42"]
|
|
12
|
+
|
|
13
|
+
[tool.setuptools]
|
|
14
|
+
packages = ["astra_verify"]
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.package-dir]
|
|
17
|
+
"" = "."
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.package-data]
|
|
20
|
+
astra_verify = [
|
|
21
|
+
"roots/*",
|
|
22
|
+
"roots/v1/*",
|
|
23
|
+
"roots/v2/*",
|
|
24
|
+
"SELF_SHA256SUMS",
|
|
25
|
+
"SELF_SHA256SUMS.sig",
|
|
26
|
+
"SELF_publisher.pub",
|
|
27
|
+
]
|