From 4663c12414a1a6b59072594dc471dc2cc5786f6d Mon Sep 17 00:00:00 2001 From: Fisher Darling Date: Fri, 31 May 2024 17:59:19 +0200 Subject: [PATCH 1/3] Fix final request chunk prefix --- client.go | 6 +++--- gateway.go | 2 +- messages.go | 15 +++++++++++++++ ohttp_test.go | 14 +++++++++++--- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index ab55a9f..5f45eb7 100644 --- a/client.go +++ b/client.go @@ -131,13 +131,13 @@ func (c *EncapsulatedRequestContext) EncapsulateRequestChunk(requestChunk []byte }, nil } -func (c *EncapsulatedRequestContext) EncapsulateFinalRequestChunk(requestChunk []byte) (EncapsulatedRequestChunk, error) { +func (c *EncapsulatedRequestContext) EncapsulateFinalRequestChunk(requestChunk []byte) (EncapsulatedFinalRequestChunk, error) { ct, err := c.context.Seal(requestChunk, []byte("final")) if err != nil { - return EncapsulatedRequestChunk{}, err + return EncapsulatedFinalRequestChunk{}, err } - return EncapsulatedRequestChunk{ + return EncapsulatedFinalRequestChunk{ ct: ct, }, nil } diff --git a/gateway.go b/gateway.go index d9a32ce..096c692 100644 --- a/gateway.go +++ b/gateway.go @@ -271,7 +271,7 @@ func (s *GatewayRequestContext) DecapsulateRequestChunk(requestChunk Encapsulate return s.opener.Open(requestChunk.ct, nil) } -func (s *GatewayRequestContext) DecapsulateFinalRequestChunk(requestChunk EncapsulatedRequestChunk) ([]byte, error) { +func (s *GatewayRequestContext) DecapsulateFinalRequestChunk(requestChunk EncapsulatedFinalRequestChunk) ([]byte, error) { return s.opener.Open(requestChunk.ct, []byte("final")) } diff --git a/messages.go b/messages.go index 987da02..a53d474 100644 --- a/messages.go +++ b/messages.go @@ -107,6 +107,10 @@ type EncapsulatedRequestChunk struct { ct []byte } +type EncapsulatedFinalRequestChunk struct { + ct []byte +} + // Non-Final Request Chunk { // Length (i) = 1.., // HPKE-Protected Chunk (..), @@ -122,6 +126,17 @@ func (r EncapsulatedRequestChunk) Marshal() []byte { return b.BytesOrPanic() } +// Final Request Chunk Indicator (i) = 0, +// HPKE-Protected Final Chunk (..), +func (r EncapsulatedFinalRequestChunk) Marshal() []byte { + b := cryptobyte.NewBuilder(nil) + + b.AddBytes([]byte{0}) + b.AddBytes(r.ct) + + return b.BytesOrPanic() +} + type EncapsulatedRequestContext struct { responseLabel []byte enc []byte diff --git a/ohttp_test.go b/ohttp_test.go index 3288848..9e92ba9 100644 --- a/ohttp_test.go +++ b/ohttp_test.go @@ -101,7 +101,11 @@ func TestChunkedRoundTrip(t *testing.T) { rawRequestChunks := [][]byte{[]byte("hello"), []byte("world")} encapsulatedRequestChunks := make([]EncapsulatedRequestChunk, len(rawRequestChunks)) + var finalRequestChunk EncapsulatedFinalRequestChunk + gatewayRequestChunks := make([][]byte, len(encapsulatedRequestChunks)) + var finalGatewayRequestChunk []byte + rawResponseChunks := [][]byte{[]byte("foo"), []byte("bar")} encapsulatedResponseChunks := make([]EncapsulatedResponseChunk, len(rawResponseChunks)) clientResponseChunks := make([][]byte, len(encapsulatedResponseChunks)) @@ -116,7 +120,7 @@ func TestChunkedRoundTrip(t *testing.T) { encapsulatedRequestChunks[i], err = clientRequestContext.EncapsulateRequestChunk(rawRequestChunks[i]) require.Nil(t, err, "EncapsulateRequestChunk failed") } else { - encapsulatedRequestChunks[i], err = clientRequestContext.EncapsulateFinalRequestChunk(rawRequestChunks[i]) + finalRequestChunk, err = clientRequestContext.EncapsulateFinalRequestChunk(rawRequestChunks[i]) require.Nil(t, err, "EncapsulateFinalRequestChunk failed") } } @@ -131,16 +135,20 @@ func TestChunkedRoundTrip(t *testing.T) { gatewayRequestChunks[i], err = gatewayRequestContext.DecapsulateRequestChunk(encapsulatedRequestChunks[i]) require.Nil(t, err, "DecapsulateRequestChunk failed") } else { - gatewayRequestChunks[i], err = gatewayRequestContext.DecapsulateFinalRequestChunk(encapsulatedRequestChunks[i]) + finalGatewayRequestChunk, err = gatewayRequestContext.DecapsulateFinalRequestChunk(finalRequestChunk) require.Nil(t, err, "DecapsulateFinalRequestChunk failed") } } // Compare request chunks for equality for i, _ := range rawRequestChunks { - require.Equal(t, rawRequestChunks[i], gatewayRequestChunks[i], "Request chunk mismatch") + if i < len(encapsulatedRequestChunks)-1 { + require.Equal(t, rawRequestChunks[i], gatewayRequestChunks[i], "Request chunk mismatch") + } } + require.Equal(t, rawRequestChunks[len(rawRequestChunks)-1], finalGatewayRequestChunk) + // Encapsulate each response chunk for i, _ := range rawResponseChunks { if i < len(rawResponseChunks)-1 { From f9a06b73999be90e0f3950b857fb49b2796ea2cc Mon Sep 17 00:00:00 2001 From: Fisher Darling Date: Fri, 31 May 2024 19:07:47 +0200 Subject: [PATCH 2/3] Decapsulate chunked hpke stream --- client.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/client.go b/client.go index 5f45eb7..7000fce 100644 --- a/client.go +++ b/client.go @@ -1,9 +1,12 @@ package ohttp import ( + "bufio" + "bytes" "crypto/rand" "encoding/binary" "fmt" + "io" "github.com/cloudflare/circl/hpke" "github.com/cloudflare/circl/kem" @@ -111,6 +114,99 @@ func (c Client) EncapsulateRequest(request []byte) (EncapsulatedRequest, Encapsu }, nil } +type ChunkedHpkeReader struct { + rc *EncapsulatedResponseContext + inner *bufio.Reader + buffer *bytes.Buffer +} + +func NewChunkedHpkeReader(requestContext EncapsulatedRequestContext, body *bufio.Reader) (*ChunkedHpkeReader, error) { + _, _, AEAD := requestContext.suite.Params() + + // Nonce is Nk + responseNonceLen := max(int(AEAD.KeySize()), 12) + responseNonce := make([]byte, responseNonceLen) + n, err := io.ReadFull(body, responseNonce) + + if n != responseNonceLen || err != nil { + return nil, fmt.Errorf("unable to read response nonce: %s", err) + } + + rc, err := requestContext.Prepare(EncapsulatedResponseHeader{responseNonce: responseNonce}) + if err != nil { + return nil, fmt.Errorf("unable to create response decapsulation context: %s", err) + } + + return &ChunkedHpkeReader{ + rc: rc, + inner: body, + buffer: bytes.NewBuffer([]byte{}), + }, nil +} + +func (r ChunkedHpkeReader) Read(buf []byte) (int, error) { + // if the buffer has not been fully read, passthrough reads + if r.buffer.Len() != 0 { + return r.buffer.Read(buf) + } + + len, err := r.readNextChunk() + if err != nil { + return r.buffer.Read(buf) + } + + // We are done parsing the body: + if len == 0 { + return 0, io.EOF + } + + return 0, err +} + +func (r ChunkedHpkeReader) readNextChunk() (int, error) { + len, err := Read(r.inner) + length := int(len) + + if err != nil { + return length, nil + } + + var chunk []byte + + // read the chunk to the end + if length == 0 { + finalChunk, err := io.ReadAll(r.inner) + if err != nil { + return 0, fmt.Errorf("unable to read final chunk: %s", err) + } + + chunk, err = r.rc.DecapsulateFinalResponseChunk(EncapsulatedResponseChunk{raw: finalChunk}) + if err != nil { + return 0, fmt.Errorf("unable to decapsulate final chunk: %s", err) + } + } else { + // We have a normal, length-delimited chunk + encappedChunk := make([]byte, len) + n, err := io.ReadFull(r.inner, encappedChunk) + if n != length || err != nil { + return 0, fmt.Errorf("unable to read chunk: %s, len=%d", err, n) + } + + chunk, err = r.rc.DecapsulateResponseChunk(EncapsulatedResponseChunk{raw: encappedChunk}) + if err != nil { + return 0, fmt.Errorf("unable to read chunk length: %s", err) + } + } + + r.buffer.Write(chunk) + + return length, nil +} + +func (r ChunkedHpkeReader) Close() error { + return nil +} + func (c *ChunkedClient) Prepare() (EncapsulatedRequestHeader, EncapsulatedRequestContext, error) { return c.header, EncapsulatedRequestContext{ responseLabel: []byte(c.responseLabel), From b4dda97d7a03bfe2620cd4ad0523047f72772a77 Mon Sep 17 00:00:00 2001 From: Fisher Darling Date: Mon, 3 Jun 2024 10:43:23 +0200 Subject: [PATCH 3/3] parse indeterminate length responses --- bhttp.go | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/bhttp.go b/bhttp.go index 7c53b99..737ed7c 100644 --- a/bhttp.go +++ b/bhttp.go @@ -1,6 +1,7 @@ package ohttp import ( + "bufio" "bytes" "errors" "fmt" @@ -594,3 +595,182 @@ func UnmarshalBinaryResponse(data []byte) (*http.Response, error) { func CreateBinaryResponse(resp *http.Response) BinaryResponse { return BinaryResponse(*resp) } + +func ConvertBhttpResponse(resp *http.Response) (*http.Response, error) { + body := bufio.NewReader(resp.Body) + return ParseBhttpResponse(body) +} + +func ParseBhttpResponse(body *bufio.Reader) (*http.Response, error) { + frame, err := Read(body) + if err != nil { + return nil, fmt.Errorf("unable to read bhttp framing indicator: %s", err) + } + + switch frame { + case 1: + return ParseKnownLengthBhttpResponse(body) + case 3: + return ParseIndeterminateBhttpResponse(body) + default: + return nil, fmt.Errorf("bad response framing indicator: %d", frame) + } +} + +func ParseKnownLengthBhttpResponse(body *bufio.Reader) (*http.Response, error) { + data, err := io.ReadAll(body) + if err != nil { + return nil, fmt.Errorf("unable to read the known-length bhttp data: %s", err) + } + + // TODO: clean up switching between indeterminate and known-length parsing + return UnmarshalBinaryResponse(append([]byte{0x03}, data...)) +} + +func ParseIndeterminateBhttpResponse(body *bufio.Reader) (*http.Response, error) { + controlData, err := parseIndeterminateResponseControlData(body) + if err != nil { + return nil, fmt.Errorf("unable to parse response control data: %s", err) + } + + headers, err := parseIndeterminateFieldSection(body) + if err != nil { + return nil, fmt.Errorf("could not parse bhttp field section: %s", err) + } + + new_body, err := setupIndeterminateContentBody(body) + if err != nil { + return nil, fmt.Errorf("unable to construct intdeterminate body parser") + } + + return &http.Response{ + StatusCode: controlData.statusCode, + Header: headers, + Body: new_body, + }, nil +} + +func parseIndeterminateResponseControlData(body *bufio.Reader) (responseControlData, error) { + statusCode, err := Read(body) + if err != nil { + return responseControlData{}, fmt.Errorf("unable to read response status code: %s", err) + } + + if statusCode <= 199 || statusCode >= 600 { + return responseControlData{}, fmt.Errorf("informational responses are unsupported") + } + + return responseControlData{ + statusCode: int(statusCode), + }, nil +} + +func parseIndeterminateFieldSection(body *bufio.Reader) (http.Header, error) { + headers := make(http.Header) + + for { + // Name Length (i) = 1 + nameLength, err := Read(body) + + // The headers are truncated + if err == io.EOF { + return headers, nil + } + + if err != nil && err != io.EOF { + return nil, fmt.Errorf("unable to read response status code: %s", err) + } + + // Content Terminator (i) = 0 + if nameLength == 0 { + break + } + + name := make([]byte, nameLength) + n, err := io.ReadFull(body, name) + if n != int(nameLength) || err != nil { + return nil, fmt.Errorf("unable to read header name: %s", err) + } + + // Value Length (i) = 1 + valueLength, err := Read(body) + if err != nil { + return nil, fmt.Errorf("unable to read header value length: %s", err) + } + + value := make([]byte, valueLength) + n, err = io.ReadFull(body, value) + if n != int(valueLength) || err != nil { + return nil, fmt.Errorf("unable to read header value: %s", err) + } + + headers.Add(string(name), string(value)) + } + + return headers, nil +} + +func setupIndeterminateContentBody(body *bufio.Reader) (io.ReadCloser, error) { + return NewBhttpBody(body) +} + +type BhttpBodyReader struct { + inner *bufio.Reader + buffer *bytes.Buffer +} + +var _ io.ReadCloser = (*BhttpBodyReader)(nil) + +func NewBhttpBody(inner *bufio.Reader) (BhttpBodyReader, error) { + buffer := bytes.NewBuffer([]byte{}) + return BhttpBodyReader{ + inner, + buffer, + }, nil +} + +func (b BhttpBodyReader) Read(buf []byte) (int, error) { + // if the buffer has not been fully read, passthrough reads + if b.buffer.Len() != 0 { + return b.buffer.Read(buf) + } + + len, err := b.readNextChunk() + if err != nil { + return b.buffer.Read(buf) + } + + // We are done parsing the body: + if len == 0 && err == io.EOF { + return 0, io.EOF + } + + return 0, err +} + +func (b BhttpBodyReader) readNextChunk() (int, error) { + length, err := Read(b.inner) + + if length == 0 { + return int(length), io.EOF + } + + if err != nil { + return int(length), err + } + + chunk := make([]byte, length) + + n, err := io.ReadFull(b.inner, chunk) + if n != int(length) || err != nil { + return int(length), fmt.Errorf("unable to read chunk of length %d: %s", length, err) + } + + b.buffer.Write(chunk) + + return int(length), nil +} + +func (b BhttpBodyReader) Close() error { + return nil +}