OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-client / describe.js

760 lines · javascript ref:
import b4a from 'b4a'
import {
  ADVERTISED_OPERATION_BITS,
  AUXILIARY_SIGNATURE_DOMAIN_ID,
  CLOCK_UNSAFE_OPERATION_BITS,
  DISPATCH_LIMITS,
  FAMILY,
  HEALTH_CLOCK_STATE,
  HEALTH_INTEGRITY_STATE,
  OPERATION,
  OPERATION_PROFILE_ROWS,
  RESULT_SIGNATURE_DOMAIN_ID,
  STORE_LIFECYCLE_STATE,
  TRANSPORT_ID,
  TRANSPORT_SUPPORT,
  operationBit,
  operationProfile,
  isAdvertisedOperation
} from '@hiverelay/blind-protocol/wire-runtime-authority'
import {
  admissionParametersHash,
  auxiliarySignaturePayload,
  durabilityContinuityHash,
  durabilityProfileHash,
  serviceDescriptorHash
} from '@hiverelay/blind-protocol/hashes'
import {
  admissionParametersV1,
  blindAdmissionParametersRequestV1,
  blindDescribeGetV1,
  blindHealthChallengeV1,
  blindHealthResultV1,
  blindServiceDescriptorV1,
  durabilityProfileV1,
  relayIdentityTransitionV1
} from '@hiverelay/blind-protocol/schemas'
import { encodeCanonical } from '@hiverelay/blind-protocol/codec'
import { smallestOuterClass } from '@hiverelay/blind-protocol/outer-envelope'
import { asBytes, randomBytes } from './bytes.js'
import { fail } from './errors.js'
import { issueVerifiedEndpoint } from './verified-endpoint.js'
import {
  decodeCanonicalCopy,
  sameBytes,
  verifyDetached,
  verifyResultSignedValue
} from './signed.js'

const VERIFIED_DESCRIPTOR = Symbol('VerifiedDescriptor')
const TRUSTED_DESCRIPTOR = Symbol('TrustedDescriptor')
const VERIFIED_HEALTH = Symbol('VerifiedHealth')
const VERIFIED_ADMISSION = Symbol('VerifiedAdmissionParameters')
const MAX_DESCRIPTOR_HISTORY = 4096
const MAX_HEALTH_QUALIFICATION_AGE_MILLIS = 10 * 60 * 1000
const descriptorInternals = new WeakMap()
const trustedInternals = new WeakMap()
const healthInternals = new WeakMap()
const admissionInternals = new WeakMap()

function bytesHex (value) {
  return b4a.toString(value, 'hex')
}

function epoch (value, field) {
  if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
    fail('BAD_CLIENT_INPUT', `${field} is outside u32`)
  }
  return value
}

function monotonicMillis (value, field) {
  if (value == null) {
    value = globalThis.performance && typeof globalThis.performance.now === 'function'
      ? globalThis.performance.now()
      : Date.now()
  }
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
    fail('BAD_CLIENT_INPUT', `${field} must be a non-negative finite monotonic timestamp`)
  }
  return value
}

function oneHot (value, field) {
  if (!Number.isSafeInteger(value) || value <= 0 || value > 0xffff || (value & (value - 1)) !== 0) {
    fail('BAD_CLIENT_INPUT', `${field} must contain exactly one known support bit`)
  }
  return value
}

function knownTransportSupportBit (value) {
  const supportBit = oneHot(value, 'transportSupportBit')
  knownRegistryValue(TRANSPORT_SUPPORT, supportBit, 'transportSupportBit')
  return supportBit
}

function supportedProfiles (values, idField, hashField, label, validate) {
  if (!Array.isArray(values) || values.length === 0) fail('BAD_CLIENT_INPUT', `${label} cannot be empty`)
  const result = new Map()
  for (const value of values) {
    if (!value || !Number.isSafeInteger(value[idField]) || value[idField] < 1 || value[idField] > 0xff) {
      fail('BAD_CLIENT_INPUT', `${label} has an invalid ID`)
    }
    validate(value)
    const hash = b4a.from(asBytes(value[hashField], `${label} hash`, 32))
    if (result.has(value[idField])) fail('BAD_CLIENT_INPUT', `${label} contains a duplicate ID`)
    result.set(value[idField], Object.freeze({ ...value, [hashField]: hash }))
  }
  return result
}

function u16 (value, field) {
  if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff) {
    fail('BAD_CLIENT_INPUT', `${field} is outside u16`)
  }
  return value
}

function knownRegistryValue (registry, value, field) {
  if (!Object.values(registry).includes(value)) fail('BAD_CLIENT_INPUT', `${field} is not in the closed registry`)
}

function continuityBinding (durability) {
  return b4a.concat([
    b4a.from([1, durability.profileId]),
    durability.externalJournalId,
    durability.externalWitnessPublicKey,
    b4a.from([durability.externalJournalReplicationClass]),
    durability.externalJournalFailureGroupId,
    durability.restoreEvidenceFeedId
  ])
}

