MAS-WP-0011 finished: peer authentication in mode contract v0.1

CONTRACT.md section 7 (founder-reviewed): pairwise HMAC-SHA256 over
TIMESTAMP.BODY in MAS-Modes-* headers, 300 s window; master commands only
from the configured master; statuses only at the handoff's tokenized URL
from its peer; primed webhooks must name a known peer. mas_modes.signing,
signed contract.post, authenticating json_server, EmailMode/stubs/gateway
signing. conformance/auth.json vectors + 7 authorization flows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
This commit is contained in:
tegwick 2026-10-19 11:25:24 +02:00
parent 2bc41a3318
commit bdf200a600
25 changed files with 1603 additions and 151 deletions

83
tests/test_modes_auth.py Normal file
View file

@ -0,0 +1,83 @@
"""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()

View file

@ -5,8 +5,7 @@ from mas_modes.contract import message, post
from mas_modes.harness import Harness, ROOT
STATUTE = ROOT / "mas-modes-v0.1-spec/conformance/statute.json"
ROLES = {"ticket": {"role": "master", "modes": {"callback": "{callback}"},
"fields": ("phone", "time", "scenario"), "timeout": 3600}}
ROLES = {"ticket": {"role": "master", "fields": ("phone", "time", "scenario"), "timeout": 3600}}
DETAILS = "Phone: +49 30 1234567\nTime: tomorrow 10:00"
@ -94,7 +93,8 @@ def test_mode_not_enabled_by_statute(h):
def test_mode_endpoint_down():
roles = {"ticket": {**ROLES["ticket"], "modes": {"callback": "http://127.0.0.1:9/handoffs"}}}
roles = {"ticket": {**ROLES["ticket"],
"modes": {"callback": {"url": "http://127.0.0.1:9/handoffs", "peer": "callback-1"}}}}
h = Harness(STATUTE, roles)
try:
d = h.mail("ticket!callback", DETAILS)
@ -120,27 +120,33 @@ def test_unauthenticated_sender_gets_no_mail(h):
assert h.outbox() == [] # no creation reply, no question: principle 10
def as_mode(h):
"""Post as the callback stub does: signed, at the handoff's status URL."""
url = h.callback.received[0]["status_url"]
return lambda msg: post(url, msg, ("callback-1", h.keys["callback-1"]))
def test_contract_guards(h):
tid, hid = start(h, DETAILS + "\nScenario: completed")
case = h.callback.run()[0]["case"]
url = h.email.base_url + "/mode-status"
send = as_mode(h)
stale = message("handoff.status", case, handoff_id=hid, seq=1, status="running")
assert post(url, stale) == (200, {"ok": True, "ignored": "stale"})
code, body = post(url, message("handoff.status", case, handoff_id=hid, seq=99, status="timed-out"))
assert send(stale) == (200, {"ok": True, "ignored": "stale"})
code, body = send(message("handoff.status", case, handoff_id=hid, seq=99, status="timed-out"))
assert code == 409 and body["error"] == "invalid-transition"
code, body = post(url, message("handoff.status", case, handoff_id=hid, seq=99, status="negotiating",
needs=["phone"]))
code, body = send(message("handoff.status", case, handoff_id=hid, seq=99, status="negotiating",
needs=["phone"]))
assert code == 409 # completed is terminal
code, body = post(url, message("handoff.status", case, handoff_id="nope", seq=1, status="running"))
code, body = send(message("handoff.status", case, handoff_id="nope", seq=1, status="running"))
assert code == 404
code, body = post(url, {"type": "handoff.status"})
code, body = send({"type": "handoff.status"})
assert code == 400 and body["error"] == "schema"
def test_repeated_message_id_is_processed_once(h):
tid, hid = start(h, DETAILS + "\nScenario: silent")
case = h.callback.received[0]["case"]
url = h.email.base_url + "/mode-status"
send = as_mode(h)
msg = message("handoff.status", case, handoff_id=hid, seq=1, status="scheduled", at="x")
assert post(url, msg) == post(url, msg) == (200, {"ok": True})
assert send(msg) == send(msg) == (200, {"ok": True})
assert h.thread(tid).count("callback: scheduled at=x") == 1

View file

@ -5,10 +5,11 @@ from mas_modes.harness import Harness, ROOT
STATUTE = ROOT / "mas-modes-v0.1-spec/conformance/statute.json"
D = "modes.example"
WEBHOOK = {"webhook": "{callback}", "peer": "callback-1"}
def harness(**cfg):
return Harness(STATUTE, {"case": {"role": "slave", "master_url": "{master}", **cfg}})
return Harness(STATUTE, {"case": {"role": "slave", **cfg}})
@pytest.fixture
@ -80,7 +81,7 @@ def test_principle_10_refuses_master_sends(h, kw, reason):
def test_send_cap_and_unknown_message():
h = Harness(STATUTE, {"case": {"role": "slave", "master_url": "{master}"}}, max_sent=0)
h = Harness(STATUTE, {"case": {"role": "slave"}}, max_sent=0)
try:
body = intake(h)
assert h.master.send(body["case"], body["message_ref"], "x")[1]["reason"] == "send-cap-reached"
@ -110,7 +111,7 @@ def test_transfer_blocked_by_default(h):
def test_transfer_to_primed_webhook():
h = harness(transfers={"webhook": "{callback}"})
h = harness(transfers=WEBHOOK)
try:
body = intake(h, local="case!callback", text="Phone: 123\nTime: now")
assert h.master.of_type("handoff.requested")[0]["forwarded"] is True
@ -138,7 +139,7 @@ def test_policy_from_assign_and_later_update(h):
def test_negotiation_only_when_autonomous():
for autonomous, mails in ((False, 0), (True, 2)): # acknowledgement + question
h = harness(transfers={"webhook": "{callback}"}, autonomous_send=autonomous)
h = harness(transfers=WEBHOOK, autonomous_send=autonomous)
try:
h.callback.default = "negotiate"
intake(h, local="case!callback", text="call me")
@ -150,7 +151,7 @@ def test_negotiation_only_when_autonomous():
def test_master_cancels_on_its_own_timeout():
h = harness(transfers={"webhook": "{callback}"}, fields=("phone", "time", "scenario"))
h = harness(transfers=WEBHOOK, fields=("phone", "time", "scenario"))
try:
body = intake(h, local="case!callback", text="Phone: 1\nTime: 2\nScenario: silent")
assert h.callback.run() == []