Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
83d4459
docs(03): research phase domain
chris-adam Jul 30, 2026
209d045
docs(03): add validation strategy
chris-adam Jul 30, 2026
1420fd3
docs(03): add pattern map
chris-adam Jul 30, 2026
908d39b
docs(03): create phase plan
chris-adam Jul 30, 2026
fdda93e
docs(03): record phase planned — 3 plans ready to execute
chris-adam Jul 30, 2026
439ce05
docs: cross-AI review for phase 3
chris-adam Jul 30, 2026
d766d09
docs(03): replan phase 3 on cross-AI review feedback
chris-adam Jul 30, 2026
1838992
feat(03-01): encrypt TOTP seeds with Fernet, render QR in-process, fi…
chris-adam Jul 30, 2026
16ea5b9
test(03-01): assert fail-closed on enrollment, login, per-call key re…
chris-adam Jul 30, 2026
cdd1681
fix(03-01): stop reporting bulk-enable success when the seed key is b…
chris-adam Jul 30, 2026
549b09a
docs(03-01): complete encrypted-seeds-and-local-qr plan
chris-adam Jul 30, 2026
754609f
feat(03-02): log CRITICAL at boot when the seed key is missing (SEC-08)
chris-adam Jul 30, 2026
6038396
docs(03-02): settle the SEC-07 four-places accounting, document DOC-03
chris-adam Jul 30, 2026
c76a67b
docs(03-02): complete encrypted-seeds-and-local-qr plan
chris-adam Jul 30, 2026
aa9f5fb
fix(03-03): constant-time bar-code reset token comparison at both sites
chris-adam Jul 30, 2026
f61be76
test(03-03): lock BUG-02's redirect invariant; changelog for the phase
chris-adam Jul 30, 2026
5568607
docs(03-03): complete encrypted-seeds-and-local-qr plan
chris-adam Jul 30, 2026
e6c089b
docs(03): add code review report
chris-adam Jul 30, 2026
77fbbe8
test(03): persist human verification items as UAT
chris-adam Jul 30, 2026
82b9a02
test(03): UAT issue - 2FA bypass for Zope-root account, 500 on null seed
chris-adam Jul 30, 2026
9beec32
test(03): UAT re-run confirms interception; new gap G-03-3 (reset email)
chris-adam Jul 30, 2026
1401d65
test(03): complete UAT - 1 passed, 0 issues; criterion 4 verified end…
chris-adam Jul 30, 2026
d8cda87
fix(03): pass charset to MailHost so the reset email survives non-ASCII
chris-adam Jul 30, 2026
3d97681
fix(03): refuse a token when the user has no stored seed
chris-adam Jul 30, 2026
01a8c04
fix(03): refuse 2FA enrolment for accounts the plugin cannot gate
chris-adam Jul 30, 2026
3a45ccd
docs(03): close both residual evidence gaps by observation
chris-adam Jul 30, 2026
08b5fcc
test(03): add Nyquist validation tests for SEC-07 and DOC-03
chris-adam Jul 30, 2026
45a4d28
docs(03): reconstruct validation strategy, nyquist_compliant true
chris-adam Jul 30, 2026
9a263dd
docs: audit milestone v1.0 - 31/71 requirements, 3/8 phases
chris-adam Jul 30, 2026
37ecb26
test(01): add Nyquist validation tests for RENAME-06 and DOC-04
chris-adam Jul 30, 2026
aee5eb0
docs(01): reconcile validation strategy, nyquist_compliant true
chris-adam Jul 30, 2026
4ce7e41
fix(01): strip inline comment from wave_0_complete frontmatter value
chris-adam Jul 30, 2026
7a845a7
test(02): correct requirement id on the import-step declaration test
chris-adam Jul 30, 2026
4307890
docs(02): reconstruct validation strategy, nyquist_compliant true
chris-adam Jul 30, 2026
6bb0032
docs: refresh milestone audit - Nyquist now compliant across all buil…
chris-adam Jul 30, 2026
1023e05
test(03): UAT session 2 - 3 passed, 0 issues
chris-adam Jul 30, 2026
0b66236
docs: transition phase 03 complete, correct stale progress figures
chris-adam Jul 30, 2026
8c1dd22
docs(03): ship phase 3 — PR #3 [ci skip]
chris-adam Jul 30, 2026
cd23705
docs(03): sync STATE body with shipped frontmatter [ci skip]
chris-adam Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions .planning/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ ASVS V2, and to APIs executed against this repo's own Python 2.7.18 interpreter.

