init: CLI key-authorization manager for envelope_crypto
Signed-off-by: disqualifier <disqualifierca@gmail.com>
This commit is contained in:
commit
cb30fa9352
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.venv/
|
||||
.pytest_cache/
|
||||
CLAUDE.md
|
||||
199
README.md
Normal file
199
README.md
Normal file
@ -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.
|
||||
Loading…
Reference in New Issue
Block a user