ssh-agent side of destination constraints
authordjm <djm@openbsd.org>
Sun, 19 Dec 2021 22:11:39 +0000 (22:11 +0000)
committerdjm <djm@openbsd.org>
Sun, 19 Dec 2021 22:11:39 +0000 (22:11 +0000)
Gives ssh-agent the ability to parse restrict-destination-v00@openssh.com
constraints and to apply them to keys.

Check constraints against the hostkeys recorded for a SocketEntry when
attempting a signature, adding, listing or deleting keys. Note that
the "delete all keys" request will remove constrained keys regardless of
location.

feedback Jann Horn & markus@
ok markus@

usr.bin/ssh/ssh-agent.c

index da814fe..c55d88e 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: ssh-agent.c,v 1.280 2021/12/19 22:09:23 djm Exp $ */
+/* $OpenBSD: ssh-agent.c,v 1.281 2021/12/19 22:11:39 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -78,6 +78,7 @@
 #include "pathnames.h"
 #include "ssh-pkcs11.h"
 #include "sk-api.h"
+#include "myproposal.h"
 
 #ifndef DEFAULT_ALLOWED_PROVIDERS
 # define DEFAULT_ALLOWED_PROVIDERS "/usr/lib*/*,/usr/local/lib*/*"
@@ -91,6 +92,8 @@
 #define AGENT_MAX_SESSION_IDS          16
 /* Maximum size of session ID */
 #define AGENT_MAX_SID_LEN              128
+/* Maximum number of destination constraints to accept on a key */
+#define AGENT_MAX_DEST_CONSTRAINTS     1024
 
 /* XXX store hostkey_sid in a refcounted tree */
 
@@ -127,6 +130,8 @@ typedef struct identity {
        time_t death;
        u_int confirm;
        char *sk_provider;
+       struct dest_constraint *dest_constraints;
+       size_t ndest_constraints;
 } Identity;
 
 struct idtable {
@@ -198,6 +203,33 @@ idtab_init(void)
        idtab->nentries = 0;
 }
 
+static void
+free_dest_constraint_hop(struct dest_constraint_hop *dch)
+{
+       u_int i;
+
+       if (dch == NULL)
+               return;
+       free(dch->user);
+       free(dch->hostname);
+       for (i = 0; i < dch->nkeys; i++)
+               sshkey_free(dch->keys[i]);
+       free(dch->keys);
+       free(dch->key_is_ca);
+}
+
+static void
+free_dest_constraints(struct dest_constraint *dcs, size_t ndcs)
+{
+       size_t i;
+
+       for (i = 0; i < ndcs; i++) {
+               free_dest_constraint_hop(&dcs[i].from);
+               free_dest_constraint_hop(&dcs[i].to);
+       }
+       free(dcs);
+}
+
 static void
 free_identity(Identity *id)
 {
@@ -205,9 +237,220 @@ free_identity(Identity *id)
        free(id->provider);
        free(id->comment);
        free(id->sk_provider);
+       free_dest_constraints(id->dest_constraints, id->ndest_constraints);
        free(id);
 }
 
