From d9866b15afe00245be196537d891529510c436d4 Mon Sep 17 00:00:00 2001 From: djm Date: Sun, 19 Dec 2021 22:09:23 +0000 Subject: [PATCH] ssh-agent side of binding record session ID/hostkey/forwarding status for each active socket. Attempt to parse data-to-be-signed at signature request time and extract session ID from the blob if it is a pubkey userauth request. ok markus@ --- usr.bin/ssh/ssh-agent.c | 177 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 168 insertions(+), 9 deletions(-) diff --git a/usr.bin/ssh/ssh-agent.c b/usr.bin/ssh/ssh-agent.c index d0d74b59ef7..da814fe8fcb 100644 --- a/usr.bin/ssh/ssh-agent.c +++ b/usr.bin/ssh/ssh-agent.c @@ -1,4 +1,4 @@ -/* $OpenBSD: ssh-agent.c,v 1.279 2021/11/18 03:31:44 djm Exp $ */ +/* $OpenBSD: ssh-agent.c,v 1.280 2021/12/19 22:09:23 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -84,9 +84,15 @@ #endif /* Maximum accepted message length */ -#define AGENT_MAX_LEN (256*1024) +#define AGENT_MAX_LEN (256*1024) /* Maximum bytes to read from client socket */ -#define AGENT_RBUF_LEN (4096) +#define AGENT_RBUF_LEN (4096) +/* Maximum number of recorded session IDs/hostkeys per connection */ +#define AGENT_MAX_SESSION_IDS 16 +/* Maximum size of session ID */ +#define AGENT_MAX_SID_LEN 128 + +/* XXX store hostkey_sid in a refcounted tree */ typedef enum { AUTH_UNUSED = 0, @@ -94,12 +100,20 @@ typedef enum { AUTH_CONNECTION = 2, } sock_type; +struct hostkey_sid { + struct sshkey *key; + struct sshbuf *sid; + int forwarded; +}; + typedef struct socket_entry { int fd; sock_type type; struct sshbuf *input; struct sshbuf *output; struct sshbuf *request; + size_t nsession_ids; + struct hostkey_sid *session_ids; } SocketEntry; u_int sockets_alloc = 0; @@ -160,10 +174,17 @@ static int restrict_websafe = 1; static void close_socket(SocketEntry *e) { + size_t i; + close(e->fd); sshbuf_free(e->input); sshbuf_free(e->output); sshbuf_free(e->request); + for (i = 0; i < e->nsession_ids; i++) { + sshkey_free(e->session_ids[i].key); + sshbuf_free(e->session_ids[i].sid); + } + free(e->session_ids); memset(e, '\0', sizeof(*e)); e->fd = -1; e->type = AUTH_UNUSED; @@ -408,16 +429,28 @@ check_websafe_message_contents(struct sshkey *key, struct sshbuf *data) return 0; } +static int +buf_equal(const struct sshbuf *a, const struct sshbuf *b) +{ + if (sshbuf_ptr(a) == NULL || sshbuf_ptr(b) == NULL) + return SSH_ERR_INVALID_ARGUMENT; + if (sshbuf_len(a) != sshbuf_len(b)) + return SSH_ERR_INVALID_FORMAT; + if (timingsafe_bcmp(sshbuf_ptr(a), sshbuf_ptr(b), sshbuf_len(a)) != 0) + return SSH_ERR_INVALID_FORMAT; + return 0; +} + /* ssh2 only */ static void process_sign_request2(SocketEntry *e) { u_char *signature = NULL; - size_t slen = 0; + size_t i, slen = 0; u_int compat = 0, flags; int r, ok = -1; - char *fp = NULL; - struct sshbuf *msg = NULL, *data = NULL; + char *fp = NULL, *user = NULL, *sig_dest = NULL; + struct sshbuf *msg = NULL, *data = NULL, *sid = NULL; struct sshkey *key = NULL; struct identity *id; struct notifier_ctx *notifier = NULL; @@ -437,7 +470,33 @@ process_sign_request2(SocketEntry *e) verbose_f("%s key not found", sshkey_type(key)); goto send; } - if (id->confirm && confirm_key(id, NULL) != 0) { + /* + * If session IDs were recorded for this socket, then use them to + * annotate the confirmation messages with the host keys. + */ + if (e->nsession_ids > 0 && + parse_userauth_request(data, key, &user, &sid) == 0) { + /* + * session ID from userauth request should match the final + * ID in the list recorded in the socket, unless the ssh + * client at that point lacks the binding extension (or if + * an attacker is trying to steal use of the agent). + */ + i = e->nsession_ids - 1; + if (buf_equal(sid, e->session_ids[i].sid) == 0) { + if ((fp = sshkey_fingerprint(e->session_ids[i].key, + SSH_FP_HASH_DEFAULT, SSH_FP_DEFAULT)) == NULL) + fatal_f("fingerprint failed"); + debug3_f("destination %s %s (slot %zu)", + sshkey_type(e->session_ids[i].key), fp, i); + xasprintf(&sig_dest, "public key request for " + "target user \"%s\" to %s %s", user, + sshkey_type(e->session_ids[i].key), fp); + free(fp); + fp = NULL; + } + } + if (id->confirm && confirm_key(id, sig_dest) != 0) { verbose_f("user refused key"); goto send; } @@ -452,8 +511,10 @@ process_sign_request2(SocketEntry *e) SSH_FP_DEFAULT)) == NULL) fatal_f("fingerprint failed"); notifier = notify_start(0, - "Confirm user presence for key %s %s", - sshkey_type(id->key), fp); + "Confirm user presence for key %s %s%s%s", + sshkey_type(id->key), fp, + sig_dest == NULL ? "" : "\n", + sig_dest == NULL ? "" : sig_dest); } } /* XXX support PIN required FIDO keys */ @@ -478,11 +539,14 @@ process_sign_request2(SocketEntry *e) if ((r = sshbuf_put_stringb(e->output, msg)) != 0) fatal_fr(r, "enqueue"); + sshbuf_free(sid); sshbuf_free(data); sshbuf_free(msg); sshkey_free(key); free(fp); free(signature); + free(sig_dest); + free(user); } /* shared */ @@ -944,6 +1008,98 @@ send: } #endif /* ENABLE_PKCS11 */ +static int +process_ext_session_bind(SocketEntry *e) +{ + int r, sid_match, key_match; + struct sshkey *key = NULL; + struct sshbuf *sid = NULL, *sig = NULL; + char *fp = NULL; + u_char fwd; + size_t i; + + debug2_f("entering"); + if ((r = sshkey_froms(e->request, &key)) != 0 || + (r = sshbuf_froms(e->request, &sid)) != 0 || + (r = sshbuf_froms(e->request, &sig)) != 0 || + (r = sshbuf_get_u8(e->request, &fwd)) != 0) { + error_fr(r, "parse"); + goto out; + } + if ((fp = sshkey_fingerprint(key, SSH_FP_HASH_DEFAULT, + SSH_FP_DEFAULT)) == NULL) + fatal_f("fingerprint failed"); + /* check signature with hostkey on session ID */ + if ((r = sshkey_verify(key, sshbuf_ptr(sig), sshbuf_len(sig), + sshbuf_ptr(sid), sshbuf_len(sid), NULL, 0, NULL)) != 0) { + error_fr(r, "sshkey_verify for %s %s", sshkey_type(key), fp); + goto out; + } + /* check whether sid/key already recorded */ + for (i = 0; i < e->nsession_ids; i++) { + sid_match = buf_equal(sid, e->session_ids[i].sid) == 0; + key_match = sshkey_equal(key, e->session_ids[i].key); + if (sid_match && key_match) { + debug_f("session ID already recorded for %s %s", + sshkey_type(key), fp); + r = 0; + goto out; + } else if (sid_match) { + error_f("session ID recorded against different key " + "for %s %s", sshkey_type(key), fp); + r = -1; + goto out; + } + /* + * new sid with previously-seen key can happen, e.g. multiple + * connections to the same host. + */ + } + /* record new key/sid */ + if (e->nsession_ids >= AGENT_MAX_SESSION_IDS) { + error_f("too many session IDs recorded"); + goto out; + } + e->session_ids = xrecallocarray(e->session_ids, e->nsession_ids, + e->nsession_ids + 1, sizeof(*e->session_ids)); + i = e->nsession_ids++; + debug_f("recorded %s %s (slot %zu of %d)", sshkey_type(key), fp, i, + AGENT_MAX_SESSION_IDS); + e->session_ids[i].key = key; + e->session_ids[i].forwarded = fwd != 0; + key = NULL; /* transferred */ + /* can't transfer sid; it's refcounted and scoped to request's life */ + if ((e->session_ids[i].sid = sshbuf_new()) == NULL) + fatal_f("sshbuf_new"); + if ((r = sshbuf_putb(e->session_ids[i].sid, sid)) != 0) + fatal_fr(r, "sshbuf_putb session ID"); + /* success */ + r = 0; + out: + sshkey_free(key); + sshbuf_free(sid); + sshbuf_free(sig); + return r == 0 ? 1 : 0; +} + +static void +process_extension(SocketEntry *e) +{ + int r, success = 0; + char *name; + + debug2_f("entering"); + if ((r = sshbuf_get_cstring(e->request, &name, NULL)) != 0) { + error_fr(r, "parse"); + goto send; + } + if (strcmp(name, "session-bind@openssh.com") == 0) + success = process_ext_session_bind(e); + else + debug_f("unsupported extension \"%s\"", name); +send: + send_status(e, success); +} /* * dispatch incoming message. * returns 1 on success, 0 for incomplete messages or -1 on error. @@ -1036,6 +1192,9 @@ process_message(u_int socknum) process_remove_smartcard_key(e); break; #endif /* ENABLE_PKCS11 */ + case SSH_AGENTC_EXTENSION: + process_extension(e); + break; default: /* Unknown message. Respond with failure. */ error("Unknown message %d", type); -- 2.20.1