1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
5 Implements the SSH v2 key agent protocol. This protocol is documented in the
6 SSH source code, in the file
7 U{PROTOCOL.agent<http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent>}.
9 Maintainer: Paul Swartz
14 from twisted.conch.ssh.common import NS, getNS, getMP
15 from twisted.conch.error import ConchError, MissingKeyStoreError
16 from twisted.conch.ssh import keys
17 from twisted.internet import defer, protocol
21 class SSHAgentClient(protocol.Protocol):
23 The client side of the SSH agent protocol. This is equivalent to
24 ssh-add(1) and can be used with either ssh-agent(1) or the SSHAgentServer
25 protocol, also in this package.
33 def dataReceived(self, data):
36 if len(self.buf) <= 4:
38 packLen = struct.unpack('!L', self.buf[:4])[0]
39 if len(self.buf) < 4 + packLen:
41 packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:]
42 reqType = ord(packet[0])
43 d = self.deferreds.pop(0)
44 if reqType == AGENT_FAILURE:
45 d.errback(ConchError('agent failure'))
46 elif reqType == AGENT_SUCCESS:
52 def sendRequest(self, reqType, data):
53 pack = struct.pack('!LB',len(data) + 1, reqType) + data
54 self.transport.write(pack)
56 self.deferreds.append(d)
60 def requestIdentities(self):
62 @return: A L{Deferred} which will fire with a list of all keys found in
63 the SSH agent. The list of keys is comprised of (public key blob,
66 d = self.sendRequest(AGENTC_REQUEST_IDENTITIES, '')
67 d.addCallback(self._cbRequestIdentities)
71 def _cbRequestIdentities(self, data):
73 Unpack a collection of identities into a list of tuples comprised of
74 public key blobs and comments.
76 if ord(data[0]) != AGENT_IDENTITIES_ANSWER:
77 raise ConchError('unexpected response: %i' % ord(data[0]))
78 numKeys = struct.unpack('!L', data[1:5])[0]
81 for i in range(numKeys):
82 blob, data = getNS(data)
83 comment, data = getNS(data)
84 keys.append((blob, comment))
88 def addIdentity(self, blob, comment = ''):
90 Add a private key blob to the agent's collection of keys.
94 return self.sendRequest(AGENTC_ADD_IDENTITY, req)
97 def signData(self, blob, data):
99 Request that the agent sign the given C{data} with the private key
100 which corresponds to the public key given by C{blob}. The private
101 key should have been added to the agent already.
105 @return: A L{Deferred} which fires with a signature for given data
106 created with the given key.
110 req += '\000\000\000\000' # flags
111 return self.sendRequest(AGENTC_SIGN_REQUEST, req).addCallback(self._cbSignData)
114 def _cbSignData(self, data):
115 if ord(data[0]) != AGENT_SIGN_RESPONSE:
116 raise ConchError('unexpected data: %i' % ord(data[0]))
117 signature = getNS(data[1:])[0]
121 def removeIdentity(self, blob):
123 Remove the private key corresponding to the public key in blob from the
127 return self.sendRequest(AGENTC_REMOVE_IDENTITY, req)
130 def removeAllIdentities(self):
132 Remove all keys from the running agent.
134 return self.sendRequest(AGENTC_REMOVE_ALL_IDENTITIES, '')
138 class SSHAgentServer(protocol.Protocol):
140 The server side of the SSH agent protocol. This is equivalent to
141 ssh-agent(1) and can be used with either ssh-add(1) or the SSHAgentClient
142 protocol, also in this package.
149 def dataReceived(self, data):
152 if len(self.buf) <= 4:
154 packLen = struct.unpack('!L', self.buf[:4])[0]
155 if len(self.buf) < 4 + packLen:
157 packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:]
158 reqType = ord(packet[0])
159 reqName = messages.get(reqType, None)
161 self.sendResponse(AGENT_FAILURE, '')
163 f = getattr(self, 'agentc_%s' % reqName)
164 if getattr(self.factory, 'keys', None) is None:
165 self.sendResponse(AGENT_FAILURE, '')
166 raise MissingKeyStoreError()
170 def sendResponse(self, reqType, data):
171 pack = struct.pack('!LB', len(data) + 1, reqType) + data
172 self.transport.write(pack)
175 def agentc_REQUEST_IDENTITIES(self, data):
177 Return all of the identities that have been added to the server
180 numKeys = len(self.factory.keys)
183 resp.append(struct.pack('!L', numKeys))
184 for key, comment in self.factory.keys.itervalues():
185 resp.append(NS(key.blob())) # yes, wrapped in an NS
186 resp.append(NS(comment))
187 self.sendResponse(AGENT_IDENTITIES_ANSWER, ''.join(resp))
190 def agentc_SIGN_REQUEST(self, data):
192 Data is a structure with a reference to an already added key object and
193 some data that the clients wants signed with that key. If the key
194 object wasn't loaded, return AGENT_FAILURE, else return the signature.
196 blob, data = getNS(data)
197 if blob not in self.factory.keys:
198 return self.sendResponse(AGENT_FAILURE, '')
199 signData, data = getNS(data)
200 assert data == '\000\000\000\000'
201 self.sendResponse(AGENT_SIGN_RESPONSE, NS(self.factory.keys[blob][0].sign(signData)))
204 def agentc_ADD_IDENTITY(self, data):
206 Adds a private key to the agent's collection of identities. On
207 subsequent interactions, the private key can be accessed using only the
208 corresponding public key.
211 # need to pre-read the key data so we can get past it to the comment string
212 keyType, rest = getNS(data)
213 if keyType == 'ssh-rsa':
215 elif keyType == 'ssh-dss':
218 raise keys.BadKeyError('unknown blob type: %s' % keyType)
220 rest = getMP(rest, nmp)[-1] # ignore the key data for now, we just want the comment
221 comment, rest = getNS(rest) # the comment, tacked onto the end of the key blob
223 k = keys.Key.fromString(data, type='private_blob') # not wrapped in NS here
224 self.factory.keys[k.blob()] = (k, comment)
225 self.sendResponse(AGENT_SUCCESS, '')
228 def agentc_REMOVE_IDENTITY(self, data):
230 Remove a specific key from the agent's collection of identities.
232 blob, _ = getNS(data)
233 k = keys.Key.fromString(blob, type='blob')
234 del self.factory.keys[k.blob()]
235 self.sendResponse(AGENT_SUCCESS, '')
238 def agentc_REMOVE_ALL_IDENTITIES(self, data):
240 Remove all keys from the agent's collection of identities.
243 self.factory.keys = {}
244 self.sendResponse(AGENT_SUCCESS, '')
246 # v1 messages that we ignore because we don't keep v1 keys
247 # open-ssh sends both v1 and v2 commands, so we have to
248 # do no-ops for v1 commands or we'll get "bad request" errors
250 def agentc_REQUEST_RSA_IDENTITIES(self, data):
252 v1 message for listing RSA1 keys; superseded by
253 agentc_REQUEST_IDENTITIES, which handles different key types.
255 self.sendResponse(AGENT_RSA_IDENTITIES_ANSWER, struct.pack('!L', 0))
258 def agentc_REMOVE_RSA_IDENTITY(self, data):
260 v1 message for removing RSA1 keys; superseded by
261 agentc_REMOVE_IDENTITY, which handles different key types.
263 self.sendResponse(AGENT_SUCCESS, '')
266 def agentc_REMOVE_ALL_RSA_IDENTITIES(self, data):
268 v1 message for removing all RSA1 keys; superseded by
269 agentc_REMOVE_ALL_IDENTITIES, which handles different key types.
271 self.sendResponse(AGENT_SUCCESS, '')
274 AGENTC_REQUEST_RSA_IDENTITIES = 1
275 AGENT_RSA_IDENTITIES_ANSWER = 2
279 AGENTC_REMOVE_RSA_IDENTITY = 8
280 AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9
282 AGENTC_REQUEST_IDENTITIES = 11
283 AGENT_IDENTITIES_ANSWER = 12
284 AGENTC_SIGN_REQUEST = 13
285 AGENT_SIGN_RESPONSE = 14
286 AGENTC_ADD_IDENTITY = 17
287 AGENTC_REMOVE_IDENTITY = 18
288 AGENTC_REMOVE_ALL_IDENTITIES = 19
291 for name, value in locals().copy().items():
292 if name[:7] == 'AGENTC_':
293 messages[value] = name[7:] # doesn't handle doubles