+/*
+ * Match 'key' against the key/CA list in a destination constraint hop
+ * Returns 0 on success or -1 otherwise.
+ */
+static int
+match_key_hop(const char *tag, const struct sshkey *key,
+    const struct dest_constraint_hop *dch)
+{
+       const char *reason = NULL;
+       u_int i;
+       char *fp;
+
+       if (key == NULL)
+               return -1;
+       /* XXX logspam */
+       if ((fp = sshkey_fingerprint(key, SSH_FP_HASH_DEFAULT,
+           SSH_FP_DEFAULT)) == NULL)
+               fatal_f("fingerprint failed");
+       debug3_f("%s: entering hostname %s, requested key %s %s, %u keys avail",
+           tag, dch->hostname, sshkey_type(key), fp, dch->nkeys);
+       free(fp);
+       for (i = 0; i < dch->nkeys; i++) {
+               if (dch->keys[i] == NULL)
+                       return -1;
+               /* XXX logspam */
+               if ((fp = sshkey_fingerprint(dch->keys[i], SSH_FP_HASH_DEFAULT,
+                   SSH_FP_DEFAULT)) == NULL)
+                       fatal_f("fingerprint failed");
+               debug3_f("%s: key %u: %s%s %s", tag, i,
+                   dch->key_is_ca[i] ? "CA " : "",
+                   sshkey_type(dch->keys[i]), fp);
+               free(fp);
+               if (!sshkey_is_cert(key)) {
+                       /* plain key */
+                       if (dch->key_is_ca[i] ||
+                           !sshkey_equal(key, dch->keys[i]))
+                               continue;
+                       return 0;
+               }
+               /* certificate */
+               if (!dch->key_is_ca[i])
+                       continue;
+               if (key->cert == NULL || key->cert->signature_key == NULL)
+                       return -1; /* shouldn't happen */
+               if (!sshkey_equal(key->cert->signature_key, dch->keys[i]))
+                       continue;
+               if (sshkey_cert_check_host(key, dch->hostname, 1,
+                   SSH_ALLOWED_CA_SIGALGS, &reason) != 0) {
+                       debug_f("cert %s / hostname %s rejected: %s",
+                           key->cert->key_id, dch->hostname, reason);
+                       continue;
+               }
+               return 0;
+       }
+       return -1;
+}
+
+/* Check destination constraints on an identity against the hostkey/user */
+static int
+permitted_by_dest_constraints(const struct sshkey *fromkey,
+    const struct sshkey *tokey, Identity *id, const char *user,
+    const char **hostnamep)
+{
+       size_t i;
+       struct dest_constraint *d;
+
+       if (hostnamep != NULL)
+               *hostnamep = NULL;
+       for (i = 0; i < id->ndest_constraints; i++) {
+               d = id->dest_constraints + i;
+               /* XXX remove logspam */
+               debug2_f("constraint %zu %s%s%s (%u keys) > %s%s%s (%u keys)",
+                   i, d->from.user ? d->from.user : "",
+                   d->from.user ? "@" : "",
+                   d->from.hostname ? d->from.hostname : "(ORIGIN)",
+                   d->from.nkeys,
+                   d->to.user ? d->to.user : "", d->to.user ? "@" : "",
+                   d->to.hostname ? d->to.hostname : "(ANY)", d->to.nkeys);
+
+               /* Match 'from' key */
+               if (fromkey == NULL) {
+                       /* We are matching the first hop */
+                       if (d->from.hostname != NULL || d->from.nkeys != 0)
+                               continue;
+               } else if (match_key_hop("from", fromkey, &d->from) != 0)
+                       continue;
+
+               /* Match 'to' key */
+               if (tokey != NULL && match_key_hop("to", tokey, &d->to) != 0)
+                       continue;
+
+               /* Match user if specified */
+               if (d->to.user != NULL && user != NULL &&
+                   !match_pattern(user, d->to.user))
+                       continue;
+
+               /* successfully matched this constraint */
+               if (hostnamep != NULL)
+                       *hostnamep = d->to.hostname;
+               debug2_f("allowed for hostname %s",
+                   d->to.hostname == NULL ? "*" : d->to.hostname);
+               return 0;
+       }
+       /* no match */
+       debug2_f("%s identity \"%s\" not permitted for this destination",
+           sshkey_type(id->key), id->comment);
+       return -1;
+}
+
+/*
+ * Check whether hostkeys on a SocketEntry and the optionally specified user
+ * are permitted by the destination constraints on the Identity.
+ * Returns 0 on success or -1 otherwise.
+ */
+static int
+identity_permitted(Identity *id, SocketEntry *e, char *user,
+    const char **forward_hostnamep, const char **last_hostnamep)
+{
+       size_t i;
+       const char **hp;
+       struct hostkey_sid *hks;
+       const struct sshkey *fromkey = NULL;
+       const char *test_user;
+       char *fp1, *fp2;
+
+       /* XXX remove logspam */
+       debug3_f("entering: key %s comment \"%s\", %zu socket bindings, "
+           "%zu constraints", sshkey_type(id->key), id->comment,
+           e->nsession_ids, id->ndest_constraints);
+       if (id->ndest_constraints == 0)
+               return 0; /* unconstrained */
+       if (e->nsession_ids == 0)
+               return 0; /* local use */
+       /*
+        * Walk through the hops recorded by session_id and try to find a
+        * constraint that satisfies each.
+        */
+       for (i = 0; i < e->nsession_ids; i++) {
+               hks = e->session_ids + i;
+               if (hks->key == NULL)
+                       fatal_f("internal error: no bound key");
+               /* XXX remove logspam */
+               fp1 = fp2 = NULL;
+               if (fromkey != NULL &&
+                   (fp1 = sshkey_fingerprint(fromkey, SSH_FP_HASH_DEFAULT,
+                   SSH_FP_DEFAULT)) == NULL)
+                       fatal_f("fingerprint failed");
+               if ((fp2 = sshkey_fingerprint(hks->key, SSH_FP_HASH_DEFAULT,
+                   SSH_FP_DEFAULT)) == NULL)
+                       fatal_f("fingerprint failed");
+               debug3_f("socketentry fd=%d, entry %zu %s, "
+                   "from hostkey %s %s to user %s hostkey %s %s",
+                   e->fd, i, hks->forwarded ? "FORWARD" : "AUTH",
+                   fromkey ? sshkey_type(fromkey) : "(ORIGIN)",
+                   fromkey ? fp1 : "", user ? user : "(ANY)",
+                   sshkey_type(hks->key), fp2);
+               free(fp1);
+               free(fp2);
+               /*
+                * Record the hostnames for the initial forwarding and
+                * the final destination.
+                */
+               hp = NULL;
+               if (i == e->nsession_ids - 1)
+                       hp = last_hostnamep;
+               else if (i == 0)
+                       hp = forward_hostnamep;
+               /* Special handling for final recorded binding */
+               test_user = NULL;
+               if (i == e->nsession_ids - 1) {
+                       /* Can only check user at final hop */
+                       test_user = user;
+                       /*
+                        * user is only presented for signature requests.
+                        * If this is the case, make sure last binding is not
+                        * for a forwarding.
+                        */
+                       if (hks->forwarded && user != NULL) {
+                               error_f("tried to sign on forwarding hop");
+                               return -1;
+                       }
+               } else if (!hks->forwarded) {
+                       error_f("tried to forward though signing bind");
+                       return -1;
+               }
+               if (permitted_by_dest_constraints(fromkey, hks->key, id,
+                   test_user, hp) != 0)
+                       return -1;
+               fromkey = hks->key;
+       }
+       /*
+        * Another special case: if the last bound session ID was for a
+        * forwarding, and this function is not being called to check a sign
+        * request (i.e. no 'user' supplied), then only permit the key if
+        * there is a permission that would allow it to be used at another
+        * destination. This hides keys that are allowed to be used to
+        * authenicate *to* a host but not permitted for *use* beyond it.
+        */
+       hks = &e->session_ids[e->nsession_ids - 1];
+       if (hks->forwarded && user == NULL &&
+           permitted_by_dest_constraints(hks->key, NULL, id,
+           NULL, NULL) != 0) {
+               debug3_f("key permitted at host but not after");
+               return -1;
+       }
+
+       /* success */
+       return 0;
+}
+
 /* return matching private key for given public key */
 static Identity *
 lookup_identity(struct sshkey *key)
