agent.py 9.3 KB

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