OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / seeding-manifest.test.js

213 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import {
  createSeedingManifest,
  verifySeedingManifest,
  isNewerManifest,
  MANIFEST_TYPE,
  MANIFEST_VERSION,
  MAX_RELAYS,
  MAX_DRIVES,
  TIMESTAMP_SKEW_MS
} from 'p2p-hiverelay/core/seeding-manifest.js'

function makeKeyPair () {
  const publicKey = b4a.alloc(32)
  const secretKey = b4a.alloc(64)
  sodium.crypto_sign_keypair(publicKey, secretKey)
  return { publicKey, secretKey }
}

function validHex (n) {
  return Array.from({ length: n }).map((_, i) => (i % 16).toString(16)).join('')
}

const SAMPLE_DRIVE_KEY = validHex(64)
const SAMPLE_DRIVE_KEY_2 = validHex(64).split('').reverse().join('')

function happyArgs (keyPair, extras = {}) {
  return {
    keyPair,
    relays: [
      { url: 'hyperswarm://pkA', role: 'primary' },
      { url: 'wss://relay.example.com/dht', role: 'backup' }
    ],
    drives: [
      { driveKey: SAMPLE_DRIVE_KEY, channel: 'production' }
    ],
    ...extras
  }
}

test('roundtrip: createSeedingManifest → verifySeedingManifest', async (t) => {
  const kp = makeKeyPair()
  const manifest = createSeedingManifest(happyArgs(kp))
  t.is(manifest.type, MANIFEST_TYPE)
  t.is(manifest.version, MANIFEST_VERSION)
  t.is(manifest.pubkey, b4a.toString(kp.publicKey, 'hex'))
  t.ok(typeof manifest.timestamp === 'number')
  t.is(typeof manifest.signature, 'string')
  t.is(manifest.signature.length, 128)

  const check = verifySeedingManifest(manifest)
  t.ok(check.valid, 'fresh manifest verifies')
  t.is(check.pubkey, b4a.toString(kp.publicKey, 'hex'))
})

test('verify rejects tampered relays', async (t) => {
  const kp = makeKeyPair()
  const manifest = createSeedingManifest(happyArgs(kp))
  manifest.relays.push({ url: 'hyperswarm://attacker', role: 'primary' })
  const check = verifySeedingManifest(manifest)
  t.absent(check.valid)
  t.is(check.reason, 'signature verification failed')
})

test('verify rejects tampered drives', async (t) => {
  const kp = makeKeyPair()
  const manifest = createSeedingManifest(happyArgs(kp))
  manifest.drives[0].driveKey = SAMPLE_DRIVE_KEY_2
  const check = verifySeedingManifest(manifest)
  t.absent(check.valid)
  t.is(check.reason, 'signature verification failed')
})

test('verify rejects tampered timestamp', async (t) => {
  const kp = makeKeyPair()
  const manifest = createSeedingManifest(happyArgs(kp))
  manifest.timestamp = manifest.timestamp - 1000
  const check = verifySeedingManifest(manifest)
  t.absent(check.valid)
  t.is(check.reason, 'signature verification failed')
})

test('verify rejects tampered pubkey (signature check catches it)', async (t) => {
  const kp = makeKeyPair()
  const other = makeKeyPair()
  const manifest = createSeedingManifest(happyArgs(kp))
  manifest.pubkey = b4a.toString(other.publicKey, 'hex')
  const check = verifySeedingManifest(manifest)
  t.absent(check.valid)
})

test('verify rejects future-dated timestamp beyond skew', async (t) => {
  const kp = makeKeyPair()
  const future = Date.now() + TIMESTAMP_SKEW_MS + 60_000
  const manifest = createSeedingManifest(happyArgs(kp, { timestamp: future }))
  const check = verifySeedingManifest(manifest)
  t.absent(check.valid)
  t.is(check.reason, 'timestamp in the future')
})

test('verify accepts timestamp within skew window', async (t) => {
  const kp = makeKeyPair()
  const nearFuture = Date.now() + 1000 // 1s in future, well within 5min skew
  const manifest = createSeedingManifest(happyArgs(kp, { timestamp: nearFuture }))
  const check = verifySeedingManifest(manifest)
  t.ok(check.valid)
})

