84 lines
3.5 KiB
Python
84 lines
3.5 KiB
Python
|
|
"""Peer authentication (CONTRACT.md section 7, MAS-WP-0011)."""
|
||
|
|
import hashlib
|
||
|
|
import hmac
|
||
|
|
import json
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from mas_modes import _load
|
||
|
|
from mas_modes.contract import message, post
|
||
|
|
from mas_modes.email_mode import EmailMode, RoleConfig
|
||
|
|
from mas_modes.harness import ROOT, Harness
|
||
|
|
|
||
|
|
STATUTE = ROOT / "mas-modes-v0.1-spec/conformance/statute.json"
|
||
|
|
|
||
|
|
|
||
|
|
def test_vectors_match_an_independent_hmac():
|
||
|
|
for v in _load("conformance/auth.json")["vectors"]:
|
||
|
|
mac = hmac.new(bytes.fromhex(v["key_hex"]), f"{v['timestamp']}.".encode() + v["body"].encode(),
|
||
|
|
hashlib.sha256).hexdigest()
|
||
|
|
assert v["signature"] == "v1=" + mac
|
||
|
|
|
||
|
|
|
||
|
|
def test_signed_events_required_when_configured():
|
||
|
|
h = Harness(STATUTE, {"ticket": {"role": "master"}}, sign_events=True)
|
||
|
|
try:
|
||
|
|
assert h.mail("ticket", "hello").status == 201 # the gateway signs
|
||
|
|
h.signer = None
|
||
|
|
d = h.mail("ticket", "unsigned")
|
||
|
|
assert d.status == 401 and d.response["error"] == "unauthenticated-peer"
|
||
|
|
h.signer = ("gateway", b"\0" * 32)
|
||
|
|
assert h.mail("ticket", "bad key").status == 401
|
||
|
|
finally:
|
||
|
|
h.close()
|
||
|
|
|
||
|
|
|
||
|
|
def test_unsigned_events_only_without_event_peer():
|
||
|
|
h = Harness(STATUTE, {"ticket": {"role": "master"}})
|
||
|
|
try:
|
||
|
|
assert h.mail("ticket", "loopback, unsigned").status == 201
|
||
|
|
h.signer = ("cms-1", h.keys["cms-1"]) # signed, but not by a gateway
|
||
|
|
assert h.mail("ticket", "x").status == 403
|
||
|
|
finally:
|
||
|
|
h.close()
|
||
|
|
|
||
|
|
|
||
|
|
def test_stubs_accept_only_email():
|
||
|
|
h = Harness(STATUTE, {"case": {"role": "slave"}})
|
||
|
|
try:
|
||
|
|
req = message("handoff.request", {"address": "a@b", "key": "a"}, handoff_id="h", mode="callback",
|
||
|
|
details={}, status_url="http://127.0.0.1:9/x")
|
||
|
|
url = h.email.roles["case"].master_url
|
||
|
|
assert post(url, message("case.intake", intake_ref="i", resource_type="case", event={}))[0] == 401
|
||
|
|
assert post(url, message("case.intake", intake_ref="i", resource_type="case", event={}),
|
||
|
|
("cms-1", h.keys["cms-1"]))[0] == 401 # the master's key is not email's identity
|
||
|
|
mode_url = h.callback_url
|
||
|
|
assert post(mode_url, req)[0] == 401
|
||
|
|
assert post(mode_url, req, ("email", b"\1" * 32))[0] == 401
|
||
|
|
assert post(mode_url, req, ("email", h.keys["callback-1"]))[0] == 202
|
||
|
|
finally:
|
||
|
|
h.close()
|
||
|
|
|
||
|
|
|
||
|
|
def test_email_refuses_config_without_keys():
|
||
|
|
with pytest.raises(ValueError, match="no key"):
|
||
|
|
EmailMode({"case": RoleConfig("slave", master_url="http://x", master_peer="cms-1")}, keys={})
|
||
|
|
with pytest.raises(ValueError, match="no key"):
|
||
|
|
EmailMode({"case": RoleConfig("slave", master_url="http://x", master_peer="cms-1",
|
||
|
|
transfers={"webhook": "http://y", "peer": "nobody"})},
|
||
|
|
keys={"cms-1": b"k" * 32})
|
||
|
|
|
||
|
|
|
||
|
|
def test_status_urls_are_unguessable_and_distinct():
|
||
|
|
h = Harness(STATUTE, {"ticket": {"role": "master", "fields": ("phone", "time", "scenario")}},
|
||
|
|
scenario="silent")
|
||
|
|
try:
|
||
|
|
h.mail("ticket!callback", "Phone: 1\nTime: 2")
|
||
|
|
h.mail("ticket!callback", "Phone: 1\nTime: 2")
|
||
|
|
urls = [m["status_url"] for m in h.callback.received]
|
||
|
|
tokens = [u.rsplit("/", 1)[1] for u in urls]
|
||
|
|
assert len(set(tokens)) == 2 and all(len(t) >= 22 for t in tokens) # >= 128 bits
|
||
|
|
assert json.dumps(h.callback.received).count(tokens[0]) == 1 # not echoed elsewhere
|
||
|
|
finally:
|
||
|
|
h.close()
|