Records that the key-ring move leaves older ciphertext unreadable

Starting both hosts after U2 fails to decrypt a stored slave API key: the
row was encrypted under the old file-based key ring, and the database key
ring holds a different, freshly generated key. The key ring working
correctly is exactly why the old key is not found.

U2 moved the ring without a path for ciphertext that predates it. Confirmed
with the user that nothing is deployed yet, so no migration ships and the
first deploy meets an empty database. ASM-08 records the condition and makes
it blocking for U6 if it ever stops holding.

Also notes that the master lets the CryptographicException escape while the
slave catches it and reports "master API key rejected" instead — the
ambiguity U4's event was designed around, showing up on day one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
This commit is contained in:
2026-07-28 11:58:19 +02:00
co-authored by Claude Opus 5
parent 8e79a72340
commit 980dc80701
2 changed files with 35 additions and 2 deletions
@@ -85,3 +85,35 @@ Also recorded for Operations: **DEV-05** — keys are stored unencrypted at rest
No failures occurred during generation.
**Not verifiable at this stage**: the composed startup path (`MigrateCoreDatabase` against a real database, and both hosts actually starting) requires SQL Server. Carried to the phase-level Build and Test stage.
---
## Follow-up found in local testing (2026-07-28)
Starting both hosts after U2 produced, on the master:
```
CryptographicException: The key {2ad1bf17-...} was not found in the key ring
at ApiKeyProtector.Unprotect
at CmsInstanceService.VerifyIntegrityAsync
```
**Not a defect in this unit — it is this unit's change meeting data written before it.** The stored
`MasterCmsInstances.ApiKey` had been encrypted under the old file-based key ring in
`%LOCALAPPDATA%\ASP.NET\DataProtection-Keys`; both databases now hold their own freshly generated
single key, which cannot decrypt it. The key ring working correctly is precisely why the old key is
not found.
**The gap this unit left**: no one-time path for ciphertext that predates the move. Recorded as
**ASM-08** in `requirements.md`. Confirmed with the user on 2026-07-28 that the CMS is not deployed
anywhere yet, so the first deploy meets an empty database and no migration is shipped. If that ever
stops being true, U6 must not deploy until either the affected rows are cleared and the instances
re-registered, or this unit gains a fallback that reads the old ring and re-encrypts.
Resolved locally by deleting the one stale row on each side and re-registering the slave.
**Worth noting for U4's alerting**: the two sides fail differently. `Modules.Master/ApiKeyProtector`
lets the exception escape, while `Modules.Availability/MasterApiKeyProtector` catches it and returns
null, so the slave reports "master API key rejected" instead. That is exactly the ambiguity U4's
`MasterApiKeyRejected` event documents — an intruder, **or** an unreadable key ring — occurring in
practice.