From bfeee807121e8f82a2165c1a8a7534bd37dbcec6 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Thu, 2 Jul 2026 16:41:09 -0400 Subject: [PATCH] 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 --- README.md | 14 ++++++---- pyproject.toml | 2 +- src/envelope_authorizer/__init__.py | 2 +- src/envelope_authorizer/commands/authorize.py | 27 ++++++++++++++++--- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1298811..06befa2 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/pyproject.toml b/pyproject.toml index 24f7c87..b363f81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/src/envelope_authorizer/__init__.py b/src/envelope_authorizer/__init__.py index b3f4756..ae73625 100644 --- a/src/envelope_authorizer/__init__.py +++ b/src/envelope_authorizer/__init__.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/src/envelope_authorizer/commands/authorize.py b/src/envelope_authorizer/commands/authorize.py index 461dc9e..2f67a43 100644 --- a/src/envelope_authorizer/commands/authorize.py +++ b/src/envelope_authorizer/commands/authorize.py @@ -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)