function assertDescriptorHashes (descriptor) {
  const profileBytes = encodeCanonical(durabilityProfileV1, descriptor.durability)
  if (!sameBytes(durabilityProfileHash(profileBytes), descriptor.durabilityProfileHash)) {
    fail('RELAY_PROTOCOL_VIOLATION', 'descriptor durability profile hash is invalid')
  }
  const bindingBytes = continuityBinding(descriptor.durability)
  if (!sameBytes(durabilityContinuityHash(bindingBytes), descriptor.durabilityContinuityHash)) {
    fail('RELAY_PROTOCOL_VIOLATION', 'descriptor durability continuity hash is invalid')
  }
}

function assertCurrentEpoch (value, nowEpoch, label) {
  nowEpoch = epoch(nowEpoch, 'nowEpoch')
  if (nowEpoch < value.issuedEpoch || nowEpoch >= value.expiresEpoch) {
    fail('RELAY_PROTOCOL_VIOLATION', `${label} is outside its signed epoch window`)
  }
}

export class VerifiedDescriptor {
  constructor (token, fields) {
    if (token !== VERIFIED_DESCRIPTOR) throw new TypeError('VerifiedDescriptor is not directly constructible')
    descriptorInternals.set(this, fields)
    Object.freeze(this)
  }

  get descriptorHash () { return b4a.from(descriptorInternals.get(this).hash) }
  get descriptorSequence () { return descriptorInternals.get(this).value.descriptorSequence }
  get relayPublicKey () { return b4a.from(descriptorInternals.get(this).value.relayPublicKey) }
  get storeId () { return b4a.from(descriptorInternals.get(this).value.storeId) }
  snapshotBytes () { return b4a.from(descriptorInternals.get(this).bytes) }
}

function verifiedFields (value) {
  const fields = descriptorInternals.get(value)
  if (!fields) fail('BAD_CLIENT_INPUT', 'a VerifiedDescriptor is required')
  return fields
}

export function verifyDescriptorBytes (input, options = {}) {
  const decoded = decodeCanonicalCopy(blindServiceDescriptorV1, input, 'service descriptor')
  const descriptor = decoded.value
  if ((descriptor.enabledOperationBits & ~ADVERTISED_OPERATION_BITS) !== 0) {
    fail('RELAY_PROTOCOL_VIOLATION',
      'service descriptor enables an operation reserved by the active release profile')
  }
  const descriptorHash = serviceDescriptorHash(decoded.bytes)
  if (options.expectedDescriptorHash != null &&
      !sameBytes(descriptorHash, asBytes(options.expectedDescriptorHash, 'expectedDescriptorHash', 32))) {
    fail('RELAY_PROTOCOL_VIOLATION', 'service descriptor hash does not match the requested history object')
  }
  assertDescriptorHashes(descriptor)
  verifyResultSignedValue(blindServiceDescriptorV1, descriptor,
    RESULT_SIGNATURE_DOMAIN_ID.DESCRIPTOR, descriptor.relayPublicKey, 'service descriptor')
  if (options.history !== true) assertCurrentEpoch(descriptor, options.nowEpoch, 'service descriptor')

  const protocols = supportedProfiles(options.supportedProtocolProfiles,
    'protocolId', 'profileHash', 'supported protocol profiles', value => {
      knownRegistryValue(FAMILY, value.protocolId, 'supported protocol ID')
      u16(value.major, 'supported protocol major')
      u16(value.minimumMinor, 'supported protocol minimumMinor')
    })
  const transports = supportedProfiles(options.supportedTransportProfiles,
    'transportId', 'transportProfileHash', 'supported transport profiles', value => {
      knownRegistryValue(TRANSPORT_ID, value.transportId, 'supported transport ID')
      knownRegistryValue(TRANSPORT_SUPPORT, value.transportSupportBit, 'supported transport support bit')
    })
  const descriptorProtocols = new Map(descriptor.protocols.map(value => [value.protocolId, value]))
  for (const [protocolId, supported] of protocols) {
    const advertised = descriptorProtocols.get(protocolId)
    if (advertised && (advertised.major !== supported.major || advertised.minor < supported.minimumMinor ||
        !sameBytes(advertised.profileHash, supported.profileHash))) {
      fail('RELAY_PROTOCOL_VIOLATION', `descriptor protocol ${protocolId} contradicts the pinned profile`)
    }
  }

  return new VerifiedDescriptor(VERIFIED_DESCRIPTOR, {
    bytes: b4a.from(decoded.bytes),
    value: descriptor,
    hash: descriptorHash,
    supportedProtocols: protocols,
    supportedTransports: transports
  })
}

