trustbeat 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.
- trustbeat-0.1.0/.github/workflows/publish.yml +50 -0
- trustbeat-0.1.0/.gitignore +9 -0
- trustbeat-0.1.0/LICENSE +21 -0
- trustbeat-0.1.0/PKG-INFO +74 -0
- trustbeat-0.1.0/README.md +46 -0
- trustbeat-0.1.0/pyproject.toml +47 -0
- trustbeat-0.1.0/smoke.py +314 -0
- trustbeat-0.1.0/tests/__init__.py +0 -0
- trustbeat-0.1.0/tests/test_client.py +440 -0
- trustbeat-0.1.0/tests/test_verify.py +166 -0
- trustbeat-0.1.0/trustbeat/__init__.py +73 -0
- trustbeat-0.1.0/trustbeat/_client.py +713 -0
- trustbeat-0.1.0/trustbeat/_exceptions.py +41 -0
- trustbeat-0.1.0/trustbeat/_models.py +434 -0
- trustbeat-0.1.0/trustbeat/_verify.py +62 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Publishes trustbeat to PyPI on a version tag.
|
|
2
|
+
#
|
|
3
|
+
# This workflow lives at the root of TrustBeat/sdk-python (mirrored from
|
|
4
|
+
# eu-security-app sdk/python). It does NOT run in the monorepo — GitHub only
|
|
5
|
+
# executes workflows at the repository root.
|
|
6
|
+
#
|
|
7
|
+
# Auth: PyPI Trusted Publishing (OIDC) — no API token stored. Configure the
|
|
8
|
+
# trusted publisher once on PyPI: project "trustbeat" → Publishing → add
|
|
9
|
+
# GitHub publisher (owner: TrustBeat, repo: sdk-python, workflow: publish.yml).
|
|
10
|
+
name: Publish to PyPI
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
tags: ['v*']
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
publish:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write # required for Trusted Publishing
|
|
25
|
+
contents: read
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
28
|
+
|
|
29
|
+
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
|
30
|
+
with:
|
|
31
|
+
python-version: '3.12'
|
|
32
|
+
|
|
33
|
+
- name: Verify tag matches package version
|
|
34
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
35
|
+
run: |
|
|
36
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
37
|
+
pkg=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
38
|
+
echo "tag=$tag pkg=$pkg"
|
|
39
|
+
[ "$tag" = "$pkg" ] || { echo "::error::tag $tag != pyproject version $pkg"; exit 1; }
|
|
40
|
+
|
|
41
|
+
- name: Run tests
|
|
42
|
+
run: python -m unittest discover -s tests -v
|
|
43
|
+
|
|
44
|
+
- name: Build sdist + wheel
|
|
45
|
+
run: |
|
|
46
|
+
python -m pip install --upgrade build
|
|
47
|
+
python -m build
|
|
48
|
+
|
|
49
|
+
- name: Publish to PyPI
|
|
50
|
+
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
|
trustbeat-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trustbeat s.r.o.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
trustbeat-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trustbeat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TrustBeat SDK — qualified timestamping and Merkle anchoring
|
|
5
|
+
Project-URL: Homepage, https://trustbeat.eu
|
|
6
|
+
Project-URL: Repository, https://github.com/TrustBeat/sdk-python
|
|
7
|
+
Project-URL: Issues, https://github.com/TrustBeat/sdk-python/issues
|
|
8
|
+
Author-email: "Trustbeat s.r.o." <radim.dejmek@trustbeat.eu>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: anchoring,eidas,merkle,qualified,rfc3161,timestamp,trust
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security :: Cryptography
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# TrustBeat Python SDK
|
|
30
|
+
|
|
31
|
+
Qualified electronic timestamps and Merkle anchoring — eIDAS-compliant, over a simple API.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install trustbeat
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from trustbeat import TrustBeat
|
|
43
|
+
|
|
44
|
+
tb = TrustBeat(api_key="tb_live_...")
|
|
45
|
+
|
|
46
|
+
# Anchor a file (SHA-256 computed locally, file never leaves your machine).
|
|
47
|
+
# anchor_file_wait() blocks until the proof is ready (next batch, up to 11 min).
|
|
48
|
+
proof = tb.anchor_file_wait("contract.pdf")
|
|
49
|
+
print(proof.id) # tracking ID
|
|
50
|
+
print(proof.anchored_at) # ISO 8601 timestamp
|
|
51
|
+
print(proof.merkle_root) # Merkle root of the batch
|
|
52
|
+
|
|
53
|
+
# Verify locally — no network call
|
|
54
|
+
assert tb.verify(proof)
|
|
55
|
+
|
|
56
|
+
# Or anchor a raw SHA-256 hash without blocking, then wait for the proof.
|
|
57
|
+
job = tb.anchor("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
|
58
|
+
print(job.id) # tracking ID, returned immediately (202)
|
|
59
|
+
proof = tb.anchor_wait(job.id) # blocks up to 11 min
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Python 3.9+
|
|
66
|
+
- Zero runtime dependencies (stdlib only)
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
Full API reference and guides at [api.trustbeat.eu/docs](https://api.trustbeat.eu/docs)
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# TrustBeat Python SDK
|
|
2
|
+
|
|
3
|
+
Qualified electronic timestamps and Merkle anchoring — eIDAS-compliant, over a simple API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install trustbeat
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from trustbeat import TrustBeat
|
|
15
|
+
|
|
16
|
+
tb = TrustBeat(api_key="tb_live_...")
|
|
17
|
+
|
|
18
|
+
# Anchor a file (SHA-256 computed locally, file never leaves your machine).
|
|
19
|
+
# anchor_file_wait() blocks until the proof is ready (next batch, up to 11 min).
|
|
20
|
+
proof = tb.anchor_file_wait("contract.pdf")
|
|
21
|
+
print(proof.id) # tracking ID
|
|
22
|
+
print(proof.anchored_at) # ISO 8601 timestamp
|
|
23
|
+
print(proof.merkle_root) # Merkle root of the batch
|
|
24
|
+
|
|
25
|
+
# Verify locally — no network call
|
|
26
|
+
assert tb.verify(proof)
|
|
27
|
+
|
|
28
|
+
# Or anchor a raw SHA-256 hash without blocking, then wait for the proof.
|
|
29
|
+
job = tb.anchor("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
|
30
|
+
print(job.id) # tracking ID, returned immediately (202)
|
|
31
|
+
proof = tb.anchor_wait(job.id) # blocks up to 11 min
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Python 3.9+
|
|
38
|
+
- Zero runtime dependencies (stdlib only)
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
Full API reference and guides at [api.trustbeat.eu/docs](https://api.trustbeat.eu/docs)
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "trustbeat"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "TrustBeat SDK — qualified timestamping and Merkle anchoring"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Trustbeat s.r.o.", email = "radim.dejmek@trustbeat.eu" }]
|
|
13
|
+
keywords = ["eidas", "timestamp", "rfc3161", "merkle", "anchoring", "trust", "qualified"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Security :: Cryptography",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [] # zero runtime dependencies — stdlib only
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=7", # optional — tests also run with stdlib unittest
|
|
32
|
+
"pytest-cov",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://trustbeat.eu"
|
|
37
|
+
Repository = "https://github.com/TrustBeat/sdk-python"
|
|
38
|
+
Issues = "https://github.com/TrustBeat/sdk-python/issues"
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
|
|
43
|
+
[tool.unittest]
|
|
44
|
+
start-dir = "tests"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["trustbeat"]
|
trustbeat-0.1.0/smoke.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TrustBeat Python SDK smoke CLI — drives the SDK against a LIVE API.
|
|
4
|
+
|
|
5
|
+
Driven by tests/e2e/sdk_smoke.py (the orchestrator), not run directly in CI.
|
|
6
|
+
Two commands:
|
|
7
|
+
|
|
8
|
+
submit anchor TB_HASH, print the tracking id on stdout
|
|
9
|
+
verify <id> fetch the proof via the SDK, check the contract, verify locally
|
|
10
|
+
|
|
11
|
+
Env:
|
|
12
|
+
TB_BASE_URL API base URL (e.g. http://localhost:8080)
|
|
13
|
+
TB_API_KEY provisioned account API key
|
|
14
|
+
TB_HASH SHA-256 hex the SDK anchors / echoes back
|
|
15
|
+
|
|
16
|
+
Exit 0 on success, non-zero on any failure.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
# smoke.py lives next to the `trustbeat` package, so a plain import works when
|
|
26
|
+
# this file's directory is on sys.path[0] (the default for `python smoke.py`).
|
|
27
|
+
from trustbeat import TrustBeat, AiDecisionMetadata, AiTimeEnvelope
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Fixed AI-decision metadata — only the input/output hashes vary per run.
|
|
31
|
+
_AI_META = AiDecisionMetadata(
|
|
32
|
+
model_id="claude-opus-4-8",
|
|
33
|
+
system_name="trustbeat-sdk-smoke",
|
|
34
|
+
risk_category="employment",
|
|
35
|
+
decision_type="classification",
|
|
36
|
+
human_oversight=True,
|
|
37
|
+
time_envelope=AiTimeEnvelope(
|
|
38
|
+
started_at="2026-06-29T10:00:00Z",
|
|
39
|
+
completed_at="2026-06-29T10:00:01Z",
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _client() -> TrustBeat:
|
|
45
|
+
base = os.environ["TB_BASE_URL"]
|
|
46
|
+
key = os.environ["TB_API_KEY"]
|
|
47
|
+
return TrustBeat(key, base_url=base)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_submit() -> int:
|
|
51
|
+
h = os.environ["TB_HASH"]
|
|
52
|
+
job = _client().anchor(h)
|
|
53
|
+
if not job.id:
|
|
54
|
+
print("submit: empty tracking id", file=sys.stderr)
|
|
55
|
+
return 1
|
|
56
|
+
print(job.id)
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_verify(tracking_id: str) -> int:
|
|
61
|
+
expected = os.environ.get("TB_HASH")
|
|
62
|
+
tb = _client()
|
|
63
|
+
|
|
64
|
+
proof = tb.get_proof(tracking_id)
|
|
65
|
+
if proof is None:
|
|
66
|
+
print(f"verify: proof for {tracking_id} not ready", file=sys.stderr)
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
# Contract checks — these are what mocks never exercise.
|
|
70
|
+
if expected and proof.hash.lower() != expected.lower():
|
|
71
|
+
print(f"verify: hash echo mismatch {proof.hash} != {expected}", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
if not proof.merkle_root:
|
|
74
|
+
print("verify: empty merkle_root", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
if not proof.token:
|
|
77
|
+
print("verify: empty token", file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
|
|
80
|
+
# Offline Merkle verification through the SDK.
|
|
81
|
+
if tb.verify(proof) is not True:
|
|
82
|
+
print("verify: local Merkle verification failed", file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
print(f"OK id={tracking_id} root={proof.merkle_root[:16]}… token={len(proof.token)}B")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _batch_hashes() -> list[str]:
|
|
90
|
+
seed = os.environ["TB_BATCH_SEED"]
|
|
91
|
+
n = int(os.environ["TB_BATCH_N"])
|
|
92
|
+
return [hashlib.sha256(f"{seed}::{i}".encode()).hexdigest() for i in range(n)]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_submit_batch() -> int:
|
|
96
|
+
hashes = _batch_hashes()
|
|
97
|
+
sub = _client().anchor_batch(hashes)
|
|
98
|
+
if not sub.submission_id:
|
|
99
|
+
print("submit-batch: empty submission_id", file=sys.stderr)
|
|
100
|
+
return 1
|
|
101
|
+
if len(sub.items) != len(hashes):
|
|
102
|
+
print(f"submit-batch: accepted {len(sub.items)} != {len(hashes)}", file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
print(sub.submission_id)
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cmd_verify_batch(submission_id: str) -> int:
|
|
109
|
+
expected = {h.lower() for h in _batch_hashes()}
|
|
110
|
+
tb = _client()
|
|
111
|
+
|
|
112
|
+
proofs = tb.get_batch_proofs(submission_id)
|
|
113
|
+
if len(proofs) != len(expected):
|
|
114
|
+
print(f"verify-batch: got {len(proofs)} proofs, want {len(expected)}", file=sys.stderr)
|
|
115
|
+
return 1
|
|
116
|
+
if {p.hash.lower() for p in proofs} != expected:
|
|
117
|
+
print("verify-batch: proof hashes do not match submitted set", file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
for p in proofs:
|
|
120
|
+
if not p.merkle_root or not p.token:
|
|
121
|
+
print(f"verify-batch: empty merkle_root/token for {p.id}", file=sys.stderr)
|
|
122
|
+
return 1
|
|
123
|
+
if tb.verify(p) is not True:
|
|
124
|
+
print(f"verify-batch: local Merkle verification failed for {p.id}", file=sys.stderr)
|
|
125
|
+
return 1
|
|
126
|
+
|
|
127
|
+
print(f"OK batch sid={submission_id} n={len(proofs)}")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ── AI decision (EU AI Act Art. 12) ───────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
def cmd_submit_ai() -> int:
|
|
134
|
+
in_hash = os.environ["TB_AI_INPUT"]
|
|
135
|
+
out_hash = os.environ["TB_AI_OUTPUT"]
|
|
136
|
+
job = _client().anchor_ai_decision(in_hash, out_hash, _AI_META)
|
|
137
|
+
if not job.id:
|
|
138
|
+
print("submit-ai: empty tracking id", file=sys.stderr)
|
|
139
|
+
return 1
|
|
140
|
+
print(job.id)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def cmd_verify_ai(tracking_id: str) -> int:
|
|
145
|
+
in_hash = os.environ["TB_AI_INPUT"]
|
|
146
|
+
out_hash = os.environ["TB_AI_OUTPUT"]
|
|
147
|
+
tb = _client()
|
|
148
|
+
|
|
149
|
+
proof = tb.get_ai_decision_proof(tracking_id)
|
|
150
|
+
if proof is None:
|
|
151
|
+
print(f"verify-ai: proof for {tracking_id} not ready", file=sys.stderr)
|
|
152
|
+
return 1
|
|
153
|
+
if proof.input_hash.lower() != in_hash.lower():
|
|
154
|
+
print(f"verify-ai: input_hash echo mismatch {proof.input_hash} != {in_hash}", file=sys.stderr)
|
|
155
|
+
return 1
|
|
156
|
+
if proof.output_hash.lower() != out_hash.lower():
|
|
157
|
+
print(f"verify-ai: output_hash echo mismatch {proof.output_hash} != {out_hash}", file=sys.stderr)
|
|
158
|
+
return 1
|
|
159
|
+
if proof.verification_status != "VERIFIED":
|
|
160
|
+
print(f"verify-ai: status {proof.verification_status} != VERIFIED", file=sys.stderr)
|
|
161
|
+
return 1
|
|
162
|
+
if proof.proof is None:
|
|
163
|
+
print("verify-ai: missing Merkle proof", file=sys.stderr)
|
|
164
|
+
return 1
|
|
165
|
+
if tb.verify(proof.proof) is not True:
|
|
166
|
+
print("verify-ai: local Merkle verification failed", file=sys.stderr)
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
print(f"OK ai id={tracking_id} combined={proof.combined_hash[:16]}…")
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── File anchoring ────────────────────────────────────────────────────────────
|
|
174
|
+
# verify reuses `verify` with TB_HASH set to the file's SHA-256.
|
|
175
|
+
|
|
176
|
+
def cmd_submit_file() -> int:
|
|
177
|
+
path = os.environ["TB_FILE_PATH"]
|
|
178
|
+
job = _client().anchor_file(path)
|
|
179
|
+
if not job.id:
|
|
180
|
+
print("submit-file: empty tracking id", file=sys.stderr)
|
|
181
|
+
return 1
|
|
182
|
+
print(job.id)
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ── NIS2 audit trail ──────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
def cmd_submit_audit() -> int:
|
|
189
|
+
event_id = _client().submit_audit_event(
|
|
190
|
+
os.environ["TB_AUDIT_CATEGORY"],
|
|
191
|
+
os.environ["TB_AUDIT_ACTOR"],
|
|
192
|
+
os.environ["TB_AUDIT_ACTION"],
|
|
193
|
+
os.environ["TB_AUDIT_TS"],
|
|
194
|
+
)
|
|
195
|
+
if not event_id:
|
|
196
|
+
print("submit-audit: empty event_id", file=sys.stderr)
|
|
197
|
+
return 1
|
|
198
|
+
print(event_id)
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_verify_audit(event_id: str) -> int:
|
|
203
|
+
tb = _client()
|
|
204
|
+
proof = tb.get_audit_event_proof(event_id)
|
|
205
|
+
if proof is None:
|
|
206
|
+
print(f"verify-audit: proof for {event_id} not ready", file=sys.stderr)
|
|
207
|
+
return 1
|
|
208
|
+
if proof.event_id != event_id:
|
|
209
|
+
print(f"verify-audit: event_id echo mismatch {proof.event_id} != {event_id}", file=sys.stderr)
|
|
210
|
+
return 1
|
|
211
|
+
if not proof.canonical_hash:
|
|
212
|
+
print("verify-audit: empty canonical_hash", file=sys.stderr)
|
|
213
|
+
return 1
|
|
214
|
+
if not proof.batch_id:
|
|
215
|
+
print("verify-audit: empty batch_id", file=sys.stderr)
|
|
216
|
+
return 1
|
|
217
|
+
if proof.leaf_index < 0 or proof.merkle_path is None:
|
|
218
|
+
print("verify-audit: invalid leaf_index/merkle_path", file=sys.stderr)
|
|
219
|
+
return 1
|
|
220
|
+
|
|
221
|
+
# The event must also surface through the query endpoint.
|
|
222
|
+
events = tb.list_audit_events(trail_category=os.environ["TB_AUDIT_CATEGORY"])
|
|
223
|
+
if not any(e.event_id == event_id for e in events):
|
|
224
|
+
print(f"verify-audit: {event_id} not returned by list_audit_events", file=sys.stderr)
|
|
225
|
+
return 1
|
|
226
|
+
|
|
227
|
+
print(f"OK audit id={event_id} batch={proof.batch_id[:12]}… leaf={proof.leaf_index}")
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ── Signature & certificate verification (synchronous) ────────────────────────
|
|
232
|
+
|
|
233
|
+
def cmd_verify_sig() -> int:
|
|
234
|
+
with open(os.environ["TB_SIG_DOC"], "rb") as f:
|
|
235
|
+
doc = f.read()
|
|
236
|
+
expected = os.environ["TB_SIG_DOCHASH"]
|
|
237
|
+
report = _client().verify_signature(doc, os.environ["TB_SIG_FORMAT"])
|
|
238
|
+
if report.document_hash.lower() != expected.lower():
|
|
239
|
+
print(f"verify-sig: document_hash mismatch {report.document_hash} != {expected}", file=sys.stderr)
|
|
240
|
+
return 1
|
|
241
|
+
if not report.verdict:
|
|
242
|
+
print("verify-sig: empty verdict", file=sys.stderr)
|
|
243
|
+
return 1
|
|
244
|
+
if not report.signatures:
|
|
245
|
+
print("verify-sig: report has no signatures", file=sys.stderr)
|
|
246
|
+
return 1
|
|
247
|
+
print(f"OK sig verdict={report.verdict} signatures={len(report.signatures)}")
|
|
248
|
+
return 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def cmd_validate_cert() -> int:
|
|
252
|
+
with open(os.environ["TB_CERT_PATH"], "rb") as f:
|
|
253
|
+
cert = f.read()
|
|
254
|
+
res = _client().validate_certificate(cert)
|
|
255
|
+
if not res.subject:
|
|
256
|
+
print("validate-cert: empty subject", file=sys.stderr)
|
|
257
|
+
return 1
|
|
258
|
+
if not res.issuer:
|
|
259
|
+
print("validate-cert: empty issuer", file=sys.stderr)
|
|
260
|
+
return 1
|
|
261
|
+
if not res.validated_at:
|
|
262
|
+
print("validate-cert: empty validated_at", file=sys.stderr)
|
|
263
|
+
return 1
|
|
264
|
+
print(f"OK cert subject={res.subject[:24]}… qualified={res.qualified}")
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main() -> int:
|
|
269
|
+
if len(sys.argv) < 2:
|
|
270
|
+
print("usage: smoke.py {submit|verify <id>|submit-batch|verify-batch <id>|"
|
|
271
|
+
"submit-ai|verify-ai <id>|submit-file|submit-audit|verify-audit <id>|"
|
|
272
|
+
"verify-sig|validate-cert}", file=sys.stderr)
|
|
273
|
+
return 2
|
|
274
|
+
cmd = sys.argv[1]
|
|
275
|
+
if cmd == "submit":
|
|
276
|
+
return cmd_submit()
|
|
277
|
+
if cmd == "verify":
|
|
278
|
+
if len(sys.argv) < 3:
|
|
279
|
+
print("usage: smoke.py verify <id>", file=sys.stderr)
|
|
280
|
+
return 2
|
|
281
|
+
return cmd_verify(sys.argv[2])
|
|
282
|
+
if cmd == "submit-batch":
|
|
283
|
+
return cmd_submit_batch()
|
|
284
|
+
if cmd == "verify-batch":
|
|
285
|
+
if len(sys.argv) < 3:
|
|
286
|
+
print("usage: smoke.py verify-batch <id>", file=sys.stderr)
|
|
287
|
+
return 2
|
|
288
|
+
return cmd_verify_batch(sys.argv[2])
|
|
289
|
+
if cmd == "submit-ai":
|
|
290
|
+
return cmd_submit_ai()
|
|
291
|
+
if cmd == "verify-ai":
|
|
292
|
+
if len(sys.argv) < 3:
|
|
293
|
+
print("usage: smoke.py verify-ai <id>", file=sys.stderr)
|
|
294
|
+
return 2
|
|
295
|
+
return cmd_verify_ai(sys.argv[2])
|
|
296
|
+
if cmd == "submit-file":
|
|
297
|
+
return cmd_submit_file()
|
|
298
|
+
if cmd == "submit-audit":
|
|
299
|
+
return cmd_submit_audit()
|
|
300
|
+
if cmd == "verify-audit":
|
|
301
|
+
if len(sys.argv) < 3:
|
|
302
|
+
print("usage: smoke.py verify-audit <id>", file=sys.stderr)
|
|
303
|
+
return 2
|
|
304
|
+
return cmd_verify_audit(sys.argv[2])
|
|
305
|
+
if cmd == "verify-sig":
|
|
306
|
+
return cmd_verify_sig()
|
|
307
|
+
if cmd == "validate-cert":
|
|
308
|
+
return cmd_validate_cert()
|
|
309
|
+
print(f"unknown command: {cmd}", file=sys.stderr)
|
|
310
|
+
return 2
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
sys.exit(main())
|
|
File without changes
|