### Secret handling (SEC)

- [ ] **SEC-01**: TOTP seeds are Fernet-encrypted at rest; no plaintext seed is ever written to a memberdata property
- [ ] **SEC-02**: The encryption key is read per-call from the process environment, never stored in the ZODB, a memberdata property, a log line, or an exception message
- [ ] **SEC-03**: Enrollment and validation both fail closed when the key is missing or invalid — login is refused, never downgraded to plaintext or to password-only
- [ ] **SEC-04**: Ciphertext carries a `v1$` version prefix
- [ ] **SEC-05**: The enrollment QR code is rendered in-process by `qrcode == 6.1`; the seed is transmitted to no external service and appears in no subprocess argv
- [ ] **SEC-06**: New seeds are 160 bits of `os.urandom`, satisfying RFC 4226 §4 R6's 128-bit minimum
- [ ] **SEC-07**: The required environment variable is documented and present in all four places it must exist — `[instance]`, `[testenv]`, the CI workflow, and (out of repo) the Puppet fragment
- [ ] **SEC-08**: A missing key logs CRITICAL at process start rather than raising from module import or ZCML
- [x] **SEC-01**: TOTP seeds are Fernet-encrypted at rest; no plaintext seed is ever written to a memberdata property
- [x] **SEC-02**: The encryption key is read per-call from the process environment, never stored in the ZODB, a memberdata property, a log line, or an exception message
- [x] **SEC-03**: Enrollment and validation both fail closed when the key is missing or invalid — login is refused, never downgraded to plaintext or to password-only
- [x] **SEC-04**: Ciphertext carries a `v1$` version prefix
- [x] **SEC-05**: The enrollment QR code is rendered in-process by `qrcode == 6.1`; the seed is transmitted to no external service and appears in no subprocess argv
- [x] **SEC-06**: New seeds are 160 bits of `os.urandom`, satisfying RFC 4226 §4 R6's 128-bit minimum
- [x] **SEC-07**: The required environment variable is documented and present in all four places it must exist — `[instance]`, `[testenv]`, the CI workflow, and (out of repo) the Puppet fragment
- [x] **SEC-08**: A missing key logs CRITICAL at process start rather than raising from module import or ZCML
Comment on lines +41 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clarify SEC-07’s configuration ownership.

This says the key is “present” in [instance] and the CI workflow, but the phase records state that the production [instance] value is supplied by the separate Puppet repository and CI inherits it through [testenv]. Rewrite SEC-07 to distinguish “configured by deployment” from “inherited by CI”; otherwise this can be incorrectly audited as a four-file edit in this repository.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.planning/REQUIREMENTS.md around lines 41 - 44, Rewrite SEC-07 to clarify
ownership: state that the production [instance] value is configured by the
separate Puppet deployment repository, while CI inherits the key through
[testenv]. Do not describe the requirement as four in-repository file edits;
retain the requirement that the environment variable is documented and available
in all required runtime contexts.


### Second-factor integrity (MFA)

Expand Down Expand Up @@ -84,10 +84,10 @@ ASVS V2, and to APIs executed against this repo's own Python 2.7.18 interpreter.
### Known bug fixes (BUG)

