Metadata-Version: 2.4
Name: certisigma
Version: 1.10.0
Summary: Official Python SDK for the CertiSigma attestation & verification API
Author: CertiSigma
License: MIT
Project-URL: Homepage, https://developers.certisigma.ch/sdk
Project-URL: Documentation, https://developers.certisigma.ch/sdk
Project-URL: API Reference, https://developers.certisigma.ch
Project-URL: Repository, https://github.com/massimocavallin/CertiSigma
Project-URL: Changelog, https://developers.certisigma.ch/sdk#changelog
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.25.0
Provides-Extra: crypto
Requires-Dist: cryptography>=44.0.0; extra == "crypto"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: cryptography>=44.0.0; extra == "dev"

# CertiSigma Python SDK

Official Python client for the [CertiSigma](https://developers.certisigma.ch/sdk) cryptographic attestation [API](https://developers.certisigma.ch).

[![PyPI](https://img.shields.io/pypi/v/certisigma)](https://pypi.org/project/certisigma/)
[![npm](https://img.shields.io/npm/v/@certisigma/sdk)](https://www.npmjs.com/package/@certisigma/sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

## Installation

```bash
pip install certisigma
```

Or from source:

```bash
cd sdk/python
pip install -e .
```

## Quick Start

```python
import os
from certisigma import CertiSigmaClient, hash_file, hash_bytes, hash_string

client = CertiSigmaClient(api_key=os.environ["CERTISIGMA_API_KEY"])

# 1. Compute the SHA-256 hash of your file
file_hash = hash_file("contract.pdf")

# 2. Attest — creates a timestamped, signed proof of existence
result = client.attest(file_hash, source="my-app")
print(f"Attestation: {result.id} at {result.timestamp}")
print(f"ECDSA signature: {result.signature}")

# 3. Verify — confirm the hash was attested
check = client.verify(file_hash)
print(f"Exists: {check.exists}, Level: {check.level}")

# Or hash raw bytes
data_hash = hash_bytes(b"any raw content")
result = client.attest(data_hash)
```

## Public Verification (No API Key)

Verification endpoints are public. You can verify attestations without any API key:

```python
from certisigma import CertiSigmaClient, hash_file

# No api_key needed — works out of the box
client = CertiSigmaClient()

file_hash = hash_file("contract.pdf")
check = client.verify(file_hash)
print(f"Exists: {check.exists}, Level: {check.level}")

# Batch verify also works without a key
results = client.batch_verify([file_hash])
print(f"Found: {results.found}/{results.count}")
```

## Hashing Utilities

Standalone SHA-256 hash functions -- compute hashes without attestation:

```python
from certisigma import hash_file, hash_bytes, hash_string

# Hash a file (streamed, constant memory)
file_hash = hash_file("/path/to/document.pdf")

# Hash raw bytes
data_hash = hash_bytes(b"raw content")

# Hash a string (UTF-8, no file needed)
str_hash = hash_string("SN-2026-001234")
```

Or hash + attest in one step:

```python
result = client.attest_file("/path/to/document.pdf")
print(f"Attested: {result.hash_hex}")
```

## String Attestation

Attest identifiers, serial numbers, or any non-file content without temporary files.
The string is hashed client-side (UTF-8); only the hash reaches the API:

```python
# One-step: hash + attest
result = client.attest_string("SN-2026-001234", source="serial-registry")

# Verify later
check = client.verify_string("SN-2026-001234")
assert check.exists

# Also works for files
check = client.verify_file("/path/to/document.pdf")
```

> **Canonical form:** UTF-8, no BOM, no trailing newline, no normalization.
> `hash_string("SN-001")` and `hash_string("SN-001\n")` produce different hashes.
>
> **Note:** `source` is never auto-populated from the string content. Pass it
> explicitly if needed — otherwise the string would leak into server-visible metadata.

## Async Support

```python
import asyncio, os
from certisigma import AsyncCertiSigmaClient

async def main():
    async with AsyncCertiSigmaClient(api_key=os.environ["CERTISIGMA_API_KEY"]) as client:
        result = await client.attest("abcdef..." * 4 + "0" * 16)
        print(result.id)

asyncio.run(main())
```

## Batch Operations

```python
# Attest up to 100 hashes in one call — returns full claim metadata per item
batch = client.batch_attest(
    ["aabb..." * 4, "ccdd..." * 4],
    source="monthly-invoices"
)
print(f"Created: {batch.created}, Existing: {batch.existing}")
for att in batch.attestations:
    print(f"  {att['id']} claim={att['claim_id']} src={att['source']}")

# Verify batch (public, no key needed)
results = client.batch_verify(["aabb..." * 4, "ccdd..." * 4])
print(f"Found: {results.found}/{results.count}")

# Detailed mode — certification level + claim metadata (requires api_key)
results = client.batch_verify(["aabb..." * 4], detailed=True)
for r in results.results:
    if r["exists"]:
        print(f"  {r['id']} level={r['level']} src={r['source']}")
```

## Attestation Status

Check the current trust tier of an attestation (public, no API key needed):

```python
status = client.status("att_1234")
print(f"Level: {status.level}")  # "T0", "T1", or "T2"
print(f"Signature: {status.signature_available}")
print(f"Merkle: {status.merkle_proof_available}")
print(f"OTS: {status.ots_available}")
```

| Method | Input | Auth | Best for |
|--------|-------|------|----------|
| `verify(hash)` | SHA-256 hash | Optional | Compliance checks, "does this hash exist?" |
| `status(att_id)` | Attestation ID | No | Dashboards, progress tracking, polling |
| `get_evidence(att_id)` | Attestation ID | No | Independent verification, long-term archival |

## Metadata Management

```python
# Update claim metadata
result = client.update_metadata("att_1234", source="pipeline-v2", extra_data={"project": "alpha"})

# Soft-delete claim
client.delete_metadata("att_1234")

# Get evidence
evidence = client.get_evidence("att_1234")
print(evidence.level, evidence.t0)
```

## Evidence & OTS Verification

```python
import os
from certisigma import CertiSigmaClient, get_blockchain_url, save_ots_proof

client = CertiSigmaClient(api_key=os.environ["CERTISIGMA_API_KEY"])

# Get full cryptographic evidence (T0 + T1 + T2)
evidence = client.get_evidence("att_1234")

if evidence.level == "T2":
    # Bitcoin block explorer link
    print(get_blockchain_url(evidence))        # mempool.space/block/...
    print(get_blockchain_url(evidence, "tx"))   # mempool.space/tx/...

    # Save the raw .ots proof for independent verification
    save_ots_proof(evidence, "contract.pdf.ots")
    # Then verify with: ots verify contract.pdf.ots
```

## Client-Side Encryption (Zero Knowledge)

Requires: `pip install certisigma[crypto]`

```python
from certisigma.crypto import generate_key, encrypt_metadata, decrypt_metadata

# Generate a key (store securely — server never sees it)
key = generate_key()

# Encrypt before sending
encrypted = encrypt_metadata({"secret": "classified"}, key)
result = client.attest(hash_hex, extra_data=encrypted, client_encrypted=True)

# Decrypt after retrieving
plaintext = decrypt_metadata(result.extra_data, key)
```

## Error Handling

```python
from certisigma import (
    CertiSigmaError,
    AuthenticationError,
    RateLimitError,
    QuotaExceededError,
)

try:
    client.attest(hash_hex)
except AuthenticationError:
    print("Invalid API key")
except RateLimitError as e:
    print(f"Rate limited, retry after {e.retry_after}s")
except QuotaExceededError:
    print("Monthly quota reached")
except CertiSigmaError as e:
    print(f"API error {e.status_code}: {e}")
```

## Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `api_key` | `None` | Bearer token (`cs_live_...`). Optional for verify/health. |
| `base_url` | `https://api.certisigma.ch` | API endpoint |
| `timeout` | `30.0` | Request timeout in seconds |

**Proxy:** `httpx` respects `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` environment variables.
**Custom TLS CA:** Configure `httpx` with `verify="/path/to/ca-bundle.pem"` for corporate proxies.
**Retry:** Not built-in. Implement at caller level — retry only on `429` and `5xx`, fail fast on other `4xx`.

## Share Tokens (Forensic Metadata Sharing)

Create time-limited, auditable tokens for sharing attestation metadata with forensic analysts:

```python
token = client.create_share_token(
    attestation_ids=[42, 43, 44],
    expires_in=86400,
    recipient_label="forensic-analyst",
    max_uses=10,
)
print(f"Share token: {token.share_token}")  # shown once, save it

tokens = client.list_share_tokens()
info = client.get_share_token_info(token.id)
client.revoke_share_token(token.id)
```

## Structured Tagging

Multi-dimensional classification of attestations with server-side querying:

```python
# Upsert tags (max 50 per attestation per key)
client.put_tags("att_42", tags=[
    {"key": "department", "value": "hr"},
    {"key": "classification", "value": "confidential"},
])

tags = client.get_tags("att_42")

# Query by tags — AND semantics, max 10 conditions per query
results = client.query_tags(
    filter={"and": [
        {"key": "department", "value": "hr"},
        {"key": "classification", "value": "confidential"},
    ]},
    limit=100,
)
print(f"Found {results.count} matching attestations")

client.delete_tag("att_42", "classification")
```

Tag key format: `^[a-z][a-z0-9_-]{0,62}$` (lowercase, no `_` prefix — reserved). Client-encrypted tags (`value_enc` + `value_nonce` hex) are supported but excluded from server-side query.

## Census — Derived Lists

Create opaque HMAC-SHA256 derived lists for third-party hash verification without revealing your inventory. Requires `census` scope and `org_id`.

```python
import hashlib, hmac

# Create a derived list
dl = client.create_derived_list(
    hashes=["a665a459...", "b4c9a289..."],
    label="Partner audit Q1",
    expires_in_hours=720,
)
print(f"List key (save now!): {dl.list_key}")

# Third party: match files (no API key needed)
public = CertiSigmaClient()
file_hash = "a665a459..."
derived = hmac.new(
    bytes.fromhex(dl.list_key), file_hash.encode(), hashlib.sha256,
).hexdigest()
result = public.match_derived_list(dl.id, dl.list_key, [derived])
print(f"Matched: {result.matched}/{result.total}")

# Owner operations
lists = client.list_derived_lists()
detail = client.get_derived_list(dl.id)
sig = client.get_derived_list_signature(dl.id)
log = client.get_derived_list_access_log(dl.id)
client.revoke_derived_list(dl.id)
```

## Webhooks (T1 / T2 lifecycle)

Requires an API key with the **`webhook`** scope. Events: `t1_complete`, `t2_complete`. The backend validates URLs (HTTPS in production; SSRF protections apply server-side).

```python
import os
from certisigma import CertiSigmaClient, verify_webhook_signature

client = CertiSigmaClient(api_key=os.environ["CERTISIGMA_API_KEY"])

# Register — signing_secret is shown once; store it securely (env/secret manager)
wh = client.register_webhook(
    url="https://your-server.example.com/certisigma-hook",
    events=["t1_complete", "t2_complete"],
    label="census-prod",  # optional
)
print(wh.id, wh.signing_secret)  # never log signing_secret

# List / delete / delivery history
all_hooks = client.list_webhooks()
client.delete_webhook(wh.id)
history = client.list_webhook_deliveries(wh.id)

# In your HTTP handler: verify raw body bytes against X-CertiSigma-Signature
# is_ok = verify_webhook_signature(request.body(), signature_header, signing_secret)
```

`verify_webhook_signature` uses `hmac.compare_digest` (constant-time). Pass **raw request body as `bytes`**, not a parsed JSON string, or verification will fail.

The public API **does not** accept a custom signing secret on `register_webhook` (omit `secret` or use `null` in raw JSON). The server always returns a random `signing_secret` once per registration.

`AsyncCertiSigmaClient` exposes the same four methods (`register_webhook`, `list_webhooks`, `delete_webhook`, `list_webhook_deliveries`).

## Read Metadata

Explicit metadata read without re-verifying:

```python
meta = client.get_metadata("att_42")
print(f"Source: {meta.source}, Extra: {meta.extra_data}")
```

## Security & Privacy

CertiSigma is a public attestation platform. Cryptographic proofs (hash, signature, Merkle, OTS) are intentionally public and verifiable by any party. Organizational metadata (`source`, `extra_data`, tags) is never exposed on public endpoints — only the authenticated API key owner can read their own claim data. Tags are scoped per API key with full tenant isolation. Share tokens provide time-limited, auditable, revocable read-only access to specific attestations — all accesses are logged. For sensitive metadata, use client-side encryption (`encrypt_metadata()`); for sensitive tag values, use client-side encrypted tags (`value_enc` + `value_nonce`).

See the [Security & Privacy Model](https://developers.certisigma.ch/sdk#security-privacy-model) in the full documentation for the complete threat model and data boundary.

## Test Vectors

Canonical test vectors with real production data are available for independent T0/T1/T2 verification:

- [Test Vectors documentation](https://developers.certisigma.ch/sdk#test-vectors)
- [Machine-readable JSON](https://developers.certisigma.ch/test-vectors.json)

## Compatibility

- Follows [Semantic Versioning 2.0.0](https://semver.org/).
- SDK v1.x targets API v1 (`/v1/` prefix). Breaking API changes get a new prefix with 12-month deprecation window.
- See [full SDK documentation](https://developers.certisigma.ch/sdk) for error codes, webhook semantics, T0 signature format, and compatibility policy.

## Requirements

- Python 3.10+
- `httpx` >= 0.25.0
- `cryptography` >= 44.0.0 (optional, for `certisigma.crypto`)

## License

MIT — Ten Sigma Sagl, Lugano, Switzerland
