| // Copyright 2021 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_tls_client/test/test_server.h" |
| |
| #include <openssl/bio.h> |
| #include <openssl/pem.h> |
| #include <openssl/ssl.h> |
| |
| #include <cstring> |
| |
| #include "pw_log/log.h" |
| #include "pw_result/result.h" |
| #include "pw_status/status.h" |
| #include "pw_stream/stream.h" |
| |
| namespace { |
| |
| int TestBioNew(BIO* bio) { |
| bio->init = 1; |
| return 1; |
| } |
| |
| long TestBioCtrl(BIO*, int, long, void*) { return 1; } |
| |
| int TestBioFree(BIO*) { return 1; } |
| |
| } // namespace |
| |
| namespace pw::tls_client::test { |
| |
| Result<X509*> ParseDerCertificate(ConstByteSpan cert) { |
| BIO* bio = BIO_new_mem_buf(cert.data(), cert.size()); |
| if (!bio) { |
| return Status::Internal(); |
| } |
| |
| X509* x509 = d2i_X509_bio(bio, nullptr); |
| if (x509 == nullptr) { |
| PW_LOG_DEBUG("Failed to parse x509"); |
| BIO_free(bio); |
| return Status::Internal(); |
| } |
| |
| BIO_free(bio); |
| return x509; |
| } |
| |
| StatusWithSize FixedSizeFIFOBuffer::DoRead(ByteSpan dest) { |
| size_t to_read = std::min(current_size_, dest.size()); |
| memcpy(dest.data(), buffer_.data(), to_read); |
| // Push out the read data. |
| memmove(buffer_.data(), buffer_.data() + to_read, current_size_ - to_read); |
| current_size_ -= to_read; |
| return StatusWithSize(to_read); |
| } |
| |
| Status FixedSizeFIFOBuffer::DoWrite(ConstByteSpan data) { |
| if (data.size() + current_size_ > buffer_.size()) { |
| PW_LOG_ERROR("Write overflow. Buffer is too small."); |
| return Status::ResourceExhausted(); |
| } |
| |
| // Append incoming data at the end. |
| memcpy(buffer_.begin() + current_size_, data.data(), data.size()); |
| current_size_ += data.size(); |
| return OkStatus(); |
| } |
| |
| InMemoryTestServer::InMemoryTestServer(ByteSpan input_buffer, |
| ByteSpan output_buffer) |
| : input_buffer_(input_buffer), output_buffer_(output_buffer) {} |
| |
| int InMemoryTestServer::BioRead(BIO* bio, char* out, int output_length) { |
| auto server = static_cast<InMemoryTestServer*>(bio->ptr); |
| auto read = server->input_buffer_.Read(std::as_writable_bytes( |
| std::span{out, static_cast<size_t>(output_length)})); |
| if (!read.ok()) { |
| server->last_bio_status_ = read.status(); |
| return -1; |
| } |
| if (read.value().empty()) { |
| BIO_set_retry_read(bio); |
| return -1; |
| } |
| return static_cast<int>(read.value().size()); |
| } |
| |
| int InMemoryTestServer::BioWrite(BIO* bio, |
| const char* input, |
| int input_length) { |
| auto server = static_cast<InMemoryTestServer*>(bio->ptr); |
| if (auto status = server->output_buffer_.Write( |
| std::as_bytes(std::span{input, static_cast<size_t>(input_length)})); |
| !status.ok()) { |
| server->last_bio_status_ = status; |
| return -1; |
| } |
| |
| return input_length; |
| } |
| |
| Status InMemoryTestServer::Initialize(ConstByteSpan key, |
| ConstByteSpan cert, |
| std::span<const ConstByteSpan> chains) { |
| input_buffer_.clear(); |
| output_buffer_.clear(); |
| is_handshake_done_ = false; |
| |
| ctx_ = bssl::UniquePtr<SSL_CTX>(SSL_CTX_new(TLS_method())); |
| if (!ctx_) { |
| return Status::Internal(); |
| } |
| |
| if (auto status = LoadPrivateKey(key); !status.ok()) { |
| return status; |
| } |
| |
| if (auto status = LoadCertificate(cert); !status.ok()) { |
| return status; |
| } |
| |
| if (auto status = LoadCAChain(chains); !status.ok()) { |
| return status; |
| } |
| |
| ssl_ = bssl::UniquePtr<SSL>(SSL_new(ctx_.get())); |
| if (!ssl_) { |
| return Status::Internal(); |
| } |
| |
| static const BIO_METHOD bio_method = { |
| BIO_TYPE_MEM, |
| "bio boringssl test server", |
| InMemoryTestServer::BioWrite, |
| InMemoryTestServer::BioRead, |
| nullptr, // puts |
| nullptr, // gets |
| TestBioCtrl, // ctrl |
| TestBioNew, |
| TestBioFree, // free |
| nullptr // callback_ctrl |
| }; |
| |
| BIO* bio = BIO_new(&bio_method); |
| if (!bio) { |
| return Status::Internal(); |
| } |
| |
| bio->ptr = this; |
| |
| SSL_set_bio(ssl_.get(), bio, bio); |
| return OkStatus(); |
| } |
| |
| Status InMemoryTestServer::LoadPrivateKey(ConstByteSpan key) { |
| BIO* bio = BIO_new_mem_buf(key.data(), key.size()); |
| if (!bio) { |
| return Status::Internal(); |
| } |
| |
| // Requires PEM format. |
| EVP_PKEY* pkey = d2i_PrivateKey_bio(bio, nullptr); |
| int ret = SSL_CTX_use_PrivateKey(ctx_.get(), pkey); |
| if (ret != 1) { |
| BIO_free(bio); |
| PW_LOG_DEBUG("Failed to load private key for test server"); |
| return Status::Internal(); |
| } |
| |
| EVP_PKEY_free(pkey); |
| BIO_free(bio); |
| return OkStatus(); |
| } |
| |
| Status InMemoryTestServer::LoadCertificate(ConstByteSpan cert) { |
| auto res = ParseDerCertificate(cert); |
| if (!res.ok()) { |
| return res.status(); |
| } |
| |
| int ret = SSL_CTX_use_certificate(ctx_.get(), res.value()); |
| if (ret != 1) { |
| X509_free(res.value()); |
| PW_LOG_DEBUG("Failed to user server certificate %d", ret); |
| return Status::Internal(); |
| } |
| |
| X509_free(res.value()); |
| return OkStatus(); |
| } |
| |
| Status InMemoryTestServer::LoadCAChain(std::span<const ConstByteSpan> chains) { |
| for (auto cert : chains) { |
| auto res = ParseDerCertificate(cert); |
| if (!res.ok()) { |
| return res.status(); |
| } |
| |
| int ret = SSL_CTX_add0_chain_cert(ctx_.get(), res.value()); |
| if (ret != 1) { |
| X509_free(res.value()); |
| PW_LOG_DEBUG("Failed to add certificate to chain %d", ret); |
| return Status::Internal(); |
| } |
| } |
| return OkStatus(); |
| } |
| |
| bool InMemoryTestServer::ClientShutdownReceived() { |
| return SSL_get_shutdown(ssl_.get()) & SSL_RECEIVED_SHUTDOWN; |
| } |
| |
| Status InMemoryTestServer::ProcessPackets() { |
| // Process handshake if it has not been completed. |
| if (!is_handshake_done_) { |
| int ret = SSL_accept(ssl_.get()); |
| if (ret != 1) { |
| int ssl_err = SSL_get_error(ssl_.get(), ret); |
| if (ssl_err != SSL_ERROR_WANT_READ) { |
| PW_LOG_DEBUG("Error while server accepting, %d, %d", ssl_err, ret); |
| return Status::Internal(); |
| } |
| } else { |
| // handshake complete. |
| is_handshake_done_ = true; |
| } |
| } |
| |
| // Hanshake may be completed above and there may already be application data. |
| if (is_handshake_done_) { |
| static std::array<std::byte, 1024> buf; |
| while (true) { |
| int ssl_ret = SSL_read(ssl_.get(), buf.data(), buf.size()); |
| |
| if (ssl_ret == 0) { |
| // All input has been processed. |
| break; |
| } |
| |
| if (ssl_ret < 0) { |
| // An error may have occured. |
| int ssl_err = SSL_get_error(ssl_.get(), ssl_ret); |
| if (ssl_err == SSL_ERROR_WANT_READ) { |
| // Need to wait for client to finish sending data. Non-blocking |
| // return. |
| break; |
| } |
| |
| PW_LOG_DEBUG("Error while server reading"); |
| return Status::Internal(); |
| } |
| |
| // Echo the message |
| int write_status = SSL_write(ssl_.get(), buf.data(), ssl_ret); |
| if (write_status <= 0) { |
| PW_LOG_DEBUG("Failed to write for test server"); |
| return Status::Internal(); |
| } |
| } |
| } |
| |
| return OkStatus(); |
| } |
| |
| StatusWithSize InMemoryTestServer::DoRead(ByteSpan dest) { |
| auto res = output_buffer_.Read(dest); |
| if (!res.ok()) { |
| return StatusWithSize(res.status(), 0); |
| } |
| |
| return StatusWithSize(res.value().size()); |
| } |
| |
| Status InMemoryTestServer::DoWrite(ConstByteSpan data) { |
| auto status = input_buffer_.Write(data); |
| if (!status.ok()) { |
| return status; |
| } |
| |
| // Invoke the server to process the data. |
| return ProcessPackets(); |
| } |
| |
| } // namespace pw::tls_client::test |