FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,141 @@
|
||||
"""Real TLS and certificate-bootstrap checks in isolated application profiles."""
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import ssl
|
||||
import stat
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from framebridge_expanded import broker, node, tls
|
||||
from framebridge_expanded.config import default_config, load_config, save_config
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class CertificateTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix="tls-test-", dir=ROOT / "qa")
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
|
||||
def profile(self, name, role="central"):
|
||||
state = self.root / name
|
||||
save_config(state, default_config(role, name))
|
||||
return state
|
||||
|
||||
def test_certificate_sans_fingerprint_private_modes_and_config(self):
|
||||
state = self.profile("cert-central")
|
||||
result = tls.create_certificates(state, "central.example.test", "192.0.2.10")
|
||||
cfg = load_config(state)
|
||||
x509, hashes, serialization, *_ = tls._crypto()
|
||||
ca = x509.load_pem_x509_certificate(Path(result["ca_public_file"]).read_bytes())
|
||||
cert = x509.load_pem_x509_certificate(Path(result["server_certificate_file"]).read_bytes())
|
||||
self.assertEqual(result["ca_fingerprint_sha256"], ca.fingerprint(hashes.SHA256()).hex())
|
||||
self.assertEqual(result["server_fingerprint_sha256"], cert.fingerprint(hashes.SHA256()).hex())
|
||||
san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
|
||||
self.assertEqual(set(san.get_values_for_type(x509.DNSName)), {"central.example.test", "localhost"})
|
||||
self.assertEqual(set(san.get_values_for_type(x509.IPAddress)), {
|
||||
ipaddress.ip_address("192.0.2.10"), ipaddress.ip_address("127.0.0.1"), ipaddress.ip_address("::1")})
|
||||
self.assertTrue(ca.extensions.get_extension_for_class(x509.BasicConstraints).value.ca)
|
||||
self.assertFalse(cert.extensions.get_extension_for_class(x509.BasicConstraints).value.ca)
|
||||
self.assertEqual(stat.S_IMODE((state / "tls").stat().st_mode), 0o700)
|
||||
for path in (state / "tls").iterdir():
|
||||
expected = 0o644 if path.name == "ca-public.pem" else 0o600
|
||||
self.assertEqual(stat.S_IMODE(path.stat().st_mode), expected, path.name)
|
||||
self.assertEqual(cfg["tls_ca"], result["ca_public_file"])
|
||||
self.assertEqual(cfg["broker_url"], "https://192.0.2.10:20878")
|
||||
self.assertEqual(cfg["broker_control_url"], "https://127.0.0.1:20878")
|
||||
self.assertEqual(cfg["broker_listen_host"], "127.0.0.1")
|
||||
self.assertFalse(result["private_key_values_exported"])
|
||||
self.assertFalse(result["openai_credentials_created_or_saved"])
|
||||
self.assertFalse(any("private.key" in key for key in result))
|
||||
|
||||
def test_existing_material_is_never_overwritten(self):
|
||||
state = self.profile("no-overwrite")
|
||||
tls.create_certificates(state, "central.test", "127.0.0.1")
|
||||
before = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in (state / "tls").iterdir()}
|
||||
with self.assertRaises(FileExistsError):
|
||||
tls.create_certificates(state, "replacement.test", "127.0.0.1")
|
||||
after = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in (state / "tls").iterdir()}
|
||||
self.assertEqual(before, after)
|
||||
|
||||
def test_invalid_or_active_profile_never_creates_material(self):
|
||||
standalone = self.profile("standalone", "standalone")
|
||||
with self.assertRaises(ValueError):
|
||||
tls.create_certificates(standalone, "central.test", "127.0.0.1")
|
||||
state = self.profile("active")
|
||||
with mock.patch("framebridge_expanded.runtime.status", return_value={"running": True}):
|
||||
with self.assertRaises(RuntimeError):
|
||||
tls.create_certificates(state, "central.test", "127.0.0.1")
|
||||
with self.assertRaises(ValueError):
|
||||
tls.create_certificates(state, "bad host", "127.0.0.1")
|
||||
with self.assertRaises(ValueError):
|
||||
tls.create_certificates(state, "central.test", "0.0.0.0")
|
||||
self.assertFalse((state / "tls").exists())
|
||||
|
||||
def test_real_https_node_enrollment_and_wrong_ca_rejection(self):
|
||||
# Override only with a local, assigned address to exercise an actual LAN route.
|
||||
address = os.environ.get("FRAMEBRIDGE_TLS_TEST_ADDRESS", "127.0.0.1")
|
||||
ipaddress.ip_address(address)
|
||||
with socket.socket() as probe:
|
||||
probe.bind(("0.0.0.0", 0))
|
||||
port = probe.getsockname()[1]
|
||||
central = self.profile("https-central")
|
||||
cfg = load_config(central)
|
||||
cfg["ports"]["broker"] = port
|
||||
if port in (cfg["ports"]["mcp"], cfg["ports"]["hub"]):
|
||||
self.skipTest("Ephemeral broker port overlaps a default service port.")
|
||||
save_config(central, cfg)
|
||||
public = tls.create_certificates(central, "central.test", address, listen_host="0.0.0.0")
|
||||
server = broker.make_server("0.0.0.0", port, central)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
joining = self.profile("https-node", "node")
|
||||
nc = load_config(joining)
|
||||
nc.update(broker_url=public["broker_url"], tls_ca=public["ca_public_file"])
|
||||
save_config(joining, nc)
|
||||
enrolled = node.enroll(public["broker_url"], "https-node", joining)
|
||||
broker.configure(state_dir=central)
|
||||
pending = broker.list_pending()
|
||||
entry = next(p for p in pending if p["node_id"] == enrolled["node_id"])
|
||||
self.assertEqual(entry["fingerprint"], enrolled["fingerprint"])
|
||||
approved = broker.approve(entry["request_id"], enrolled["node_id"], central)
|
||||
polled = node.poll_enrollment(joining)
|
||||
self.assertEqual(polled["state"], "paired")
|
||||
self.assertFalse(polled.get("broker_confirm_required", False))
|
||||
self.assertTrue(any(p["node_id"] == enrolled["node_id"] for p in broker.list_confirmed_nodes()))
|
||||
|
||||
wrong = self.profile("wrong-authority")
|
||||
wrong_public = tls.create_certificates(wrong, "other.test", address)
|
||||
rejected = self.profile("wrong-ca-node", "node")
|
||||
rc = load_config(rejected)
|
||||
rc.update(broker_url=public["broker_url"], tls_ca=wrong_public["ca_public_file"])
|
||||
save_config(rejected, rc)
|
||||
before = broker.list_pending()
|
||||
with self.assertRaises(Exception) as failure:
|
||||
node.enroll(public["broker_url"], "wrong-ca-node", rejected)
|
||||
# Check the verification error, rather than accepting any transport failure.
|
||||
chain = []
|
||||
error = failure.exception
|
||||
for _ in range(6):
|
||||
if error is None:
|
||||
break
|
||||
chain.append(str(error))
|
||||
error = error.__cause__ or error.__context__
|
||||
self.assertIn("CERTIFICATE_VERIFY_FAILED", " ".join(chain))
|
||||
self.assertEqual(before, broker.list_pending())
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
self.assertFalse(thread.is_alive())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user