@@ -255,27 +498,36 @@ static void
 process_request_identities(SocketEntry *e)
 {
        Identity *id;
-       struct sshbuf *msg;
+       struct sshbuf *msg, *keys;
        int r;
+       u_int nentries = 0;
 
        debug2_f("entering");
 
-       if ((msg = sshbuf_new()) == NULL)
+       if ((msg = sshbuf_new()) == NULL || (keys = sshbuf_new()) == NULL)
                fatal_f("sshbuf_new failed");
-       if ((r = sshbuf_put_u8(msg, SSH2_AGENT_IDENTITIES_ANSWER)) != 0 ||
-           (r = sshbuf_put_u32(msg, idtab->nentries)) != 0)
-               fatal_fr(r, "compose");
        TAILQ_FOREACH(id, &idtab->idlist, next) {
-               if ((r = sshkey_puts_opts(id->key, msg,
+               /* identity not visible, don't include in response */
+               if (identity_permitted(id, e, NULL, NULL, NULL) != 0)
+                       continue;
+               if ((r = sshkey_puts_opts(id->key, keys,
                    SSHKEY_SERIALIZE_INFO)) != 0 ||
-                   (r = sshbuf_put_cstring(msg, id->comment)) != 0) {
+                   (r = sshbuf_put_cstring(keys, id->comment)) != 0) {
                        error_fr(r, "compose key/comment");
                        continue;
                }
+               nentries++;
        }
+       debug2_f("replying with %u allowed of %u available keys",
+           nentries, idtab->nentries);
+       if ((r = sshbuf_put_u8(msg, SSH2_AGENT_IDENTITIES_ANSWER)) != 0 ||
+           (r = sshbuf_put_u32(msg, nentries)) != 0 ||
+           (r = sshbuf_putb(msg, keys)) != 0)
+               fatal_fr(r, "compose");
        if ((r = sshbuf_put_stringb(e->output, msg)) != 0)
                fatal_fr(r, "enqueue");
        sshbuf_free(msg);
+       sshbuf_free(keys);
 }
 
 
@@ -446,10 +698,11 @@ static void
 process_sign_request2(SocketEntry *e)
 {
        u_char *signature = NULL;
-       size_t i, slen = 0;
+       size_t slen = 0;
        u_int compat = 0, flags;
        int r, ok = -1;
        char *fp = NULL, *user = NULL, *sig_dest = NULL;
+       const char *fwd_host = NULL, *dest_host = NULL;
        struct sshbuf *msg = NULL, *data = NULL, *sid = NULL;
        struct sshkey *key = NULL;
        struct identity *id;
@@ -470,31 +723,41 @@ process_sign_request2(SocketEntry *e)
                verbose_f("%s key not found", sshkey_type(key));
                goto send;
        }
-       /*
-        * 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) {
+       if ((fp = sshkey_fingerprint(key, SSH_FP_HASH_DEFAULT,
+           SSH_FP_DEFAULT)) == NULL)
+               fatal_f("fingerprint failed");
+
+       if (id->ndest_constraints != 0) {
+               if (e->nsession_ids == 0) {
+                       logit_f("refusing use of destination-constrained key "
+                           "to sign on unbound connection");
+                       goto send;
+               }
+               if (parse_userauth_request(data, key, &user, &sid) != 0) {
+                       logit_f("refusing use of destination-constrained key "
+                          "to sign an unidentified signature");
+                       goto send;
+               }
+               /* XXX logspam */
+               debug_f("user=%s", user);
+               if (identity_permitted(id, e, user, &fwd_host, &dest_host) != 0)
+                       goto send;
+               /* XXX display fwd_host/dest_host in askpass UI */
                /*
-                * 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).
+                * Ensure that the session ID is the most recent one
+                * registered on the socket - it should have been bound by
+                * ssh immediately before userauth.
                 */
-               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 (buf_equal(sid,
+                   e->session_ids[e->nsession_ids - 1].sid) != 0) {
+                       error_f("unexpected session ID (%zu listed) on "
+                           "signature request for target user %s with "
+                           "key %s %s", e->nsession_ids, user,
+                           sshkey_type(id->key), fp);
+                       goto send;
                }
+               xasprintf(&sig_dest, "public key authentication request for "
+                   "user \"%s\" to listed host", user);
        }
        if (id->confirm && confirm_key(id, sig_dest) != 0) {
                verbose_f("user refused key");
@@ -507,9 +770,6 @@ process_sign_request2(SocketEntry *e)
                        goto send;
                }
                if ((id->key->sk_flags & SSH_SK_USER_PRESENCE_REQD)) {
-                       if ((fp = sshkey_fingerprint(key, SSH_FP_HASH_DEFAULT,
-                           SSH_FP_DEFAULT)) == NULL)
-                               fatal_f("fingerprint failed");
                        notifier = notify_start(0,
                            "Confirm user presence for key %s %s%s%s",
                            sshkey_type(id->key), fp,
@@ -566,6 +826,9 @@ process_remove_identity(SocketEntry *e)
                debug_f("key not found");
                goto done;
        }
+       /* identity not visible, cannot be removed */
+       if (identity_permitted(id, e, NULL, NULL, NULL) != 0)
+               goto done; /* error already logged */
        /* We have this key, free it. */
        if (idtab->nentries < 1)
                fatal_f("internal error: nentries %d", idtab->nentries);
@@ -625,10 +888,119 @@ reaper(void)
 }
 
 static int
-parse_key_constraint_extension(struct sshbuf *m, char **sk_providerp)
+parse_dest_constraint_hop(struct sshbuf *b, struct dest_constraint_hop *dch)
+{
+       u_char key_is_ca;
+       size_t elen = 0;
+       int r;
+       struct sshkey *k = NULL;
+       char *fp;
+
+       memset(dch, '\0', sizeof(*dch));
+       if ((r = sshbuf_get_cstring(b, &dch->user, NULL)) != 0 ||
+           (r = sshbuf_get_cstring(b, &dch->hostname, NULL)) != 0 ||
+           (r = sshbuf_get_string_direct(b, NULL, &elen)) != 0) {
+               error_fr(r, "parse");
+               goto out;
+       }
+       if (elen != 0) {
+               error_f("unsupported extensions (len %zu)", elen);
+               r = SSH_ERR_FEATURE_UNSUPPORTED;
+               goto out;
+       }
+       if (*dch->hostname == '\0') {
+               free(dch->hostname);
+               dch->hostname = NULL;
+       }
+       if (*dch->user == '\0') {
+               free(dch->user);
+               dch->user = NULL;
+       }
+       while (sshbuf_len(b) != 0) {
+               dch->keys = xrecallocarray(dch->keys, dch->nkeys,
+                   dch->nkeys + 1, sizeof(*dch->keys));
+               dch->key_is_ca = xrecallocarray(dch->key_is_ca, dch->nkeys,
+                   dch->nkeys + 1, sizeof(*dch->key_is_ca));
+               if ((r = sshkey_froms(b, &k)) != 0 ||
+                   (r = sshbuf_get_u8(b, &key_is_ca)) != 0)
+                       goto out;
+               if ((fp = sshkey_fingerprint(k, SSH_FP_HASH_DEFAULT,
+                   SSH_FP_DEFAULT)) == NULL)
+                       fatal_f("fingerprint failed");
+               debug3_f("%s%s%s: adding %skey %s %s",
+                   dch->user == NULL ? "" : dch->user,
+                   dch->user == NULL ? "" : "@",
+                   dch->hostname, key_is_ca ? "CA " : "", sshkey_type(k), fp);
+               free(fp);
+               dch->keys[dch->nkeys] = k;
+               dch->key_is_ca[dch->nkeys] = key_is_ca != 0;
+               dch->nkeys++;
+               k = NULL; /* transferred */
+       }
+       /* success */
+       r = 0;
+ out:
+       sshkey_free(k);
+       return r;
+}
+
+static int
+parse_dest_constraint(struct sshbuf *m, struct dest_constraint *dc)
+{
+       struct sshbuf *b = NULL, *frombuf = NULL, *tobuf = NULL;
+       int r;
+       size_t elen = 0;
+
+       debug3_f("entering");
+
+       memset(dc, '\0', sizeof(*dc));
+       if ((r = sshbuf_froms(m, &b)) != 0 ||
+           (r = sshbuf_froms(b, &frombuf)) != 0 ||
+           (r = sshbuf_froms(b, &tobuf)) != 0 ||
+           (r = sshbuf_get_string_direct(b, NULL, &elen)) != 0) {
+               error_fr(r, "parse");
+               goto out;
+       }
+       if ((r = parse_dest_constraint_hop(frombuf, &dc->from) != 0) ||
+           (r = parse_dest_constraint_hop(tobuf, &dc->to) != 0))
+               goto out; /* already logged */
+       if (elen != 0) {
+               error_f("unsupported extensions (len %zu)", elen);
+               r = SSH_ERR_FEATURE_UNSUPPORTED;
+               goto out;
+       }
+       debug2_f("parsed %s (%u keys) > %s%s%s (%u keys)",
+           dc->from.hostname ? dc->from.hostname : "(ORIGIN)", dc->from.nkeys,
+           dc->to.user ? dc->to.user : "", dc->to.user ? "@" : "",
+           dc->to.hostname ? dc->to.hostname : "(ANY)", dc->to.nkeys);
+       /* check consistency */
+       if ((dc->from.hostname == NULL) != (dc->from.nkeys == 0) ||
+           dc->from.user != NULL) {
+               error_f("inconsistent \"from\" specification");
+               r = SSH_ERR_INVALID_FORMAT;
+               goto out;
+       }
+       if (dc->to.hostname == NULL || dc->to.nkeys == 0) {
+               error_f("incomplete \"to\" specification");
+               r = SSH_ERR_INVALID_FORMAT;
+               goto out;
+       }
+       /* success */
+       r = 0;
+ out:
+       sshbuf_free(b);
+       sshbuf_free(frombuf);
+       sshbuf_free(tobuf);
+       return r;
+}
+
+static int
+parse_key_constraint_extension(struct sshbuf *m, char **sk_providerp,
+    struct dest_constraint **dcsp, size_t *ndcsp)
 {
        char *ext_name = NULL;
        int r;
+       struct sshbuf *b = NULL;
 
        if ((r = sshbuf_get_cstring(m, &ext_name, NULL)) != 0) {
                error_fr(r, "parse constraint extension");
@@ -650,6 +1022,27 @@ parse_key_constraint_extension(struct sshbuf *m, char **sk_providerp)
                        error_fr(r, "parse %s", ext_name);
                        goto out;
                }
+       } else if (strcmp(ext_name,
+           "restrict-destination-v00@openssh.com") == 0) {
+               if (*dcsp != NULL) {
+                       error_f("%s already set", ext_name);
+                       goto out;
+               }
+               if ((r = sshbuf_froms(m, &b)) != 0) {
+                       error_fr(r, "parse %s outer", ext_name);
+                       goto out;
+               }
+               while (sshbuf_len(b) != 0) {
+                       if (*ndcsp >= AGENT_MAX_DEST_CONSTRAINTS) {
+                               error_f("too many %s constraints", ext_name);
+                               goto out;
+                       }
+                       *dcsp = xrecallocarray(*dcsp, *ndcsp, *ndcsp + 1,
+                           sizeof(**dcsp));
+                       if ((r = parse_dest_constraint(b,
+                           *dcsp + (*ndcsp)++)) != 0)
+                               goto out; /* error already logged */
+               }
        } else {
                error_f("unsupported constraint \"%s\"", ext_name);
                r = SSH_ERR_FEATURE_UNSUPPORTED;
@@ -659,12 +1052,14 @@ parse_key_constraint_extension(struct sshbuf *m, char **sk_providerp)
        r = 0;
  out:
        free(ext_name);
+       sshbuf_free(b);
        return r;
 }
 
 static int
 parse_key_constraints(struct sshbuf *m, struct sshkey *k, time_t *deathp,
-    u_int *secondsp, int *confirmp, char **sk_providerp)
+    u_int *secondsp, int *confirmp, char **sk_providerp,
+    struct dest_constraint **dcsp, size_t *ndcsp)
 {
        u_char ctype;
        int r;
@@ -719,7 +1114,7 @@ parse_key_constraints(struct sshbuf *m, struct sshkey *k, time_t *deathp,
                        break;
                case SSH_AGENT_CONSTRAIN_EXTENSION:
                        if ((r = parse_key_constraint_extension(m,
-                           sk_providerp)) != 0)
+                           sk_providerp, dcsp, ndcsp)) != 0)
                                goto out; /* error already logged */
                        break;
                default:
