"""Cryptographic and intent-binding checks for the fixed target attestation.""" from __future__ import annotations import hashlib import hmac import importlib import importlib.util import json import sqlite3 import sys from datetime import datetime, timedelta, timezone from pathlib import Path import pytest def _module(monkeypatch): root = Path(__file__).resolve().parents[2] / "ops" / "gitea-deploy-broker" monkeypatch.syspath_prepend(str(root)) return importlib.import_module("repopulse_target_attestation") @pytest.mark.parametrize("code_root", [Path("/opt/gitea-deploy"), Path("/private/controller-stage")]) def test_bootstrap_producer_and_verifier_share_the_config_path(monkeypatch, code_root): root = Path(__file__).resolve().parents[2] / "ops/gitea-deploy-broker" monkeypatch.syspath_prepend(str(root)) spec = importlib.util.spec_from_file_location("config_path_attestation_test", root / "repopulse_target_attestation.py") verifier = importlib.util.module_from_spec(spec) verifier.__file__ = str(code_root / "repopulse_target_attestation.py") exec(compile((root / "repopulse_target_attestation.py").read_bytes(), str(spec.origin), "exec"), verifier.__dict__) expected = ( Path("/controller-hub-releases/controller-config") if code_root == Path("/opt/gitea-deploy") else code_root ) / "appops-enrollment-bootstrap" assert verifier.BOOTSTRAP_PATH == expected monkeypatch.setitem(sys.modules, "repopulse_target_attestation", verifier) producer_spec = importlib.util.spec_from_file_location("config_path_producer_test", root / "stage_repopulse_target_attestation.py") producer = importlib.util.module_from_spec(producer_spec) producer_spec.loader.exec_module(producer) observed = [] def unavailable_secret(path, _maximum): observed.append(path) raise OSError("fixture stops before any external operation") monkeypatch.setattr(producer.os, "geteuid", lambda: 0, raising=False) monkeypatch.setattr(producer.sys, "argv", ["stage_repopulse_target_attestation.py"]) monkeypatch.setattr(producer, "_private_bytes", unavailable_secret) assert producer.main() == 2 assert observed == [expected] observed.clear() def verifier_file(path, *, maximum): if path == verifier.ATTESTATION_PATH: return json.dumps({"schema": verifier.ENVELOPE_SCHEMA, "claims": {}, "signatureSha256": "0" * 64}).encode() return unavailable_secret(path, maximum) monkeypatch.setattr(verifier, "_private_file", verifier_file) required = { "repository", "applicationId", "profileId", "profileVersion", "planSha256", "intentId", "targetSecretRef", "controllerTlsCertificateSha256", } assert verifier.FixedTargetAttestationVerifier().verify_target_attestation( expected={key: "fixture" for key in required} ) is None assert observed == [expected] def _sample(now: datetime) -> dict: return { "schema": "itworx.repopulse-target-attestation/v1", "attestation_id": "rpa-target-0123456789abcdef0123", "intent_id": "rpa-0123456789abcdef0123", "nonce": "1" * 32, "issued_at": now.isoformat(), "expires_at": (now + timedelta(minutes=2)).isoformat(), "controller_identity": "unraid-controller", "controller_trust_profile_id": "repopulse-authenticated", "controller_trust_profile_version": "enrollment-profile/v1", "controller_bootstrap_key_ref": "ENROLLMENT_BOOTSTRAP", "controller_tls_certificate_sha256": "a" * 64, "repository": "jens/repopulse", "application_id": "repopulse", "profile_id": "repopulse-authenticated", "profile_version": "enrollment-profile/v1", "plan_sha256": "b" * 64, "target_secret_ref": "jens__repopulse", "npm_hostname": "repopulse.example.com", "npm_access_list_route_hostname": "repopulse.example.com", "npm_upstream_host": "repopulse", "npm_upstream_network": "itworx-mcp-hub-npm", "npm_upstream_port": 8000, "npm_certificate_sha256": "c" * 64, "npm_certificate_sans": ["repopulse.example.com"], "npm_certificate_not_before": (now - timedelta(days=1)).isoformat(), "npm_certificate_not_after": (now + timedelta(days=1)).isoformat(), "npm_access_list_id": 1, "npm_access_list_satisfy_any": 0, "npm_access_list_is_deleted": 0, "npm_access_list_pass_auth": 1, "npm_access_list_client_count": 0, "npm_access_list_rule_count": 1, "npm_access_list_rules_sha256": "d" * 64, "npm_access_list_rules": [ { "action": "authenticate", "subject_type": "basic_user_sha256", "subject": "e" * 64, } ], "maintenance_method": "GET", "maintenance_path": "/api/v1/operator/maintenance", "maintenance_status_code": 200, "maintenance_auth_mechanism": "operator_token", "maintenance_authenticated_principal": "root_operator", "maintenance_response_sha256": "f" * 64, "maintenance_observed_at": now.isoformat(), } def test_signed_target_attestation_is_bound_to_one_intent_and_nonce( monkeypatch, tmp_path ): module = _module(monkeypatch) secret = b"fixed-test-bootstrap-secret-value-0123456789" claim_path = tmp_path / "target-attestation.json" bootstrap_path = tmp_path / "bootstrap" bootstrap_path.write_bytes(secret + b"\n") monkeypatch.setattr(module, "ATTESTATION_PATH", claim_path) monkeypatch.setattr(module, "BOOTSTRAP_PATH", bootstrap_path) monkeypatch.setattr( module, "_private_file", lambda path, maximum: path.read_bytes() ) consumed: dict[str, tuple[str, str]] = {} def consume(nonce, digest, intent): value = (digest, intent) if nonce in consumed and consumed[nonce] != value: raise ValueError("nonce_replayed") consumed[nonce] = value monkeypatch.setattr(module, "_record_nonce", consume) now = datetime.now(timezone.utc) claims = _sample(now) def stage(value): raw = module._canonical(value) claim_path.write_text( json.dumps( { "schema": module.ENVELOPE_SCHEMA, "claims": value, "signatureSha256": hmac.new( secret, module._SIGNING_DOMAIN + raw, hashlib.sha256 ).hexdigest(), } ), encoding="utf-8", ) expected = { "repository": "jens/repopulse", "applicationId": "repopulse", "profileId": "repopulse-authenticated", "profileVersion": "enrollment-profile/v1", "planSha256": "b" * 64, "intentId": claims["intent_id"], "targetSecretRef": "jens__repopulse", "controllerTlsCertificateSha256": "a" * 64, } verifier = module.FixedTargetAttestationVerifier() stage(claims) result = verifier.verify_target_attestation(expected=expected) assert result is not None and result.intent_id == claims["intent_id"] assert verifier.verify_target_attestation(expected=expected) == result tampered = dict(claims, plan_sha256="0" * 64) claim_path.write_text( json.dumps( { "schema": module.ENVELOPE_SCHEMA, "claims": tampered, "signatureSha256": hmac.new( secret, module._SIGNING_DOMAIN + module._canonical(claims), hashlib.sha256, ).hexdigest(), } ), encoding="utf-8", ) assert verifier.verify_target_attestation(expected=expected) is None expired = dict( claims, issued_at=(now - timedelta(minutes=9)).isoformat(), expires_at=(now - timedelta(minutes=8)).isoformat(), ) stage(expired) assert verifier.verify_target_attestation(expected=expected) is None changed_intent = dict(claims, intent_id="rpa-ffffffffffffffffffff") stage(changed_intent) assert verifier.verify_target_attestation(expected=expected) is None reused_nonce = dict(claims, attestation_id="rpa-target-elsewhere") stage(reused_nonce) assert verifier.verify_target_attestation(expected=expected) is None def test_npm_producer_requires_the_fixed_authenticated_route(monkeypatch, tmp_path): _module(monkeypatch) producer = importlib.import_module("stage_repopulse_target_attestation") database_path = tmp_path / "npm.sqlite" with sqlite3.connect(database_path) as database: database.executescript(""" CREATE TABLE proxy_host (id INTEGER, domain_names TEXT, forward_host TEXT, forward_port INTEGER, access_list_id INTEGER, certificate_id INTEGER, ssl_forced INTEGER, enabled INTEGER, forward_scheme TEXT, is_deleted INTEGER); CREATE TABLE certificate (id INTEGER, domain_names TEXT, is_deleted INTEGER); CREATE TABLE access_list (id INTEGER, is_deleted INTEGER, satisfy_any INTEGER, pass_auth INTEGER); CREATE TABLE access_list_auth (id INTEGER, access_list_id INTEGER, username TEXT); CREATE TABLE access_list_client (id INTEGER, access_list_id INTEGER, address TEXT, directive TEXT); INSERT INTO proxy_host VALUES (7, '["repopulse.example.com"]', 'repopulse', 8000, 1, 9, 1, 1, 'http', 0); INSERT INTO certificate VALUES (9, '["repopulse.example.com"]', 0); INSERT INTO access_list VALUES (1, 0, 0, 1); INSERT INTO access_list_auth VALUES (3, 1, 'private-user'); """) monkeypatch.setattr(producer, "NPM_DATABASE", database_path) rules, cert_id = producer._npm_host_and_rules(1) assert cert_id == 9 assert rules == [ { "action": "authenticate", "subjectType": "basic_user_sha256", "subject": hashlib.sha256(b"private-user").hexdigest(), } ] with sqlite3.connect(database_path) as database: database.execute("UPDATE access_list SET satisfy_any=1 WHERE id=1") try: producer._npm_host_and_rules(1) except ValueError: pass else: raise AssertionError("permissive access list must fail closed") def test_maintenance_proof_timestamp_is_captured_with_the_probe(monkeypatch): _module(monkeypatch) producer = importlib.import_module("stage_repopulse_target_attestation") monkeypatch.setattr( producer, "_private_bytes", lambda *_args, **_kwargs: ( b"REPOPULSE_GITEA_URL=https://gitea.example.com\n" b"REPOPULSE_GITEA_TOKEN=token-value\n" b"REPOPULSE_MAINTENANCE_TOKEN=maintenance-token-value-123456789\n" ), ) monkeypatch.setattr( producer, "_docker_python", lambda *_args, **_kwargs: {"status": 200, "responseSha256": "a" * 64}, ) before = datetime.now(timezone.utc) mechanism, digest, observed = producer._maintenance() after = datetime.now(timezone.utc) assert mechanism == "operator_token" assert digest == "a" * 64 assert before <= datetime.fromisoformat(observed) <= after def test_producer_main_builds_verifier_shape_with_proof_before_issue( monkeypatch, capsys ): verifier = _module(monkeypatch) producer = importlib.import_module("stage_repopulse_target_attestation") intent_id = "rpa-" + "a" * 20 now = datetime.now(timezone.utc) observed = now.isoformat() captured: dict = {} monkeypatch.setattr(producer.os, "geteuid", lambda: 0, raising=False) monkeypatch.setattr(producer.sys, "argv", ["stage_repopulse_target_attestation.py"]) monkeypatch.setattr( producer, "_private_bytes", lambda path, _maximum: ( b"x" * 64 if path == verifier.BOOTSTRAP_PATH else b'{"certificate_file":"/fixed/server.crt"}' ), ) monkeypatch.setattr( producer.store, "RepoPulseAdoptionStore", lambda: type( "Store", (), { "load_intent": lambda self: { "status": "in_progress", "intentId": intent_id, } }, )(), ) monkeypatch.setattr( producer, "_appops_trust", lambda _secret: {"repopulseProxyAccessListId": 1} ) monkeypatch.setattr( producer, "_npm_host_and_rules", lambda _acl: ( [ { "action": "authenticate", "subjectType": "basic_user_sha256", "subject": "c" * 64, } ], 9, ), ) monkeypatch.setattr(producer, "_npm_network", lambda: None) monkeypatch.setattr( producer, "_npm_tls", lambda: ( "d" * 64, ("repopulse.example.com",), (now - timedelta(days=1)).isoformat(), (now + timedelta(days=1)).isoformat(), ), ) monkeypatch.setattr( producer, "_maintenance", lambda: ("operator_token", "e" * 64, observed) ) monkeypatch.setattr(producer, "_certificate_bytes", lambda _path: b"pem") monkeypatch.setattr(producer.ssl, "PEM_cert_to_DER_cert", lambda _pem: b"der") monkeypatch.setattr(producer, "_plan", lambda _intent: "b" * 64) def capture(claims, _secret): captured.update(claims) return "f" * 64 monkeypatch.setattr(producer, "_stage", capture) assert producer.main() == 0 assert '"status":"staged"' in capsys.readouterr().out assert verifier._claims(captured).intent_id == intent_id assert datetime.fromisoformat( captured["maintenance_observed_at"] ) <= datetime.fromisoformat(captured["issued_at"])