aboutsummaryrefslogtreecommitdiff
path: root/ssh-agent-filter.C
diff options
context:
space:
mode:
authorTimo Weingärtner <timo@tiwe.de>2013-06-17 20:33:38 +0200
committerTimo Weingärtner <timo@tiwe.de>2013-06-17 20:33:38 +0200
commit7ed93d297cda37e9f0e89a54ab270d73e3cb4f3c (patch)
treef41e113acf5722c8479059cde81284ae9502b738 /ssh-agent-filter.C
downloadssh-agent-filter-7ed93d297cda37e9f0e89a54ab270d73e3cb4f3c.tar.gz
Initial commit
Diffstat (limited to 'ssh-agent-filter.C')
-rw-r--r--ssh-agent-filter.C378
1 files changed, 378 insertions, 0 deletions
diff --git a/ssh-agent-filter.C b/ssh-agent-filter.C
new file mode 100644
index 0000000..5aa086b
--- /dev/null
+++ b/ssh-agent-filter.C
@@ -0,0 +1,378 @@
+/*
+ * ssh-agent-filter.C -- filtering proxy for ssh-agent meant to be forwarded to untrusted servers
+ *
+ * Copyright (C) 2013 Timo Weingärtner <timo@tiwe.de>
+ *
+ * This file is part of ssh-agent-filter.
+ *
+ * ssh-agent-filter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ssh-agent-filter is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with ssh-agent-filter. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <boost/program_options.hpp>
+namespace po = boost::program_options;
+
+#include <boost/filesystem.hpp>
+namespace fs = boost::filesystem;
+
+#include <string>
+#include <vector>
+#include <set>
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+#include <thread>
+#include <ext/stdio_filebuf.h>
+
+#include <csignal>
+#include <cstdlib>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sysexits.h>
+#include <nettle/md5.h>
+#include <nettle/base64.h>
+#include <nettle/base16.h>
+
+#include "rfc4251.h"
+#include "ssh-agent.h"
+#include "version.h"
+
+
+std::vector<std::string> allowed_b64;
+std::vector<std::string> allowed_md5;
+std::vector<std::string> allowed_comment;
+std::set<rfc4251string> allowed_pubkeys;
+bool debug{false};
+fs::path path;
+
+
+std::string md5_hex (std::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);
+ uint8_t hex[BASE16_ENCODE_LENGTH(MD5_DIGEST_SIZE)];
+ base16_encode_update(hex, MD5_DIGEST_SIZE, bin);
+ return std::string(reinterpret_cast<char const *>(hex), sizeof(hex));
+}
+
+std::string base64_encode (std::string const & s) {
+ struct base64_encode_ctx ctx;
+ base64_encode_init(&ctx);
+ uint8_t b64[BASE64_ENCODE_LENGTH(s.size())];
+ auto len = base64_encode_update(&ctx, b64, s.size(), reinterpret_cast<uint8_t const *>(s.data()));
+ len += base64_encode_final(&ctx, b64 + len);
+ return std::string(reinterpret_cast<char const *>(b64), len);
+}
+
+int make_upstream_agent_conn () {
+ char const * path;
+ int sock;
+ struct sockaddr_un addr;
+
+ if (!(path = getenv("SSH_AUTH_SOCK"))) {
+ std::clog << "no $SSH_AUTH_SOCK" << std::endl;
+ exit(EX_UNAVAILABLE);
+ }
+
+ if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
+ perror("socket");
+ exit(EX_UNAVAILABLE);
+ }
+
+ addr.sun_family = AF_UNIX;
+
+ if (strlen(path) >= sizeof(addr.sun_path)) {
+ std::clog << "$SSH_AUTH_SOCK too long" << std::endl;
+ exit(EX_UNAVAILABLE);
+ }
+
+ strcpy(addr.sun_path, path);
+
+ if (connect(sock, reinterpret_cast<struct sockaddr const *>(&addr), sizeof(addr))) {
+ perror("connect");
+ exit(EX_UNAVAILABLE);
+ }
+
+ return sock;
+}
+
+int make_listen_sock () {
+ int sock;
+ struct sockaddr_un addr;
+
+ if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
+ perror("socket");
+ exit(EX_UNAVAILABLE);
+ }
+
+ addr.sun_family = AF_UNIX;
+
+ if (path.native().length() >= sizeof(addr.sun_path)) {
+ std::clog << "path for listen socket too long" << std::endl;
+ exit(EX_UNAVAILABLE);
+ }
+
+ strcpy(addr.sun_path, path.c_str());
+
+ if (bind(sock, reinterpret_cast<struct sockaddr const *>(&addr), sizeof(addr))) {
+ perror("bind");
+ exit(EX_UNAVAILABLE);
+ }
+
+ if (listen(sock, 0)) {
+ perror("listen");
+ exit(EX_UNAVAILABLE);
+ }
+
+ return sock;
+}
+
+void parse_cmdline (int const argc, char const * const * const argv) {
+ po::options_description opts("OPTIONS");
+ opts.add_options()
+ ("comment,c", po::value(&allowed_comment), "key specified by comment")
+ ("debug,d", po::bool_switch(&debug), "show even more stuff")
+ ("fingerprint,fp,f", po::value(&allowed_md5), "key specified by pubkey's hex-encoded md5 fingerprint")
+ ("help,h", "print this help message")
+ ("key,k", po::value(&allowed_b64), "key specified by base64-encoded pubkey")
+ ("version,V", "print version information")
+ ;
+ po::variables_map config;
+ po::store(po::parse_command_line(argc, argv, opts), config);
+ po::notify(config);
+
+ if (config.count("help")) {
+ std::cout << "Invocation: ssh-agent-filter [ OPTIONS ] -- [ SSH ARGUMENTS ]" << std::endl;
+ std::cout << opts << std::endl;
+ exit(EX_OK);
+ }
+
+ if (config.count("version")) {
+ std::cout << SSH_AGENT_FILTER_VERSION << std::endl;
+ exit(EX_OK);
+ }
+
+ // canonicalize hashes
+ for (auto & s : allowed_md5)
+ for (auto it = s.begin(); it != s.end(); )
+ if (isxdigit(*it)) {
+ *it = tolower(*it);
+ ++it;
+ } else
+ it = s.erase(it);
+}
+
+void setup_filters () {
+ __gnu_cxx::stdio_filebuf<char> agent_filebuf(make_upstream_agent_conn(), std::ios::in | std::ios::out);
+ std::iostream agent(&agent_filebuf);
+ agent.exceptions(std::ios::badbit | std::ios::failbit);
+
+ agent << rfc4251string(std::string(1, SSH2_AGENTC_REQUEST_IDENTITIES));
+ rfc4251string answer;
+ agent >> answer;
+ std::istringstream answer_iss(answer);
+ answer_iss.exceptions(std::ios::badbit | std::ios::failbit);
+ rfc4251byte resp_code;
+ answer_iss >> resp_code;
+ if (resp_code != SSH2_AGENT_IDENTITIES_ANSWER)
+ throw std::runtime_error("unexpected answer from ssh-agent");
+ rfc4251uint32 keycount;
+ answer_iss >> keycount;
+ for (uint32_t i = keycount; i; --i) {
+ rfc4251string key;
+ rfc4251string comment;
+ answer_iss >> key >> comment;
+
+ auto b64 = base64_encode(key);
+ if (debug) std::clog << b64 << std::endl;
+
+ auto md5 = md5_hex(key);
+ if (debug) std::clog << md5 << std::endl;
+
+ std::string comm(comment);
+ if (debug) std::clog << comm << std::endl;
+
+ if (std::count(allowed_b64.begin(), allowed_b64.end(), b64)) {
+ allowed_pubkeys.insert(key);
+ if (debug) std::clog << "key allowed by equal base64 representation" << std::endl;
+ }
+ if (std::count(allowed_md5.begin(), allowed_md5.end(), md5)) {
+ allowed_pubkeys.insert(key);
+ if (debug) std::clog << "key allowed by matching md5 fingerprint" << std::endl;
+ }
+ if (std::count(allowed_comment.begin(), allowed_comment.end(), comm)) {
+ allowed_pubkeys.insert(key);
+ if (debug) std::clog << "key allowed by matching comment" << std::endl;
+ }
+
+ if (debug) std::clog << std::endl;
+ }
+}
+
+rfc4251string handle_request (rfc4251string const & r) {
+ std::istringstream request(r);
+ std::ostringstream answer;
+ request.exceptions(std::ios::badbit | std::ios::failbit);
+ answer.exceptions(std::ios::badbit | std::ios::failbit);
+ rfc4251byte request_code;
+ request >> request_code;
+ switch (request_code) {
+ case SSH2_AGENTC_REQUEST_IDENTITIES:
+ {
+ __gnu_cxx::stdio_filebuf<char> agent_filebuf(make_upstream_agent_conn(), std::ios::in | std::ios::out);
+ std::iostream agent(&agent_filebuf);
+ agent.exceptions(std::ios::badbit | std::ios::failbit);
+ rfc4251string agent_answer;
+ agent << rfc4251string(std::string(1, SSH2_AGENTC_REQUEST_IDENTITIES));
+ agent >> agent_answer;
+ // temp to test key filtering when signing
+ //return agent_answer;
+ std::istringstream agent_answer_iss(agent_answer);
+ rfc4251byte answer_code;
+ rfc4251uint32 keycount;
+ agent_answer_iss >> answer_code >> keycount;
+ if (answer_code != SSH2_AGENT_IDENTITIES_ANSWER)
+ throw std::runtime_error("unexpected answer from ssh-agent");
+ std::vector<std::pair<rfc4251string, rfc4251string>> keys;
+ for (uint32_t i = keycount; i; --i) {
+ rfc4251string key;
+ rfc4251string comment;
+ agent_answer_iss >> key >> comment;
+ if (allowed_pubkeys.count(key))
+ keys.emplace_back(std::move(key), std::move(comment));
+ }
+ answer << answer_code << rfc4251uint32(keys.size());
+ for (auto const & k : keys)
+ answer << k.first << k.second;
+ }
+ break;
+ case SSH2_AGENTC_SIGN_REQUEST:
+ {
+ rfc4251string key;
+ request >> key;
+ if (allowed_pubkeys.count(key)) {
+ __gnu_cxx::stdio_filebuf<char> agent_filebuf(make_upstream_agent_conn(), std::ios::in | std::ios::out);
+ std::iostream agent(&agent_filebuf);
+ agent.exceptions(std::ios::badbit | std::ios::failbit);
+ rfc4251string agent_answer;
+
+ agent << r;
+ agent >> agent_answer;
+ return agent_answer;
+ } else
+ answer << rfc4251byte(SSH_AGENT_FAILURE);
+ }
+ break;
+ case SSH_AGENTC_REQUEST_RSA_IDENTITIES:
+ answer << rfc4251byte(SSH_AGENT_RSA_IDENTITIES_ANSWER);
+ // we got no SSHv1 keys
+ answer << rfc4251uint32(0);
+ break;
+ case SSH_AGENTC_REMOVE_ALL_RSA_IDENTITIES:
+ answer << rfc4251byte(SSH_AGENT_SUCCESS);
+ break;
+ case SSH_AGENTC_RSA_CHALLENGE:
+ case SSH_AGENTC_ADD_RSA_IDENTITY:
+ case SSH_AGENTC_REMOVE_RSA_IDENTITY:
+ case SSH_AGENTC_ADD_RSA_ID_CONSTRAINED:
+ case SSH2_AGENTC_ADD_IDENTITY:
+ case SSH2_AGENTC_REMOVE_IDENTITY:
+ case SSH2_AGENTC_REMOVE_ALL_IDENTITIES:
+ case SSH2_AGENTC_ADD_ID_CONSTRAINED:
+ case SSH_AGENTC_ADD_SMARTCARD_KEY:
+ case SSH_AGENTC_REMOVE_SMARTCARD_KEY:
+ case SSH_AGENTC_LOCK:
+ case SSH_AGENTC_UNLOCK:
+ case SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED:
+ default:
+ answer << rfc4251byte(SSH_AGENT_FAILURE);
+ break;
+ }
+
+ return rfc4251string(answer.str());
+}
+
+void handle_client (int const 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
+ __gnu_cxx::stdio_filebuf<char> client_filebuf_in(sock, std::ios::in);
+ __gnu_cxx::stdio_filebuf<char> client_filebuf_out(sock, std::ios::out);
+ std::istream client_in(&client_filebuf_in);
+ std::ostream client_out(&client_filebuf_out);
+ client_out.exceptions(std::ios::badbit | std::ios::failbit);
+
+ rfc4251string request;
+ while (client_in >> request) try {
+ client_out << handle_request(request) << std::flush;
+ } catch (...) {
+ break;
+ }
+}
+
+void sighandler (int sig) {
+ switch (sig) {
+ case SIGINT:
+ case SIGPIPE:
+ break;
+ default:
+ remove(path);
+ std::abort();
+ }
+}
+
+int main (int const argc, char const * const * const argv) {
+ parse_cmdline(argc, argv);
+
+ setup_filters();
+
+ path = fs::current_path() / ("agent." + std::to_string(getpid()));
+ int listen_sock = make_listen_sock();
+
+ if (not debug) {
+ pid_t pid = fork();
+ if (pid == -1) {
+ perror("fork");
+ exit(EX_OSERR);
+ }
+ if (pid > 0) {
+ std::cout << "SSH_AUTH_SOCK='" << path << "'; export SSH_AUTH_SOCK;" << std::endl;
+ std::cout << "SSH_AGENT_PID='" << pid << "'; export SSH_AGENT_PID;" << std::endl;
+ std::cout << "echo 'Agent pid " << pid << "';" << std::endl;
+ exit(EX_OK);
+ }
+
+ setsid();
+ chdir("/");
+ int devnull = open("/dev/null", O_RDWR);
+ dup2(devnull, 0);
+ dup2(devnull, 1);
+ dup2(devnull, 2);
+ close(devnull);
+
+ signal(SIGINT, sighandler);
+ signal(SIGPIPE, sighandler);
+ signal(SIGHUP, sighandler);
+ signal(SIGTERM, sighandler);
+ }
+
+ int client_sock;
+ while ((client_sock = accept(listen_sock, nullptr, nullptr)) != -1) {
+ std::thread t(handle_client, client_sock);
+ t.detach();
+ }
+}