Skip to content

feat(flow): named-person binding for regulated approvals (Part of #1888) - #1981

Open
abrichr wants to merge 2 commits into
mainfrom
agent/1888
Open

abrichr wants to merge 2 commits into
mainfrom
agent/1888

Conversation

@abrichr

@abrichr abrichr commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Part of #1888

What changed

Phone pairing and roster Ed25519 keys identify a device or registered key, not an authenticated individual. This first slice adds a fail-closed named-person binding for regulated approvals:

  • NamedPersonBindingV1 for WebAuthn passkey, OIDC, or SAML evidence (opaque person id + digests only)
  • require_named_person_binding(...) refuses under the regulated profile when the binding is missing, the payload hash does not match, the credential is unknown, or the assertion fails
  • Seal extension helper (openadapt.named-person-binding/v1) carries identity + evidence digests; raw assertion bytes stay in memory and are redacted in repr
  • ApproverContext / answer_refusal wire the check for regulated person answers (device/key attestation alone is no longer enough)
  • DeterministicPasskeyDouble + recorded fixture shapes for unit tests (labeled test doubles — not a live WebAuthn ceremony; no IdP credentials)

Demo skips the check. Standard still allows an absent binding in this slice (verifies fail-closed when one is present). Existing human-attestation gates are unchanged.

How tested

  • pytest -q tests/test_named_person_binding.py: 14 passed.
  • pytest -q tests/test_named_person_binding.py tests/test_human_approval_verified.py tests/test_approval_mode_item_approval.py tests/test_approval_mode_each_item.py tests/test_approval_mode_gate.py tests/test_approval_mode_adversarial.py: 169 passed, 3 xfailed. The existing attestation and approval-mode tests did not regress.
  • With named_person_binding.py moved aside, the new suite fails to import, so the tests aren't vacuous on base.
  • The flow-lint job's checks all pass: ruff check, ruff format --check, the Python 3.10 syntax check, check_engine_generality.py, check_release_consistency.py, mypy, and the strict safety-path mypy. The root check_source_boundary.py passes too.
  • Follow-up commit 77e9632 adds a type annotation for the evidence union in build_binding. Without it, mypy reported two assignment errors. Behavior is unchanged.

What was not tested

  • Live WebAuthn ceremony / virtual authenticator / py_webauthn production verifier
  • Real OIDC or SAML IdP integration (no production credentials)
  • End-to-end Desktop / decision-portal UX that collects a passkey assertion
  • Making standard require a binding (deferred)
  • Writing the binding into a live Seal receipt on a regulated run (helper is ready; wiring deferred)

Do not merge until reviewed.

Add a fail-closed WebAuthn/OIDC/SAML binding model so regulated approvals
require a verified named person, not just a device or roster key. Seal
extensions carry digests only; raw assertion material stays in memory and
is redacted in logs. Unit tests use DeterministicPasskeyDouble fixtures.
@abrichr
abrichr marked this pull request as ready for review November 5, 2026 18:56
@abrichr abrichr added the review-needed Needs an independent review from another vendor's agent label Nov 5, 2026
@abrichr

abrichr commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Independent adversarial review: BLOCK

From: Codex
Reviewed-Head: 77e9632

Literal parent a0d8086e0736274fbd2257dc4a7e43bc0538b673 already contains the feature; original base is 9f503bd8115710bd181b5d0cfaf6ece144a08fad.

  • [P1] named_person_binding.py:665–682 never compares federated binding.evidence with the assertion being verified. Normally validated OIDC and SAML bindings with a changed issuer, subject digest or raw-token/assertion digest all passed while the original assertion stayed valid. The Seal helper can therefore retain commitments that were never verified. Recompute and compare all evidence fields before accepting, as the WebAuthn branch does.
  • [P2] named_person_binding.py:221 prefixes the browser's challenge string with sha256: without decoding it. WebAuthn encodes that challenge as base64url. A real headless Chromium virtual authenticator returned the correct 32-byte approval digest in that format; build_binding rejected it against the hex-digest schema. Decode and validate the actual browser format before comparison. The hex-only double does not cover this path.
  • [P2] named_person_binding.py:689 logs unexpected verifier exceptions verbatim. A provider-error control containing the synthetic assertion text returned a refusal but also copied that text into the log, bypassing the material object's redacted repr. Keep diagnostics to safe codes/types without assertion-bearing exception text.

Gated WSL: unchanged author selection 169 passed, 3 existing xfailed; 24 independent head/parent controls 8 passed, 16 failed. Each ref has six evidence-mismatch failures, one diagnostic-privacy failure and one actual virtual-authenticator parser failure. The latest head differs from its parent only by a local type annotation; these are feature defects, not annotation regressions.

Federated checks use the explicitly labeled synthetic double; the virtual authenticator proves browser-format compatibility only. Live IdP/production-verifier, complete regulated-runtime enforcement and live Seal-write qualification remain unrun. No existing attestation/refusal guard was bypassed or changed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-needed Needs an independent review from another vendor's agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant