High-level overview
A user holds one identity (seed + master password → key K0) and
incepts it on-chain as a Key Event Log (KEL). Each app or website gets its
own off-chain branch derived from K0 plus a
site id. Using a password always rotates that branch:
the authenticator proves the current secret and commits a hash of the next one.
- Node verifies signed KEL transactions only
(
public_key_hash,prerotated_key_hash,twice_prerotated_key_hash,prev_public_key_hash). It does not receive or store passwords. - Authenticator (Yada Password extension or Android app)
derives site keys, posts the KEL step, and returns
password+nextPasswordHash(PHC) to the relying party. - Relying party (web harness or Yada Auth Demo) stores the next password hash at register time. On sign-in it checks that the presented password matches that hash, then replaces it with the new next hash.
Closest analogues: KERI for the key log, S/KEY / Lamport OTP for “store hash of next secret,” BIP32 for per-site derivation from K0.
Roles
| Role | Implementation | Holds |
|---|---|---|
| Identity / authenticator | Extension, Android Yada Password | Seed, master password, per-site keys and plaintext passwords |
| Relying party | This node’s harness, Yada Auth Demo | Next password PHC only (plus optional nonce) |
| Ledger | YadaCoin node | On-chain identity KEL + off-chain site branch KELs |
Deep dive
1. Identity inception
K0 = BIP32-style derive(seed, master password).
One on-chain inception transaction is signed by K0 with
prerotated_key_hash = K1 and
twice_prerotated_key_hash = K2.
Site registrations do not rotate this identity KEL.
2. Per-site branch
peerFactor = masterPassword + siteId
(HTTP origins are normalized to scheme://host[:port];
app ids such as yadademo://app are kept as-is).
kp0 = derive(K0, peerFactor)
kp1 = derive(kp0, peerFactor)
kp2 = derive(kp1, peerFactor)
…
Register writes off-chain counter 0 (kp0 → kp1) then counter 1 (kp1 → kp2).
Each later sign-in appends counter n+1. The signer’s
public_key_hash must equal the previous
prerotated_key_hash.
Register resets any existing branch for that site id, then writes counters 0 and 1, so a re-register is a clean start.
3. Passwords (RP-side ratchet)
Passwords are deterministic:
P_i = generateSitePassword(kp_i, siteId, i).
Hashes sent to the RP are PHC strings (default
$pbkdf2-sha256$i=310000$…). Salt is
SHA-256("yada-phc-salt-v1|" + password)[0:16]
so the same password always produces the same PHC (needed for string
compare on the harness). Also supported: argon2id, scrypt, bcrypt.
| Event | Authenticator returns | RP action |
|---|---|---|
| Register | P0 and H(P1) | Store H(P1) as next hash |
| Sign-in | P1 and H(P2) | Verify P1 against stored H(P1); store H(P2) |
A production RP would POST password + nextPasswordHash
to its own backend instead of verifying in the WebView. The Yada node is unchanged.
4. What the node checks
- Transaction signature over the canonical hash.
- Counter = previous counter + 1 (or 0 if none).
public_key_hash= previousprerotated_key_hash.- No password field required on
POST /password-rotation/offchain.
POST /password-rotation/verify still exists for password-gated
advances; the current clients do not use it for sign-in.
5. Resync
If mempool/off-chain tips disappear, Resync rebuilds site keys from K0
to the node tip and drops branches whose inception key no longer matches.
Identity inception is recovered from /key-event-log?public_key=
(not /has-key-event-log, which misses K0).
Node endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /transaction | Identity inception (on-chain) |
| POST | /password-rotation/offchain | Append site-branch KEL step |
| GET | /password-rotation/offchain/tip?branch_peer= | Latest step for a site |
| POST | /password-rotation/offchain/reset | Delete a site branch |
| GET | /key-event-log?public_key= | Identity KEL (inception detection) |
Client transports
| Client | How the RP is invoked | Site id |
|---|---|---|
| Extension + harness | postMessage YADA_PASSWORD_REGISTER / YADA_PASSWORD_SIGNIN | https://host:port origin |
| Android Yada Password + Auth Demo | yadapass://auth?… then callback yadademo://result?… | yadademo://app |
Both authenticators use @yadacoin/password-core
(registerSite, rotateSitePassword,
hashPassword). The RP either compares
passwordHash === storedNext (harness) or
verifyPassword(password, storedNext) (demo). With deterministic
PHC salts those checks are equivalent.