function verifyIdentityTransition (transition, previous, descriptor) {
  const complete = encodeCanonical(relayIdentityTransitionV1, transition)
  const unsigned = complete.subarray(0, complete.byteLength - 128)
  const payload = auxiliarySignaturePayload(AUXILIARY_SIGNATURE_DOMAIN_ID.IDENTITY_TRANSITION, unsigned)
  verifyDetached(previous.relayPublicKey, payload, transition.oldSignature, 'old identity transition')
  verifyDetached(transition.newRelayKey, payload, transition.newSignature, 'new identity transition')
  if (!sameBytes(transition.oldRelayKey, previous.relayPublicKey) ||
      BigInt(transition.oldIdentitySequence) !== BigInt(previous.identitySequence) ||
      BigInt(transition.newIdentitySequence) !== BigInt(previous.identitySequence) + 1n ||
      transition.validFromEpoch !== descriptor.issuedEpoch) {
    fail('DESCRIPTOR_CHAIN_INVALID', 'identity transition does not continue the trusted identity')
  }
}

function copyState (state) {
  if (state == null) return null
  return {
    rootRelayPublicKey: b4a.from(state.rootRelayPublicKey),
    storeId: b4a.from(state.storeId),
    currentBytes: b4a.from(state.currentBytes),
    currentHash: b4a.from(state.currentHash),
    sequence: BigInt(state.sequence),
    identitySequence: BigInt(state.identitySequence),
    relayPublicKey: b4a.from(state.relayPublicKey),
    durabilityProfileId: state.durabilityProfileId,
    durabilityContinuityHash: b4a.from(state.durabilityContinuityHash),
    history: state.history.map(bytes => b4a.from(bytes)),
    quarantined: state.quarantined === true
  }
}

export class MemoryDescriptorTrustBackend {
  constructor () {
    this.records = new Map()
  }

  async read (key) {
    const record = this.records.get(key)
    return record == null ? { version: 0, value: null } : { version: record.version, value: copyState(record.value) }
  }

  async compareAndSwap (key, expectedVersion, value) {
    const current = this.records.get(key)
    const version = current == null ? 0 : current.version
    if (version !== expectedVersion) return false
    this.records.set(key, { version: version + 1, value: copyState(value) })
    return true
  }
}

export class TrustedDescriptor {
  constructor (token, verified, rootRelayPublicKey, trustVersion) {
    if (token !== TRUSTED_DESCRIPTOR) throw new TypeError('TrustedDescriptor is not directly constructible')
    trustedInternals.set(this, {
      verified,
      rootRelayPublicKey: b4a.from(rootRelayPublicKey),
      trustVersion
    })
    Object.freeze(this)
  }

  get descriptorHash () { return b4a.from(verifiedFields(trustedInternals.get(this).verified).hash) }
  get descriptorSequence () { return verifiedFields(trustedInternals.get(this).verified).value.descriptorSequence }
  get rootRelayPublicKey () { return b4a.from(trustedInternals.get(this).rootRelayPublicKey) }
  get trustVersion () { return trustedInternals.get(this).trustVersion }
}

// Validity is deliberately exposed only through the package-owned trust brand.
// Returning a fresh frozen value prevents callers from reaching or mutating the
// decoded signed descriptor held by the continuity store.
export function trustedDescriptorValidity (trusted) {
  const trust = trustedFields(trusted)
  const descriptor = verifiedFields(trust.verified).value
  return Object.freeze({
    issuedEpoch: descriptor.issuedEpoch,
    expiresEpoch: descriptor.expiresEpoch
  })
}

function copyAdmissionProfile (value) {
  return Object.freeze({
    profileId: value.profileId,
    schemeId: value.schemeId,
    conformanceClass: value.conformanceClass,
    roleBits: value.roleBits,
    parameterUrl: value.parameterUrl == null ? null : b4a.from(value.parameterUrl),
    parameterHash: b4a.from(value.parameterHash)
  })
}

export function trustedAdmissionProfile (trusted, profileId) {
  if (!(trusted instanceof TrustedDescriptor)) fail('BAD_CLIENT_INPUT', 'a TrustedDescriptor is required')
  if (!Number.isSafeInteger(profileId) || profileId < 1 || profileId > 0xffff) {
    fail('BAD_CLIENT_INPUT', 'admission profileId is outside u16')
  }
  const trust = trustedFields(trusted)
  const descriptor = verifiedFields(trust.verified).value
  const profile = descriptor.admissionProfiles.find(value => value.profileId === profileId)
  return profile == null ? null : copyAdmissionProfile(profile)
}

function trustedFields (value) {
  const fields = trustedInternals.get(value)
  if (!fields) fail('BAD_CLIENT_INPUT', 'a TrustedDescriptor is required')
  return fields
}

export class DescriptorTrustStore {
  constructor (backend = new MemoryDescriptorTrustBackend()) {
    if (!backend || typeof backend.read !== 'function' || typeof backend.compareAndSwap !== 'function') {
      fail('BAD_CLIENT_INPUT', 'descriptor trust backend must implement read and compareAndSwap')
    }
    this.backend = backend
  }

