@@ -29,6 +29,7 @@ import {
2929 findIndexInUint8Array ,
3030 getHttpRequestDataFromTranscript ,
3131 logger ,
32+ makeHttpResponseParser ,
3233 REDACTION_CHAR ,
3334 REDACTION_CHAR_CODE ,
3435 strToUint8Array ,
@@ -207,6 +208,8 @@ const HTTP_PROVIDER: Provider<'http'> = {
207208 throw new Error ( 'Failed to find response body' )
208209 }
209210
211+ const revealFraming = shouldRevealChunkFraming ( ctx . version )
212+
210213 const reveals : RedactedOrHashedArraySlice [ ] = [
211214 { fromIndex : 0 , toIndex : headerEndIndex }
212215 ]
@@ -230,20 +233,39 @@ const HTTP_PROVIDER: Provider<'http'> = {
230233 reveals . push ( res . headerIndices [ 'date' ] )
231234 }
232235
236+ //reveal transfer-encoding header so the verifier can dechunk the body
237+ if ( revealFraming && res . headerIndices [ 'transfer-encoding' ] ) {
238+ reveals . push ( res . headerIndices [ 'transfer-encoding' ] )
239+ }
240+
233241 const body = uint8ArrayToBinaryStr ( res . body )
234242
235243 const redactions : RedactedOrHashedArraySlice [ ] = [ ]
236244 for ( const rs of params . responseRedactions || [ ] ) {
237245 const processor = processRedactionRequest (
238- body , rs , bodyStartIdx , res . chunks
246+ body , rs , bodyStartIdx , res . chunks , revealFraming
239247 )
240248 for ( const { reveal, redactions : reds } of processor ) {
241249 reveals . push ( reveal )
242250 redactions . push ( ...reds )
243251 }
244252 }
245253
246- reveals . sort ( ( a , b ) => a . toIndex - b . toIndex )
254+ //reveal all chunk framing (size lines + terminator) so the verifier
255+ //can dechunk; chunk data stays redacted unless a redaction reveals it
256+ if ( revealFraming && res . chunks ?. length ) {
257+ let prev = res . headerEndIdx + 4
258+ for ( const chunk of res . chunks ) {
259+ reveals . push ( { fromIndex : prev , toIndex : chunk . fromIndex } )
260+ prev = chunk . toIndex
261+ }
262+
263+ reveals . push ( { fromIndex : prev , toIndex : response . length } )
264+ }
265+
266+ //reveals can overlap (e.g. a redaction reveal spanning chunk framing),
267+ //so redact the complement of their union
268+ reveals . sort ( ( a , b ) => a . fromIndex - b . fromIndex )
247269
248270 if ( reveals . length > 1 ) {
249271 let currentIndex = 0
@@ -252,10 +274,12 @@ const HTTP_PROVIDER: Provider<'http'> = {
252274 redactions . push ( { fromIndex : currentIndex , toIndex : r . fromIndex } )
253275 }
254276
255- currentIndex = r . toIndex
277+ currentIndex = Math . max ( currentIndex , r . toIndex )
256278 }
257279
258- redactions . push ( { fromIndex : currentIndex , toIndex : response . length } )
280+ if ( currentIndex < response . length ) {
281+ redactions . push ( { fromIndex : currentIndex , toIndex : response . length } )
282+ }
259283 }
260284
261285 for ( const r of reveals ) {
@@ -321,11 +345,17 @@ const HTTP_PROVIDER: Provider<'http'> = {
321345 throw new Error ( `Connection header must be "close", got "${ connectionHeader } "` )
322346 }
323347
324- const serverBlocks = receipt
348+ const allServerBlocks = receipt
325349 . filter ( s => s . sender === 'server' )
326350 . map ( ( r ) => r . message )
327- // filter out fully redacted blocks
328- . filter ( b => ! b . every ( b => b === REDACTION_CHAR_CODE ) )
351+ // drop leading fully-redacted (handshake) blocks so the response starts
352+ // at the status line, but keep redacted body records so chunked byte
353+ // alignment is preserved for dechunking
354+ const firstRealIdx = allServerBlocks
355+ . findIndex ( b => ! b . every ( x => x === REDACTION_CHAR_CODE ) )
356+ const serverBlocks = firstRealIdx === - 1
357+ ? [ ]
358+ : allServerBlocks . slice ( firstRealIdx )
329359 const response = concatArrays ( ...serverBlocks )
330360
331361 let res : string
@@ -389,12 +419,19 @@ const HTTP_PROVIDER: Provider<'http'> = {
389419 }
390420
391421
392- //remove asterisks to account for chunks in the middle of revealed strings
393- if ( ! secretParams ) {
422+ if ( shouldRevealChunkFraming ( clientVersion ) ) {
423+ //dechunk the body so matches see contiguous content; redaction
424+ //asterisks are preserved as boundaries between revealed slices
425+ const headersStr = res . slice ( 0 , bodyStart )
426+ res = / t r a n s f e r - e n c o d i n g : \s * c h u n k e d / i. test ( headersStr )
427+ ? headersStr + dechunkResponseBody ( res . slice ( bodyStart ) )
428+ : res
429+ } else if ( ! secretParams ) {
430+ //legacy clients redact chunk framing; collapse asterisk runs so
431+ //matches can span the redacted framing
394432 res = res . slice ( bodyStart ) . replace ( / \* { 3 , } / g, '' )
395433 }
396434
397-
398435 for ( const { type, value, invert } of params . responseMatches || [ ] ) {
399436 const inv = Boolean ( invert ) // explicitly cast to boolean
400437
@@ -510,6 +547,23 @@ function shouldRevealCrlf({ version }: ProviderCtx) {
510547 return version >= AttestorVersion . ATTESTOR_VERSION_2_0_1
511548}
512549
550+ // revealing chunk framing (instead of redacting it) + dechunking on the
551+ // verifier is a breaking change; older clients still redact framing and rely
552+ // on the asterisk-collapse matching path
553+ function shouldRevealChunkFraming ( version : AttestorVersion ) {
554+ return version >= AttestorVersion . ATTESTOR_VERSION_3_2_0
555+ }
556+
557+ // dechunk a revealed body by reusing the parser: wrap it in a minimal
558+ // (already-validated) header block. streamEnded() skipped to tolerate trailing bytes
559+ function dechunkResponseBody ( body : string ) {
560+ const parser = makeHttpResponseParser ( )
561+ parser . onChunk (
562+ strToUint8Array ( 'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n' + body )
563+ )
564+ return uint8ArrayToStr ( parser . res . body ?? new Uint8Array ( ) )
565+ }
566+
513567function getHostPort ( params : ProviderParams < 'http' > , secretParams : ProviderSecretParams < 'http' > ) {
514568 const { host } = new URL ( getURL ( params , secretParams ) )
515569 if ( ! host ) {
@@ -550,6 +604,7 @@ function* processRedactionRequest(
550604 rs : HTTPResponseRedaction ,
551605 bodyStartIdx : number ,
552606 resChunks : ArraySlice [ ] | undefined ,
607+ revealFraming : boolean ,
553608) : Generator < RedactionItem > {
554609 let element = body
555610 let elementIdx = 0
@@ -665,22 +720,19 @@ function* processRedactionRequest(
665720
666721 function * addRedaction (
667722 hash : RedactedOrHashedArraySlice [ 'hash' ] | null = rs . hash ,
668- _resChunks = resChunks
669723 ) : Generator < RedactionItem > {
670724 if ( elementIdx < 0 || ! elementLength ) {
671725 return
672726 }
673727
674728 const reveal = getReveal ( elementIdx , elementLength , hash || undefined )
675729
676- yield {
677- reveal,
678- redactions : getRedactionsForChunkHeaders (
679- reveal . fromIndex ,
680- reveal . toIndex ,
681- _resChunks
682- )
683- }
730+ // new clients leave chunk framing revealed (verifier dechunks); older
731+ // clients redact the framing inside a reveal that spans chunks
732+ const redactions = revealFraming
733+ ? [ ]
734+ : getRedactionsForChunkHeaders ( reveal . fromIndex , reveal . toIndex , resChunks )
735+ yield { reveal, redactions }
684736 }
685737
686738 function getReveal (
0 commit comments