Diogenes Developer SDK Reference¶
The Diogenes Developer SDK provides a high-level Python API for document signing, verification, trust evaluation, and webhook management. It wraps the core Diogenes modules behind a clean facade so you can integrate cryptographic provenance into your application with minimal boilerplate.
Installation¶
From a GitHub Release asset (recommended for partners)¶
Internal teams and partners install a pinned, versioned wheel
attached to a GitHub Release on the (private) ubiquitousthey/diogenes
repository. See Installing the Diogenes SDK for
the full guide, including CI usage, latest-release lookup, and
pyproject.toml pinning.
Quick form, for tag v0.0.2:
gh release download v0.0.2 \
--repo ubiquitousthey/diogenes \
--pattern '*.whl' \
--dir /tmp
pip install /tmp/diogenes_sdk-0.0.2-py3-none-any.whl
gh must be authenticated with a PAT (repo scope) — the repo is
private, so the raw release-asset URL is not directly pip-installable.
From a local checkout (development)¶
If you're working in the monorepo itself, install editable with dev dependencies:
Or just the runtime package:
The SDK is included in the diogenes package at diogenes.sdk.
Quickstart¶
from diogenes.sdk import DiogenesSDK, generate_key_pair, compute_fingerprint
from diogenes.core.transparency_log import TransparencyLogService
from diogenes.core.schemas import EventType
from diogenes.core.keys import _serialize_public_key, compute_fingerprint as core_fp
# 1. Set up a transparency log
log = TransparencyLogService()
# 2. Generate and register a key pair
private_key, public_key = generate_key_pair("ed25519")
fp = core_fp(public_key)
log.append_entry(EventType.KEY_REGISTRATION, {
"fingerprint": fp,
"algorithm": "ed25519",
"public_key": _serialize_public_key(public_key),
})
# 3. Create the SDK client
sdk = DiogenesSDK(transparency_log=log)
# 4. Sign a document
receipt = sdk.sign("Hello, Diogenes!", private_key)
manifest, attestation = receipt.manifest, receipt.attestation
# receipt.leaf_index → pass to GET /api/v1/log/proof/{leaf_index}
# receipt.log_entry_id → autoincrement PK on the log
# Legacy 2-tuple unpack still works: ``manifest, attestation = sdk.sign(...)``
# 5. Verify the document
result = sdk.verify(manifest, source_bytes="Hello, Diogenes!")
print(result.overall.value) # "valid"
See examples/sdk_quickstart.py for a complete runnable example including trust evaluation and webhooks.
Core Classes¶
DiogenesSDK¶
The primary entry point. Wraps signing, verification, and trust evaluation.
Constructor¶
DiogenesSDK(
transparency_log: TransparencyLogService,
endorsement_service: EndorsementService | None = None,
trust_config: TrustConfiguration | None = None,
)
| Parameter | Required | Description |
|---|---|---|
transparency_log |
Yes | The transparency log service for key registration and attestation recording. |
endorsement_service |
No | Required for evaluate_trust() and explain_trust(). |
trust_config |
No | Required for evaluate_trust() and explain_trust(). |
Methods¶
sign(content, private_key, attestation_type="authorship", predecessors=None, intent_statement=None)¶
Sign content and produce a manifest with an attestation.
| Parameter | Type | Default | Description |
|---|---|---|---|
content |
bytes \| str |
-- | The document content to sign. |
private_key |
PrivateKey |
-- | The signer's private key (must be registered). |
attestation_type |
str |
"authorship" |
One of: authorship, editorial, review, publication, custodial_transcription, translation, compilation. |
predecessors |
list[str] \| None |
None |
Predecessor attestation IDs for DAG chaining. |
intent_statement |
str \| None |
None |
Human-readable intent statement. |
Returns: SignReceipt — a frozen dataclass exposing manifest, attestation, log_entry_id, leaf_index, fingerprint, and pseudonym. Iterates as (manifest, attestation) so legacy callers can still unpack with manifest, attestation = sdk.sign(...). leaf_index is the 0-based Merkle position — pass it to GET /api/v1/log/proof/{leaf_index} to fetch an inclusion proof for offline-verification bundles (issue #375).
Raises: SigningError if signing fails or the server response is missing required receipt fields.
receipt = sdk.sign("My document", private_key)
print(receipt.attestation.id, receipt.attestation.signature)
print(receipt.log_entry_id, receipt.leaf_index)
# Legacy unpack still works:
manifest, attestation = sdk.sign("My document", private_key)
verify(manifest, source_bytes=None)¶
Verify a signed manifest.
| Parameter | Type | Default | Description |
|---|---|---|---|
manifest |
Manifest |
-- | The signed manifest to verify. |
source_bytes |
bytes \| str \| None |
None |
Original content for hash verification. If None, hash-only verification is used. |
Returns: VerificationResult with overall, layer1 (crypto), layer2 (key status), and optionally layer3 (trust) results.
Raises: VerificationError if verification encounters an error.
evaluate_trust(fingerprint)¶
Evaluate trust for a key fingerprint using the configured trust anchors and endorsement graph.
| Parameter | Type | Description |
|---|---|---|
fingerprint |
str |
SHA-256 fingerprint of the public key (with sha256: prefix). |
Returns: TrustPathReport with aggregate_score, threshold_met, and contributing_paths.
Raises: TrustEvaluationError if trust evaluation is not configured or fails.
explain_trust(fingerprint)¶
Get a human-readable trust explanation for a key.
| Parameter | Type | Description |
|---|---|---|
fingerprint |
str |
SHA-256 fingerprint of the public key. |
Returns: TrustExplanation with trusted, score, summary, contributing_paths, and recommendations.
Raises: TrustEvaluationError if trust evaluation is not configured or fails.
explanation = sdk.explain_trust("sha256:abc123...")
print(explanation.summary)
# "Yes, you should trust this key. It scores 0.90 against ..."
Key Management¶
generate_key_pair(algorithm="ed25519")¶
Generate a cryptographic key pair.
| Parameter | Type | Default | Description |
|---|---|---|---|
algorithm |
str |
"ed25519" |
One of "ed25519", "ecdsa-p256", "rsa-2048". |
Returns: tuple[PrivateKey, PublicKey]
compute_fingerprint(public_key)¶
Compute the SHA-256 fingerprint of a public key.
Returns: Hex-encoded string (64 characters).
from diogenes.sdk import compute_fingerprint
fp = compute_fingerprint(public_key)
# "a1b2c3d4e5f6..."
serialize_private_key(key, password=None)¶
Serialize a private key to PEM format, optionally encrypted.
from diogenes.sdk import serialize_private_key
pem = serialize_private_key(private_key, password=b"secret")
load_private_key(data, password=None)¶
Load a private key from PEM-encoded bytes.
Trust Configuration¶
load_trust_config(source)¶
Load a TrustConfiguration from a JSON string.
from diogenes.sdk import load_trust_config
config = load_trust_config('{"direct_signer_anchors": [{"fingerprint": "sha256:abc", "weight": 0.9}], "minimum_threshold": 0.5}')
Raises: TrustConfigError on invalid JSON or validation failure.
load_trust_config_file(path)¶
Load a TrustConfiguration from a JSON file on disk.
from diogenes.sdk import load_trust_config_file
config = load_trust_config_file("trust-policy.json")
Raises: TrustConfigError on file read or validation failure.
Encrypted Claims¶
The Diogenes server records opaque metadata claims on the public transparency
log via the encrypted-claims primitive (ECIES P-256 + AES-GCM, with a blinded
commitment for auditor-verifiable reveal). The SDK exposes a high-level wrapper
so consumers do not need to re-implement the cryptography against the raw
/api/v1/claims/* endpoints.
When to use it. Use cases that need opaque-on-the-log metadata — sealed contracts pre-disclosure, medical-records claims, embargoed-journalism attestations, and the Diogenes Vault sealed-manifest mode — are the natural consumers. The encrypted payload sits on the public log indefinitely; only holders of the recipient's P-256 private key can decrypt it, and a party holding both the plaintext and that entry's commitment key can verify a revealed plaintext without ever needing decryption capability.
What this claim's commitment does and does not guarantee¶
Scope: this section is about encrypted-claim commitments specifically. It is not a statement about redacted fields elsewhere on the log.
- Ciphertext. ECIES against the recipient's P-256 key: only a holder of that private key recovers the plaintext.
- Commitment (since #517).
HMAC-SHA256(commitment_key, canonical_json(plaintext)), wherecommitment_keyis derived from the same ECDH shared secret that protects the ciphertext, under a distinct HKDF label. The key is never published. The log alone therefore reveals nothing about the plaintext — which was not true before #517: the commitment was then an unkeyedSHA-256of a 4–5 field structured plaintext, recoverable by offline brute force in seconds, and identical for equal plaintexts across entries. - Unlinkability. The ephemeral key is fresh per encryption, so the same plaintext sealed twice produces unrelated commitments.
- The trade, stated plainly. The commitment is no longer verifiable by a member of the public who has not been given that entry's commitment key. A third party handed a non-matching result cannot distinguish "wrong plaintext" from "you were given the wrong key". Three things bound that: the reduction is per entry and the recipient controls it (publishing one entry's commitment key restores full public verifiability for that entry alone — selective disclosure is what the medical-records and embargoed-journalism cases actually want); the ciphertext remains the ultimate arbiter for anyone given the recipient private key; and the alternative on offer was a commitment that was publicly verifiable and publicly brute-forceable.
- Residual leakage. Ciphertext length, entry timestamp and position, and
lookup_keycorrelation across entries are all visible on the public log. Neither encryption nor blinding hides them.
Auth. post_encrypted_claim requires a JWT (call sdk.login(...) first)
and the signing key must satisfy the server's hardware-signer policy
(require_hardware_signer). reveal_encrypted_claim also requires a JWT and
is rate-limited (#517), and a reveal discloses both the plaintext and that
entry's commitment key to the log operator — prefer the offline path below,
which discloses neither. fetch_encrypted_claims remains unauthenticated:
possession of the lookup_key is the access-control token.
EncryptedClaimReceipt, EncryptedClaimEntry, RevealResult¶
| Type | Fields |
|---|---|
EncryptedClaimReceipt |
entry_id: int, commitment: str (64 hex), lookup_key: str (hex token), leaf_index: int, log_entry_id: int, commitment_key: str (64 hex; "" on the relay submit path) |
EncryptedClaimEntry |
id, timestamp, event_type, payload, previous_hashes, entry_hashes |
RevealResult |
valid: bool, stored_commitment: str, recomputed_commitment: str |
DiogenesSDK.post_encrypted_claim(plaintext, *, recipient_p256_public_key, lookup_key=None)¶
Encrypts the plaintext against the recipient's P-256 public key and posts the
sealed payload to the transparency log. Returns an EncryptedClaimReceipt.
plaintextmay be adict(canonicalised via RFC 8785 before encryption) orbytes(auto-wrapped as{"data": "<base64>"}so the commitment shape is well-defined).recipient_p256_public_keymay be either a PEM string or a loadedcryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey. Non-PEM strings or non-SECP256R1 keys raiseKeyOperationError.lookup_keyis optional; when omitted the SDK uses the cryptographically random token generated by the core encryption helper. Supply a value to group multiple posts under one retrieval token.
Raises: KeyOperationError (malformed key / wrong curve), AuthError
(no JWT or hardware-signer-policy failure), ServerError for any other
4xx/5xx response.
DiogenesSDK.fetch_encrypted_claims(lookup_key)¶
Unauthenticated GET. Returns a list[EncryptedClaimEntry] in ascending
id order. Empty list when no entries match.
DiogenesSDK.decrypt_claim(entry, *, recipient_private_key)¶
Decrypts an EncryptedClaimEntry entirely in-process — no HTTP call is
made and the private key never crosses the wire. recipient_private_key
must be a loaded EllipticCurvePrivateKey object (callers handle PEM
loading and passwords themselves). Returns the original plaintext dict.
Verifying offline (preferred)¶
No server involved, nothing disclosed to anyone:
from diogenes.sdk import derive_claim_commitment_key, verify_commitment
entry = public_sdk.fetch_encrypted_claims(receipt.lookup_key)[0]
key = derive_claim_commitment_key(entry.payload, recipient_priv)
assert verify_commitment(entry.payload, plaintext, commitment_key=key) is True
The poster can skip the derivation entirely and keep receipt.commitment_key.
A pre-#517 entry (no commitment_alg) verifies with no key at all:
verify_commitment(entry.payload, plaintext).
DiogenesSDK.reveal_encrypted_claim(entry_id, plaintext, *, commitment_key=None)¶
Authenticated POST to /api/v1/claims/{id}/reveal (#517). The server
canonicalises the submitted plaintext, recomputes the commitment under the
entry's declared algorithm, and compares against the stored one. The SDK also
performs the same recomputation locally and bundles both commitments into a
RevealResult so callers can diff them when valid=False without parsing the
server response.
This call discloses the plaintext and that entry's commitment key to the log operator. The plaintext was always in this request body; the key adds no information about the same entry that the body did not already carry in the clear, and is per-entry, so it says nothing about any other entry. Even so, offline verification above is the documented default.
Raises: AuthError (401, no JWT); ServerError for 404 (no such entry),
409 (the entry carries a pre-#517 unsalted commitment — verify it offline
instead; server-side reveal for such entries is only ever a brute-force
oracle), 400 (blinded entry, no commitment_key), and 429 (per-IP or
per-(entry, caller) budget spent).
Worked example: post → fetch → decrypt → reveal¶
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from diogenes.sdk import DiogenesSDK, LocalKeystoreSigner
# 1. Generate a recipient P-256 key pair locally (kept by the recipient).
recipient_priv = ec.generate_private_key(ec.SECP256R1())
recipient_pem = (
recipient_priv.public_key()
.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
.decode()
)
# 2. Log in with a hardware-backed credential (required for POST).
sdk = DiogenesSDK(server_url="https://trustdiogenes.com")
sdk.login(fingerprint=my_fp, password="hunter2")
# 3. Post the encrypted claim.
plaintext = {
"claim_type": "training_completion",
"trainee_id": "alice",
"completion_date": "2026-03-01",
}
receipt = sdk.post_encrypted_claim(
plaintext,
recipient_p256_public_key=recipient_pem,
)
# Keep receipt.commitment_key: it cannot be recovered from the public entry
# without the recipient's private key.
print(receipt.entry_id, receipt.commitment, receipt.lookup_key)
# 4. (Recipient side) Fetch by lookup_key (no auth required).
public_sdk = DiogenesSDK(server_url="https://trustdiogenes.com")
entries = public_sdk.fetch_encrypted_claims(receipt.lookup_key)
assert len(entries) == 1
# 5. Decrypt locally — the private key never leaves this process.
revealed = public_sdk.decrypt_claim(
entries[0], recipient_private_key=recipient_priv
)
assert revealed == plaintext
# 6. (Auditor side) Verify the revealed plaintext against the commitment.
# (Or verify offline with derive_claim_commitment_key — see above.)
public_sdk.set_jwt(my_jwt) # reveal requires a JWT since #517
result = public_sdk.reveal_encrypted_claim(
receipt.entry_id, plaintext, commitment_key=receipt.commitment_key
)
assert result.valid is True
assert result.stored_commitment == result.recomputed_commitment == receipt.commitment
Posting raw bytes¶
# Raw bytes are wrapped automatically; decrypt returns the wrapper dict
# and the caller base64-decodes the "data" key to recover the bytes.
import base64
receipt = sdk.post_encrypted_claim(
b"\x00\x01\x02 binary blob",
recipient_p256_public_key=recipient_pem,
)
entry = public_sdk.fetch_encrypted_claims(receipt.lookup_key)[0]
decrypted = public_sdk.decrypt_claim(entry, recipient_private_key=recipient_priv)
original_bytes = base64.b64decode(decrypted["data"])
Webhook Management¶
WebhookClient¶
Client for managing webhook subscriptions against the Diogenes REST API.
Constructor¶
WebhookClient(
base_url: str,
api_key: str | None = None,
http_client: httpx.Client | None = None,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
base_url |
str |
-- | Base URL of the Diogenes server (e.g. http://localhost:8000). |
api_key |
str \| None |
None |
Optional API key for authentication. |
http_client |
httpx.Client \| None |
None |
Optional pre-configured HTTP client. |
Supports context manager usage:
create_webhook(url, events=None, secret=None, key_fingerprint=None, document_hash=None)¶
Create a new webhook subscription.
| Parameter | Type | Default | Description |
|---|---|---|---|
url |
str |
-- | Callback URL to receive events. |
events |
list[str] \| None |
None |
Event types to subscribe to. Empty list = all events. |
secret |
str \| None |
None |
Shared secret for HMAC-SHA256 payload signatures. |
key_fingerprint |
str \| None |
None |
Filter events by key fingerprint. |
document_hash |
str \| None |
None |
Filter events by document hash. |
Returns: WebhookSubscription
Raises: WebhookError on failure.
sub = client.create_webhook(
url="https://example.com/hook",
events=["attestation.created", "key.registered"],
secret="my-shared-secret",
)
print(sub.id)
list_webhooks()¶
List all active webhook subscriptions.
Returns: list[WebhookSubscription]
get_webhook(subscription_id)¶
Get a single webhook subscription by ID.
Returns: WebhookSubscription
Raises: WebhookError if not found.
delete_webhook(subscription_id)¶
Delete (deactivate) a webhook subscription.
Raises: WebhookError if not found.
WebhookSubscription¶
Data class representing a webhook subscription.
| Field | Type | Description |
|---|---|---|
id |
int |
Unique identifier. |
url |
str |
Callback URL. |
events |
list[str] |
Subscribed event types. |
key_fingerprint |
str \| None |
Key fingerprint filter. |
document_hash |
str \| None |
Document hash filter. |
active |
bool |
Whether the subscription is active. |
healthy |
bool |
Whether the endpoint is healthy. |
retry_count |
int |
Consecutive delivery failures. |
created_at |
str \| None |
ISO-8601 creation timestamp. |
Exceptions¶
All SDK exceptions inherit from DiogenesSDKError.
| Exception | When raised |
|---|---|
DiogenesSDKError |
Base class for all SDK errors. |
SigningError |
Signing fails (e.g. unregistered or revoked key). |
VerificationError |
Verification encounters an error. |
TrustConfigError |
Trust configuration loading or validation fails. |
TrustEvaluationError |
Trust evaluation not configured or fails. |
KeyOperationError |
Key operation fails. |
WebhookError |
Webhook API call fails. |
AuthError |
Authentication against the server fails (raised by post_encrypted_claim when no JWT is set or the signing key fails the hardware-signer policy). |
ServerError |
Server returned an unexpected error response. Exposes status_code and detail attributes. |
from diogenes.sdk import DiogenesSDKError, SigningError, WebhookError
try:
sdk.sign("doc", unregistered_key)
except SigningError as e:
print(f"Signing failed: {e}")
Supported Event Types¶
The following event types can be used when creating webhook subscriptions:
| Event | Description |
|---|---|
attestation.created |
A new attestation was signed and recorded. |
key.registered |
A new key was registered on the transparency log. |
key.revoked |
A key was revoked. |
endorsement.created |
A new endorsement was accepted. |
endorsement.revoked |
An endorsement was revoked. |
Architecture Notes¶
- The SDK is a facade over the core Diogenes modules (
diogenes.core.*). Advanced users can import core modules directly for lower-level access. - Signing and verification are synchronous operations.
- The
WebhookClientuses httpx for HTTP and is safe for use in both sync and async contexts (viahttpx.Client). - Trust evaluation uses a web-of-trust model with configurable anchors and thresholds, not a certificate authority.