| … | … | @@ -0,0 +1,55 @@ |
| 1 | // DID web sign-in verify (atproto-native): a #atproto key delegates a browser sub-key; the sub-key |
| 2 | // signs a challenge; makeAtprotoWebVerify checks both. resolveVerifyKey is mocked as the PLC directory |
| 3 | // would return the DID's #atproto pubkey. |
| 4 | import test from 'brittle' |
| 5 | import { deriveAtprotoKey, generateAtprotoKey } from '../lib/atproto-identity.js' |
| 6 | import { signAssertion } from '../lib/auth.js' |
| 7 | import { makeAtprotoWebVerify, verifyChallengeSig, makeVerifyDelegation } from '../lib/web-auth.js' |
| 8 | |
| 9 | const DID = 'did:plc:webauthviewer222222222222' |
| 10 | const hex = (u8) => Array.from(u8).map((b) => b.toString(16).padStart(2, '0')).join('') |
| 11 | const nowSec = () => Math.floor(Date.now() / 1000) |
| 12 | |
| 13 | // The DID's #atproto signing key (the keyholder / hpp holds this), and the browser's fresh sub-key. |
| 14 | const atproto = deriveAtprotoKey('legal winner thank year wave sausage worth useful legal winner thank yellow') |
| 15 | const sub = generateAtprotoKey() |
| 16 | const subHex = hex(sub.publicKey) |
| 17 | // PLC mock: the DID resolves to its #atproto compressed pubkey; other DIDs → null. |
| 18 | const resolveVerifyKey = async (did) => (did === DID ? atproto.publicKey : null) |
| 19 | |
| 20 | // The delegation (minted once by the keyholder): "did DID authorizes sub-key subHex". |
| 21 | const delegation = signAssertion({ iss: DID, sub: subHex, iat: nowSec(), exp: nowSec() + 3600 }, atproto.privateKey) |
| 22 | // A per-login challenge signature by the sub-key. |
| 23 | const CHALLENGE = 'a'.repeat(64) |
| 24 | const sig = signAssertion({ challenge: CHALLENGE, iat: nowSec(), exp: nowSec() + 300 }, sub.privateKey) |
| 25 | |
| 26 | test('verifyChallengeSig: the sub-key signed the exact challenge', (t) => { |
| 27 | t.ok(verifyChallengeSig({ subKey: subHex, challenge: CHALLENGE, sig }), 'valid challenge sig accepted') |
| 28 | t.absent(verifyChallengeSig({ subKey: subHex, challenge: 'b'.repeat(64), sig }), 'a sig for a different challenge is rejected') |
| 29 | const wrong = signAssertion({ challenge: CHALLENGE }, generateAtprotoKey().privateKey) |
| 30 | t.absent(verifyChallengeSig({ subKey: subHex, challenge: CHALLENGE, sig: wrong }), 'a sig by a different key is rejected') |
| 31 | }) |
| 32 | |
| 33 | test('verifyDelegation: the DID #atproto key delegated this sub-key (PLC-anchored)', async (t) => { |
| 34 | const verifyDelegation = makeVerifyDelegation({ resolveVerifyKey }) |
| 35 | t.ok(await verifyDelegation({ did: DID, subKey: subHex, delegation }), 'valid delegation accepted') |
| 36 | t.absent(await verifyDelegation({ did: DID, subKey: hex(generateAtprotoKey().publicKey), delegation }), 'a delegation for a DIFFERENT sub-key is rejected') |
| 37 | t.absent(await verifyDelegation({ did: 'did:plc:someoneelse', subKey: subHex, delegation }), 'an unknown DID (no PLC key) is rejected') |
| 38 | const forged = signAssertion({ iss: DID, sub: subHex, exp: nowSec() + 3600 }, generateAtprotoKey().privateKey) |
| 39 | t.absent(await verifyDelegation({ did: DID, subKey: subHex, delegation: forged }), 'a delegation NOT signed by the #atproto key is rejected') |
| 40 | const expired = signAssertion({ iss: DID, sub: subHex, iat: nowSec() - 7200, exp: nowSec() - 3600 }, atproto.privateKey) |
| 41 | t.absent(await verifyDelegation({ did: DID, subKey: subHex, delegation: expired }), 'an expired delegation is rejected') |
| 42 | }) |
| 43 | |
| 44 | test('makeAtprotoWebVerify: full login proof (challenge sig + delegation)', async (t) => { |
| 45 | const verify = makeAtprotoWebVerify({ resolveVerifyKey }) |
| 46 | const binding = await verify({ did: DID, subKey: subHex, challenge: CHALLENGE, sig, delegation, handle: 'viewer.test' }) |
| 47 | t.ok(binding && binding.did === DID && binding.subKey === subHex, 'a full valid proof returns the binding') |
| 48 | t.is(binding.handle, 'viewer.test', 'handle carried through') |
| 49 | t.absent(await verify({ did: DID, subKey: subHex, challenge: CHALLENGE, sig }), 'missing delegation → false') |
| 50 | t.absent(await verify({ did: DID, subKey: subHex, challenge: CHALLENGE, delegation }), 'missing sig → false') |
| 51 | // a sub-key NOT delegated by this DID cannot log in even with a valid self-sig |
| 52 | const rogue = generateAtprotoKey() |
| 53 | const rogueSig = signAssertion({ challenge: CHALLENGE, exp: nowSec() + 300 }, rogue.privateKey) |
| 54 | t.absent(await verify({ did: DID, subKey: hex(rogue.publicKey), challenge: CHALLENGE, sig: rogueSig, delegation }), 'an undelegated sub-key is rejected (delegation is for a different key)') |
| 55 | }) |