OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / retrievability-proof-api.test.js

262 lines · javascript ref:
import test from 'brittle'
import http from 'http'
import Hypercore from 'hypercore'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import os from 'os'
import path from 'path'
import { RelayAPI } from 'p2p-hiverelay/core/relay-node/api.js'
import {
  PROOF_KIND_RETRIEVABILITY,
  RETRIEVABILITY_PROOF_SIGNATURE_PROFILE,
  verifyStorageProof
} from 'p2p-hiverelay/core/protocol/proof-of-storage.js'
import {
  resolveRetrievabilityProofRoute
} from 'p2p-hiverelay/core/relay-node/retrievability-proof.js'

let seq = 0
function tmp () {
  return path.join(os.tmpdir(), 'hr-proof-api-' + process.pid + '-' + (seq++))
}

function keyPair () {
  const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
  const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
  sodium.crypto_sign_keypair(publicKey, secretKey)
  return { publicKey, secretKey }
}

function nonce () {
  const out = b4a.alloc(32)
  sodium.randombytes_buf(out)
  return out
}

async function seededCore (blocks) {
  const core = new Hypercore(tmp())
  await core.ready()
  await core.append(blocks)
  return core
}

function mockNode (core, kp, opts = {}) {
  const keyHex = b4a.toString(core.key, 'hex')
  const entry = {
    drive: { core, db: { core }, closed: false, closing: false },
    blind: opts.blind === true,
    privacyTier: opts.privacyTier || 'public'
  }
  return {
    running: true,
    config: {
      storage: null,
      acceptMode: 'review',
      enableRelay: true,
      enableSeeding: true,
      enableAPI: true
    },
    metrics: { getSummary () { return {} }, toPrometheus () { return '' } },
    appRegistry: {
      apps: new Map(),
      has: (key) => !opts.notSeeded && key === keyHex,
      get: (key) => key === keyHex ? entry : null,
      catalog () { return [] },
      catalogForBroadcast () { return [] },
      _shouldRedactEntry (e, o = {}) {
        return e.blind === true ||
          (o.redactPrivate === true && String(e.privacyTier || 'public').toLowerCase() !== 'public')
      }
    },
    seededApps: new Map(),
    getStats () { return { running: true } },
    getHealthStatus () { return { healthy: true } },
    swarm: { keyPair: kp },
    keyPair: kp,
    relay: null,
    seeder: null,
    router: null,
    serviceRegistry: null,
    seedingRegistry: null,
    reputation: null,
    networkDiscovery: null,
    store: { close: async () => {}, replicate: () => {} },
    on () {},
    removeListener () {},
    emit () {}
  }
}

async function setupApi (t, node) {
  const api = new RelayAPI(node, { apiPort: 0, apiHost: '127.0.0.1', apiKey: 'proof-test' })
  await api.start()
  t.teardown(async () => {
    try { await api.stop() } catch {}
  })
  return api.server.address().port
}

function request (port, body) {
  return new Promise((resolve, reject) => {
    const req = http.request({
      hostname: '127.0.0.1',
      port,
      method: 'POST',
      path: '/api/proof/retrievability',
      headers: { 'Content-Type': 'application/json' }
    }, (res) => {
      let data = ''
      res.on('data', chunk => { data += chunk })
      res.on('end', () => {
        let parsed
        try { parsed = JSON.parse(data) } catch { parsed = data }
        resolve({ statusCode: res.statusCode, body: parsed, headers: res.headers })
      })
    })
    req.on('error', reject)
    req.write(JSON.stringify(body))
    req.end()
  })
}

test('api retrievability proof: route helper maps exact public proof route', (t) => {
  t.alike(resolveRetrievabilityProofRoute('POST', '/api/proof/retrievability'), {
    kind: 'retrievability-proof'
  })

  t.is(resolveRetrievabilityProofRoute('GET', '/api/proof/retrievability'), null)
  t.is(resolveRetrievabilityProofRoute('POST', '/api/proof/retrievability/extra'), null)
  t.is(resolveRetrievabilityProofRoute('POST', '/api/proofs/retrievability'), null)
})