@@ -743,6 +1138,8 @@ process_add_identity(SocketEntry *e)
        char canonical_provider[PATH_MAX];
        time_t death = 0;
        u_int seconds = 0;
+       struct dest_constraint *dest_constraints = NULL;
+       size_t ndest_constraints = 0;
        struct sshkey *k = NULL;
        int r = SSH_ERR_INTERNAL_ERROR;
 
@@ -754,7 +1151,7 @@ process_add_identity(SocketEntry *e)
                goto out;
        }
        if (parse_key_constraints(e->request, k, &death, &seconds, &confirm,
-           &sk_provider) != 0) {
+           &sk_provider, &dest_constraints, &ndest_constraints) != 0) {
                error_f("failed to parse constraints");
                sshbuf_reset(e->request);
                goto out;
@@ -797,10 +1194,15 @@ process_add_identity(SocketEntry *e)
                /* Increment the number of identities. */
                idtab->nentries++;
        } else {
+               /* identity not visible, do not update */
+               if (identity_permitted(id, e, NULL, NULL, NULL) != 0)
+                       goto out; /* error already logged */
                /* key state might have been updated */
                sshkey_free(id->key);
                free(id->comment);
                free(id->sk_provider);
+               free_dest_constraints(id->dest_constraints,
+                   id->ndest_constraints);
        }
        /* success */
        id->key = k;
