Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/client/create-claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
makeHttpResponseParser,
preparePacketsForReveal,
redactSlices,
replaceByteSequence,
strToUint8Array,
uint8ArrayToStr,
unixTimestampSeconds
} from '#src/utils/index.ts'
Expand Down Expand Up @@ -534,6 +536,18 @@ async function _createClaimOnAttestor<N extends ProviderName>(
toprf.dataLocation!.length
)
strParams = strParams.replaceAll(ogText, hashedText)

// also replace in client request packets so the
// request URL matches the updated params
const ogBytes = strToUint8Array(ogText)
const hashBytes = strToUint8Array(hashedText)
for(const pkt of revealedPackets) {
if(pkt.sender === 'client') {
replaceByteSequence(
pkt.message, ogBytes, hashBytes
)
}
}
}

params = JSON.parse(strParams)
Expand Down
65 changes: 62 additions & 3 deletions src/providers/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import type {
ProviderCtx,
ProviderParams,
ProviderSecretParams,
RedactedOrHashedArraySlice
RedactedOrHashedArraySlice,
Transcript,
} from '#src/types/index.ts'
import {
findIndexInUint8Array,
getHttpRequestDataFromTranscript,
logger,
REDACTION_CHAR_CODE,
replaceByteSequence,
strToUint8Array,
uint8ArrayToStr,
} from '#src/utils/index.ts'
Expand Down Expand Up @@ -281,8 +283,13 @@ const HTTP_PROVIDER: Provider<'http'> = {
//brackets in URL path turn into %7B and %7D, so replace them back
const expectedPath = pathname.replaceAll('%7B', '{').replaceAll('%7D', '}') + (searchParams?.length ? '?' + searchParams : '')
if(!matchRedactedStrings(strToUint8Array(expectedPath), strToUint8Array(req.url))) {
logger.error('params URL: %s', params.url)
throw new Error(`Expected path: ${expectedPath}, found: ${req.url}`)
// when TOPRF hashes replaced values in params that also
// appear in the request URL, derive the originals and
// reconcile the client request packets
if(!reconcileToprfInUrl(expectedPath, req.url, ctx, receipt)) {
logger.error('params URL: %s', params.url)
throw new Error(`Expected path: ${expectedPath}, found: ${req.url}`)
}
}
Comment thread
Scratch-net marked this conversation as resolved.

const expectedHostStr = getHostHeaderString(url)
Expand Down Expand Up @@ -444,6 +451,58 @@ const HTTP_PROVIDER: Provider<'http'> = {
},
}

/**
* When TOPRF hashes appear in the expected URL but the actual
* request URL has the originals (sent before OPRF), derive
* the originals by positional comparison and replace in the
* client request packets so subsequent validation succeeds.
*
* Returns true if reconciliation succeeded.
*/
function reconcileToprfInUrl(
expectedPath: string,
actualUrl: string,
ctx: ProviderCtx,
receipt: Transcript<Uint8Array>,
): boolean {
const nullifiers = ctx.toprfNullifiers
if(!nullifiers?.length || expectedPath.length !== actualUrl.length) {
return false
}

let reconciled = false
for(const hash of nullifiers) {
let pos = 0
while((pos = expectedPath.indexOf(hash, pos)) >= 0) {
const original = actualUrl.substring(pos, pos + hash.length)
if(original !== hash) {
const ogBytes = strToUint8Array(original)
const hashBytes = strToUint8Array(hash)
for(const pkt of receipt) {
if(pkt.sender === 'client') {
replaceByteSequence(pkt.message, ogBytes, hashBytes)
}
}

reconciled = true
}

pos += hash.length
}
}

if(!reconciled) {
return false
}

// re-parse and re-check after reconciliation
const req = getHttpRequestDataFromTranscript(receipt)
return matchRedactedStrings(
strToUint8Array(expectedPath),
strToUint8Array(req.url)
)
}
Comment thread
Scratch-net marked this conversation as resolved.

