Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Agent restrictions / host-bound public key signatures combined #5

Closed
wants to merge 21 commits into from

Conversation

djmdjm
Copy link
Owner

@djmdjm djmdjm commented Feb 8, 2021

NB. This is a work in progress.

This tree combines the agent session ID binding and host-bound userauth signatures approaches to restricting agent forwarding.

Note also that these patches are for the OpenBSD version of OpenSSH. A version of the patches adapted for Portable OpenSSH is also available.

Overview

These changes implement a key permission model for ssh-agent that allows granular control over which keys are available to particular destinations and forwarding hops. Destination hosts specified in these permissions are cryptographically verifiable by the agent.

Keys may be destination restricted when they added to the agent by ssh-add. Destination restrictions take the form of one or more [user@]host or host1>[user@]host2 strings, e.g.

ssh-add -h djm@example.org -h example.com -h 'example.org>example.net' ~/.ssh/id_ed25519

This would permit the key to be used to authenticate to any account on example.com, to user djm on example.org, and to example.net using an agent forwarded through example.org.

Internally, hosts are identified by their host keys. ssh-add will perform the name to key lookup using the known_hosts files on the system where was ran.

Connections forwarded across multiple hops must satisfy all restriction conditions for the keys to be usable at the destination. E.g. for the above set of restrictions (and assuming all hosts have independent host keys, the key is in authorized_keys, etc)

ssh -A djm@example.org ssh example.net # works
ssh -A example.com ssh djm@example.org # fails; no permission for 2nd hop
ssh -A example.org ssh root@example.net # fails (incorrect user)

TODO decide whether this "hop by hop" permission model is better than requiring the user to explicitly specify full paths, e.g.

ssh-add -h "host_a>user@host_b>host_c" /path/key

(allowing access to host_c by the specified path, but also implicitly allowing the intermediate hosts too.)

Goals

The goal of these changes is to allow control over selection and delegation of keys stored in SSH agent.

Delegation of key subsets

The primary motivation of this feature is providing the ability to delegate access to subsets of keys hosted in an agent. For example:

ssh-add /path/key_a
ssh-add -h host_a -h 'host_a>host_b' /path/key_b
ssh-add -h host_a -h 'host_a>host_b' -h 'host_a>host_c' /path/key_c
ssh-add -h host_a -h 'host_a>host_b' -h 'host_b>host_c' /path/key_d

Running these commands would load four keys into a ssh-agent, each with different delegation permissions:

  • key_a has no delegation restrictions. It will be available for use on all hosts to which the agent has been forwarded.
  • key_b is permitted for authentication to host_a and delegated for use at host_a only for authentication to host_b.
  • key_c is permitted for authentication to host_a and delegated for authentication from there to hosts host_b or host_c
  • key_d is permitted for authentication to host_a, authentication from host_a to host_b and use on host_b to authenticate to host_c

At each step, only the delegated keys should be visible. I.e. ssh-add -l on the origin host or host_a should show all four keys whereas running the same command on host_b will display only the unrestricted key_a and the delegated key_d.

Selection of keys

It is already possible to select particular keys to be offered to specific destination hosts using the IdentityFile and IdentiesOnly directives in ssh_config.

These changes offer an alternative that may be more friendly, at least for one-off or ad-hoc use cases As an example,

ssh-add -h user_1@host_a /path/key_a
ssh-add -h user_2@host_b /path/key_b

May be easier than the equivalent configuration:

Match user user_1 host host_a
        IdentitiesOnly yes
        IdentityFile /path/key_a
Match user user_2 host host_b
        IdentitiesOnly yes
        IdentityFile /path/key_b

Implementation

This assumes familiarity with the SSH agent protocol draft-miller-ssh-agent-04, the SSH public key authentication subprotocol and how they are used in practice.

Key permissions are built on a pair of agent protocol extensions: a trivial restrict-destination-v00@openssh.com key constraint that is used to encode the permissions attached to keys being added to the agent and and a new session-bind@openssh.com extension request that is used to cryptographically bind hostkeys to agent connections.

An additional modification is made to the public key authentication request to include the host key signature in the data signed by the client.

Session / hostkey binding

The session binding mechanism depends on a number of SSH protocol features across the client, server and agent to establish a cryptographically provable binding between a session ID and a SSH host key.

When a SSH connection is established, a key exchange process is performed between the client and server (e.g. using ECDH). One of the outputs of this process is a session ID that is signed by the server to prove ownership of a host key. This same session ID is subsequently used to included in public key authentication requests that are signed and sent by the client and verified by the server. The session ID is included in the to-be-signed data to bind them to the connection instance, preventing replay of authentication attempts across connections.

