Skip to main content

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. blind never chooses crypto params by hand. The signed manifest, code, and env/uv.lock are 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

  1. 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.
  2. 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.
  3. 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 (alias simulate) · 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

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.

Metadata

Release files for blindmachine 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for blindmachine 0.1.0
File Size Uploaded
blindmachine-0.1.0.tar.gz 242.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for blindmachine 0.1.0
File Interpreter ABI Platform
blindmachine-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 381.5 kB

Release files / blindmachine-0.1.0.tar.gz

Download URL blindmachine-0.1.0.tar.gz
Size 242.8 kB
Tags Source
SHA-256 checksum
How to use checksums
03cd2585e224a91ce6aefe1e6f03f6d76130144740579d7827e96a5033ba4a6a
BLAKE2b-256 checksum
How to use checksums
a404a4d56addbed2f1e15bd25ddaa0597d3bfa65e87a03328c8fb8e64779db7b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 13, 2026.

Transparency log

Release files / blindmachine-0.1.0-py3-none-any.whl

Download URL blindmachine-0.1.0-py3-none-any.whl
Size 138.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aa2bff1f51c1b5a5476f2329c09c95afb50c2081684bebe3210caeb8255d9e7b
BLAKE2b-256 checksum
How to use checksums
fb71868ee4413265ca4f6b5d93d54fa2bd83c357ee3b959ea972a6253907083a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 13, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page