| 1 | # Copyright (c) Twisted Matrix Laboratories. |
|---|
| 2 | # See LICENSE for details. |
|---|
| 3 | |
|---|
| 4 | """ |
|---|
| 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>}. |
|---|
| 8 | |
|---|
| 9 | Maintainer: Paul Swartz |
|---|
| 10 | """ |
|---|
| 11 | |
|---|
| 12 | import struct |
|---|
| 13 | |
|---|
| 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 |
|---|
| 18 | |
|---|
| 19 | |
|---|
| 20 | |
|---|
| 21 | class SSHAgentClient(protocol.Protocol): |
|---|
| 22 | """ |
|---|
| 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. |
|---|
| 26 | """ |
|---|
| 27 | |
|---|
| 28 | def __init__(self): |
|---|
| 29 | self.buf = '' |
|---|
| 30 | self.deferreds = [] |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | def dataReceived(self, data): |
|---|
| 34 | self.buf += data |
|---|
| 35 | while 1: |
|---|
| 36 | if len(self.buf) <= 4: |
|---|
| 37 | return |
|---|
| 38 | packLen = struct.unpack('!L', self.buf[:4])[0] |
|---|
| 39 | if len(self.buf) < 4 + packLen: |
|---|
| 40 | return |
|---|
| 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: |
|---|
| 47 | d.callback('') |
|---|
| 48 | else: |
|---|
| 49 | d.callback(packet) |
|---|
| 50 | |
|---|
| 51 | |
|---|
| 52 | def sendRequest(self, reqType, data): |
|---|
| 53 | pack = struct.pack('!LB',len(data) + 1, reqType) + data |
|---|
| 54 | self.transport.write(pack) |
|---|
| 55 | d = defer.Deferred() |
|---|
| 56 | self.deferreds.append(d) |
|---|
| 57 | return d |
|---|
| 58 | |
|---|
| 59 | |
|---|
| 60 | def requestIdentities(self): |
|---|
| 61 | """ |
|---|
| 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, |
|---|
| 64 | comment) tuples. |
|---|
| 65 | """ |
|---|
| 66 | d = self.sendRequest(AGENTC_REQUEST_IDENTITIES, '') |
|---|
| 67 | d.addCallback(self._cbRequestIdentities) |
|---|
| 68 | return d |
|---|
| 69 | |
|---|
| 70 | |
|---|
| 71 | def _cbRequestIdentities(self, data): |
|---|
| 72 | """ |
|---|
| 73 | Unpack a collection of identities into a list of tuples comprised of |
|---|
| 74 | public key blobs and comments. |
|---|
| 75 | """ |
|---|
| 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] |
|---|
| 79 | keys = [] |
|---|
| 80 | data = data[5:] |
|---|
| 81 | for i in range(numKeys): |
|---|
| 82 | blob, data = getNS(data) |
|---|
| 83 | comment, data = getNS(data) |
|---|
| 84 | keys.append((blob, comment)) |
|---|
| 85 | return keys |
|---|
| 86 | |
|---|
| 87 | |
|---|
| 88 | def addIdentity(self, blob, comment = ''): |
|---|
| 89 | """ |
|---|
| 90 | Add a private key blob to the agent's collection of keys. |
|---|
| 91 | """ |
|---|
| 92 | req = blob |
|---|
| 93 | req += NS(comment) |
|---|
| 94 | return self.sendRequest(AGENTC_ADD_IDENTITY, req) |
|---|
| 95 | |
|---|
| 96 | |
|---|
| 97 | def signData(self, blob, data): |
|---|
| 98 | """ |
|---|
| 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. |
|---|
| 102 | |
|---|
| 103 | @type blob: C{str} |
|---|
| 104 | @type data: C{str} |
|---|
| 105 | @return: A L{Deferred} which fires with a signature for given data |
|---|
| 106 | created with the given key. |
|---|
| 107 | """ |
|---|
| 108 | req = NS(blob) |
|---|
| 109 | req += NS(data) |
|---|
| 110 | req += '\000\000\000\000' # flags |
|---|
| 111 | return self.sendRequest(AGENTC_SIGN_REQUEST, req).addCallback(self._cbSignData) |
|---|
| 112 | |
|---|
| 113 | |
|---|
| 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] |
|---|
| 118 | return signature |
|---|
| 119 | |
|---|
| 120 | |
|---|
| 121 | def removeIdentity(self, blob): |
|---|
| 122 | """ |
|---|
| 123 | Remove the private key corresponding to the public key in blob from the |
|---|
| 124 | running agent. |
|---|
| 125 | """ |
|---|
| 126 | req = NS(blob) |
|---|
| 127 | return self.sendRequest(AGENTC_REMOVE_IDENTITY, req) |
|---|
| 128 | |
|---|
| 129 | |
|---|
| 130 | def removeAllIdentities(self): |
|---|
| 131 | """ |
|---|
| 132 | Remove all keys from the running agent. |
|---|
| 133 | """ |
|---|
| 134 | return self.sendRequest(AGENTC_REMOVE_ALL_IDENTITIES, '') |
|---|
| 135 | |
|---|
| 136 | |
|---|
| 137 | |
|---|
| 138 | class SSHAgentServer(protocol.Protocol): |
|---|
| 139 | """ |
|---|
| 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. |
|---|
| 143 | """ |
|---|
| 144 | |
|---|
| 145 | def __init__(self): |
|---|
| 146 | self.buf = '' |
|---|
| 147 | |
|---|
| 148 | |
|---|
| 149 | def dataReceived(self, data): |
|---|
| 150 | self.buf += data |
|---|
| 151 | while 1: |
|---|
| 152 | if len(self.buf) <= 4: |
|---|
| 153 | return |
|---|
| 154 | packLen = struct.unpack('!L', self.buf[:4])[0] |
|---|
| 155 | if len(self.buf) < 4 + packLen: |
|---|
| 156 | return |
|---|
| 157 | packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:] |
|---|
| 158 | reqType = ord(packet[0]) |
|---|
| 159 | reqName = messages.get(reqType, None) |
|---|
| 160 | if not reqName: |
|---|
| 161 | self.sendResponse(AGENT_FAILURE, '') |
|---|
| 162 | else: |
|---|
| 163 | f = getattr(self, 'agentc_%s' % reqName) |
|---|
| 164 | if getattr(self.factory, 'keys', None) is None: |
|---|
| 165 | self.sendResponse(AGENT_FAILURE, '') |
|---|
| 166 | raise MissingKeyStoreError() |
|---|
| 167 | f(packet[1:]) |
|---|
| 168 | |
|---|
| 169 | |
|---|
| 170 | def sendResponse(self, reqType, data): |
|---|
| 171 | pack = struct.pack('!LB', len(data) + 1, reqType) + data |
|---|
| 172 | self.transport.write(pack) |
|---|
| 173 | |
|---|
| 174 | |
|---|
| 175 | def agentc_REQUEST_IDENTITIES(self, data): |
|---|
| 176 | """ |
|---|
| 177 | Return all of the identities that have been added to the server |
|---|
| 178 | """ |
|---|
| 179 | assert data == '' |
|---|
| 180 | numKeys = len(self.factory.keys) |
|---|
| 181 | resp = [] |
|---|
| 182 | |
|---|
| 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)) |
|---|
| 188 | |
|---|
| 189 | |
|---|
| 190 | def agentc_SIGN_REQUEST(self, data): |
|---|
| 191 | """ |
|---|
| 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. |
|---|
| 195 | """ |
|---|
| 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))) |
|---|
| 202 | |
|---|
| 203 | |
|---|
| 204 | def agentc_ADD_IDENTITY(self, data): |
|---|
| 205 | """ |
|---|
| 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. |
|---|
| 209 | """ |
|---|
| 210 | |
|---|
| 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': |
|---|
| 214 | nmp = 6 |
|---|
| 215 | elif keyType == 'ssh-dss': |
|---|
| 216 | nmp = 5 |
|---|
| 217 | else: |
|---|
| 218 | raise keys.BadKeyError('unknown blob type: %s' % keyType) |
|---|
| 219 | |
|---|
| 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 |
|---|
| 222 | |
|---|
| 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, '') |
|---|
| 226 | |
|---|
| 227 | |
|---|
| 228 | def agentc_REMOVE_IDENTITY(self, data): |
|---|
| 229 | """ |
|---|
| 230 | Remove a specific key from the agent's collection of identities. |
|---|
| 231 | """ |
|---|
| 232 | blob, _ = getNS(data) |
|---|
| 233 | k = keys.Key.fromString(blob, type='blob') |
|---|
| 234 | del self.factory.keys[k.blob()] |
|---|
| 235 | self.sendResponse(AGENT_SUCCESS, '') |
|---|
| 236 | |
|---|
| 237 | |
|---|
| 238 | def agentc_REMOVE_ALL_IDENTITIES(self, data): |
|---|
| 239 | """ |
|---|
| 240 | Remove all keys from the agent's collection of identities. |
|---|
| 241 | """ |
|---|
| 242 | assert data == '' |
|---|
| 243 | self.factory.keys = {} |
|---|
| 244 | self.sendResponse(AGENT_SUCCESS, '') |
|---|
| 245 | |
|---|
| 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 |
|---|
| 249 | |
|---|
| 250 | def agentc_REQUEST_RSA_IDENTITIES(self, data): |
|---|
| 251 | """ |
|---|
| 252 | v1 message for listing RSA1 keys; superseded by |
|---|
| 253 | agentc_REQUEST_IDENTITIES, which handles different key types. |
|---|
| 254 | """ |
|---|
| 255 | self.sendResponse(AGENT_RSA_IDENTITIES_ANSWER, struct.pack('!L', 0)) |
|---|
| 256 | |
|---|
| 257 | |
|---|
| 258 | def agentc_REMOVE_RSA_IDENTITY(self, data): |
|---|
| 259 | """ |
|---|
| 260 | v1 message for removing RSA1 keys; superseded by |
|---|
| 261 | agentc_REMOVE_IDENTITY, which handles different key types. |
|---|
| 262 | """ |
|---|
| 263 | self.sendResponse(AGENT_SUCCESS, '') |
|---|
| 264 | |
|---|
| 265 | |
|---|
| 266 | def agentc_REMOVE_ALL_RSA_IDENTITIES(self, data): |
|---|
| 267 | """ |
|---|
| 268 | v1 message for removing all RSA1 keys; superseded by |
|---|
| 269 | agentc_REMOVE_ALL_IDENTITIES, which handles different key types. |
|---|
| 270 | """ |
|---|
| 271 | self.sendResponse(AGENT_SUCCESS, '') |
|---|
| 272 | |
|---|
| 273 | |
|---|
| 274 | AGENTC_REQUEST_RSA_IDENTITIES = 1 |
|---|
| 275 | AGENT_RSA_IDENTITIES_ANSWER = 2 |
|---|
| 276 | AGENT_FAILURE = 5 |
|---|
| 277 | AGENT_SUCCESS = 6 |
|---|
| 278 | |
|---|
| 279 | AGENTC_REMOVE_RSA_IDENTITY = 8 |
|---|
| 280 | AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9 |
|---|
| 281 | |
|---|
| 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 |
|---|
| 289 | |
|---|
| 290 | messages = {} |
|---|
| 291 | for name, value in locals().copy().items(): |
|---|
| 292 | if name[:7] == 'AGENTC_': |
|---|
| 293 | messages[value] = name[7:] # doesn't handle doubles |
|---|