@@ -808,23 +1210,29 @@ process_add_identity(SocketEntry *e)
        id->death = death;
        id->confirm = confirm;
        id->sk_provider = sk_provider;
+       id->dest_constraints = dest_constraints;
+       id->ndest_constraints = ndest_constraints;
 
        if ((fp = sshkey_fingerprint(k, SSH_FP_HASH_DEFAULT,
            SSH_FP_DEFAULT)) == NULL)
                fatal_f("sshkey_fingerprint failed");
        debug_f("add %s %s \"%.100s\" (life: %u) (confirm: %u) "
-           "(provider: %s)", sshkey_ssh_name(k), fp, comment, seconds,
-           confirm, sk_provider == NULL ? "none" : sk_provider);
+           "(provider: %s) (destination constraints: %zu)",
+           sshkey_ssh_name(k), fp, comment, seconds, confirm,
+           sk_provider == NULL ? "none" : sk_provider, ndest_constraints);
        free(fp);
        /* transferred */
        k = NULL;
        comment = NULL;
        sk_provider = NULL;
+       dest_constraints = NULL;
+       ndest_constraints = 0;
        success = 1;
  out:
        free(sk_provider);
        free(comment);
        sshkey_free(k);
+       free_dest_constraints(dest_constraints, ndest_constraints);
        send_status(e, success);
 }
 