The session ID is therefore cryptographically traceable to a host key and included in public key authentication requests. The session-bind@openssh.com extension takes advantage of these two properties. It allows a SSH client to communicate the to the agent the host key used in the initial key exchange, the session ID derived for the connection and the server's signature (over the session ID) proving the binding between them.

When the agent receives this binding on a connected socket, it verifies the signature and records the host key and session ID as bound to that socket. When publickey authentication requests subsequently arrive on that socket, the agent may check the bound session ID against the session ID included in the authentication request to be signed, and the associated hostkey against the constraints associated with the key that will perform the signing. If one of these fails to match the signing request will be denied.

If a SSH agent is forwarded across multiple "hops" of SSH clients, each client will issue a session-bind@openssh.com request when it attempts to use or forward the agent. The agent connection will therefore accrue a list of {session ID, hostkey} that it may use to match against destination restrictions that it has set on loaded keys. This implementation requires hostkeys gathered on a chained connection be permitted by a destination restriction.

Certificate matching is slightly more complex than plain hostkey matching. As per above, certificates are represented in destination restrictions by their CA key and a hostname wildcard pattern that is matched against the host certificate principals. This allows for common patterns of CA use such as whole domain *.example.com trust and individual hostname trust.

Destination restriction key restrictions

Destination restrictions are an array of permission tuples that may be attached to a key when it is added to ssh-agent. Each entry in the array identifies a single forwarding step that a key is allowed for. A forwarding step is identified by its source host keys and destination user and host keys.

A special case is the initial authentication / forwarding step, from the host that is running ssh-agent locally. The step is distinguished by having no source host keys specified. All other steps must have both source and destination hostkeys present.

Restrictions are applied by ssh-agent when it receives a request to use a key (e.g. to sign a challenge). The agent will walk the array of bound session IDs hostkeys for the connection on which the request was received and try to match each hop to a permission tuple. All entries in the bound session array must match a corresponding permission for the request to be permitted. Additionally, for signature requests, the agent will require that the session ID present in the to-be-signed challenge matches the most-recently bound session ID.

Restrictions are communicated to ssh-agent when keys are added using a key constraint extension described below.

Modification to public key authentication subprotocol

