| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
| 2 # See LICENSE for details. | |
| 3 | |
| 4 # | |
| 5 | |
| 6 """ | |
| 7 Implementation of the ssh-userauth service. | |
| 8 Currently implemented authentication types are public-key and password. | |
| 9 | |
| 10 Maintainer: U{Paul Swartz<mailto:z3p@twistedmatrix.com>} | |
| 11 """ | |
| 12 | |
| 13 import struct | |
| 14 from twisted.conch import error, interfaces | |
| 15 from twisted.cred import credentials | |
| 16 from twisted.internet import defer, reactor | |
| 17 from twisted.python import failure, log | |
| 18 from common import NS, getNS, MP | |
| 19 import keys, transport, service | |
| 20 | |
| 21 class SSHUserAuthServer(service.SSHService): | |
| 22 name = 'ssh-userauth' | |
| 23 loginTimeout = 10 * 60 * 60 # 10 minutes before we disconnect them | |
| 24 attemptsBeforeDisconnect = 20 # number of attempts to allow before a disconn
ect | |
| 25 passwordDelay = 1 # number of seconds to delay on a failed password | |
| 26 protocolMessages = None # set later | |
| 27 interfaceToMethod = { | |
| 28 credentials.ISSHPrivateKey : 'publickey', | |
| 29 credentials.IUsernamePassword : 'password', | |
| 30 credentials.IPluggableAuthenticationModules : 'keyboard-interactive', | |
| 31 } | |
| 32 | |
| 33 def serviceStarted(self): | |
| 34 self.authenticatedWith = [] | |
| 35 self.loginAttempts = 0 | |
| 36 self.user = None | |
| 37 self.nextService = None | |
| 38 self.portal = self.transport.factory.portal | |
| 39 | |
| 40 self.supportedAuthentications = [] | |
| 41 for i in self.portal.listCredentialsInterfaces(): | |
| 42 if i in self.interfaceToMethod: | |
| 43 self.supportedAuthentications.append(self.interfaceToMethod[i]) | |
| 44 | |
| 45 if not self.transport.isEncrypted('out'): | |
| 46 if 'password' in self.supportedAuthentications: | |
| 47 self.supportedAuthentications.remove('password') | |
| 48 if 'keyboard-interactive' in self.supportedAuthentications: | |
| 49 self.supportedAuthentications.remove('keyboard-interactive') | |
| 50 # don't let us transport password in plaintext | |
| 51 self.cancelLoginTimeout = reactor.callLater(self.loginTimeout, | |
| 52 self.timeoutAuthentication) | |
| 53 | |
| 54 def serviceStopped(self): | |
| 55 if self.cancelLoginTimeout: | |
| 56 self.cancelLoginTimeout.cancel() | |
| 57 self.cancelLoginTimeout = None | |
| 58 | |
| 59 def timeoutAuthentication(self): | |
| 60 self.cancelLoginTimeout = None | |
| 61 self.transport.sendDisconnect( | |
| 62 transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, | |
| 63 'you took too long') | |
| 64 | |
| 65 | |
| 66 def tryAuth(self, kind, user, data): | |
| 67 log.msg('%s trying auth %s' % (user, kind)) | |
| 68 if kind not in self.supportedAuthentications: | |
| 69 return defer.fail(error.ConchError('unsupported authentication, fail
ing')) | |
| 70 kind = kind.replace('-', '_') | |
| 71 f = getattr(self,'auth_%s'%kind, None) | |
| 72 if f: | |
| 73 ret = f(data) | |
| 74 if not ret: | |
| 75 return defer.fail(error.ConchError('%s return None instead of a
Deferred' % kind)) | |
| 76 else: | |
| 77 return ret | |
| 78 return defer.fail(error.ConchError('bad auth type: %s' % kind)) | |
| 79 | |
| 80 def ssh_USERAUTH_REQUEST(self, packet): | |
| 81 user, nextService, method, rest = getNS(packet, 3) | |
| 82 if user != self.user or nextService != self.nextService: | |
| 83 self.authenticatedWith = [] # clear auth state | |
| 84 self.user = user | |
| 85 self.nextService = nextService | |
| 86 self.method = method | |
| 87 d = self.tryAuth(method, user, rest) | |
| 88 if not d: | |
| 89 self._ebBadAuth( | |
| 90 failure.Failure(error.ConchError('auth returned none'))) | |
| 91 return | |
| 92 d.addCallbacks(self._cbFinishedAuth) | |
| 93 d.addErrback(self._ebMaybeBadAuth) | |
| 94 d.addErrback(self._ebBadAuth) | |
| 95 return d | |
| 96 | |
| 97 def _cbFinishedAuth(self, (interface, avatar, logout)): | |
| 98 self.transport.avatar = avatar | |
| 99 self.transport.logoutFunction = logout | |
| 100 service = self.transport.factory.getService(self.transport, | |
| 101 self.nextService) | |
| 102 if not service: | |
| 103 raise error.ConchError('could not get next service: %s' | |
| 104 % self.nextService) | |
| 105 log.msg('%s authenticated with %s' % (self.user, self.method)) | |
| 106 if self.cancelLoginTimeout: | |
| 107 self.cancelLoginTimeout.cancel() | |
| 108 self.cancelLoginTimeout = None | |
| 109 self.transport.sendPacket(MSG_USERAUTH_SUCCESS, '') | |
| 110 self.transport.setService(service()) | |
| 111 | |
| 112 def _ebMaybeBadAuth(self, reason): | |
| 113 reason.trap(error.NotEnoughAuthentication) | |
| 114 self.transport.sendPacket(MSG_USERAUTH_FAILURE, NS(','.join(self.support
edAuthentications))+'\xff') | |
| 115 | |
| 116 def _ebBadAuth(self, reason): | |
| 117 if reason.type == error.IgnoreAuthentication: | |
| 118 return | |
| 119 if self.method != 'none': | |
| 120 log.msg('%s failed auth %s' % (self.user, self.method)) | |
| 121 log.msg('reason:') | |
| 122 if reason.type == error.ConchError: | |
| 123 log.msg(str(reason)) | |
| 124 else: | |
| 125 log.msg(reason.getTraceback()) | |
| 126 self.loginAttempts += 1 | |
| 127 if self.loginAttempts > self.attemptsBeforeDisconnect: | |
| 128 self.transport.sendDisconnect(transport.DISCONNECT_NO_MORE_AUTH_
METHODS_AVAILABLE, | |
| 129 'too many bad auths') | |
| 130 self.transport.sendPacket(MSG_USERAUTH_FAILURE, NS(','.join(self.support
edAuthentications))+'\x00') | |
| 131 | |
| 132 def auth_publickey(self, packet): | |
| 133 hasSig = ord(packet[0]) | |
| 134 algName, blob, rest = getNS(packet[1:], 2) | |
| 135 pubKey = keys.getPublicKeyObject(data = blob) | |
| 136 signature = hasSig and getNS(rest)[0] or None | |
| 137 if hasSig: | |
| 138 b = NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) + \ | |
| 139 NS(self.user) + NS(self.nextService) + NS('publickey') + \ | |
| 140 chr(hasSig) + NS(keys.objectType(pubKey)) + NS(blob) | |
| 141 c = credentials.SSHPrivateKey(self.user, algName, blob, b, signature
) | |
| 142 return self.portal.login(c, None, interfaces.IConchUser) | |
| 143 else: | |
| 144 c = credentials.SSHPrivateKey(self.user, algName, blob, None, None) | |
| 145 return self.portal.login(c, None, interfaces.IConchUser).addErrback( | |
| 146 self._ebCheckKey, | |
| 147 packet[1:]) | |
| 148 | |
| 149 def _ebCheckKey(self, reason, packet): | |
| 150 reason.trap(error.ValidPublicKey) | |
| 151 # if we make it here, it means that the publickey is valid | |
| 152 self.transport.sendPacket(MSG_USERAUTH_PK_OK, packet) | |
| 153 return failure.Failure(error.IgnoreAuthentication()) | |
| 154 | |
| 155 def auth_password(self, packet): | |
| 156 password = getNS(packet[1:])[0] | |
| 157 c = credentials.UsernamePassword(self.user, password) | |
| 158 return self.portal.login(c, None, interfaces.IConchUser).addErrback( | |
| 159 self._ebPassword) | |
| 160 | |
| 161 def _ebPassword(self, f): | |
| 162 d = defer.Deferred() | |
| 163 reactor.callLater(self.passwordDelay, lambda d,f:d.callback(f), d, f) | |
| 164 return d | |
| 165 | |
| 166 def auth_keyboard_interactive(self, packet): | |
| 167 if hasattr(self, '_pamDeferred'): | |
| 168 self.transport.sendDisconnect(transport.DISCONNECT_PROTOCOL_ERROR, "
only one keyboard interactive attempt at a time") | |
| 169 return failure.Failure(error.IgnoreAuthentication()) | |
| 170 c = credentials.PluggableAuthenticationModules(self.user, self._pamConv) | |
| 171 return self.portal.login(c, None, interfaces.IConchUser) | |
| 172 | |
| 173 def _pamConv(self, items): | |
| 174 resp = [] | |
| 175 for message, kind in items: | |
| 176 if kind == 1: # password | |
| 177 resp.append((message, 0)) | |
| 178 elif kind == 2: # text | |
| 179 resp.append((message, 1)) | |
| 180 elif kind in (3, 4): | |
| 181 return defer.fail(error.ConchError('cannot handle PAM 3 or 4 mes
sages')) | |
| 182 else: | |
| 183 return defer.fail(error.ConchError('bad PAM auth kind %i' % kind
)) | |
| 184 packet = NS('')+NS('')+NS('') | |
| 185 packet += struct.pack('>L', len(resp)) | |
| 186 for prompt, echo in resp: | |
| 187 packet += NS(prompt) | |
| 188 packet += chr(echo) | |
| 189 self.transport.sendPacket(MSG_USERAUTH_INFO_REQUEST, packet) | |
| 190 self._pamDeferred = defer.Deferred() | |
| 191 return self._pamDeferred | |
| 192 | |
| 193 def ssh_USERAUTH_INFO_RESPONSE(self, packet): | |
| 194 d = self._pamDeferred | |
| 195 del self._pamDeferred | |
| 196 try: | |
| 197 resp = [] | |
| 198 numResps = struct.unpack('>L', packet[:4])[0] | |
| 199 packet = packet[4:] | |
| 200 while packet: | |
| 201 response, packet = getNS(packet) | |
| 202 resp.append((response, 0)) | |
| 203 assert len(resp) == numResps | |
| 204 except: | |
| 205 d.errback(failure.Failure()) | |
| 206 else: | |
| 207 d.callback(resp) | |
| 208 | |
| 209 class SSHUserAuthClient(service.SSHService): | |
| 210 name = 'ssh-userauth' | |
| 211 protocolMessages = None # set later | |
| 212 | |
| 213 preferredOrder = ['publickey', 'password', 'keyboard-interactive'] | |
| 214 | |
| 215 def __init__(self, user, instance): | |
| 216 self.user = user | |
| 217 self.instance = instance | |
| 218 | |
| 219 def serviceStarted(self): | |
| 220 self.authenticatedWith = [] | |
| 221 self.triedPublicKeys = [] | |
| 222 self.lastPublicKey = None | |
| 223 self.askForAuth('none', '') | |
| 224 | |
| 225 def askForAuth(self, kind, extraData): | |
| 226 self.lastAuth = kind | |
| 227 self.transport.sendPacket(MSG_USERAUTH_REQUEST, NS(self.user) + \ | |
| 228 NS(self.instance.name) + NS(kind) + extraData) | |
| 229 def tryAuth(self, kind): | |
| 230 kind = kind.replace('-', '_') | |
| 231 log.msg('trying to auth with %s' % kind) | |
| 232 f= getattr(self,'auth_%s'%kind, None) | |
| 233 if f: | |
| 234 return f() | |
| 235 | |
| 236 def _ebAuth(self, ignored, *args): | |
| 237 self.tryAuth('none') | |
| 238 | |
| 239 def ssh_USERAUTH_SUCCESS(self, packet): | |
| 240 self.transport.setService(self.instance) | |
| 241 #self.ssh_USERAUTH_SUCCESS = lambda *a: None # ignore these | |
| 242 | |
| 243 def ssh_USERAUTH_FAILURE(self, packet): | |
| 244 canContinue, partial = getNS(packet) | |
| 245 canContinue = canContinue.split(',') | |
| 246 partial = ord(partial) | |
| 247 if partial: | |
| 248 self.authenticatedWith.append(self.lastAuth) | |
| 249 def _(x, y): | |
| 250 try: | |
| 251 i1 = self.preferredOrder.index(x) | |
| 252 except ValueError: | |
| 253 return 1 | |
| 254 try: | |
| 255 i2 = self.preferredOrder.index(y) | |
| 256 except ValueError: | |
| 257 return -1 | |
| 258 return cmp(i1, i2) | |
| 259 canContinue.sort(_) | |
| 260 log.msg('can continue with: %s' % canContinue) | |
| 261 for method in canContinue: | |
| 262 if method not in self.authenticatedWith and self.tryAuth(method): | |
| 263 return | |
| 264 self.transport.sendDisconnect(transport.DISCONNECT_NO_MORE_AUTH_METHODS_
AVAILABLE, 'no more authentication methods available') | |
| 265 | |
| 266 def ssh_USERAUTH_PK_OK(self, packet): | |
| 267 if self.lastAuth == 'publickey': | |
| 268 # this is ok | |
| 269 publicKey = self.lastPublicKey | |
| 270 keyType = getNS(publicKey)[0] | |
| 271 b = NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) + \ | |
| 272 NS(self.user) + NS(self.instance.name) + NS('publickey') + '\xff' +\ | |
| 273 NS(keyType) + NS(publicKey) | |
| 274 d = self.signData(publicKey, b) | |
| 275 if not d: | |
| 276 self.askForAuth('none', '') | |
| 277 # this will fail, we'll move on | |
| 278 return | |
| 279 d.addCallback(self._cbSignedData) | |
| 280 d.addErrback(self._ebAuth) | |
| 281 elif self.lastAuth == 'password': | |
| 282 prompt, language, rest = getNS(packet, 2) | |
| 283 self._oldPass = self._newPass = None | |
| 284 self.getPassword('Old Password: ').addCallbacks(self._setOldPass, se
lf._ebAuth) | |
| 285 self.getPassword(prompt).addCallbacks(self._setNewPass, self._ebAuth
) | |
| 286 elif self.lastAuth == 'keyboard-interactive': | |
| 287 name, instruction, lang, data = getNS(packet, 3) | |
| 288 numPrompts = struct.unpack('!L', data[:4])[0] | |
| 289 data = data[4:] | |
| 290 prompts = [] | |
| 291 for i in range(numPrompts): | |
| 292 prompt, data = getNS(data) | |
| 293 echo = bool(ord(data[0])) | |
| 294 data = data[1:] | |
| 295 prompts.append((prompt, echo)) | |
| 296 d = self.getGenericAnswers(name, instruction, prompts) | |
| 297 d.addCallback(self._cbGenericAnswers) | |
| 298 d.addErrback(self._ebAuth) | |
| 299 | |
| 300 def _cbSignedData(self, signedData): | |
| 301 publicKey = self.lastPublicKey | |
| 302 keyType = getNS(publicKey)[0] | |
| 303 self.askForAuth('publickey', '\xff' + NS(keyType) + NS(publicKey) + \ | |
| 304 NS(signedData)) | |
| 305 | |
| 306 | |
| 307 | |
| 308 | |
| 309 | |
| 310 | |
| 311 def _setOldPass(self, op): | |
| 312 if self._newPass: | |
| 313 np = self._newPass | |
| 314 self._newPass = None | |
| 315 self.askForAuth('password', '\xff'+NS(op)+NS(np)) | |
| 316 else: | |
| 317 self._oldPass = op | |
| 318 | |
| 319 def _setNewPass(self, np): | |
| 320 if self._oldPass: | |
| 321 op = self._oldPass | |
| 322 self._oldPass = None | |
| 323 self.askForAuth('password', '\xff'+NS(op)+NS(np)) | |
| 324 else: | |
| 325 self._newPass = np | |
| 326 | |
| 327 def _cbGenericAnswers(self, responses): | |
| 328 data = struct.pack('!L', len(responses)) | |
| 329 for r in responses: | |
| 330 data += NS(r.encode('UTF8')) | |
| 331 self.transport.sendPacket(MSG_USERAUTH_INFO_RESPONSE, data) | |
| 332 | |
| 333 def auth_publickey(self): | |
| 334 publicKey = self.getPublicKey() | |
| 335 if publicKey: | |
| 336 self.lastPublicKey = publicKey | |
| 337 self.triedPublicKeys.append(publicKey) | |
| 338 keyType = getNS(publicKey)[0] | |
| 339 log.msg('using key of type %s' % keyType) | |
| 340 self.askForAuth('publickey', '\x00' + NS(keyType) + \ | |
| 341 NS(publicKey)) | |
| 342 return 1 | |
| 343 else: | |
| 344 return 0 | |
| 345 | |
| 346 def auth_password(self): | |
| 347 d = self.getPassword() | |
| 348 if d: | |
| 349 d.addCallbacks(self._cbPassword, self._ebAuth) | |
| 350 return 1 | |
| 351 else: # returned None, don't do password auth | |
| 352 return 0 | |
| 353 | |
| 354 def auth_keyboard_interactive(self): | |
| 355 log.msg('authing with keyboard-interactive') | |
| 356 self.askForAuth('keyboard-interactive', NS('') + NS('')) | |
| 357 return 1 | |
| 358 | |
| 359 def _cbPassword(self, password): | |
| 360 self.askForAuth('password', '\x00'+NS(password)) | |
| 361 | |
| 362 def signData(self, publicKey, signData): | |
| 363 """ | |
| 364 Sign the given data with the given public key blob. | |
| 365 By default, this will call getPrivateKey to get the private key, | |
| 366 the sign the data using keys.signData. | |
| 367 However, this is factored out so that it can use alternate methods, | |
| 368 such as a key agent. | |
| 369 """ | |
| 370 key = self.getPrivateKey() | |
| 371 if not key: | |
| 372 return | |
| 373 return key.addCallback(self._cbSignData, signData) | |
| 374 | |
| 375 def _cbSignData(self, privateKey, signData): | |
| 376 return keys.signData(privateKey, signData) | |
| 377 | |
| 378 def getPublicKey(self): | |
| 379 """ | |
| 380 Return a public key for the user. If no more public keys are | |
| 381 available, return None. | |
| 382 | |
| 383 @rtype: C{str}/C{None} | |
| 384 """ | |
| 385 return None | |
| 386 #raise NotImplementedError | |
| 387 | |
| 388 | |
| 389 def getPrivateKey(self): | |
| 390 """ | |
| 391 Return a L{Deferred} that will be called back with the private key | |
| 392 corresponding to the last public key from getPublicKey(). | |
| 393 If the private key is not available, errback on the Deferred. | |
| 394 | |
| 395 @rtype: L{Deferred} | |
| 396 """ | |
| 397 return defer.fail(NotImplementedError()) | |
| 398 | |
| 399 def getPassword(self, prompt = None): | |
| 400 """ | |
| 401 Return a L{Deferred} that will be called back with a password. | |
| 402 prompt is a string to display for the password, or None for a generic | |
| 403 'user@hostname's password: '. | |
| 404 | |
| 405 @type prompt: C{str}/C{None} | |
| 406 @rtype: L{Deferred} | |
| 407 """ | |
| 408 return defer.fail(NotImplementedError()) | |
| 409 | |
| 410 def getGenericAnswers(self, name, instruction, prompts): | |
| 411 """ | |
| 412 Returns a L{Deferred} with the responses to the promopts. | |
| 413 | |
| 414 @param name: The name of the authentication currently in progress. | |
| 415 @param instruction: Describes what the authentication wants. | |
| 416 @param prompts: A list of (prompt, echo) pairs, where prompt is a | |
| 417 string to display and echo is a boolean indicating whether the | |
| 418 user's response should be echoed as they type it. | |
| 419 """ | |
| 420 return defer.fail(NotImplementedError()) | |
| 421 | |
| 422 MSG_USERAUTH_REQUEST = 50 | |
| 423 MSG_USERAUTH_FAILURE = 51 | |
| 424 MSG_USERAUTH_SUCCESS = 52 | |
| 425 MSG_USERAUTH_BANNER = 53 | |
| 426 MSG_USERAUTH_PASSWD_CHANGEREQ = 60 | |
| 427 MSG_USERAUTH_INFO_REQUEST = 60 | |
| 428 MSG_USERAUTH_INFO_RESPONSE = 61 | |
| 429 MSG_USERAUTH_PK_OK = 60 | |
| 430 | |
| 431 messages = {} | |
| 432 import userauth | |
| 433 for v in dir(userauth): | |
| 434 if v[:4]=='MSG_': | |
| 435 messages[getattr(userauth,v)] = v # doesn't handle doubles | |
| 436 | |
| 437 SSHUserAuthServer.protocolMessages = messages | |
| 438 SSHUserAuthClient.protocolMessages = messages | |
| OLD | NEW |