OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-client / test / cell-readback.test.js

278 lines · javascript ref:
import b4a from 'b4a'
import test from 'brittle'
import {
  CELL_SIZE_CLASS,
  FAMILY,
  OPERATION,
  TRANSPORT_ID,
  encodeCanonical,
  getCellResultV1
} from '@hiverelay/blind-protocol'
import {
  VerifiedOperationResult,
  createCellReplica,
  createGetCellRequest,
  openVerifiedCellGetResult,
  verifyOperationResult
} from '../control.js'
import { sealCell } from '../cells.js'
import { createNodeCryptoRuntime } from '../runtime/node.js'
import { verifiedEndpointFixture } from './endpoint-fixture.js'

const runtime = createNodeCryptoRuntime()
const relayPublicKey = b4a.alloc(32, 0x31)
const admission = {
  profileId: 1,
  schemeId: 1,
  parameterHash: b4a.alloc(32, 0x32),
  token: b4a.from([0x33])
}

async function throwsCode (t, pending, code) {
  try {
    await pending
    t.fail(`expected ${code}`)
  } catch (error) {
    t.is(error.code, code)
  }
}

async function fixture () {
  const structuredContent = b4a.from('peerit recovery record')
  const replica = await createCellReplica({
    runtime,
    relayPublicKey,
    allocationEpoch: 7,
    sizeClass: 1,
    leaseClass: 1,
    structuredContent,
    admission
  })
  const get = await createGetCellRequest({ runtime, readCap: replica.readCap })
  const endpoint = verifiedEndpointFixture({
    endpointId: 1,
    transportId: TRANSPORT_ID.HTTPS_DIRECT,
    envelopeClassBits: 0x007e,
    canonicalUrl: b4a.from('https://relay.example:443/api/blind/v1/describe')
  }, FAMILY.CELL, OPERATION.CELL.GET, { relayPublicKey })
  const resultBytes = encodeCanonical(getCellResultV1, {
    version: 1,
    sizeClass: replica.readCap.sizeClass,
    cellBlob: replica.request.cellBlob
  })
  const verifiedResult = verifyOperationResult({
    endpoint,
    request: get.request,
    requestCommitment: get.requestCommitment,
    resultBytes
  })
  return { endpoint, get, replica, resultBytes, structuredContent, verifiedResult }
}

test('control surface opens only a verified CELL.GET result and returns an independent copy', async t => {
  const value = await fixture()
  t.is(typeof createGetCellRequest, 'function')
  t.is(typeof openVerifiedCellGetResult, 'function')

  value.resultBytes.fill(0)
  const opened = await openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap: value.replica.readCap
  })
  t.alike(opened, value.structuredContent)
  opened.fill(0)
  t.alike(await openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap: value.replica.readCap
  }), value.structuredContent)
})

test('CELL.GET readback requires the package-owned verifier brand', async t => {
  const value = await fixture()
  const forged = Object.create(VerifiedOperationResult.prototype)
  t.ok(forged instanceof VerifiedOperationResult)
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: forged,
    runtime,
    readCap: value.replica.readCap
  }), 'BAD_CLIENT_INPUT')
})

test('CELL.GET readback requires the frozen read capability version', async t => {
  const value = await fixture()
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap: { ...value.replica.readCap, version: 2 }
  }), 'BAD_CLIENT_INPUT')
})

test('CELL.GET readback requires the expected cell-blob hash', async t => {
  const value = await fixture()
  const readCap = { ...value.replica.readCap }
  delete readCap.expectedCellBlobHash
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap
  }), 'BAD_CLIENT_INPUT')
})

test('CELL.GET readback wipes its copied key after success and failure', async t => {
  const value = await fixture()
  let copiedKey
  const observingRuntime = Object.freeze({
    ...runtime,
    async aes256GcmDecrypt (options) {
      copiedKey = options.key
      return runtime.aes256GcmDecrypt(options)
    }
  })
  const originalKey = b4a.from(value.replica.readCap.cellKey)

  t.alike(await openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime: observingRuntime,
    readCap: value.replica.readCap
  }), value.structuredContent)
  t.alike(copiedKey, b4a.alloc(32))
  t.alike(value.replica.readCap.cellKey, originalKey)

  copiedKey = null
  const wrongKey = b4a.alloc(32, 0x43)
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime: observingRuntime,
    readCap: { ...value.replica.readCap, cellKey: wrongKey }
  }), 'CELL_AUTHENTICATION_FAILED')
  t.alike(copiedKey, b4a.alloc(32))
  t.alike(wrongKey, b4a.alloc(32, 0x43))
})

