Metadata-Version: 2.4
Name: certysign-sdk
Version: 1.3.0
Summary: CertySign Trust Services SDK — sign, verify, and manage digital signatures
Author-email: CertySign Trust Services <sdk@certysign.io>
License-Expression: MIT
Project-URL: Homepage, https://certysign.io
Project-URL: Documentation, https://docs.certysign.io
Project-URL: Repository, https://github.com/certysign/certysign-python-sdk
Project-URL: Bug Tracker, https://github.com/certysign/certysign-python-sdk/issues
Keywords: digital-signature,pades,pkcs7,esign,trust-services,certificate,pki,crl,ocsp,certysign
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
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 :: Internet :: WWW/HTTP
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31.0
Requires-Dist: pypdf>=4.0.0
Requires-Dist: cryptography>=41.0.0
Provides-Extra: asn1
Requires-Dist: asn1crypto>=1.5.1; extra == "asn1"
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: responses>=0.25.0; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: types-requests; extra == "dev"

# CertySign SDK — Python

[![PyPI version](https://badge.fury.io/py/certysign-sdk.svg)](https://pypi.org/project/certysign-sdk/)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://python.org)

Official Python SDK for the [CertySign Trust Services](https://certysign.io) platform. Mirrors the `@certysign/sdk` Node.js package feature-for-feature.

> **Privacy-first**: Your documents never leave your infrastructure. Only cryptographic hashes are transmitted to the API.

---

## Installation

```bash
pip install certysign-sdk
```

For PAdES PDF signing with TSA timestamp support:

```bash
pip install "certysign-sdk[asn1]"
```

---

## Quick start

```python
from certysign_sdk import CertySignClient

client = CertySignClient(
    public_key="YOUR_API_KEY_ID",
    secret_key="YOUR_API_KEY_SECRET",
    environment="staging",
)

# Optional: check that the configured key can reach the API
status = client.ping()
print(status["success"])
```

---

## Core workflows

### 1 — Hash-based signing (recommended)

The document never leaves your server.

```python
# Hash raw document bytes locally and send only the hash
result = client.sign.hash_and_sign(
    document=open("invoice.pdf", "rb").read(),
    file_name="invoice.pdf",
    reason="Invoice approval",
)
```

If you already computed the hash yourself, use `sign_hash()` instead:

```python
doc_hash = client.hasher.hash_file("invoice.pdf")

result = client.sign.sign_hash(
    document_hash=doc_hash["hash"],
    hash_algorithm=doc_hash["algorithm"],
    file_name="invoice.pdf",
)
```

### 2 — Local PDF signature embedding (PAdES)

```python
with open("invoice.pdf", "rb") as f:
    pdf_bytes = f.read()

signed_pdf = client.embedder.embed_in_pdf(
    pdf_bytes,
    reason="Invoice approval",
    sign_callback=lambda h: client.sign.sign_hash(
        document_hash=h,
        hash_algorithm="sha256",
    ),
)

with open("invoice-signed.pdf", "wb") as f:
    f.write(signed_pdf)
```

### 3 — PAdES B-T (with RFC 3161 timestamp)

```python
client = CertySignClient(
    public_key="...",
    secret_key="...",
    tsa_url="https://tsa.certysign.io",  # upgrades to PAdES B-T automatically
)

signed_pdf = client.embedder.embed_in_pdf(pdf_bytes, sign_callback=...)
```

### 4 — XML signing (XMLDSig enveloped)

```python
with open("document.xml") as f:
    xml_content = f.read()

# Hash and sign
doc_hash = client.hasher.hash(xml_content.encode())
result = client.sign.sign_hash(
    document_hash=doc_hash["hash"],
    hash_algorithm=doc_hash["algorithm"],
)

# Embed signature
signed_xml = client.embedder.embed_in_xml(
    xml_content,
    signature=result["data"]["signature"],
    certificate=result["data"]["certificate"],
    document_hash=doc_hash["hash"],
)
```

### 5 — JSON signing

```python
payload = {"amount": 1500, "currency": "USD", "recipient": "Alice"}
doc_hash = client.hasher.hash(json.dumps(payload).encode())
result = client.sign.sign_hash(document_hash=doc_hash["hash"])

signed_envelope = client.embedder.embed_in_json(
    payload,
    signature=result["data"]["signature"],
    certificate=result["data"]["certificate"],
)
```

### 6 — Signature envelope workflow (multi-recipient)

```python
# Create envelope
envelope = client.envelopes.create(
    title="Service Agreement",
    signers=[
        {"email": "alice@example.com", "name": "Alice"},
        {"email": "bob@example.com", "name": "Bob"},
    ],
)

# Upload documents
client.envelopes.upload_documents(
    envelope["data"]["id"],
    [{"name": "agreement.pdf", "data": open("agreement.pdf", "rb").read()}],
)

# Send to signers
client.envelopes.send(envelope["data"]["id"])
```

### 7 — Signing sessions (OTP verification)

```python
session = client.sessions.create(
    envelope_id=envelope_id,
    recipients=[{"email": "alice@example.com"}],
)

# Trigger OTP delivery
client.sessions.send_otp(session["data"]["id"], recipient_id)

# Verify OTP and sign
client.sessions.verify_otp(session["data"]["id"], recipient_id, "123456")
client.sessions.recipient_sign(session["data"]["id"], recipient_id, signing_token)
```

---

## API Reference

### `CertySignClient`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `public_key` | str | *required* | `X-API-Key-Id` header value |
| `secret_key` | str | *required* | `X-API-Key-Secret` header value |
| `environment` | str | `'production'` | `'production'` \| `'staging'` \| `'development'` \| `'test'` |
| `base_url` | str | `None` | Override API base URL completely |
| `timeout` | int | `30` | HTTP timeout in seconds |
| `retries` | int | `3` | Max retries for transient failures |
| `debug` | bool | `False` | Verbose request logging |
| `tsa_url` | str | `None` | TSA service URL for PAdES-T timestamps |

### Resources

| Property | Class | Description |
|----------|-------|-------------|
| `client.sign` | `HashSigningResource` | Hash-based signing (recommended) |
| `client.certificates` | `CertificateResource` | Certificate lifecycle |
| `client.pki` | `PkiResource` | CRL, OCSP, chain, info |
| `client.envelopes` | `EnvelopeResource` | Signature envelopes |
| `client.sessions` | `SigningSessionResource` | OTP-verified signing sessions |
| `client.dashboard` | `DashboardResource` | Analytics |
| `client.hasher` | `DocumentHasher` | Local hashing (no network) |
| `client.embedder` | `SignatureEmbedder` | Local signature embedding (no network) |
| `client.legacy_sign` | `SigningResource` | Legacy upload-based signing |

---

## `DocumentHasher`

```python
from certysign_sdk.utils import DocumentHasher

hasher = DocumentHasher()

# Hash bytes
result = hasher.hash(b"hello world")
# → {"hash": "b94d27b9...", "algorithm": "sha256", "size": 11}

# Hash a file
result = hasher.hash_file("document.pdf")
# → {"hash": "...", "algorithm": "sha256", "size": 204800, "file_name": "document.pdf"}

# Hash many documents
results = hasher.hash_many([b"doc1", b"doc2"])

# Hash multiple files
results = hasher.hash_files(["a.pdf", "b.pdf"])
```

Supported algorithms: `sha256` (default), `sha384`, `sha512`.

---

## `SignatureEmbedder`

```python
from certysign_sdk.utils import SignatureEmbedder

embedder = SignatureEmbedder(tsa_url="https://tsa.certysign.io")

# PDF (PAdES)
signed_pdf = embedder.embed_in_pdf(pdf_bytes, sign_callback=my_sign_fn)

# XML (XMLDSig enveloped)
signed_xml = embedder.embed_in_xml(xml_str, signature="...", certificate="...")

# JSON (signed envelope)
signed_json = embedder.embed_in_json(data, signature="...", certificate="...")
```

---

## Error handling

```python
from certysign_sdk import CertySignError

try:
    result = client.sign.hash_and_sign(document_hash="...")
except CertySignError as e:
    print(f"Status: {e.status_code}, Code: {e.code}, Message: {e}")
    print(f"Details: {e.details}")
```

---

## Environments

| `environment` | Base URL |
|---------------|----------|
| `production`  | `https://core.certysign.io` |
| `staging`     | `https://service.certysign.io` |
| `development` | `http://localhost:8000` |
| `test`        | `http://localhost:8000` |

---

## License

MIT © CertySign Trust Services
