tbz 0.1.0__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.
- tbz-0.1.0/PKG-INFO +117 -0
- tbz-0.1.0/README.md +84 -0
- tbz-0.1.0/pyproject.toml +48 -0
- tbz-0.1.0/setup.cfg +4 -0
- tbz-0.1.0/tbz/__init__.py +26 -0
- tbz-0.1.0/tbz/archive.py +248 -0
- tbz-0.1.0/tbz/mirror.py +184 -0
- tbz-0.1.0/tbz.egg-info/PKG-INFO +117 -0
- tbz-0.1.0/tbz.egg-info/SOURCES.txt +10 -0
- tbz-0.1.0/tbz.egg-info/dependency_links.txt +1 -0
- tbz-0.1.0/tbz.egg-info/requires.txt +5 -0
- tbz-0.1.0/tbz.egg-info/top_level.txt +1 -0
tbz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tbz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TBZ (TIBET-zip) — Block-level authenticated compression for the Zero-Trust era
|
|
5
|
+
Author-email: Jasper van de Meent <jasper@humotica.nl>
|
|
6
|
+
License: MIT OR Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/jaspertvdm/tbz
|
|
8
|
+
Project-URL: Repository, https://github.com/jaspertvdm/tbz
|
|
9
|
+
Project-URL: Issues, https://github.com/jaspertvdm/tbz/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/jaspertvdm/tbz/blob/main/ARCHITECTURE.md
|
|
11
|
+
Keywords: compression,security,provenance,supply-chain,zero-trust,ed25519
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Rust
|
|
24
|
+
Classifier: Topic :: Security
|
|
25
|
+
Classifier: Topic :: Security :: Cryptography
|
|
26
|
+
Classifier: Topic :: System :: Archiving :: Compression
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
Requires-Dist: requests>=2.28.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# tbz — TIBET-zip for Python
|
|
35
|
+
|
|
36
|
+
**Block-level authenticated compression for the Zero-Trust era.**
|
|
37
|
+
|
|
38
|
+
Python client for TBZ archives and the Transparency Mirror network. Every block carries its own TIBET provenance envelope and Ed25519 signature. Tampered blocks are rejected before decompression touches memory.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install tbz
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from tbz import TBZArchive, Mirror
|
|
50
|
+
|
|
51
|
+
# Read and inspect a TBZ archive (pure Python, no binary needed)
|
|
52
|
+
archive = TBZArchive("release.tbz")
|
|
53
|
+
info = archive.inspect()
|
|
54
|
+
print(f"Blocks: {info['block_count']}, Hash: {info['content_hash']}")
|
|
55
|
+
|
|
56
|
+
# Verify integrity (uses Rust CLI if available, falls back to Python)
|
|
57
|
+
result = archive.verify()
|
|
58
|
+
print(result) # TBZ VERIFIED: 3 blocks (hash + Ed25519), 0 errors
|
|
59
|
+
|
|
60
|
+
# Look up in the Transparency Mirror (public, no auth needed)
|
|
61
|
+
mirror = Mirror()
|
|
62
|
+
entry = mirror.lookup("sha256:abc123...")
|
|
63
|
+
if entry:
|
|
64
|
+
print(f"Source: {entry['source_repo']}, Attestations: {len(entry['attestations'])}")
|
|
65
|
+
|
|
66
|
+
# Search by publisher
|
|
67
|
+
results = mirror.search(jis_id="jis:ed25519:77214ce9c262843e")
|
|
68
|
+
|
|
69
|
+
# Mirror stats
|
|
70
|
+
stats = mirror.stats()
|
|
71
|
+
print(f"Mirror node: {stats['node']}, entries: {stats['total_entries']}")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## With Rust CLI (full features)
|
|
75
|
+
|
|
76
|
+
For full Ed25519 signature verification and pack/unpack support, install the Rust binary:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# From source
|
|
80
|
+
git clone https://github.com/jaspertvdm/tbz
|
|
81
|
+
cd tbz && cargo build --release
|
|
82
|
+
export PATH=$PATH:$(pwd)/target/release
|
|
83
|
+
|
|
84
|
+
# Then in Python
|
|
85
|
+
archive = TBZArchive("release.tbz")
|
|
86
|
+
result = archive.verify() # Full Ed25519 + SHA-256 verification
|
|
87
|
+
archive.unpack("./extracted") # Extract through TIBET Airlock
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Transparency Mirror
|
|
91
|
+
|
|
92
|
+
The Mirror is a distributed trust database for verifying TBZ package provenance. The bootstrap node runs at `brein.jaspervandemeent.nl`.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from tbz import Mirror
|
|
96
|
+
|
|
97
|
+
# Default: connects to bootstrap node
|
|
98
|
+
mirror = Mirror()
|
|
99
|
+
|
|
100
|
+
# Custom node
|
|
101
|
+
mirror = Mirror(node_url="https://your-mirror.example.com")
|
|
102
|
+
|
|
103
|
+
# Public endpoints (no auth)
|
|
104
|
+
mirror.lookup("sha256:...") # Look up by hash
|
|
105
|
+
mirror.search(verdict="safe") # Search attestations
|
|
106
|
+
mirror.stats() # Node statistics
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Links
|
|
110
|
+
|
|
111
|
+
- [GitHub](https://github.com/jaspertvdm/tbz)
|
|
112
|
+
- [Architecture](https://github.com/jaspertvdm/tbz/blob/main/ARCHITECTURE.md)
|
|
113
|
+
- [Mirror API](https://brein.jaspervandemeent.nl/api/tbz-mirror/stats)
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT / Apache-2.0
|
tbz-0.1.0/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# tbz — TIBET-zip for Python
|
|
2
|
+
|
|
3
|
+
**Block-level authenticated compression for the Zero-Trust era.**
|
|
4
|
+
|
|
5
|
+
Python client for TBZ archives and the Transparency Mirror network. Every block carries its own TIBET provenance envelope and Ed25519 signature. Tampered blocks are rejected before decompression touches memory.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install tbz
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from tbz import TBZArchive, Mirror
|
|
17
|
+
|
|
18
|
+
# Read and inspect a TBZ archive (pure Python, no binary needed)
|
|
19
|
+
archive = TBZArchive("release.tbz")
|
|
20
|
+
info = archive.inspect()
|
|
21
|
+
print(f"Blocks: {info['block_count']}, Hash: {info['content_hash']}")
|
|
22
|
+
|
|
23
|
+
# Verify integrity (uses Rust CLI if available, falls back to Python)
|
|
24
|
+
result = archive.verify()
|
|
25
|
+
print(result) # TBZ VERIFIED: 3 blocks (hash + Ed25519), 0 errors
|
|
26
|
+
|
|
27
|
+
# Look up in the Transparency Mirror (public, no auth needed)
|
|
28
|
+
mirror = Mirror()
|
|
29
|
+
entry = mirror.lookup("sha256:abc123...")
|
|
30
|
+
if entry:
|
|
31
|
+
print(f"Source: {entry['source_repo']}, Attestations: {len(entry['attestations'])}")
|
|
32
|
+
|
|
33
|
+
# Search by publisher
|
|
34
|
+
results = mirror.search(jis_id="jis:ed25519:77214ce9c262843e")
|
|
35
|
+
|
|
36
|
+
# Mirror stats
|
|
37
|
+
stats = mirror.stats()
|
|
38
|
+
print(f"Mirror node: {stats['node']}, entries: {stats['total_entries']}")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## With Rust CLI (full features)
|
|
42
|
+
|
|
43
|
+
For full Ed25519 signature verification and pack/unpack support, install the Rust binary:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# From source
|
|
47
|
+
git clone https://github.com/jaspertvdm/tbz
|
|
48
|
+
cd tbz && cargo build --release
|
|
49
|
+
export PATH=$PATH:$(pwd)/target/release
|
|
50
|
+
|
|
51
|
+
# Then in Python
|
|
52
|
+
archive = TBZArchive("release.tbz")
|
|
53
|
+
result = archive.verify() # Full Ed25519 + SHA-256 verification
|
|
54
|
+
archive.unpack("./extracted") # Extract through TIBET Airlock
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Transparency Mirror
|
|
58
|
+
|
|
59
|
+
The Mirror is a distributed trust database for verifying TBZ package provenance. The bootstrap node runs at `brein.jaspervandemeent.nl`.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from tbz import Mirror
|
|
63
|
+
|
|
64
|
+
# Default: connects to bootstrap node
|
|
65
|
+
mirror = Mirror()
|
|
66
|
+
|
|
67
|
+
# Custom node
|
|
68
|
+
mirror = Mirror(node_url="https://your-mirror.example.com")
|
|
69
|
+
|
|
70
|
+
# Public endpoints (no auth)
|
|
71
|
+
mirror.lookup("sha256:...") # Look up by hash
|
|
72
|
+
mirror.search(verdict="safe") # Search attestations
|
|
73
|
+
mirror.stats() # Node statistics
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Links
|
|
77
|
+
|
|
78
|
+
- [GitHub](https://github.com/jaspertvdm/tbz)
|
|
79
|
+
- [Architecture](https://github.com/jaspertvdm/tbz/blob/main/ARCHITECTURE.md)
|
|
80
|
+
- [Mirror API](https://brein.jaspervandemeent.nl/api/tbz-mirror/stats)
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT / Apache-2.0
|
tbz-0.1.0/pyproject.toml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tbz"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "TBZ (TIBET-zip) — Block-level authenticated compression for the Zero-Trust era"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT OR Apache-2.0"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Jasper van de Meent", email = "jasper@humotica.nl"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["compression", "security", "provenance", "supply-chain", "zero-trust", "ed25519"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"License :: OSI Approved :: Apache Software License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Rust",
|
|
29
|
+
"Topic :: Security",
|
|
30
|
+
"Topic :: Security :: Cryptography",
|
|
31
|
+
"Topic :: System :: Archiving :: Compression",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"requests>=2.28.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = ["pytest>=7.0", "pytest-cov"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/jaspertvdm/tbz"
|
|
42
|
+
Repository = "https://github.com/jaspertvdm/tbz"
|
|
43
|
+
Issues = "https://github.com/jaspertvdm/tbz/issues"
|
|
44
|
+
Documentation = "https://github.com/jaspertvdm/tbz/blob/main/ARCHITECTURE.md"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["."]
|
|
48
|
+
include = ["tbz*"]
|
tbz-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TBZ (TIBET-zip) — Block-level authenticated compression for the Zero-Trust era.
|
|
3
|
+
|
|
4
|
+
Every block carries its own TIBET provenance envelope and Ed25519 signature.
|
|
5
|
+
Invalid blocks are rejected before decompression touches memory.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from tbz import TBZArchive, Mirror
|
|
9
|
+
|
|
10
|
+
# Verify an archive
|
|
11
|
+
archive = TBZArchive("release.tbz")
|
|
12
|
+
result = archive.verify()
|
|
13
|
+
print(result)
|
|
14
|
+
|
|
15
|
+
# Look up in Transparency Mirror
|
|
16
|
+
mirror = Mirror()
|
|
17
|
+
entry = mirror.lookup("sha256:abc123...")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__author__ = "Jasper van de Meent"
|
|
22
|
+
|
|
23
|
+
from tbz.archive import TBZArchive
|
|
24
|
+
from tbz.mirror import Mirror
|
|
25
|
+
|
|
26
|
+
__all__ = ["TBZArchive", "Mirror", "__version__"]
|
tbz-0.1.0/tbz/archive.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TBZ Archive — Python interface for TBZ archives.
|
|
3
|
+
|
|
4
|
+
Wraps the Rust `tbz` CLI for pack/unpack/verify/inspect operations.
|
|
5
|
+
For native speed, install the Rust toolchain and build with `cargo build --release`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import struct
|
|
13
|
+
import subprocess
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import List, Optional, Dict, Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# TBZ magic bytes
|
|
20
|
+
MAGIC = bytes([0x54, 0x42, 0x5A])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class BlockInfo:
|
|
25
|
+
"""Information about a single block in a TBZ archive."""
|
|
26
|
+
index: int
|
|
27
|
+
block_type: str # "Manifest", "Data", "Nested"
|
|
28
|
+
jis_level: int
|
|
29
|
+
compressed_size: int
|
|
30
|
+
uncompressed_size: int
|
|
31
|
+
content_hash: str
|
|
32
|
+
erachter: str # intent
|
|
33
|
+
signature_present: bool
|
|
34
|
+
path: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class VerifyResult:
|
|
39
|
+
"""Result of verifying a TBZ archive."""
|
|
40
|
+
ok: bool
|
|
41
|
+
blocks_checked: int
|
|
42
|
+
errors: int
|
|
43
|
+
signing_key: Optional[str] = None
|
|
44
|
+
block_results: List[Dict[str, Any]] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
def __str__(self):
|
|
47
|
+
status = "VERIFIED" if self.ok else "FAILED"
|
|
48
|
+
sig = f" (hash + Ed25519)" if self.signing_key else " (hash only)"
|
|
49
|
+
return f"TBZ {status}: {self.blocks_checked} blocks{sig}, {self.errors} errors"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TBZArchive:
|
|
53
|
+
"""Interface for TBZ archives.
|
|
54
|
+
|
|
55
|
+
Can operate in two modes:
|
|
56
|
+
1. CLI mode: delegates to the Rust `tbz` binary (fast, full features)
|
|
57
|
+
2. Pure Python mode: reads block headers directly (no external binary needed)
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: Path to the .tbz archive file
|
|
61
|
+
tbz_binary: Path to the tbz CLI binary (auto-detected if not specified)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, path: str, tbz_binary: str = None):
|
|
65
|
+
self.path = Path(path)
|
|
66
|
+
self._tbz_bin = tbz_binary or self._find_binary()
|
|
67
|
+
|
|
68
|
+
def _find_binary(self) -> Optional[str]:
|
|
69
|
+
"""Try to find the tbz binary."""
|
|
70
|
+
# Check common locations
|
|
71
|
+
candidates = [
|
|
72
|
+
shutil.which("tbz"),
|
|
73
|
+
os.path.expanduser("~/.cargo/bin/tbz"),
|
|
74
|
+
str(Path(__file__).parent.parent.parent / "target" / "release" / "tbz"),
|
|
75
|
+
str(Path(__file__).parent.parent.parent / "target" / "debug" / "tbz"),
|
|
76
|
+
]
|
|
77
|
+
for c in candidates:
|
|
78
|
+
if c and os.path.isfile(c) and os.access(c, os.X_OK):
|
|
79
|
+
return c
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def exists(self) -> bool:
|
|
84
|
+
return self.path.exists()
|
|
85
|
+
|
|
86
|
+
def content_hash(self) -> str:
|
|
87
|
+
"""Compute SHA-256 hash of the entire archive file."""
|
|
88
|
+
h = hashlib.sha256()
|
|
89
|
+
with open(self.path, "rb") as f:
|
|
90
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
91
|
+
h.update(chunk)
|
|
92
|
+
return f"sha256:{h.hexdigest()}"
|
|
93
|
+
|
|
94
|
+
def read_blocks(self) -> List[BlockInfo]:
|
|
95
|
+
"""Read block headers from the archive (pure Python, no CLI needed)."""
|
|
96
|
+
blocks = []
|
|
97
|
+
with open(self.path, "rb") as f:
|
|
98
|
+
while True:
|
|
99
|
+
magic = f.read(3)
|
|
100
|
+
if len(magic) < 3:
|
|
101
|
+
break
|
|
102
|
+
if magic != MAGIC:
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
# Read header
|
|
106
|
+
header_len = struct.unpack("<I", f.read(4))[0]
|
|
107
|
+
header_json = f.read(header_len)
|
|
108
|
+
header = json.loads(header_json)
|
|
109
|
+
|
|
110
|
+
# Read envelope
|
|
111
|
+
envelope_len = struct.unpack("<I", f.read(4))[0]
|
|
112
|
+
envelope_json = f.read(envelope_len)
|
|
113
|
+
envelope = json.loads(envelope_json)
|
|
114
|
+
|
|
115
|
+
# Read payload
|
|
116
|
+
payload_len = struct.unpack("<Q", f.read(8))[0]
|
|
117
|
+
f.seek(payload_len, 1) # skip payload
|
|
118
|
+
|
|
119
|
+
# Read signature (64 bytes)
|
|
120
|
+
sig = f.read(64)
|
|
121
|
+
sig_present = any(b != 0 for b in sig)
|
|
122
|
+
|
|
123
|
+
blocks.append(BlockInfo(
|
|
124
|
+
index=header.get("block_index", 0),
|
|
125
|
+
block_type=header.get("block_type", "Unknown"),
|
|
126
|
+
jis_level=header.get("jis_level", 0),
|
|
127
|
+
compressed_size=header.get("compressed_size", 0),
|
|
128
|
+
uncompressed_size=header.get("uncompressed_size", 0),
|
|
129
|
+
content_hash=envelope.get("erin", {}).get("content_hash", ""),
|
|
130
|
+
erachter=envelope.get("erachter", ""),
|
|
131
|
+
signature_present=sig_present,
|
|
132
|
+
path=None,
|
|
133
|
+
))
|
|
134
|
+
|
|
135
|
+
return blocks
|
|
136
|
+
|
|
137
|
+
def verify(self) -> VerifyResult:
|
|
138
|
+
"""Verify the archive integrity.
|
|
139
|
+
|
|
140
|
+
Uses the Rust CLI if available (fast, full Ed25519 verification).
|
|
141
|
+
Falls back to pure Python header reading if CLI not found.
|
|
142
|
+
"""
|
|
143
|
+
if self._tbz_bin:
|
|
144
|
+
return self._verify_cli()
|
|
145
|
+
return self._verify_python()
|
|
146
|
+
|
|
147
|
+
def _verify_cli(self) -> VerifyResult:
|
|
148
|
+
"""Verify using the Rust CLI binary."""
|
|
149
|
+
result = subprocess.run(
|
|
150
|
+
[self._tbz_bin, "verify", str(self.path)],
|
|
151
|
+
capture_output=True, text=True, timeout=60,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
output = result.stdout + result.stderr
|
|
155
|
+
errors = output.count("FAIL")
|
|
156
|
+
blocks = output.count("] OK") + errors
|
|
157
|
+
signing_key = None
|
|
158
|
+
|
|
159
|
+
for line in output.splitlines():
|
|
160
|
+
if "Signing key:" in line:
|
|
161
|
+
signing_key = line.split("Ed25519")[-1].strip()
|
|
162
|
+
|
|
163
|
+
return VerifyResult(
|
|
164
|
+
ok=(errors == 0 and result.returncode == 0),
|
|
165
|
+
blocks_checked=blocks,
|
|
166
|
+
errors=errors,
|
|
167
|
+
signing_key=signing_key,
|
|
168
|
+
block_results=[{"raw_output": output}],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def _verify_python(self) -> VerifyResult:
|
|
172
|
+
"""Basic verification using pure Python (header checks only)."""
|
|
173
|
+
blocks = self.read_blocks()
|
|
174
|
+
return VerifyResult(
|
|
175
|
+
ok=len(blocks) > 0,
|
|
176
|
+
blocks_checked=len(blocks),
|
|
177
|
+
errors=0,
|
|
178
|
+
signing_key=None,
|
|
179
|
+
block_results=[{"index": b.index, "type": b.block_type, "hash": b.content_hash} for b in blocks],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def inspect(self) -> Dict[str, Any]:
|
|
183
|
+
"""Get detailed information about the archive."""
|
|
184
|
+
blocks = self.read_blocks()
|
|
185
|
+
return {
|
|
186
|
+
"path": str(self.path),
|
|
187
|
+
"size": self.path.stat().st_size,
|
|
188
|
+
"content_hash": self.content_hash(),
|
|
189
|
+
"block_count": len(blocks),
|
|
190
|
+
"blocks": [
|
|
191
|
+
{
|
|
192
|
+
"index": b.index,
|
|
193
|
+
"type": b.block_type,
|
|
194
|
+
"jis_level": b.jis_level,
|
|
195
|
+
"compressed_size": b.compressed_size,
|
|
196
|
+
"uncompressed_size": b.uncompressed_size,
|
|
197
|
+
"content_hash": b.content_hash,
|
|
198
|
+
"intent": b.erachter,
|
|
199
|
+
"signed": b.signature_present,
|
|
200
|
+
}
|
|
201
|
+
for b in blocks
|
|
202
|
+
],
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def unpack(self, output_dir: str = ".") -> bool:
|
|
206
|
+
"""Extract the archive through the TIBET Airlock.
|
|
207
|
+
|
|
208
|
+
Requires the Rust CLI binary.
|
|
209
|
+
"""
|
|
210
|
+
if not self._tbz_bin:
|
|
211
|
+
raise RuntimeError("tbz binary not found — install with: cargo install --path crates/tbz-cli")
|
|
212
|
+
|
|
213
|
+
result = subprocess.run(
|
|
214
|
+
[self._tbz_bin, "unpack", str(self.path), "-o", output_dir],
|
|
215
|
+
capture_output=True, text=True, timeout=120,
|
|
216
|
+
)
|
|
217
|
+
return result.returncode == 0
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def pack(source: str, output: str = "output.tbz", tbz_binary: str = None) -> "TBZArchive":
|
|
221
|
+
"""Pack files into a TBZ archive.
|
|
222
|
+
|
|
223
|
+
Requires the Rust CLI binary.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
source: Path to file or directory to archive
|
|
227
|
+
output: Output .tbz file path
|
|
228
|
+
tbz_binary: Path to tbz CLI binary
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
TBZArchive instance for the created archive.
|
|
232
|
+
"""
|
|
233
|
+
binary = tbz_binary or shutil.which("tbz")
|
|
234
|
+
if not binary:
|
|
235
|
+
raise RuntimeError("tbz binary not found — install with: cargo install --path crates/tbz-cli")
|
|
236
|
+
|
|
237
|
+
result = subprocess.run(
|
|
238
|
+
[binary, "pack", source, "-o", output],
|
|
239
|
+
capture_output=True, text=True, timeout=120,
|
|
240
|
+
)
|
|
241
|
+
if result.returncode != 0:
|
|
242
|
+
raise RuntimeError(f"Pack failed: {result.stderr}")
|
|
243
|
+
|
|
244
|
+
return TBZArchive(output, tbz_binary=binary)
|
|
245
|
+
|
|
246
|
+
def __repr__(self):
|
|
247
|
+
blocks = len(self.read_blocks()) if self.exists else 0
|
|
248
|
+
return f"TBZArchive({self.path!r}, blocks={blocks})"
|
tbz-0.1.0/tbz/mirror.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TBZ Transparency Mirror client.
|
|
3
|
+
|
|
4
|
+
Query the distributed trust database for package provenance verification.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, Any, List
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_MIRROR = "https://brein.jaspervandemeent.nl"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MirrorError(Exception):
|
|
15
|
+
"""Error communicating with a Transparency Mirror node."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Mirror:
|
|
20
|
+
"""Client for the TBZ Transparency Mirror network.
|
|
21
|
+
|
|
22
|
+
The Mirror is a distributed trust database that stores content hashes,
|
|
23
|
+
Ed25519 signing keys, and attestations for TBZ archives.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
node_url: Base URL of the mirror node (default: bootstrap node)
|
|
27
|
+
timeout: Request timeout in seconds
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, node_url: str = DEFAULT_MIRROR, timeout: float = 10.0):
|
|
31
|
+
self.node_url = node_url.rstrip("/")
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
self._base = f"{self.node_url}/api/tbz-mirror"
|
|
34
|
+
|
|
35
|
+
def lookup(self, content_hash: str) -> Optional[Dict[str, Any]]:
|
|
36
|
+
"""Look up a TBZ archive by its content hash.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
content_hash: SHA-256 hash (format: "sha256:<hex>")
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Trust entry dict if found, None if not found.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
MirrorError: On network or server errors.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
resp = requests.get(
|
|
49
|
+
f"{self._base}/lookup/{content_hash}",
|
|
50
|
+
timeout=self.timeout,
|
|
51
|
+
)
|
|
52
|
+
if resp.status_code == 404:
|
|
53
|
+
return None
|
|
54
|
+
resp.raise_for_status()
|
|
55
|
+
return resp.json().get("entry")
|
|
56
|
+
except requests.ConnectionError as e:
|
|
57
|
+
raise MirrorError(f"Cannot reach mirror node {self.node_url}: {e}")
|
|
58
|
+
except requests.HTTPError as e:
|
|
59
|
+
raise MirrorError(f"Mirror error: {e}")
|
|
60
|
+
|
|
61
|
+
def register(self, content_hash: str, signing_key: str = None,
|
|
62
|
+
jis_id: str = None, source_repo: str = None,
|
|
63
|
+
block_count: int = 0, total_size: int = 0,
|
|
64
|
+
auth_token: str = None) -> Dict[str, Any]:
|
|
65
|
+
"""Register a TBZ archive in the Transparency Mirror.
|
|
66
|
+
|
|
67
|
+
Requires authentication on the mirror node.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
content_hash: SHA-256 hash of the archive
|
|
71
|
+
signing_key: Ed25519 public key (hex)
|
|
72
|
+
jis_id: JIS identity string
|
|
73
|
+
source_repo: Repository identifier
|
|
74
|
+
block_count: Number of blocks in the archive
|
|
75
|
+
total_size: Total uncompressed size in bytes
|
|
76
|
+
auth_token: JWT bearer token for authentication
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Registration result from the mirror.
|
|
80
|
+
"""
|
|
81
|
+
headers = {"Content-Type": "application/json"}
|
|
82
|
+
if auth_token:
|
|
83
|
+
headers["Authorization"] = f"Bearer {auth_token}"
|
|
84
|
+
|
|
85
|
+
payload = {"content_hash": content_hash}
|
|
86
|
+
if signing_key:
|
|
87
|
+
payload["signing_key"] = signing_key
|
|
88
|
+
if jis_id:
|
|
89
|
+
payload["jis_id"] = jis_id
|
|
90
|
+
if source_repo:
|
|
91
|
+
payload["source_repo"] = source_repo
|
|
92
|
+
if block_count:
|
|
93
|
+
payload["block_count"] = block_count
|
|
94
|
+
if total_size:
|
|
95
|
+
payload["total_size"] = total_size
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
resp = requests.post(
|
|
99
|
+
f"{self._base}/register",
|
|
100
|
+
json=payload,
|
|
101
|
+
headers=headers,
|
|
102
|
+
timeout=self.timeout,
|
|
103
|
+
)
|
|
104
|
+
resp.raise_for_status()
|
|
105
|
+
return resp.json()
|
|
106
|
+
except requests.HTTPError as e:
|
|
107
|
+
raise MirrorError(f"Registration failed: {e}")
|
|
108
|
+
|
|
109
|
+
def attest(self, content_hash: str, attester: str,
|
|
110
|
+
verdict: str = "safe", notes: str = None,
|
|
111
|
+
auth_token: str = None) -> Dict[str, Any]:
|
|
112
|
+
"""Add an attestation to an existing entry.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
content_hash: SHA-256 hash of the archive
|
|
116
|
+
attester: JIS ID or name of the attester
|
|
117
|
+
verdict: "safe", "suspicious", "malicious", or "unknown"
|
|
118
|
+
notes: Optional evidence/notes
|
|
119
|
+
auth_token: JWT bearer token for authentication
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Attestation result from the mirror.
|
|
123
|
+
"""
|
|
124
|
+
headers = {"Content-Type": "application/json"}
|
|
125
|
+
if auth_token:
|
|
126
|
+
headers["Authorization"] = f"Bearer {auth_token}"
|
|
127
|
+
|
|
128
|
+
payload = {"attester": attester, "verdict": verdict}
|
|
129
|
+
if notes:
|
|
130
|
+
payload["notes"] = notes
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
resp = requests.post(
|
|
134
|
+
f"{self._base}/attest/{content_hash}",
|
|
135
|
+
json=payload,
|
|
136
|
+
headers=headers,
|
|
137
|
+
timeout=self.timeout,
|
|
138
|
+
)
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
return resp.json()
|
|
141
|
+
except requests.HTTPError as e:
|
|
142
|
+
raise MirrorError(f"Attestation failed: {e}")
|
|
143
|
+
|
|
144
|
+
def search(self, jis_id: str = None, verdict: str = None,
|
|
145
|
+
signing_key: str = None, limit: int = 50) -> List[Dict[str, Any]]:
|
|
146
|
+
"""Search the Transparency Mirror.
|
|
147
|
+
|
|
148
|
+
All parameters are optional filters.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of matching trust entries.
|
|
152
|
+
"""
|
|
153
|
+
params = {}
|
|
154
|
+
if jis_id:
|
|
155
|
+
params["jis_id"] = jis_id
|
|
156
|
+
if verdict:
|
|
157
|
+
params["verdict"] = verdict
|
|
158
|
+
if signing_key:
|
|
159
|
+
params["signing_key"] = signing_key
|
|
160
|
+
params["limit"] = limit
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
resp = requests.get(
|
|
164
|
+
f"{self._base}/search",
|
|
165
|
+
params=params,
|
|
166
|
+
timeout=self.timeout,
|
|
167
|
+
)
|
|
168
|
+
resp.raise_for_status()
|
|
169
|
+
return resp.json().get("results", [])
|
|
170
|
+
except requests.HTTPError as e:
|
|
171
|
+
raise MirrorError(f"Search failed: {e}")
|
|
172
|
+
|
|
173
|
+
def stats(self) -> Dict[str, Any]:
|
|
174
|
+
"""Get mirror node statistics.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dict with node info, entry counts, attestation counts.
|
|
178
|
+
"""
|
|
179
|
+
resp = requests.get(f"{self._base}/stats", timeout=self.timeout)
|
|
180
|
+
resp.raise_for_status()
|
|
181
|
+
return resp.json()
|
|
182
|
+
|
|
183
|
+
def __repr__(self):
|
|
184
|
+
return f"Mirror(node={self.node_url!r})"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tbz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TBZ (TIBET-zip) — Block-level authenticated compression for the Zero-Trust era
|
|
5
|
+
Author-email: Jasper van de Meent <jasper@humotica.nl>
|
|
6
|
+
License: MIT OR Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/jaspertvdm/tbz
|
|
8
|
+
Project-URL: Repository, https://github.com/jaspertvdm/tbz
|
|
9
|
+
Project-URL: Issues, https://github.com/jaspertvdm/tbz/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/jaspertvdm/tbz/blob/main/ARCHITECTURE.md
|
|
11
|
+
Keywords: compression,security,provenance,supply-chain,zero-trust,ed25519
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Rust
|
|
24
|
+
Classifier: Topic :: Security
|
|
25
|
+
Classifier: Topic :: Security :: Cryptography
|
|
26
|
+
Classifier: Topic :: System :: Archiving :: Compression
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
Requires-Dist: requests>=2.28.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# tbz — TIBET-zip for Python
|
|
35
|
+
|
|
36
|
+
**Block-level authenticated compression for the Zero-Trust era.**
|
|
37
|
+
|
|
38
|
+
Python client for TBZ archives and the Transparency Mirror network. Every block carries its own TIBET provenance envelope and Ed25519 signature. Tampered blocks are rejected before decompression touches memory.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install tbz
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from tbz import TBZArchive, Mirror
|
|
50
|
+
|
|
51
|
+
# Read and inspect a TBZ archive (pure Python, no binary needed)
|
|
52
|
+
archive = TBZArchive("release.tbz")
|
|
53
|
+
info = archive.inspect()
|
|
54
|
+
print(f"Blocks: {info['block_count']}, Hash: {info['content_hash']}")
|
|
55
|
+
|
|
56
|
+
# Verify integrity (uses Rust CLI if available, falls back to Python)
|
|
57
|
+
result = archive.verify()
|
|
58
|
+
print(result) # TBZ VERIFIED: 3 blocks (hash + Ed25519), 0 errors
|
|
59
|
+
|
|
60
|
+
# Look up in the Transparency Mirror (public, no auth needed)
|
|
61
|
+
mirror = Mirror()
|
|
62
|
+
entry = mirror.lookup("sha256:abc123...")
|
|
63
|
+
if entry:
|
|
64
|
+
print(f"Source: {entry['source_repo']}, Attestations: {len(entry['attestations'])}")
|
|
65
|
+
|
|
66
|
+
# Search by publisher
|
|
67
|
+
results = mirror.search(jis_id="jis:ed25519:77214ce9c262843e")
|
|
68
|
+
|
|
69
|
+
# Mirror stats
|
|
70
|
+
stats = mirror.stats()
|
|
71
|
+
print(f"Mirror node: {stats['node']}, entries: {stats['total_entries']}")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## With Rust CLI (full features)
|
|
75
|
+
|
|
76
|
+
For full Ed25519 signature verification and pack/unpack support, install the Rust binary:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# From source
|
|
80
|
+
git clone https://github.com/jaspertvdm/tbz
|
|
81
|
+
cd tbz && cargo build --release
|
|
82
|
+
export PATH=$PATH:$(pwd)/target/release
|
|
83
|
+
|
|
84
|
+
# Then in Python
|
|
85
|
+
archive = TBZArchive("release.tbz")
|
|
86
|
+
result = archive.verify() # Full Ed25519 + SHA-256 verification
|
|
87
|
+
archive.unpack("./extracted") # Extract through TIBET Airlock
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Transparency Mirror
|
|
91
|
+
|
|
92
|
+
The Mirror is a distributed trust database for verifying TBZ package provenance. The bootstrap node runs at `brein.jaspervandemeent.nl`.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from tbz import Mirror
|
|
96
|
+
|
|
97
|
+
# Default: connects to bootstrap node
|
|
98
|
+
mirror = Mirror()
|
|
99
|
+
|
|
100
|
+
# Custom node
|
|
101
|
+
mirror = Mirror(node_url="https://your-mirror.example.com")
|
|
102
|
+
|
|
103
|
+
# Public endpoints (no auth)
|
|
104
|
+
mirror.lookup("sha256:...") # Look up by hash
|
|
105
|
+
mirror.search(verdict="safe") # Search attestations
|
|
106
|
+
mirror.stats() # Node statistics
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Links
|
|
110
|
+
|
|
111
|
+
- [GitHub](https://github.com/jaspertvdm/tbz)
|
|
112
|
+
- [Architecture](https://github.com/jaspertvdm/tbz/blob/main/ARCHITECTURE.md)
|
|
113
|
+
- [Mirror API](https://brein.jaspervandemeent.nl/api/tbz-mirror/stats)
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT / Apache-2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tbz
|