| 1 |
|
|---|
| 2 |
|
|---|
| 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' |
|---|
| 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())) |
|---|
| 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 |
|
|---|
| 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] |
|---|
| 221 |
comment, rest = getNS(rest) |
|---|
| 222 |
|
|---|
| 223 |
k = keys.Key.fromString(data, type='private_blob') |
|---|
| 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 |
|
|---|
| 247 |
|
|---|
| 248 |
|
|---|
| 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:] |
|---|
| 294 |
|
|---|