Files
Unraid-AppOps-Gateway-Public/backend/tests/test_repopulse_target_attestation.py
T

359 lines
14 KiB
Python

"""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"])