fix: EA-1 refuse authorize of an already-recorded key (v0.1.3)

authorize never checked the target fingerprint against existing docs before
save()'s upsert-by-_id, so authorizing the local machine's own public key
under a new friendly name silently replaced the local authorizer record
(can_authorize demoted to False) while printing a success banner. With a
sole authorizer this bricks the CLI: authorize refuses (not permitted),
init refuses (already initialized), and revoke of the local key refuses
(refusing to revoke the local key) -- no in-CLI recovery. Mirror revoke's
local-key guard and extend it to any existing _id, so a duplicate target
is refused with a clear message instead of silently replacing the record.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-02 16:41:09 -04:00
parent f2e9e5fe35
commit db2706daf5
4 changed files with 34 additions and 11 deletions
+9 -5
View File
@@ -13,26 +13,26 @@ authorization system and the key-document schema; the crypto primitives live in
## Install
```
envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.2
envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.3
```
Direct:
```bash
pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.2"
pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.3"
```
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"
pip install "envelope_authorizer[mongo] @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.3"
```
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.
Drop the `@v0.1.3` suffix from the line above to install the latest unpinned.
## Trust model (read this)
@@ -129,7 +129,11 @@ initialized or the friendly name is taken.
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.
dev/home machines. Refuses a target key whose fingerprint already has a record
(most importantly the local key itself) — `save` upserts by `_id`, so
authorizing an already-known key would silently replace its existing doc
(capability flag and friendly name) under a success banner instead of adding a
new key. Revoke the existing record first if you intend to re-authorize it.
```
[✔] Authorized Jy7k2ey7... | friendly: server1 [can_authorize=False]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "envelope_authorizer"
version = "0.1.2"
version = "0.1.3"
description = "CLI key-authorization manager for envelope_crypto"
requires-python = ">=3.10"
dependencies = [
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.2"
__version__ = "0.1.3"
+23 -4
View File
@@ -2,7 +2,11 @@
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. `--can-authorize`
decides whether the new key may authorize others (omit it for servers).
decides whether the new key may authorize others (omit it for servers). Refuses
a target key that fingerprints to an existing `_id` — `save` upserts by `_id`, so
authorizing a key that is already on record (most dangerously the local key
itself) would silently replace that doc's capability flag and friendly name
under a success banner instead of adding a new key.
"""
from . import (
@@ -10,6 +14,7 @@ from . import (
boot_local,
build_doc,
find_by_friendly,
local_fingerprint,
make_flag,
read_flag,
)
@@ -25,9 +30,23 @@ def run(config, storage, args) -> None:
if not read_flag(crypto, local_doc["meta"]["authorizer"]):
raise CommandError("this key is not permitted to authorize others")
new_fp, new_wrapped = crypto.encrypt_aes_key_with_rsa(
crypto.master_key, args.key
)
new_fp = crypto.get_rsa_key_fingerprint(args.key)
if new_fp == local_fingerprint(crypto, config):
raise CommandError(
"target key is the local key; authorize would silently replace the "
"local authorizer record — use a different keypair, or `authorizer "
"list` if you meant to check its status"
)
existing = storage.get(new_fp)
if existing:
existing_friendly = existing.get("meta", {}).get("friendly", "?")
raise CommandError(
f"target key is already authorized as '{existing_friendly}'; "
f"authorize would silently replace that record — revoke it first "
f"if you intend to re-authorize it"
)
_, new_wrapped = crypto.encrypt_aes_key_with_rsa(crypto.master_key, args.key)
flag = make_flag(crypto, args.can_authorize)
doc = build_doc(new_fp, new_wrapped, flag, config.identity, args.friendly)
storage.save(doc)