- [ ] **BUG-01**: `next_url` is validated against the portal URL before redirect; an off-site value is refused (`token.py:112-113`)
- [ ] **BUG-02**: `redirect_url` is always bound on every code path through `user_setup.py`
- [ ] **BUG-03**: The bar-code reset token comparison is constant-time, with both operands encoded first to avoid `TypeError` across `str`/`unicode`
- [x] **BUG-02**: `redirect_url` is always bound on every code path through `user_setup.py`
- [x] **BUG-03**: The bar-code reset token comparison is constant-time, with both operands encoded first to avoid `TypeError` across `str`/`unicode`
- [x] **BUG-04**: The derived `ska` key separates its components rather than concatenating them bare
- [ ] **BUG-05**: `py2-ipaddress` is replaced by `ipaddress == 1.0.23`, with `unicode` coercion at the two call sites, so adding `cryptography` cannot break every login through module shadowing
- [x] **BUG-05**: `py2-ipaddress` is replaced by `ipaddress == 1.0.23`, with `unicode` coercion at the two call sites, so adding `cryptography` cannot break every login through module shadowing
- [ ] **BUG-06**: Query-string values are URL-encoded on the way in, resolving the `+`-escaping FIXME

### Quality (QUAL)
Expand All @@ -104,7 +104,7 @@ ASVS V2, and to APIs executed against this repo's own Python 2.7.18 interpreter.

- [ ] **DOC-01**: The Zope-root limitation is documented — MFA covers users and site admins inside the Plone site; root `acl_users` admins are architecturally out of reach for an in-site PAS plugin
- [ ] **DOC-02**: The basic-auth consequence is documented, naming the supported alternative for scripts and API consumers
- [ ] **DOC-03**: The required encryption-key environment variable is documented for deployment, including the failure mode when a single ZEO client has a stale value
- [x] **DOC-03**: The required encryption-key environment variable is documented for deployment, including the failure mode when a single ZEO client has a stale value
- [x] **DOC-04**: `CHANGES.txt` records the rename and that existing databases are discarded rather than migrated

## v2 Requirements
Expand Down Expand Up @@ -176,14 +176,14 @@ lists above is mechanical. Phase names are in `.planning/ROADMAP.md`.
| REG-03 | Phase 2 | Complete |
| REG-04 | Phase 2 | Complete |
| REG-05 | Phase 2 | Complete |
| SEC-01 | Phase 3 | Pending |
| SEC-02 | Phase 3 | Pending |
| SEC-03 | Phase 3 | Pending |
| SEC-04 | Phase 3 | Pending |
| SEC-05 | Phase 3 | Pending |
| SEC-06 | Phase 3 | Pending |
| SEC-07 | Phase 3 | Pending |
| SEC-08 | Phase 3 | Pending |
| SEC-01 | Phase 3 | Complete |
| SEC-02 | Phase 3 | Complete |
| SEC-03 | Phase 3 | Complete |
| SEC-04 | Phase 3 | Complete |
| SEC-05 | Phase 3 | Complete |
| SEC-06 | Phase 3 | Complete |
| SEC-07 | Phase 3 | Complete |
| SEC-08 | Phase 3 | Complete |
| MFA-01 | Phase 4 | Pending |
| MFA-02 | Phase 4 | Pending |
| MFA-03 | Phase 4 | Pending |
Expand Down Expand Up @@ -214,10 +214,10 @@ lists above is mechanical. Phase names are in `.planning/ROADMAP.md`.
| COEX-08 | Phase 4 | Pending |
| COEX-09 | Phase 7 | Pending |
| BUG-01 | Phase 7 | Pending |
| BUG-02 | Phase 3 | Pending |
| BUG-03 | Phase 3 | Pending |
| BUG-02 | Phase 3 | Complete |
| BUG-03 | Phase 3 | Complete |
| BUG-04 | Phase 2 | Complete |
| BUG-05 | Phase 3 | Pending |
| BUG-05 | Phase 3 | Complete |
| BUG-06 | Phase 7 | Pending |
| QUAL-01 | Phase 8 | Pending |
| QUAL-02 | Phase 8 | Pending |
Expand All @@ -228,7 +228,7 @@ lists above is mechanical. Phase names are in `.planning/ROADMAP.md`.
| QUAL-07 | Phase 8 | Pending |
| DOC-01 | Phase 4 | Pending |
| DOC-02 | Phase 4 | Pending |
| DOC-03 | Phase 3 | Pending |
| DOC-03 | Phase 3 | Complete |
| DOC-04 | Phase 1 | Complete |