  async accept (verified, options = {}) {
    if (!(verified instanceof VerifiedDescriptor)) fail('BAD_CLIENT_INPUT', 'a VerifiedDescriptor is required')
    const verifiedState = verifiedFields(verified)
    const descriptor = verifiedState.value
    const descriptorSequence = BigInt(descriptor.descriptorSequence)
    let continuityRoot
    if (descriptorSequence === 0n) {
      continuityRoot = b4a.from(descriptor.relayPublicKey)
      if (options.continuityRootRelayPublicKey != null &&
          !sameBytes(continuityRoot, asBytes(options.continuityRootRelayPublicKey,
            'continuityRootRelayPublicKey', 32))) {
        fail('UNTRUSTED_RELAY_IDENTITY', 'genesis continuity root does not match its relay key')
      }
    } else {
      if (options.continuityRootRelayPublicKey == null) {
        fail('UNTRUSTED_RELAY_IDENTITY', 'descriptor continuation requires its persisted continuity root')
      }
      continuityRoot = b4a.from(asBytes(options.continuityRootRelayPublicKey,
        'continuityRootRelayPublicKey', 32))
    }
    // A relay chooses its own storeId. Keying trust only by that field lets a
    // hostile candidate collide with and quarantine an unrelated operator.
    const key = `descriptor:${bytesHex(continuityRoot)}:${bytesHex(descriptor.storeId)}`
    for (let attempt = 0; attempt < 8; attempt++) {
      const record = await this.backend.read(key)
      const previousState = record.value
      let next
      if (previousState == null) {
        if (descriptorSequence !== 0n || options.pinnedDescriptorHash == null ||
            !sameBytes(verifiedState.hash, asBytes(options.pinnedDescriptorHash, 'pinnedDescriptorHash', 32))) {
          fail('UNTRUSTED_RELAY_IDENTITY', 'descriptor genesis requires an exact authenticated pin')
        }
        next = {
          rootRelayPublicKey: continuityRoot,
          storeId: descriptor.storeId,
          currentBytes: verifiedState.bytes,
          currentHash: verifiedState.hash,
          sequence: 0n,
          identitySequence: BigInt(descriptor.identitySequence),
          relayPublicKey: descriptor.relayPublicKey,
          durabilityProfileId: descriptor.durability.profileId,
          durabilityContinuityHash: descriptor.durabilityContinuityHash,
          history: [verifiedState.bytes],
          quarantined: false
        }
      } else {
        if (previousState.quarantined) fail('DESCRIPTOR_FORK', 'relay continuity is quarantined')
        if (!sameBytes(previousState.rootRelayPublicKey, continuityRoot)) {
          fail('DESCRIPTOR_CHAIN_INVALID', 'descriptor continuation changed its persisted continuity root')
        }
        const sequence = BigInt(descriptor.descriptorSequence)
        if (sequence === previousState.sequence) {
          if (sameBytes(verifiedState.hash, previousState.currentHash)) {
            return new TrustedDescriptor(TRUSTED_DESCRIPTOR, verified,
              previousState.rootRelayPublicKey, record.version)
          }
          next = { ...previousState, quarantined: true }
          if (await this.backend.compareAndSwap(key, record.version, next)) {
            fail('DESCRIPTOR_FORK', 'same-sequence descriptor fork detected and quarantined')
          }
          continue
        }
        if (sequence !== previousState.sequence + 1n || !sameBytes(descriptor.previousDescriptorHash, previousState.currentHash)) {
          fail('DESCRIPTOR_CHAIN_INVALID', 'descriptor does not exactly extend the trusted sequence/hash')
        }
        if (descriptor.durability.profileId !== previousState.durabilityProfileId ||
            !sameBytes(descriptor.durabilityContinuityHash, previousState.durabilityContinuityHash)) {
          fail('DESCRIPTOR_CHAIN_INVALID', 'descriptor changed the trusted durability continuity')
        }
        const sameIdentity = sameBytes(descriptor.relayPublicKey, previousState.relayPublicKey)
        if (sameIdentity) {
          if (BigInt(descriptor.identitySequence) !== previousState.identitySequence ||
              descriptor.previousRelayKey != null || descriptor.identityTransition != null) {
            fail('DESCRIPTOR_CHAIN_INVALID', 'same-key descriptor changed identity continuity')
          }
        } else {
          if (descriptor.identityTransition == null ||
              !sameBytes(descriptor.previousRelayKey, previousState.relayPublicKey) ||
              BigInt(descriptor.identitySequence) !== previousState.identitySequence + 1n) {
            fail('DESCRIPTOR_CHAIN_INVALID', 'relay-key rotation lacks exact dual-signed continuity')
          }
          verifyIdentityTransition(descriptor.identityTransition, previousState, descriptor)
        }
        if (previousState.history.length >= MAX_DESCRIPTOR_HISTORY) {
          fail('DESCRIPTOR_HISTORY_LIMIT', 'descriptor history exceeds 4096 linked objects')
        }
        next = {
          ...previousState,
          currentBytes: verifiedState.bytes,
          currentHash: verifiedState.hash,
          sequence,
          identitySequence: BigInt(descriptor.identitySequence),
          relayPublicKey: descriptor.relayPublicKey,
          history: [...previousState.history, verifiedState.bytes]
        }
      }
      if (await this.backend.compareAndSwap(key, record.version, next)) {
        return new TrustedDescriptor(TRUSTED_DESCRIPTOR, verified, next.rootRelayPublicKey, record.version + 1)
      }
    }
    fail('TRUST_STORE_BUSY', 'descriptor trust CAS did not converge')
  }
}

