Skip to main content

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. 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

blindmachine-0.1.0.tar.gz (242.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

blindmachine-0.1.0-py3-none-any.whl (138.7 kB view details)

Uploaded Python 3

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

Hashes for blindmachine-0.1.0.tar.gz
Algorithm Hash digest
SHA256 03cd2585e224a91ce6aefe1e6f03f6d76130144740579d7827e96a5033ba4a6a
MD5 be60d8c9fe39f236b1a64f8262722855
BLAKE2b-256 a404a4d56addbed2f1e15bd25ddaa0597d3bfa65e87a03328c8fb8e64779db7b

See more details on using hashes here.

Provenance

The following attestation bundles were made for blindmachine-0.1.0.tar.gz:

Publisher: release.yml on blindmachine/blind

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for blindmachine-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aa2bff1f51c1b5a5476f2329c09c95afb50c2081684bebe3210caeb8255d9e7b
MD5 1d1512bf2d05c41b5ed79221b659394f
BLAKE2b-256 fb71868ee4413265ca4f6b5d93d54fa2bd83c357ee3b959ea972a6253907083a

See more details on using hashes here.

Provenance

The following attestation bundles were made for blindmachine-0.1.0-py3-none-any.whl:

Publisher: release.yml on blindmachine/blind

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page