Metadata-Version: 2.5
Name: trustbeat
Version: 0.5.0
Summary: TrustBeat SDK — qualified timestamping and Merkle anchoring
Project-URL: Homepage, https://trustbeat.eu
Project-URL: Repository, https://github.com/TrustBeat/sdk-python
Project-URL: Issues, https://github.com/TrustBeat/sdk-python/issues
Author-email: "Trustbeat s.r.o." <radim.dejmek@trustbeat.eu>
License: MIT
License-File: LICENSE
Keywords: anchoring,eidas,merkle,qualified,rfc3161,timestamp,trust
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Description-Content-Type: text/markdown

# TrustBeat Python SDK

Qualified electronic timestamps and Merkle anchoring — eIDAS-compliant, over a simple API.

Part of **[TrustBeat](https://trustbeat.eu)** — digital trust infrastructure for the EU.
All SDKs (Python, TypeScript, Java, C#, Go): **[trustbeat.eu/sdks](https://trustbeat.eu/sdks)**.

## Install

```bash
pip install trustbeat
```

## Quickstart

```python
from trustbeat import TrustBeat

tb = TrustBeat(api_key="tb_live_...")

# Anchor a file (SHA-256 computed locally, file never leaves your machine).
# anchor_file_wait() blocks until the proof is ready (next batch, up to 11 min).
proof = tb.anchor_file_wait("contract.pdf")
print(proof.id)           # tracking ID
print(proof.anchored_at)  # ISO 8601 timestamp
print(proof.merkle_root)  # Merkle root of the batch

# Verify locally — no network call
assert tb.verify(proof)

# Or anchor a raw SHA-256 hash without blocking, then wait for the proof.
job = tb.anchor("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
print(job.id)                   # tracking ID, returned immediately (202)
proof = tb.anchor_wait(job.id)  # blocks up to 11 min

```

## Tamper-Evident Logs (NIS2)

Anchor a log hash together with canonical metadata for NIS2 Article 21 audit trails.
The server seals your metadata into the Merkle leaf, so the proof covers both the log
content and its context.

```python
import hashlib
from trustbeat import TrustBeat, LogMetadata, LogSource, LogSourceIdentity, LogTimeEnvelope

tb = TrustBeat(api_key="tb_live_...")

# Hash the log yourself — content never leaves your machine.
with open("app.log", "rb") as f:
    log_hash = hashlib.sha256(f.read()).hexdigest()

job = tb.anchor_log(
    log_hash,
    LogMetadata(
        log_source=LogSource(uri="/var/log/app.log", name="Application log"),
        source_identity=LogSourceIdentity(hostname="web-01", service_name="payments"),
        time_envelope=LogTimeEnvelope(start_at="2026-04-15T00:00:00Z",
                                      end_at="2026-04-15T23:59:59Z"),
    ),
    label="incident-2026-05",
)
print(job.id, job.combined_hash)

# Wait for the qualified anchor (next batch, up to 11 min), then verify locally.
proof = tb.anchor_log_wait(job.id)
assert proof.verification_status == "VERIFIED"
assert tb.verify(proof.proof)
```

## Webhooks

If your account has a webhook secret configured, every delivery is signed with
an `X-TrustBeat-Signature` header. Verify it with the raw request body —
before any JSON parsing:

```python
from trustbeat import verify_webhook_signature

# e.g. in a Flask/FastAPI handler; body must be the raw bytes as received
if not verify_webhook_signature(raw_body, signature_header, webhook_secret):
    raise ValueError("Invalid webhook signature")
```

Also available as `TrustBeat.verify_webhook_signature(...)`. Rejects replays
older than 5 minutes by default (`tolerance_secs` to override).

Portable proof bundles for offline verification: `export_ai_decision(id)`,
`export_verification(id)`, `export_log(id)` — each returns raw JSON bundle bytes.

## Batches and rate limits

A batch carries up to **1,000** hashes and is all-or-nothing: if the call fails, none of them
was queued. (API servers deployed before 26 Sep 2026 accept at most 100.)

Anchoring is rate-limited per account (see your plan). A rate-limited request (HTTP 429) is
**retried automatically**, waiting the `Retry-After` the server sends — twice by default.
Retrying is always safe: a refused submission was never queued. When the retries run out you
get the rate-limit error, carrying the wait the server asked for.

```python
from trustbeat import TrustBeat, RateLimitError

tb = TrustBeat(api_key="tb_live_...", max_retries=2)   # 0 turns retrying off

try:
    submission = tb.anchor_batch(hashes)               # up to 1,000
except RateLimitError as e:
    print(f"still limited; server asked to wait {e.retry_after} s")
```

## Requirements

- Python 3.9+
- Zero runtime dependencies (stdlib only)

## Documentation

Full API reference and guides at [api.trustbeat.eu/docs](https://api.trustbeat.eu/docs)

## License

MIT — see [LICENSE](LICENSE)


### Merkle algorithm

Every proof declares how it must be folded, in `proof.merkle_algorithm`:

| Value | Construction |
|---|---|
| `trustbeat-legacy-sha256` | leaf = your hash, parent = `SHA-256(left \|\| right)` |
| `rfc6962-sha256` | leaf = `SHA-256(0x00 \|\| hash)`, parent = `SHA-256(0x01 \|\| left \|\| right)` |

`verify()` dispatches on it for you. A proof with no label was issued before the
field existed and is legacy. If a proof declares an algorithm this SDK version
does not implement, `verify()` raises `UnsupportedAlgorithmError` rather than
returning `False` — "cannot check" is not "invalid".

```python
from trustbeat import UnsupportedAlgorithmError

try:
    ok = tb.verify(proof)
except UnsupportedAlgorithmError:
    # This SDK is older than the proof. Upgrade, or check it server-side.
    raise
```

### Audit event proofs

`verify_audit_event()` folds an audit event proof the same way:

```python
from trustbeat import IncompleteProofError

proof = tb.get_audit_event_proof(event_id)
try:
    assert tb.verify_audit_event(proof)
except IncompleteProofError:
    # The server predates API 1.46 and sent no merkle_root, so there is nothing
    # to fold against. The proof is not invalid — verify it server-side instead.
    pass
```

Treating that exception as a failed proof would be wrong: it means "cannot
check", not "tampered".
