diff options
Diffstat (limited to 'ssh-agent-filter.C')
| -rw-r--r-- | ssh-agent-filter.C | 341 |
1 files changed, 177 insertions, 164 deletions
diff --git a/ssh-agent-filter.C b/ssh-agent-filter.C index b6d906b..8a2772d 100644 --- a/ssh-agent-filter.C +++ b/ssh-agent-filter.C @@ -22,8 +22,8 @@ #include <boost/program_options.hpp> namespace po = boost::program_options; -#include <boost/filesystem.hpp> -namespace fs = boost::filesystem; +#include <filesystem> +namespace fs = std::filesystem; #include <boost/iostreams/stream.hpp> #include <boost/iostreams/device/array.hpp> @@ -61,8 +61,8 @@ using std::pair; #include <thread> #include <mutex> -using std::mutex; -using std::lock_guard; + +#include <optional> #include <chrono> @@ -77,6 +77,7 @@ using std::lock_guard; #include <sys/un.h> #include <sys/wait.h> #include <sysexits.h> +#include <nettle/version.h> #include <nettle/md5.h> #include <nettle/base64.h> #include <nettle/base16.h> @@ -88,6 +89,9 @@ using std::lock_guard; #ifndef SOCK_CLOEXEC #define SOCK_CLOEXEC 0 #endif +#ifndef SOCK_NONBLOCK +#define SOCK_NONBLOCK 0 +#endif vector<string> allowed_b64; vector<string> allowed_md5; @@ -101,57 +105,74 @@ bool debug{false}; bool all_confirmed{false}; string saf_name; fs::path path; -mutex fd_fork_mutex; +std::mutex fd_fork_mutex; string md5_hex (string const & s) { struct md5_ctx ctx; md5_init(&ctx); md5_update(&ctx, s.size(), reinterpret_cast<uint8_t const *>(s.data())); - uint8_t bin[MD5_DIGEST_SIZE]; - md5_digest(&ctx, MD5_DIGEST_SIZE, bin); - char hex[BASE16_ENCODE_LENGTH(MD5_DIGEST_SIZE)]; - base16_encode_update(hex, MD5_DIGEST_SIZE, bin); - return {hex, sizeof(hex)}; + std::array<uint8_t, MD5_DIGEST_SIZE> bin; +#if (NETTLE_VERSION_MAJOR >= 4) + md5_digest(&ctx, bin.data()); +#else + md5_digest(&ctx, MD5_DIGEST_SIZE, bin.data()); +#endif + std::array<char, BASE16_ENCODE_LENGTH(MD5_DIGEST_SIZE)> hex; + base16_encode_update(hex.data(), MD5_DIGEST_SIZE, bin.data()); + return {begin(hex), end(hex)}; } string base64_encode (string const & s) { - char b64[BASE64_ENCODE_RAW_LENGTH(s.size())]; - base64_encode_raw(b64, s.size(), reinterpret_cast<uint8_t const *>(s.data())); - return {b64, sizeof(b64)}; + std::vector<char> b64(BASE64_ENCODE_RAW_LENGTH(s.size())); + base64_encode_raw(b64.data(), s.size(), reinterpret_cast<uint8_t const *>(s.data())); + return {begin(b64), end(b64)}; } -void cloexec (int fd) { - if (fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC)) +template <int write_cmd, int set_flags, int clear_flags> +void alter_fd_flags (int const fd) { + static_assert(write_cmd == F_SETFD || write_cmd == F_SETFL, "unsupported fcntl() operation"); + constexpr int read_cmd = (write_cmd == F_SETFD) ? F_GETFD : F_GETFL; + static_assert((set_flags & clear_flags) == 0, "overlap in set and clear"); + + int const current_flags = fcntl(fd, read_cmd); + if (current_flags == -1) throw system_error(errno, system_category(), "fcntl"); + int const new_flags = (current_flags | set_flags) & ~clear_flags; + if (current_flags == new_flags) + return; + if (fcntl(fd, write_cmd, new_flags)) + throw system_error(errno, system_category(), "fcntl"); +} + +int make_cloexec_socket (int const address_family, int const type, int const protocol) { + int sock; + std::lock_guard lock{fd_fork_mutex}; + if ((sock = socket(address_family, type | SOCK_CLOEXEC, protocol)) == -1) + throw system_error(errno, system_category(), "socket"); + alter_fd_flags<F_SETFD, FD_CLOEXEC, 0>(sock); + return sock; } -void arm(std::ios & stream) { +void arm_exceptions (std::ios & stream) { stream.exceptions(stream.badbit | stream.failbit); } int make_upstream_agent_conn () { char const * path; - int sock; - struct sockaddr_un addr; if (!(path = getenv("SSH_AUTH_SOCK"))) throw invalid_argument("no $SSH_AUTH_SOCK"); - { - lock_guard<mutex> lock{fd_fork_mutex}; - if ((sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) - throw system_error(errno, system_category(), "socket"); - cloexec(sock); - } + int const sock = make_cloexec_socket(AF_UNIX, SOCK_STREAM, 0); - addr.sun_family = AF_UNIX; + struct sockaddr_un addr{AF_UNIX, {}}; - if (strlen(path) >= sizeof(addr.sun_path)) + if (auto len = strlen(path); len < sizeof(addr.sun_path)) + std::copy(path, path + len, addr.sun_path); + else throw length_error("$SSH_AUTH_SOCK too long"); - strcpy(addr.sun_path, path); - if (connect(sock, reinterpret_cast<struct sockaddr const *>(&addr), sizeof(addr))) throw system_error(errno, system_category(), "connect"); @@ -159,26 +180,17 @@ int make_upstream_agent_conn () { } int make_listen_sock () { - int sock; - struct sockaddr_un addr; - - { - lock_guard<mutex> lock{fd_fork_mutex}; - if ((sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) - throw system_error(errno, system_category(), "socket"); - cloexec(sock); - } + int const sock = make_cloexec_socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0); - if (fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK)) - throw system_error(errno, system_category(), "fcntl"); + alter_fd_flags<F_SETFL, O_NONBLOCK, 0>(sock); - addr.sun_family = AF_UNIX; + struct sockaddr_un addr{AF_UNIX, {}}; - if (path.native().length() >= sizeof(addr.sun_path)) + if (path.native().length() < sizeof(addr.sun_path)) + std::copy(path.native().begin(), path.native().end(), addr.sun_path); + else throw length_error("path for listen socket too long"); - strcpy(addr.sun_path, path.c_str()); - if (bind(sock, reinterpret_cast<struct sockaddr const *>(&addr), sizeof(addr))) throw system_error(errno, system_category(), "bind"); @@ -188,6 +200,14 @@ int make_listen_sock () { return sock; } +rfc4251::string ask_upstream_agent (rfc4251::string const & request) { + io::stream<io::file_descriptor> agent{make_upstream_agent_conn(), io::close_handle}; + arm_exceptions(agent); + + agent << request; + return rfc4251::string{agent}; +} + void parse_cmdline (int const argc, char const * const * const argv) { po::options_description opts{"Options"}; opts.add_options() @@ -200,7 +220,7 @@ void parse_cmdline (int const argc, char const * const * const argv) { ("help,h", "print this help message") ("key,k", po::value(&allowed_b64), "key specified by base64-encoded pubkey") ("key-confirmed,K", po::value(&confirmed_b64), "key specified by base64-encoded pubkey, with confirmation") - ("name,n", po::value(&saf_name), "name for this instance of ssh-agent-filter, for confirmation puposes") + ("name,n", po::value(&saf_name), "name for this instance of ssh-agent-filter, for confirmation purposes") ("version,V", "print version information") ; po::variables_map config; @@ -235,28 +255,24 @@ void parse_cmdline (int const argc, char const * const * const argv) { } void setup_filters () { - io::stream<io::file_descriptor> agent{make_upstream_agent_conn(), io::close_handle}; - arm(agent); - - agent << rfc4251::string{string{SSH2_AGENTC_REQUEST_IDENTITIES}}; - rfc4251::string answer{agent}; + auto const answer = ask_upstream_agent({SSH2_AGENTC_REQUEST_IDENTITIES}); io::stream<io::array_source> answer_iss{answer.data(), answer.size()}; - arm(answer_iss); - rfc4251::byte resp_code{answer_iss}; + arm_exceptions(answer_iss); + rfc4251::byte const resp_code{answer_iss}; if (resp_code != SSH2_AGENT_IDENTITIES_ANSWER) throw runtime_error{"unexpected answer from ssh-agent"}; - rfc4251::uint32 keycount{answer_iss}; + rfc4251::uint32 const keycount{answer_iss}; for (uint32_t i = keycount; i; --i) { - rfc4251::string key{answer_iss}; - rfc4251::string comment{answer_iss}; + rfc4251::string key{answer_iss}; + rfc4251::string const comment{answer_iss}; - auto b64 = base64_encode(key); + auto const b64 = base64_encode(key); if (debug) clog << b64 << endl; - auto md5 = md5_hex(key); + auto const md5 = md5_hex(key); if (debug) clog << md5 << endl; - string comm(comment); + string comm{comment}; if (debug) clog << comm << endl; bool allow{false}; @@ -308,16 +324,16 @@ bool confirm (string const & question) { sap = "ssh-askpass"; pid_t pid; { - lock_guard<mutex> lock{fd_fork_mutex}; + std::lock_guard lock{fd_fork_mutex}; pid = fork(); } if (pid < 0) throw runtime_error("fork()"); if (pid == 0) { // child - char const * args[3] = { sap, question.c_str(), nullptr }; + std::array<char const *, 3> const args{ sap, question.c_str(), nullptr }; // see execvp(3p) for cast rationale - execvp(sap, const_cast<char * const *>(args)); + execvp(sap, const_cast<char * const *>(args.data())); throw system_error(errno, system_category(), "exec"); } else { // parent @@ -326,97 +342,99 @@ bool confirm (string const & question) { } } -bool dissect_auth_data_ssh_cert (rfc4251::string const & data, string & request_description) try { +std::optional<string> dissect_auth_data_ssh_cert (rfc4251::string const & data) try { io::stream<io::array_source> datastream{data.data(), data.size()}; - arm(datastream); + arm_exceptions(datastream); + string request_description{}; // Format specified in https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=1.13 - rfc4251::string keytype{datastream}; + rfc4251::string const keytype{datastream}; std::string keytype_str{keytype}; { // check for and remove suffix to get the base keytype std::string const suffix{"-cert-v01@openssh.com"}; if (keytype_str.length() <= suffix.length()) - return false; + return {}; auto suffix_start = keytype_str.end() - suffix.length(); if (!std::equal(suffix.begin(), suffix.end(), suffix_start)) - return false; + return {}; keytype_str.erase(suffix_start, keytype_str.end()); } - rfc4251::string nonce{datastream}; + rfc4251::string const nonce{datastream}; std::ostringstream key_to_be_signed{}; if (keytype_str == "ssh-rsa") { - rfc4251::string e{datastream}; - rfc4251::string n{datastream}; + rfc4251::mpint const e{datastream}; + rfc4251::mpint const n{datastream}; key_to_be_signed << rfc4251::string{keytype_str} << e << n; } else if (keytype_str == "ssh-dss") { - rfc4251::string p{datastream}; - rfc4251::string q{datastream}; - rfc4251::string g{datastream}; - rfc4251::string y{datastream}; + rfc4251::mpint const p{datastream}; + rfc4251::mpint const q{datastream}; + rfc4251::mpint const g{datastream}; + rfc4251::mpint const y{datastream}; key_to_be_signed << rfc4251::string{keytype_str} << p << q << g << y; } else if (keytype_str == "ecdsa-sha2-nistp256" || keytype_str == "ecdsa-sha2-nistp384" || keytype_str == "ecdsa-sha2-nistp521") { - rfc4251::string curve{datastream}; - rfc4251::string public_key{datastream}; + rfc4251::string const curve{datastream}; + rfc4251::string const public_key{datastream}; key_to_be_signed << rfc4251::string{keytype_str} << curve << public_key; } else if (keytype_str == "ssh-ed25519") { - rfc4251::string pk{datastream}; + rfc4251::string const pk{datastream}; key_to_be_signed << rfc4251::string{keytype_str} << pk; } else { - return false; + return {}; } - rfc4251::uint64 serial{datastream}; - rfc4251::uint32 type{datastream}; - rfc4251::string key_id{datastream}; - rfc4251::string valid_principals{datastream}; - rfc4251::uint64 valid_after{datastream}; - rfc4251::uint64 valid_before{datastream}; - rfc4251::string critical_options{datastream}; - rfc4251::string extensions{datastream}; - rfc4251::string reserved{datastream}; - rfc4251::string signature_key{datastream}; + rfc4251::uint64 const serial{datastream}; + rfc4251::uint32 const type{datastream}; + rfc4251::string const key_id{datastream}; + rfc4251::string const valid_principals{datastream}; + rfc4251::uint64 const valid_after{datastream}; + rfc4251::uint64 const valid_before{datastream}; + rfc4251::string const critical_options{datastream}; + rfc4251::string const extensions{datastream}; + rfc4251::string const reserved{datastream}; + rfc4251::string const signature_key{datastream}; request_description = "The request is for a certificate signature on key " + base64_encode(key_to_be_signed.str()) + "."; - return true; + return request_description; } catch (...) { - return false; + return {}; } -bool dissect_auth_data_ssh (rfc4251::string const & data, string & request_description) try { +std::optional<string> dissect_auth_data_ssh (rfc4251::string const & data) try { io::stream<io::array_source> datastream{data.data(), data.size()}; - arm(datastream); + arm_exceptions(datastream); + string request_description{}; // Format specified in RFC 4252 Section 7 - rfc4251::string session_identifier{datastream}; - rfc4251::byte requesttype{datastream}; - rfc4251::string username{datastream}; - rfc4251::string servicename{datastream}; - rfc4251::string publickeystring{datastream}; - rfc4251::boolean shouldbetrue{datastream}; - rfc4251::string publickeyalgorithm{datastream}; - rfc4251::string publickey{datastream}; + rfc4251::string const session_identifier{datastream}; + rfc4251::byte const requesttype{datastream}; + rfc4251::string const username{datastream}; + rfc4251::string const servicename{datastream}; + rfc4251::string const publickeystring{datastream}; + rfc4251::boolean const shouldbetrue{datastream}; + rfc4251::string const publickeyalgorithm{datastream}; + rfc4251::string const publickey{datastream}; request_description = "The request is for an ssh connection as user '" + string{username} + "' with service name '" + string{servicename} + "'."; if (string{servicename} == "pam_ssh_agent_auth") try { clog << base64_encode(session_identifier) << endl; io::stream<io::array_source> idstream{session_identifier.data(), session_identifier.size()}; - arm(idstream); + arm_exceptions(idstream); - rfc4251::uint32 type{idstream}; + rfc4251::uint32 const type{idstream}; if (type == 101) { // PAM_SSH_AGENT_AUTH_REQUESTv1 - rfc4251::string cookie{idstream}; - rfc4251::string user{idstream}; - rfc4251::string ruser{idstream}; - rfc4251::string pam_service{idstream}; - rfc4251::string pwd{idstream}; - rfc4251::string action{idstream}; - rfc4251::string hostname{idstream}; - rfc4251::uint64 timestamp{idstream}; + rfc4251::string const cookie{idstream}; + rfc4251::string const user{idstream}; + rfc4251::string const ruser{idstream}; + rfc4251::string const pam_service{idstream}; + rfc4251::string const pwd{idstream}; + rfc4251::string const action{idstream}; + rfc4251::string const hostname{idstream}; + rfc4251::uint64 const timestamp{idstream}; string singleuser{user}; if (user != ruser) @@ -427,14 +445,14 @@ bool dissect_auth_data_ssh (rfc4251::string const & data, string & request_descr additional += "' in '" + string{pwd}; io::stream<io::array_source> actionstream{action.data(), action.size()}; - arm(actionstream); + arm_exceptions(actionstream); - rfc4251::uint32 argc{actionstream}; + rfc4251::uint32 const argc{actionstream}; if (argc) { additional += " to run"; for (uint32_t i = argc; i; --i) { - rfc4251::string argv{actionstream}; + rfc4251::string const argv{actionstream}; additional += ' ' + string{argv}; } } @@ -450,34 +468,45 @@ bool dissect_auth_data_ssh (rfc4251::string const & data, string & request_descr } } catch (...) {} - return true; + return request_description; } catch (...) { - return false; + return {}; +} + +string describe_sign_request (rfc4251::string const & data_to_be_signed) { + auto const dissectors = { + dissect_auth_data_ssh_cert, + dissect_auth_data_ssh, + }; + std::optional<string> request_description; + for (auto const dissector : dissectors) + if (request_description = dissector(data_to_be_signed); request_description) + break; + + return request_description.value_or("The request format is unknown."); } + rfc4251::string handle_request (rfc4251::string const & r) { io::stream<io::array_source> request{r.data(), r.size()}; rfc4251::string ret; - io::stream<io::back_insert_device<vector<char>>> answer{ret.value}; - arm(request); - arm(answer); - rfc4251::byte request_code{request}; + io::stream<io::back_insert_device<vector<char>>> answer{ret.buf}; + arm_exceptions(request); + arm_exceptions(answer); + rfc4251::byte const request_code{request}; switch (request_code) { case SSH2_AGENTC_REQUEST_IDENTITIES: { - io::stream<io::file_descriptor> agent{make_upstream_agent_conn(), io::close_handle}; - arm(agent); - agent << rfc4251::string{string{SSH2_AGENTC_REQUEST_IDENTITIES}}; + auto const agent_answer = ask_upstream_agent({SSH2_AGENTC_REQUEST_IDENTITIES}); // temp to test key filtering when signing - //return rfc4251::string{agent}; - rfc4251::string agent_answer{agent}; + //return agent_answer; io::stream<io::array_source> agent_answer_iss{agent_answer.data(), agent_answer.size()}; - arm(agent_answer_iss); - rfc4251::byte answer_code{agent_answer_iss}; - rfc4251::uint32 keycount{agent_answer_iss}; + arm_exceptions(agent_answer_iss); + rfc4251::byte const answer_code{agent_answer_iss}; + rfc4251::uint32 const keycount{agent_answer_iss}; if (answer_code != SSH2_AGENT_IDENTITIES_ANSWER) throw runtime_error{"unexpected answer from ssh-agent"}; - vector<pair<rfc4251::string, rfc4251::string>> keys; + vector<pair<rfc4251::string const, rfc4251::string const>> keys; for (uint32_t i = keycount; i; --i) { rfc4251::string key{agent_answer_iss}; rfc4251::string comment{agent_answer_iss}; @@ -491,41 +520,24 @@ rfc4251::string handle_request (rfc4251::string const & r) { break; case SSH2_AGENTC_SIGN_REQUEST: { - rfc4251::string key{request}; - rfc4251::string data{request}; - rfc4251::uint32 flags{request}; + rfc4251::string const key{request}; + rfc4251::string const data_to_be_signed{request}; + rfc4251::uint32 const flags{request}; bool allow{false}; if (allowed_pubkeys.count(key)) allow = true; - else { - auto it = confirmed_pubkeys.find(key); - if (it != confirmed_pubkeys.end()) { - string request_description; - bool dissect_ok{false}; - if (!dissect_ok) - dissect_ok = dissect_auth_data_ssh_cert(data, request_description); - if (!dissect_ok) - dissect_ok = dissect_auth_data_ssh(data, request_description); - if (!dissect_ok) - request_description = "The request format is unknown."; - - string question = "Something behind the ssh-agent-filter"; - if (saf_name.length()) - question += " named '" + saf_name + "'"; - question += " requested use of the key named '" + it->second + "'.\n"; - question += request_description; - allow = confirm(question); - } + else if (auto it = confirmed_pubkeys.find(key); it != confirmed_pubkeys.end()) { + string question = "Something behind the ssh-agent-filter"; + if (saf_name.length()) + question += " named '" + saf_name + "'"; + question += " requested use of the key named '" + it->second + "'.\n"; + question += describe_sign_request(data_to_be_signed); + allow = confirm(question); } if (allow) { - io::stream<io::file_descriptor> agent{make_upstream_agent_conn(), io::close_handle}; - arm(agent); - rfc4251::string agent_answer; - - agent << r; - return rfc4251::string{agent}; + return ask_upstream_agent(r); } else answer << rfc4251::byte{SSH_AGENT_FAILURE}; } @@ -561,15 +573,14 @@ rfc4251::string handle_request (rfc4251::string const & r) { } void handle_client (int const sock) try { - if (fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) & ~O_NONBLOCK)) - throw system_error(errno, system_category(), "fcntl"); + alter_fd_flags<F_SETFL, 0, O_NONBLOCK>(sock); // we could use only one streambuf and iostream but when // switching from read to write an lseek call is made that // fails with ESPIPE and causes an exception io::stream<io::file_descriptor_source> client_in{sock, io::close_handle}; io::stream<io::file_descriptor_sink> client_out{sock, io::never_close_handle}; - arm(client_out); + arm_exceptions(client_out); for (;;) client_out << handle_request(rfc4251::string{client_in}) << flush; @@ -608,11 +619,13 @@ int main (int const argc, char const * const * const argv) { // the following stuff is optional, so we don't do error checking setsid(); static_cast<void>(chdir("/")); - int devnull = open("/dev/null", O_RDWR); - dup2(devnull, 0); - dup2(devnull, 1); - dup2(devnull, 2); - close(devnull); + if (int devnull = open("/dev/null", O_RDWR); devnull != -1) { + dup2(devnull, 0); + dup2(devnull, 1); + dup2(devnull, 2); + if (devnull > 2) + close(devnull); + } } else { cout << "copy this to another terminal:" << endl; cout << "SSH_AUTH_SOCK='" << path.native() << "'; export SSH_AUTH_SOCK;" << endl; @@ -630,14 +643,14 @@ int main (int const argc, char const * const * const argv) { select(listen_sock + 1, &fds, nullptr, nullptr, nullptr); int client_sock; { - lock_guard<mutex> lock{fd_fork_mutex}; + std::lock_guard lock{fd_fork_mutex}; if ((client_sock = accept(listen_sock, nullptr, nullptr)) == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) continue; else break; } - cloexec(client_sock); + alter_fd_flags<F_SETFD, FD_CLOEXEC, 0>(client_sock); } std::thread t{handle_client, client_sock}; t.detach(); |
