envelope_authorizer/README.md
2026-06-29 18:07:37 -04:00

8.3 KiB

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

Direct:

pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git"

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"

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

# 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

Releases are tagged vX.Y.Z. The install line above is unpinned and tracks the latest on the default branch; append @vX.Y.Z to pin a specific release for reproducible installs. envelope_crypto is pinned at v0.1.0 in pyproject.toml; to change it, edit the pin and re-test.