agent.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Implements the SSH v2 key agent protocol. This protocol is documented in the
  5. SSH source code, in the file
  6. U{PROTOCOL.agent<http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent>}.
  7. Maintainer: Paul Swartz
  8. """
  9. from __future__ import absolute_import, division
  10. import struct
  11. from twisted.conch.ssh.common import NS, getNS, getMP
  12. from twisted.conch.error import ConchError, MissingKeyStoreError
  13. from twisted.conch.ssh import keys
  14. from twisted.internet import defer, protocol
  15. from twisted.python.compat import itervalues
  16. class SSHAgentClient(protocol.Protocol):
  17. """
  18. The client side of the SSH agent protocol. This is equivalent to
  19. ssh-add(1) and can be used with either ssh-agent(1) or the SSHAgentServer
  20. protocol, also in this package.
  21. """
  22. def __init__(self):
  23. self.buf = b''
  24. self.deferreds = []
  25. def dataReceived(self, data):
  26. self.buf += data
  27. while 1:
  28. if len(self.buf) <= 4:
  29. return
  30. packLen = struct.unpack('!L', self.buf[:4])[0]
  31. if len(self.buf) < 4 + packLen:
  32. return
  33. packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:]
  34. reqType = ord(packet[0:1])
  35. d = self.deferreds.pop(0)
  36. if reqType == AGENT_FAILURE:
  37. d.errback(ConchError('agent failure'))
  38. elif reqType == AGENT_SUCCESS:
  39. d.callback(b'')
  40. else:
  41. d.callback(packet)
  42. def sendRequest(self, reqType, data):
  43. pack = struct.pack('!LB',len(data) + 1, reqType) + data
  44. self.transport.write(pack)
  45. d = defer.Deferred()
  46. self.deferreds.append(d)
  47. return d
  48. def requestIdentities(self):
  49. """
  50. @return: A L{Deferred} which will fire with a list of all keys found in
  51. the SSH agent. The list of keys is comprised of (public key blob,
  52. comment) tuples.
  53. """
  54. d = self.sendRequest(AGENTC_REQUEST_IDENTITIES, b'')
  55. d.addCallback(self._cbRequestIdentities)
  56. return d
  57. def _cbRequestIdentities(self, data):
  58. """
  59. Unpack a collection of identities into a list of tuples comprised of
  60. public key blobs and comments.
  61. """
  62. if ord(data[0:1]) != AGENT_IDENTITIES_ANSWER:
  63. raise ConchError('unexpected response: %i' % ord(data[0:1]))
  64. numKeys = struct.unpack('!L', data[1:5])[0]
  65. result = []
  66. data = data[5:]
  67. for i in range(numKeys):
  68. blob, data = getNS(data)
  69. comment, data = getNS(data)
  70. result.append((blob, comment))
  71. return result
  72. def addIdentity(self, blob, comment = b''):
  73. """
  74. Add a private key blob to the agent's collection of keys.
  75. """
  76. req = blob
  77. req += NS(comment)
  78. return self.sendRequest(AGENTC_ADD_IDENTITY, req)
  79. def signData(self, blob, data):
  80. """
  81. Request that the agent sign the given C{data} with the private key
  82. which corresponds to the public key given by C{blob}. The private
  83. key should have been added to the agent already.
  84. @type blob: L{bytes}
  85. @type data: L{bytes}
  86. @return: A L{Deferred} which fires with a signature for given data
  87. created with the given key.
  88. """
  89. req = NS(blob)
  90. req += NS(data)
  91. req += b'\000\000\000\000' # flags
  92. return self.sendRequest(AGENTC_SIGN_REQUEST, req).addCallback(self._cbSignData)
  93. def _cbSignData(self, data):
  94. if ord(data[0:1]) != AGENT_SIGN_RESPONSE:
  95. raise ConchError('unexpected data: %i' % ord(data[0:1]))
  96. signature = getNS(data[1:])[0]
  97. return signature
  98. def removeIdentity(self, blob):
  99. """
  100. Remove the private key corresponding to the public key in blob from the
  101. running agent.
  102. """
  103. req = NS(blob)
  104. return self.sendRequest(AGENTC_REMOVE_IDENTITY, req)
  105. def removeAllIdentities(self):
  106. """
  107. Remove all keys from the running agent.
  108. """
  109. return self.sendRequest(AGENTC_REMOVE_ALL_IDENTITIES, b'')
  110. class SSHAgentServer(protocol.Protocol):
  111. """
  112. The server side of the SSH agent protocol. This is equivalent to
  113. ssh-agent(1) and can be used with either ssh-add(1) or the SSHAgentClient
  114. protocol, also in this package.
  115. """
  116. def __init__(self):
  117. self.buf = b''
  118. def dataReceived(self, data):
  119. self.buf += data
  120. while 1:
  121. if len(self.buf) <= 4:
  122. return
  123. packLen = struct.unpack('!L', self.buf[:4])[0]
  124. if len(self.buf) < 4 + packLen:
  125. return
  126. packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:]
  127. reqType = ord(packet[0:1])
  128. reqName = messages.get(reqType, None)
  129. if not reqName:
  130. self.sendResponse(AGENT_FAILURE, b'')
  131. else:
  132. f = getattr(self, 'agentc_%s' % reqName)
  133. if getattr(self.factory, 'keys', None) is None:
  134. self.sendResponse(AGENT_FAILURE, b'')
  135. raise MissingKeyStoreError()
  136. f(packet[1:])
  137. def sendResponse(self, reqType, data):
  138. pack = struct.pack('!LB', len(data) + 1, reqType) + data
  139. self.transport.write(pack)
  140. def agentc_REQUEST_IDENTITIES(self, data):
  141. """
  142. Return all of the identities that have been added to the server
  143. """
  144. assert data == b''
  145. numKeys = len(self.factory.keys)
  146. resp = []
  147. resp.append(struct.pack('!L', numKeys))
  148. for key, comment in itervalues(self.factory.keys):
  149. resp.append(NS(key.blob())) # yes, wrapped in an NS
  150. resp.append(NS(comment))
  151. self.sendResponse(AGENT_IDENTITIES_ANSWER, b''.join(resp))
  152. def agentc_SIGN_REQUEST(self, data):
  153. """
  154. Data is a structure with a reference to an already added key object and
  155. some data that the clients wants signed with that key. If the key
  156. object wasn't loaded, return AGENT_FAILURE, else return the signature.
  157. """
  158. blob, data = getNS(data)
  159. if blob not in self.factory.keys:
  160. return self.sendResponse(AGENT_FAILURE, b'')
  161. signData, data = getNS(data)
  162. assert data == b'\000\000\000\000'
  163. self.sendResponse(AGENT_SIGN_RESPONSE, NS(self.factory.keys[blob][0].sign(signData)))
  164. def agentc_ADD_IDENTITY(self, data):
  165. """
  166. Adds a private key to the agent's collection of identities. On
  167. subsequent interactions, the private key can be accessed using only the
  168. corresponding public key.
  169. """
  170. # need to pre-read the key data so we can get past it to the comment string
  171. keyType, rest = getNS(data)
  172. if keyType == b'ssh-rsa':
  173. nmp = 6
  174. elif keyType == b'ssh-dss':
  175. nmp = 5
  176. else:
  177. raise keys.BadKeyError('unknown blob type: %s' % keyType)
  178. rest = getMP(rest, nmp)[-1] # ignore the key data for now, we just want the comment
  179. comment, rest = getNS(rest) # the comment, tacked onto the end of the key blob
  180. k = keys.Key.fromString(data, type='private_blob') # not wrapped in NS here
  181. self.factory.keys[k.blob()] = (k, comment)
  182. self.sendResponse(AGENT_SUCCESS, b'')
  183. def agentc_REMOVE_IDENTITY(self, data):
  184. """
  185. Remove a specific key from the agent's collection of identities.
  186. """
  187. blob, _ = getNS(data)
  188. k = keys.Key.fromString(blob, type='blob')
  189. del self.factory.keys[k.blob()]
  190. self.sendResponse(AGENT_SUCCESS, b'')
  191. def agentc_REMOVE_ALL_IDENTITIES(self, data):
  192. """
  193. Remove all keys from the agent's collection of identities.
  194. """
  195. assert data == b''
  196. self.factory.keys = {}
  197. self.sendResponse(AGENT_SUCCESS, b'')
  198. # v1 messages that we ignore because we don't keep v1 keys
  199. # open-ssh sends both v1 and v2 commands, so we have to
  200. # do no-ops for v1 commands or we'll get "bad request" errors
  201. def agentc_REQUEST_RSA_IDENTITIES(self, data):
  202. """
  203. v1 message for listing RSA1 keys; superseded by
  204. agentc_REQUEST_IDENTITIES, which handles different key types.
  205. """
  206. self.sendResponse(AGENT_RSA_IDENTITIES_ANSWER, struct.pack('!L', 0))
  207. def agentc_REMOVE_RSA_IDENTITY(self, data):
  208. """
  209. v1 message for removing RSA1 keys; superseded by
  210. agentc_REMOVE_IDENTITY, which handles different key types.
  211. """
  212. self.sendResponse(AGENT_SUCCESS, b'')
  213. def agentc_REMOVE_ALL_RSA_IDENTITIES(self, data):
  214. """
  215. v1 message for removing all RSA1 keys; superseded by
  216. agentc_REMOVE_ALL_IDENTITIES, which handles different key types.
  217. """
  218. self.sendResponse(AGENT_SUCCESS, b'')
  219. AGENTC_REQUEST_RSA_IDENTITIES = 1
  220. AGENT_RSA_IDENTITIES_ANSWER = 2
  221. AGENT_FAILURE = 5
  222. AGENT_SUCCESS = 6
  223. AGENTC_REMOVE_RSA_IDENTITY = 8
  224. AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9
  225. AGENTC_REQUEST_IDENTITIES = 11
  226. AGENT_IDENTITIES_ANSWER = 12
  227. AGENTC_SIGN_REQUEST = 13
  228. AGENT_SIGN_RESPONSE = 14
  229. AGENTC_ADD_IDENTITY = 17
  230. AGENTC_REMOVE_IDENTITY = 18
  231. AGENTC_REMOVE_ALL_IDENTITIES = 19
  232. messages = {}
  233. for name, value in locals().copy().items():
  234. if name[:7] == 'AGENTC_':
  235. messages[value] = name[7:] # doesn't handle doubles