export class VerifiedHealth {
  constructor (token, fields) {
    if (token !== VERIFIED_HEALTH) throw new TypeError('VerifiedHealth is not directly constructible')
    healthInternals.set(this, fields)
    Object.freeze(this)
  }

  get readyRoleBits () { return healthInternals.get(this).value.readyRoleBits }
  get readyOperationBits () { return healthInternals.get(this).value.readyOperationBits }
}

export function verifiedHealthValidity (health) {
  const fields = healthInternals.get(health)
  if (!fields) fail('BAD_CLIENT_INPUT', 'a VerifiedHealth is required')
  return Object.freeze({
    verifiedAtMonotonicMillis: fields.verifiedAtMonotonicMillis,
    expiresAtMonotonicMillis: fields.verifiedAtMonotonicMillis + MAX_HEALTH_QUALIFICATION_AGE_MILLIS
  })
}

export function verifyHealthResultBytes (input, trusted, challenge, options = {}) {
  if (!(trusted instanceof TrustedDescriptor)) fail('BAD_CLIENT_INPUT', 'a TrustedDescriptor is required')
  const trust = trustedFields(trusted)
  const descriptorState = verifiedFields(trust.verified)
  const decoded = decodeCanonicalCopy(blindHealthResultV1, input, 'health result')
  const value = decoded.value
  const descriptor = descriptorState.value
  verifyResultSignedValue(blindHealthResultV1, value,
    RESULT_SIGNATURE_DOMAIN_ID.HEALTH_RESULT, descriptor.relayPublicKey, 'health result')
  for (const field of ['relayPublicKey', 'storeId', 'durabilityContinuityHash', 'durabilityProfileHash']) {
    if (!sameBytes(value[field], descriptor[field])) fail('RELAY_PROTOCOL_VIOLATION', `health ${field} does not match descriptor`)
  }
  if (BigInt(value.descriptorSequence) !== BigInt(descriptor.descriptorSequence) ||
      !sameBytes(value.descriptorHash, descriptorState.hash) || !sameBytes(value.clientNonce, challenge.clientNonce) ||
      value.endpointId !== challenge.endpointId ||
      value.transportSupportBit !== challenge.transportSupportBit) {
    fail('RELAY_PROTOCOL_VIOLATION', 'health result does not correlate to the descriptor challenge')
  }
  if ((value.readyRoleBits & ~challenge.requestedRoleBits) !== 0 ||
      (value.readyOperationBits & ~challenge.requestedOperationBits) !== 0 ||
      (value.readyOperationBits & ~descriptor.enabledOperationBits) !== 0) {
    fail('RELAY_PROTOCOL_VIOLATION', 'health readiness exceeds the challenged or enabled subset')
  }
  const nowEpoch = epoch(options.nowEpoch, 'nowEpoch')
  if (value.challengeEpoch > nowEpoch || nowEpoch - value.challengeEpoch > 1) {
    fail('RELAY_PROTOCOL_VIOLATION', 'health result is stale or future-dated')
  }
  return new VerifiedHealth(VERIFIED_HEALTH, {
    value,
    descriptorHash: b4a.from(descriptorState.hash),
    descriptorSequence: descriptor.descriptorSequence,
    relayPublicKey: b4a.from(descriptor.relayPublicKey),
    storeId: b4a.from(descriptor.storeId),
    verifiedAtMonotonicMillis: monotonicMillis(
      options.observedMonotonicMillis, 'observedMonotonicMillis')
  })
}

export class VerifiedAdmissionParameters {
  constructor (token, value, hash) {
    if (token !== VERIFIED_ADMISSION) throw new TypeError('VerifiedAdmissionParameters is not directly constructible')
    admissionInternals.set(this, { value, hash })
    Object.freeze(this)
  }

  get parameterHash () { return b4a.from(admissionInternals.get(this).hash) }
}

export function verifiedAdmissionParametersValidity (verified) {
  const fields = admissionInternals.get(verified)
  if (!fields) fail('BAD_CLIENT_INPUT', 'VerifiedAdmissionParameters are required')
  return Object.freeze({
    validFromEpoch: fields.value.validFromEpoch,
    expiresEpoch: fields.value.expiresEpoch
  })
}

