You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

credentials.py 16KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. # -*- test-case-name: twisted.cred.test.test_cred-*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. This module defines L{ICredentials}, an interface for objects that represent
  6. authentication credentials to provide, and also includes a number of useful
  7. implementations of that interface.
  8. """
  9. from __future__ import division, absolute_import
  10. from zope.interface import implementer, Interface
  11. import base64
  12. import hmac
  13. import random
  14. import re
  15. import time
  16. from binascii import hexlify
  17. from hashlib import md5
  18. from twisted.python.randbytes import secureRandom
  19. from twisted.python.compat import networkString, nativeString
  20. from twisted.python.compat import intToBytes, unicode
  21. from twisted.cred._digest import calcResponse, calcHA1, calcHA2
  22. from twisted.cred import error
  23. class ICredentials(Interface):
  24. """
  25. I check credentials.
  26. Implementors _must_ specify which sub-interfaces of ICredentials
  27. to which it conforms, using L{zope.interface.declarations.implementer}.
  28. """
  29. class IUsernameDigestHash(ICredentials):
  30. """
  31. This credential is used when a CredentialChecker has access to the hash
  32. of the username:realm:password as in an Apache .htdigest file.
  33. """
  34. def checkHash(digestHash):
  35. """
  36. @param digestHash: The hashed username:realm:password to check against.
  37. @return: C{True} if the credentials represented by this object match
  38. the given hash, C{False} if they do not, or a L{Deferred} which
  39. will be called back with one of these values.
  40. """
  41. class IUsernameHashedPassword(ICredentials):
  42. """
  43. I encapsulate a username and a hashed password.
  44. This credential is used when a hashed password is received from the
  45. party requesting authentication. CredentialCheckers which check this
  46. kind of credential must store the passwords in plaintext (or as
  47. password-equivalent hashes) form so that they can be hashed in a manner
  48. appropriate for the particular credentials class.
  49. @type username: L{bytes}
  50. @ivar username: The username associated with these credentials.
  51. """
  52. def checkPassword(password):
  53. """
  54. Validate these credentials against the correct password.
  55. @type password: L{bytes}
  56. @param password: The correct, plaintext password against which to
  57. check.
  58. @rtype: C{bool} or L{Deferred}
  59. @return: C{True} if the credentials represented by this object match the
  60. given password, C{False} if they do not, or a L{Deferred} which will
  61. be called back with one of these values.
  62. """
  63. class IUsernamePassword(ICredentials):
  64. """
  65. I encapsulate a username and a plaintext password.
  66. This encapsulates the case where the password received over the network
  67. has been hashed with the identity function (That is, not at all). The
  68. CredentialsChecker may store the password in whatever format it desires,
  69. it need only transform the stored password in a similar way before
  70. performing the comparison.
  71. @type username: L{bytes}
  72. @ivar username: The username associated with these credentials.
  73. @type password: L{bytes}
  74. @ivar password: The password associated with these credentials.
  75. """
  76. def checkPassword(password):
  77. """
  78. Validate these credentials against the correct password.
  79. @type password: L{bytes}
  80. @param password: The correct, plaintext password against which to
  81. check.
  82. @rtype: C{bool} or L{Deferred}
  83. @return: C{True} if the credentials represented by this object match the
  84. given password, C{False} if they do not, or a L{Deferred} which will
  85. be called back with one of these values.
  86. """
  87. class IAnonymous(ICredentials):
  88. """
  89. I am an explicitly anonymous request for access.
  90. """
  91. @implementer(IUsernameHashedPassword, IUsernameDigestHash)
  92. class DigestedCredentials(object):
  93. """
  94. Yet Another Simple HTTP Digest authentication scheme.
  95. """
  96. def __init__(self, username, method, realm, fields):
  97. self.username = username
  98. self.method = method
  99. self.realm = realm
  100. self.fields = fields
  101. def checkPassword(self, password):
  102. """
  103. Verify that the credentials represented by this object agree with the
  104. given plaintext C{password} by hashing C{password} in the same way the
  105. response hash represented by this object was generated and comparing
  106. the results.
  107. """
  108. response = self.fields.get('response')
  109. uri = self.fields.get('uri')
  110. nonce = self.fields.get('nonce')
  111. cnonce = self.fields.get('cnonce')
  112. nc = self.fields.get('nc')
  113. algo = self.fields.get('algorithm', b'md5').lower()
  114. qop = self.fields.get('qop', b'auth')
  115. expected = calcResponse(
  116. calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
  117. calcHA2(algo, self.method, uri, qop, None),
  118. algo, nonce, nc, cnonce, qop)
  119. return expected == response
  120. def checkHash(self, digestHash):
  121. """
  122. Verify that the credentials represented by this object agree with the
  123. credentials represented by the I{H(A1)} given in C{digestHash}.
  124. @param digestHash: A precomputed H(A1) value based on the username,
  125. realm, and password associate with this credentials object.
  126. """
  127. response = self.fields.get('response')
  128. uri = self.fields.get('uri')
  129. nonce = self.fields.get('nonce')
  130. cnonce = self.fields.get('cnonce')
  131. nc = self.fields.get('nc')
  132. algo = self.fields.get('algorithm', b'md5').lower()
  133. qop = self.fields.get('qop', b'auth')
  134. expected = calcResponse(
  135. calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
  136. calcHA2(algo, self.method, uri, qop, None),
  137. algo, nonce, nc, cnonce, qop)
  138. return expected == response
  139. class DigestCredentialFactory(object):
  140. """
  141. Support for RFC2617 HTTP Digest Authentication
  142. @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
  143. opaque should be valid.
  144. @type privateKey: L{bytes}
  145. @ivar privateKey: A random string used for generating the secure opaque.
  146. @type algorithm: L{bytes}
  147. @param algorithm: Case insensitive string specifying the hash algorithm to
  148. use. Must be either C{'md5'} or C{'sha'}. C{'md5-sess'} is B{not}
  149. supported.
  150. @type authenticationRealm: L{bytes}
  151. @param authenticationRealm: case sensitive string that specifies the realm
  152. portion of the challenge
  153. """
  154. _parseparts = re.compile(
  155. b'([^= ]+)' # The key
  156. b'=' # Conventional key/value separator (literal)
  157. b'(?:' # Group together a couple options
  158. b'"([^"]*)"' # A quoted string of length 0 or more
  159. b'|' # The other option in the group is coming
  160. b'([^,]+)' # An unquoted string of length 1 or more, up to a comma
  161. b')' # That non-matching group ends
  162. b',?') # There might be a comma at the end (none on last pair)
  163. CHALLENGE_LIFETIME_SECS = 15 * 60 # 15 minutes
  164. scheme = b"digest"
  165. def __init__(self, algorithm, authenticationRealm):
  166. self.algorithm = algorithm
  167. self.authenticationRealm = authenticationRealm
  168. self.privateKey = secureRandom(12)
  169. def getChallenge(self, address):
  170. """
  171. Generate the challenge for use in the WWW-Authenticate header.
  172. @param address: The client address to which this challenge is being
  173. sent.
  174. @return: The L{dict} that can be used to generate a WWW-Authenticate
  175. header.
  176. """
  177. c = self._generateNonce()
  178. o = self._generateOpaque(c, address)
  179. return {'nonce': c,
  180. 'opaque': o,
  181. 'qop': b'auth',
  182. 'algorithm': self.algorithm,
  183. 'realm': self.authenticationRealm}
  184. def _generateNonce(self):
  185. """
  186. Create a random value suitable for use as the nonce parameter of a
  187. WWW-Authenticate challenge.
  188. @rtype: L{bytes}
  189. """
  190. return hexlify(secureRandom(12))
  191. def _getTime(self):
  192. """
  193. Parameterize the time based seed used in C{_generateOpaque}
  194. so we can deterministically unittest it's behavior.
  195. """
  196. return time.time()
  197. def _generateOpaque(self, nonce, clientip):
  198. """
  199. Generate an opaque to be returned to the client. This is a unique
  200. string that can be returned to us and verified.
  201. """
  202. # Now, what we do is encode the nonce, client ip and a timestamp in the
  203. # opaque value with a suitable digest.
  204. now = intToBytes(int(self._getTime()))
  205. if not clientip:
  206. clientip = b''
  207. elif isinstance(clientip, unicode):
  208. clientip = clientip.encode('ascii')
  209. key = b",".join((nonce, clientip, now))
  210. digest = hexlify(md5(key + self.privateKey).digest())
  211. ekey = base64.b64encode(key)
  212. return b"-".join((digest, ekey.replace(b'\n', b'')))
  213. def _verifyOpaque(self, opaque, nonce, clientip):
  214. """
  215. Given the opaque and nonce from the request, as well as the client IP
  216. that made the request, verify that the opaque was generated by us.
  217. And that it's not too old.
  218. @param opaque: The opaque value from the Digest response
  219. @param nonce: The nonce value from the Digest response
  220. @param clientip: The remote IP address of the client making the request
  221. or L{None} if the request was submitted over a channel where this
  222. does not make sense.
  223. @return: C{True} if the opaque was successfully verified.
  224. @raise error.LoginFailed: if C{opaque} could not be parsed or
  225. contained the wrong values.
  226. """
  227. # First split the digest from the key
  228. opaqueParts = opaque.split(b'-')
  229. if len(opaqueParts) != 2:
  230. raise error.LoginFailed('Invalid response, invalid opaque value')
  231. if not clientip:
  232. clientip = b''
  233. elif isinstance(clientip, unicode):
  234. clientip = clientip.encode('ascii')
  235. # Verify the key
  236. key = base64.b64decode(opaqueParts[1])
  237. keyParts = key.split(b',')
  238. if len(keyParts) != 3:
  239. raise error.LoginFailed('Invalid response, invalid opaque value')
  240. if keyParts[0] != nonce:
  241. raise error.LoginFailed(
  242. 'Invalid response, incompatible opaque/nonce values')
  243. if keyParts[1] != clientip:
  244. raise error.LoginFailed(
  245. 'Invalid response, incompatible opaque/client values')
  246. try:
  247. when = int(keyParts[2])
  248. except ValueError:
  249. raise error.LoginFailed(
  250. 'Invalid response, invalid opaque/time values')
  251. if (int(self._getTime()) - when >
  252. DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
  253. raise error.LoginFailed(
  254. 'Invalid response, incompatible opaque/nonce too old')
  255. # Verify the digest
  256. digest = hexlify(md5(key + self.privateKey).digest())
  257. if digest != opaqueParts[0]:
  258. raise error.LoginFailed('Invalid response, invalid opaque value')
  259. return True
  260. def decode(self, response, method, host):
  261. """
  262. Decode the given response and attempt to generate a
  263. L{DigestedCredentials} from it.
  264. @type response: L{bytes}
  265. @param response: A string of comma separated key=value pairs
  266. @type method: L{bytes}
  267. @param method: The action requested to which this response is addressed
  268. (GET, POST, INVITE, OPTIONS, etc).
  269. @type host: L{bytes}
  270. @param host: The address the request was sent from.
  271. @raise error.LoginFailed: If the response does not contain a username,
  272. a nonce, an opaque, or if the opaque is invalid.
  273. @return: L{DigestedCredentials}
  274. """
  275. response = b' '.join(response.splitlines())
  276. parts = self._parseparts.findall(response)
  277. auth = {}
  278. for (key, bare, quoted) in parts:
  279. value = (quoted or bare).strip()
  280. auth[nativeString(key.strip())] = value
  281. username = auth.get('username')
  282. if not username:
  283. raise error.LoginFailed('Invalid response, no username given.')
  284. if 'opaque' not in auth:
  285. raise error.LoginFailed('Invalid response, no opaque given.')
  286. if 'nonce' not in auth:
  287. raise error.LoginFailed('Invalid response, no nonce given.')
  288. # Now verify the nonce/opaque values for this client
  289. if self._verifyOpaque(auth.get('opaque'), auth.get('nonce'), host):
  290. return DigestedCredentials(username,
  291. method,
  292. self.authenticationRealm,
  293. auth)
  294. @implementer(IUsernameHashedPassword)
  295. class CramMD5Credentials(object):
  296. """
  297. An encapsulation of some CramMD5 hashed credentials.
  298. @ivar challenge: The challenge to be sent to the client.
  299. @type challenge: L{bytes}
  300. @ivar response: The hashed response from the client.
  301. @type response: L{bytes}
  302. @ivar username: The username from the response from the client.
  303. @type username: L{bytes} or L{None} if not yet provided.
  304. """
  305. username = None
  306. challenge = b''
  307. response = b''
  308. def __init__(self, host=None):
  309. self.host = host
  310. def getChallenge(self):
  311. if self.challenge:
  312. return self.challenge
  313. # The data encoded in the first ready response contains an
  314. # presumptively arbitrary string of random digits, a timestamp, and
  315. # the fully-qualified primary host name of the server. The syntax of
  316. # the unencoded form must correspond to that of an RFC 822 'msg-id'
  317. # [RFC822] as described in [POP3].
  318. # -- RFC 2195
  319. r = random.randrange(0x7fffffff)
  320. t = time.time()
  321. self.challenge = networkString('<%d.%d@%s>' % (
  322. r, t, nativeString(self.host) if self.host else None))
  323. return self.challenge
  324. def setResponse(self, response):
  325. self.username, self.response = response.split(None, 1)
  326. def moreChallenges(self):
  327. return False
  328. def checkPassword(self, password):
  329. verify = hexlify(hmac.HMAC(password, self.challenge).digest())
  330. return verify == self.response
  331. @implementer(IUsernameHashedPassword)
  332. class UsernameHashedPassword:
  333. def __init__(self, username, hashed):
  334. self.username = username
  335. self.hashed = hashed
  336. def checkPassword(self, password):
  337. return self.hashed == password
  338. @implementer(IUsernamePassword)
  339. class UsernamePassword:
  340. def __init__(self, username, password):
  341. self.username = username
  342. self.password = password
  343. def checkPassword(self, password):
  344. return self.password == password
  345. @implementer(IAnonymous)
  346. class Anonymous:
  347. pass
  348. class ISSHPrivateKey(ICredentials):
  349. """
  350. L{ISSHPrivateKey} credentials encapsulate an SSH public key to be checked
  351. against a user's private key.
  352. @ivar username: The username associated with these credentials.
  353. @type username: L{bytes}
  354. @ivar algName: The algorithm name for the blob.
  355. @type algName: L{bytes}
  356. @ivar blob: The public key blob as sent by the client.
  357. @type blob: L{bytes}
  358. @ivar sigData: The data the signature was made from.
  359. @type sigData: L{bytes}
  360. @ivar signature: The signed data. This is checked to verify that the user
  361. owns the private key.
  362. @type signature: L{bytes} or L{None}
  363. """
  364. @implementer(ISSHPrivateKey)
  365. class SSHPrivateKey:
  366. def __init__(self, username, algName, blob, sigData, signature):
  367. self.username = username
  368. self.algName = algName
  369. self.blob = blob
  370. self.sigData = sigData
  371. self.signature = signature