This commit is contained in:
@@ -9,6 +9,8 @@ analytics ──> requestlog ──> requestmeta
|
||||
abuse ─────────────────────> requestmeta
|
||||
authhttp ──> websec ───────> requestmeta
|
||||
authhttp ──> auth <───────── authsqlite
|
||||
│ ▲ ▲
|
||||
└──> authwebauthn ───────────┘
|
||||
organizations <───────────── authsqlite
|
||||
access <──────────────────── authsqlite
|
||||
```
|
||||
@@ -27,6 +29,12 @@ access resolves roles against that hierarchy. Existing `auth` roles remain a
|
||||
platform-level compatibility surface and do not implicitly grant access to an
|
||||
organization's data. Emergency access is a separate, expiring, audited grant.
|
||||
|
||||
`authwebauthn` owns relying-party policy and ceremony orchestration but not
|
||||
application routes. It stores only opaque token digests, bounded verifier
|
||||
session state, credential public records, and audit metadata through an
|
||||
interface implemented by `authsqlite`. The existing `auth` service issues the
|
||||
ordinary opaque session only after the passkey verifier succeeds.
|
||||
|
||||
The package model is developed from explicit threat and data contracts, not by
|
||||
moving an existing application's internals into a shared directory. See
|
||||
[ADOPTION.md](ADOPTION.md), [GETTING_STARTED.md](GETTING_STARTED.md), and the
|
||||
|
||||
+33
-1
@@ -2,12 +2,44 @@
|
||||
|
||||
# Dependency boundary
|
||||
|
||||
Most packages use only the Go standard library. Two direct modules are pinned:
|
||||
Most packages use only the Go standard library. The principal implementation
|
||||
dependencies are pinned:
|
||||
|
||||
- `golang.org/x/crypto` supplies the reviewed Argon2id implementation used by
|
||||
`auth` (BSD-3-Clause upstream licence).
|
||||
- `modernc.org/sqlite` supplies the no-CGO SQLite adapter in `authsqlite`
|
||||
(BSD-3-Clause upstream licence).
|
||||
- `github.com/go-webauthn/webauthn` `v0.17.1` supplies the audited source for
|
||||
WebAuthn Level 3 parsing and cryptographic verification in `authwebauthn`
|
||||
(BSD-3-Clause upstream licence; source commit
|
||||
`de0a809e3027957ca15b72b252540317f9ba581b`). Its imported transitive modules
|
||||
are pinned directly in `go.mod` because the verifier is compiled internally.
|
||||
|
||||
The exact, unchanged `go-webauthn` module source is retained at
|
||||
`third_party/go-webauthn`. The non-test Go files from the packages used by
|
||||
`authwebauthn` are copied into `internal/webauthnvendored`; only their
|
||||
self-import prefix is mechanically rewritten. A derivation gate recreates that
|
||||
internal tree from the audited source and requires a byte-for-byte match before
|
||||
tests or builds. The complete upstream file manifest, upstream module checksum,
|
||||
source commit, licence, and downloaded module ZIP SHA-256 are checked in.
|
||||
The derivative is exercised by ordinary and race-enabled tests, but is excluded
|
||||
from repository formatting so that gate cannot rewrite the audited upstream
|
||||
source. The repository still runs `go vet` over the complete graph and permits
|
||||
only the exact upstream warning for its unexported COSE structure sentinel;
|
||||
every other vet diagnostic fails verification.
|
||||
|
||||
This arrangement is deliberate. A `replace` directive in a library module is
|
||||
ignored by downstream consumers, so it cannot guarantee which verifier source
|
||||
an application compiles. The public module has no local replacement and no
|
||||
direct `github.com/go-webauthn/webauthn` module requirement; applications
|
||||
compile the checked internal derivative instead. Its transitive modules remain
|
||||
pinned by `go.mod`, `go.sum`, and SumDB. Release builders populate an isolated
|
||||
verified module cache before offline compilation.
|
||||
|
||||
A conventional repository-wide `go mod vendor` would also copy the SQLite and
|
||||
full transitive graph, currently roughly 143 MiB and more than 2,300 files.
|
||||
That unrelated expansion is deliberately avoided: only the security-sensitive
|
||||
WebAuthn verifier named by the policy is source-vendored here.
|
||||
|
||||
Applications that do not import `auth` or `authsqlite` do not link those
|
||||
implementations into their binaries. Optional GeoIP enrichment is an interface
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- SPDX-License-Identifier: MPL-2.0 -->
|
||||
|
||||
# Passkey integration
|
||||
|
||||
`authwebauthn` is a passkey ceremony service, not a login page or account
|
||||
policy. An application supplies its exact relying-party identity, routes,
|
||||
authorization decisions, session cookie, HTML, and local recovery command.
|
||||
|
||||
## Fixed security policy
|
||||
|
||||
- Use an exact HTTPS origin whose hostname equals the relying-party ID.
|
||||
- Reject cross-origin ceremonies.
|
||||
- Require discoverable credentials and user verification.
|
||||
- Request no attestation conveyance.
|
||||
- Permit ES256 only until another algorithm has explicit interoperability and
|
||||
security evidence.
|
||||
- Store random challenges and verifier session data only behind opaque,
|
||||
single-use ceremony tokens.
|
||||
- Treat clone warnings as audit signals rather than automatic lockout for
|
||||
synchronized passkeys.
|
||||
|
||||
The service uses a random WebAuthn challenge for every ceremony. A sensitive
|
||||
application operation is bound separately by storing the SHA-256 digest of its
|
||||
canonical payload with that ceremony. Never substitute an operation hash,
|
||||
timestamp, UUID, or counter for the random challenge.
|
||||
|
||||
## Application flow
|
||||
|
||||
1. A local command calls `Bootstrap` or `Recover` and writes the returned
|
||||
enrollment token once to a newly created mode-`0600` file.
|
||||
2. A server-rendered enrollment page calls `BeginEnrollment`; the browser uses
|
||||
`navigator.credentials.create` with the returned `public_key` value.
|
||||
3. The browser posts the credential and opaque ceremony token to a bounded JSON
|
||||
endpoint; `FinishRegistration` verifies and stores the public credential.
|
||||
4. Login uses `BeginLogin`, `navigator.credentials.get`, and `FinishLogin`.
|
||||
The successful result contains an ordinary opaque `auth` session token.
|
||||
5. Sensitive operations call `BeginApproval` with a canonical application
|
||||
payload and `FinishApproval` with those exact same bytes. Any drift fails.
|
||||
|
||||
`authhttp.WritePasskeyBegin` and `authhttp.ReadPasskeyFinish` provide bounded
|
||||
JSON framing only. They do not register routes, authorize requests, serve
|
||||
JavaScript, or set sessions automatically.
|
||||
|
||||
## Recovery and credential lifecycle
|
||||
|
||||
Recovery is deliberately host-local and should never be reachable through an
|
||||
HTTP handler. It revokes all user sessions and pending ceremonies, replaces
|
||||
prior enrollment tokens, appends a secret-free audit event, and returns one
|
||||
15-minute token. It does not delete existing passkeys. After enrolling a
|
||||
replacement, the operator reviews credential labels and removes lost keys with
|
||||
a fresh passkey-bound removal ceremony. The final passkey cannot be removed
|
||||
remotely.
|
||||
|
||||
Before enabling production mutations, applications should require at least two
|
||||
independent passkeys and complete a local recovery drill.
|
||||
@@ -14,6 +14,16 @@ errors, separate safe/sensitive analytics projections, organization-scoped
|
||||
bindings, single-use invitation digests, and short-lived audited break-glass
|
||||
grants.
|
||||
|
||||
Passkey ceremonies require one exact HTTPS origin and relying-party ID,
|
||||
discoverable credentials, user verification, and single-use random challenges.
|
||||
The SQLite adapter consumes enrollment tokens and ceremonies atomically.
|
||||
Operation approvals retain a separate digest of canonical application state;
|
||||
the random WebAuthn challenge is never replaced by a predictable state hash.
|
||||
Cross-origin ceremonies, unsupported credential algorithms, expired state, and
|
||||
binding drift fail closed. Signature-counter clone warnings are surfaced for
|
||||
audit but do not automatically lock out multi-device passkeys whose counters
|
||||
legitimately remain zero.
|
||||
|
||||
An application may create an account with a cryptographically generated
|
||||
temporary credential and `RequirePasswordChange`. Successful rotation compares
|
||||
the current credential, replaces its Argon2id hash, clears the requirement, and
|
||||
@@ -32,6 +42,13 @@ generate the credential cryptographically, and write it only to a newly created
|
||||
private file. A recovery must not reveal whether an account exists through a
|
||||
public request surface.
|
||||
|
||||
Passkey-only recovery never creates a password or remote fallback. A local
|
||||
administrator revokes sessions and active ceremonies and issues a short-lived
|
||||
single-use enrollment token. Existing passkeys remain visible so the operator
|
||||
can review and remove lost credentials after enrolling a replacement. The
|
||||
library prevents remote removal of the final credential; applications should
|
||||
require a freshly bound passkey assertion before every removal.
|
||||
|
||||
Unsafe methods without an exact Origin or trustworthy same-origin Fetch
|
||||
Metadata fail the origin check. Authentication middleware fails closed when its
|
||||
service or `__Host-` cookie policy is invalid. Imported request records have
|
||||
|
||||
Reference in New Issue
Block a user