| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
""" |
|---|
| 6 |
Implementation of the ssh-userauth service. |
|---|
| 7 |
Currently implemented authentication types are public-key and password. |
|---|
| 8 |
|
|---|
| 9 |
Maintainer: Paul Swartz |
|---|
| 10 |
""" |
|---|
| 11 |
|
|---|
| 12 |
import struct, warnings |
|---|
| 13 |
from twisted.conch import error, interfaces |
|---|
| 14 |
from twisted.conch.ssh import keys, transport, service |
|---|
| 15 |
from twisted.conch.ssh.common import NS, getNS |
|---|
| 16 |
from twisted.cred import credentials |
|---|
| 17 |
from twisted.cred.error import UnauthorizedLogin |
|---|
| 18 |
from twisted.internet import defer, reactor |
|---|
| 19 |
from twisted.python import failure, log, util |
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
class SSHUserAuthServer(service.SSHService): |
|---|
| 24 |
""" |
|---|
| 25 |
A service implementing the server side of the 'ssh-userauth' service. It |
|---|
| 26 |
is used to authenticate the user on the other side as being able to access |
|---|
| 27 |
this server. |
|---|
| 28 |
|
|---|
| 29 |
@ivar name: the name of this service: 'ssh-userauth' |
|---|
| 30 |
@type name: C{str} |
|---|
| 31 |
@ivar authenticatedWith: a list of authentication methods that have |
|---|
| 32 |
already been used. |
|---|
| 33 |
@type authenticatedWith: C{list} |
|---|
| 34 |
@ivar loginTimeout: the number of seconds we wait before disconnecting |
|---|
| 35 |
the user for taking too long to authenticate |
|---|
| 36 |
@type loginTimeout: C{int} |
|---|
| 37 |
@ivar attemptsBeforeDisconnect: the number of failed login attempts we |
|---|
| 38 |
allow before disconnecting. |
|---|
| 39 |
@type attemptsBeforeDisconnect: C{int} |
|---|
| 40 |
@ivar loginAttempts: the number of login attempts that have been made |
|---|
| 41 |
@type loginAttempts: C{int} |
|---|
| 42 |
@ivar passwordDelay: the number of seconds to delay when the user gives |
|---|
| 43 |
an incorrect password |
|---|
| 44 |
@type passwordDelay: C{int} |
|---|
| 45 |
@ivar interfaceToMethod: a C{dict} mapping credential interfaces to |
|---|
| 46 |
authentication methods. The server checks to see which of the |
|---|
| 47 |
cred interfaces have checkers and tells the client that those methods |
|---|
| 48 |
are valid for authentication. |
|---|
| 49 |
@type interfaceToMethod: C{dict} |
|---|
| 50 |
@ivar supportedAuthentications: A list of the supported authentication |
|---|
| 51 |
methods. |
|---|
| 52 |
@type supportedAuthentications: C{list} of C{str} |
|---|
| 53 |
@ivar user: the last username the client tried to authenticate with |
|---|
| 54 |
@type user: C{str} |
|---|
| 55 |
@ivar method: the current authentication method |
|---|
| 56 |
@type method: C{str} |
|---|
| 57 |
@ivar nextService: the service the user wants started after authentication |
|---|
| 58 |
has been completed. |
|---|
| 59 |
@type nextService: C{str} |
|---|
| 60 |
@ivar portal: the L{twisted.cred.portal.Portal} we are using for |
|---|
| 61 |
authentication |
|---|
| 62 |
@type portal: L{twisted.cred.portal.Portal} |
|---|
| 63 |
@ivar clock: an object with a callLater method. Stubbed out for testing. |
|---|
| 64 |
""" |
|---|
| 65 |
|
|---|
| 66 |
|
|---|
| 67 |
name = 'ssh-userauth' |
|---|
| 68 |
loginTimeout = 10 * 60 * 60 |
|---|
| 69 |
|
|---|
| 70 |
attemptsBeforeDisconnect = 20 |
|---|
| 71 |
|
|---|
| 72 |
passwordDelay = 1 |
|---|
| 73 |
clock = reactor |
|---|
| 74 |
interfaceToMethod = { |
|---|
| 75 |
credentials.ISSHPrivateKey : 'publickey', |
|---|
| 76 |
credentials.IUsernamePassword : 'password', |
|---|
| 77 |
credentials.IPluggableAuthenticationModules : 'keyboard-interactive', |
|---|
| 78 |
} |
|---|
| 79 |
|
|---|
| 80 |
|
|---|
| 81 |
def serviceStarted(self): |
|---|
| 82 |
""" |
|---|
| 83 |
Called when the userauth service is started. Set up instance |
|---|
| 84 |
variables, check if we should allow password/keyboard-interactive |
|---|
| 85 |
authentication (only allow if the outgoing connection is encrypted) and |
|---|
| 86 |
set up a login timeout. |
|---|
| 87 |
""" |
|---|
| 88 |
self.authenticatedWith = [] |
|---|
| 89 |
self.loginAttempts = 0 |
|---|
| 90 |
self.user = None |
|---|
| 91 |
self.nextService = None |
|---|
| 92 |
self._pamDeferred = None |
|---|
| 93 |
self.portal = self.transport.factory.portal |
|---|
| 94 |
|
|---|
| 95 |
self.supportedAuthentications = [] |
|---|
| 96 |
for i in self.portal.listCredentialsInterfaces(): |
|---|
| 97 |
if i in self.interfaceToMethod: |
|---|
| 98 |
self.supportedAuthentications.append(self.interfaceToMethod[i]) |
|---|
| 99 |
|
|---|
| 100 |
if not self.transport.isEncrypted('in'): |
|---|
| 101 |
|
|---|
| 102 |
if 'password' in self.supportedAuthentications: |
|---|
| 103 |
self.supportedAuthentications.remove('password') |
|---|
| 104 |
if 'keyboard-interactive' in self.supportedAuthentications: |
|---|
| 105 |
self.supportedAuthentications.remove('keyboard-interactive') |
|---|
| 106 |
self._cancelLoginTimeout = self.clock.callLater( |
|---|
| 107 |
self.loginTimeout, |
|---|
| 108 |
self.timeoutAuthentication) |
|---|
| 109 |
|
|---|
| 110 |
|
|---|
| 111 |
def serviceStopped(self): |
|---|
| 112 |
""" |
|---|
| 113 |
Called when the userauth service is stopped. Cancel the login timeout |
|---|
| 114 |
if it's still going. |
|---|
| 115 |
""" |
|---|
| 116 |
if self._cancelLoginTimeout: |
|---|
| 117 |
self._cancelLoginTimeout.cancel() |
|---|
| 118 |
self._cancelLoginTimeout = None |
|---|
| 119 |
|
|---|
| 120 |
|
|---|
| 121 |
def timeoutAuthentication(self): |
|---|
| 122 |
""" |
|---|
| 123 |
Called when the user has timed out on authentication. Disconnect |
|---|
| 124 |
with a DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE message. |
|---|
| 125 |
""" |
|---|
| 126 |
self._cancelLoginTimeout = None |
|---|
| 127 |
self.transport.sendDisconnect( |
|---|
| 128 |
transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, |
|---|
| 129 |
'you took too long') |
|---|
| 130 |
|
|---|
| 131 |
|
|---|
| 132 |
def tryAuth(self, kind, user, data): |
|---|
| 133 |
""" |
|---|
| 134 |
Try to authenticate the user with the given method. Dispatches to a |
|---|
| 135 |
auth_* method. |
|---|
| 136 |
|
|---|
| 137 |
@param kind: the authentication method to try. |
|---|
| 138 |
@type kind: C{str} |
|---|
| 139 |
@param user: the username the client is authenticating with. |
|---|
| 140 |
@type user: C{str} |
|---|
| 141 |
@param data: authentication specific data sent by the client. |
|---|
| 142 |
@type data: C{str} |
|---|
| 143 |
@return: A Deferred called back if the method succeeded, or erred back |
|---|
| 144 |
if it failed. |
|---|
| 145 |
@rtype: C{defer.Deferred} |
|---|
| 146 |
""" |
|---|
| 147 |
log.msg('%s trying auth %s' % (user, kind)) |
|---|
| 148 |
if kind not in self.supportedAuthentications: |
|---|
| 149 |
return defer.fail( |
|---|
| 150 |
error.ConchError('unsupported authentication, failing')) |
|---|
| 151 |
kind = kind.replace('-', '_') |
|---|
| 152 |
f = getattr(self,'auth_%s'%kind, None) |
|---|
| 153 |
if f: |
|---|
| 154 |
ret = f(data) |
|---|
| 155 |
if not ret: |
|---|
| 156 |
return defer.fail( |
|---|
| 157 |
error.ConchError('%s return None instead of a Deferred' |
|---|
| 158 |
% kind)) |
|---|
| 159 |
else: |
|---|
| 160 |
return ret |
|---|
| 161 |
return defer.fail(error.ConchError('bad auth type: %s' % kind)) |
|---|
| 162 |
|
|---|
| 163 |
|
|---|
| 164 |
def ssh_USERAUTH_REQUEST(self, packet): |
|---|
| 165 |
""" |
|---|
| 166 |
The client has requested authentication. Payload:: |
|---|
| 167 |
string user |
|---|
| 168 |
string next service |
|---|
| 169 |
string method |
|---|
| 170 |
<authentication specific data> |
|---|
| 171 |
|
|---|
| 172 |
@type packet: C{str} |
|---|
| 173 |
""" |
|---|
| 174 |
user, nextService, method, rest = getNS(packet, 3) |
|---|
| 175 |
if user != self.user or nextService != self.nextService: |
|---|
| 176 |
self.authenticatedWith = [] |
|---|
| 177 |
self.user = user |
|---|
| 178 |
self.nextService = nextService |
|---|
| 179 |
self.method = method |
|---|
| 180 |
d = self.tryAuth(method, user, rest) |
|---|
| 181 |
if not d: |
|---|
| 182 |
self._ebBadAuth( |
|---|
| 183 |
failure.Failure(error.ConchError('auth returned none'))) |
|---|
| 184 |
return |
|---|
| 185 |
d.addCallback(self._cbFinishedAuth) |
|---|
| 186 |
d.addErrback(self._ebMaybeBadAuth) |
|---|
| 187 |
d.addErrback(self._ebBadAuth) |
|---|
| 188 |
return d |
|---|
| 189 |
|
|---|
| 190 |
|
|---|
| 191 |
def _cbFinishedAuth(self, (interface, avatar, logout)): |
|---|
| 192 |
""" |
|---|
| 193 |
The callback when user has successfully been authenticated. For a |
|---|
| 194 |
description of the arguments, see L{twisted.cred.portal.Portal.login}. |
|---|
| 195 |
We start the service requested by the user. |
|---|
| 196 |
""" |
|---|
| 197 |
self.transport.avatar = avatar |
|---|
| 198 |
self.transport.logoutFunction = logout |
|---|
| 199 |
service = self.transport.factory.getService(self.transport, |
|---|
| 200 |
self.nextService) |
|---|
| 201 |
if not service: |
|---|
| 202 |
raise error.ConchError('could not get next service: %s' |
|---|
| 203 |
% self.nextService) |
|---|
| 204 |
log.msg('%s authenticated with %s' % (self.user, self.method)) |
|---|
| 205 |
self.transport.sendPacket(MSG_USERAUTH_SUCCESS, '') |
|---|
| 206 |
self.transport.setService(service()) |
|---|
| 207 |
|
|---|
| 208 |
|
|---|
| 209 |
def _ebMaybeBadAuth(self, reason): |
|---|
| 210 |
""" |
|---|
| 211 |
An intermediate errback. If the reason is |
|---|
| 212 |
error.NotEnoughAuthentication, we send a MSG_USERAUTH_FAILURE, but |
|---|
| 213 |
with the partial success indicator set. |
|---|
| 214 |
|
|---|
| 215 |
@type reason: L{twisted.python.failure.Failure} |
|---|
| 216 |
""" |
|---|
| 217 |
reason.trap(error.NotEnoughAuthentication) |
|---|
| 218 |
self.transport.sendPacket(MSG_USERAUTH_FAILURE, |
|---|
| 219 |
NS(','.join(self.supportedAuthentications)) + '\xff') |
|---|
| 220 |
|
|---|
| 221 |
|
|---|
| 222 |
def _ebBadAuth(self, reason): |
|---|
| 223 |
""" |
|---|
| 224 |
The final errback in the authentication chain. If the reason is |
|---|
| 225 |
error.IgnoreAuthentication, we simply return; the authentication |
|---|
| 226 |
method has sent its own response. Otherwise, send a failure message |
|---|
| 227 |
and (if the method is not 'none') increment the number of login |
|---|
| 228 |
attempts. |
|---|
| 229 |
|
|---|
| 230 |
@type reason: L{twisted.python.failure.Failure} |
|---|
| 231 |
""" |
|---|
| 232 |
if reason.check(error.IgnoreAuthentication): |
|---|
| 233 |
return |
|---|
| 234 |
if self.method != 'none': |
|---|
| 235 |
log.msg('%s failed auth %s' % (self.user, self.method)) |
|---|
| 236 |
if reason.check(UnauthorizedLogin): |
|---|
| 237 |
log.msg('unauthorized login: %s' % reason.getErrorMessage()) |
|---|
| 238 |
elif reason.check(error.ConchError): |
|---|
| 239 |
log.msg('reason: %s' % reason.getErrorMessage()) |
|---|
| 240 |
else: |
|---|
| 241 |
log.msg(reason.getTraceback()) |
|---|
| 242 |
self.loginAttempts += 1 |
|---|
| 243 |
if self.loginAttempts > self.attemptsBeforeDisconnect: |
|---|
| 244 |
self.transport.sendDisconnect( |
|---|
| 245 |
transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, |
|---|
| 246 |
'too many bad auths') |
|---|
| 247 |
return |
|---|
| 248 |
self.transport.sendPacket( |
|---|
| 249 |
MSG_USERAUTH_FAILURE, |
|---|
| 250 |
NS(','.join(self.supportedAuthentications)) + '\x00') |
|---|
| 251 |
|
|---|
| 252 |
|
|---|
| 253 |
def auth_publickey(self, packet): |
|---|
| 254 |
""" |
|---|
| 255 |
Public key authentication. Payload:: |
|---|
| 256 |
byte has signature |
|---|
| 257 |
string algorithm name |
|---|
| 258 |
string key blob |
|---|
| 259 |
[string signature] (if has signature is True) |
|---|
| 260 |
|
|---|
| 261 |
Create a SSHPublicKey credential and verify it using our portal. |
|---|
| 262 |
""" |
|---|
| 263 |
hasSig = ord(packet[0]) |
|---|
| 264 |
algName, blob, rest = getNS(packet[1:], 2) |
|---|
| 265 |
pubKey = keys.Key.fromString(blob) |
|---|
| 266 |
signature = hasSig and getNS(rest)[0] or None |
|---|
| 267 |
if hasSig: |
|---|
| 268 |
b = (NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) + |
|---|
| 269 |
NS(self.user) + NS(self.nextService) + NS('publickey') + |
|---|
| 270 |
chr(hasSig) + NS(pubKey.sshType()) + NS(blob)) |
|---|
| 271 |
c = credentials.SSHPrivateKey(self.user, algName, blob, b, |
|---|
| 272 |
signature) |
|---|
| 273 |
return self.portal.login(c, None, interfaces.IConchUser) |
|---|
| 274 |
else: |
|---|
| 275 |
c = credentials.SSHPrivateKey(self.user, algName, blob, None, None) |
|---|
| 276 |
return self.portal.login(c, None, |
|---|
| 277 |
interfaces.IConchUser).addErrback(self._ebCheckKey, |
|---|
| 278 |
packet[1:]) |
|---|
| 279 |
|
|---|
| 280 |
|
|---|
| 281 |
def _ebCheckKey(self, reason, packet): |
|---|
| 282 |
""" |
|---|
| 283 |
Called back if the user did not sent a signature. If reason is |
|---|
| 284 |
error.ValidPublicKey then this key is valid for the user to |
|---|
| 285 |
authenticate with. Send MSG_USERAUTH_PK_OK. |
|---|
| 286 |
""" |
|---|
| 287 |
reason.trap(error.ValidPublicKey) |
|---|
| 288 |
|
|---|
| 289 |
self.transport.sendPacket(MSG_USERAUTH_PK_OK, packet) |
|---|
| 290 |
return failure.Failure(error.IgnoreAuthentication()) |
|---|
| 291 |
|
|---|
| 292 |
|
|---|
| 293 |
def auth_password(self, packet): |
|---|
| 294 |
""" |
|---|
| 295 |
Password authentication. Payload:: |
|---|
| 296 |
string password |
|---|
| 297 |
|
|---|
| 298 |
Make a UsernamePassword credential and verify it with our portal. |
|---|
| 299 |
""" |
|---|
| 300 |
password = getNS(packet[1:])[0] |
|---|
| 301 |
c = credentials.UsernamePassword(self.user, password) |
|---|
| 302 |
return self.portal.login(c, None, interfaces.IConchUser).addErrback( |
|---|
| 303 |
self._ebPassword) |
|---|
| 304 |
|
|---|
| 305 |
|
|---|
| 306 |
def _ebPassword(self, f): |
|---|
| 307 |
""" |
|---|
| 308 |
If the password is invalid, wait before sending the failure in order |
|---|
| 309 |
to delay brute-force password guessing. |
|---|
| 310 |
""" |
|---|
| 311 |
d = defer.Deferred() |
|---|
| 312 |
self.clock.callLater(self.passwordDelay, d.callback, f) |
|---|
| 313 |
return d |
|---|
| 314 |
|
|---|
| 315 |
|
|---|
| 316 |
def auth_keyboard_interactive(self, packet): |
|---|
| 317 |
""" |
|---|
| 318 |
Keyboard interactive authentication. No payload. We create a |
|---|
| 319 |
PluggableAuthenticationModules credential and authenticate with our |
|---|
| 320 |
portal. |
|---|
| 321 |
""" |
|---|
| 322 |
if self._pamDeferred is not None: |
|---|
| 323 |
self.transport.sendDisconnect( |
|---|
| 324 |
transport.DISCONNECT_PROTOCOL_ERROR, |
|---|
| 325 |
"only one keyboard interactive attempt at a time") |
|---|
| 326 |
return defer.fail(error.IgnoreAuthentication()) |
|---|
| 327 |
c = credentials.PluggableAuthenticationModules(self.user, |
|---|
| 328 |
self._pamConv) |
|---|
| 329 |
return self.portal.login(c, None, interfaces.IConchUser) |
|---|
| 330 |
|
|---|
| 331 |
|
|---|
| 332 |
def _pamConv(self, items): |
|---|
| 333 |
""" |
|---|
| 334 |
Convert a list of PAM authentication questions into a |
|---|
| 335 |
MSG_USERAUTH_INFO_REQUEST. Returns a Deferred that will be called |
|---|
| 336 |
back when the user has responses to the questions. |
|---|
| 337 |
|
|---|
| 338 |
@param items: a list of 2-tuples (message, kind). We only care about |
|---|
| 339 |
kinds 1 (password) and 2 (text). |
|---|
| 340 |
@type items: C{list} |
|---|
| 341 |
@rtype: L{defer.Deferred} |
|---|
| 342 |
""" |
|---|
| 343 |
resp = [] |
|---|
| 344 |
for message, kind in items: |
|---|
| 345 |
if kind == 1: |
|---|
| 346 |
resp.append((message, 0)) |
|---|
| 347 |
elif kind == 2: |
|---|
| 348 |
resp.append((message, 1)) |
|---|
| 349 |
elif kind in (3, 4): |
|---|
| 350 |
return defer.fail(error.ConchError( |
|---|
| 351 |
'cannot handle PAM 3 or 4 messages')) |
|---|
| 352 |
else: |
|---|
| 353 |
return defer.fail(error.ConchError( |
|---|
| 354 |
'bad PAM auth kind %i' % kind)) |
|---|
| 355 |
packet = NS('') + NS('') + NS('') |
|---|
| 356 |
packet += struct.pack('>L', len(resp)) |
|---|
| 357 |
for prompt, echo in resp: |
|---|
| 358 |
packet += NS(prompt) |
|---|
| 359 |
packet += chr(echo) |
|---|
| 360 |
self.transport.sendPacket(MSG_USERAUTH_INFO_REQUEST, packet) |
|---|
| 361 |
self._pamDeferred = defer.Deferred() |
|---|
| 362 |
return self._pamDeferred |
|---|
| 363 |
|
|---|
| 364 |
|
|---|
| 365 |
def ssh_USERAUTH_INFO_RESPONSE(self, packet): |
|---|
| 366 |
""" |
|---|
| 367 |
The user has responded with answers to PAMs authentication questions. |
|---|
| 368 |
Parse the packet into a PAM response and callback self._pamDeferred. |
|---|
| 369 |
Payload:: |
|---|
| 370 |
uint32 numer of responses |
|---|
| 371 |
string response 1 |
|---|
| 372 |
... |
|---|
| 373 |
string response n |
|---|
| 374 |
""" |
|---|
| 375 |
d, self._pamDeferred = self._pamDeferred, None |
|---|
| 376 |
|
|---|
| 377 |
try: |
|---|
| 378 |
resp = [] |
|---|
| 379 |
numResps = struct.unpack('>L', packet[:4])[0] |
|---|
| 380 |
packet = packet[4:] |
|---|
| 381 |
while len(resp) < numResps: |
|---|
| 382 |
response, packet = getNS(packet) |
|---|
| 383 |
resp.append((response, 0)) |
|---|
| 384 |
if packet: |
|---|
| 385 |
raise error.ConchError("%i bytes of extra data" % len(packet)) |
|---|
| 386 |
except: |
|---|
| 387 |
d.errback(failure.Failure()) |
|---|
| 388 |
else: |
|---|
| 389 |
d.callback(resp) |
|---|
| 390 |
|
|---|
| 391 |
|
|---|
| 392 |
|
|---|
| 393 |
class SSHUserAuthClient(service.SSHService): |
|---|
| 394 |
""" |
|---|
| 395 |
A service implementing the client side of 'ssh-userauth'. |
|---|
| 396 |
|
|---|
| 397 |
@ivar name: the name of this service: 'ssh-userauth' |
|---|
| 398 |
@type name: C{str} |
|---|
| 399 |
@ivar preferredOrder: a list of authentication methods we support, in |
|---|
| 400 |
order of preference. The client will try authentication methods in |
|---|
| 401 |
this order, making callbacks for information when necessary. |
|---|
| 402 |
@type preferredOrder: C{list} |
|---|
| 403 |
@ivar user: the name of the user to authenticate as |
|---|
| 404 |
@type user: C{str} |
|---|
| 405 |
@ivar instance: the service to start after authentication has finished |
|---|
| 406 |
@type instance: L{service.SSHService} |
|---|
| 407 |
@ivar authenticatedWith: a list of strings of authentication methods we've tried |
|---|
| 408 |
@type authenticatedWith: C{list} of C{str} |
|---|
| 409 |
@ivar triedPublicKeys: a list of public key objects that we've tried to |
|---|
| 410 |
authenticate with |
|---|
| 411 |
@type triedPublicKeys: C{list} of L{Key} |
|---|
| 412 |
@ivar lastPublicKey: the last public key object we've tried to authenticate |
|---|
| 413 |
with |
|---|
| 414 |
@type lastPublicKey: L{Key} |
|---|
| 415 |
""" |
|---|
| 416 |
|
|---|
| 417 |
|
|---|
| 418 |
name = 'ssh-userauth' |
|---|
| 419 |
preferredOrder = ['publickey', 'password', 'keyboard-interactive'] |
|---|
| 420 |
|
|---|
| 421 |
|
|---|
| 422 |
def __init__(self, user, instance): |
|---|
| 423 |
self.user = user |
|---|
| 424 |
self.instance = instance |
|---|
| 425 |
|
|---|
| 426 |
|
|---|
| 427 |
def serviceStarted(self): |
|---|
| 428 |
self.authenticatedWith = [] |
|---|
| 429 |
self.triedPublicKeys = [] |
|---|
| 430 |
self.lastPublicKey = None |
|---|
| 431 |
self.askForAuth('none', '') |
|---|
| 432 |
|
|---|
| 433 |
|
|---|
| 434 |
def askForAuth(self, kind, extraData): |
|---|
| 435 |
""" |
|---|
| 436 |
Send a MSG_USERAUTH_REQUEST. |
|---|
| 437 |
|
|---|
| 438 |
@param kind: the authentication method to try. |
|---|
| 439 |
@type kind: C{str} |
|---|
| 440 |
@param extraData: method-specific data to go in the packet |
|---|
| 441 |
@type extraData: C{str} |
|---|
| 442 |
""" |
|---|
| 443 |
self.lastAuth = kind |
|---|
| 444 |
self.transport.sendPacket(MSG_USERAUTH_REQUEST, NS(self.user) + |
|---|
| 445 |
NS(self.instance.name) + NS(kind) + extraData) |
|---|
| 446 |
|
|---|
| 447 |
|
|---|
| 448 |
def tryAuth(self, kind): |
|---|
| 449 |
""" |
|---|
| 450 |
Dispatch to an authentication method. |
|---|
| 451 |
|
|---|
| 452 |
@param kind: the authentication method |
|---|
| 453 |
@type kind: C{str} |
|---|
| 454 |
""" |
|---|
| 455 |
kind = kind.replace('-', '_') |
|---|
| 456 |
log.msg('trying to auth with %s' % (kind,)) |
|---|
| 457 |
f = getattr(self,'auth_%s' % (kind,), None) |
|---|
| 458 |
if f: |
|---|
| 459 |
return f() |
|---|
| 460 |
|
|---|
| 461 |
|
|---|
| 462 |
def _ebAuth(self, ignored, *args): |
|---|
| 463 |
""" |
|---|
| 464 |
Generic callback for a failed authentication attempt. Respond by |
|---|
| 465 |
asking for the list of accepted methods (the 'none' method) |
|---|
| 466 |
""" |
|---|
| 467 |
self.askForAuth('none', '') |
|---|
| 468 |
|
|---|
| 469 |
|
|---|
| 470 |
def ssh_USERAUTH_SUCCESS(self, packet): |
|---|
| 471 |
""" |
|---|
| 472 |
We received a MSG_USERAUTH_SUCCESS. The server has accepted our |
|---|
| 473 |
authentication, so start the next service. |
|---|
| 474 |
""" |
|---|
| 475 |
self.transport.setService(self.instance) |
|---|
| 476 |
|
|---|
| 477 |
|
|---|
| 478 |
def ssh_USERAUTH_FAILURE(self, packet): |
|---|
| 479 |
""" |
|---|
| 480 |
We received a MSG_USERAUTH_FAILURE. Payload:: |
|---|
| 481 |
string methods |
|---|
| 482 |
byte partial success |
|---|
| 483 |
|
|---|
| 484 |
If partial success is True, then the previous method succeeded but is |
|---|
| 485 |
not sufficent for authentication. methods is a comma-separated list of |
|---|
| 486 |
accepted authentication methods. |
|---|
| 487 |
|
|---|
| 488 |
We sort the list of methods by their position in self.preferredOrder, |
|---|
| 489 |
removing methods that have already succeeded. We then call |
|---|
| 490 |
self.tryAuth with the most preferred method, |
|---|
| 491 |
""" |
|---|
| 492 |
canContinue, partial = getNS(packet) |
|---|
| 493 |
partial = ord(partial) |
|---|
| 494 |
if partial: |
|---|
| 495 |
self.authenticatedWith.append(self.lastAuth) |
|---|
| 496 |
def orderByPreference(meth): |
|---|
| 497 |
if meth in self.preferredOrder: |
|---|
| 498 |
return self.preferredOrder.index(meth) |
|---|
| 499 |
else: |
|---|
| 500 |
return -1 |
|---|
| 501 |
canContinue = util.dsu([meth for meth in canContinue.split(',') |
|---|
| 502 |
if meth not in self.authenticatedWith], |
|---|
| 503 |
orderByPreference) |
|---|
| 504 |
|
|---|
| 505 |
log.msg('can continue with: %s' % canContinue) |
|---|
| 506 |
return self._cbUserauthFailure(None, iter(canContinue)) |
|---|
| 507 |
|
|---|
| 508 |
|
|---|
| 509 |
def _cbUserauthFailure(self, result, iterator): |
|---|
| 510 |
if result: |
|---|
| 511 |
return |
|---|
| 512 |
try: |
|---|
| 513 |
method = iterator.next() |
|---|
| 514 |
except StopIteration: |
|---|
| 515 |
self.transport.sendDisconnect( |
|---|
| 516 |
transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, |
|---|
| 517 |
'no more authentication methods available') |
|---|
| 518 |
else: |
|---|
| 519 |
d = defer.maybeDeferred(self.tryAuth, method) |
|---|
| 520 |
d.addCallback(self._cbUserauthFailure, iterator) |
|---|
| 521 |
return d |
|---|
| 522 |
|
|---|
| 523 |
|
|---|
| 524 |
def ssh_USERAUTH_PK_OK(self, packet): |
|---|
| 525 |
""" |
|---|
| 526 |
This message (number 60) can mean several different messages depending |
|---|
| 527 |
on the current authentication type. We dispatch to individual methods |
|---|
| 528 |
in order to handle this request. |
|---|
| 529 |
""" |
|---|
| 530 |
func = getattr(self, 'ssh_USERAUTH_PK_OK_%s' % |
|---|
| 531 |
self.lastAuth.replace('-', '_'), None) |
|---|
| 532 |
if func is not None: |
|---|
| 533 |
return func(packet) |
|---|
| 534 |
else: |
|---|
| 535 |
self.askForAuth('none', '') |
|---|
| 536 |
|
|---|
| 537 |
|
|---|
| 538 |
def ssh_USERAUTH_PK_OK_publickey(self, packet): |
|---|
| 539 |
""" |
|---|
| 540 |
This is MSG_USERAUTH_PK. Our public key is valid, so we create a |
|---|
| 541 |
signature and try to authenticate with it. |
|---|
| 542 |
""" |
|---|
| 543 |
publicKey = self.lastPublicKey |
|---|
| 544 |
b = (NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) + |
|---|
| 545 |
NS(self.user) + NS(self.instance.name) + NS('publickey') + |
|---|
| 546 |
'\xff' + NS(publicKey.sshType()) + NS(publicKey.blob())) |
|---|
| 547 |
d = self.signData(publicKey, b) |
|---|
| 548 |
if not d: |
|---|
| 549 |
self.askForAuth('none', '') |
|---|
| 550 |
|
|---|
| 551 |
return |
|---|
| 552 |
d.addCallback(self._cbSignedData) |
|---|
| 553 |
d.addErrback(self._ebAuth) |
|---|
| 554 |
|
|---|
| 555 |
|
|---|
| 556 |
def ssh_USERAUTH_PK_OK_password(self, packet): |
|---|
| 557 |
""" |
|---|
| 558 |
This is MSG_USERAUTH_PASSWD_CHANGEREQ. The password given has expired. |
|---|
| 559 |
We ask for an old password and a new password, then send both back to |
|---|
| 560 |
the server. |
|---|
| 561 |
""" |
|---|
| 562 |
prompt, language, rest = getNS(packet, 2) |
|---|
| 563 |
self._oldPass = self._newPass = None |
|---|
| 564 |
d = self.getPassword('Old Password: ') |
|---|
| 565 |
d = d.addCallbacks(self._setOldPass, self._ebAuth) |
|---|
| 566 |
d.addCallback(lambda ignored: self.getPassword(prompt)) |
|---|
| 567 |
d.addCallbacks(self._setNewPass, self._ebAuth) |
|---|
| 568 |
|
|---|
| 569 |
|
|---|
| 570 |
def ssh_USERAUTH_PK_OK_keyboard_interactive(self, packet): |
|---|
| 571 |
""" |
|---|
| 572 |
This is MSG_USERAUTH_INFO_RESPONSE. The server has sent us the |
|---|
| 573 |
questions it wants us to answer, so we ask the user and sent the |
|---|
| 574 |
responses. |
|---|
| 575 |
""" |
|---|
| 576 |
name, instruction, lang, data = getNS(packet, 3) |
|---|
| 577 |
numPrompts = struct.unpack('!L', data[:4])[0] |
|---|
| 578 |
data = data[4:] |
|---|
| 579 |
prompts = [] |
|---|
| 580 |
for i in range(numPrompts): |
|---|
| 581 |
prompt, data = getNS(data) |
|---|
| 582 |
echo = bool(ord(data[0])) |
|---|
| 583 |
data = data[1:] |
|---|
| 584 |
prompts.append((prompt, echo)) |
|---|
| 585 |
d = self.getGenericAnswers(name, instruction, prompts) |
|---|
| 586 |
d.addCallback(self._cbGenericAnswers) |
|---|
| 587 |
d.addErrback(self._ebAuth) |
|---|
| 588 |
|
|---|
| 589 |
|
|---|
| 590 |
def _cbSignedData(self, signedData): |
|---|
| 591 |
""" |
|---|
| 592 |
Called back out of self.signData with the signed data. Send the |
|---|
| 593 |
authentication request with the signature. |
|---|
| 594 |
|
|---|
| 595 |
@param signedData: the data signed by the user's private key. |
|---|
| 596 |
@type signedData: C{str} |
|---|
| 597 |
""" |
|---|
| 598 |
publicKey = self.lastPublicKey |
|---|
| 599 |
self.askForAuth('publickey', '\xff' + NS(publicKey.sshType()) + |
|---|
| 600 |
NS(publicKey.blob()) + NS(signedData)) |
|---|
| 601 |
|
|---|
| 602 |
|
|---|
| 603 |
def _setOldPass(self, op): |
|---|
| 604 |
""" |
|---|
| 605 |
Called back when we are choosing a new password. Simply store the old |
|---|
| 606 |
password for now. |
|---|
| 607 |
|
|---|
| 608 |
@param op: the old password as entered by the user |
|---|
| 609 |
@type op: C{str} |
|---|
| 610 |
""" |
|---|
| 611 |
self._oldPass = op |
|---|
| 612 |
|
|---|
| 613 |
|
|---|
| 614 |
def _setNewPass(self, np): |
|---|
| 615 |
""" |
|---|
| 616 |
Called back when we are choosing a new password. Get the old password |
|---|
| 617 |
and send the authentication message with both. |
|---|
| 618 |
|
|---|
| 619 |
@param np: the new password as entered by the user |
|---|
| 620 |
@type np: C{str} |
|---|
| 621 |
""" |
|---|
| 622 |
op = self._oldPass |
|---|
| 623 |
self._oldPass = None |
|---|
| 624 |
self.askForAuth('password', '\xff' + NS(op) + NS(np)) |
|---|
| 625 |
|
|---|
| 626 |
|
|---|
| 627 |
def _cbGenericAnswers(self, responses): |
|---|
| 628 |
""" |
|---|
| 629 |
Called back when we are finished answering keyboard-interactive |
|---|
| 630 |
questions. Send the info back to the server in a |
|---|
| 631 |
MSG_USERAUTH_INFO_RESPONSE. |
|---|
| 632 |
|
|---|
| 633 |
@param responses: a list of C{str} responses |
|---|
| 634 |
@type responses: C{list} |
|---|
| 635 |
""" |
|---|
| 636 |
data = struct.pack('!L', len(responses)) |
|---|
| 637 |
for r in responses: |
|---|
| 638 |
data += NS(r.encode('UTF8')) |
|---|
| 639 |
self.transport.sendPacket(MSG_USERAUTH_INFO_RESPONSE, data) |
|---|
| 640 |
|
|---|
| 641 |
|
|---|
| 642 |
def auth_publickey(self): |
|---|
| 643 |
""" |
|---|
| 644 |
Try to authenticate with a public key. Ask the user for a public key; |
|---|
| 645 |
if the user has one, send the request to the server and return True. |
|---|
| 646 |
Otherwise, return False. |
|---|
| 647 |
|
|---|
| 648 |
@rtype: C{bool} |
|---|
| 649 |
""" |
|---|
| 650 |
d = defer.maybeDeferred(self.getPublicKey) |
|---|
| 651 |
d.addBoth(self._cbGetPublicKey) |
|---|
| 652 |
return d |
|---|
| 653 |
|
|---|
| 654 |
|
|---|
| 655 |
def _cbGetPublicKey(self, publicKey): |
|---|
| 656 |
if isinstance(publicKey, str): |
|---|
| 657 |
warnings.warn("Returning a string from " |
|---|
| 658 |
"SSHUserAuthClient.getPublicKey() is deprecated " |
|---|
| 659 |
"since Twisted 9.0. Return a keys.Key() instead.", |
|---|
| 660 |
DeprecationWarning) |
|---|
| 661 |
publicKey = keys.Key.fromString(publicKey) |
|---|
| 662 |
if not isinstance(publicKey, keys.Key): |
|---|
| 663 |
publicKey = None |
|---|
| 664 |
if publicKey is not None: |
|---|
| 665 |
self.lastPublicKey = publicKey |
|---|
| 666 |
self.triedPublicKeys.append(publicKey) |
|---|
| 667 |
log.msg('using key of type %s' % publicKey.type()) |
|---|
| 668 |
self.askForAuth('publickey', '\x00' + NS(publicKey.sshType()) + |
|---|
| 669 |
NS(publicKey.blob())) |
|---|
| 670 |
return True |
|---|
| 671 |
else: |
|---|
| 672 |
return False |
|---|
| 673 |
|
|---|
| 674 |
|
|---|
| 675 |
def auth_password(self): |
|---|
| 676 |
""" |
|---|
| 677 |
Try to authenticate with a password. Ask the user for a password. |
|---|
| 678 |
If the user will return a password, return True. Otherwise, return |
|---|
| 679 |
False. |
|---|
| 680 |
|
|---|
| 681 |
@rtype: C{bool} |
|---|
| 682 |
""" |
|---|
| 683 |
d = self.getPassword() |
|---|
| 684 |
if d: |
|---|
| 685 |
d.addCallbacks(self._cbPassword, self._ebAuth) |
|---|
| 686 |
return True |
|---|
| 687 |
else: |
|---|
| 688 |
return False |
|---|
| 689 |
|
|---|
| 690 |
|
|---|
| 691 |
def auth_keyboard_interactive(self): |
|---|
| 692 |
""" |
|---|
| 693 |
Try to authenticate with keyboard-interactive authentication. Send |
|---|
| 694 |
the request to the server and return True. |
|---|
| 695 |
|
|---|
| 696 |
@rtype: C{bool} |
|---|
| 697 |
""" |
|---|
| 698 |
log.msg('authing with keyboard-interactive') |
|---|
| 699 |
self.askForAuth('keyboard-interactive', NS('') + NS('')) |
|---|
| 700 |
return True |
|---|
| 701 |
|
|---|
| 702 |
|
|---|
| 703 |
def _cbPassword(self, password): |
|---|
| 704 |
""" |
|---|
| 705 |
Called back when the user gives a password. Send the request to the |
|---|
| 706 |
server. |
|---|
| 707 |
|
|---|
| 708 |
@param password: the password the user entered |
|---|
| 709 |
@type password: C{str} |
|---|
| 710 |
""" |
|---|
| 711 |
self.askForAuth('password', '\x00' + NS(password)) |
|---|
| 712 |
|
|---|
| 713 |
|
|---|
| 714 |
def signData(self, publicKey, signData): |
|---|
| 715 |
""" |
|---|
| 716 |
Sign the given data with the given public key. |
|---|
| 717 |
|
|---|
| 718 |
By default, this will call getPrivateKey to get the private key, |
|---|
| 719 |
then sign the data using Key.sign(). |
|---|
| 720 |
|
|---|
| 721 |
This method is factored out so that it can be overridden to use |
|---|
| 722 |
alternate methods, such as a key agent. |
|---|
| 723 |
|
|---|
| 724 |
@param publicKey: The public key object returned from L{getPublicKey} |
|---|
| 725 |
@type publicKey: L{keys.Key} |
|---|
| 726 |
|
|---|
| 727 |
@param signData: the data to be signed by the private key. |
|---|
| 728 |
@type signData: C{str} |
|---|
| 729 |
@return: a Deferred that's called back with the signature |
|---|
| 730 |
@rtype: L{defer.Deferred} |
|---|
| 731 |
""" |
|---|
| 732 |
key = self.getPrivateKey() |
|---|
| 733 |
if not key: |
|---|
| 734 |
return |
|---|
| 735 |
return key.addCallback(self._cbSignData, signData) |
|---|
| 736 |
|
|---|
| 737 |
|
|---|
| 738 |
def _cbSignData(self, privateKey, signData): |
|---|
| 739 |
""" |
|---|
| 740 |
Called back when the private key is returned. Sign the data and |
|---|
| 741 |
return the signature. |
|---|
| 742 |
|
|---|
| 743 |
@param privateKey: the private key object |
|---|
| 744 |
@type publicKey: L{keys.Key} |
|---|
| 745 |
@param signData: the data to be signed by the private key. |
|---|
| 746 |
@type signData: C{str} |
|---|
| 747 |
@return: the signature |
|---|
| 748 |
@rtype: C{str} |
|---|
| 749 |
""" |
|---|
| 750 |
if not isinstance(privateKey, keys.Key): |
|---|
| 751 |
warnings.warn("Returning a PyCrypto key object from " |
|---|
| 752 |
"SSHUserAuthClient.getPrivateKey() is deprecated " |
|---|
| 753 |
"since Twisted 9.0. Return a keys.Key() instead.", |
|---|
| 754 |
DeprecationWarning) |
|---|
| 755 |
privateKey = keys.Key(privateKey) |
|---|
| 756 |
return privateKey.sign(signData) |
|---|
| 757 |
|
|---|
| 758 |
|
|---|
| 759 |
def getPublicKey(self): |
|---|
| 760 |
""" |
|---|
| 761 |
Return a public key for the user. If no more public keys are |
|---|
| 762 |
available, return C{None}. |
|---|
| 763 |
|
|---|
| 764 |
This implementation always returns C{None}. Override it in a |
|---|
| 765 |
subclass to actually find and return a public key object. |
|---|
| 766 |
|
|---|
| 767 |
@rtype: L{Key} or L{NoneType} |
|---|
| 768 |
""" |
|---|
| 769 |
return None |
|---|
| 770 |
|
|---|
| 771 |
|
|---|
| 772 |
def getPrivateKey(self): |
|---|
| 773 |
""" |
|---|
| 774 |
Return a L{Deferred} that will be called back with the private key |
|---|
| 775 |
object corresponding to the last public key from getPublicKey(). |
|---|
| 776 |
If the private key is not available, errback on the Deferred. |
|---|
| 777 |
|
|---|
| 778 |
@rtype: L{Deferred} called back with L{Key} |
|---|
| 779 |
""" |
|---|
| 780 |
return defer.fail(NotImplementedError()) |
|---|
| 781 |
|
|---|
| 782 |
|
|---|
| 783 |
def getPassword(self, prompt = None): |
|---|
| 784 |
""" |
|---|
| 785 |
Return a L{Deferred} that will be called back with a password. |
|---|
| 786 |
prompt is a string to display for the password, or None for a generic |
|---|
| 787 |
'user@hostname's password: '. |
|---|
| 788 |
|
|---|
| 789 |
@type prompt: C{str}/C{None} |
|---|
| 790 |
@rtype: L{defer.Deferred} |
|---|
| 791 |
""" |
|---|
| 792 |
return defer.fail(NotImplementedError()) |
|---|
| 793 |
|
|---|
| 794 |
|
|---|
| 795 |
def getGenericAnswers(self, name, instruction, prompts): |
|---|
| 796 |
""" |
|---|
| 797 |
Returns a L{Deferred} with the responses to the promopts. |
|---|
| 798 |
|
|---|
| 799 |
@param name: The name of the authentication currently in progress. |
|---|
| 800 |
@param instruction: Describes what the authentication wants. |
|---|
| 801 |
@param prompts: A list of (prompt, echo) pairs, where prompt is a |
|---|
| 802 |
string to display and echo is a boolean indicating whether the |
|---|
| 803 |
user's response should be echoed as they type it. |
|---|
| 804 |
""" |
|---|
| 805 |
return defer.fail(NotImplementedError()) |
|---|
| 806 |
|
|---|
| 807 |
|
|---|
| 808 |
MSG_USERAUTH_REQUEST = 50 |
|---|
| 809 |
MSG_USERAUTH_FAILURE = 51 |
|---|
| 810 |
MSG_USERAUTH_SUCCESS = 52 |
|---|
| 811 |
MSG_USERAUTH_BANNER = 53 |
|---|
| 812 |
MSG_USERAUTH_PASSWD_CHANGEREQ = 60 |
|---|
| 813 |
MSG_USERAUTH_INFO_REQUEST = 60 |
|---|
| 814 |
MSG_USERAUTH_INFO_RESPONSE = 61 |
|---|
| 815 |
MSG_USERAUTH_PK_OK = 60 |
|---|
| 816 |
|
|---|
| 817 |
messages = {} |
|---|
| 818 |
for k, v in locals().items(): |
|---|
| 819 |
if k[:4]=='MSG_': |
|---|
| 820 |
messages[v] = k |
|---|
| 821 |
|
|---|
| 822 |
SSHUserAuthServer.protocolMessages = messages |
|---|
| 823 |
SSHUserAuthClient.protocolMessages = messages |
|---|
| 824 |
del messages |
|---|
| 825 |
del v |
|---|