blind — the open-source trust CLI for The Blind Machine: governed, content-addressed computation on encrypted data.
Project description
blind — The Blind Machine CLI
The machine can compute, but it cannot see.
blind is the command-line trust surface for The Blind Machine:
a platform for governed, content-addressed computation on encrypted data. Data
owners encrypt locally, the server computes on ciphertext only, and a researcher
decrypts only the approved aggregate — with every application, cohort, and result
independently verifiable by hash.
Every operation that touches plaintext or a secret key happens on your machine, in this CLI. The server never runs keygen, encoding, encryption, or decryption. It sees ciphertext plus a public context, and nothing else. That is the whole point of shipping the trust surface as an auditable, open-source program you can read before you run.
- v1 demo:
allele_frequency_count— sum encrypted variant-presence vectors across ≥20 contributors, decrypt only the per-variant count. - Crypto is a application dependency, not a platform backend. A application is a
self-contained, sandboxed uv-native Python bundle that brings its own crypto library via
env/pyproject.toml+env/uv.lock. v1's curated applications standardize on TenSEAL (Apache-2.0) — BFV with minimal additive-only params for exact-integer counts, depth-supporting BFV where a multiply is unavoidable, and CKKS only as a fast-follow approximate-real branch.blindnever chooses crypto params by hand. The signed manifest, code, andenv/uv.lockare the application trust boundary.
The trust surface: what stays local vs. what is uploadable
blind classifies every artifact into one of five trust classes and prints the
class, loudly, at every boundary crossing. The classification is not cosmetic —
the local storage layout, the upload code paths, and the printed banners all key
off it.
| Artifact | Trust class | Produced by | Leaves your machine? |
|---|---|---|---|
| Raw data (your CSV / VCF / vectors) | Raw | you | NEVER — not even cached by default |
Encoded data (10_encode.py output, still plaintext) |
Encoded | local blind data encode |
NEVER |
| Private Crypto Context / secret key | Private | local blind keys create |
NEVER — OS keychain |
| Public Crypto Context (public key + params) | Public | local blind keys create |
shareable / uploaded once |
| Encrypted data (ciphertext) | Encrypted | local blind contributions create (encode+encrypt), or the blind data encrypt primitive |
uploadable |
| Encrypted result (ciphertext) | Encrypted | server compute worker | arrives from server |
| Decrypted result (the aggregate) | Raw (local) | local blind results decrypt |
stays local unless you publish it |
The only things that ever go up: Encrypted ciphertext and the Public Crypto Context. Keygen / encode / encrypt / decode / decrypt are 100% local. The server is structurally unable to store a Private Crypto Context — it has no column for it (see requirements: hard invariants).
Why this is a separate, open-source project
- It is the security boundary, so it must be auditable. If the program that holds your secret key and encrypts your data is closed or bundled inside a web app, "the server never sees plaintext" is a promise, not a property you can check. Open source + reproducible builds let you verify it.
- It runs on the data owner's machine, not ours. A hospital contributor
installs
blind, drops ciphertext, and never needs a The Blind Machine account. That client can't be a hosted service. - It has its own release cadence, deps, and tests. The heavy native deps (a sandbox/container runtime plus whatever crypto a application pins in its uv lock — TenSEAL/SEAL for the curated set) have their own versioning; the Rails platform should not carry them.
The CLI is maintained in its own public repository,
blindmachine/blind, with its own
release workflow, tests, security policy, and MIT license. Nothing here imports from the Rails app;
it talks to the platform exclusively over the documented HTTP API
(see COMMANDS.md → HTTP API contract).
No platform backends — a application brings its own crypto
There is no platform "backend" abstraction. A application is a self-contained,
sandboxed Python bundle whose signed payload lives under signed/
(manifest.yml, author files, env/pyproject.toml, env/uv.lock,
env/.python-version). Root README.md, SECURITY.md, optional
BENCHMARK.md, and tests live beside signed/ and are not part of the
digest/signature. Its crypto library is a locked
application dependency, not a capability blind provides. You never choose
crypto params by hand. The bundle declares what it needs, and a data-free,
digest-pinned container build runs uv sync --frozen --no-dev. The signed tree
is mounted read-only and reverified afterward.
v1's curated core applications all standardize on TenSEAL BFV (Apache-2.0);
TenSEAL CKKS is reserved for the fast-follow approximate-real application.
The crypto: field in manifest.yml is a display hint only — the real
crypto lives in the code + pinned deps:
| Crypto hint | Library | Class | Exactness | Used by (v1) |
|---|---|---|---|---|
tenseal-bfv · additive |
TenSEAL | minimal-params add-only | exact integer (mod t) |
allele/carrier/histogram/PGS counts |
tenseal-bfv · depth-1 |
TenSEAL | one homomorphic multiply | exact integer (mod t) |
variance, covariance |
tenseal-ckks (fast-follow) |
TenSEAL | bounded-depth multiply | tolerance-bounded real | real-valued mean/variance |
Rule of thumb blind follows: use the least-powerful configuration that
works. Reach for a multiply only when two values no single party can see in
plaintext must be combined after encryption — the paper's thesis is the cost of
multiplicative depth within one library (additive-only BFV vs multiply-capable
BFV, with CKKS as a fast-follow branch). If a future application wants a different library (e.g. Paillier), that
license and dependency are isolated to that bundle through its uv-locked environment.
Install
Requires Python ≥ 3.11 and a sandbox/container runtime (podman or
docker) — blind runs every application stage inside a pinned, network-isolated
sealed environment with a read-only output directory and only its declared,
size-bounded output files writable, so a runtime must be present. Application crypto (TenSEAL for
the curated set) is not a blind dependency; each application's env/uv.lock
fetches its own pinned deps at install time.
Run the CLI directly in an isolated, cached environment:
uvx blindmachine
uvx downloads the blindmachine package from PyPI, installs its exactly
pinned runtime closure into uv's cache, and runs the blindmachine entry point.
Nothing separate is published to "uvx": publishing blindmachine on PyPI makes
this command available.
For a persistent blind command instead:
uv tool install blindmachine
blind doctor
Then check your toolchain:
blind doctor # verifies python, the sandbox runtime, the uv env-sealer,
# keychain, Ed25519 verify, ~/.blind perms, sealed-env
# self-test, and API reachability
Runtime dependencies
The runtime uses Typer/Rich for its command surface, HTTPX/Pydantic for the API
contract, keyring for OS-backed secrets, and cryptography for pinned Ed25519
verification. Every direct and transitive Python dependency is exact-pinned in
wheel metadata because uvx does not consume this repository's uv.lock; the
release gate proves that metadata and the lock export have the same runtime
closure. A container runtime (podman/docker) is required but external.
Application crypto is isolated per application and installed only from its
signed env/uv.lock. See the
output design.
Quickstart
Two roles. The researcher runs the study; data owners contribute ciphertext with no account via a 7-day link.
Researcher
blind login # REMOTE: get an API token (device/browser code)
blind applications install allele_frequency_count # BOTH: fetch, verify sig + digest
blind projects create --application allele_frequency_count@<digest> --name "Rare disease cohort"
blind keys create --project <project-id> # LOCAL: keygen; secret → keychain, only the public half published
blind projects invite <project-id> --expires 7d --qr # REMOTE: mint accountless bearer link (+QR) for contributors
# ... contributors contribute (below) ...
blind projects retrieve <project-id> # REMOTE: cohort size, min-N satisfied?
blind projects freeze <project-id> # REMOTE: commit cohort → prints cohort hash
blind jobs estimate --project <project-id> # REMOTE: marked-up CPU-second cost, no dispatch
blind jobs create --project <project-id> # REMOTE: confirm the estimate, then dispatch
blind jobs watch <job-id> # REMOTE: stream stages
blind results decrypt <job-id> # REMOTE download + LOCAL decrypt → aggregate
blind certificates verify <cert-hash> # LOCAL: recompute + check all hashes offline
Data owner (no account)
uvx blindmachine contributions create \
--link https://blindmachine.org/c/AbC123... \
--data ./my_vector.csv
# encode → encrypt happen LOCALLY; only ciphertext is uploaded.
# raw + encoded stay local; no secret key is generated here (uses the project's public context).
Simulate feasibility before any real contributor exists (simulate is an alias
for simulations create):
blind simulate allele_frequency_count --synthetic --n 20,100,1000 --emit methods,table
Local storage: ~/.blind
blind keeps all local state under ~/.blind. Secret keys live in the OS
keychain (macOS Keychain / GNOME Keyring / Windows Credential Manager); the
on-disk tree holds only a reference to the keychain entry. Raw data is never
cached. Every file below is either Public/Encrypted (safe to share) or is a
Private/Encoded/Raw local-only artifact, and the layout keeps the two physically
separate.
~/.blind/
├── config.yml # api base URL, active profile, output prefs (json/color)
├── auth/
│ └── <profile>.token # API bearer token for the Rails platform (chmod 600)
├── keys/
│ └── projects/
│ └── <project-id>/
│ ├── public.context # Public Crypto Context — SHAREABLE, uploaded to server
│ ├── private.ref # pointer to the OS-keychain entry (NOT the key itself)
│ ├── private.key # explicit insecure file backend only (chmod 600)
│ └── meta.yml # crypto hint, params, pinned application name@digest, env_lock
├── applications/
│ └── <name>@<sha256-digest>/ # content-addressed install; digest re-checked on every load
│ ├── signed/ # digest/signature payload
│ │ ├── manifest.yml # declarations + fixed coordinate definitions
│ │ ├── server.py # SERVER compute(inputs, public_context)
│ │ ├── local_project_owner.py
│ │ ├── local_data_owner.py
│ │ ├── env/
│ │ │ ├── pyproject.toml
│ │ │ ├── uv.lock
│ │ │ └── .python-version
│ │ ├── .digest # recomputed SHA-256, must equal <sha256-digest>
│ │ └── env_lock # sha256 of uv.lock + python version + runner metadata
│ ├── README.md # support docs; not signed
│ ├── SECURITY.md # review notes; not signed
│ ├── BENCHMARK.md # optional benchmark notes; not signed
│ ├── tests/ # review/support fixtures; not signed
│ └── .blind-signature # Ed25519 signature over signed/** digest
├── cache/
│ ├── encoded/ <sha>.enc-in # Encoded Data — LOCAL ONLY (plaintext-derived), never uploaded
│ └── encrypted/<sha>.ct # Encrypted Data — the ONLY input class that is uploadable
├── results/
│ └── <project-id>/<job-id>/
│ ├── result.ct # Encrypted result (downloaded ciphertext)
│ ├── result.json # Decrypted aggregate — LOCAL ONLY unless you publish
│ └── certificate.json # Computation Certificate (application/cohort/data/result hashes)
├── simulations/
│ └── <sim-run-hash>/ # NON-authoritative SimulationRun: config.yml, equivalence.json,
│ # benchmark.{csv,md,tex}, plots/, methods.md, threat_model.md,
│ # provenance.json
└── logs/
└── blind.log # command + hash audit trail (no plaintext, no secrets)
Raw data (my_vector.csv, a VCF, …) is not copied into ~/.blind. It stays
wherever you point --input. blind reads it, encodes/encrypts, and forgets it.
The loud trust statements blind prints
Every command that crosses a trust boundary prints a banner naming the trust class of what it produced and whether it left the machine. Two examples:
$ blind data encrypt --project <project-id> --input ./my_vector.csv
Raw ./my_vector.csv LOCAL ONLY — never uploaded
Encoded cache/encoded/9f3a…e1.enc-in LOCAL ONLY — never uploaded
Encrypted cache/encrypted/2c8b…7a.ct UPLOADABLE — ciphertext only
application allele_frequency_count@sha256:4d1e…c0
public context sha256:7b22…9f (matches project) Public — shareable
private key kept in OS keychain Private — never leaves this machine
Nothing has been uploaded. Run `blind contributions create` to upload the Encrypted artifact.
$ blind projects freeze <project-id>
Cohort frozen. No contributor can be added, removed, or re-run.
cohort commitment sha256:1a90…44 (= sha256 of sorted contribution hashes + project + application)
contributors 23
minimum contributors satisfied ✔ (min 20)
Result and certificate commands print the same way:
$ blind results decrypt <job-id>
application allele_frequency_count@sha256:4d1e…c0 ✔ digest verified
cohort sha256:1a90…44 ✔ matches frozen commitment
result (cipher) sha256:8f0c…2d ✔ matches server result digest
result (plain) results/<project>/<job>/result.json Raw (local) — not uploaded
sentinel N 23 ✔ (append-1 integrity check, not a MAC)
minimum contributors satisfied ✔
Hashes printed/verified across the CLI: application digest, public-context hash, cohort commitment, result ciphertext digest, and the certificate binding all of them. The design intent — Raw/Encoded/Private are local-only; Encrypted/Public are the only uploadable classes — is stated at every step so a data owner can see the boundary, not just trust it.
Honest scoping the CLI never overstates: the append-1 sentinel is an integrity check, not a MAC; FHE hides contributor inputs from the server, not zero-knowledge; cohort-freeze + min-N mitigate but do not fully solve K-vs-K+1 differencing.
Command overview
Full Stripe-CLI-style resource/verb surface (CRUD verbs create · retrieve ·
update · list · delete plus resource actions), per-command flags,
LOCAL/REMOTE classification, --json output, and the HTTP API contract are in
the command reference. Groups:
- Top-level:
login·logout·config·doctor·version·resources·get/post(raw API) - Resources:
applications·projects·keys·contributions·data·jobs·results·certificates·simulations(aliassimulate) ·dev
(The old auth group is now the top-level login/logout; the append-only
event log moved to projects events; encrypted-data upload is contributions create.)
Implementation stack
blind is a Python ≥ 3.11 package. Output is designed to be beautiful and very
informative, inspired by the Rails command line — aligned colored action
labels, rails routes-grade tables, trees, timed progress bars, and the loud
trust banners as panels. The full output design and the stack evaluation
(including a serious look at google/python-fire) live in the
output design.
Recommended stack: typer (typed dispatch; resource sub-apps map 1:1 to the
Stripe surface; clean global --json) · rich (all tables,
trees, panels, progress, themed help) · httpx (HTTP) · pydantic v2
(models + the --json schema the desktop GUI consumes) · uv (seals each
application's pinned env) · a container runtime (podman/docker) for the
network-isolated sandbox · keyring · cryptography (Ed25519) ·
platformdirs · pyyaml. We keep python-fire's object model (resource
classes are the CLI) but use Typer to dispatch, for typed input validation and
a stable machine contract. --json is wired
at the framework level, so it is guaranteed on every command.
Development
uv sync --locked # install the reviewed runtime + dev closure
uv run pytest # unit + golden-vector tests
uv run ruff check src tests # lint
uv run python scripts/check_runtime_lock.py
Key test suites: manifest/coordinate digest reproducibility, uv env-seal +
env_lock reproducibility, byte-exact artifact contract against the platform
fixtures, keychain round-trip, and the dev compare plaintext-vs-encrypted
equivalence harness (bit-exact for BFV integer results, tolerance-bounded for
CKKS reals).
License
MIT. See LICENSE.
Source and security
- Source: github.com/blindmachine/blind
- Command contract: COMMANDS.md
- Vulnerability reporting and trust boundaries: SECURITY.md
PyPI releases are built from tags by GitHub Actions, published through PyPI Trusted Publishing, and include attestations. The build job has no OIDC publish permission; the isolated publish job only receives already-verified artifacts.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file blindmachine-0.1.0.tar.gz.
File metadata
- Download URL: blindmachine-0.1.0.tar.gz
- Upload date:
- Size: 242.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03cd2585e224a91ce6aefe1e6f03f6d76130144740579d7827e96a5033ba4a6a
|
|
| MD5 |
be60d8c9fe39f236b1a64f8262722855
|
|
| BLAKE2b-256 |
a404a4d56addbed2f1e15bd25ddaa0597d3bfa65e87a03328c8fb8e64779db7b
|
Provenance
The following attestation bundles were made for blindmachine-0.1.0.tar.gz:
Publisher:
release.yml on blindmachine/blind
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
blindmachine-0.1.0.tar.gz -
Subject digest:
03cd2585e224a91ce6aefe1e6f03f6d76130144740579d7827e96a5033ba4a6a - Sigstore transparency entry: 2165430584
- Sigstore integration time:
-
Permalink:
blindmachine/blind@17b1e8792972b697ce63a8541ecb3e6e3cddf067 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/blindmachine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@17b1e8792972b697ce63a8541ecb3e6e3cddf067 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file blindmachine-0.1.0-py3-none-any.whl.
File metadata
- Download URL: blindmachine-0.1.0-py3-none-any.whl
- Upload date:
- Size: 138.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa2bff1f51c1b5a5476f2329c09c95afb50c2081684bebe3210caeb8255d9e7b
|
|
| MD5 |
1d1512bf2d05c41b5ed79221b659394f
|
|
| BLAKE2b-256 |
fb71868ee4413265ca4f6b5d93d54fa2bd83c357ee3b959ea972a6253907083a
|
Provenance
The following attestation bundles were made for blindmachine-0.1.0-py3-none-any.whl:
Publisher:
release.yml on blindmachine/blind
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
blindmachine-0.1.0-py3-none-any.whl -
Subject digest:
aa2bff1f51c1b5a5476f2329c09c95afb50c2081684bebe3210caeb8255d9e7b - Sigstore transparency entry: 2165430587
- Sigstore integration time:
-
Permalink:
blindmachine/blind@17b1e8792972b697ce63a8541ecb3e6e3cddf067 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/blindmachine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@17b1e8792972b697ce63a8541ecb3e6e3cddf067 -
Trigger Event:
workflow_dispatch
-
Statement type: