checkers.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. # -*- test-case-name: twisted.conch.test.test_checkers -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Provide L{ICredentialsChecker} implementations to be used in Conch protocols.
  6. """
  7. from __future__ import absolute_import, division
  8. import sys
  9. import binascii
  10. import errno
  11. try:
  12. import pwd
  13. except ImportError:
  14. pwd = None
  15. else:
  16. import crypt
  17. try:
  18. import spwd
  19. except ImportError:
  20. spwd = None
  21. from zope.interface import providedBy, implementer, Interface
  22. from incremental import Version
  23. from twisted.conch import error
  24. from twisted.conch.ssh import keys
  25. from twisted.cred.checkers import ICredentialsChecker
  26. from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey
  27. from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials
  28. from twisted.internet import defer
  29. from twisted.python.compat import _keys, _PY3, _b64decodebytes
  30. from twisted.python import failure, reflect, log
  31. from twisted.python.deprecate import deprecatedModuleAttribute
  32. from twisted.python.util import runAsEffectiveUser
  33. from twisted.python.filepath import FilePath
  34. def verifyCryptedPassword(crypted, pw):
  35. """
  36. Check that the password, when crypted, matches the stored crypted password.
  37. @param crypted: The stored crypted password.
  38. @type crypted: L{str}
  39. @param pw: The password the user has given.
  40. @type pw: L{str}
  41. @rtype: L{bool}
  42. """
  43. return crypt.crypt(pw, crypted) == crypted
  44. def _pwdGetByName(username):
  45. """
  46. Look up a user in the /etc/passwd database using the pwd module. If the
  47. pwd module is not available, return None.
  48. @param username: the username of the user to return the passwd database
  49. information for.
  50. @type username: L{str}
  51. """
  52. if pwd is None:
  53. return None
  54. return pwd.getpwnam(username)
  55. def _shadowGetByName(username):
  56. """
  57. Look up a user in the /etc/shadow database using the spwd module. If it is
  58. not available, return L{None}.
  59. @param username: the username of the user to return the shadow database
  60. information for.
  61. @type username: L{str}
  62. """
  63. if spwd is not None:
  64. f = spwd.getspnam
  65. else:
  66. return None
  67. return runAsEffectiveUser(0, 0, f, username)
  68. @implementer(ICredentialsChecker)
  69. class UNIXPasswordDatabase:
  70. """
  71. A checker which validates users out of the UNIX password databases, or
  72. databases of a compatible format.
  73. @ivar _getByNameFunctions: a C{list} of functions which are called in order
  74. to valid a user. The default value is such that the C{/etc/passwd}
  75. database will be tried first, followed by the C{/etc/shadow} database.
  76. """
  77. credentialInterfaces = IUsernamePassword,
  78. def __init__(self, getByNameFunctions=None):
  79. if getByNameFunctions is None:
  80. getByNameFunctions = [_pwdGetByName, _shadowGetByName]
  81. self._getByNameFunctions = getByNameFunctions
  82. def requestAvatarId(self, credentials):
  83. # We get bytes, but the Py3 pwd module uses str. So attempt to decode
  84. # it using the same method that CPython does for the file on disk.
  85. if _PY3:
  86. username = credentials.username.decode(sys.getfilesystemencoding())
  87. password = credentials.password.decode(sys.getfilesystemencoding())
  88. else:
  89. username = credentials.username
  90. password = credentials.password
  91. for func in self._getByNameFunctions:
  92. try:
  93. pwnam = func(username)
  94. except KeyError:
  95. return defer.fail(UnauthorizedLogin("invalid username"))
  96. else:
  97. if pwnam is not None:
  98. crypted = pwnam[1]
  99. if crypted == '':
  100. continue
  101. if verifyCryptedPassword(crypted, password):
  102. return defer.succeed(credentials.username)
  103. # fallback
  104. return defer.fail(UnauthorizedLogin("unable to verify password"))
  105. @implementer(ICredentialsChecker)
  106. class SSHPublicKeyDatabase:
  107. """
  108. Checker that authenticates SSH public keys, based on public keys listed in
  109. authorized_keys and authorized_keys2 files in user .ssh/ directories.
  110. """
  111. credentialInterfaces = (ISSHPrivateKey,)
  112. _userdb = pwd
  113. def requestAvatarId(self, credentials):
  114. d = defer.maybeDeferred(self.checkKey, credentials)
  115. d.addCallback(self._cbRequestAvatarId, credentials)
  116. d.addErrback(self._ebRequestAvatarId)
  117. return d
  118. def _cbRequestAvatarId(self, validKey, credentials):
  119. """
  120. Check whether the credentials themselves are valid, now that we know
  121. if the key matches the user.
  122. @param validKey: A boolean indicating whether or not the public key
  123. matches a key in the user's authorized_keys file.
  124. @param credentials: The credentials offered by the user.
  125. @type credentials: L{ISSHPrivateKey} provider
  126. @raise UnauthorizedLogin: (as a failure) if the key does not match the
  127. user in C{credentials}. Also raised if the user provides an invalid
  128. signature.
  129. @raise ValidPublicKey: (as a failure) if the key matches the user but
  130. the credentials do not include a signature. See
  131. L{error.ValidPublicKey} for more information.
  132. @return: The user's username, if authentication was successful.
  133. """
  134. if not validKey:
  135. return failure.Failure(UnauthorizedLogin("invalid key"))
  136. if not credentials.signature:
  137. return failure.Failure(error.ValidPublicKey())
  138. else:
  139. try:
  140. pubKey = keys.Key.fromString(credentials.blob)
  141. if pubKey.verify(credentials.signature, credentials.sigData):
  142. return credentials.username
  143. except: # any error should be treated as a failed login
  144. log.err()
  145. return failure.Failure(UnauthorizedLogin('error while verifying key'))
  146. return failure.Failure(UnauthorizedLogin("unable to verify key"))
  147. def getAuthorizedKeysFiles(self, credentials):
  148. """
  149. Return a list of L{FilePath} instances for I{authorized_keys} files
  150. which might contain information about authorized keys for the given
  151. credentials.
  152. On OpenSSH servers, the default location of the file containing the
  153. list of authorized public keys is
  154. U{$HOME/.ssh/authorized_keys<http://www.openbsd.org/cgi-bin/man.cgi?query=sshd_config>}.
  155. I{$HOME/.ssh/authorized_keys2} is also returned, though it has been
  156. U{deprecated by OpenSSH since
  157. 2001<http://marc.info/?m=100508718416162>}.
  158. @return: A list of L{FilePath} instances to files with the authorized keys.
  159. """
  160. pwent = self._userdb.getpwnam(credentials.username)
  161. root = FilePath(pwent.pw_dir).child('.ssh')
  162. files = ['authorized_keys', 'authorized_keys2']
  163. return [root.child(f) for f in files]
  164. def checkKey(self, credentials):
  165. """
  166. Retrieve files containing authorized keys and check against user
  167. credentials.
  168. """
  169. ouid, ogid = self._userdb.getpwnam(credentials.username)[2:4]
  170. for filepath in self.getAuthorizedKeysFiles(credentials):
  171. if not filepath.exists():
  172. continue
  173. try:
  174. lines = filepath.open()
  175. except IOError as e:
  176. if e.errno == errno.EACCES:
  177. lines = runAsEffectiveUser(ouid, ogid, filepath.open)
  178. else:
  179. raise
  180. with lines:
  181. for l in lines:
  182. l2 = l.split()
  183. if len(l2) < 2:
  184. continue
  185. try:
  186. if _b64decodebytes(l2[1]) == credentials.blob:
  187. return True
  188. except binascii.Error:
  189. continue
  190. return False
  191. def _ebRequestAvatarId(self, f):
  192. if not f.check(UnauthorizedLogin):
  193. log.msg(f)
  194. return failure.Failure(UnauthorizedLogin("unable to get avatar id"))
  195. return f
  196. @implementer(ICredentialsChecker)
  197. class SSHProtocolChecker:
  198. """
  199. SSHProtocolChecker is a checker that requires multiple authentications
  200. to succeed. To add a checker, call my registerChecker method with
  201. the checker and the interface.
  202. After each successful authenticate, I call my areDone method with the
  203. avatar id. To get a list of the successful credentials for an avatar id,
  204. use C{SSHProcotolChecker.successfulCredentials[avatarId]}. If L{areDone}
  205. returns True, the authentication has succeeded.
  206. """
  207. def __init__(self):
  208. self.checkers = {}
  209. self.successfulCredentials = {}
  210. def get_credentialInterfaces(self):
  211. return _keys(self.checkers)
  212. credentialInterfaces = property(get_credentialInterfaces)
  213. def registerChecker(self, checker, *credentialInterfaces):
  214. if not credentialInterfaces:
  215. credentialInterfaces = checker.credentialInterfaces
  216. for credentialInterface in credentialInterfaces:
  217. self.checkers[credentialInterface] = checker
  218. def requestAvatarId(self, credentials):
  219. """
  220. Part of the L{ICredentialsChecker} interface. Called by a portal with
  221. some credentials to check if they'll authenticate a user. We check the
  222. interfaces that the credentials provide against our list of acceptable
  223. checkers. If one of them matches, we ask that checker to verify the
  224. credentials. If they're valid, we call our L{_cbGoodAuthentication}
  225. method to continue.
  226. @param credentials: the credentials the L{Portal} wants us to verify
  227. """
  228. ifac = providedBy(credentials)
  229. for i in ifac:
  230. c = self.checkers.get(i)
  231. if c is not None:
  232. d = defer.maybeDeferred(c.requestAvatarId, credentials)
  233. return d.addCallback(self._cbGoodAuthentication,
  234. credentials)
  235. return defer.fail(UnhandledCredentials("No checker for %s" % \
  236. ', '.join(map(reflect.qual, ifac))))
  237. def _cbGoodAuthentication(self, avatarId, credentials):
  238. """
  239. Called if a checker has verified the credentials. We call our
  240. L{areDone} method to see if the whole of the successful authentications
  241. are enough. If they are, we return the avatar ID returned by the first
  242. checker.
  243. """
  244. if avatarId not in self.successfulCredentials:
  245. self.successfulCredentials[avatarId] = []
  246. self.successfulCredentials[avatarId].append(credentials)
  247. if self.areDone(avatarId):
  248. del self.successfulCredentials[avatarId]
  249. return avatarId
  250. else:
  251. raise error.NotEnoughAuthentication()
  252. def areDone(self, avatarId):
  253. """
  254. Override to determine if the authentication is finished for a given
  255. avatarId.
  256. @param avatarId: the avatar returned by the first checker. For
  257. this checker to function correctly, all the checkers must
  258. return the same avatar ID.
  259. """
  260. return True
  261. deprecatedModuleAttribute(
  262. Version("Twisted", 15, 0, 0),
  263. ("Please use twisted.conch.checkers.SSHPublicKeyChecker, "
  264. "initialized with an instance of "
  265. "twisted.conch.checkers.UNIXAuthorizedKeysFiles instead."),
  266. __name__, "SSHPublicKeyDatabase")
  267. class IAuthorizedKeysDB(Interface):
  268. """
  269. An object that provides valid authorized ssh keys mapped to usernames.
  270. @since: 15.0
  271. """
  272. def getAuthorizedKeys(avatarId):
  273. """
  274. Gets an iterable of authorized keys that are valid for the given
  275. C{avatarId}.
  276. @param avatarId: the ID of the avatar
  277. @type avatarId: valid return value of
  278. L{twisted.cred.checkers.ICredentialsChecker.requestAvatarId}
  279. @return: an iterable of L{twisted.conch.ssh.keys.Key}
  280. """
  281. def readAuthorizedKeyFile(fileobj, parseKey=keys.Key.fromString):
  282. """
  283. Reads keys from an authorized keys file. Any non-comment line that cannot
  284. be parsed as a key will be ignored, although that particular line will
  285. be logged.
  286. @param fileobj: something from which to read lines which can be parsed
  287. as keys
  288. @type fileobj: L{file}-like object
  289. @param parseKey: a callable that takes a string and returns a
  290. L{twisted.conch.ssh.keys.Key}, mainly to be used for testing. The
  291. default is L{twisted.conch.ssh.keys.Key.fromString}.
  292. @type parseKey: L{callable}
  293. @return: an iterable of L{twisted.conch.ssh.keys.Key}
  294. @rtype: iterable
  295. @since: 15.0
  296. """
  297. for line in fileobj:
  298. line = line.strip()
  299. if line and not line.startswith(b'#'): # for comments
  300. try:
  301. yield parseKey(line)
  302. except keys.BadKeyError as e:
  303. log.msg('Unable to parse line "{0}" as a key: {1!s}'
  304. .format(line, e))
  305. def _keysFromFilepaths(filepaths, parseKey):
  306. """
  307. Helper function that turns an iterable of filepaths into a generator of
  308. keys. If any file cannot be read, a message is logged but it is
  309. otherwise ignored.
  310. @param filepaths: iterable of L{twisted.python.filepath.FilePath}.
  311. @type filepaths: iterable
  312. @param parseKey: a callable that takes a string and returns a
  313. L{twisted.conch.ssh.keys.Key}
  314. @type parseKey: L{callable}
  315. @return: generator of L{twisted.conch.ssh.keys.Key}
  316. @rtype: generator
  317. @since: 15.0
  318. """
  319. for fp in filepaths:
  320. if fp.exists():
  321. try:
  322. with fp.open() as f:
  323. for key in readAuthorizedKeyFile(f, parseKey):
  324. yield key
  325. except (IOError, OSError) as e:
  326. log.msg("Unable to read {0}: {1!s}".format(fp.path, e))
  327. @implementer(IAuthorizedKeysDB)
  328. class InMemorySSHKeyDB(object):
  329. """
  330. Object that provides SSH public keys based on a dictionary of usernames
  331. mapped to L{twisted.conch.ssh.keys.Key}s.
  332. @since: 15.0
  333. """
  334. def __init__(self, mapping):
  335. """
  336. Initializes a new L{InMemorySSHKeyDB}.
  337. @param mapping: mapping of usernames to iterables of
  338. L{twisted.conch.ssh.keys.Key}s
  339. @type mapping: L{dict}
  340. """
  341. self._mapping = mapping
  342. def getAuthorizedKeys(self, username):
  343. return self._mapping.get(username, [])
  344. @implementer(IAuthorizedKeysDB)
  345. class UNIXAuthorizedKeysFiles(object):
  346. """
  347. Object that provides SSH public keys based on public keys listed in
  348. authorized_keys and authorized_keys2 files in UNIX user .ssh/ directories.
  349. If any of the files cannot be read, a message is logged but that file is
  350. otherwise ignored.
  351. @since: 15.0
  352. """
  353. def __init__(self, userdb=None, parseKey=keys.Key.fromString):
  354. """
  355. Initializes a new L{UNIXAuthorizedKeysFiles}.
  356. @param userdb: access to the Unix user account and password database
  357. (default is the Python module L{pwd})
  358. @type userdb: L{pwd}-like object
  359. @param parseKey: a callable that takes a string and returns a
  360. L{twisted.conch.ssh.keys.Key}, mainly to be used for testing. The
  361. default is L{twisted.conch.ssh.keys.Key.fromString}.
  362. @type parseKey: L{callable}
  363. """
  364. self._userdb = userdb
  365. self._parseKey = parseKey
  366. if userdb is None:
  367. self._userdb = pwd
  368. def getAuthorizedKeys(self, username):
  369. try:
  370. passwd = self._userdb.getpwnam(username)
  371. except KeyError:
  372. return ()
  373. root = FilePath(passwd.pw_dir).child('.ssh')
  374. files = ['authorized_keys', 'authorized_keys2']
  375. return _keysFromFilepaths((root.child(f) for f in files),
  376. self._parseKey)
  377. @implementer(ICredentialsChecker)
  378. class SSHPublicKeyChecker(object):
  379. """
  380. Checker that authenticates SSH public keys, based on public keys listed in
  381. authorized_keys and authorized_keys2 files in user .ssh/ directories.
  382. Initializing this checker with a L{UNIXAuthorizedKeysFiles} should be
  383. used instead of L{twisted.conch.checkers.SSHPublicKeyDatabase}.
  384. @since: 15.0
  385. """
  386. credentialInterfaces = (ISSHPrivateKey,)
  387. def __init__(self, keydb):
  388. """
  389. Initializes a L{SSHPublicKeyChecker}.
  390. @param keydb: a provider of L{IAuthorizedKeysDB}
  391. @type keydb: L{IAuthorizedKeysDB} provider
  392. """
  393. self._keydb = keydb
  394. def requestAvatarId(self, credentials):
  395. d = defer.maybeDeferred(self._sanityCheckKey, credentials)
  396. d.addCallback(self._checkKey, credentials)
  397. d.addCallback(self._verifyKey, credentials)
  398. return d
  399. def _sanityCheckKey(self, credentials):
  400. """
  401. Checks whether the provided credentials are a valid SSH key with a
  402. signature (does not actually verify the signature).
  403. @param credentials: the credentials offered by the user
  404. @type credentials: L{ISSHPrivateKey} provider
  405. @raise ValidPublicKey: the credentials do not include a signature. See
  406. L{error.ValidPublicKey} for more information.
  407. @raise BadKeyError: The key included with the credentials is not
  408. recognized as a key.
  409. @return: the key in the credentials
  410. @rtype: L{twisted.conch.ssh.keys.Key}
  411. """
  412. if not credentials.signature:
  413. raise error.ValidPublicKey()
  414. return keys.Key.fromString(credentials.blob)
  415. def _checkKey(self, pubKey, credentials):
  416. """
  417. Checks the public key against all authorized keys (if any) for the
  418. user.
  419. @param pubKey: the key in the credentials (just to prevent it from
  420. having to be calculated again)
  421. @type pubKey:
  422. @param credentials: the credentials offered by the user
  423. @type credentials: L{ISSHPrivateKey} provider
  424. @raise UnauthorizedLogin: If the key is not authorized, or if there
  425. was any error obtaining a list of authorized keys for the user.
  426. @return: C{pubKey} if the key is authorized
  427. @rtype: L{twisted.conch.ssh.keys.Key}
  428. """
  429. if any(key == pubKey for key in
  430. self._keydb.getAuthorizedKeys(credentials.username)):
  431. return pubKey
  432. raise UnauthorizedLogin("Key not authorized")
  433. def _verifyKey(self, pubKey, credentials):
  434. """
  435. Checks whether the credentials themselves are valid, now that we know
  436. if the key matches the user.
  437. @param pubKey: the key in the credentials (just to prevent it from
  438. having to be calculated again)
  439. @type pubKey: L{twisted.conch.ssh.keys.Key}
  440. @param credentials: the credentials offered by the user
  441. @type credentials: L{ISSHPrivateKey} provider
  442. @raise UnauthorizedLogin: If the key signature is invalid or there
  443. was any error verifying the signature.
  444. @return: The user's username, if authentication was successful
  445. @rtype: L{bytes}
  446. """
  447. try:
  448. if pubKey.verify(credentials.signature, credentials.sigData):
  449. return credentials.username
  450. except: # Any error should be treated as a failed login
  451. log.err()
  452. raise UnauthorizedLogin('Error while verifying key')
  453. raise UnauthorizedLogin("Key signature invalid.")