| #include "quiche/oblivious_http/oblivious_http_gateway.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/aead.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_random.h" | |
| #include "quiche/common/quiche_status_utils.h" | |
| #include "quiche/oblivious_http/buffers/oblivious_http_request.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; | |
| } | |
| // Constructor. | |
| ObliviousHttpGateway::ObliviousHttpGateway( | |
| bssl::UniquePtr<EVP_HPKE_KEY> recipient_key, | |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config, | |
| QuicheRandom* quiche_random) | |
| : server_hpke_key_(std::move(recipient_key)), | |
| ohttp_key_config_(ohttp_key_config), | |
| quiche_random_(quiche_random) {} | |
| absl::StatusOr<bssl::UniquePtr<EVP_HPKE_KEY>> CreateServerRecipientKey( | |
| absl::string_view hpke_private_key, | |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config) { | |
| if (hpke_private_key.empty()) { | |
| return absl::InvalidArgumentError("Invalid/Empty HPKE private key."); | |
| } | |
| // Initialize HPKE key and context. | |
| bssl::UniquePtr<EVP_HPKE_KEY> recipient_key(EVP_HPKE_KEY_new()); | |
| if (recipient_key == nullptr) { | |
| return SslErrorAsStatus( | |
| "Failed to initialize ObliviousHttpGateway/Server's Key."); | |
| } | |
| if (!EVP_HPKE_KEY_init( | |
| recipient_key.get(), ohttp_key_config.GetHpkeKem(), | |
| reinterpret_cast<const uint8_t*>(hpke_private_key.data()), | |
| hpke_private_key.size())) { | |
| return SslErrorAsStatus("Failed to import HPKE private key."); | |
| } | |
| return recipient_key; | |
| } | |
| // Initialize ObliviousHttpGateway(Recipient/Server) context. | |
| absl::StatusOr<ObliviousHttpGateway> ObliviousHttpGateway::Create( | |
| absl::string_view hpke_private_key, | |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config, | |
| QuicheRandom* quiche_random) { | |
| QUICHE_ASSIGN_OR_RETURN( | |
| bssl::UniquePtr<EVP_HPKE_KEY> recipient_key, | |
| CreateServerRecipientKey(hpke_private_key, ohttp_key_config)); | |
| if (quiche_random == nullptr) quiche_random = QuicheRandom::GetInstance(); | |
| return ObliviousHttpGateway(std::move(recipient_key), ohttp_key_config, | |
| quiche_random); | |
| } | |
| absl::StatusOr<ObliviousHttpRequest> | |
| ObliviousHttpGateway::DecryptObliviousHttpRequest( | |
| absl::string_view encrypted_data, absl::string_view request_label) const { | |
| return ObliviousHttpRequest::CreateServerObliviousRequest( | |
| encrypted_data, *(server_hpke_key_), ohttp_key_config_, request_label); | |
| } | |
| absl::StatusOr<ObliviousHttpResponse> | |
| ObliviousHttpGateway::CreateObliviousHttpResponse( | |
| std::string plaintext_data, | |
| ObliviousHttpRequest::Context& oblivious_http_request_context, | |
| absl::string_view response_label) const { | |
| return ObliviousHttpResponse::CreateServerObliviousResponse( | |
| std::move(plaintext_data), oblivious_http_request_context, response_label, | |
| quiche_random_); | |
| } | |
| // Constructor. | |
| ChunkedObliviousHttpGateway::ChunkedObliviousHttpGateway( | |
| bssl::UniquePtr<EVP_HPKE_KEY> recipient_key, | |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config, | |
| ObliviousHttpChunkHandler* chunk_handler, QuicheRandom* quiche_random) | |
| : server_hpke_key_(std::move(recipient_key)), | |
| ohttp_key_config_(ohttp_key_config), | |
| chunk_handler_(*chunk_handler), | |
| quiche_random_(quiche_random) {} | |
| absl::StatusOr<ChunkedObliviousHttpGateway> ChunkedObliviousHttpGateway::Create( | |
| absl::string_view hpke_private_key, | |
| const ObliviousHttpHeaderKeyConfig& ohttp_key_config, | |
| ObliviousHttpChunkHandler* chunk_handler, QuicheRandom* quiche_random) { | |
| if (chunk_handler == nullptr) { | |
| return absl::InvalidArgumentError("Chunk handler is null"); | |
| } | |
| QUICHE_ASSIGN_OR_RETURN( | |
| bssl::UniquePtr<EVP_HPKE_KEY> recipient_key, | |
| CreateServerRecipientKey(hpke_private_key, ohttp_key_config)); | |
| if (quiche_random == nullptr) { | |
| quiche_random = QuicheRandom::GetInstance(); | |
| } | |
| return ChunkedObliviousHttpGateway(std::move(recipient_key), ohttp_key_config, | |
| chunk_handler, quiche_random); | |
| } | |
| void ChunkedObliviousHttpGateway::InitializeRequestCheckpoint( | |
| absl::string_view data) { | |
| request_checkpoint_view_ = data; | |
| // Prepend buffered data if present. This is the data from a previous call to | |
| // DecryptRequest that could not finish because it needed this new data. | |
| if (!request_buffer_.empty()) { | |
| if (!data.empty()) { | |
| absl::StrAppend(&request_buffer_, data); | |
| } | |
| request_checkpoint_view_ = request_buffer_; | |
| } | |
| } | |
| absl::Status ChunkedObliviousHttpGateway::DecryptRequestCheckpoint( | |
| bool end_stream) { | |
| QuicheDataReader reader(request_checkpoint_view_); | |
| switch (request_current_section_) { | |
| case RequestMessageSection::kEnd: | |
| return absl::InternalError("Request is invalid."); | |
| case RequestMessageSection::kHeader: { | |
| // Check there is enough data for the chunked request header. | |
| // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#name-request-format | |
| if (reader.PeekRemainingPayload().size() < | |
| ObliviousHttpHeaderKeyConfig::kHeaderLength + | |
| EVP_HPKE_KEM_enc_len(EVP_HPKE_KEY_kem(server_hpke_key_.get()))) { | |
| return absl::OutOfRangeError("Not enough data to read header."); | |
| } | |
| QUICHE_ASSIGN_OR_RETURN( | |
| ObliviousHttpRequest::Context context, | |
| ObliviousHttpRequest::DecodeEncapsulatedRequestHeader( | |
| reader, *server_hpke_key_, ohttp_key_config_, | |
| ObliviousHttpHeaderKeyConfig::kChunkedOhttpRequestLabel)); | |
| oblivious_http_request_context_ = std::move(context); | |
| SaveCheckpoint(reader); | |
| request_current_section_ = RequestMessageSection::kChunk; | |
| } | |
| ABSL_FALLTHROUGH_INTENDED; | |
| case RequestMessageSection::kChunk: { | |
| uint64_t length_or_final_chunk_indicator; | |
| do { | |
| if (!reader.ReadVarInt62(&length_or_final_chunk_indicator)) { | |
| return absl::OutOfRangeError("Not enough data to read chunk length."); | |
| } | |
| absl::string_view chunk; | |
| if (length_or_final_chunk_indicator != kFinalChunkIndicator) { | |
| if (!reader.ReadStringPiece(&chunk, | |
| length_or_final_chunk_indicator)) { | |
| return absl::OutOfRangeError("Not enough data to read chunk."); | |
| } | |
| if (!oblivious_http_request_context_.has_value()) { | |
| return absl::InternalError( | |
| "HPKE context has not been derived from an encrypted request."); | |
| } | |
| QUICHE_ASSIGN_OR_RETURN(std::string decrypted_chunk, | |
| ObliviousHttpRequest::DecryptChunk( | |
| *oblivious_http_request_context_, chunk, | |
| /*is_final_chunk=*/false)); | |
| absl::Status handler_status = | |
| chunk_handler_.OnDecryptedChunk(decrypted_chunk); | |
| if (!handler_status.ok()) { | |
| return absl::InternalError(absl::StrCat( | |
| "Chunk handler failed to process decrypted chunk: ", | |
| handler_status.message())); | |
| } | |
| } | |
| SaveCheckpoint(reader); | |
| } while (length_or_final_chunk_indicator != kFinalChunkIndicator); | |
| request_current_section_ = RequestMessageSection::kFinalChunk; | |
| } | |
| ABSL_FALLTHROUGH_INTENDED; | |
| case RequestMessageSection::kFinalChunk: { | |
| if (!end_stream) { | |
| return absl::OutOfRangeError("Not enough data to read final chunk."); | |
| } | |
| if (!oblivious_http_request_context_.has_value()) { | |
| return absl::InternalError( | |
| "HPKE context has not been derived from an encrypted request."); | |
| } | |
| QUICHE_ASSIGN_OR_RETURN( | |
| std::string decrypted_chunk, | |
| ObliviousHttpRequest::DecryptChunk(*oblivious_http_request_context_, | |
| reader.PeekRemainingPayload(), | |
| /*is_final_chunk=*/true)); | |
| absl::Status handler_status = | |
| chunk_handler_.OnDecryptedChunk(decrypted_chunk); | |
| if (!handler_status.ok()) { | |
| return absl::InternalError( | |
| absl::StrCat("Chunk handler failed to process decrypted chunk: ", | |
| handler_status.message())); | |
| } | |
| handler_status = chunk_handler_.OnChunksDone(); | |
| if (!handler_status.ok()) { | |
| return absl::InternalError( | |
| absl::StrCat("Chunk handler failed to process chunks done: ", | |
| handler_status.message())); | |
| } | |
| } | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ChunkedObliviousHttpGateway::DecryptRequest(absl::string_view data, | |
| bool end_stream) { | |
| if (request_current_section_ == RequestMessageSection::kEnd) { | |
| return absl::InternalError("Decrypting is marked as invalid."); | |
| } | |
| InitializeRequestCheckpoint(data); | |
| absl::Status status = DecryptRequestCheckpoint(end_stream); | |
| if (end_stream) { | |
| request_current_section_ = RequestMessageSection::kEnd; | |
| request_buffer_.clear(); | |
| if (absl::IsOutOfRange(status)) { | |
| // OutOfRange only used internally for buffering, so return | |
| // InvalidArgument if this is the end of the stream. | |
| status = absl::InvalidArgumentError(status.message()); | |
| } | |
| return status; | |
| } | |
| if (absl::IsOutOfRange(status)) { | |
| BufferRequestCheckpoint(); | |
| return absl::OkStatus(); | |
| } | |
| if (!status.ok()) { | |
| request_current_section_ = RequestMessageSection::kEnd; | |
| } | |
| request_buffer_.clear(); | |
| return status; | |
| } | |
| absl::StatusOr<std::string> ChunkedObliviousHttpGateway::EncryptResponse( | |
| absl::string_view plaintext_payload, bool is_final_chunk) { | |
| if (response_current_section_ == ResponseMessageSection::kEnd) { | |
| return absl::InvalidArgumentError("Encrypting is marked as invalid."); | |
| } | |
| absl::StatusOr<std::string> response_chunk = | |
| EncryptResponseChunk(plaintext_payload, is_final_chunk); | |
| if (!response_chunk.ok()) { | |
| response_current_section_ = ResponseMessageSection::kEnd; | |
| } | |
| return response_chunk; | |
| } | |
| absl::StatusOr<std::string> ChunkedObliviousHttpGateway::EncryptResponseChunk( | |
| absl::string_view plaintext_payload, bool is_final_chunk) { | |
| if (response_chunk_counter_.has_value() && | |
| response_chunk_counter_->LimitExceeded()) { | |
| return absl::InternalError( | |
| "Response chunk counter has exceeded the maximum allowed value."); | |
| } | |
| if (!oblivious_http_request_context_.has_value()) { | |
| return absl::InternalError( | |
| "HPKE context has not been derived from an encrypted request."); | |
| } | |
| if (!aead_context_data_.has_value()) { | |
| QUICHE_ASSIGN_OR_RETURN( | |
| ObliviousHttpResponse::CommonAeadParamsResult aead_params, | |
| ObliviousHttpResponse::GetCommonAeadParams( | |
| *oblivious_http_request_context_)); | |
| // secret_len represents max(Nn, Nk)) | |
| response_nonce_ = std::string(aead_params.secret_len, '\0'); | |
| quiche_random_->RandBytes(response_nonce_.data(), response_nonce_.size()); | |
| QUICHE_ASSIGN_OR_RETURN( | |
| ObliviousHttpResponse::AeadContextData aead_context_data, | |
| ObliviousHttpResponse::GetAeadContextData( | |
| *oblivious_http_request_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)); | |
| } | |
| if (!response_chunk_counter_.has_value()) { | |
| return absl::InternalError( | |
| "Response chunk counter has not been initialized."); | |
| } | |
| QUICHE_ASSIGN_OR_RETURN( | |
| std::string encrypted_data, | |
| ObliviousHttpResponse::EncryptChunk( | |
| *oblivious_http_request_context_, *aead_context_data_, | |
| plaintext_payload, response_chunk_counter_->GetChunkNonce(), | |
| is_final_chunk)); | |
| absl::string_view maybe_nonce; | |
| if (response_current_section_ == ResponseMessageSection::kNonce) { | |
| maybe_nonce = response_nonce_; | |
| response_current_section_ = ResponseMessageSection::kChunk; | |
| } | |
| uint8_t chunk_var_int_length = | |
| QuicheDataWriter::GetVarInt62Len(encrypted_data.size()); | |
| uint64_t chunk_var_int = encrypted_data.size(); | |
| if (is_final_chunk) { | |
| response_current_section_ = ResponseMessageSection::kEnd; | |
| chunk_var_int_length = | |
| QuicheDataWriter::GetVarInt62Len(kFinalChunkIndicator); | |
| // encrypted_data is guaranteed to be non-empty, so chunk_var_int_length | |
| // should never be 0. | |
| if (chunk_var_int_length == 0) { | |
| return absl::InvalidArgumentError( | |
| "Encrypted data is too large to be represented as a varint."); | |
| } | |
| chunk_var_int = kFinalChunkIndicator; | |
| } | |
| std::string response_buffer( | |
| maybe_nonce.size() + chunk_var_int_length + encrypted_data.size(), '\0'); | |
| QuicheDataWriter writer(response_buffer.size(), response_buffer.data()); | |
| if (!writer.WriteStringPiece(maybe_nonce)) { | |
| return absl::InternalError("Failed to write response nonce to buffer."); | |
| } | |
| if (!writer.WriteVarInt62(chunk_var_int)) { | |
| return absl::InternalError("Failed to write chunk to buffer."); | |
| } | |
| if (!writer.WriteStringPiece(encrypted_data)) { | |
| return absl::InternalError("Failed to write encrypted data to buffer."); | |
| } | |
| if (writer.remaining() != 0) { | |
| return absl::InternalError("Failed to write all data."); | |
| } | |
| response_chunk_counter_->Increment(); | |
| return response_buffer; | |
| } | |
| } // namespace quiche |