Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,308 @@
|
||||
"""Cryptographic and intent-binding checks for the fixed target attestation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import importlib
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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"])
|
||||
Reference in new issue
Block a user