# 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

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 a file (streamed, constant memory)
file_hash = hash_file("/path/to/document.pdf")
print(f"SHA-256: {file_hash}")

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

# Verify a file against a known hash
assert hash_file("/path/to/document.pdf") == file_hash
```

Or hash + attest in one step:

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

## 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)
```

## 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
