Linux lhjmq-records 5.15.0-118-generic #128-Ubuntu SMP Fri Jul 5 09:28:59 UTC 2024 x86_64
Your IP : 3.147.78.249
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.cred._digest} and the associated bits in
L{twisted.cred.credentials}.
"""
import base64
from binascii import hexlify
from hashlib import md5, sha1
from zope.interface.verify import verifyObject
from twisted.cred.credentials import (
DigestCredentialFactory,
IUsernameDigestHash,
calcHA1,
calcHA2,
calcResponse,
)
from twisted.cred.error import LoginFailed
from twisted.internet.address import IPv4Address
from twisted.python.compat import networkString
from twisted.trial.unittest import TestCase
def b64encode(s):
return base64.b64encode(s).strip()
class FakeDigestCredentialFactory(DigestCredentialFactory):
"""
A Fake Digest Credential Factory that generates a predictable
nonce and opaque
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.privateKey = b"0"
def _generateNonce(self):
"""
Generate a static nonce
"""
return b"178288758716122392881254770685"
def _getTime(self):
"""
Return a stable time
"""
return 0
class DigestAuthTests(TestCase):
"""
L{TestCase} mixin class which defines a number of tests for
L{DigestCredentialFactory}. Because this mixin defines C{setUp}, it
must be inherited before L{TestCase}.
"""
def setUp(self):
"""
Create a DigestCredentialFactory for testing
"""
self.username = b"foobar"
self.password = b"bazquux"
self.realm = b"test realm"
self.algorithm = b"md5"
self.cnonce = b"29fc54aa1641c6fa0e151419361c8f23"
self.qop = b"auth"
self.uri = b"/write/"
self.clientAddress = IPv4Address("TCP", "10.2.3.4", 43125)
self.method = b"GET"
self.credentialFactory = DigestCredentialFactory(self.algorithm, self.realm)
def test_MD5HashA1(self, _algorithm=b"md5", _hash=md5):
"""
L{calcHA1} accepts the C{'md5'} algorithm and returns an MD5 hash of
its parameters, excluding the nonce and cnonce.
"""
nonce = b"abc123xyz"
hashA1 = calcHA1(
_algorithm, self.username, self.realm, self.password, nonce, self.cnonce
)
a1 = b":".join((self.username, self.realm, self.password))
expected = hexlify(_hash(a1).digest())
self.assertEqual(hashA1, expected)
def test_MD5SessionHashA1(self):
"""
L{calcHA1} accepts the C{'md5-sess'} algorithm and returns an MD5 hash
of its parameters, including the nonce and cnonce.
"""
nonce = b"xyz321abc"
hashA1 = calcHA1(
b"md5-sess", self.username, self.realm, self.password, nonce, self.cnonce
)
a1 = self.username + b":" + self.realm + b":" + self.password
ha1 = hexlify(md5(a1).digest())
a1 = ha1 + b":" + nonce + b":" + self.cnonce
expected = hexlify(md5(a1).digest())
self.assertEqual(hashA1, expected)
def test_SHAHashA1(self):
"""
L{calcHA1} accepts the C{'sha'} algorithm and returns a SHA hash of its
parameters, excluding the nonce and cnonce.
"""
self.test_MD5HashA1(b"sha", sha1)
def test_MD5HashA2Auth(self, _algorithm=b"md5", _hash=md5):
"""
L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of
its arguments, excluding the entity hash for QOP other than
C{'auth-int'}.
"""
method = b"GET"
hashA2 = calcHA2(_algorithm, method, self.uri, b"auth", None)
a2 = method + b":" + self.uri
expected = hexlify(_hash(a2).digest())
self.assertEqual(hashA2, expected)
def test_MD5HashA2AuthInt(self, _algorithm=b"md5", _hash=md5):
"""
L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of
its arguments, including the entity hash for QOP of C{'auth-int'}.
"""
method = b"GET"
hentity = b"foobarbaz"
hashA2 = calcHA2(_algorithm, method, self.uri, b"auth-int", hentity)
a2 = method + b":" + self.uri + b":" + hentity
expected = hexlify(_hash(a2).digest())
self.assertEqual(hashA2, expected)
def test_MD5SessHashA2Auth(self):
"""
L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth'} and
returns the same value as it does for the C{'md5'} algorithm.
"""
self.test_MD5HashA2Auth(b"md5-sess")
def test_MD5SessHashA2AuthInt(self):
"""
L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth-int'}
and returns the same value as it does for the C{'md5'} algorithm.
"""
self.test_MD5HashA2AuthInt(b"md5-sess")
def test_SHAHashA2Auth(self):
"""
L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of
its arguments, excluding the entity hash for QOP other than
C{'auth-int'}.
"""
self.test_MD5HashA2Auth(b"sha", sha1)
def test_SHAHashA2AuthInt(self):
"""
L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of
its arguments, including the entity hash for QOP of C{'auth-int'}.
"""
self.test_MD5HashA2AuthInt(b"sha", sha1)
def test_MD5HashResponse(self, _algorithm=b"md5", _hash=md5):
"""
L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash
of its parameters, excluding the nonce count, client nonce, and QoP
value if the nonce count and client nonce are L{None}
"""
hashA1 = b"abc123"
hashA2 = b"789xyz"
nonce = b"lmnopq"
response = hashA1 + b":" + nonce + b":" + hashA2
expected = hexlify(_hash(response).digest())
digest = calcResponse(hashA1, hashA2, _algorithm, nonce, None, None, None)
self.assertEqual(expected, digest)
def test_MD5SessionHashResponse(self):
"""
L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5
hash of its parameters, excluding the nonce count, client nonce, and
QoP value if the nonce count and client nonce are L{None}
"""
self.test_MD5HashResponse(b"md5-sess")
def test_SHAHashResponse(self):
"""
L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash
of its parameters, excluding the nonce count, client nonce, and QoP
value if the nonce count and client nonce are L{None}
"""
self.test_MD5HashResponse(b"sha", sha1)
def test_MD5HashResponseExtra(self, _algorithm=b"md5", _hash=md5):
"""
L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash
of its parameters, including the nonce count, client nonce, and QoP
value if they are specified.
"""
hashA1 = b"abc123"
hashA2 = b"789xyz"
nonce = b"lmnopq"
nonceCount = b"00000004"
clientNonce = b"abcxyz123"
qop = b"auth"
response = (
hashA1
+ b":"
+ nonce
+ b":"
+ nonceCount
+ b":"
+ clientNonce
+ b":"
+ qop
+ b":"
+ hashA2
)
expected = hexlify(_hash(response).digest())
digest = calcResponse(
hashA1, hashA2, _algorithm, nonce, nonceCount, clientNonce, qop
)
self.assertEqual(expected, digest)
def test_MD5SessionHashResponseExtra(self):
"""
L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5
hash of its parameters, including the nonce count, client nonce, and
QoP value if they are specified.
"""
self.test_MD5HashResponseExtra(b"md5-sess")
def test_SHAHashResponseExtra(self):
"""
L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash
of its parameters, including the nonce count, client nonce, and QoP
value if they are specified.
"""
self.test_MD5HashResponseExtra(b"sha", sha1)
def formatResponse(self, quotes=True, **kw):
"""
Format all given keyword arguments and their values suitably for use as
the value of an HTTP header.
@types quotes: C{bool}
@param quotes: A flag indicating whether to quote the values of each
field in the response.
@param **kw: Keywords and C{bytes} values which will be treated as field
name/value pairs to include in the result.
@rtype: C{bytes}
@return: The given fields formatted for use as an HTTP header value.
"""
if "username" not in kw:
kw["username"] = self.username
if "realm" not in kw:
kw["realm"] = self.realm
if "algorithm" not in kw:
kw["algorithm"] = self.algorithm
if "qop" not in kw:
kw["qop"] = self.qop
if "cnonce" not in kw:
kw["cnonce"] = self.cnonce
if "uri" not in kw:
kw["uri"] = self.uri
if quotes:
quote = b'"'
else:
quote = b""
return b", ".join(
[
b"".join((networkString(k), b"=", quote, v, quote))
for (k, v) in kw.items()
if v is not None
]
)
def getDigestResponse(self, challenge, ncount):
"""
Calculate the response for the given challenge
"""
nonce = challenge.get("nonce")
algo = challenge.get("algorithm").lower()
qop = challenge.get("qop")
ha1 = calcHA1(
algo, self.username, self.realm, self.password, nonce, self.cnonce
)
ha2 = calcHA2(algo, b"GET", self.uri, qop, None)
expected = calcResponse(ha1, ha2, algo, nonce, ncount, self.cnonce, qop)
return expected
def test_response(self, quotes=True):
"""
L{DigestCredentialFactory.decode} accepts a digest challenge response
and parses it into an L{IUsernameHashedPassword} provider.
"""
challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
nc = b"00000001"
clientResponse = self.formatResponse(
quotes=quotes,
nonce=challenge["nonce"],
response=self.getDigestResponse(challenge, nc),
nc=nc,
opaque=challenge["opaque"],
)
creds = self.credentialFactory.decode(
clientResponse, self.method, self.clientAddress.host
)
self.assertTrue(creds.checkPassword(self.password))
self.assertFalse(creds.checkPassword(self.password + b"wrong"))
def test_responseWithoutQuotes(self):
"""
L{DigestCredentialFactory.decode} accepts a digest challenge response
which does not quote the values of its fields and parses it into an
L{IUsernameHashedPassword} provider in the same way it would a
response which included quoted field values.
"""
self.test_response(False)
def test_responseWithCommaURI(self):
"""
L{DigestCredentialFactory.decode} accepts a digest challenge response
which quotes the values of its fields and includes a C{b","} in the URI
field.
"""
self.uri = b"/some,path/"
self.test_response(True)
def test_caseInsensitiveAlgorithm(self):
"""
The case of the algorithm value in the response is ignored when
checking the credentials.
"""
self.algorithm = b"MD5"
self.test_response()
def test_md5DefaultAlgorithm(self):
"""
The algorithm defaults to MD5 if it is not supplied in the response.
"""
self.algorithm = None
self.test_response()
def test_responseWithoutClientIP(self):
"""
L{DigestCredentialFactory.decode} accepts a digest challenge response
even if the client address it is passed is L{None}.
"""
challenge = self.credentialFactory.getChallenge(None)
nc = b"00000001"
clientResponse = self.formatResponse(
nonce=challenge["nonce"],
response=self.getDigestResponse(challenge, nc),
nc=nc,
opaque=challenge["opaque"],
)
creds = self.credentialFactory.decode(clientResponse, self.method, None)
self.assertTrue(creds.checkPassword(self.password))
self.assertFalse(creds.checkPassword(self.password + b"wrong"))
def test_multiResponse(self):
"""
L{DigestCredentialFactory.decode} handles multiple responses to a
single challenge.
"""
challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
nc = b"00000001"
clientResponse = self.formatResponse(
nonce=challenge["nonce"],
response=self.getDigestResponse(challenge, nc),
nc=nc,
opaque=challenge["opaque"],
)
creds = self.credentialFactory.decode(
clientResponse, self.method, self.clientAddress.host
)
self.assertTrue(creds.checkPassword(self.password))
self.assertFalse(creds.checkPassword(self.password + b"wrong"))
nc = b"00000002"
clientResponse = self.formatResponse(
nonce=challenge["nonce"],
response=self.getDigestResponse(challenge, nc),
nc=nc,
opaque=challenge["opaque"],
)
creds = self.credentialFactory.decode(
clientResponse, self.method, self.clientAddress.host
)
self.assertTrue(creds.checkPassword(self.password))
self.assertFalse(creds.checkPassword(self.password + b"wrong"))
def test_failsWithDifferentMethod(self):
"""
L{DigestCredentialFactory.decode} returns an L{IUsernameHashedPassword}
provider which rejects a correct password for the given user if the
challenge response request is made using a different HTTP method than
was used to request the initial challenge.
"""
challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
nc = b"00000001"
clientResponse = self.formatResponse(
nonce=challenge["nonce"],
response=self.getDigestResponse(challenge, nc),
nc=nc,
opaque=challenge["opaque"],
)
creds = self.credentialFactory.decode(
clientResponse, b"POST", self.clientAddress.host
)
self.assertFalse(creds.checkPassword(self.password))
self.assertFalse(creds.checkPassword(self.password + b"wrong"))
def test_noUsername(self):
"""
L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
has no username field or if the username field is empty.
"""
# Check for no username
e = self.assertRaises(
LoginFailed,
self.credentialFactory.decode,
self.formatResponse(username=None),
self.method,
self.clientAddress.host,
)
self.assertEqual(str(e), "Invalid response, no username given.")
# Check for an empty username
e = self.assertRaises(
LoginFailed,
self.credentialFactory.decode,
self.formatResponse(username=b""),
self.method,
self.clientAddress.host,
)
self.assertEqual(str(e), "Invalid response, no username given.")
def test_noNonce(self):
"""
L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
has no nonce.
"""
e = self.assertRaises(
LoginFailed,
self.credentialFactory.decode,
self.formatResponse(opaque=b"abc123"),
self.method,
self.clientAddress.host,
)
self.assertEqual(str(e), "Invalid response, no nonce given.")
def test_noOpaque(self):
"""
L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
has no opaque.
"""
e = self.assertRaises(
LoginFailed,
self.credentialFactory.decode,
self.formatResponse(),
self.method,
self.clientAddress.host,
)
self.assertEqual(str(e), "Invalid response, no opaque given.")
def test_checkHash(self):
"""
L{DigestCredentialFactory.decode} returns an L{IUsernameDigestHash}
provider which can verify a hash of the form 'username:realm:password'.
"""
challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
nc = b"00000001"
clientResponse = self.formatResponse(
nonce=challenge["nonce"],
response=self.getDigestResponse(challenge, nc),
nc=nc,
opaque=challenge["opaque"],
)
creds = self.credentialFactory.decode(
clientResponse, self.method, self.clientAddress.host
)
self.assertTrue(verifyObject(IUsernameDigestHash, creds))
cleartext = self.username + b":" + self.realm + b":" + self.password
hash = md5(cleartext)
self.assertTrue(creds.checkHash(hexlify(hash.digest())))
hash.update(b"wrong")
self.assertFalse(creds.checkHash(hexlify(hash.digest())))
def test_invalidOpaque(self):
"""
L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque
value does not contain all the required parts.
"""
credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
challenge = credentialFactory.getChallenge(self.clientAddress.host)
exc = self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
b"badOpaque",
challenge["nonce"],
self.clientAddress.host,
)
self.assertEqual(str(exc), "Invalid response, invalid opaque value")
badOpaque = b"foo-" + b64encode(b"nonce,clientip")
exc = self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
badOpaque,
challenge["nonce"],
self.clientAddress.host,
)
self.assertEqual(str(exc), "Invalid response, invalid opaque value")
exc = self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
b"",
challenge["nonce"],
self.clientAddress.host,
)
self.assertEqual(str(exc), "Invalid response, invalid opaque value")
badOpaque = b"foo-" + b64encode(
b",".join(
(challenge["nonce"], networkString(self.clientAddress.host), b"foobar")
)
)
exc = self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
badOpaque,
challenge["nonce"],
self.clientAddress.host,
)
self.assertEqual(str(exc), "Invalid response, invalid opaque/time values")
def test_incompatibleNonce(self):
"""
L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given
nonce from the response does not match the nonce encoded in the opaque.
"""
credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
challenge = credentialFactory.getChallenge(self.clientAddress.host)
badNonceOpaque = credentialFactory._generateOpaque(
b"1234567890", self.clientAddress.host
)
exc = self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
badNonceOpaque,
challenge["nonce"],
self.clientAddress.host,
)
self.assertEqual(str(exc), "Invalid response, incompatible opaque/nonce values")
exc = self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
badNonceOpaque,
b"",
self.clientAddress.host,
)
self.assertEqual(str(exc), "Invalid response, incompatible opaque/nonce values")
def test_incompatibleClientIP(self):
"""
L{DigestCredentialFactory.decode} raises L{LoginFailed} when the
request comes from a client IP other than what is encoded in the
opaque.
"""
credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
challenge = credentialFactory.getChallenge(self.clientAddress.host)
badAddress = "10.0.0.1"
# Sanity check
self.assertNotEqual(self.clientAddress.host, badAddress)
badNonceOpaque = credentialFactory._generateOpaque(
challenge["nonce"], badAddress
)
self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
badNonceOpaque,
challenge["nonce"],
self.clientAddress.host,
)
def test_oldNonce(self):
"""
L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given
opaque is older than C{DigestCredentialFactory.CHALLENGE_LIFETIME_SECS}
"""
credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
challenge = credentialFactory.getChallenge(self.clientAddress.host)
key = b",".join(
(challenge["nonce"], networkString(self.clientAddress.host), b"-137876876")
)
digest = hexlify(md5(key + credentialFactory.privateKey).digest())
ekey = b64encode(key)
oldNonceOpaque = b"-".join((digest, ekey.strip(b"\n")))
self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
oldNonceOpaque,
challenge["nonce"],
self.clientAddress.host,
)
def test_mismatchedOpaqueChecksum(self):
"""
L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque
checksum fails verification.
"""
credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
challenge = credentialFactory.getChallenge(self.clientAddress.host)
key = b",".join(
(challenge["nonce"], networkString(self.clientAddress.host), b"0")
)
digest = hexlify(md5(key + b"this is not the right pkey").digest())
badChecksum = b"-".join((digest, b64encode(key)))
self.assertRaises(
LoginFailed,
credentialFactory._verifyOpaque,
badChecksum,
challenge["nonce"],
self.clientAddress.host,
)
def test_incompatibleCalcHA1Options(self):
"""
L{calcHA1} raises L{TypeError} when any of the pszUsername, pszRealm,
or pszPassword arguments are specified with the preHA1 keyword
argument.
"""
arguments = (
(b"user", b"realm", b"password", b"preHA1"),
(None, b"realm", None, b"preHA1"),
(None, None, b"password", b"preHA1"),
)
for pszUsername, pszRealm, pszPassword, preHA1 in arguments:
self.assertRaises(
TypeError,
calcHA1,
b"md5",
pszUsername,
pszRealm,
pszPassword,
b"nonce",
b"cnonce",
preHA1=preHA1,
)
def test_noNewlineOpaque(self):
"""
L{DigestCredentialFactory._generateOpaque} returns a value without
newlines, regardless of the length of the nonce.
"""
opaque = self.credentialFactory._generateOpaque(b"long nonce " * 10, None)
self.assertNotIn(b"\n", opaque)
|