**Coverage:**
Expand Down
21 changes: 17 additions & 4 deletions .planning/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Decimal phases appear between their surrounding integers in numeric order.

- [x] **Phase 1: Rename and Fail-Closed** - `imio.googleauthenticator` everywhere, and a plugin exception becomes a 500 instead of a password-only login (completed 2026-07-29)
- [x] **Phase 2: Registry Seeding and Import-Step Ordering** - New Plone sites install cleanly, and the ordering that makes them clean is asserted rather than accidental (completed 2026-07-29)
- [ ] **Phase 3: Encrypted Seeds and Local QR** - Seeds are Fernet-encrypted at rest, never sent to Google, and never fall back to plaintext
- [x] **Phase 3: Encrypted Seeds and Local QR** - Seeds are Fernet-encrypted at rest, never sent to Google, and never fall back to plaintext (completed 2026-07-30)
- [ ] **Phase 4: PAS Boundary** - The second factor cannot be bypassed by any credentials extractor, and the refusal leaks nothing
- [ ] **Phase 5: Drift, Replay and Lockout** - A replayed code fails, brute force stops at N attempts, and the counters actually persist
- [ ] **Phase 6: Recovery Codes** - A user who loses their phone gets back in without an admin, on a throttled path
Expand Down Expand Up @@ -123,9 +123,22 @@ Plans:
2. Two tests assert login is **refused** with the key unset and refused again with the key set to garbage, at both enrollment and validation — never downgraded to plaintext and never to password-only. Fail-closed is the one mistake that silently undoes the entire phase.
3. The enrollment QR renders in-process via `qrcode == 6.1`: no request reaches `chart.googleapis.com`, and no subprocess argv carries the seed (the reason `imio.helpers` + zint was rejected — `--data=otpauth://...secret=<SEED>` is readable in `ps` by any local user).
4. A user enrolls with a real authenticator app and logs in end to end, against a seed that is 160 bits of `os.urandom` (RFC 4226 §4 R6 requires ≥128; `b32encode(str(uuid4()))` gave ~122).
5. `py2-ipaddress` is gone and `ipaddress == 1.0.23` pinned, with `unicode` coercion at `helpers.py:459` and `:496`; a login from a whitelisted CIDR still succeeds. Both distributions install a top-level `ipaddress` module, so without this the site works on a dev box and every login fails on a Puppet-built one, decided by egg ordering.
5. `py2-ipaddress` is gone and `ipaddress == 1.0.23` pinned, with `unicode` coercion at **all three** `ipaddress.*()` call sites in `helpers.py`; a login from a whitelisted CIDR still succeeds. Both distributions install a top-level `ipaddress` module, so without this the site works on a dev box and every login fails on a Puppet-built one, decided by egg ordering. *(Corrected during planning: this criterion previously said two call sites at `helpers.py:459` and `:496`. Those line numbers are stale, and there are three calls — `ip_address(proxies[0])` inside the private-hop strip loop is the third. Missing it is not cosmetic: `AddressValueError` subclasses `ValueError`, so the existing `except ValueError: break` would fire on the first iteration on every request, silently disabling private-hop stripping and making the whitelist trust an attacker-supplied hop.)*

**Plans**: TBD
**Plans**: 3/3 plans executed

Plans:
**Wave 1**

- [x] 03-01-PLAN.md — The ROADMAP's own same-commit group: the `cryptography`/`qrcode`/`ipaddress`/`Pillow` pin swap, the `v1$` Fernet envelope with a per-call key read, a 160-bit `os.urandom` seed via stdlib base32, in-process QR rendering, `unicode` coercion at all three `ipaddress` call sites, `[testenv]`'s throwaway key so Wave 1 ends green, and fail-closed asserted at all four live `get_or_create_secret` surfaces — enrollment, login, bulk enable (unswallowed, with both callers reporting failure instead of "Changes saved.") and account creation (SEC-01/02/03/04/05/06, BUG-05)

