297 lines
7.7 KiB
C++
297 lines
7.7 KiB
C++
|
// 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
|