test('verify rejects wrong type or version', async (t) => {
  const kp = makeKeyPair()
  const m1 = createSeedingManifest(happyArgs(kp))
  m1.type = 'wrong/type'
  t.is(verifySeedingManifest(m1).reason, 'wrong type')

  const m2 = createSeedingManifest(happyArgs(kp))
  m2.version = 999
  t.is(verifySeedingManifest(m2).reason, 'unsupported version')
})

test('verify rejects malformed fields', async (t) => {
  const kp = makeKeyPair()
  // Build a valid manifest, then mutate individual fields.
  const base = createSeedingManifest(happyArgs(kp))

  t.is(verifySeedingManifest({ ...base, pubkey: 'nothex' }).reason, 'bad pubkey')
  t.is(verifySeedingManifest({ ...base, timestamp: 'now' }).reason, 'bad timestamp')
  t.is(verifySeedingManifest({ ...base, relays: 'notarray' }).reason, 'relays not array')
  t.is(verifySeedingManifest({ ...base, drives: 'notarray' }).reason, 'drives not array')
  t.is(verifySeedingManifest({ ...base, signature: 'nothex' }).reason, 'bad signature')
  t.is(verifySeedingManifest(null).reason, 'not an object')
})

test('createSeedingManifest rejects too many relays', async (t) => {
  const kp = makeKeyPair()
  const relays = Array.from({ length: MAX_RELAYS + 1 }).map((_, i) => ({
    url: 'hyperswarm://r' + i, role: 'primary'
  }))
  try {
    createSeedingManifest({ keyPair: kp, relays, drives: [{ driveKey: SAMPLE_DRIVE_KEY }] })
    t.fail('should reject')
  } catch (err) {
    t.ok(err.message.includes('too many relays'))
  }
})

test('createSeedingManifest rejects too many drives', async (t) => {
  const kp = makeKeyPair()
  const drives = Array.from({ length: MAX_DRIVES + 1 }).map(() => ({ driveKey: SAMPLE_DRIVE_KEY }))
  try {
    createSeedingManifest({ keyPair: kp, relays: [{ url: 'hyperswarm://a' }], drives })
    t.fail('should reject')
  } catch (err) {
    t.ok(err.message.includes('too many drives'))
  }
})

test('createSeedingManifest rejects invalid relay role', async (t) => {
  const kp = makeKeyPair()
  try {
    createSeedingManifest({
      keyPair: kp,
      relays: [{ url: 'hyperswarm://a', role: 'primary-backup' }],
      drives: [{ driveKey: SAMPLE_DRIVE_KEY }]
    })
    t.fail('should reject')
  } catch (err) {
    t.ok(err.message.includes('bad relay role'))
  }
})

test('createSeedingManifest rejects non-hex drive key', async (t) => {
  const kp = makeKeyPair()
  try {
    createSeedingManifest({
      keyPair: kp,
      relays: [{ url: 'hyperswarm://a' }],
      drives: [{ driveKey: 'nothex' }]
    })
    t.fail('should reject')
  } catch (err) {
    t.ok(err.message.includes('bad driveKey'))
  }
})

test('isNewerManifest compares timestamps for same pubkey only', async (t) => {
  const kp = makeKeyPair()
  const older = createSeedingManifest(happyArgs(kp, { timestamp: 100 }))
  const newer = createSeedingManifest(happyArgs(kp, { timestamp: 200 }))
  t.ok(isNewerManifest(newer, older))
  t.absent(isNewerManifest(older, newer))
  t.ok(isNewerManifest(newer, null), 'any manifest is newer than null')

  const otherKp = makeKeyPair()
  const otherAuthor = createSeedingManifest(happyArgs(otherKp, { timestamp: 999 }))
  t.absent(isNewerManifest(otherAuthor, newer),
    'different-pubkey manifest is never a replacement')
})

test('roundtrip preserves canonical payload despite key order shuffling', async (t) => {
  // Build a manifest, then re-serialize its drives with keys in reverse
  // alphabetical order. canonicalPayload sorts keys internally, so the
  // signature should still verify.
  const kp = makeKeyPair()
  const m = createSeedingManifest(happyArgs(kp))
  // Round-trip via JSON to simulate a client that re-serializes before
  // re-sending. Key order in the JSON representation should not matter.
  const roundtripped = JSON.parse(JSON.stringify(m))
  const check = verifySeedingManifest(roundtripped)
  t.ok(check.valid)
})