blob: aa6ff1217b2f5018c58f33d5dcdfc52afd89f88b [file]
"""RFC 2617 Digest challenge/response scenarios against mod_auth_digest's
default configuration (AuthDigestProvider file, AuthDigestQop auth (the only
supported value), AuthDigestNonceLifetime 300, no AuthDigestDomain).
"""
from . import digest_client as dc
from .env import AAATestEnv
class TestDigestChallengeResponse:
def url(self, env, path="secret.txt", location="default"):
return env.mkurl("http", "aaa", f"/digest/{location}/{path}")
def challenge(self, env, location="default"):
r = env.curl_get(self.url(env, location=location))
assert r.response["status"] == 401
return dc.DigestChallenge.parse(r.response["header"]["www-authenticate"])
def test_digest_001_no_credentials(self, env):
# No Authorization header at all -> 401 with a well-formed challenge.
r = env.curl_get(self.url(env))
assert r.response["status"] == 401
auth = r.response["header"]["www-authenticate"]
challenge = dc.DigestChallenge.parse(auth)
assert challenge.realm == AAATestEnv.REALM
assert challenge.algorithm == "MD5"
assert challenge.qop == "auth"
assert challenge.stale is False
# no AuthDigestDomain configured for this Location -> no domain=
assert challenge.domain is None
# nonce-count checking is off and lifetime isn't 0 here, so the
# server has no reason to track this client -> no opaque=
assert challenge.opaque is None
def test_digest_002_success(self, env):
challenge = self.challenge(env)
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
method="GET", uri="/digest/default/secret.txt")
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 200
assert r.response["body"].decode() == "digest-default-secret\n"
def test_digest_003_rspauth(self, env):
# Authentication-Info's rspauth= must match what we independently
# compute from the same HA1 -- proves the server round-trips the
# session parameters (nonce/nc/cnonce/qop) correctly.
challenge = self.challenge(env)
nc = "00000001"
cnonce = "test-cnonce-rspauth"
uri = "/digest/default/secret.txt"
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
method="GET", uri=uri, nc=nc, cnonce=cnonce)
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 200
ai = dc.parse_params(r.response["header"]["authentication-info"])
h1 = dc.ha1(AAATestEnv.DIGEST_USER, challenge.realm, AAATestEnv.DIGEST_PASSWORD)
expected = dc.rspauth_digest(h1, challenge.nonce, nc, cnonce, "auth", uri)
assert ai["rspauth"] == expected
assert ai["qop"] == "auth"
assert ai["nc"] == nc
assert ai["cnonce"] == cnonce
def test_digest_004_wrong_password(self, env):
challenge = self.challenge(env)
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, "not-the-password",
method="GET", uri="/digest/default/secret.txt")
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 401
env.httpd_error_log.ignore_recent(lognos=["AH01794"])
def test_digest_005_unknown_user(self, env):
challenge = self.challenge(env)
auth = dc.build_authorization(
"no-such-user", challenge, "whatever",
method="GET", uri="/digest/default/secret.txt")
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 401
env.httpd_error_log.ignore_recent(lognos=["AH01790"])
def test_digest_006_second_user(self, env):
# a distinct user in the same password file also works
challenge = self.challenge(env)
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER2, challenge, AAATestEnv.DIGEST_PASSWORD2,
method="GET", uri="/digest/default/secret.txt")
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 200
def test_digest_007_wrong_realm(self, env):
challenge = self.challenge(env)
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
method="GET", uri="/digest/default/secret.txt",
realm="Some Other Realm")
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 401
env.httpd_error_log.ignore_recent(lognos=["AH01788"])
def test_digest_008_bad_algorithm_token(self, env):
# a client claiming an algorithm other than MD5 is rejected outright,
# even though the response hash below is computed correctly for MD5.
challenge = self.challenge(env)
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
method="GET", uri="/digest/default/secret.txt",
algorithm="MD5-sess")
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 401
env.httpd_error_log.ignore_recent(lognos=["AH01789"])
def test_digest_009_legacy_no_qop_rejected(self, env):
# RFC 2069-style digest (no qop/cnonce/nc) is syntactically valid but
# explicitly no longer supported by this module.
challenge = self.challenge(env)
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
method="GET", uri="/digest/default/secret.txt",
qop=None, include_qop_fields=False)
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 401
env.httpd_error_log.ignore_recent(lognos=["AH10560"])
def test_digest_010_malformed_header_missing_field(self, env):
# missing "uri" entirely -> header is syntactically INVALID, so the
# server issues a fresh (non-stale) challenge rather than evaluating
# the (nonexistent) response hash.
challenge = self.challenge(env)
h1 = dc.ha1(AAATestEnv.DIGEST_USER, challenge.realm, AAATestEnv.DIGEST_PASSWORD)
auth = ('Digest username="digestuser", '
f'realm="{challenge.realm}", nonce="{challenge.nonce}", '
f'response="{h1}", qop=auth, nc=00000001, cnonce="x"')
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 401
new_challenge = dc.DigestChallenge.parse(r.response["header"]["www-authenticate"])
assert new_challenge.stale is False
env.httpd_error_log.ignore_recent(lognos=["AH01782"])
def test_digest_011_wrong_scheme(self, env):
r = env.curl_get(self.url(env), options=[
"-H", "Authorization: Basic ZGlnZXN0dXNlcjpkaWdlc3RwYXNz"])
assert r.response["status"] == 401
env.httpd_error_log.ignore_recent(lognos=["AH01781"])
def test_digest_012_uri_mismatch(self, env):
# The Authorization uri= must match the actual request-target; a
# self-consistent response computed for a *different* uri than the
# one actually requested is rejected as a bad request, before the
# hash is even checked.
challenge = self.challenge(env)
other_uri = "/digest/default/other-secret.txt"
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
method="GET", uri=other_uri)
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 400
env.httpd_error_log.ignore_recent(lognos=["AH01786"])
def test_digest_013_invalid_opaque(self, env):
challenge = self.challenge(env)
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
method="GET", uri="/digest/default/secret.txt",
opaque="not-a-hex-number")
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 401
env.httpd_error_log.ignore_recent(lognos=["AH01787"])
def test_digest_014_tampered_response_hash(self, env):
challenge = self.challenge(env)
auth = dc.build_authorization(
AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
method="GET", uri="/digest/default/secret.txt",
response="0" * 32)
r = env.curl_get(self.url(env), options=["-H", f"Authorization: {auth}"])
assert r.response["status"] == 401
env.httpd_error_log.ignore_recent(lognos=["AH01794"])