Skip to content

Commit 609c6f0

Browse files
Rickypcopybara-github
authored andcommitted
Treat OHTTP non-final zero-length chunks as errors.
Per the recent [chunked OHTTP draft update](https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-07.html#section-6-6), a non-final chunk MUST NOT contain a zero-length plaintext and must be treated as a decryption error upon receipt. PiperOrigin-RevId: 862088253
1 parent 8a8f2f3 commit 609c6f0

File tree

4 files changed

+83
-6
lines changed

4 files changed

+83
-6
lines changed

quiche/oblivious_http/buffers/oblivious_http_request.cc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ absl::StatusOr<std::string> ObliviousHttpRequest::EncryptChunk(
165165
absl::string_view plaintext_payload, const Context& context,
166166
bool is_final_chunk) {
167167
if (plaintext_payload.empty() && !is_final_chunk) {
168-
return absl::InvalidArgumentError("Invalid input.");
168+
return absl::InvalidArgumentError(
169+
"A non-final chunk MUST NOT contain a zero-length plaintext.");
169170
}
170171

171172
uint8_t* ad = nullptr;
@@ -277,6 +278,12 @@ absl::StatusOr<std::string> ObliviousHttpRequest::DecryptChunk(
277278
return SslErrorAsStatus("Failed to decrypt.",
278279
absl::StatusCode::kInvalidArgument);
279280
}
281+
282+
if (decrypted_len == 0 && !is_final_chunk) {
283+
return absl::InvalidArgumentError(
284+
"Decrypted non-final chunk plaintext is zero-length. A non-final chunk "
285+
"MUST NOT contain a zero-length plaintext.");
286+
}
280287
decrypted.resize(decrypted_len);
281288
return decrypted;
282289
}

quiche/oblivious_http/buffers/oblivious_http_response.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ absl::StatusOr<std::string> ObliviousHttpResponse::EncryptChunk(
346346
// Empty plaintext_payload is only allowed for the final chunk.
347347
if (!is_final_chunk && plaintext_payload.empty()) {
348348
return absl::InvalidArgumentError(
349-
"Payload cannot be empty for non-final chunks.");
349+
"A non-final chunk MUST NOT contain a zero-length plaintext.");
350350
}
351351
if (chunk_nonce.empty()) {
352352
return absl::InvalidArgumentError("Chunk nonce cannot be empty.");
@@ -405,7 +405,13 @@ absl::StatusOr<std::string> ObliviousHttpResponse::DecryptChunk(
405405
reinterpret_cast<const uint8_t*>(encrypted_chunk.data()),
406406
encrypted_chunk.size(), ad, ad_len)) {
407407
return SslErrorAsStatus(
408-
"Failed to decrypt the response with derived AEAD key and nonce.");
408+
"Failed to decrypt the response with derived AEAD key and nonce.",
409+
absl::StatusCode::kInvalidArgument);
410+
}
411+
if (decrypted_len == 0 && !is_final_chunk) {
412+
return absl::InvalidArgumentError(
413+
"Decrypted non-final chunk plaintext is zero-length. A non-final chunk "
414+
"MUST NOT contain a zero-length plaintext.");
409415
}
410416
decrypted.resize(decrypted_len);
411417
return decrypted;

quiche/oblivious_http/oblivious_http_client_test.cc

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,41 @@ TEST(ObliviousHttpClient, TestEncryptingMultipleRequestsWithSingleInstance) {
186186
EXPECT_NE(serialized_ohttp_req_1, serialized_ohttp_req_2);
187187
}
188188

