envelope_authorizer/README.md
disqualifier f6c43f49c6 init: CLI key-authorization manager for envelope_crypto
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-25 00:04:37 -04:00

200 lines
6.8 KiB
Markdown

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