commit f6c43f49c6cd688c253488e98ae93135fc3bfc92 Author: disqualifier Date: Thu Jun 25 00:04:37 2026 -0400 init: CLI key-authorization manager for envelope_crypto Signed-off-by: disqualifier diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f3b1d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.venv/ +.pytest_cache/ +CLAUDE.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fd3175 --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +# envelope_authorizer + +A CLI key-authorization manager for [`envelope_crypto`](https://git.rethinkstudios.io/rethink-public/envelope_crypto). +Installing it gives a project an `authorizer` command to manage **DEK access**: +initialize the key system, authorize other machines, verify, list, and revoke. + +It is the clean extraction of the authorization half of an older monolith — the +domain logic (vaults, services, API keys, wallet scan/sweep) is **out of scope** +and stays in the projects that consume this lib. This lib owns only the +authorization system and the key-document schema; the crypto primitives live in +`envelope_crypto`. + +## Install + +``` +envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.0 +``` + +Direct: + +```bash +pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.0" +``` + +The base install uses a local JSON file for storage (stdlib only). For shared +dev→server storage, install the mongo extra: + +```bash +pip install "envelope_authorizer[mongo] @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.0" +``` + +Installing pulls `envelope_crypto` (and `mongo` with the extra). After install, +the `authorizer` command is on your PATH; `python -m envelope_authorizer` also works. + +## Trust model (read this) + +There is one shared **AES data-encryption key (DEK)** per project. Each key doc +stores that same DEK **RSA-wrapped to a different authorized public key**, so each +machine unwraps it with its own RSA private key. **Private RSA keys are never +stored** — only the wrapped DEK lives in the doc. + +Each key carries an **encrypted capability flag** — `meta.authorizer` = +`encrypt_data({"allowed": bool})` — meaning "may this key authorize *other* keys?" + +- **Dev / home machines** are initialized or authorized with `allowed: True` → + they can run `authorize` to grant new machines access. +- **Servers** are authorized with `allowed: False` → they can boot and use the DEK + (decrypt project data) but **cannot authorize anything else**. + +The flag is GCM-sealed under the DEK on purpose: a server **cannot flip its own +flag in the database to escalate**, because forging a valid flag requires already +holding the DEK, and editing the stored ciphertext breaks the auth tag. The +guarantee is precisely: *you cannot grant yourself authority without already +having it.* (A legitimate authorizer can of course mint new flags — that is its +job. This stops passive DB tampering, not an authorizer.) + +`revoke` is **bookkeeping, not rotation**: it deletes the record so the key can no +longer unwrap at boot, but it does not rotate the DEK or scrub it from a machine +that already holds it. If a key is compromised, re-init the system and +re-authorize trusted machines (DEK rotation lives in `envelope_crypto`). + +## Quick start + +```bash +# 1. scaffold a config in the current project, then edit it +authorizer config init + +# 2. on the DEV machine: create the key system (this key is an authorizer) +authorizer init --friendly dev + +# 3. authorize a SERVER (no --can-authorize → it can decrypt but not authorize) +authorizer authorize --key /path/to/server_pub.pem --friendly server1 + +# 4. see who's authorized +authorizer list +``` + +The dev→server flow works across machines when both point at the same storage +(the mongo backend). With the JSON backend they must share the file. + +## Commands + +### `authorizer config init` + +Writes a starter `.authorizer.toml` in the current directory (with a commented +mongo section). Refuses to overwrite an existing file. Prints the path written. + +### `authorizer init --friendly NAME` + +Creates a fresh DEK, wraps it to the local public key, and stores the first key +doc as an **authorizer** (`allowed: True`). Refuses if the system is already +initialized or the friendly name is taken. + +``` +[✔] Initialized — fingerprint: O3MBQfll... | friendly: dev [authorizer=True] +``` + +### `authorizer authorize --key PATH --friendly NAME [--can-authorize]` + +Boots the local DEK, verifies the local key is itself an authorizer, then wraps +the same DEK to the target public key and stores a new key doc. Omit +`--can-authorize` for servers (`allowed: False`); pass it only for trusted +dev/home machines. + +``` +[✔] Authorized Jy7k2ey7... | friendly: server1 [can_authorize=False] +``` + +A key with `allowed: False` that tries to authorize is refused: + +``` +[✘] this key is not permitted to authorize others +``` + +### `authorizer verify` + +Boots the local DEK and runs `envelope_crypto`'s `self_test` (data round-trip + +key wrap/unwrap) against the local keypair. Prints `[✔] Crypto system OK` or the +failure. Never prints key material. + +### `authorizer list` + +Prints every authorized key as a padded table (fingerprint, friendly, created_by, +created_at, can-authorize). `CAN_AUTH` is decrypted from each doc's flag; a key the +local machine can't unwrap shows `?`. Never prints the wrapped key or DEK. + +``` +FINGERPRINT FRIENDLY CREATED_BY CREATED_AT CAN_AUTH +------------------------------------------------------------------------------------- +O3MBQflldInI6Vgt dev dsql@dev 2026-06-25 03:37:00 Yes +Jy7k2ey7KfQl0w5V server1 dsql@dev 2026-06-25 03:37:05 No + +2 key(s) authorized +``` + +### `authorizer revoke --friendly NAME` | `--fingerprint FP` + +Finds the key by friendly name or fingerprint prefix, refuses to revoke the local +key, deletes the record, and prints the honesty warning that revoke is not +rotation. + +## Config reference + +TOML, searched cwd-first then `~`: `.authorizer.toml`. Paths expand `~`. No +defaults are baked in — a missing required field raises an error naming the field +and the config path. + +### JSON backend (default, stdlib only) + +```toml +[keys] +public = "~/.ssh/id_rsa.pub" +private = "~/.ssh/id_rsa" +identity = "user@hostname" # stamped as created_by on every key doc + +[storage] +backend = "json" +path = ".authorizer_keys.json" +``` + +### Mongo backend (`envelope_authorizer[mongo]`) + +Shared storage so dev authorizes a server across machines. Writes are upserts on +`_id`. + +```toml +[keys] +public = "~/.ssh/id_rsa.pub" +private = "~/.ssh/id_rsa" +identity = "user@hostname" + +[storage] +backend = "mongo" +uri = "mongodb://localhost:27017" +database = "myapp" +collection = "keys" +``` + +## Key document schema + +Owned by this lib (not `envelope_crypto`): + +```python +{ + "_id": fingerprint, # SHA-256 fingerprint of the public key + "key": wrapped_b64, # the AES DEK, RSA-wrapped to THIS pubkey + "meta": { + "authorizer": {"secure": True, "iv": "...", "data": "..."}, # encrypted flag + "created_by": "dsql@dev", + "created_at": 1782358620, + "friendly": "dev", + }, +} +``` + +## Versioning + +Tagged `vX.Y.Z`. Pin the tag. `envelope_crypto` is pinned at `v0.1.0` in +`pyproject.toml`; to change it, edit the pin and re-test.