@@ -907,6 +1315,8 @@ process_add_smartcard_key(SocketEntry *e)
        time_t death = 0;
        struct sshkey **keys = NULL, *k;
        Identity *id;
+       struct dest_constraint *dest_constraints = NULL;
+       size_t ndest_constraints = 0;
 
        debug2_f("entering");
        if ((r = sshbuf_get_cstring(e->request, &provider, NULL)) != 0 ||
@@ -915,7 +1325,7 @@ process_add_smartcard_key(SocketEntry *e)
                goto send;
        }
        if (parse_key_constraints(e->request, NULL, &death, &seconds, &confirm,
-           NULL) != 0) {
+           NULL, &dest_constraints, &ndest_constraints) != 0) {
                error_f("failed to parse constraints");
                goto send;
        }
@@ -949,6 +1359,10 @@ process_add_smartcard_key(SocketEntry *e)
                        }
                        id->death = death;
                        id->confirm = confirm;
+                       id->dest_constraints = dest_constraints;
+                       id->ndest_constraints = ndest_constraints;
+                       dest_constraints = NULL; /* transferred */
+                       ndest_constraints = 0;
                        TAILQ_INSERT_TAIL(&idtab->idlist, id, next);
                        idtab->nentries++;
                        success = 1;
@@ -962,6 +1376,7 @@ send:
        free(provider);
        free(keys);
        free(comments);
+       free_dest_constraints(dest_constraints, ndest_constraints);
        send_status(e, success);
 }
 
@@ -1015,8 +1430,8 @@ process_ext_session_bind(SocketEntry *e)
        struct sshkey *key = NULL;
        struct sshbuf *sid = NULL, *sig = NULL;
        char *fp = NULL;
-       u_char fwd;
        size_t i;
+       u_char fwd = 0;
 
        debug2_f("entering");
        if ((r = sshkey_froms(e->request, &key)) != 0 ||
@@ -1037,6 +1452,12 @@ process_ext_session_bind(SocketEntry *e)
        }
        /* check whether sid/key already recorded */
        for (i = 0; i < e->nsession_ids; i++) {
+               if (!e->session_ids[i].forwarded) {
+                       error_f("attempt to bind session ID to socket "
+                           "previously bound for authentication attempt");
+                       r = -1;
+                       goto out;
+               }
                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) {