root / trunk / twisted / conch / checkers.py

Revision 26534, 8.9 kB (checked in by z3p, 3 months ago)

Merge 'userauth-2682-7'

Author: z3p
Reviewers: exarkun, radix, therve

Fixes #2682

This updates the SSHv2 user authentication framework in Conch tohave a full
set of unittests.

Line 
1 # -*- test-case-name: twisted.conch.test.test_checkers -*-
2 # Copyright (c) 2001-2009 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 Provide L{ICredentialsChecker} implementations to be used in Conch protocols.
7 """
8
9 import os, base64, binascii, errno
10 try:
11     import pwd
12 except ImportError:
13     pwd = None
14 else:
15     import crypt
16
17 try:
18     # get this from http://www.twistedmatrix.com/users/z3p/files/pyshadow-0.2.tar.gz
19     import shadow
20 except:
21     shadow = None
22
23 try:
24     from twisted.cred import pamauth
25 except ImportError:
26     pamauth = None
27
28 from zope.interface import implements, providedBy
29
30 from twisted.conch import error
31 from twisted.conch.ssh import keys
32 from twisted.cred.checkers import ICredentialsChecker
33 from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey
34 from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials
35 from twisted.internet import defer
36 from twisted.python import failure, reflect, log
37 from twisted.python.util import runAsEffectiveUser
38
39 def verifyCryptedPassword(crypted, pw):
40     if crypted[0] == '$': # md5_crypt encrypted
41         salt = '$1$' + crypted.split('$')[2]
42     else:
43         salt = crypted[:2]
44     return crypt.crypt(pw, salt) == crypted
45
46 class UNIXPasswordDatabase:
47     credentialInterfaces = IUsernamePassword,
48     implements(ICredentialsChecker)
49
50     def requestAvatarId(self, credentials):
51         if pwd:
52             try:
53                 cryptedPass = pwd.getpwnam(credentials.username)[1]
54             except KeyError:
55                 return defer.fail(UnauthorizedLogin("invalid username"))
56             else:
57                 if cryptedPass not in ['*', 'x'] and \
58                     verifyCryptedPassword(cryptedPass, credentials.password):
59                     return defer.succeed(credentials.username)
60         if shadow:
61             gid = os.getegid()
62             uid = os.geteuid()
63             os.setegid(0)
64             os.seteuid(0)
65             try:
66                 shadowPass = shadow.getspnam(credentials.username)[1]
67             except KeyError:
68                 os.setegid(gid)
69                 os.seteuid(uid)
70                 return defer.fail(UnauthorizedLogin("invalid username"))
71             os.setegid(gid)
72             os.seteuid(uid)
73             if verifyCryptedPassword(shadowPass, credentials.password):
74                 return defer.succeed(credentials.username)
75             return defer.fail(UnauthorizedLogin("invalid password"))
76
77         return defer.fail(UnauthorizedLogin("unable to verify password"))
78
79
80 class SSHPublicKeyDatabase:
81     """
82     Checker that authenticates SSH public keys, based on public keys listed in
83     authorized_keys and authorized_keys2 files in user .ssh/ directories.
84     """
85
86     credentialInterfaces = ISSHPrivateKey,
87     implements(ICredentialsChecker)
88
89     def requestAvatarId(self, credentials):
90         d = defer.maybeDeferred(self.checkKey, credentials)
91         d.addCallback(self._cbRequestAvatarId, credentials)
92         d.addErrback(self._ebRequestAvatarId)
93         return d
94
95     def _cbRequestAvatarId(self, validKey, credentials):
96         """
97         Check whether the credentials themselves are valid, now that we know
98         if the key matches the user.
99
100         @param validKey: A boolean indicating whether or not the public key
101             matches a key in the user's authorized_keys file.
102
103         @param credentials: The credentials offered by the user.
104         @type credentials: L{ISSHPrivateKey} provider
105
106         @raise UnauthorizedLogin: (as a failure) if the key does not match the
107             user in C{credentials}. Also raised if the user provides an invalid
108             signature.
109
110         @raise ValidPublicKey: (as a failure) if the key matches the user but
111             the credentials do not include a signature. See
112             L{error.ValidPublicKey} for more information.
113
114         @return: The user's username, if authentication was successful.
115         """
116         if not validKey:
117             return failure.Failure(UnauthorizedLogin("invalid key"))
118         if not credentials.signature:
119             return failure.Failure(error.ValidPublicKey())
120         else:
121             try:
122                 pubKey = keys.Key.fromString(credentials.blob)
123                 if pubKey.verify(credentials.signature, credentials.sigData):
124                     return credentials.username
125             except: # any error should be treated as a failed login
126                 log.err()
127                 return failure.Failure(UnauthorizedLogin('error while verifying key'))
128         return failure.Failure(UnauthorizedLogin("unable to verify key"))
129
130     def checkKey(self, credentials):
131         """
132         Retrieve the keys of the user specified by the credentials, and check
133         if one matches the blob in the credentials.
134         """
135         sshDir = os.path.expanduser(
136             os.path.join("~", credentials.username, ".ssh"))
137         if sshDir.startswith('~'): # didn't expand
138             return False
139         uid, gid = os.geteuid(), os.getegid()
140         ouid, ogid = pwd.getpwnam(credentials.username)[2:4]
141         for name in ['authorized_keys2', 'authorized_keys']:
142             filename = os.path.join(sshDir, name)
143             if not os.path.exists(filename):
144                 continue
145             try:
146                 lines = open(filename)
147             except IOError, e:
148                 if e.errno == errno.EACCES:
149                     lines = runAsEffectiveUser(ouid, ogid, open, filename)
150                 else:
151                     raise
152             for l in lines:
153                 l2 = l.split()
154                 if len(l2) < 2:
155                     continue
156                 try:
157                     if base64.decodestring(l2[1]) == credentials.blob:
158                         return True
159                 except binascii.Error:
160                     continue
161         return False
162
163     def _ebRequestAvatarId(self, f):
164         if not f.check(UnauthorizedLogin):
165             log.msg(f)
166             return failure.Failure(UnauthorizedLogin("unable to get avatar id"))
167         return f
168
169
170 class SSHProtocolChecker:
171     """
172     SSHProtocolChecker is a checker that requires multiple authentications
173     to succeed.  To add a checker, call my registerChecker method with
174     the checker and the interface.
175
176     After each successful authenticate, I call my areDone method with the
177     avatar id.  To get a list of the successful credentials for an avatar id,
178     use C{SSHProcotolChecker.successfulCredentials[avatarId]}.  If L{areDone}
179     returns True, the authentication has succeeded.
180     """
181
182     implements(ICredentialsChecker)
183
184     def __init__(self):
185         self.checkers = {}
186         self.successfulCredentials = {}
187
188     def get_credentialInterfaces(self):
189         return self.checkers.keys()
190
191     credentialInterfaces = property(get_credentialInterfaces)
192
193     def registerChecker(self, checker, *credentialInterfaces):
194         if not credentialInterfaces:
195             credentialInterfaces = checker.credentialInterfaces
196         for credentialInterface in credentialInterfaces:
197             self.checkers[credentialInterface] = checker
198
199     def requestAvatarId(self, credentials):
200         """
201         Part of the L{ICredentialsChecker} interface.  Called by a portal with
202         some credentials to check if they'll authenticate a user.  We check the
203         interfaces that the credentials provide against our list of acceptable
204         checkers.  If one of them matches, we ask that checker to verify the
205         credentials.  If they're valid, we call our L{_cbGoodAuthentication}
206         method to continue.
207
208         @param credentials: the credentials the L{Portal} wants us to verify
209         """
210         ifac = providedBy(credentials)
211         for i in ifac:
212             c = self.checkers.get(i)
213             if c is not None:
214                 d = defer.maybeDeferred(c.requestAvatarId, credentials)
215                 return d.addCallback(self._cbGoodAuthentication,
216                         credentials)
217         return defer.fail(UnhandledCredentials("No checker for %s" % \
218             ', '.join(map(reflect.qual, ifac))))
219
220     def _cbGoodAuthentication(self, avatarId, credentials):
221         """
222         Called if a checker has verified the credentials.  We call our
223         L{areDone} method to see if the whole of the successful authentications
224         are enough.  If they are, we return the avatar ID returned by the first
225         checker.
226         """
227         if avatarId not in self.successfulCredentials:
228             self.successfulCredentials[avatarId] = []
229         self.successfulCredentials[avatarId].append(credentials)
230         if self.areDone(avatarId):
231             del self.successfulCredentials[avatarId]
232             return avatarId
233         else:
234             raise error.NotEnoughAuthentication()
235
236     def areDone(self, avatarId):
237         """
238         Override to determine if the authentication is finished for a given
239         avatarId.
240
241         @param avatarId: the avatar returned by the first checker.  For
242             this checker to function correctly, all the checkers must
243             return the same avatar ID.
244         """
245         return True
246
Note: See TracBrowser for help on using the browser.