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
8 changes: 8 additions & 0 deletions proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ message MessageReveal {
message MessageRevealZk {
repeated ZKProof proofs = 1;
repeated TOPRFProof toprfs = 2;
/** Markers for server-side OPRF computation (oprf-raw mode) */
repeated OPRFRawMarker oprfRawMarkers = 3;
}

/** Marker for server-side OPRF computation (oprf-raw mode). */
message OPRFRawMarker {
/** Location of the data to OPRF in the revealed plaintext */
DataSlice dataLocation = 1;
}

message ZKProof {
Expand Down
3 changes: 2 additions & 1 deletion src/client/create-claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,12 +515,13 @@ async function _createClaimOnAttestor<N extends ProviderName>(
revealedPackets.push(...packets.filter(p => p.sender === 'server'))
} else {
for(const {
block, redactedPlaintext, overshotToprfFromPrevBlock, toprfs
block, redactedPlaintext, overshotToprfFromPrevBlock, toprfs, oprfRawMarkers
} of serverPacketsToReveal) {
setRevealOfMessage(block.message, {
type: 'zk',
redactedPlaintext,
toprfs,
oprfRawMarkers,
overshotToprfFromPrevBlock
})
revealedPackets.push(
Expand Down
88 changes: 87 additions & 1 deletion src/proto/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 48 additions & 3 deletions src/server/utils/assert-valid-claim-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ import {
} from '#src/proto/api.ts'
import { providers } from '#src/providers/index.ts'
import { niceParseJsonObject } from '#src/server/utils/generics.ts'
import { computeOPRFRaw } from '#src/server/utils/oprf-raw.ts'
import { processHandshake } from '#src/server/utils/process-handshake.ts'
import { assertValidateProviderParams } from '#src/server/utils/validation.ts'
import type {
IDecryptedTranscript, IDecryptedTranscriptMessage,
Logger,
OPRFRawReplacement,
ProviderCtx,
ProviderName,
TCPSocketProperties,
Transcript,
} from '#src/types/index.ts'
import {
AttestorError,
binaryHashToStr,
canonicalStringify, decryptDirect,
extractApplicationDataFromTranscript,
hashProviderParams, SIGNATURES,
Expand Down Expand Up @@ -102,7 +105,8 @@ export async function assertValidClaimRequest(
// 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 },
receipt.oprfRawReplacements
)
if(newData !== data) {
logger.info({ newData }, 'updated claim info')
Expand All @@ -119,7 +123,8 @@ export async function assertValidProviderTranscript<T extends ProviderClaimInfo>
applData: Transcript<Uint8Array>,
info: T,
logger: Logger,
providerCtx: ProviderCtx
providerCtx: ProviderCtx,
oprfRawReplacements?: OPRFRawReplacement[]
) {
const providerName = info.provider as ProviderName
const provider = providers[providerName]
Expand All @@ -130,9 +135,25 @@ export async function assertValidProviderTranscript<T extends ProviderClaimInfo>
)
}

const params = niceParseJsonObject(info.parameters, 'params')
let params = niceParseJsonObject(info.parameters, 'params')
const ctx = niceParseJsonObject(info.context, 'context')

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

params = JSON.parse(strParams)
// Update info.parameters with replaced values
info.parameters = strParams
logger.debug(
{ replacements: oprfRawReplacements.length },
'applied oprf-raw parameter replacements'
)
}
Comment thread
Scratch-net marked this conversation as resolved.

assertValidateProviderParams(providerName, params)

const rslt = await provider.assertValidProviderReceipt({
Expand Down Expand Up @@ -223,6 +244,7 @@ export async function decryptTranscript(

const overshotMap: { [pkt: number]: { data: Uint8Array } } = {}
const decryptedTranscript: IDecryptedTranscriptMessage[] = []
const oprfRawReplacements: { originalText: string, nullifierText: string }[] = []

for(const [i, {
sender,
Expand Down Expand Up @@ -250,6 +272,7 @@ export async function decryptTranscript(
transcript: decryptedTranscript,
hostname: hostname,
tlsVersion: tlsVersion,
oprfRawReplacements: oprfRawReplacements.length ? oprfRawReplacements : undefined
}

async function decryptMessage(
Expand Down Expand Up @@ -312,6 +335,28 @@ export async function decryptTranscript(
}
)
plaintext = result.redactedPlaintext

// Process oprf-raw markers: compute OPRF server-side and replace with nullifier
if(result.oprfRawMarkers?.length) {
const oprfResults = await computeOPRFRaw(
plaintext,
result.oprfRawMarkers,
logger
)
// Replace plaintext at marker positions with nullifier string
for(const { dataLocation, nullifier } of oprfResults) {
// Capture original text for parameter replacement
const originalText = new TextDecoder().decode(
plaintext.slice(dataLocation.fromIndex, dataLocation.fromIndex + dataLocation.length)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const nullifierStr = binaryHashToStr(nullifier, dataLocation.length)
oprfRawReplacements.push({ originalText, nullifierText: nullifierStr })

const nullifierBytes = new TextEncoder().encode(nullifierStr)
plaintext.set(nullifierBytes, dataLocation.fromIndex)
}
}

redacted = false
plaintextLength = plaintext.length
} else {
Expand Down
104 changes: 104 additions & 0 deletions src/server/utils/oprf-raw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ethers } from 'ethers'

import { TOPRF_DOMAIN_SEPARATOR } from '#src/config/index.ts'
import type { MessageReveal_OPRFRawMarker as OPRFRawMarker } from '#src/proto/api.ts'
import type { Logger } from '#src/types/index.ts'
import { getEnvVariable } from '#src/utils/env.ts'
import { makeDefaultOPRFOperator } from '#src/utils/zk.ts'

export type OPRFRawResult = {
/** Location of the data that was OPRF'd */
dataLocation: {
fromIndex: number
length: number
}
/** The OPRF nullifier (hash output) */
nullifier: Uint8Array
}

/**
* Compute OPRF for plaintext data marked with oprf-raw.
* This runs server-side since the attestor has access to the revealed plaintext.
*
* @param plaintext - The revealed plaintext from the TLS transcript
* @param markers - Positions in the plaintext to compute OPRF for
* @param logger - Logger instance
* @returns Array of OPRF results with nullifiers
*/
export async function computeOPRFRaw(
plaintext: Uint8Array,
markers: OPRFRawMarker[],
logger: Logger
): Promise<OPRFRawResult[]> {
if(!markers.length) {
return []
}

const PRIVATE_KEY_STR = getEnvVariable('TOPRF_SHARE_PRIVATE_KEY')
const PUBLIC_KEY_STR = getEnvVariable('TOPRF_SHARE_PUBLIC_KEY')
if(!PRIVATE_KEY_STR || !PUBLIC_KEY_STR) {
throw new Error('TOPRF keys not configured. Cannot compute oprf-raw.')
}

const privateKey = ethers.utils.arrayify(PRIVATE_KEY_STR)
const publicKey = ethers.utils.arrayify(PUBLIC_KEY_STR)

// Use gnark engine for server-side OPRF (same as TOPRF handler)
const operator = makeDefaultOPRFOperator('chacha20', 'gnark', logger)

const results: OPRFRawResult[] = []

for(const marker of markers) {
const { dataLocation } = marker
if(!dataLocation) {
logger.warn('oprf-raw marker missing dataLocation, skipping')
continue
}

const { fromIndex, length } = dataLocation
const endIndex = fromIndex + length

if(endIndex > plaintext.length) {
throw new Error(
`oprf-raw marker out of bounds: fromIndex=${fromIndex}, length=${length}, plaintextLength=${plaintext.length}`
)
}

// Extract the data to OPRF
const data = plaintext.slice(fromIndex, endIndex)

// Generate OPRF request (server-side, we do the full flow)
const request = await operator.generateOPRFRequestData(
data,
TOPRF_DOMAIN_SEPARATOR,
logger
)

// Evaluate OPRF with server's private key
const response = await operator.evaluateOPRF(
privateKey,
request.maskedData,
logger
)

// Finalize to get the nullifier
const nullifier = await operator.finaliseOPRF(
publicKey,
request,
[{ ...response, publicKeyShare: publicKey }],
logger
)

results.push({
dataLocation: { fromIndex, length },
nullifier
})

logger.debug(
{ fromIndex, length, nullifierHex: Buffer.from(nullifier).toString('hex').slice(0, 16) + '...' },
'computed oprf-raw nullifier'
)
}

return results
}
Loading
Loading