Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum AttestorVersion {
ATTESTOR_VERSION_2_0_1 = 4;
ATTESTOR_VERSION_3_0_0 = 5;
ATTESTOR_VERSION_3_1_0 = 6;
ATTESTOR_VERSION_3_2_0 = 7;
}

enum ErrorCode {
Expand Down
23 changes: 20 additions & 3 deletions src/client/create-claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
makeDefaultOPRFOperator,
makeHttpResponseParser,
preparePacketsForReveal,
REDACTION_CHAR_CODE,
redactSlices,
uint8ArrayToStr,
unixTimestampSeconds
Expand Down Expand Up @@ -512,9 +513,25 @@ async function _createClaimOnAttestor<N extends ProviderName>(

revealedPackets.push(...packets.filter(p => p.sender === 'server'))
} else {
for(const {
block, redactedPlaintext, overshotToprfFromPrevBlock, toprfs, oprfRawMarkers
} of serverPacketsToReveal) {
const revealByBlock = new Map(
serverPacketsToReveal.map(r => [r.block, r])
)
// walk every server block in order: revealed blocks are proven via
// zk, fully-redacted blocks get an asterisk placeholder so the local
// receipt stays byte-aligned for chunked dechunking
for(const sb of serverBlocks) {
const reveal = revealByBlock.get(sb)
if(!reveal) {
revealedPackets.push({
sender: 'server',
message: new Uint8Array(sb.plaintext.length).fill(REDACTION_CHAR_CODE)
})
continue
}

const {
block, redactedPlaintext, overshotToprfFromPrevBlock, toprfs, oprfRawMarkers
} = reveal
setRevealOfMessage(block.message, {
type: 'zk',
redactedPlaintext,
Expand Down
2 changes: 1 addition & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const DNS_SERVERS = [
// 10m
export const MAX_CLAIM_TIMESTAMP_DIFF_S = 10 * 60

export const CURRENT_ATTESTOR_VERSION = AttestorVersion.ATTESTOR_VERSION_3_1_0
export const CURRENT_ATTESTOR_VERSION = AttestorVersion.ATTESTOR_VERSION_3_2_0

export const DEFAULT_METADATA: InitRequest = {
signatureType: ServiceSignatureType.SERVICE_SIGNATURE_TYPE_ETH,
Expand Down
9 changes: 8 additions & 1 deletion src/proto/api.ts

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

90 changes: 71 additions & 19 deletions src/providers/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
findIndexInUint8Array,
getHttpRequestDataFromTranscript,
logger,
makeHttpResponseParser,
REDACTION_CHAR,
REDACTION_CHAR_CODE,
strToUint8Array,
Expand Down Expand Up @@ -207,6 +208,8 @@ const HTTP_PROVIDER: Provider<'http'> = {
throw new Error('Failed to find response body')
}

const revealFraming = shouldRevealChunkFraming(ctx.version)

const reveals: RedactedOrHashedArraySlice[] = [
{ fromIndex: 0, toIndex: headerEndIndex }
]
Expand All @@ -230,20 +233,39 @@ const HTTP_PROVIDER: Provider<'http'> = {
reveals.push(res.headerIndices['date'])
}

//reveal transfer-encoding header so the verifier can dechunk the body
if(revealFraming && res.headerIndices['transfer-encoding']) {
reveals.push(res.headerIndices['transfer-encoding'])
}

const body = uint8ArrayToBinaryStr(res.body)

const redactions: RedactedOrHashedArraySlice[] = []
for(const rs of params.responseRedactions || []) {
const processor = processRedactionRequest(
body, rs, bodyStartIdx, res.chunks
body, rs, bodyStartIdx, res.chunks, revealFraming
)
for(const { reveal, redactions: reds } of processor) {
reveals.push(reveal)
redactions.push(...reds)
}
}

reveals.sort((a, b) => a.toIndex - b.toIndex)
//reveal all chunk framing (size lines + terminator) so the verifier
//can dechunk; chunk data stays redacted unless a redaction reveals it
if(revealFraming && res.chunks?.length) {
let prev = res.headerEndIdx + 4
for(const chunk of res.chunks) {
reveals.push({ fromIndex: prev, toIndex: chunk.fromIndex })
prev = chunk.toIndex
}

reveals.push({ fromIndex: prev, toIndex: response.length })
}

//reveals can overlap (e.g. a redaction reveal spanning chunk framing),
//so redact the complement of their union
reveals.sort((a, b) => a.fromIndex - b.fromIndex)

if(reveals.length > 1) {
let currentIndex = 0
Expand All @@ -252,10 +274,12 @@ const HTTP_PROVIDER: Provider<'http'> = {
redactions.push({ fromIndex: currentIndex, toIndex: r.fromIndex })
}

currentIndex = r.toIndex
currentIndex = Math.max(currentIndex, r.toIndex)
}

redactions.push({ fromIndex: currentIndex, toIndex: response.length })
if(currentIndex < response.length) {
redactions.push({ fromIndex: currentIndex, toIndex: response.length })
}
}

for(const r of reveals) {
Expand Down Expand Up @@ -321,11 +345,17 @@ const HTTP_PROVIDER: Provider<'http'> = {
throw new Error(`Connection header must be "close", got "${connectionHeader}"`)
}

const serverBlocks = receipt
const allServerBlocks = receipt
.filter(s => s.sender === 'server')
.map((r) => r.message)
// filter out fully redacted blocks
.filter(b => !b.every(b => b === REDACTION_CHAR_CODE))
// drop leading fully-redacted (handshake) blocks so the response starts
// at the status line, but keep redacted body records so chunked byte
// alignment is preserved for dechunking
const firstRealIdx = allServerBlocks
.findIndex(b => !b.every(x => x === REDACTION_CHAR_CODE))
const serverBlocks = firstRealIdx === -1
? []
: allServerBlocks.slice(firstRealIdx)
const response = concatArrays(...serverBlocks)

let res: string
Expand Down Expand Up @@ -389,12 +419,19 @@ const HTTP_PROVIDER: Provider<'http'> = {
}


//remove asterisks to account for chunks in the middle of revealed strings
if(!secretParams) {
if(shouldRevealChunkFraming(clientVersion)) {
//dechunk the body so matches see contiguous content; redaction
//asterisks are preserved as boundaries between revealed slices
const headersStr = res.slice(0, bodyStart)
res = /transfer-encoding:\s*chunked/i.test(headersStr)
? headersStr + dechunkResponseBody(res.slice(bodyStart))
: res
} else if(!secretParams) {
//legacy clients redact chunk framing; collapse asterisk runs so
//matches can span the redacted framing
res = res.slice(bodyStart).replace(/\*{3,}/g, '')
}


for(const { type, value, invert } of params.responseMatches || []) {
const inv = Boolean(invert) // explicitly cast to boolean

Expand Down Expand Up @@ -510,6 +547,23 @@ function shouldRevealCrlf({ version }: ProviderCtx) {
return version >= AttestorVersion.ATTESTOR_VERSION_2_0_1
}

// revealing chunk framing (instead of redacting it) + dechunking on the
// verifier is a breaking change; older clients still redact framing and rely
// on the asterisk-collapse matching path
function shouldRevealChunkFraming(version: AttestorVersion) {
return version >= AttestorVersion.ATTESTOR_VERSION_3_2_0
}

// dechunk a revealed body by reusing the parser: wrap it in a minimal
// (already-validated) header block. streamEnded() skipped to tolerate trailing bytes
function dechunkResponseBody(body: string) {
Comment thread
Scratch-net marked this conversation as resolved.
const parser = makeHttpResponseParser()
parser.onChunk(
strToUint8Array('HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n' + body)
)
return uint8ArrayToStr(parser.res.body ?? new Uint8Array())
}

function getHostPort(params: ProviderParams<'http'>, secretParams: ProviderSecretParams<'http'>) {
const { host } = new URL(getURL(params, secretParams))
if(!host) {
Expand Down Expand Up @@ -550,6 +604,7 @@ function* processRedactionRequest(
rs: HTTPResponseRedaction,
bodyStartIdx: number,
resChunks: ArraySlice[] | undefined,
revealFraming: boolean,
): Generator<RedactionItem> {
let element = body
let elementIdx = 0
Expand Down Expand Up @@ -665,22 +720,19 @@ function* processRedactionRequest(

function* addRedaction(
hash: RedactedOrHashedArraySlice['hash'] | null = rs.hash,
_resChunks = resChunks
): Generator<RedactionItem> {
if(elementIdx < 0 || !elementLength) {
return
}

const reveal = getReveal(elementIdx, elementLength, hash || undefined)

yield {
reveal,
redactions: getRedactionsForChunkHeaders(
reveal.fromIndex,
reveal.toIndex,
_resChunks
)
}
// new clients leave chunk framing revealed (verifier dechunks); older
// clients redact the framing inside a reveal that spans chunks
const redactions = revealFraming
? []
: getRedactionsForChunkHeaders(reveal.fromIndex, reveal.toIndex, resChunks)
yield { reveal, redactions }
}

function getReveal(
Expand Down
12 changes: 10 additions & 2 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'
import { areUint8ArraysEqual, AUTH_TAG_BYTE_LENGTH, concatenateUint8Arrays, SUPPORTED_CIPHER_SUITE_MAP } from '@reclaimprotocol/tls'
import type { ZKEngine } from '@reclaimprotocol/zk-symmetric-crypto'

import type {
Expand Down Expand Up @@ -457,7 +457,15 @@ export async function decryptTranscript(
plaintextLength = plaintext.length
} else {
plaintext = content
plaintextLength = plaintext.length
// ciphertext = explicit-nonce + plaintext + auth-tag; strip both so the
// reconstructed redacted length matches the true plaintext length.
// TLS1.2 AES-GCM prepends an 8-byte explicit nonce; TLS1.3 and
// ChaCha20-Poly1305 have none
const { cipher } = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite]
const explicitNonceLength = tlsVersion !== 'TLS1_3' && cipher.includes('GCM')
? 8
: 0
plaintextLength = content.length - explicitNonceLength - AUTH_TAG_BYTE_LENGTH
}

decryptedTranscript.push({
Expand Down
Loading
Loading