| #include "quiche/oblivious_http/oblivious_http_client.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "absl/base/attributes.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "openssl/base.h" |
| #include "openssl/hpke.h" |
| #include "quiche/common/quiche_crypto_logging.h" |
| #include "quiche/common/quiche_data_reader.h" |
| #include "quiche/common/quiche_data_writer.h" |
| #include "quiche/common/quiche_status_utils.h" |
| #include "quiche/oblivious_http/buffers/oblivious_http_request.h" |
| #include "quiche/oblivious_http/buffers/oblivious_http_response.h" |
| #include "quiche/oblivious_http/common/oblivious_http_chunk_handler.h" |
| #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" |
| |
| namespace quiche { |
| |
| namespace { |
| constexpr uint64_t kFinalChunkIndicator = 0; |
| |
| // Use BoringSSL's setup_sender API to validate whether the HPKE public key |
| // input provided by the user is valid. |
| absl::Status ValidateClientParameters( |
| absl::string_view hpke_public_key, |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config) { |
| // Initialize HPKE client context and check if context can be setup with the |
| // given public key to verify if the public key is indeed valid. |
| bssl::UniquePtr<EVP_HPKE_CTX> client_ctx(EVP_HPKE_CTX_new()); |
| if (client_ctx == nullptr) { |
| return SslErrorAsStatus( |
| "Failed to initialize HPKE ObliviousHttpClient Context."); |
| } |
| // Setup the sender (client) |
| std::string encapsulated_key(EVP_HPKE_MAX_ENC_LENGTH, '\0'); |
| size_t enc_len; |
| absl::string_view info = "verify if given HPKE public key is valid"; |
| if (!EVP_HPKE_CTX_setup_sender( |
| client_ctx.get(), reinterpret_cast<uint8_t*>(encapsulated_key.data()), |
| &enc_len, encapsulated_key.size(), ohttp_key_config.GetHpkeKem(), |
| ohttp_key_config.GetHpkeKdf(), ohttp_key_config.GetHpkeAead(), |
| reinterpret_cast<const uint8_t*>(hpke_public_key.data()), |
| hpke_public_key.size(), reinterpret_cast<const uint8_t*>(info.data()), |
| info.size())) { |
| return SslErrorAsStatus( |
| "Failed to setup HPKE context with given public key param " |
| "hpke_public_key."); |
| } |
| return absl::OkStatus(); |
| } |
| |
| } // namespace |
| |
| ObliviousHttpClient::ObliviousHttpClient( |
| std::string client_public_key, |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config) |
| : hpke_public_key_(std::move(client_public_key)), |
| ohttp_key_config_(ohttp_key_config) {} |
| |
| // Initialize Bssl. |
| absl::StatusOr<ObliviousHttpClient> ObliviousHttpClient::Create( |
| absl::string_view hpke_public_key, |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config) { |
| if (hpke_public_key.empty()) { |
| return absl::InvalidArgumentError("Invalid/Empty HPKE public key."); |
| } |
| auto is_valid_input = |
| ValidateClientParameters(hpke_public_key, ohttp_key_config); |
| if (!is_valid_input.ok()) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Invalid input received in method parameters. ", |
| is_valid_input.message())); |
| } |
| return ObliviousHttpClient(std::string(hpke_public_key), ohttp_key_config); |
| } |
| |
| absl::StatusOr<ObliviousHttpRequest> |
| ObliviousHttpClient::CreateObliviousHttpRequest( |
| std::string plaintext_data) const { |
| return ObliviousHttpRequest::CreateClientObliviousRequest( |
| std::move(plaintext_data), hpke_public_key_, ohttp_key_config_); |
| } |
| |
| absl::StatusOr<ObliviousHttpResponse> |
| ObliviousHttpClient::DecryptObliviousHttpResponse( |
| std::string encrypted_data, |
| ObliviousHttpRequest::Context& oblivious_http_request_context) const { |
| return ObliviousHttpResponse::CreateClientObliviousResponse( |
| std::move(encrypted_data), oblivious_http_request_context); |
| } |
| |
| ChunkedObliviousHttpClient::ChunkedObliviousHttpClient( |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config, |
| ObliviousHttpRequest::Context hpke_sender_context, |
| ObliviousHttpResponse::CommonAeadParamsResult aead_params, |
| ObliviousHttpChunkHandler* chunk_handler) |
| : ohttp_key_config_(ohttp_key_config), |
| hpke_sender_context_(std::move(hpke_sender_context)), |
| aead_params_(aead_params), |
| chunk_handler_(chunk_handler) {}; |
| |
| absl::StatusOr<ChunkedObliviousHttpClient> ChunkedObliviousHttpClient::Create( |
| absl::string_view hpke_public_key, |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config, |
| ObliviousHttpChunkHandler* chunk_handler, absl::string_view seed) { |
| if (hpke_public_key.empty()) { |
| return absl::InvalidArgumentError("Empty HPKE public key."); |
| } |
| if (chunk_handler == nullptr) { |
| return absl::InvalidArgumentError("Null chunk handler."); |
| } |
| absl::Status is_valid_input = |
| ValidateClientParameters(hpke_public_key, ohttp_key_config); |
| if (!is_valid_input.ok()) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Invalid input received in method parameters. ", |
| is_valid_input.message())); |
| } |
| QUICHE_ASSIGN_OR_RETURN( |
| ObliviousHttpRequest::Context hpke_sender_context, |
| ObliviousHttpRequest::CreateHpkeSenderContext( |
| hpke_public_key, ohttp_key_config, seed, |
| ObliviousHttpHeaderKeyConfig::kChunkedOhttpRequestLabel)); |
| QUICHE_ASSIGN_OR_RETURN( |
| ObliviousHttpResponse::CommonAeadParamsResult aead_params, |
| ObliviousHttpResponse::GetCommonAeadParams(hpke_sender_context)); |
| |
| return ChunkedObliviousHttpClient(ohttp_key_config, |
| std::move(hpke_sender_context), |
| std::move(aead_params), chunk_handler); |
| } |
| |
| absl::StatusOr<std::string> ChunkedObliviousHttpClient::EncryptRequestChunk( |
| absl::string_view plaintext_payload, bool is_final_chunk) { |
| if (request_current_section_ == RequestMessageSection::kEnd) { |
| return absl::InvalidArgumentError("Cannot encrypt in bad state."); |
| } |
| absl::StatusOr<std::string> request_chunk = |
| EncryptRequestChunkImpl(plaintext_payload, is_final_chunk); |
| if (!request_chunk.ok()) { |
| request_current_section_ = RequestMessageSection::kEnd; |
| } |
| return request_chunk; |
| } |
| |
| absl::StatusOr<std::string> ChunkedObliviousHttpClient::EncryptRequestChunkImpl( |
| absl::string_view plaintext_payload, bool is_final_chunk) { |
| QUICHE_ASSIGN_OR_RETURN( |
| std::string encrypted_data, |
| ObliviousHttpRequest::EncryptChunk(plaintext_payload, |
| hpke_sender_context_, is_final_chunk)); |
| |
| std::string maybe_key_header_data = ""; |
| if (request_current_section_ == RequestMessageSection::kHeader) { |
| maybe_key_header_data = |
| absl::StrCat(ohttp_key_config_.SerializeOhttpPayloadHeader(), |
| hpke_sender_context_.GetEncapsulatedKey()); |
| request_current_section_ = RequestMessageSection::kChunk; |
| } |
| |
| uint8_t chunk_var_int_length = |
| QuicheDataWriter::GetVarInt62Len(encrypted_data.size()); |
| if (chunk_var_int_length == 0) { |
| return absl::InvalidArgumentError( |
| "Encrypted data is too large to be represented as a varint."); |
| } |
| uint64_t chunk_var_int = encrypted_data.size(); |
| if (is_final_chunk) { |
| request_current_section_ = RequestMessageSection::kEnd; |
| chunk_var_int_length = |
| QuicheDataWriter::GetVarInt62Len(kFinalChunkIndicator); |
| if (chunk_var_int_length == 0) { |
| return absl::InvalidArgumentError( |
| "Final chunk indicator is too large to be represented as a varint."); |
| } |
| chunk_var_int = kFinalChunkIndicator; |
| } |
| |
| std::string request_buffer(maybe_key_header_data.size() + |
| chunk_var_int_length + encrypted_data.size(), |
| '\0'); |
| |
| QuicheDataWriter writer(request_buffer.size(), request_buffer.data()); |
| |
| if (!writer.WriteStringPiece(maybe_key_header_data)) { |
| return absl::InternalError( |
| "Failed to write key header data to request buffer."); |
| } |
| if (!writer.WriteVarInt62(chunk_var_int)) { |
| return absl::InternalError( |
| "Failed to write encrypted chunk length to buffer."); |
| } |
| if (!writer.WriteStringPiece(encrypted_data)) { |
| return absl::InternalError( |
| "Failed to write encrypted chunk to request buffer."); |
| } |
| if (writer.remaining() != 0) { |
| return absl::InternalError("Failed to write all data."); |
| } |
| |
| return request_buffer; |
| } |
| |
| absl::string_view ChunkedObliviousHttpClient::InitializeResponseCheckpoint( |
| absl::string_view data) { |
| absl::string_view response_checkpoint = data; |
| // Prepend buffered data if present. This is the data from a previous call to |
| // DecryptResponse that could not finish because it needed this new data. |
| if (!response_buffer_.empty()) { |
| absl::StrAppend(&response_buffer_, data); |
| response_checkpoint = response_buffer_; |
| } |
| return response_checkpoint; |
| } |
| |
| absl::Status ChunkedObliviousHttpClient::DecryptResponseCheckpoint( |
| absl::string_view& response_checkpoint, bool end_stream) { |
| QuicheDataReader reader(response_checkpoint); |
| switch (response_current_section_) { |
| case ResponseMessageSection::kEnd: |
| return absl::InternalError("Response is invalid."); |
| case ResponseMessageSection::kNonce: { |
| absl::string_view response_nonce; |
| if (!reader.ReadStringPiece(&response_nonce, aead_params_.secret_len)) { |
| return absl::OutOfRangeError("Not enough data to read response nonce."); |
| } |
| |
| QUICHE_ASSIGN_OR_RETURN( |
| ObliviousHttpResponse::AeadContextData aead_context_data, |
| ObliviousHttpResponse::GetAeadContextData( |
| hpke_sender_context_, aead_params_, |
| ObliviousHttpHeaderKeyConfig::kChunkedOhttpResponseLabel, |
| response_nonce)); |
| aead_context_data_.emplace(std::move(aead_context_data)); |
| |
| QUICHE_ASSIGN_OR_RETURN( |
| ObliviousHttpResponse::ChunkCounter response_chunk_counter, |
| ObliviousHttpResponse::ChunkCounter::Create( |
| aead_context_data_->aead_nonce)); |
| |
| response_chunk_counter_.emplace(std::move(response_chunk_counter)); |
| UpdateCheckpoint(reader, response_checkpoint); |
| response_current_section_ = ResponseMessageSection::kChunk; |
| } |
| ABSL_FALLTHROUGH_INTENDED; |
| case ResponseMessageSection::kChunk: { |
| if (!aead_context_data_.has_value()) { |
| return absl::InternalError( |
| "HPKE context has not been derived from the response nonce."); |
| } |
| ObliviousHttpResponse::AeadContextData& aead_context_data = |
| *aead_context_data_; |
| if (!response_chunk_counter_.has_value()) { |
| return absl::InternalError("Chunk counter has not been initialized."); |
| } |
| ObliviousHttpResponse::ChunkCounter& response_chunk_counter = |
| *response_chunk_counter_; |
| |
| bool is_final_chunk = false; |
| do { |
| if (response_chunk_counter.LimitExceeded()) { |
| return absl::InvalidArgumentError( |
| "Maximum number of chunks allowed in the response has been " |
| "exceeded."); |
| } |
| uint64_t length_or_final_chunk_indicator; |
| if (!reader.ReadVarInt62(&length_or_final_chunk_indicator)) { |
| return absl::OutOfRangeError("Not enough data to read chunk length."); |
| } |
| |
| is_final_chunk = |
| length_or_final_chunk_indicator == kFinalChunkIndicator; |
| if (!is_final_chunk) { |
| absl::string_view chunk; |
| if (!reader.ReadStringPiece(&chunk, |
| length_or_final_chunk_indicator)) { |
| return absl::OutOfRangeError("Not enough data to read chunk."); |
| } |
| |
| QUICHE_ASSIGN_OR_RETURN( |
| std::string decrypted_chunk, |
| ObliviousHttpResponse::DecryptChunk( |
| chunk, aead_context_data, |
| response_chunk_counter.GetChunkNonce(), is_final_chunk)); |
| |
| response_chunk_counter.Increment(); |
| if (chunk_handler_ == nullptr) { |
| return absl::InternalError("Chunk handler is null."); |
| } |
| absl::Status handler_status = |
| chunk_handler_->OnDecryptedChunk(decrypted_chunk); |
| if (!handler_status.ok()) { |
| return handler_status; |
| } |
| } |
| |
| UpdateCheckpoint(reader, response_checkpoint); |
| } while (!is_final_chunk); |
| |
| response_current_section_ = ResponseMessageSection::kFinalChunk; |
| } |
| ABSL_FALLTHROUGH_INTENDED; |
| case ResponseMessageSection::kFinalChunk: { |
| if (!end_stream) { |
| return absl::OutOfRangeError("Not enough data to read final chunk."); |
| } |
| if (!aead_context_data_.has_value()) { |
| return absl::InternalError( |
| "HPKE context has not been derived from the response nonce."); |
| } |
| ObliviousHttpResponse::AeadContextData& aead_context_data = |
| *aead_context_data_; |
| if (!response_chunk_counter_.has_value()) { |
| return absl::InternalError("Chunk counter has not been initialized."); |
| } |
| ObliviousHttpResponse::ChunkCounter& response_chunk_counter = |
| *response_chunk_counter_; |
| QUICHE_ASSIGN_OR_RETURN( |
| std::string decrypted_chunk, |
| ObliviousHttpResponse::DecryptChunk( |
| reader.PeekRemainingPayload(), aead_context_data, |
| response_chunk_counter.GetChunkNonce(), |
| /*is_final_chunk=*/true)); |
| |
| if (chunk_handler_ == nullptr) { |
| return absl::InternalError("Chunk handler is null."); |
| } |
| absl::Status handler_status = |
| chunk_handler_->OnDecryptedChunk(decrypted_chunk); |
| if (!handler_status.ok()) { |
| return handler_status; |
| } |
| handler_status = chunk_handler_->OnChunksDone(); |
| if (!handler_status.ok()) { |
| return handler_status; |
| } |
| } |
| return absl::OkStatus(); |
| } |
| } |
| |
| absl::Status ChunkedObliviousHttpClient::DecryptResponse( |
| absl::string_view encrypted_data, bool end_stream) { |
| if (response_current_section_ == ResponseMessageSection::kEnd) { |
| return absl::InternalError("Response is invalid."); |
| } |
| absl::string_view response_checkpoint = |
| InitializeResponseCheckpoint(encrypted_data); |
| absl::Status status = |
| DecryptResponseCheckpoint(response_checkpoint, end_stream); |
| if (end_stream) { |
| response_current_section_ = ResponseMessageSection::kEnd; |
| response_buffer_.clear(); |
| return status; |
| } |
| if (absl::IsOutOfRange(status)) { |
| BufferResponseCheckpoint(response_checkpoint); |
| return absl::OkStatus(); |
| } |
| if (!status.ok()) { |
| response_current_section_ = ResponseMessageSection::kEnd; |
| } |
| |
| response_buffer_.clear(); |
| return status; |
| } |
| |
| } // namespace quiche |