OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / delegate.test.js

51 lines · javascript ref:
// hpp delegate — the running device signs a delegation authorizing a browser sub-key for DID web
// sign-in, and favela's keyless web-auth verify accepts a login proof built from it. Proves the full
// atproto-native auth loop server-side (no browser / no PLC network). Run: bare test/delegate.test.js
import '../lib/bare-globals.js'
import test from 'brittle'
import { deriveAtprotoKey, keyFromPrivate } from 'atproto-identity/lib/atproto-identity.js'
import { signAssertion } from 'atproto-identity/lib/auth.js'
import { makeAtprotoWebVerify } from 'atproto-identity/lib/web-auth.js'
import { SwarmDevice } from '../lib/swarm-device.js'
import b4a from 'b4a'

// The browser uses generateAtprotoKey() (webcrypto) in prod; under Bare there is no
// crypto.getRandomValues, so tests use deterministic keyFromPrivate(fixed bytes).
const subKeyFrom = (h) => keyFromPrivate(b4a.from(h.repeat(32), 'hex'))

// delegate() only needs this.did + this.privateKey — a stub KV substrate is enough (no StateBase).
const stubSb = { set: async () => {}, del: async () => {}, get: async () => null, list: async () => ({}) }
const hex = (u8) => Array.from(u8).map((b) => b.toString(16).padStart(2, '0')).join('')
const nowSec = () => Math.floor(Date.now() / 1000)

test('hpp delegate → browser sign → favela verify (full DID web-auth loop)', async (t) => {
  const key = deriveAtprotoKey('legal winner thank year wave sausage worth useful legal winner thank yellow') // the DID's #atproto key
  const DID = 'did:plc:hppdelegate2222222222222'
  const device = new SwarmDevice(stubSb, { did: DID, privateKey: key.privateKey, self: 'device-a' })
  const sub = subKeyFrom('11') // the browser sub-key
  const subHex = hex(sub.publicKey)

  // 1. the hpp mints the delegation (signs with the shared #atproto key)
  const { delegation, did, subKey, exp } = await device.delegate({ subKey: subHex })
  t.is(did, DID, 'delegation is for this DID')
  t.is(subKey, subHex, 'delegation names the sub-key')
  t.ok(exp > nowSec(), 'delegation carries a future expiry')

  // 2. the browser signs a login challenge with the sub-key; favela verifies the full proof
  //    (mock PLC returns the DID's #atproto pubkey, as the directory would).
  const verify = makeAtprotoWebVerify({ resolveVerifyKey: async (d) => (d === DID ? key.publicKey : null) })
  const challenge = 'c'.repeat(64)
  const sig = signAssertion({ challenge, iat: nowSec(), exp: nowSec() + 300 }, sub.privateKey)
  const binding = await verify({ did: DID, subKey: subHex, challenge, sig, delegation })
  t.ok(binding && binding.did === DID && binding.subKey === subHex, 'favela accepts the hpp-delegated sub-key login')

  // an UNDELEGATED sub-key still cannot log in (delegation is bound to the real sub-key)
  const rogue = subKeyFrom('22')
  const rogueSig = signAssertion({ challenge, exp: nowSec() + 300 }, rogue.privateKey)
  t.absent(await verify({ did: DID, subKey: hex(rogue.publicKey), challenge, sig: rogueSig, delegation }), 'a sub-key the hpp did NOT delegate is rejected')

  // input validation
  await t.exception(async () => { await device.delegate({ subKey: 'nothex' }) }, /subKey/, 'bad subKey rejected')
  await t.exception(async () => { await device.delegate({}) }, /subKey/, 'missing subKey rejected')
})