| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 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 |
|
|---|
| 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] == '$': |
|---|
| 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: |
|---|
| 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('~'): |
|---|
| 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 |
|
|---|