| # -*- test-case-name: twisted.test.test_digestauth -*- |
| # Copyright (c) 2008 Twisted Matrix Laboratories. |
| # See LICENSE for details. |
| |
| """ |
| Calculations for HTTP Digest authentication. |
| |
| @see: U{http://www.faqs.org/rfcs/rfc2617.html} |
| """ |
| |
| from twisted.python.hashlib import md5, sha1 |
| |
| |
| |
| # The digest math |
| |
| algorithms = { |
| 'md5': md5, |
| |
| # md5-sess is more complicated than just another algorithm. It requires |
| # H(A1) state to be remembered from the first WWW-Authenticate challenge |
| # issued and re-used to process any Authorization header in response to |
| # that WWW-Authenticate challenge. It is *not* correct to simply |
| # recalculate H(A1) each time an Authorization header is received. Read |
| # RFC 2617, section 3.2.2.2 and do not try to make DigestCredentialFactory |
| # support this unless you completely understand it. -exarkun |
| 'md5-sess': md5, |
| |
| 'sha': sha1, |
| } |
| |
| # DigestCalcHA1 |
| def calcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce, pszCNonce, |
| preHA1=None): |
| """ |
| Compute H(A1) from RFC 2617. |
| |
| @param pszAlg: The name of the algorithm to use to calculate the digest. |
| Currently supported are md5, md5-sess, and sha. |
| @param pszUserName: The username |
| @param pszRealm: The realm |
| @param pszPassword: The password |
| @param pszNonce: The nonce |
| @param pszCNonce: The cnonce |
| |
| @param preHA1: If available this is a str containing a previously |
| calculated H(A1) as a hex string. If this is given then the values for |
| pszUserName, pszRealm, and pszPassword must be C{None} and are ignored. |
| """ |
| |
| if (preHA1 and (pszUserName or pszRealm or pszPassword)): |
| raise TypeError(("preHA1 is incompatible with the pszUserName, " |
| "pszRealm, and pszPassword arguments")) |
| |
| if preHA1 is None: |
| # We need to calculate the HA1 from the username:realm:password |
| m = algorithms[pszAlg]() |
| m.update(pszUserName) |
| m.update(":") |
| m.update(pszRealm) |
| m.update(":") |
| m.update(pszPassword) |
| HA1 = m.digest() |
| else: |
| # We were given a username:realm:password |
| HA1 = preHA1.decode('hex') |
| |
| if pszAlg == "md5-sess": |
| m = algorithms[pszAlg]() |
| m.update(HA1) |
| m.update(":") |
| m.update(pszNonce) |
| m.update(":") |
| m.update(pszCNonce) |
| HA1 = m.digest() |
| |
| return HA1.encode('hex') |
| |
| |
| def calcHA2(algo, pszMethod, pszDigestUri, pszQop, pszHEntity): |
| """ |
| Compute H(A2) from RFC 2617. |
| |
| @param pszAlg: The name of the algorithm to use to calculate the digest. |
| Currently supported are md5, md5-sess, and sha. |
| @param pszMethod: The request method. |
| @param pszDigestUri: The request URI. |
| @param pszQop: The Quality-of-Protection value. |
| @param pszHEntity: The hash of the entity body or C{None} if C{pszQop} is |
| not C{'auth-int'}. |
| @return: The hash of the A2 value for the calculation of the response |
| digest. |
| """ |
| m = algorithms[algo]() |
| m.update(pszMethod) |
| m.update(":") |
| m.update(pszDigestUri) |
| if pszQop == "auth-int": |
| m.update(":") |
| m.update(pszHEntity) |
| return m.digest().encode('hex') |
| |
| |
| def calcResponse(HA1, HA2, algo, pszNonce, pszNonceCount, pszCNonce, pszQop): |
| """ |
| Compute the digest for the given parameters. |
| |
| @param HA1: The H(A1) value, as computed by L{calcHA1}. |
| @param HA2: The H(A2) value, as computed by L{calcHA2}. |
| @param pszNonce: The challenge nonce. |
| @param pszNonceCount: The (client) nonce count value for this response. |
| @param pszCNonce: The client nonce. |
| @param pszQop: The Quality-of-Protection value. |
| """ |
| m = algorithms[algo]() |
| m.update(HA1) |
| m.update(":") |
| m.update(pszNonce) |
| m.update(":") |
| if pszNonceCount and pszCNonce: |
| m.update(pszNonceCount) |
| m.update(":") |
| m.update(pszCNonce) |
| m.update(":") |
| m.update(pszQop) |
| m.update(":") |
| m.update(HA2) |
| respHash = m.digest().encode('hex') |
| return respHash |