export function verifyAdmissionParametersBytes (input, trusted, requestedProfile, options = {}) {
  if (!(trusted instanceof TrustedDescriptor)) fail('BAD_CLIENT_INPUT', 'a TrustedDescriptor is required')
  const trust = trustedFields(trusted)
  const decoded = decodeCanonicalCopy(admissionParametersV1, input, 'admission parameters')
  const value = decoded.value
  const descriptor = verifiedFields(trust.verified).value
  const hash = admissionParametersHash(decoded.bytes)
  verifyResultSignedValue(admissionParametersV1, value,
    RESULT_SIGNATURE_DOMAIN_ID.ADMISSION_PARAMETERS, descriptor.relayPublicKey, 'admission parameters')
  if (!requestedProfile || !Number.isSafeInteger(requestedProfile.profileId)) {
    fail('BAD_CLIENT_INPUT', 'requested admission profile is invalid')
  }
  const advertised = descriptor.admissionProfiles.find(profile => profile.profileId === requestedProfile.profileId)
  const sameOptionalUrl = advertised != null &&
    ((advertised.parameterUrl == null && requestedProfile.parameterUrl == null) ||
      (advertised.parameterUrl != null && requestedProfile.parameterUrl != null &&
       sameBytes(advertised.parameterUrl, requestedProfile.parameterUrl)))
  if (!advertised || value.profileId !== advertised.profileId || value.schemeId !== advertised.schemeId ||
      value.conformanceClass !== advertised.conformanceClass || value.roleBits !== advertised.roleBits ||
      requestedProfile.schemeId !== advertised.schemeId ||
      requestedProfile.conformanceClass !== advertised.conformanceClass ||
      requestedProfile.roleBits !== advertised.roleBits || !sameOptionalUrl ||
      !sameBytes(requestedProfile.parameterHash, advertised.parameterHash) ||
      !sameBytes(value.relayPublicKey, descriptor.relayPublicKey) || !sameBytes(hash, advertised.parameterHash)) {
    fail('RELAY_PROTOCOL_VIOLATION', 'admission parameters do not match the signed descriptor profile')
  }
  const nowEpoch = epoch(options.nowEpoch, 'nowEpoch')
  if (nowEpoch < value.validFromEpoch || nowEpoch >= value.expiresEpoch) {
    fail('RELAY_PROTOCOL_VIOLATION', 'admission parameters are outside their signed epoch window')
  }
  return new VerifiedAdmissionParameters(VERIFIED_ADMISSION, value, hash)
}

function qualificationAuthority (options) {
  const trusted = options && options.trustedDescriptor
  if (!(trusted instanceof TrustedDescriptor)) fail('BAD_CLIENT_INPUT', 'a TrustedDescriptor is required')
  const trust = trustedFields(trusted)
  const descriptorState = verifiedFields(trust.verified)
  const descriptor = descriptorState.value
  const nowEpoch = epoch(options.nowEpoch, 'nowEpoch')
  if (nowEpoch < descriptor.issuedEpoch || nowEpoch >= descriptor.expiresEpoch ||
      descriptor.storeLifecycleState === STORE_LIFECYCLE_STATE.RETIRED) {
    fail('RELAY_NOT_QUALIFIED', 'descriptor is expired or retired')
  }
  const profile = operationProfile(options.familyId, options.operationId)
  const bit = operationBit(options.familyId, options.operationId)
  if (!profile || !isAdvertisedOperation(options.familyId, options.operationId) ||
      bit === 0 || (descriptor.enabledOperationBits & bit) === 0) {
    fail('RELAY_NOT_QUALIFIED', 'operation is not enabled by the trusted descriptor')
  }
  const advertisedProtocol = descriptor.protocols.find(value => value.protocolId === options.familyId)
  const supportedProtocol = descriptorState.supportedProtocols.get(options.familyId)
  if (!advertisedProtocol || !supportedProtocol || advertisedProtocol.major !== supportedProtocol.major ||
      advertisedProtocol.minor < supportedProtocol.minimumMinor ||
      !sameBytes(advertisedProtocol.profileHash, supportedProtocol.profileHash)) {
    fail('RELAY_NOT_QUALIFIED', 'operation family has no pinned compatible protocol profile')
  }
  const endpoint = descriptor.endpoints.find(value => value.endpointId === options.endpointId)
  if (!endpoint) fail('RELAY_NOT_QUALIFIED', 'endpoint is absent from the trusted descriptor')
  const supportedTransport = descriptorState.supportedTransports.get(endpoint.transportId)
  const transportSupportBit = knownTransportSupportBit(options.transportSupportBit)
  if (!supportedTransport || !sameBytes(endpoint.transportProfileHash, supportedTransport.transportProfileHash) ||
      supportedTransport.transportSupportBit !== transportSupportBit ||
      (profile.transportSupportBits & transportSupportBit) === 0) {
    fail('RELAY_NOT_QUALIFIED', 'endpoint transport profile is not pinned for this operation')
  }
  if ((transportSupportBit & (TRANSPORT_SUPPORT.DIRECT_HTTP | TRANSPORT_SUPPORT.OHTTP |
      TRANSPORT_SUPPORT.TOR_HTTP)) !== 0) {
    const prefix = DISPATCH_LIMITS.PREFIX_BYTES + DISPATCH_LIMITS.HEADER_BYTES
    const requiredOuterClass = smallestOuterClass(Math.max(
      prefix + profile.maxRequestBodyBytes,
      prefix + profile.maxResultBodyBytes
    ))
    if ((endpoint.envelopeClassBits & (1 << requiredOuterClass)) === 0) {
      fail('RELAY_NOT_QUALIFIED', 'endpoint cannot carry the operation maximum in a signed outer class')
    }
  }
  const requiredRoleBits = options.requiredRoleBits
  const privacyProfileBit = oneHot(options.privacyProfileBit, 'privacyProfileBit')
  if (!Number.isSafeInteger(requiredRoleBits) || requiredRoleBits <= 0 ||
      (endpoint.roleBits & requiredRoleBits) !== requiredRoleBits ||
      (endpoint.privacyProfileBits & privacyProfileBit) === 0) {
    fail('RELAY_NOT_QUALIFIED', 'endpoint lacks the requested role or privacy profile')
  }
  return {
    trust,
    descriptorState,
    descriptor,
    endpoint,
    bit,
    requiredRoleBits,
    transportSupportBit,
    privacyProfileBit
  }
}