Given the session binding modification above, the data signed by a SSH client (or by ssh-agent on a client's behalf) is only indirectly linked to the host key used to authenticate the connection, by way of the binding between session ID and hostkey. This situation allows a malicious host that has access to a trusted hostkey to forge bindings and potentially circumvent destination restrictions.

Consider a user on hostA who has authorised a key for authentication to hostB and for forwarding through hostB for authentication to hostC. Assume that an attacker has access to the user's agent socket on hostB and has use of hostC's hostkey. The attacker could establish a connection to some unauthorised host (say hostX) and issue a session-bind@ request to the agent, offering the session ID for the connection to hostX but including a copy of hostC's hostkey and a valid signature. At this point the agent would permit signature requests made over that socket as authorised.

To avoid this situation, we introduce a slightly-modified publickey-hostbound-v00@openssh.com authentication request that extends the data signed by the client to include the server's host key. This effectively makes the host key available to the agent at the time of signing and allows the agent to ensure that it matches the most recently-bound session ID and hostkey.

To enable use of this new public key request, both the client and server must support it. This uses the SSH2_MSG_EXT_INFO extension mechanism to allow the server to inform the client of its availability, via a publickey-hostbound@openssh.com advertisment string. A client with support for the modified method that sees this extension offered by the server may use it.

Protocol extension message formats

Session binding

Session bindings are communicated using an agent extension request:

       byte                    SSH_AGENTC_EXTENSION
       string                  "session-bind@openssh.com"
       string                  hostkey blob
       string                  session ID
       string                  signature
       bool                    is_forwarding

Where is_forwarding is a hint from the client as to whether this connection is intended solely for user authentication (false) or is a forwarding channel for arbitrary future requests (true).

Destination restriction key constraint

Destination restrictions are encoded using a key constraint extension that may be attached to SSH2_AGENTC_ADD_IDENTITY and SSH_AGENTC_ADD_SMARTCARD_KEY requests.

       byte                    SSH_AGENT_CONSTRAIN_EXTENSION
       string                  "restrict-destination-v00@openssh.com"
       string[]                permission

where each permission is the encoded tuple:

       string                  source_hop
       string                  dest_hop
       string                  reserved

The reserved string must be empty at this time. The source and desination hops are encoded as:

       string                  user
       string                  hostname
       string                  reserved
       byte[]                  key_specs

A zero-length username represents any user. The username in the source hop specification must be empty. key_specs is an array of:

       string                  hostkey blob
       bool                    ca_flag

If the ca_flag is set, then the correponding hostkey is assumed to be a trusted CA key for host names matching the hostname wildcard pattern. Otherwise, the hostkey directly specifies the expected host key for the destination and the hostname field is not used for permission checking (though it may still be used for display).

Host-bound public key authentication

The publickey-hostbound-v00@openssh.com authentication method modifies the data signed during public key authentication.

The request packet is unmodified except for the method name:

       byte                    SSH2_MSG_USERAUTH_REQUEST
       string                  user name
       string                  service name
       string                  "publickey-hostbound-v00@openssh.com"
       bool                    TRUE
       string                  public key algorithm name
       string                  public key to be used for authentication
       string                  signature

The signed data is modified to include the server's initial host key after the user key attempting authentication:

       string                  session identifier
       byte                    SSH2_MSG_USERAUTH_REQUEST
       string                  user name
       string                  service name
       string                  "publickey-hostbound-v00@openssh.com"
       bool                    TRUE
       string                  public key algorithm name
       string                  public key to be used for authentication
       string                  initial server host key

Negotiation of host-bound public key authentication

The host-bound public key authentication method is offered via the ext-info mechanism via a SSH2_MSG_EXT_INFO notification. The format of the notification is just the extension name and version number:

       string                  "publickey-hostbound@openssh.com"
       string                  "0"

Testing

A corresponding regression test is at djmdjm/openssh-regress-wip#1

Threat model

This feature is intended to protect forwarded agents. We assume the local host (i.e. the host running the agent) to be trustworthy.

We assume a remote attacker to be an attacker with root-equivalent access on a host that has had a restricted agent forwarded to it. They are expected to have full access to the forwarded agent socket, are able to attempt arbitrary requests over it and be able to make use of the forwarded-to host's host-key. For this design to be successful, such an attacker must have no more access to keys stored in ssh-agent than what was delegated to the remote host.

Potential attacks/vulnerabilities include:

  • Exploitation of memory fault in new attack surface; attacker could gain access to private key material in agent and/or RCE on agent host.
  • Logic error in implementation of permission system; attacker could gain access to use of keys not intended for them (e.g. to connect to a host using unauthorised agent keys)
  • Unrecognised design error in session/host binding protocol that causes it not to offer the guarantees I think it does.
  • Poorly-considered UI in ssh-add command-line interface causes user misunderstanding resulting in unintentional use-of-key exposure.

Future work

The underlying infrastructure implemented here has the potential to offer additional capabilities beyond what has been implemented.

Wildcard forwarding

It would be relatively simple to allow true wildcard hosts in the forwarding
specification. E.g.

ssh-add -h host_a -h 'host_a>*' /path/key

At present, wildcard host names are expanded to the set of host keys matching the wildcard in the local known_hosts files, which isn't really optimal.

Limitation of agent requests

This change only meaningfully restricts visibility and use of keys. It would be possible however, to use the path information made available by the session-bind@openssh.com extension to control access to other agent features.

In particular, it would be possible to restrict a forwarded agent's ability to add or remove keys, or to load a PKCS#11 token into a running agent.

The caveat here is that, unlike signature requests, there is no binding of these other to a host key. As such, the forwarding path only really trustworthy for the first hop.

Addition of restrictions by hop

Similarly, it would be possible to use path information to modify existing key restrictions. For example, it would be possible to require per-signature confirmation (using the existing askpass mechanism) for keys forwarded to/beyond a particular host.

Forwarded access to FIDO tokens

Currently, ssh-agent refuses to sign data using FIDO tokens that does not parse as a SSH messages (e.g. a public key userauth request). This check exists to prevent a forwarded agent from granting access to sign web authentication challenges remotely.

But, sometimes you want to sign web authentication challenges via a forwarded agent. An example might be a command-line too that needs to authenticate to a web server.

It would be possible to use the forwarding path information to selectively relax this restriction, though the above caveat about trustworthiness of the path beyond the first hop apply.

Selective relaxation of host-bound signature requirement

These ssh-agent changes require user authentication signature requests be host-bound for forwarded agents. This has the consequence of requiring upgraded SSH servers for all destinations that the user wished to authenticate to with a forwarded key.

It would similarly be possible to relax this requirement, at the cost of reintroducing the hostkey collusion signing attack that motivated host-bound signatures initially. This relaxation could be done on a per-hop basis.

Credits

Thanks to Markus Friedl and Jann Horn for providing invaluable feedback on the design and implementation. Jann also identified the hostkey re-signing attack with embarrassing speed.

These will be used later for agent session ID / hostkey binding
send session ID, hostkey, signature and a flag indicating whether the
agent connection is being forwarded to ssh agent each time a connection
is opened via a new "session-bind@openssh.com" agent extension.
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.
Have ssh-add accept a list of "destination constraints" that allow
restricting where keys may be used in conjunction with a ssh-agent/ssh
that supports session ID/hostkey binding.

Constraints are specified as either "[user@]host-pattern" or
"host-pattern>[user@]host-pattern".

The first form permits a key to be used to authenticate as the
specified user to the specified host.

The second form permits a key that has previously been permitted
for use at a host to be available via a forwarded agent to an
additional host.

For example, constraining a key with "user1@host_a" and
"host_a>host_b". Would permit authentication as "user1" at
"host_a", and allow the key to be available on an agent forwarded
to "host_a" only for authentication to "host_b". The key would not
be visible on agent forwarded to other hosts or usable for
authentication there.

Internally, destination constraints use host keys to identify hosts.
The host patterns are used to obtain lists of host keys for that
destination that are communicated to the agent. The user/hostkeys are
encoded using a new restrict-destination-v00@openssh.com key
constraint.

host keys are looked up in the default client user/system known_hosts
files. It is possible to override this set on the command-line.
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.
allow authentication methods to have one additional name beyond their
primary name.

allow lookup by this synonym

Use primary name for authentication decisions, e.g. for
PermitRootLogin=publickey

Pass actual invoked name to the authmethods, so they can tell whether they
were requested via the their primary name or synonym.
This is identical to the standard "publickey" method, but it also includes
the initial server hostkey in the message signed by the client.
Add kex->flags member to enable the publickey-hostbound-v00@openssh.com
authentication method.

Use the new hostbound method in client if the kex->flags flag was set,
and include the inital KEX hostkey in the userauth request.

Note: nothing in kex.c actually sets the new flag yet
the EXT_INFO packet gets a new publickey-hostbound@openssh.com to
advertise the hostbound public key method.

Client side support to parse this feature flag and set the kex->flags
indicator if the expected version is offered (currently "0").
Allow parse_userauth_request() to work with blobs from
publickey-hostbound-v00@openssh.com userauth attempts.

Extract hostkey from these blobs.
Require host-bound userauth requests for forwarded SSH connections.

The hostkey parsed from the host-bound userauth request is now checked
against the most recently bound session ID / hostkey on the agent socket
and the signature refused if they do not match.
Allow control over which pubkey methods are used. Added out of
concern that some hardware devices may have difficulty signing
the longer pubkey authentication challenges. This provides a
way for them to disable the extension. It's also handy for
testing.
Copy link
Contributor

@mfriedl mfriedl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

....
tried to comment per commit....

ssh-agent.c Show resolved Hide resolved
ssh-agent.c Show resolved Hide resolved
ssh-agent.c Outdated Show resolved Hide resolved
}
/* XXX logspam */
debug_f("user=%s", user);
if (identity_permitted(id, e, user, &fwd_host, &dest_host) != 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwd_host and dest_host are not used (printed?)

ssh-agent.c Show resolved Hide resolved
@djmdjm
Copy link
Owner Author

djmdjm commented Nov 29, 2021

what is the meaning of d->from.user ?
aren't users only useful as d->to.user?

Right, it's enforced as empty when it is loaded in parse_dest_constraint(). The complete pretty-printing is probably vestigial from an earlier version of the patch :)

        if ((dc->from.hostname == NULL) != (dc->from.nkeys == 0) ||
            dc->from.user != NULL) {

ssh-agent.c Show resolved Hide resolved
ssh-agent.c Outdated Show resolved Hide resolved
@djmdjm
Copy link
Owner Author

djmdjm commented Nov 29, 2021

would this be more explicit? i.e. that 'fromkey' is from the same entry?

hks = e->session_ids[e->nsession_ids - 1];
if (hks->forwarded && user == NULL && permitted_by_dest_constraints(hks->key, NULL, id, NULL, NULL) != 0) { .... }

good point; done

@djmdjm
Copy link
Owner Author

djmdjm commented Nov 29, 2021

fwd_host and dest_host are not used (printed?)

Not at present. I'd like to use them in the askpass UI, but there is enough in this patchset already...

ssh-add.1 Outdated Show resolved Hide resolved
@djmdjm
Copy link
Owner Author

djmdjm commented Nov 29, 2021

/* existing identity not visible, cannot be updated */

done

readconf.h Outdated Show resolved Hide resolved
@djmdjm
Copy link
Owner Author

djmdjm commented Nov 29, 2021

should this be the first check in the loop?

moved

auth2-pubkey.c Show resolved Hide resolved
sshconnect2.c Show resolved Hide resolved
ssh-agent.c Show resolved Hide resolved
ssh-agent.c Show resolved Hide resolved
mfriedl
mfriedl approved these changes Dec 8, 2021
Copy link
Contributor

@mfriedl mfriedl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok markus@

@djmdjm djmdjm mentioned this pull request Nov 16, 2021
@djmdjm djmdjm closed this Dec 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants