- JsonStore._write used a fixed '<path>.tmp' name with no lock, so two concurrent authorizer invocations could clobber each other's temp and corrupt/lose the key store. use tempfile.mkstemp in the same dir (unique per write) then os.replace (atomic), cleaning up the temp on failure. - list 'created_at' formatting did int(raw) unguarded; one hand-edited/legacy doc with a bad timestamp aborted the whole table. guard per-row, fall back to '-'. verified by execution: 20 concurrent writers -> 0 errors, file stays valid JSON, no leftover .tmp; upsert still dedupes/updates; bad/absent created_at -> '-'. Signed-off-by: disqualifier <dev@disqualifier.me> |
||
|---|---|---|
| src/envelope_authorizer | ||
| .gitignore | ||
| pyproject.toml | ||
| README.md | ||
envelope_authorizer
A CLI key-authorization manager for 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:
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:
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 runauthorizeto 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
# 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)
[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.
[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):
{
"_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.