This blog post explains what an SSH agent does, and then gives initial hints and presents some example code using the ssh-agent wire protocol (the SSH2 version of it, as implemented by OpenSSH) for listing the public keys added to the agent, and for signing a message with an RSA key. The motivation for this blog post is to teach the reader how to use ssh-agent to sign a message with RSA. Currently there is no command-line tool for that.
The SSH agent (started as the ssh-agent command in Unix, or usually as Pageant in Windows) is a background application which can store in its process memory some SSH public key pairs in unencrypted form, for the convenience of the user. When logging in, ssh-agent is usually started for the user; the user then calls ssh-add (or a similar GUI application) to add his key pairs (e.g.
$HOME/.ssh/id*) to the agent, typing the key passphrases if necessary. Afterwards, the user initiates SSH connections, for which the keys used for authentication are taken from the currently running agent. The SSH agent provides the convenience for the user that the user doesn't have to type the key passphrases multiple time, plus that if agent forwarding is enabled (
ForwardAgent yes is present in
$HOME/.ssh/config), then the agent is available in SSH connections initiated from SSH connections (of arbitrary depth). The agent forwarding feature of ssh-agent is unique, because other in-memory key stores such as gpg-agent don't have that feature, so the keys stored there are available only locally, and not within SSH sessions.
The SSH agent stores both the public and the private keys of a key pair (and the comment as well), but it only ever discloses the public keys to applications connecting to it. The public keys can be queried (displayed) with the
ssh-add -L command. But to the ssh-agent can prove to the external world that it knows the private keys (without revealing the keys themselves), because it offers a service to sign the SHA-1 checksum (hash) of any string. SSH uses public-key cryptography, which has the basic assumption that a party can sign a message with a key only he knows the private key; but anyone who knows the public key can verify the signature.
ssh-add uses the
SSH_AUTH_SOCK environment variable (containing the pathname of a Unix domain socket) to figure out which ssh-agent to connect to. The permissions for the socket pathname are set up so that only the rightful owner (or root) can connect, other users get a Permission denied.
For more information about how SSH uses public-key cryptography and agents, please read this excellent article.
Below there is an example wire dump of an application using the SSH agent protocol to ask the list of public keys added to an ssh-agent (just like
ssh-add -L), and to ask the ssh-agent to sign a message with an RSA key added to it. The sign request is usually sent when an SSH client (ssh(1)) is authenticating itself to an SSH server, when establishing a connection.
To understand the wire dump below, one has to know that in the RSA public-key cryptography system the public key consists of a modulus and a public exponent, the modulus being a very large integer (2048 bits or longer), the exponent being positive a small or large integer, smaller than the modulus. Verifying a signature consits of interpreting a signature as a large integer, exponentiating it to the public exponent, taking the result modulo the modulus, and comparing that result with the original message (or, in case of SSH, the SHA-1 checksum of the original message). Please read the article linked above for a full definition and the operation of RSA.
# The client connects to the agent. client.connect_to_agent() # The clients lists the key pairs added to the agent. client.write("\0\0\0\1") # request_size == 1 client.write("\v") # 11 == SSH2_AGENTC_REQUEST_IDENTITIES agent.write("\0\0\3\5") # response_size == 773 agent.write("\f") # 12 == SSH2_AGENT_IDENTITIES_ANSWER agent.write("\0\0\0\2") # num_entries == 2 agent.write("\0\0\1\261") # entry.key_size == 433 agent.write("\0\0\0\7") # key_type_size == 7 agent.write("ssh-dss") agent.write("...") # 443-4-7 bytes of the DSA key agent.write("\0\0\0\25") # entry.comment_size == 21 agent.write("/home/foo/.ssh/id_dsa") agent.write("\0\0\1\25") # entry.key_size == 275 agent.write("\0\0\0\7") # key_type_size == 7 agent.write("ssh-rsa") agent.write("\0\0\0\1") # public_exponent_size == 1 agent.write("#") # public_exponent == 35 agent.write("\0\0\1\1") # modulus_size == 257 agent.write("\0...") # p * q in MSBFirst order agent.write("\0\0\0\25") # entry.comment_size == 21 agent.write("/home/foo/.ssh/id_rsa") # The client gets the agent sign some data. data = "..." # 356 arbitrary bytes to get signed. client.write("\0\0\2\206") # request_size == 646 client.write("\r") # 13 == SSH2_AGENTC_SIGN_REQUEST client.write("\0\0\1\25") # key_size == 277 client.write("\0\0\0\7") # key_type_size == 7 client.write("ssh-rsa") client.write("\0\0\0\1") # public_exponent_size == 1 client.write("#") # public_exponent == 35 client.write("\0\0\1\1") # modulus_size == 257 client.write("\0...") # p * q in MSBFirst order client.write("\0\0\1d") # data_size == 356 client.write(data) # arbitary bytes to sign client.write("\0\0\0\0") # flags == 0 agent.write("\0\0\1\24") # response_size == 276 agent.write("\16") # 14 == SSH2_AGENT_SIGN_RESPONSE (could be 5 == SSH_AGENT_FAILURE) agent.write("\0\0\1\17") # signature_size == 271 agent.write("\0\0\0\7") # key_type_size == 7 agent.write("ssh-rsa") agent.write("\0\0\1\0") # signed_value_size == 256 agent.write("...") # MSBFirst order # The client closes the SSH connection. client.close() agent.read("") # EOF
Below there is an example Python script which acts as a client to ssh-agent, listing the keys added (similarly to
ssh-add -L), selecting a key, asking ssh-agent to sign a message with an RSA key, and finally verifying the signature. View and download the latest version of the script here.
#! /usr/bin/python2.4 import cStringIO import os import re import sha import socket import struct import sys SSH2_AGENTC_REQUEST_IDENTITIES = 11 SSH2_AGENT_IDENTITIES_ANSWER = 12 SSH2_AGENTC_SIGN_REQUEST = 13 SSH2_AGENT_SIGN_RESPONSE = 14 SSH_AGENT_FAILURE = 5 def RecvAll(sock, size): if size == 0: return '' assert size >= 0 if hasattr(sock, 'recv'): recv = sock.recv else: recv = sock.read data = recv(size) if len(data) >= size: return data assert data, 'unexpected EOF' output = [data] size -= len(data) while size > 0: output.append(recv(size)) assert output[-1], 'unexpected EOF' size -= len(output[-1]) return ''.join(output) def RecvU32(sock): return struct.unpack('>L', RecvAll(sock, 4)) def RecvStr(sock): return RecvAll(sock, RecvU32(sock)) def AppendStr(ary, data): assert isinstance(data, str) ary.append(struct.pack('>L', len(data))) ary.append(data) if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv: ssh_key_comment = sys.argv else: # We won't open this file, but we will use the file name to select the key # added to the SSH agent. ssh_key_comment = '%s/.ssh/id_rsa' % os.getenv('HOME') if len(sys.argv) > 2: # There is no limitation on the message size (because ssh-agent will # SHA-1 it before signing anywa). msg_to_sign = sys.argv else: msg_to_sign = 'Hello, World! Test message to sign.' # Connect to ssh-agent. assert 'SSH_AUTH_SOCK' in os.environ, ( 'ssh-agent not found, set SSH_AUTH_SOCK') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) sock.connect(os.getenv('SSH_AUTH_SOCK')) # Get list of public keys, and find our key. sock.sendall('\0\0\0\1\v') # SSH2_AGENTC_REQUEST_IDENTITIES response = RecvStr(sock) resf = cStringIO.StringIO(response) assert RecvAll(resf, 1) == chr(SSH2_AGENT_IDENTITIES_ANSWER) num_keys = RecvU32(resf) assert num_keys < 2000 # A quick sanity check. assert num_keys, 'no keys have_been added to ssh-agent' matching_keys =  for i in xrange(num_keys): key = RecvStr(resf) comment = RecvStr(resf) if comment == ssh_key_comment: matching_keys.append(key) assert '' == resf.read(1), 'EOF expected in resf' assert matching_keys, 'no keys in ssh-agent with comment %r' % ssh_key_comment assert len(matching_keys) == 1, ( 'multiple keys in ssh-agent with comment %r' % ssh_key_comment) assert matching_keys.startswith('\x00\x00\x00\x07ssh-rsa\x00\x00'), ( 'non-RSA key in ssh-agent with comment %r' % ssh_key_comment) keyf = cStringIO.StringIO(matching_keys[11:]) public_exponent = int(RecvStr(keyf).encode('hex'), 16) modulus_str = RecvStr(keyf) modulus = int(modulus_str.encode('hex'), 16) assert '' == keyf.read(1), 'EOF expected in keyf' # Ask ssh-agent to sign with our key. request_output = [chr(SSH2_AGENTC_SIGN_REQUEST)] AppendStr(request_output, matching_keys) AppendStr(request_output, msg_to_sign) request_output.append(struct.pack('>L', 0)) # flags == 0 full_request_output =  AppendStr(full_request_output, ''.join(request_output)) full_request_str = ''.join(full_request_output) sock.sendall(full_request_str) response = RecvStr(sock) resf = cStringIO.StringIO(response) assert RecvAll(resf, 1) == chr(SSH2_AGENT_SIGN_RESPONSE) signature = RecvStr(resf) assert '' == resf.read(1), 'EOF expected in resf' assert signature.startswith('\0\0\0\7ssh-rsa\0\0') sigf = cStringIO.StringIO(signature[11:]) signed_value = int(RecvStr(sigf).encode('hex'), 16) assert '' == sigf.read(1), 'EOF expected in sigf' # Verify the signature. decoded_value = pow(signed_value, public_exponent, modulus) decoded_hex = '%x' % decoded_value if len(decoded_hex) & 1: decoded_hex = '0' + decoded_hex decoded_str = decoded_hex.decode('hex') assert len(decoded_str) == len(modulus_str) - 2 # e.g. (255, 257) assert re.match(r'\x01\xFF+\Z', decoded_str[:-36]), 'bad padding found' expected_sha1_hex = decoded_hex[-40:] msg_sha1_hex = sha.sha(msg_to_sign).hexdigest() assert expected_sha1_hex == msg_sha1_hex, 'bad signature (SHA1 mismatch)' print >>sys.stderr, 'info: good signature'
The wire dump above was created by attaching to a running ssh-agent with strace. The Python example code was written by understanding the ssh-agent.c in the OpenSSH codebase.