Python integration starter
© Pi Soft, 2018-2026 · Tricryption Engine 8.1
A standalone example for an application developer. It needs Python 3.10+ (the python3 of Ubuntu 22.04 and
newer) and one OpenSSL
executable, nothing else: no native SDK, no package from an index, no knowledge of any demo
estate. It talks to a Tricryption REST Gateway at contract 1.7.1 (every path under /api/v1)
and proves two things on a live deployment:
- selective field sharing -- two protected JSON fields under two keys; the owner grants a recipient READ on one; the recipient reads that field and is refused the other;
- same-session revocation -- the owner revokes the grant and the recipient is refused that field on the same session it just read it on (and on a fresh one).
Every request is a real certificate login and a real DPoP proof (RFC 9449, plus the Gateway's
bth body-hash claim). Both identities are fresh for each run (run.<uuid>.owner,
run.<uuid>.agent), and the run removes what it created.
A backend written in TypeScript has an edition of its own: the TypeScript integration starter
-- the same client, example and workflow adapter with no dependencies, run as .ts by Node.js 22.18 or newer (qualified on v24.22.2, the Node the product ships), from the same config.json.
| file | what it is |
|---|---|
client.py | GatewayClient -- certificate login, DPoP on every request, delegated sessions, scoped sessions bound to a per-task ES256 key (HolderKey, scoped_mint, scoped_revoke), bounded read retries |
example.py | the selective-sharing and revocation run, its clean-up, and --cleanup for an interrupted run |
preflight.py | checks Python, the OpenSSL executable, the trust file and GET /api/v1/ready |
config.example.json | the configuration -- paths only, no secret |
bootstrap/ | the OPERATOR's side: operator-bootstrap.md and bootstrap.py |
mcp/ | an MCP server for an agent -- over stdio for one local user, or over Streamable HTTP on the loopback interface for many users, each acting as themselves through their own access token -- with three document tools over protected documents (list_approved_documents, read_protected_document, save_protected_result) and, when the operator's launcher names a folder, two file tools (encrypt_file, decrypt_file) that protect a file inside that folder under a new key of its own -- see mcp/README.md. It is the one part of the starter that needs a package (the MCP Python SDK, installed from a hash lock into a virtual environment) |
The nine commands
Your operator registers your service (see bootstrap/operator-bootstrap.md)
and gives you: a certificate and its key, the Gateway's CA file, the role id for your principals
and the clean-up recipient -- bootstrap.py prints the last two as principal_roles [...] and
cleanup_recipient '...'. You give the operator one thing: your sender PUBLIC key (step 5 below).
cd examples/python-agent-starter
python3 --version # 3.10 or newer (Windows: py -3 --version)
command -v openssl # the ABSOLUTE path goes in config.json (Windows: where openssl)
mkdir -p keys && chmod 700 keys # keys/ is not in the starter; it holds your keys, private
openssl genrsa -out keys/sender.key 2048 # your request-signing key -- NOT the certificate's key
openssl pkey -in keys/sender.key -pubout -out keys/sender.pub # send keys/sender.pub to your operator
cp config.example.json config.json # then edit it -- see "Filling in config.json"
python3 preflight.py --config config.json # five lines; it stops at the first that fails
python3 example.py --config config.json # the run, narrated; --json for one verdict line per step
Any OpenSSL 3.x command-line tool works (measured with Ubuntu 22.04's 3.0.2 and with 3.5.9); the
starter calls it by the absolute path you configure, never through PATH. genrsa on OpenSSL 3
writes the key mode 600; keep it that way.
Filling in config.json
Put what the operator gave you in keys/ under the names config.example.json already uses --
keys/service.pem (the certificate), keys/service.key (its key) and keys/gateway-ca.pem (the
Gateway's CA) -- or change those three paths. Paths are resolved against the directory of
config.json. Then set endpoint (the Gateway's origin, no path), openssl (the absolute path
command -v openssl printed), and replace the two <...> placeholders with the role id and the
clean-up recipient. For a loopback evaluation Gateway on http://127.0.0.1:<port> there is no TLS:
set "trust_file": null and "allow_http_loopback": true.
preflight.py's fifth line is the registration itself: your certificate logs in as a registered
service, and one read signed with keys/sender.key is served. PROOF_SIGNATURE_REJECTED there means
the operator registered a different sender key; "NOT a registered service" means the registration is
missing or the Gateway has not re-read its registry yet (it polls every 2 s by default).
A good run prints PASS for OWNER_ENCRYPT, AGENT_READ_A, AGENT_DENIED_B,
REVOKED_SAME_SESSION, REVOKED_FRESH_SESSION and CLEANUP, and exits 0.
If a run is interrupted (Ctrl-C, or Ctrl-Break on Windows) it makes no further calls and writes
runs/run.<uuid>/cleanup-report.json, naming what it left behind (identities by id, keys by a hash
prefix). Remove exactly that, and nothing of any other run's:
python3 example.py --config config.json --cleanup runs/run.<uuid>/run-record.json
Keys are removed (POST /api/v1/keys/remove: each key, its access control list and every
cached copy are deleted from the key server, and each one is read back absent); the principals
are removed, and the key server hands a removed principal's remaining keys to
cleanup_recipient, a password principal your operator names. Removal is local to that key
server: a copy of a key exported earlier is not reached.
client.py carries the three key-lifecycle calls, each answering one result per key:
expire_keys (the key's record is kept and every later use of it is refused 410 KEY_EXPIRED),
remove_keys (deleted) and import_keys. When any key of a request is refused, the
GatewayError raised carries every key's result in results -- the other keys were applied.
Behind a proxy
The Gateway itself terminates no TLS; in a deployment a reverse proxy does. endpoint is where you
connect and trust_file is what you verify it against. external_origin is the origin the GATEWAY
believes it has -- what its operator set as TE_GATEWAY_PUBLIC_ORIGIN -- and it is what the DPoP
htu claim is built from. When the two are the same (the usual case) leave it null:
{ "endpoint": "https://gw.example.com", "trust_file": "keys/proxy-ca.pem", "external_origin": null }
When you reach the Gateway by a different name than the one it was configured with -- an internal load balancer, say -- set it to the configured one:
{ "endpoint": "https://gw-lb.internal:8443", "trust_file": "keys/internal-ca.pem",
"external_origin": "https://gw.example.com" }
A mismatch is refused 401 PROOF_BINDING_MISMATCH naming htu. Plain http:// is accepted only for
a loopback evaluation Gateway (127.0.0.1 / localhost) with "allow_http_loopback": true.
Redirects are never followed.
What is retried, and what is not
| request | retried? |
|---|---|
GET, POST /api/v1/crypto/decrypt, POST /api/v1/keys/acl/get | yes -- on a transport error, a 429 or a 503, up to retry.max_attempts, honouring Retry-After (never longer than retry.max_retry_after_seconds), with a NEW proof (jti, signature) each time |
| every other request -- encrypt (which MINTS a key), grant, revoke, expire, key removal and import, principal creation and removal, logins | never. A write that was sent and got no answer raises AmbiguousOutcome, carrying what was sent (method, path, body size and hash) and how to read back what happened. It is not replayed: a second encrypt would mint a second key. |
| a certificate login | never -- a refused login is raised on the first refusal |
A refusal is a GatewayError with status, a stable code (ACL_DENIED, PROOF_REPLAYED,
RATE_LIMITED, ... or the client's own TLS_VERIFY_FAILED, TRANSPORT) and a sanitized detail.
What this is not
No key export, no offline mode. Every key stays in the key server; every encrypt and decrypt is a request. This is a starter, not a general SDK: it implements the routes the example and the MCP server use and the transport they share.
Credentials
Nothing in config.json is a secret -- the keys are named by path and this program never reads them
into memory; OpenSSL signs with them, the message on stdin and the signature on stdout. Bearers and
proofs exist only in memory and are never printed; an error never carries a header.