function issueQualifiedEndpoint (options, authority) {
  const { trust, descriptorState, descriptor, endpoint, transportSupportBit, privacyProfileBit } = authority
  return issueVerifiedEndpoint({
    endpoint,
    descriptorHash: descriptorState.hash,
    descriptorSequence: descriptor.descriptorSequence,
    relayPublicKey: descriptor.relayPublicKey,
    storeId: descriptor.storeId,
    continuityRoot: trust.rootRelayPublicKey,
    familyId: options.familyId,
    operationId: options.operationId,
    transportSupportBit,
    privacyProfileBit,
    durabilityProfileId: descriptor.durability.profileId,
    durabilityContinuityHash: descriptor.durabilityContinuityHash,
    durabilityProfileHash: descriptor.durabilityProfileHash,
    restoreEvidenceHeadSequence: descriptor.durability.restoreEvidenceCheckpointSequence,
    restoreEvidenceHeadHash: descriptor.durability.restoreEvidenceCheckpointHash,
    externalWitnessPublicKey: descriptor.durability.externalWitnessPublicKey,
    externalJournalId: descriptor.durability.externalJournalId
  })
}

// Health qualification cannot itself require a health-qualified endpoint. This
// issuer breaks that cycle without exposing a raw ordinary-operation bypass: it
// accepts only the three DESCRIBE control operations and binds the resulting
// handle to an already trusted, current signed descriptor.
export function qualifyDescribeControlEndpoint (options) {
  if (!options || options.familyId !== FAMILY.DESCRIBE ||
      ![OPERATION.DESCRIBE.GET, OPERATION.DESCRIBE.CHALLENGE,
        OPERATION.DESCRIBE.ADMISSION_PARAMETERS].includes(options.operationId)) {
    fail('BAD_CLIENT_INPUT', 'control qualification accepts only a DESCRIBE operation')
  }
  return issueQualifiedEndpoint(options, qualificationAuthority(options))
}

export function qualifyRelay (options) {
  const authority = qualificationAuthority(options)
  const { descriptorState, descriptor, bit, requiredRoleBits } = authority
  const nowEpoch = epoch(options.nowEpoch, 'nowEpoch')
  const health = options.health
  const healthState = healthInternals.get(health)
  const healthValue = healthState && healthState.value
  const nowMonotonicMillis = monotonicMillis(options.nowMonotonicMillis, 'nowMonotonicMillis')
  const healthAgeMillis = healthState == null
    ? Number.POSITIVE_INFINITY
    : nowMonotonicMillis - healthState.verifiedAtMonotonicMillis
  if (!(health instanceof VerifiedHealth) ||
      healthAgeMillis < 0 || healthAgeMillis > MAX_HEALTH_QUALIFICATION_AGE_MILLIS ||
      !sameBytes(healthState.descriptorHash, descriptorState.hash) ||
      BigInt(healthState.descriptorSequence) !== BigInt(descriptor.descriptorSequence) ||
      !sameBytes(healthState.relayPublicKey, descriptor.relayPublicKey) ||
      !sameBytes(healthState.storeId, descriptor.storeId) ||
      healthValue.endpointId !== authority.endpoint.endpointId ||
      healthValue.transportSupportBit !== authority.transportSupportBit ||
      (healthValue.readyRoleBits & requiredRoleBits) !== requiredRoleBits ||
      (healthValue.readyOperationBits & bit) !== bit ||
      healthValue.clockState !== HEALTH_CLOCK_STATE.READY ||
      healthValue.integrityState !== HEALTH_INTEGRITY_STATE.VERIFIED ||
      ((bit & CLOCK_UNSAFE_OPERATION_BITS) !== 0 && healthValue.effectiveEpochFloor > nowEpoch)) {
    fail('RELAY_NOT_QUALIFIED', 'fresh health does not prove requested readiness')
  }
  return issueQualifiedEndpoint(options, authority)
}

