NK-WP-0049-T42: isolated existing-token MFA stage, fixture-tested
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

isolated-privacyidea-mfa.py restores the privacyIDEA database in the accepted
network-none stage and runs privacyIDEA's own check_serial_pass three times in
separate processes: wrong key, wrong code, the owner's code. The serial list is
terminal-only; receipts carry booleans, categories and a serial hash. A first
fixture run was vacuous (PRIVACYIDEA_CONFIGFILE overrode the wrong-key config);
the probe now fixes the variable and asserts the key path. Eight native fixture
tests pass (HOTP, TOTP with PIN, negatives, encrypted bundle). The user-to-token
resolver is not proven here and is recorded as such. T19 awaits the founder run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 328925@bnt-lap001
Assistant-Session: 14b795fd-c632-4c01-8d28-e7e490c7b0ed
This commit is contained in:
tegwick 2026-10-24 17:37:57 +02:00
parent 1959a184cb
commit 1f04c23392
7 changed files with 551 additions and 2 deletions

View file

@ -255,6 +255,52 @@ T20/T31 are done. Source/ciphertext/target bindings and the safe receipt are in
`evidence/2026-10-16-identity-outage-recovery/actual-isolated-privacyidea-review.json`.
Existing-token MFA remains T19; event completeness is not claimed.
## Prepared owner step: isolated existing-token MFA (T19)
Prepared and fixture-tested 2026-10-24 under T42; not yet run on the actual
backup. Execute in your own terminal with a verified transfer receipt (the T23
procedure) and your authenticator ready:
```bash
bash tools/bootstrap-recovery/run-isolated-privacyidea-mfa.sh /path/to/verified/transfer.json
```
Prompts, in order: the `railiance-backup` age key (hidden); a token serial chosen
from the list printed on your terminal (serial, type, usable flag; the list never
enters the receipt); then that token's PIN plus current code exactly as typed at
login (hidden). Pick a token you hold and whose PIN you know. A TOTP code is valid
for tens of seconds, so enter it only when asked and keep the authenticator open.
The restore is the privacyIDEA stage's: restored database and original keys in
`--network none` containers, read-only, capabilities dropped, user 1000, tmpfs
only, isolation inspected before private import. Three checks then run through
privacyIDEA's own `check_serial_pass`, each in a separate process:
1. **Wrong key.** The application is pointed at a random key file; the run asserts
the configuration really names it. The typed code must be refused (native
decryption raises). This is the discriminating step: the same code is accepted
with the original key in step 3.
2. **Wrong code.** The final digit of what you typed is changed; it must be refused.
3. **Your code.** Accepted by the original key and retained token material.
The order is deliberate. Only the last step can spend a counter or replay-protected
code. A first fixture run showed why: when the wrong-key step silently used the
original key, it accepted the code and the real check was then refused. The
configuration assertion now prevents that vacuous result, and the receipt only
claims proof when all three outcomes line up.
The receipt carries booleans, bounded refusal categories, token type, a SHA-256 of
the serial, counts and source hashes only. No serial, PIN or code is recorded, and
the disposable copy is destroyed. Fixture tests cover HOTP and TOTP-with-PIN, a
wrong code alone, an unknown serial, a malformed original key and an encrypted
bundle through `main()`.
**Not proven here:** the user-to-token path. KeyCape calls `/validate/check` with
user, realm and pass, so the restored LDAP resolver, its encrypted bind password
and the LLDAP lookup are exercised only by a chained run (T18 phase B). A policy
such as `otppin=userstore` would not apply to a serial-based check. The receipt
therefore records `user_to_token_resolver_proven: false`.
## Unresolved inputs before execution
- Named immutable disposable target, approved private browser route if required,