// revealing CRLF is a breaking change -- and should only be done
// if the client's version supports it
function shouldRevealCrlf({ version }: ProviderCtx) {
Expand Down
32 changes: 29 additions & 3 deletions src/server/utils/assert-valid-claim-request.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { areUint8ArraysEqual, concatenateUint8Arrays } from '@reclaimprotocol/tls'

Check failure on line 1 in src/server/utils/assert-valid-claim-request.ts

View workflow job for this annotation

GitHub Actions / test

Run autofix to sort these imports!
import type { ZKEngine } from '@reclaimprotocol/zk-symmetric-crypto'

import type {
Expand Down Expand Up @@ -30,6 +30,8 @@
decryptDirect,
extractApplicationDataFromTranscript,
hashProviderParams,
replaceByteSequence,
strToUint8Array,
SIGNATURES,
verifyZkPacket
} from '#src/utils/index.ts'
Expand Down Expand Up @@ -104,7 +106,8 @@
// get all application data messages
const applData = extractApplicationDataFromTranscript(receipt)
const newData = await assertValidProviderTranscript(
applData, data, logger, { version: metadata.clientVersion },
applData, data, logger,
{ version: metadata.clientVersion, toprfNullifiers: receipt.toprfNullifiers },
receipt.oprfRawReplacements
)
if(newData !== data) {
Expand Down Expand Up @@ -137,11 +140,21 @@
let params = niceParseJsonObject(info.parameters, 'params')
const ctx = niceParseJsonObject(info.context, 'context')

// Apply oprf-raw replacements to parameters (server-side OPRF)
// Apply oprf-raw replacements to parameters and client request packets
if(oprfRawReplacements?.length) {
let strParams = canonicalStringify(params) ?? '{}'
for(const { originalText, nullifierText } of oprfRawReplacements) {
strParams = strParams.replaceAll(originalText, nullifierText)

// also replace in client request packets so the
// request URL matches the updated params
const ogBytes = strToUint8Array(originalText)
const hashBytes = strToUint8Array(nullifierText)
for(const pkt of applData) {
if(pkt.sender === 'client') {
replaceByteSequence(pkt.message, ogBytes, hashBytes)
}
}
}

params = JSON.parse(strParams)
Expand Down Expand Up @@ -244,6 +257,7 @@
const overshotMap: { [pkt: number]: { data: Uint8Array } } = {}
const decryptedTranscript: IDecryptedTranscriptMessage[] = []
const oprfRawReplacements: { originalText: string, nullifierText: string }[] = []
const toprfNullifiers: string[] = []
// Track pending oprf-raw markers that span multiple packets
// keyed by packet index that will receive the overshot data
const pendingOprfRaw: {
Expand Down Expand Up @@ -289,7 +303,8 @@
transcript: decryptedTranscript,
hostname: hostname,
tlsVersion: tlsVersion,
oprfRawReplacements: oprfRawReplacements.length ? oprfRawReplacements : undefined
oprfRawReplacements: oprfRawReplacements.length ? oprfRawReplacements : undefined,
toprfNullifiers: toprfNullifiers.length ? toprfNullifiers : undefined
}

async function decryptMessage(
Expand Down Expand Up @@ -353,6 +368,17 @@
)
plaintext = result.redactedPlaintext

// Collect TOPRF nullifier strings from verified proofs
if(isServer && zkReveal.toprfs?.length) {
for(const toprf of zkReveal.toprfs) {
if(toprf.payload?.nullifier?.length && toprf.payload.dataLocation) {
toprfNullifiers.push(
binaryHashToStr(toprf.payload.nullifier, toprf.payload.dataLocation.length)
)
}
}
}

// Handle pending oprf-raw data from previous packet (cross-block)
const pendingForThis = pendingOprfRaw[i]
if(pendingForThis && zkReveal?.overshotOprfRawLength) {
Expand Down
2 changes: 2 additions & 0 deletions src/types/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export type IDecryptedTranscript = {
* for server-side parameter replacement
*/
oprfRawReplacements?: OPRFRawReplacement[]
/** TOPRF nullifier strings from ZK-verified OPRF proofs */
toprfNullifiers?: string[]
}

export type OPRFRawReplacement = {
Expand Down
2 changes: 2 additions & 0 deletions src/types/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export type ProviderField<Params, SecretParams, T> = T | ((params: Params, secre

export type ProviderCtx = {
version: AttestorVersion
/** TOPRF nullifier strings for reconciling request URLs */
toprfNullifiers?: string[]
}

type GetResponseRedactionsOpts<P> = {
Expand Down
25 changes: 25 additions & 0 deletions src/utils/generics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,31 @@ export function findIndexInUint8Array(
return -1
}

/**
* In-place replacement of all occurrences of `search` with `replace`
* in the buffer. Both must be the same length.
*/
export function replaceByteSequence(
buf: Uint8Array,
search: Uint8Array,
replace: Uint8Array,
) {
for(let i = 0; i <= buf.length - search.length; i++) {
let match = true
for(let j = 0; j < search.length; j++) {
if(buf[i + j] !== search[j]) {
match = false
break
}
}

if(match) {
buf.set(replace, i)
i += replace.length - 1
}
}
}
Comment thread
Scratch-net marked this conversation as resolved.

/**
* Fetch the ZK algorithm for the specified cipher suite
*/
Expand Down
Loading