docs: plan fixups from Task 1 review (blob path, dangling-symlink fix to Task 8)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brian Majewski 2026-06-24 12:37:55 -07:00
parent 0f9de1c2fd
commit 14cf86fba9

View file

@ -62,7 +62,7 @@ make_second_identity() {
# No recipients.txt was created by push.
[ ! -e "$SECRETS_DIR/recipients.txt" ]
# Blob decrypts with the store's own key.
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@ -75,10 +75,10 @@ make_second_identity() {
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
# Bob (a recipient) can decrypt the pushed blob with HIS key.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
# And the store key still can too.
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@ -361,18 +361,18 @@ git commit -m "feat: secrets recipients list (EGB-283)"
@test "reencrypt re-encrypts existing blobs to a newly added recipient line" {
init_with_remote
create_project_dir myproj
run "$SECRETS_BIN" push # single-key blob
run "$SECRETS_BIN" push # single-key blob (project name = "myproj"; blob at $SECRETS_DIR/myproj/.env.age)
[ "$status" -eq 0 ]
make_second_identity
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '%s\n%s\n' "$STORE_PUB" "$BOB_PUB" > "$SECRETS_DIR/recipients.txt"
# Bob cannot read the old single-key blob yet.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -ne 0 ]
run "$SECRETS_BIN" reencrypt
[ "$status" -eq 0 ]
# Now he can.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@ -389,7 +389,7 @@ git commit -m "feat: secrets recipients list (EGB-283)"
# No new keypair was generated.
[ "$(cat "$SECRETS_DIR/key.txt")" = "$before" ]
# Both recipients can decrypt.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@ -401,7 +401,7 @@ git commit -m "feat: secrets recipients list (EGB-283)"
run "$SECRETS_BIN" rekey
[ "$status" -eq 0 ]
[ "$(cat "$SECRETS_DIR/key.txt")" != "$before" ]
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
```
@ -562,7 +562,7 @@ git commit -m "feat: shared _reencrypt_all + reencrypt cmd + dual rekey (EGB-283
[[ "$output" == *"recipients: 2"* ]] || false
[[ "$output" == *"bob"* ]] || false
# Existing blob re-encrypted: bob can read it.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@ -696,7 +696,7 @@ git commit -m "feat: secrets recipients add (EGB-283)"
run "$SECRETS_BIN" recipients list
[[ "$output" == *"recipients: 1"* ]] || false
# Store key still reads its own blobs.
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/work/myproj/.env.age"
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@ -1023,12 +1023,42 @@ git commit -m "feat: verify asserts blob recipient-count matches recipients.txt
### Task 8: Security regression fixtures for `recipients.txt`
**Files:**
- Modify: `test/recipients.bats` (adversarial fixtures only — no `secrets` changes expected; if a case slips through, fix the rail in `secrets` here)
- Modify: `secrets` (one ordering fix in `_load_recipients`, carried over from the Task 1 review); `test/recipients.bats` (adversarial fixtures; if any other case slips through, fix the rail in `secrets` here)
**Interfaces:** none new — exercises `_load_recipients` / `_recipients_add` rails via the CLI.
> Policy reminder: these are ordinary defensive regression tests. Do NOT dispatch red-team/adversarial-review subagents and do NOT run `./test/run-security.sh` — that is operator-local.
- [ ] **Step 0: Fix the dangling-symlink ordering gap (from Task 1 review)**
In `_load_recipients`, the `[ ! -e "$RECIPIENTS_FILE" ]` check runs BEFORE the `[ -L "$RECIPIENTS_FILE" ]` check. A *dangling* symlink (link exists, target missing) makes `[ ! -e ]` true, so it bypasses the symlink refusal and silently degrades to legacy single-key mode. Swap the order so the symlink check comes first:
```bash
_load_recipients() {
RECIPIENT_ARGS=()
if [ -L "$RECIPIENTS_FILE" ]; then
die "Refusing to read symlinked $RECIPIENTS_FILE_NAME (security)."
fi
if [ ! -e "$RECIPIENTS_FILE" ]; then
RECIPIENT_ARGS=(-r "$(get_pubkey)")
return 0
fi
# … rest unchanged (line-by-line parse + validate) …
```
Add a regression test alongside the other SECURITY fixtures:
```bash
@test "SECURITY: a dangling symlink recipients.txt is refused, not silently ignored" {
init_with_remote
ln -s "$TEST_TMPDIR/does-not-exist.txt" "$SECRETS_DIR/recipients.txt"
create_project_dir myproj
run "$SECRETS_BIN" push
[ "$status" -ne 0 ]
[[ "$output" == *"symlink"* ]] || false
}
```
- [ ] **Step 1: Write the fixtures**
```bash