function nonce (runtime, value, label) {
  return value == null ? randomBytes(runtime, 32, label) : b4a.from(asBytes(value, label, 32))
}

export function createDescribeGetRequest (options = {}) {
  const request = {
    version: 1,
    descriptorHash: options.descriptorHash == null ? null : b4a.from(asBytes(options.descriptorHash, 'descriptorHash', 32)),
    clientNonce: nonce(options.runtime, options.clientNonce, 'describe client nonce')
  }
  return {
    request,
    requestBytes: encodeCanonical(blindDescribeGetV1, request),
    wire: Object.freeze({ familyId: FAMILY.DESCRIBE, operationId: OPERATION.DESCRIBE.GET, expectedResultBodyBytes: 16384 })
  }
}

export function createHealthChallenge (options) {
  const trusted = options && options.trustedDescriptor
  if (!(trusted instanceof TrustedDescriptor)) fail('BAD_CLIENT_INPUT', 'a TrustedDescriptor is required')
  const descriptorState = verifiedFields(trustedFields(trusted).verified)
  const descriptor = descriptorState.value
  const endpointId = options.endpointId
  if (!Number.isSafeInteger(endpointId) || endpointId < 1 || endpointId > 0xff) {
    fail('BAD_CLIENT_INPUT', 'endpointId is outside 1..255')
  }
  const transportSupportBit = knownTransportSupportBit(options.transportSupportBit)
  const endpoint = descriptor.endpoints.find(value => value.endpointId === endpointId)
  const supportedTransport = endpoint == null ? null : descriptorState.supportedTransports.get(endpoint.transportId)
  if (!endpoint || !supportedTransport ||
      !sameBytes(endpoint.transportProfileHash, supportedTransport.transportProfileHash) ||
      supportedTransport.transportSupportBit !== transportSupportBit) {
    fail('RELAY_NOT_QUALIFIED', 'health challenge endpoint transport is not pinned by the trusted descriptor')
  }
  if (!Number.isSafeInteger(options.requestedRoleBits) || options.requestedRoleBits <= 0 ||
      (options.requestedRoleBits & ~endpoint.roleBits) !== 0 ||
      !Number.isSafeInteger(options.requestedOperationBits) || options.requestedOperationBits <= 0 ||
      (options.requestedOperationBits & ~ADVERTISED_OPERATION_BITS) !== 0 ||
      (options.requestedOperationBits & ~descriptor.enabledOperationBits) !== 0) {
    fail('RELAY_NOT_QUALIFIED', 'health challenge exceeds the endpoint role or descriptor operation set')
  }
  for (let ordinal = 0; ordinal < OPERATION_PROFILE_ROWS.length; ordinal++) {
    const operationBit = 1 << ordinal
    if ((options.requestedOperationBits & operationBit) !== 0 &&
        (OPERATION_PROFILE_ROWS[ordinal].transportSupportBits & transportSupportBit) === 0) {
      fail('RELAY_NOT_QUALIFIED', 'health challenge operation is unsupported by the bound endpoint transport')
    }
  }
  const request = {
    version: 1,
    descriptorSequence: descriptor.descriptorSequence,
    descriptorHash: descriptorState.hash,
    endpointId,
    transportSupportBit,
    requestedRoleBits: options.requestedRoleBits,
    requestedOperationBits: options.requestedOperationBits,
    clientNonce: nonce(options.runtime, options.clientNonce, 'health client nonce')
  }
  return {
    request,
    requestBytes: encodeCanonical(blindHealthChallengeV1, request),
    wire: Object.freeze({ familyId: FAMILY.DESCRIBE, operationId: OPERATION.DESCRIBE.CHALLENGE, expectedResultBodyBytes: 16384 })
  }
}

export function createAdmissionParametersRequest (options) {
  const request = {
    version: 1,
    profileId: options.profileId,
    schemeId: options.schemeId,
    clientNonce: nonce(options.runtime, options.clientNonce, 'admission client nonce')
  }
  return {
    request,
    requestBytes: encodeCanonical(blindAdmissionParametersRequestV1, request),
    wire: Object.freeze({
      familyId: FAMILY.DESCRIBE,
      operationId: OPERATION.DESCRIBE.ADMISSION_PARAMETERS,
      expectedResultBodyBytes: 16384
    })
  }
}

export const HEALTH_QUALIFICATION_LIMITS = Object.freeze({
  maximumAgeMillis: MAX_HEALTH_QUALIFICATION_AGE_MILLIS
})