/* * (C) 2014,2015 Jack Lloyd * 2016 Matthias Gierlings * 2017 René Korthaus, Rohde & Schwarz Cybersecurity * * Botan is released under the Simplified BSD License (see license.txt) */ #include "cli.h" #if defined(BOTAN_HAS_TLS) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) && defined(BOTAN_TARGET_OS_HAS_SOCKETS) #include #include #include #include #include #include #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER) #include #endif #include #include #include "socket_utils.h" #include "tls_helpers.h" namespace Botan_CLI { class TLS_Client final : public Command, public Botan::TLS::Callbacks { public: TLS_Client() : Command("tls_client host --port=443 --print-certs --policy=default " "--skip-system-cert-store --trusted-cas= " "--session-db= --session-db-pass= --next-protocols= --type=tcp " "--client-cert= --client-cert-key=") { init_sockets(); } ~TLS_Client() override { stop_sockets(); } TLS_Client(const TLS_Client& other) = delete; TLS_Client(TLS_Client&& other) = delete; TLS_Client& operator=(const TLS_Client& other) = delete; TLS_Client& operator=(TLS_Client&& other) = delete; std::string group() const override { return "tls"; } std::string description() const override { return "Connect to a host using TLS/DTLS"; } void go() override { std::unique_ptr session_mgr; const std::string sessions_db = get_arg("session-db"); const std::string host = get_arg("host"); const uint16_t port = get_arg_u16("port"); const std::string transport = get_arg("type"); const std::string next_protos = get_arg("next-protocols"); const bool use_system_cert_store = flag_set("skip-system-cert-store") == false; const std::string trusted_CAs = get_arg("trusted-cas"); if(!sessions_db.empty()) { #if defined(BOTAN_HAS_TLS_SQLITE3_SESSION_MANAGER) const std::string sessions_passphrase = get_passphrase_arg("Session DB passphrase", "session-db-pass"); session_mgr.reset(new Botan::TLS::Session_Manager_SQLite(sessions_passphrase, rng(), sessions_db)); #else error_output() << "Ignoring session DB file, sqlite not enabled\n"; #endif } if(!session_mgr) { session_mgr.reset(new Botan::TLS::Session_Manager_In_Memory(rng())); } auto policy = load_tls_policy(get_arg("policy")); if(transport != "tcp" && transport != "udp") { throw CLI_Usage_Error("Invalid transport type '" + transport + "' for TLS"); } const bool use_tcp = (transport == "tcp"); const std::vector protocols_to_offer = Command::split_on(next_protos, ','); Botan::TLS::Protocol_Version version = use_tcp ? Botan::TLS::Protocol_Version::TLS_V12 : Botan::TLS::Protocol_Version::DTLS_V12; if(!policy) { policy.reset(new Botan::TLS::Policy); } if(policy->acceptable_protocol_version(version) == false) { throw CLI_Usage_Error("The policy specified does not allow the requested TLS version"); } struct sockaddr_storage addrbuf; std::string hostname; if(!host.empty() && inet_pton(AF_INET, host.c_str(), &addrbuf) != 1 && inet_pton(AF_INET6, host.c_str(), &addrbuf) != 1) { hostname = host; } m_sockfd = connect_to_host(host, port, use_tcp); const auto client_crt_path = get_arg_maybe("client-cert"); const auto client_key_path = get_arg_maybe("client-cert-key"); Basic_Credentials_Manager creds(use_system_cert_store, trusted_CAs, client_crt_path, client_key_path); Botan::TLS::Client client(*this, *session_mgr, creds, *policy, rng(), Botan::TLS::Server_Information(hostname, port), version, protocols_to_offer); bool first_active = true; while(!client.is_closed()) { fd_set readfds; FD_ZERO(&readfds); FD_SET(m_sockfd, &readfds); if(client.is_active()) { FD_SET(STDIN_FILENO, &readfds); if(first_active && !protocols_to_offer.empty()) { std::string app = client.application_protocol(); if(!app.empty()) { output() << "Server choose protocol: " << client.application_protocol() << "\n"; } first_active = false; } } struct timeval timeout = { 1, 0 }; ::select(static_cast(m_sockfd + 1), &readfds, nullptr, nullptr, &timeout); if(FD_ISSET(m_sockfd, &readfds)) { uint8_t buf[4 * 1024] = { 0 }; ssize_t got = ::read(m_sockfd, buf, sizeof(buf)); if(got == 0) { output() << "EOF on socket\n"; break; } else if(got == -1) { output() << "Socket error: " << errno << " " << err_to_string(errno) << "\n"; continue; } client.received_data(buf, got); } if(FD_ISSET(STDIN_FILENO, &readfds)) { uint8_t buf[1024] = { 0 }; ssize_t got = read(STDIN_FILENO, buf, sizeof(buf)); if(got == 0) { output() << "EOF on stdin\n"; client.close(); break; } else if(got == -1) { output() << "Stdin error: " << errno << " " << err_to_string(errno) << "\n"; continue; } if(got == 2 && buf[1] == '\n') { char cmd = buf[0]; if(cmd == 'R' || cmd == 'r') { output() << "Client initiated renegotiation\n"; client.renegotiate(cmd == 'R'); } else if(cmd == 'Q') { output() << "Client initiated close\n"; client.close(); } } else { client.send(buf, got); } } if(client.timeout_check()) { output() << "Timeout detected\n"; } } ::close(m_sockfd); } private: static socket_type connect_to_host(const std::string& host, uint16_t port, bool tcp) { addrinfo hints; Botan::clear_mem(&hints, 1); hints.ai_family = AF_UNSPEC; hints.ai_socktype = tcp ? SOCK_STREAM : SOCK_DGRAM; addrinfo* res, *rp = nullptr; if(::getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res) != 0) { throw CLI_Error("getaddrinfo failed for " + host); } socket_type fd = 0; for(rp = res; rp != nullptr; rp = rp->ai_next) { fd = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if(fd == invalid_socket()) { continue; } if(::connect(fd, rp->ai_addr, static_cast(rp->ai_addrlen)) != 0) { ::close(fd); continue; } break; } ::freeaddrinfo(res); if(rp == nullptr) // no address succeeded { throw CLI_Error("connect failed"); } return fd; } void tls_verify_cert_chain( const std::vector& cert_chain, const std::vector>& ocsp, const std::vector& trusted_roots, Botan::Usage_Type usage, const std::string& hostname, const Botan::TLS::Policy& policy) override { if(cert_chain.empty()) { throw Botan::Invalid_Argument("Certificate chain was empty"); } Botan::Path_Validation_Restrictions restrictions( policy.require_cert_revocation_info(), policy.minimum_signature_strength()); auto ocsp_timeout = std::chrono::milliseconds(1000); Botan::Path_Validation_Result result = Botan::x509_path_validate( cert_chain, restrictions, trusted_roots, hostname, usage, tls_current_timestamp(), ocsp_timeout, ocsp); output() << "Certificate validation status: " << result.result_string() << "\n"; if(result.successful_validation()) { auto status = result.all_statuses(); if(!status.empty() && status[0].count(Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD)) { output() << "Valid OCSP response for this server\n"; } } } bool tls_session_established(const Botan::TLS::Session& session) override { output() << "Handshake complete, " << session.version().to_string() << " using " << session.ciphersuite().to_string() << "\n"; if(!session.session_id().empty()) { output() << "Session ID " << Botan::hex_encode(session.session_id()) << "\n"; } if(!session.session_ticket().empty()) { output() << "Session ticket " << Botan::hex_encode(session.session_ticket()) << "\n"; } if(flag_set("print-certs")) { const std::vector& certs = session.peer_certs(); for(size_t i = 0; i != certs.size(); ++i) { output() << "Certificate " << i + 1 << "/" << certs.size() << "\n"; output() << certs[i].to_string(); output() << certs[i].PEM_encode(); } } return true; } static void dgram_socket_write(int sockfd, const uint8_t buf[], size_t length) { auto r = ::send(sockfd, buf, length, MSG_NOSIGNAL); if(r == -1) { throw CLI_Error("Socket write failed errno=" + std::to_string(errno)); } } void tls_emit_data(const uint8_t buf[], size_t length) override { size_t offset = 0; while(length) { ssize_t sent = ::send(m_sockfd, buf + offset, length, MSG_NOSIGNAL); if(sent == -1) { if(errno == EINTR) { sent = 0; } else { throw CLI_Error("Socket write failed errno=" + std::to_string(errno)); } } offset += sent; length -= sent; } } void tls_alert(Botan::TLS::Alert alert) override { output() << "Alert: " << alert.type_string() << "\n"; } void tls_record_received(uint64_t /*seq_no*/, const uint8_t buf[], size_t buf_size) override { for(size_t i = 0; i != buf_size; ++i) { output() << buf[i]; } } std::vector tls_sign_message( const Botan::Private_Key& key, Botan::RandomNumberGenerator& rng, const std::string& emsa, Botan::Signature_Format format, const std::vector& msg) override { output() << "Performing client authentication\n"; return Botan::TLS::Callbacks::tls_sign_message(key, rng, emsa, format, msg); } socket_type m_sockfd = invalid_socket(); }; BOTAN_REGISTER_COMMAND("tls_client", TLS_Client); } #endif