docs(auth): proofs bound to the base URL and to what they approve (ADR 0017)
The full reading's part in ADR 0017 records the device's medium 1 as done (the identity's purpose and digest, the base URL in place of the host, an instance under a path taking no password sign-in), and "The rules as they stand" says what "password" binds; the module's and halkyon_app's READMEs say the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
0a7679396b
commit
7bef70948d
3 changed files with 48 additions and 18 deletions
|
|
@ -271,6 +271,7 @@ deletion):
|
|||
production the host must be the server's `apiServer.publicHost` (`otherInstance`
|
||||
otherwise), any in development; members forward their own. A hostile instance relaying
|
||||
a sign-in or a phrase's proof to the real one, or swapping in a password, gets nothing.
|
||||
(Since the full reading, the instance's whole base URL in place of the host.)
|
||||
- **M7: data keys weren't authenticated.** A sealed data key is now signed by its sealer
|
||||
and checked against the expected one on opening, so the register can't plant a key of
|
||||
its own (before step 4 encrypts anything).
|
||||
|
|
@ -471,18 +472,34 @@ refused but on this machine; the phrase is saved, never copied. Server side:
|
|||
matters only once someone holding it made codes wait; L8, decoys answer today's costs
|
||||
(`newPasswordCosts`), revisit when `PasswordKdf.current` rises; L9, a key proof's first
|
||||
step gives out an address's public keys (step 4 shares keys: revisit then).
|
||||
- **Not done yet, the device's M1:** the owner's proof is bound to the challenge and the
|
||||
host, not to the operation it approves nor the instance's whole base URL; binding a
|
||||
purpose label and the digest of what is set into SRP's identity on both sides, and
|
||||
scheme, host, port and path in place of the host, is the next change. Until then a
|
||||
`change` challenge's proof can be replayed for another signed-in change by whoever
|
||||
reads and changes the traffic (plain `http://`, TLS intercepted), which the refusal of
|
||||
`http://` instances (the device's part 2) narrows.
|
||||
- **The device's medium 1, the owner's proof bound to the challenge and the host
|
||||
alone.** Whoever read and changed the traffic (plain `http://`, TLS intercepted or
|
||||
broken: what SRP is for) could keep the owner's proof and code and swap in a password,
|
||||
a phrase or an authenticator of their own, or turn a proof made for new recovery codes
|
||||
into a deletion; and instances on one host under another port, path or scheme could
|
||||
relay each other's proofs (the device's low 4). SRP's identity is now a label, the
|
||||
challenge's ID, the instance's base URL, what the proof is for and the digest of what
|
||||
it sets (`passwordIdentity`, identity v2), and the phrase's proof signs the base URL
|
||||
too (key proof v3). The base URL is written alike on both sides
|
||||
(`canonicalInstanceUrl`: scheme and host in lower case, the port only where it isn't
|
||||
the scheme's own, the path ending in `/`); the register takes only its own public
|
||||
scheme, host and port at the root (`publicInstanceUrlOf`; Serverpod serves at the root,
|
||||
so an instance under a path takes no password sign-in), any in development, a member's
|
||||
as the member names it. The purposes are signing in, an authenticator's reset (each of
|
||||
its own, so one never stands for the other), a new password (the digest of the
|
||||
`NewPassword` sent), a new phrase (of its wrapping and proof key), the first or another
|
||||
authenticator and removing one (of its ID), new recovery codes, calling resets off and
|
||||
deleting (none); the register works out purpose and digest from the call that brings
|
||||
the proof, never from the device. `OwnerProof`'s and `DeletionProof`'s `host`, and
|
||||
every endpoint's, are `instanceUrl`. Each refusal has a test, which fails with the
|
||||
binding taken out on both sides.
|
||||
|
||||
### The rules as they stand (21 Oct 2026)
|
||||
|
||||
Who changes what of an account, with which proofs. "Password" is SRP's proof bound to
|
||||
the host the device addressed; "code" a TOTP code from a confirmed authenticator or a
|
||||
the instance's base URL the device addressed, and to what it approves (the purpose and
|
||||
the digest of what it sets: a proof made for one change counts for no other); "code" a
|
||||
TOTP code from a confirmed authenticator or a
|
||||
recovery code; "mailed code" one asked for that purpose, for the account's own address
|
||||
unless said otherwise. Signed-in proofs count per session (ten wrong in a row end it);
|
||||
sign-ins' codes per account and way, after the password or the phrase (waits from ten
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
|
|||
`showAccountRecoverySheet`, and `AccountSecuritySection` for an app's
|
||||
account part. Every change of how the account is secured asks for the
|
||||
password with the code, one answer per sheet (`OwnerAnswer`, the owner's
|
||||
proof: a session alone gets no code checked); deleting by a code by mail
|
||||
proof: a session alone gets no code checked), each answer bound to the
|
||||
instance's base URL and to the change it approves (its purpose and the
|
||||
digest of what it sets, `proof_digests.dart`), so nobody between the device
|
||||
and the instance swaps anything in; deleting by a code by mail
|
||||
follows `secondFactorWorks`, as the instance tells it, and asks for the
|
||||
address only where the device doesn't know it. Anonymous people get no keys until something is encrypted
|
||||
(step 4). Every build of an app needs a C compiler and make: sodium's build
|
||||
|
|
|
|||
|
|
@ -210,13 +210,17 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
session. A setup never finished (never a confirmed authenticator) signs in with a code
|
||||
mailed to the address it began with in its place (`request`, `code`), which calls off
|
||||
a reset of the account waiting; any other account takes no code by mail there and
|
||||
leaves it unused. SRP's identity I is `passwordIdentity`: the challenge's ID and the
|
||||
host the device addressed (`host` on every proving call: in production it must be the
|
||||
server's `apiServer.publicHost`, else `otherInstance`; in development any, as an
|
||||
emulator reaches the machine as 10.0.2.2), so a hostile instance relaying a sign-in or
|
||||
a phrase's proof here gets nothing taken; a member forwards its own host, and the
|
||||
phrase's proof for a new password signs that password's digest
|
||||
(`newPasswordDigest`). An address nobody has (or whose
|
||||
leaves it unused. SRP's identity I is `passwordIdentity`: the challenge's ID, the
|
||||
instance's base URL the device addressed (`instanceUrl` on every proving call, written
|
||||
alike on both sides by `canonicalInstanceUrl`: in production it must be the server's
|
||||
own public scheme, host and port at the root, `publicInstanceUrlOf`, else
|
||||
`otherInstance`, so an instance served under a path takes no password sign-in; in
|
||||
development any, as an emulator reaches the machine as 10.0.2.2), what the proof is for
|
||||
and the digest of what it sets (`PasswordProofPurpose`; signing in and an
|
||||
authenticator's reset each its own, with none), so a hostile instance relaying a
|
||||
sign-in or a phrase's proof here gets nothing taken, nor one beside it on the same host;
|
||||
a member forwards its own base URL, and the phrase's proof for a new password signs
|
||||
that URL and that password's digest (`newPasswordDigest`). An address nobody has (or whose
|
||||
account has no password) gets a salt and verifier from `signInDecoyKey` and the
|
||||
address's hash, the same queries and arithmetic, and the same refusal: a wrong
|
||||
password, an unknown address and a wrong, used or missing code are all
|
||||
|
|
@ -238,7 +242,13 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
recovery codes, a new phrase, a new password, deleting) takes the password's SRP proof
|
||||
for a challenge `password.beginChange` gave (30 begun an hour per session, holding
|
||||
back no other) and then a TOTP or recovery code (`proveOwner`, `ownersCode`). A proof
|
||||
given where none is taken is refused, so none passes unchecked. Each proof counts for
|
||||
given where none is taken is refused, so none passes unchecked. The proof is bound to
|
||||
what it approves: SRP's identity carries the purpose and the digest of what is set
|
||||
(`newPasswordDigest` of the new password, `newPhraseDigest` of the phrase's wrapping
|
||||
and proof key, `authenticatorDigest` of the authenticator confirmed or removed; none
|
||||
for new recovery codes, calling resets off and deleting), which the register works out
|
||||
from the call itself, so whoever stands between swaps nothing in and a proof made for
|
||||
one change makes no other. Each proof counts for
|
||||
its session alone (`owner_proof_tries.dart`, `asOwnerProofTry`, table
|
||||
`session_code_failures`, kept where the session is, on a member too), before it is
|
||||
checked; the tenth wrong in a row ends the session (`sessionEnded`), only a proof
|
||||
|
|
@ -295,7 +305,7 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
neither, nor a session and a code.
|
||||
- **Deleting an account with a password** (`account.delete(proof:)`, a `DeletionProof`)
|
||||
takes the same: the password's proof for a `beginChange` challenge and a code, bound
|
||||
to the host the device addressed (`requireOwnerToDelete`; a member passes the proof on
|
||||
to the base URL the device addressed and made for deleting (`requireOwnerToDelete`; a member passes the proof on
|
||||
and the register checks it), counted for the session as every owner's proof is. The
|
||||
code is a TOTP or recovery code wherever an authenticator is confirmed; where no second
|
||||
factor works (a setup never finished, or the authenticators' reset taken effect;
|
||||
|
|
|
|||
Loading…
Reference in a new issue