test('CELL.GET readback binds relay, requested slot, result class, key and expected hash', async t => {
  const value = await fixture()
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap: { ...value.replica.readCap, relayPublicKey: b4a.alloc(32, 0x41) }
  }), 'BAD_CLIENT_INPUT')
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap: { ...value.replica.readCap, storageSlot: b4a.alloc(32, 0x42) }
  }), 'BAD_CLIENT_INPUT')
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap: { ...value.replica.readCap, sizeClass: 2 }
  }), 'RELAY_PROTOCOL_VIOLATION')
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap: { ...value.replica.readCap, cellKey: b4a.alloc(32, 0x43) }
  }), 'CELL_AUTHENTICATION_FAILED')
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult: value.verifiedResult,
    runtime,
    readCap: { ...value.replica.readCap, expectedCellBlobHash: b4a.alloc(32, 0x44) }
  }), 'CELL_HASH_MISMATCH')
})

test('CELL.GET verification snapshots an adversarial request slot exactly once', async t => {
  const slotA = await fixture()
  const structuredContentB = b4a.from('slot B must not cross the verified request boundary')
  const replicaB = await createCellReplica({
    runtime,
    relayPublicKey,
    allocationEpoch: 8,
    sizeClass: 1,
    leaseClass: 1,
    structuredContent: structuredContentB,
    admission
  })
  t.unlike(replicaB.readCap.storageSlot, slotA.replica.readCap.storageSlot)

  let storageSlotReads = 0
  const switchingRequest = new Proxy(slotA.get.request, {
    get (target, property, receiver) {
      if (property === 'storageSlot') {
        storageSlotReads++
        return storageSlotReads === 1 ? slotA.replica.readCap.storageSlot : replicaB.readCap.storageSlot
      }
      return Reflect.get(target, property, receiver)
    }
  })
  const verifiedResult = verifyOperationResult({
    endpoint: slotA.endpoint,
    request: switchingRequest,
    requestCommitment: slotA.get.requestCommitment,
    resultBytes: encodeCanonical(getCellResultV1, {
      version: 1,
      sizeClass: replicaB.readCap.sizeClass,
      cellBlob: replicaB.request.cellBlob
    })
  })

  t.is(storageSlotReads, 1)
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult,
    runtime,
    readCap: replicaB.readCap
  }), 'BAD_CLIENT_INPUT')
})

test('CELL.GET readback rejects a valid but capability-inconsistent result class', async t => {
  const value = await fixture()
  const otherClass = await sealCell({
    runtime,
    storageSlot: value.replica.readCap.storageSlot,
    cellKey: value.replica.readCap.cellKey,
    sizeClass: 2,
    structuredContent: value.structuredContent
  })
  t.is(otherClass.cellBlob.byteLength, CELL_SIZE_CLASS[2])
  const verifiedResult = verifyOperationResult({
    endpoint: value.endpoint,
    request: value.get.request,
    requestCommitment: value.get.requestCommitment,
    resultBytes: encodeCanonical(getCellResultV1, {
      version: 1,
      sizeClass: 2,
      cellBlob: otherClass.cellBlob
    })
  })
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult,
    runtime,
    readCap: value.replica.readCap
  }), 'RELAY_PROTOCOL_VIOLATION')
})

test('CELL.GET readback detects post-verification cell-blob tampering', async t => {
  const value = await fixture()
  const tamperedBytes = encodeCanonical(getCellResultV1, {
    version: 1,
    sizeClass: value.replica.readCap.sizeClass,
    cellBlob: b4a.from(value.replica.request.cellBlob)
  })
  tamperedBytes[tamperedBytes.byteLength - 1] ^= 1
  const verifiedResult = verifyOperationResult({
    endpoint: value.endpoint,
    request: value.get.request,
    requestCommitment: value.get.requestCommitment,
    resultBytes: tamperedBytes
  })
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult,
    runtime,
    readCap: value.replica.readCap
  }), 'CELL_HASH_MISMATCH')
  await throwsCode(t, openVerifiedCellGetResult({
    verifiedResult,
    runtime,
    readCap: { ...value.replica.readCap, expectedCellBlobHash: null }
  }), 'BAD_CLIENT_INPUT')
})