root/trunk/twisted/conch/ssh/agent.py

Revision 30752, 9.3 KB (checked in by exarkun, 15 months ago)

Rewrite the copyright headers to exclude date information.

Author: exarkun
Reviewer: glyph
Fixes: #4857

To avoid the need to perpetually update copyright dates in each file in Twisted,
remove the dates from most files and just leave them in the LICENSE file.

As a side effect, some files also have had a trailing newline added where it was
missing before.

Line 
1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4"""
5Implements the SSH v2 key agent protocol.  This protocol is documented in the
6SSH source code, in the file
7U{PROTOCOL.agent<http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent>}.
8
9Maintainer: Paul Swartz
10"""
11
12import struct
13
14from twisted.conch.ssh.common import NS, getNS, getMP
15from twisted.conch.error import ConchError, MissingKeyStoreError
16from twisted.conch.ssh import keys
17from twisted.internet import defer, protocol
18
19
20
21class 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
138class 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
274AGENTC_REQUEST_RSA_IDENTITIES   = 1
275AGENT_RSA_IDENTITIES_ANSWER     = 2
276AGENT_FAILURE                   = 5
277AGENT_SUCCESS                   = 6
278
279AGENTC_REMOVE_RSA_IDENTITY         = 8
280AGENTC_REMOVE_ALL_RSA_IDENTITIES   = 9
281
282AGENTC_REQUEST_IDENTITIES       = 11
283AGENT_IDENTITIES_ANSWER         = 12
284AGENTC_SIGN_REQUEST             = 13
285AGENT_SIGN_RESPONSE             = 14
286AGENTC_ADD_IDENTITY             = 17
287AGENTC_REMOVE_IDENTITY          = 18
288AGENTC_REMOVE_ALL_IDENTITIES    = 19
289
290messages = {}
291for name, value in locals().copy().items():
292    if name[:7] == 'AGENTC_':
293        messages[value] = name[7:] # doesn't handle doubles
Note: See TracBrowser for help on using the browser.