359 lines
14 KiB
Python
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"])
|