test('api retrievability proof: public HTTP proof verifies against drive key', async (t) => {
  const core = await seededCore([b4a.from('a'), b4a.from('bb'), b4a.from('ccc')])
  const relay = keyPair()
  const port = await setupApi(t, mockNode(core, relay))
  const challenge = nonce()

  const res = await request(port, {
    coreKey: b4a.toString(core.key, 'hex'),
    index: 2,
    nonce: b4a.toString(challenge, 'hex')
  })

  t.is(res.statusCode, 200)
  t.is(res.body.ok, true)
  t.is(res.body.proofKind, PROOF_KIND_RETRIEVABILITY)
  t.is(res.body.relayPubkey, b4a.toString(relay.publicKey, 'hex'))

  const verifierCore = new Hypercore(tmp(), core.key)
  await verifierCore.ready()
  const verdict = await verifyStorageProof({
    verifierCore,
    response: res.body,
    expect: {
      driveKey: core.key,
      index: 2,
      nonce: challenge,
      relayPubkey: relay.publicKey,
      minLength: core.length
    }
  })

  t.ok(verdict.valid, 'HTTP proof verifies')
  await verifierCore.close()
  await core.close()
})

test('api retrievability proof: opt-in domain signature profile verifies', async (t) => {
  const core = await seededCore([b4a.from('a'), b4a.from('bb'), b4a.from('ccc')])
  const relay = keyPair()
  const port = await setupApi(t, mockNode(core, relay))
  const challenge = nonce()

  const res = await request(port, {
    coreKey: b4a.toString(core.key, 'hex'),
    index: 1,
    nonce: b4a.toString(challenge, 'hex'),
    signatureProfile: RETRIEVABILITY_PROOF_SIGNATURE_PROFILE
  })

  t.is(res.statusCode, 200)
  t.is(res.body.ok, true)
  t.is(res.body.signatureProfile, RETRIEVABILITY_PROOF_SIGNATURE_PROFILE)

  const verifierCore = new Hypercore(tmp(), core.key)
  await verifierCore.ready()
  const verdict = await verifyStorageProof({
    verifierCore,
    response: res.body,
    expect: {
      driveKey: core.key,
      index: 1,
      nonce: challenge,
      relayPubkey: relay.publicKey,
      minLength: core.length
    }
  })

  t.ok(verdict.valid, 'domain-profile HTTP proof verifies')
  t.is(verdict.signatureProfile, RETRIEVABILITY_PROOF_SIGNATURE_PROFILE)
  await verifierCore.close()
  await core.close()
})

test('api retrievability proof: unsupported signature profile is a cheap 400', async (t) => {
  const core = await seededCore([b4a.from('a')])
  const relay = keyPair()
  const port = await setupApi(t, mockNode(core, relay))

  const res = await request(port, {
    coreKey: b4a.toString(core.key, 'hex'),
    index: 0,
    nonce: b4a.toString(nonce(), 'hex'),
    signatureProfile: 'unknown-proof-profile'
  })

  t.is(res.statusCode, 400)
  t.alike(res.body, { error: 'UNSUPPORTED_SIGNATURE_PROFILE' })
  await core.close()
})

test('api retrievability proof: oversized index is a cheap 400', async (t) => {
  const core = await seededCore([b4a.from('a')])
  const relay = keyPair()
  const port = await setupApi(t, mockNode(core, relay))

  const res = await request(port, {
    coreKey: b4a.toString(core.key, 'hex'),
    index: 2 ** 32,
    nonce: b4a.toString(nonce(), 'hex')
  })

  t.is(res.statusCode, 400)
  t.alike(res.body, { error: 'BAD_INDEX' })
  await core.close()
})

test('api retrievability proof: blind and private drives are indistinguishable from not-seeded', async (t) => {
  const blindCore = await seededCore([b4a.from('secret')])
  const blindPort = await setupApi(t, mockNode(blindCore, keyPair(), { blind: true }))
  const blind = await request(blindPort, {
    coreKey: b4a.toString(blindCore.key, 'hex'),
    index: 0,
    nonce: b4a.toString(nonce(), 'hex')
  })
  t.is(blind.statusCode, 404)
  t.alike(blind.body, { error: 'NOT_SEEDED' })

  const privateCore = await seededCore([b4a.from('private')])
  const privatePort = await setupApi(t, mockNode(privateCore, keyPair(), { privacyTier: 'private' }))
  const privateRes = await request(privatePort, {
    coreKey: b4a.toString(privateCore.key, 'hex'),
    index: 0,
    nonce: b4a.toString(nonce(), 'hex')
  })
  t.is(privateRes.statusCode, 404)
  t.alike(privateRes.body, { error: 'NOT_SEEDED' })

  await blindCore.close()
  await privateCore.close()
})