// DID web sign-in verify (atproto-native): a #atproto key delegates a browser sub-key; the sub-key
// signs a challenge; makeAtprotoWebVerify checks both. resolveVerifyKey is mocked as the PLC directory
// would return the DID's #atproto pubkey.
import test from 'brittle'
import { deriveAtprotoKey, generateAtprotoKey } from '../lib/atproto-identity.js'
import { signAssertion } from '../lib/auth.js'
import { makeAtprotoWebVerify, verifyChallengeSig, makeVerifyDelegation } from '../lib/web-auth.js'
const DID = 'did:plc:webauthviewer222222222222'
const hex = (u8) => Array.from(u8).map((b) => b.toString(16).padStart(2, '0')).join('')
const nowSec = () => Math.floor(Date.now() / 1000)
// The DID's #atproto signing key (the keyholder / hpp holds this), and the browser's fresh sub-key.
const atproto = deriveAtprotoKey('legal winner thank year wave sausage worth useful legal winner thank yellow')
const sub = generateAtprotoKey()
const subHex = hex(sub.publicKey)
// PLC mock: the DID resolves to its #atproto compressed pubkey; other DIDs → null.
const resolveVerifyKey = async (did) => (did === DID ? atproto.publicKey : null)
// The delegation (minted once by the keyholder): "did DID authorizes sub-key subHex".
const delegation = signAssertion({ iss: DID, sub: subHex, iat: nowSec(), exp: nowSec() + 3600 }, atproto.privateKey)
// A per-login challenge signature by the sub-key.
const CHALLENGE = 'a'.repeat(64)
const sig = signAssertion({ challenge: CHALLENGE, iat: nowSec(), exp: nowSec() + 300 }, sub.privateKey)
test('verifyChallengeSig: the sub-key signed the exact challenge', (t) => {
t.ok(verifyChallengeSig({ subKey: subHex, challenge: CHALLENGE, sig }), 'valid challenge sig accepted')
t.absent(verifyChallengeSig({ subKey: subHex, challenge: 'b'.repeat(64), sig }), 'a sig for a different challenge is rejected')
const wrong = signAssertion({ challenge: CHALLENGE }, generateAtprotoKey().privateKey)
t.absent(verifyChallengeSig({ subKey: subHex, challenge: CHALLENGE, sig: wrong }), 'a sig by a different key is rejected')
})
test('verifyDelegation: the DID #atproto key delegated this sub-key (PLC-anchored)', async (t) => {
const verifyDelegation = makeVerifyDelegation({ resolveVerifyKey })
t.ok(await verifyDelegation({ did: DID, subKey: subHex, delegation }), 'valid delegation accepted')
t.absent(await verifyDelegation({ did: DID, subKey: hex(generateAtprotoKey().publicKey), delegation }), 'a delegation for a DIFFERENT sub-key is rejected')
t.absent(await verifyDelegation({ did: 'did:plc:someoneelse', subKey: subHex, delegation }), 'an unknown DID (no PLC key) is rejected')
const forged = signAssertion({ iss: DID, sub: subHex, exp: nowSec() + 3600 }, generateAtprotoKey().privateKey)
t.absent(await verifyDelegation({ did: DID, subKey: subHex, delegation: forged }), 'a delegation NOT signed by the #atproto key is rejected')
const expired = signAssertion({ iss: DID, sub: subHex, iat: nowSec() - 7200, exp: nowSec() - 3600 }, atproto.privateKey)
t.absent(await verifyDelegation({ did: DID, subKey: subHex, delegation: expired }), 'an expired delegation is rejected')
})
test('makeAtprotoWebVerify: full login proof (challenge sig + delegation)', async (t) => {
const verify = makeAtprotoWebVerify({ resolveVerifyKey })
const binding = await verify({ did: DID, subKey: subHex, challenge: CHALLENGE, sig, delegation, handle: 'viewer.test' })
t.ok(binding && binding.did === DID && binding.subKey === subHex, 'a full valid proof returns the binding')
t.is(binding.handle, 'viewer.test', 'handle carried through')
t.absent(await verify({ did: DID, subKey: subHex, challenge: CHALLENGE, sig }), 'missing delegation → false')
t.absent(await verify({ did: DID, subKey: subHex, challenge: CHALLENGE, delegation }), 'missing sig → false')
// a sub-key NOT delegated by this DID cannot log in even with a valid self-sig
const rogue = generateAtprotoKey()
const rogueSig = signAssertion({ challenge: CHALLENGE, exp: nowSec() + 300 }, rogue.privateKey)
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)')
})