# 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.2 ``` Direct: ```bash pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.2" ``` 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.2" ``` Installing pulls `envelope_crypto` (and `mongo` with the extra). After install, the `authorizer` command is on your PATH; `python -m envelope_authorizer` also works. Drop the `@v0.1.2` suffix from the line above to install the latest unpinned. ## 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. What that buys, stated honestly: - **Without the DEK, the flag is inert.** A stolen database at rest cannot be escalated — the flags are ciphertext, unforgeable and unflippable without the key (editing the stored ciphertext breaks the GCM auth tag). This stops passive DB tampering. - **It does not defend against a party that already holds the DEK and can write storage.** The flag is sealed under the *shared* DEK and is **not bound to key identity**, so such a party can copy a valid sealed `True` flag from an authorizer's doc onto its own and self-grant authority. This is **out of scope by design**: the trust model assumes DEK-holders are trusted. A DEK-holder already has full data access (DEK + write access is game over), so self-granting authority exposes no additional data. We deliberately do not add per-key signing to prevent it — the residual is handled operationally (below). So the precise boundary is: **the capability flag is unforgeable without the DEK; it is not a defense against a malicious DEK-holder.** (A legitimate authorizer can of course mint new flags — that is its job.) ### Operational guidance (the residual control) Because the DEK-holder case above is **not** prevented cryptographically in the shared-DEK model, the intended mitigation is **detection, not crypto**. Consumers should periodically audit the authorization state in storage: - enumerate every key doc's capability flag (`list` shows authorizer status), - compare the set of `allowed: True` keys against a known-good list of expected authorizers, - alert on any unexpected authorizer. An unexpected `allowed: True` appearing in storage is the signal that a DEK-holder self-granted — catch it by review, then revoke and re-authorize from a trusted machine. `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 Releases are tagged `vX.Y.Z`. The install line above pins a release; drop the `@vX.Y.Z` suffix to install the latest unpinned. Pin deliberately for reproducible installs.