189+
TEST(ChunkedObliviousHttpClient, DecryptZeroLengthNonFinalResponseChunkFails) {
190+
TestChunkHandler chunk_handler;
191+
// Contains the nonce and a zero-length
192+
// encrypted response chunk.
193+
std::string nonFinalZeroLengthEncryptedResponseChunk;
194+
EXPECT_TRUE(absl::HexStringToBytes(
195+
"a58cfdb3f69b5cb9ae328e25516dfed2109ac5a0b0ce59b9ff5bf6fe1ab2274715",
196+
&nonFinalZeroLengthEncryptedResponseChunk));
197+
198+
// The seed used for generating the response context.
199+
std::string seed;
200+
EXPECT_TRUE(absl::HexStringToBytes(
201+
"52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736",
202+
&seed));
203+
204+
absl::StatusOr<ObliviousHttpHeaderKeyConfig> key_config =
205+
ObliviousHttpHeaderKeyConfig::Create(
206+
/*key_id=*/1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
207+
EVP_HPKE_AES_128_GCM);
208+
QUICHE_ASSERT_OK(key_config);
209+
210+
absl::StatusOr<ChunkedObliviousHttpClient> chunked_client =
211+
ChunkedObliviousHttpClient::Create(GetHpkePublicKey(), key_config.value(),
212+
&chunk_handler,
213+
214+
seed);
215+
216+
QUICHE_ASSERT_OK(chunked_client);
217+
EXPECT_EQ(
218+
chunked_client
219+
->DecryptResponse(nonFinalZeroLengthEncryptedResponseChunk, false)
220+
.code(),
221+
absl::StatusCode::kInvalidArgument);
222+
}
223+
189224
TEST(ObliviousHttpClient, TestInvalidHPKEKey) {
190225
// Invalid public key.
191226
EXPECT_EQ(ObliviousHttpClient::Create(
@@ -671,7 +706,7 @@ TEST(ChunkedObliviousHttpClient, DecryptResponseWithCorruptedNonceFails) {
671706
corrupted_response[0] ^= 0x01; // Corrupt first byte of nonce.
672707
EXPECT_THAT(
673708
chunk_client->DecryptResponse(corrupted_response, /*end_stream=*/true),
674-
StatusIs(absl::StatusCode::kInternal));
709+
StatusIs(absl::StatusCode::kInvalidArgument));
675710
}
676711

677712
TEST(ChunkedObliviousHttpClient, DecryptResponseWithCorruptedChunkFails) {
@@ -713,7 +748,7 @@ TEST(ChunkedObliviousHttpClient, DecryptResponseWithCorruptedChunkFails) {
713748
// 12 bytes nonce + 1 byte len.
714749
EXPECT_THAT(chunk_client->DecryptResponse(corrupted_chunk_response,
715750
/*end_stream=*/false),
716-
StatusIs(absl::StatusCode::kInternal));
751+
StatusIs(absl::StatusCode::kInvalidArgument));
717752
}
718753

719754
TEST(ChunkedObliviousHttpClient, DecryptResponseWithCorruptedFinalChunkFails) {
@@ -755,7 +790,7 @@ TEST(ChunkedObliviousHttpClient, DecryptResponseWithCorruptedFinalChunkFails) {
755790
// 12 bytes nonce + 1 byte chunk indicator==0.
756791
EXPECT_THAT(
757792
chunk_client->DecryptResponse(corrupted_response, /*end_stream=*/true),
758-
StatusIs(absl::StatusCode::kInternal));
793+
StatusIs(absl::StatusCode::kInvalidArgument));
759794
}
760795

761796
TEST(ChunkedObliviousHttpClient, DecryptResponseAfterEndStreamReturnsError) {

quiche/oblivious_http/oblivious_http_gateway_test.cc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,35 @@ TEST(ChunkedObliviousHttpGateway,
283283
absl::StatusCode::kInternal);
284284
}
285285

286+
TEST(ChunkedObliviousHttpGateway, DecryptZeroLengthNonFinalRequestChunkFails) {
287+
TestChunkHandler chunk_handler;
288+
// Contains the initial encapsulated request header and a zero-length
289+
// encrypted request chunk.
290+
std::string nonFinalZeroLengthEncryptedRequestChunk;
291+
ASSERT_TRUE(
292+
absl::HexStringToBytes("010020000100013d9dde87c236fcbcfa540087557a8653738"
293+
"0e4731385cdcfcc391f7948b9"
294+
"590b108495352919fa03b19a997bb5621b1480",
295+
&nonFinalZeroLengthEncryptedRequestChunk));
296+
297+
// The key used to generate the above request.
298+
std::string privateKey;
299+
ASSERT_TRUE(absl::HexStringToBytes(
300+
"b77431ecfa8f4cfc30d6e467aafa06944dffe28cb9dd1409e33a3045f5adc8a1",
301+
&privateKey));
302+
auto gateway = ChunkedObliviousHttpGateway::Create(
303+
privateKey,
304+
GetOhttpKeyConfig(
305+
/*key_id=*/1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
306+
EVP_HPKE_AES_128_GCM),
307+
&chunk_handler);
308+
309+
EXPECT_EQ(
310+
gateway->DecryptRequest(nonFinalZeroLengthEncryptedRequestChunk, false)
311+
.code(),
312+
absl::StatusCode::kInvalidArgument);
313+
}
314+
286315
TEST(ObliviousHttpGateway, TestDecryptingMultipleRequestsWithSingleInstance) {
287316
auto instance = ObliviousHttpGateway::Create(
288317
GetHpkePrivateKey(),

0 commit comments

Comments
 (0)