**Wave 2** *(blocked on Wave 1 completion)*

- [x] 03-02-PLAN.md — The `IProcessStarting` CRITICAL log for a missing key, the SEC-07 four-places accounting settled with this repo owning exactly one site and no `[instance]` placeholder, and `README.rst` documenting all three consequences of a missing key, the ZEO-client-skew failure mode and the out-of-repo Puppet dependency (SEC-07, SEC-08, DOC-03)

**Wave 3** *(blocked on Wave 2 completion)*

- [x] 03-03-PLAN.md — One constant-time reset-token comparison used at both call sites, a regression test locking the `user_setup.py` redirect invariant with no production change, the real-authenticator-app end-to-end human check for success criterion 4, and the changelog (BUG-03, BUG-02)

**Phase notes:**

Expand Down Expand Up @@ -260,7 +273,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
|-------|----------------|--------|-----------|
| 1. Rename and Fail-Closed | 4/4 | Complete | 2026-07-29 |
| 2. Registry Seeding and Import-Step Ordering | 2/2 | Complete | 2026-07-29 |
| 3. Encrypted Seeds and Local QR | 0/TBD | Not started | - |
| 3. Encrypted Seeds and Local QR | 3/3 | Complete | 2026-07-30 |
| 4. PAS Boundary | 0/TBD | Not started | - |
| 5. Drift, Replay and Lockout | 0/TBD | Not started | - |
| 6. Recovery Codes | 0/TBD | Not started | - |
Expand Down
53 changes: 34 additions & 19 deletions .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
current_phase: 3
current_phase_name: Encrypted Seeds and Local QR
status: "Phase 2 shipped — PR #2"
stopped_at: Completed 02-02-PLAN.md
last_updated: "2026-07-29T14:43:48.711Z"
last_activity: 2026-07-29
current_phase: 4
current_phase_name: PAS Boundary
status: shipped
stopped_at: Phase 03 shipped as PR #3 (37 commits, gsd/phase-3-encrypted-seeds-and-local-qr -> master), awaiting review/merge. Phase 4 not yet planned.
last_updated: "2026-07-30T18:30:00.000Z"
last_activity: 2026-07-30
last_activity_desc: Phase 03 shipped - PR #3
progress:
total_phases: 2
completed_phases: 2
total_plans: 6
completed_plans: 6
last_activity_desc: Phase 02 complete, transitioned to Phase 3
total_phases: 8
completed_phases: 3
total_plans: 9
completed_plans: 9
---

# Project State
Expand All @@ -23,22 +23,25 @@ last_activity_desc: Phase 02 complete, transitioned to Phase 3
See: .planning/PROJECT.md (updated 2026-07-29)

