Track 1.1 of master plan: expand from sexp-only tests to cover DTOs, HTTP signatures, HMAC auth, URL utilities, Jinja filters, calendar helpers, config freeze, activity bus registry, parse utilities, sexp helpers, error classes, and jinja bridge render API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
141 lines
5.4 KiB
Python
141 lines
5.4 KiB
Python
"""Tests for RSA key generation and HTTP Signature signing/verification."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from shared.utils.http_signatures import (
|
|
generate_rsa_keypair,
|
|
sign_request,
|
|
verify_request_signature,
|
|
create_ld_signature,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Key generation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestKeyGeneration:
|
|
def test_generates_pem_strings(self):
|
|
private_pem, public_pem = generate_rsa_keypair()
|
|
assert isinstance(private_pem, str)
|
|
assert isinstance(public_pem, str)
|
|
|
|
def test_private_key_format(self):
|
|
private_pem, _ = generate_rsa_keypair()
|
|
assert "BEGIN PRIVATE KEY" in private_pem
|
|
assert "END PRIVATE KEY" in private_pem
|
|
|
|
def test_public_key_format(self):
|
|
_, public_pem = generate_rsa_keypair()
|
|
assert "BEGIN PUBLIC KEY" in public_pem
|
|
assert "END PUBLIC KEY" in public_pem
|
|
|
|
def test_keys_are_unique(self):
|
|
priv1, pub1 = generate_rsa_keypair()
|
|
priv2, pub2 = generate_rsa_keypair()
|
|
assert priv1 != priv2
|
|
assert pub1 != pub2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sign + verify round-trip
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSignVerify:
|
|
def test_round_trip_no_body(self):
|
|
private_pem, public_pem = generate_rsa_keypair()
|
|
headers = sign_request(
|
|
private_pem, key_id="https://example.com/users/alice#main-key",
|
|
method="GET", path="/users/bob/inbox", host="example.com",
|
|
date="Sat, 15 Jun 2025 12:00:00 GMT",
|
|
)
|
|
assert "Signature" in headers
|
|
assert "Date" in headers
|
|
assert "Host" in headers
|
|
assert "Digest" not in headers
|
|
|
|
ok = verify_request_signature(
|
|
public_pem, headers["Signature"], method="GET",
|
|
path="/users/bob/inbox", headers=headers,
|
|
)
|
|
assert ok is True
|
|
|
|
def test_round_trip_with_body(self):
|
|
private_pem, public_pem = generate_rsa_keypair()
|
|
body = b'{"type": "Follow"}'
|
|
headers = sign_request(
|
|
private_pem, key_id="https://example.com/users/alice#main-key",
|
|
method="POST", path="/users/bob/inbox", host="example.com",
|
|
body=body, date="Sat, 15 Jun 2025 12:00:00 GMT",
|
|
)
|
|
assert "Digest" in headers
|
|
assert headers["Digest"].startswith("SHA-256=")
|
|
|
|
ok = verify_request_signature(
|
|
public_pem, headers["Signature"], method="POST",
|
|
path="/users/bob/inbox", headers=headers,
|
|
)
|
|
assert ok is True
|
|
|
|
def test_wrong_key_fails(self):
|
|
priv1, _ = generate_rsa_keypair()
|
|
_, pub2 = generate_rsa_keypair()
|
|
headers = sign_request(
|
|
priv1, key_id="key1", method="GET", path="/inbox", host="a.com",
|
|
date="Sat, 15 Jun 2025 12:00:00 GMT",
|
|
)
|
|
ok = verify_request_signature(pub2, headers["Signature"], "GET", "/inbox", headers)
|
|
assert ok is False
|
|
|
|
def test_tampered_path_fails(self):
|
|
private_pem, public_pem = generate_rsa_keypair()
|
|
headers = sign_request(
|
|
private_pem, key_id="key1", method="GET", path="/inbox", host="a.com",
|
|
date="Sat, 15 Jun 2025 12:00:00 GMT",
|
|
)
|
|
ok = verify_request_signature(public_pem, headers["Signature"], "GET", "/tampered", headers)
|
|
assert ok is False
|
|
|
|
def test_tampered_method_fails(self):
|
|
private_pem, public_pem = generate_rsa_keypair()
|
|
headers = sign_request(
|
|
private_pem, key_id="key1", method="GET", path="/inbox", host="a.com",
|
|
date="Sat, 15 Jun 2025 12:00:00 GMT",
|
|
)
|
|
ok = verify_request_signature(public_pem, headers["Signature"], "POST", "/inbox", headers)
|
|
assert ok is False
|
|
|
|
def test_signature_header_contains_key_id(self):
|
|
private_pem, _ = generate_rsa_keypair()
|
|
headers = sign_request(
|
|
private_pem, key_id="https://my.server/actor#main-key",
|
|
method="POST", path="/inbox", host="remote.server",
|
|
date="Sat, 15 Jun 2025 12:00:00 GMT",
|
|
)
|
|
assert 'keyId="https://my.server/actor#main-key"' in headers["Signature"]
|
|
assert 'algorithm="rsa-sha256"' in headers["Signature"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Linked Data signature
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestLDSignature:
|
|
def test_creates_ld_signature(self):
|
|
private_pem, _ = generate_rsa_keypair()
|
|
activity = {"type": "Create", "actor": "https://example.com/users/alice"}
|
|
sig = create_ld_signature(private_pem, "https://example.com/users/alice#main-key", activity)
|
|
assert sig["type"] == "RsaSignature2017"
|
|
assert sig["creator"] == "https://example.com/users/alice#main-key"
|
|
assert "signatureValue" in sig
|
|
assert "created" in sig
|
|
|
|
def test_deterministic_canonical(self):
|
|
"""Same activity always produces same canonical form (signature differs due to timestamp)."""
|
|
private_pem, _ = generate_rsa_keypair()
|
|
activity = {"b": 2, "a": 1}
|
|
# The canonical form should sort keys
|
|
canonical = json.dumps(activity, sort_keys=True, separators=(",", ":"))
|
|
assert canonical == '{"a":1,"b":2}'
|