tls.cpp

1
2
3
4
5
#include <openssl/err.h>
#include <openssl/ssl.h>

#include <felspar/io/tls.hpp>
#include <felspar/io/write.hpp>

felspar::io::tls::impl

11
12
13
struct felspar::io::tls::impl {
    impl(posix::fd f)
    : ctx{SSL_CTX_new(TLS_method())}, ssl{SSL_new(ctx)}, fd{std::move(f)} {

TODO There should be some error handling here

15
        BIO_new_bio_pair(&ib, 0, &nb, 0);

Give the internal BIO to ssl

17
18
19
20
21
22
23
24
25
        SSL_set_bio(ssl, ib, ib);
    }
    ~impl() {
        if (nb) { BIO_free(nb); }
        if (ssl) { SSL_free(ssl); }
        if (ctx) { SSL_CTX_free(ctx); }
    }
    SSL_CTX *ctx = nullptr;
    SSL *ssl = nullptr;

ib is the internal BIO and nb is the network BIO

27
28
    BIO *ib = nullptr, *nb = nullptr;
    posix::fd fd;

A data buffer that we'll use whilst sending/receiving data. The TLS record size is 16KB so we make this just a little bit bigger.

34
35
36
37
38
39
40
41
    std::array<std::byte, (17 << 10)> buffer;

    std::string ssl_error(int const result) const {
        auto const error = SSL_get_error(ssl, result);
        switch (error) {
        default: return "Unknown error " + std::to_string(error);
        }
    }

Loop for handling read/write requests when trying to carry out an operation

 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
    template<typename Op>
    io::warden::task<int> service_operation(
            io::warden &warden,
            std::optional<std::chrono::nanoseconds> timeout,
            felspar::source_location const &loc,
            Op &&op) {
        while (true) {
            auto const result = op(*this);
            auto const error = SSL_get_error(ssl, result);
            switch (error) {
            case SSL_ERROR_NONE: co_return result;

            case SSL_ERROR_WANT_READ:
                co_await bio_read(warden, timeout, loc);
                [[fallthrough]];
            case SSL_ERROR_WANT_WRITE:
                co_await bio_write(warden, timeout, loc);
                break;

            default:
                throw felspar::stdexcept::runtime_error{
                        "Unknown openssl error " + std::to_string(error), loc};
            }
        }
    }

    io::warden::task<void> bio_read(
            io::warden &warden,
            std::optional<std::chrono::nanoseconds> timeout,
            felspar::source_location const &loc) {
        if (auto bytes = BIO_ctrl_pending(nb); bytes > buffer.size()) {
            throw felspar::stdexcept::logic_error{
                    "Pending read BIO buffer too small"};
        } else if (bytes) {
            if (auto const read_int = BIO_read(nb, buffer.data(), bytes);
                read_int <= 0) {
                throw felspar::stdexcept::runtime_error{
                        "Error reading from BIO"};
            } else if (std::size_t const read_bytes = read_int;
                       read_bytes != bytes) {
                throw felspar::stdexcept::runtime_error{
                        "Reading BIO read bytes mismatch"};
            } else {
                co_await felspar::io::write_all(
                        warden, fd, buffer.data(), bytes, timeout, loc);
            }
        }
    }
    io::warden::task<std::size_t> bio_write(
            io::warden &warden,
            std::optional<std::chrono::nanoseconds> timeout,
            felspar::source_location const &loc) {
        auto const bytes = co_await warden.read_some(fd, buffer, timeout, loc);
        if (auto const written_int = BIO_write(nb, buffer.data(), bytes);
            written_int <= 0) {
            throw felspar::stdexcept::runtime_error{"Error writing to BIO"};
        } else if (std::size_t const written_bytes = written_int;
                   written_bytes != bytes) {
            throw felspar::stdexcept::runtime_error{
                    "Not all bytes written to BIO"};
        } else {
            co_return written_bytes;
        }
    }
};

felspar::io::tls

114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
felspar::io::tls::tls() = default;
felspar::io::tls::tls(tls &&) = default;
felspar::io::tls::~tls() = default;
felspar::io::tls &felspar::io::tls::operator=(tls &&) = default;

felspar::io::tls::tls(std::unique_ptr<impl> i) : p{std::move(i)} {}


auto felspar::io::tls::connect(
        io::warden &warden,
        char const *const sni_hostname,
        sockaddr const *addr,
        socklen_t addrlen,
        std::optional<std::chrono::nanoseconds> timeout,
        felspar::source_location const &loc) -> warden::task<tls> {
    posix::fd fd = warden.create_socket(AF_INET, SOCK_STREAM, 0);
    co_await warden.connect(fd, addr, addrlen, timeout, loc);

    auto i = std::make_unique<impl>(std::move(fd));
    SSL_set_tlsext_host_name(i->ssl, sni_hostname);
    co_await i->service_operation(
            warden, timeout, loc, [](impl &i) { return SSL_connect(i.ssl); });

    co_return tls{std::move(i)};
}


auto felspar::io::tls::read_some(
        io::warden &warden,
        std::span<std::byte> const s,
        std::optional<std::chrono::nanoseconds> const timeout,
        felspar::source_location const &loc) -> warden::task<std::size_t> {
    int const ret =
            co_await p->service_operation(warden, timeout, loc, [s](impl &i) {
                return SSL_read(i.ssl, s.data(), s.size());
            });
    if (ret <= 0) {
        throw felspar::stdexcept::runtime_error{
                "Error performing SSL_read: " + p->ssl_error(ret), loc};
    } else {
        co_return static_cast<std::size_t>(ret);
    }
}


auto felspar::io::tls::write_some(
        io::warden &warden,
        std::span<std::byte const> const s,
        std::optional<std::chrono::nanoseconds> const timeout,
        felspar::source_location const &loc) -> warden::task<std::size_t> {
    int const ret =
            co_await p->service_operation(warden, timeout, loc, [s](impl &i) {
                return SSL_write(i.ssl, s.data(), s.size());
            });
    if (ret <= 0) {
        throw felspar::stdexcept::runtime_error{
                "Error performing SSL_write", loc};
    } else {
        co_return static_cast<std::size_t>(ret);
    }
}