**Core value:** A second factor that actually holds for in-site users, and that can be deployed alongside `imio.dms.mail` without colliding with it.
**Current focus:** Phase 3Encrypted Seeds and Local QR
**Current focus:** Phase 4PAS Boundary (Phase 03 shipped as PR #3, awaiting merge)

## Current Position

Phase: 3Encrypted Seeds and Local QR
Phase: 4PAS Boundary
Plan: Not started
Status: Phase 2 shipped PR #2
Last activity: 2026-07-29
Status: Ready to plan — Phase 03 shipped as PR #3, awaiting review/merge
Last activity: 2026-07-30 — Phase 03 shipped, PR #3

Progress: [████████████████████] 6/6 plans authored (100%) · 2 of 8 roadmap phases complete
Progress: [████████████████████] 9/9 plans authored (100%) · **3 of 8 roadmap phases complete (38%)**

The plans figure is 100% only because plans exist for the three executed phases; phases 4–8
have no plans yet. The phase figure is the honest one.

## Performance Metrics

**Velocity:**

- Total plans completed: 6
- Total plans completed: 9
- Average duration: —
- Total execution time: 0.0 hours

Expand All @@ -48,6 +51,7 @@ Progress: [████████████████████] 6/6 pla
|-------|-------|-------|----------|
| 01 | 4 | - | - |
| 02 | 2 | - | - |
| 03 | 3 | - | - |

**Recent Trend:**

Expand All @@ -65,6 +69,9 @@ Progress: [████████████████████] 6/6 pla
| Phase 01 P04 | 25min | 2 tasks | 7 files |
| Phase 02 P01 | 25min | 2 tasks | 4 files |
| Phase 02 P02 | 12min | 2 tasks | 3 files |
| Phase 03 P01 | 35min | 5 tasks | 8 files |
| Phase 03 P02 | 20min | 2 tasks | 4 files |
| Phase 03 P03 | 45min | 2 tasks | 5 files |

## Accumulated Context

Expand All @@ -90,6 +97,14 @@ Recent decisions affecting current work:
- [Phase 02]: **CORRECTION (supersedes the 02-01 plan's D-04/D-05):** `_setup_secret_key()` was NOT deleted and there is NO lazy mint. CR-02 reverted that design: `setuphandlers._setup_secret_key()` seeds `ska_secret_key` once at install time, and `get_ska_secret_key()` is a pure read that raises `ValueError` on an empty key (fail-closed). Phase 3 must build on the install-time seeding path, not a lazy accessor.
- [Phase ?]: 02-01: REG-05 double-apply test documented as a regression guard against a future schema tightening, not a fix for a currently-firing bug (D-13)
- [Phase ?]: 02-02: BUG-04 fixed via netstring-style length-prefixed join (D-08); test setUp needed a re-login after profile install because PLONE_FIXTURE's cached test-user property sheets predate the add-on's memberdata schema (own-test Rule 1 fix, no production change)
- [Phase ?]: Phase 03-01 Task 1 checkpoint: locked the TOTP-seed encryption-key env var name to IMIO_GOOGLEAUTHENTICATOR_SEED_KEY (human selected the unambiguous option over the shorter IMIO_GA_SEED_KEY plan default). Every plan reference to IMIO_GA_SEED_KEY is substituted with this literal.
- [Phase ?]: Task 1 checkpoint: environment-variable name locked to IMIO_GOOGLEAUTHENTICATOR_SEED_KEY (human overrode plan default IMIO_GA_SEED_KEY).
- [Phase ?]: Task 2 blocking-human package gate: cryptography==3.3.2, ipaddress==1.0.23, qrcode==6.1, cffi==1.15.1, Pillow all approved on live-PyPI-verified provenance.
- [Phase ?]: ska_secret_key control-panel TextLine field (02-SECURITY.md R-02-01) re-deferred again: PasswordWidget blanks an untouched field on Save, so the swap needs its own tested change, not a drive-by.
- [Phase ?]: [Phase 3]: 03-02: base.cfg [instance] deliberately carries no IMIO_GOOGLEAUTHENTICATOR_SEED_KEY entry (whitespace-form buildout can't parse an empty default, and a placeholder would silently suppress the new CRITICAL log); the deployment buildout supplies it, documented in README.rst.
- [Phase ?]: [Phase 3]: 03-02: no docs/ cross-reference added -- docs/index.rst is a stale pre-rename duplicate of an old README never kept in sync; README.rst is the deployer-facing shipped artifact DOC-03 targets.
- [Phase ?]: 03-03: BUG-02 closed by regression test with no production code change -- redirect_url confirmed bound on all three reachable branches of SetupForm.handleSubmit, both by research and by execution (empty diff on user_setup.py).
- [Phase ?]: 03-03: BUG-03 fixed via one shared validate_bar_code_reset_token helper (hmac.compare_digest with str/unicode coercion) used at both reset_bar_code.py comparison sites, not the one the requirement named.

### Pending Todos

Expand Down Expand Up @@ -119,6 +134,6 @@ Items acknowledged and carried forward from previous milestone close:

## Session Continuity

Last session: 2026-07-29T14:36:34Z
Stopped at: Phase 02 complete (UAT 1/1 passed, verification passed, threats_open 0), ready to plan Phase 3
Last session: 2026-07-30T10:12:23.316Z
Stopped at: Completed 03-03-PLAN.md -- phase 03 code-complete, ready for verification
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resume file: None
Loading