feat: enforce signed target maturity evidence
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 8s

This commit is contained in:
2026-08-02 03:41:09 +02:00
parent be8ba10ae3
commit 2c56a0fc11
12 changed files with 1245 additions and 15 deletions
+3 -1
View File
@@ -208,7 +208,9 @@ The first Campaign-centric capability and infrastructure fit assessment is in
`docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md`. Its rerun tooling can collect and
verify a bounded installed composition; target, provider and production claims
remain separate, expiring attestations signed by independently scoped proof
authorities.
authorities. The operational issuance, target-run, recovery-measurement, key
custody, and promotion-gate procedure is in
[Target Maturity Evidence Runbook](docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
# GovOPlaN Docker
+22
View File
@@ -503,6 +503,20 @@ Signatures cover UTF-8 JSON with the `signatures` member omitted, object keys
sorted, compact `,`/`:` separators and non-ASCII characters escaped, matching
the tool's deterministic canonicalization.
`tools/assessments/boundary-evidence.py` is the bounded issuance path. It
accepts a private target-run manifest conforming to
[`capability-fit-boundary-run.schema.json`](capability-fit-boundary-run.schema.json),
hashes each retained result file without following a final-component symlink,
and excludes all paths and raw results from the signed receipt. Issuance is
refused unless an independently trusted catalog, exact installed payload,
signed installer receipt, and role-scoped installer authority already pass.
Every claim must be covered by a supplied Ed25519 private key whose public key
is authorized for the full proof interval; catalog and installer key reuse is
rejected. The command immediately verifies its own result and atomically writes
both the proof and a sanitized review. The complete operator procedure and
recovery measurement definition are in
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
```bash
./.venv/bin/python tools/assessments/capability-fit.py \
--public \
@@ -513,6 +527,14 @@ the tool's deterministic canonicalization.
--expected-external-provider-subject provider-production
```
Promotion automation must opt into its required boundaries. Add
`--require-reference-readiness` to require all six target scopes,
`--require-external-provider-proof` when the product depends on a provider, and
`--require-production-approval` for production admission. These switches turn
missing, expired, revoked, mismatched, negative, or otherwise unchecked claims
into a blocking exit status rather than merely reporting them as an unproven
boundary.
With `--installed-evidence`, this command performs comparison and proof-binding
diagnostics. Without an installer receipt, the imported document remains
unsigned, so neither it nor the boundary claim becomes accepted proof. Direct
@@ -61,7 +61,7 @@ accepted as the current baseline and must not be reopened as greenfield work.
| Acting identity, function assignment, mandate, and ownership recovery | Implemented foundations span Identity, Organizations, IDM, Access, Mandates, generic ownership transfer/recovery, and audit provenance. Effective competence now resolves through a tenant-bound Mandate capability. |
| Governed data foundations | Connectors, Datasources, Dataflow, Reporting, and Search now exist. Datasources already provides live/cached/static modes, staging, immutable materializations, and publication contracts. |
| Task-focused projections and configured documentation | Views, view-surface declarations, configurable dashboards, and manifest-driven user/admin documentation exist. Rollout and content depth remain incremental. |
| Encryption as an optional capability | Core defines provider-neutral contracts; `govoplan-identity-trust` persists public device keys, key epochs and assurance evidence; and `govoplan-encryption` persists opaque vault/key lifecycle, versioned protection envelopes, quorum recovery authorization, outcome-unknown reconciliation, and disable preflight. No concrete cipher/provider or E2EE claim is bundled. |
| Encryption as an optional capability | Core defines provider-neutral contracts; `govoplan-identity-trust` persists public device keys, key epochs and assurance evidence; and `govoplan-encryption` persists opaque vault/key lifecycle, versioned protection envelopes, quorum recovery authorization, outcome-unknown reconciliation, and disable preflight. A bundled local AES-256-GCM server-envelope provider now stores wrapped key material and supports Files/Postbox protection, rotation, revocation, destruction, rewrap, tamper detection, and fail-closed restore behavior. It is explicitly neither E2EE nor a certified KMS/HSM. |
| Search without mandatory OpenSearch | PostgreSQL-backed, permission-aware search and module provider contracts exist; OpenSearch remains an optional adapter. |
| Scale-out and recovery architecture | Stateless API/worker, shared database/object storage, event delivery, deployment, and recovery contracts are documented and partly exercised. Production profiles and drills remain active work. |
@@ -83,10 +83,10 @@ were proven now have independent persistent owners:
| External providers | Core validates provider objects/field groups, operations, integration maturity, source authority, bounded reads, freshness/health, idempotency, conflicts, outcome-unknown handling, evidence, correction, reconciliation, outage, classification, purpose, retention, and secret handling. Addresses/CardDAV, Files remote storage, Mail SMTP/IMAP, Calendar CalDAV/ICS/Graph/EWS, and Connectors tabular/sanctions providers declare the contract and tenant-bounded secret-free runtime state. | Registry validation rejects any declared external provider without a sanitized state provider. Future adapters must cross the same gate before activation. |
| Institutional context | Core provides versioned temporal, actor/representation, institution/unit/function/task/mandate/jurisdiction/service/case/party/work-item/workflow/approval/decision/record, legal-basis, evidence, information-governance, external-source, presentation, and geographic references. Events, automation actions, audit records, and the transactional Audit outbox preserve the envelope. | Owning modules must progressively require the relevant subset for consequential operations. |
| Semantic provider contracts | Provider-neutral DTOs and protocols cover Mandate resolution, versioned Service definitions, procedure Parties/representation, and formal Decisions. `govoplan-mandates`, `govoplan-services`, `govoplan-parties`, and `govoplan-decisions` now persist immutable revisions behind those contracts with tenant isolation, bounded reads, replay safety, OCC, migrations, uninstall guards, permissions, APIs, capability documentation, and recovery documentation. | The owners are deliberately headless. Procedure-specific UI remains with consuming modules. |
| Formal-outcome proof | Committee persists bodies, meetings, agenda items, minutes, lifecycle events, and an optional protected local Decision projection. Voting separately owns immutable ballot definitions, frozen electorates, recorded casting/replacement, deterministic tally, certification, challenge, annulment, and provider-backed assurance profiles. Committee consumes `voting.ballots` and retains only deliberation linkage and verified aggregate outcome. | Native recorded ballots are reconstructable, not secret. A concrete confidential/secret/certified provider and deployment assurance still require protocol/custody/legal decisions and target evidence. |
| Formal-outcome proof | Committee persists bodies, meetings, agenda items, minutes, lifecycle events, and an optional protected local Decision projection. Voting separately owns immutable ballot definitions, frozen electorates, recorded casting/replacement, deterministic tally, certification, challenge, annulment, and provider-backed assurance profiles. Committee consumes `voting.ballots` and retains only deliberation linkage and verified aggregate outcome. A bundled `local_confidential` reference provider encrypts server-readable casts outside native ballot rows and exposes only receipts plus aggregate evidence to Committee. | Native recorded ballots remain reconstructable, and the reference confidential provider is neither secret nor certified: its server can decrypt casts while tallying. Secret/electronic-ballot protocol selection, custody, legal acceptance, independent review, and target evidence remain explicit product decisions. |
| Service-to-case proof | Services owns the persistent exact definitions consumed by Portal discovery and Cases intake. Forms owns immutable multi-page/conditional/localized schemas, accessibility assessment and package fragments. Forms Runtime owns definition-aware drafts, validation, submission receipts, status/evidence history, and durable native Case/Workflow handoffs with intent-before-effect and outcome-unknown reconciliation. Portal delegates URL, Case, Form, or Workflow launch to an installed owner while retaining exact Service/Form provenance. | Anonymous intake and concrete attachment/signature providers remain product depth. Portal and Runtime fail closed and explain any absent launcher, target capability, or provider prerequisite. |
| Generic approvals and process execution | Approvals persists exact-subject chains, delegation, separation of duties, quorum, signatures-as-evidence, escalation, OCC, and replay-safe decisions. Campaign proves an exact-version delivery gate. Workflow Engine owns immutable definitions/instances plus API, schedule, event and parent triggers, durable timer/event waits, scale-out claims, current-authority rechecks, and idempotent starts independently of the optional editor. | Policy-authored Approval template selection, concrete signature providers, cron adapters, and broader BPMN execution profiles are product/provider depth on explicit contracts. |
| Device trust and content protection | Identity Trust separates public device keys, epochs, assurance and key-access decisions from login and Access. Encryption separates resource ownership from opaque provider key custody, versioned envelopes, migration evidence, quorum recovery authorization and uninstall proof. Files/Postbox fixtures prove the optional boundary. | Concrete reviewed KMS/HSM/client providers, feature adapters, backup/restore/key-loss drills, and E2EE interoperability/certification remain required before a deployment protection claim. |
| Device trust and content protection | Identity Trust separates public device keys, epochs, assurance and key-access decisions from login and Access. Encryption separates resource ownership from opaque provider key custody, versioned envelopes, migration evidence, quorum recovery authorization and uninstall proof. Its local server-envelope provider and Files/Postbox adapters prove ciphertext persistence, integrity, rotation/rewrap and fail-closed key loss without leaking plaintext keys across the capability boundary. | Reviewed KMS/HSM/client providers, more owner adapters, target backup/restore/key-loss drills, and E2EE interoperability/certification remain required before stronger deployment claims. |
| Procedure-party proof | Parties persists effective procedure roles, frozen contact snapshots, and representation powers. Existing powers cannot disappear or be silently rewritten; explicit OCC-guarded revocation is required. Cases resolves the provider capability and excludes expired/revoked authority from downstream delivery. | Procedure modules still decide which contextual fields and actions to present. |
| Integrated institutional journey | The executable `product.service-to-decision` fixture uses real SQL-backed Services, Cases, Parties, Mandates, Committee, and Decisions providers. It carries one exact Service version through persisted Case intake, representation and frozen delivery authority, effective Mandate resolution, a body/meeting/agendum/vote/minute sequence, a persisted formal Decision, confirmed Postbox effect, Audit/record evidence, remedy/review, and protected reconstruction. A second executable path proves Portal to exact Form revision, persisted submission, and idempotent replay. | This is architecture and composition evidence. Signed, release-bound target accessibility, privacy, security, operator, delivery-provider, and recovery-drill evidence is still required before the package may claim `reference_ready`. |
| Governed data catalogue | Datasources stores typed governance metadata, exposes bounded tenant-scoped filters and update APIs/UI, carries governance through staging, and snapshots it into immutable materializations. Reporting now persists immutable dataset, semantic-model, report, quality-plan, saved-view, and schedule revisions; executes typed semantic queries with quality gates, access checks, replay, pivoting, export/import assessment, and provenance; and exposes the governed analytical WebUI. | Rich dependency/impact traversal, additional expression functions, and policy-specific field visibility can grow on the established contracts without moving connector, transformation, or source ownership. |
@@ -464,7 +464,10 @@ truthfully completed by adding generic platform code:
must produce signed, expiring accessibility, privacy, security, operator,
provider, backup/restore, rollback, and recovery-drill evidence. The verifier
and schemas are implemented; the actual claims require those real runs.
The pinned-release evidence run is tracked in
A bounded issuer now hashes retained reports, checks role-scoped signing
authority and exact installed-release origin, emits sanitized signed
receipts, verifies them immediately, and exposes admission-enforcing CLI
gates. The real pinned-release evidence run is tracked in
[GovOPlaN #37](https://git.add-ideas.de/GovOPlaN/govoplan/issues/37).
Forms and Forms Runtime no longer constitute an architecture gap. Conditional
@@ -533,8 +536,9 @@ Its implementation work packages and resulting owners are:
- `govoplan-voting`: governed recorded ballots plus fail-closed provider-backed
assurance profiles consumed by Committee; and
- Identity Trust #1 and Encryption #1-#3: public device trust, provider-neutral
key/protection lifecycle, recovery authorization and disable proof, while
concrete provider conformance remains separately gated.
key/protection lifecycle, recovery authorization and disable proof, plus a
bounded local server-envelope provider and Files/Postbox fixtures; external
KMS/HSM/client-provider conformance remains separately gated.
Existing Projects #1, Reporting #4, Portal #1, Cases #1, Datasources #1,
Risk Compliance #2, GovOPlaN #14, and GovOPlaN #19 carry product-depth and
+153
View File
@@ -0,0 +1,153 @@
# Target Maturity Evidence Runbook
This runbook turns retained target-environment results into a sanitized,
signed GovOPlaN capability-fit proof. It does not make a deployment suitable,
certified, supported, or production-approved by itself. The proof records what
independent authorities assessed against one exact installed release.
## Roles and custody
Use separate trust domains for release signing, installation receipts,
boundary assessment, and production approval. A private proof key must be
provisioned outside the assessed application and its matching public key must
already exist in a separately managed
`capability-fit-proof-authority-keyring.schema.json` document. Do not store
private keys, raw reports, credentials, personal data, backup material, or
target endpoints in Git.
Each authority key lists only the scopes that role may attest. At least one
supplied signing key must cover every claim, and the issuer rejects a key that:
- is absent, inactive, expired, or revoked in the authority keyring;
- expires before the proof;
- does not match its independently provisioned public key;
- reuses release-catalog or installer-authority key material.
## Target run
Install one pinned catalog release and issue its installed-composition receipt
with `tools/assessments/installer-receipt.py`. Exercise the actual target
topology, including:
- PostgreSQL and Redis as shared state services;
- shared S3-compatible object storage;
- at least two stateless API replicas and the intended worker topology;
- fenced singleton work, ingress, certificates, proxy headers, and the real
network/trust boundary;
- provider health and freshness for every provider required by the product;
- monitoring, alerting, failure response, accessibility, privacy, and security
controls;
- backup, isolated restore, failed-deployment rollback, and forward recovery.
For the recovery claim, retain the observed recovery point, measured RPO and
RTO, database/object-store consistency result, and semantic reconstruction of
the institutional and Service/Form reference journeys. Measure RPO from the
last acknowledged durable effect that survives recovery and RTO until service
health plus semantic reconstruction pass. Failed runs are evidence too and
must use a negative result.
The private reports stay in the approved evidence store. Give each report an
opaque artifact ID and each evaluated control a versioned opaque control ID.
## Private claim manifest
Create a private manifest conforming to
`capability-fit-boundary-run.schema.json`. Relative artifact paths resolve from
the manifest directory. Paths are read and hashed by the issuer and are never
copied into the signed output.
```json
{
"$schema": "./capability-fit-boundary-run.schema.json",
"schema_version": "0.1.0",
"evidence_kind": "govoplan.capability-fit-boundary-run",
"proof_id": "target:production:20260802",
"expires_at": "2026-09-01T00:00:00Z",
"claims": [
{
"scope": "target_environment",
"result": "passed",
"control_ids": ["topology:shared-state-v1"],
"artifacts": [
{"artifact_id": "target:run-20260802", "path": "private/target.json"}
]
},
{
"scope": "recovery",
"result": "passed",
"control_ids": ["recovery:restore-rollback-v1"],
"artifacts": [
{"artifact_id": "recovery:run-20260802", "path": "private/recovery.json"}
]
}
]
}
```
Reference readiness needs positive `target_environment`, `accessibility`,
`privacy`, `security`, `operations`, and `recovery` claims. Provider acceptance
and production approval are separate scopes. A production-approval authority
must not approve its own unreviewed target run.
## Issue and verify
Issue only while the signed installer observation is current. Repeat
`--signing-key` when multiple independent roles are needed. The command first
verifies the catalog, independent catalog trust root, exact installed payload,
installer receipt, and installer authority. It then hashes artifacts, signs the
sanitized proof, verifies it immediately, and writes both proof and review with
atomic private-file permissions.
```bash
./.venv/bin/python tools/assessments/boundary-evidence.py \
--assessment /srv/govoplan/assessment.json \
--catalog /srv/govoplan/catalogs/stable.json \
--keyring /srv/govoplan/catalogs/keyring.json \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
--installed-evidence /srv/govoplan/evidence/installed.json \
--installer-receipt /srv/govoplan/evidence/installer-receipt.json \
--installer-authority-keyring /srv/govoplan/trust/installer-authorities.json \
--claims /srv/govoplan/evidence/private/target-run.json \
--authority-keyring /srv/govoplan/trust/proof-authorities.json \
--signing-key authority-target=/run/secrets/target-proof-ed25519.pem \
--output /srv/govoplan/evidence/target-proof.json \
--review-output /srv/govoplan/evidence/target-proof-review.json
```
Add `--expected-external-provider-subject provider-production` when the claim
manifest contains `external_providers`. This value is an opaque deployment ID,
not a URL or credential.
## Promotion gate
The general verifier can now be made admission-enforcing. These switches return
a blocking exit status when a required claim is absent, expired, negative,
revoked, or bound to another assessment, release, installation, or subject:
```bash
./.venv/bin/python tools/assessments/capability-fit.py \
--catalog /srv/govoplan/catalogs/stable.json \
--keyring /srv/govoplan/catalogs/keyring.json \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
--installed-evidence /srv/govoplan/evidence/installed.json \
--installer-receipt /srv/govoplan/evidence/installer-receipt.json \
--installer-authority-keyring /srv/govoplan/trust/installer-authorities.json \
--boundary-evidence /srv/govoplan/evidence/target-proof.json \
--boundary-authority-keyring /srv/govoplan/trust/proof-authorities.json \
--require-reference-readiness \
--require-production-approval \
--output /srv/govoplan/evidence/admission-review.json
```
Use `--require-external-provider-proof` as well when the promoted product
requires an external provider. Live admission must not use
`--verification-time`; that switch is only for clearly labelled historical
review.
## Renewal and failure
Renew evidence after release, installed composition, deployment, control, or
provider changes and before expiry. Revoke an authority key immediately after
custody loss and rerun the affected assessment with a new independent key.
Never copy a previous positive claim to a new release. Preserve negative and
superseded receipts according to the approved evidence-retention policy.
@@ -0,0 +1,103 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit-boundary-run.schema.json",
"title": "GovOPlaN private target-run claim manifest",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"evidence_kind",
"proof_id",
"expires_at",
"claims"
],
"properties": {
"$schema": {
"type": "string",
"format": "uri-reference"
},
"schema_version": {
"const": "0.1.0"
},
"evidence_kind": {
"const": "govoplan.capability-fit-boundary-run"
},
"proof_id": {
"$ref": "#/$defs/opaque_id"
},
"expires_at": {
"type": "string",
"format": "date-time"
},
"claims": {
"type": "array",
"minItems": 1,
"maxItems": 8,
"items": {
"$ref": "#/$defs/claim"
}
}
},
"$defs": {
"opaque_id": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
},
"claim": {
"type": "object",
"additionalProperties": false,
"required": ["scope", "result", "control_ids", "artifacts"],
"properties": {
"scope": {
"enum": [
"target_environment",
"external_providers",
"accessibility",
"privacy",
"security",
"operations",
"recovery",
"production_approval"
]
},
"result": {
"enum": ["passed", "failed", "approved", "rejected"]
},
"control_ids": {
"type": "array",
"minItems": 1,
"maxItems": 256,
"uniqueItems": true,
"items": {
"$ref": "#/$defs/opaque_id"
}
},
"artifacts": {
"type": "array",
"minItems": 1,
"maxItems": 256,
"items": {
"$ref": "#/$defs/artifact"
}
}
}
},
"artifact": {
"type": "object",
"additionalProperties": false,
"required": ["artifact_id", "path"],
"properties": {
"artifact_id": {
"$ref": "#/$defs/opaque_id"
},
"path": {
"type": "string",
"minLength": 1,
"maxLength": 4096
}
}
}
}
}
+198
View File
@@ -0,0 +1,198 @@
from __future__ import annotations
import base64
from datetime import UTC, datetime
import json
from pathlib import Path
import sys
import tempfile
import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
META_ROOT = Path(__file__).resolve().parents[1]
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
if str(tools_root) not in sys.path:
sys.path.insert(0, str(tools_root))
from govoplan_assessment.boundary_evidence import ( # noqa: E402
issue_boundary_evidence,
)
from govoplan_assessment.evidence import ( # noqa: E402
InstalledEvidenceReview,
canonical_sha256,
review_boundary_evidence,
validate_payload,
)
class BoundaryEvidenceIssuerTests(unittest.TestCase):
def setUp(self) -> None:
self.assessment = {
"assessment_id": "assessment:test",
"release": {"ref": "stable-catalog-202608020001"},
"deployment_profile": {"id": "deployment:target"},
}
self.installed = {
"schema_version": "0.1.0",
"assessment_id": "assessment:test",
"artifacts": [],
}
self.private_key = Ed25519PrivateKey.generate()
public_key = base64.b64encode(
self.private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
).decode("ascii")
self.authority = {
"$schema": "./capability-fit-proof-authority-keyring.schema.json",
"schema_version": "0.1.0",
"purpose": "govoplan.capability-fit-proof-authorities",
"keys": [
{
"key_id": "authority:target",
"status": "active",
"public_key": public_key,
"allowed_scopes": ["target_environment", "recovery"],
"not_before": "2026-08-01T00:00:00Z",
"not_after": "2026-09-01T00:00:00Z",
}
],
}
self.boundary_schema = json.loads(
(
META_ROOT / "docs" / "capability-fit-boundary-evidence.schema.json"
).read_text(encoding="utf-8")
)
self.authority_schema = json.loads(
(
META_ROOT
/ "docs"
/ "capability-fit-proof-authority-keyring.schema.json"
).read_text(encoding="utf-8")
)
def test_issues_sanitized_hash_bound_proof_and_verifies_it(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
result_path = Path(temp_dir) / "recovery-result.json"
result_path.write_text(
'{"rto_seconds":42,"rpo_seconds":0,"reconstructed":true}\n',
encoding="utf-8",
)
proof = self.issue(
claims=[
{
"scope": "target_environment",
"result": "passed",
"control_ids": ["topology:shared-state-v1"],
"artifacts": [
{"artifact_id": "target:run-1", "path": result_path}
],
},
{
"scope": "recovery",
"result": "passed",
"control_ids": ["recovery:restore-v1"],
"artifacts": [
{"artifact_id": "recovery:run-1", "path": result_path}
],
},
]
)
self.assertEqual(
(), validate_payload(payload=proof, schema=self.boundary_schema)
)
self.assertNotIn(str(result_path), json.dumps(proof))
self.assertEqual(
canonical_sha256(self.installed), proof["installed_evidence_sha256"]
)
installed_review = InstalledEvidenceReview(
findings=(),
changes=(),
changed_repositories=frozenset(),
affected_module_ids=frozenset(),
proof_scope={
"installed_artifacts": {"valid": True},
"installed_release_origin": {"valid": True},
},
evidence_sha256=canonical_sha256(self.installed),
)
review = review_boundary_evidence(
assessment=self.assessment,
installed_review=installed_review,
evidence=proof,
evidence_schema=self.boundary_schema,
authority_keyring=self.authority,
authority_keyring_schema=self.authority_schema,
verification_time=datetime(2026, 8, 2, 12, 30, tzinfo=UTC),
)
self.assertEqual((), review.findings)
self.assertTrue(review.proof_scope["target_environment"]["valid"])
self.assertTrue(review.proof_scope["recovery"]["valid"])
def test_refuses_missing_release_origin_or_scope_authority(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
artifact = Path(temp_dir) / "result.txt"
artifact.write_text("passed\n", encoding="utf-8")
claim = {
"scope": "security",
"result": "passed",
"control_ids": ["security:sast-v1"],
"artifacts": [{"artifact_id": "security:run-1", "path": artifact}],
}
with self.assertRaisesRegex(ValueError, "release origin"):
self.issue(claims=[claim], release_origin_verified=False)
with self.assertRaisesRegex(ValueError, "authorized for scopes: security"):
self.issue(claims=[claim])
def test_refuses_authority_that_expires_before_the_proof(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
artifact = Path(temp_dir) / "result.txt"
artifact.write_text("passed\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "full proof interval"):
self.issue(
expires_at=datetime(2026, 10, 1, tzinfo=UTC),
claims=[
{
"scope": "recovery",
"result": "passed",
"control_ids": ["recovery:restore-v1"],
"artifacts": [
{
"artifact_id": "recovery:run-1",
"path": artifact,
}
],
}
],
)
def issue(
self,
*,
claims: list[dict[str, object]],
expires_at: datetime = datetime(2026, 8, 3, tzinfo=UTC),
release_origin_verified: bool = True,
) -> dict[str, object]:
return issue_boundary_evidence(
assessment=self.assessment,
installed_evidence=self.installed,
proof_id="proof:target:1",
claims=claims,
authority_keyring=self.authority,
signing_keys={"authority:target": self.private_key},
issued_at=datetime(2026, 8, 2, 12, tzinfo=UTC),
expires_at=expires_at,
release_origin_verified=release_origin_verified,
)
if __name__ == "__main__":
unittest.main()
+30 -1
View File
@@ -26,6 +26,7 @@ for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
sys.path.insert(0, str(tools_root))
from govoplan_assessment.capability_fit import ( # noqa: E402
enforce_required_boundary_scopes,
local_tag_provenance,
render_review,
review_capability_fit,
@@ -74,6 +75,34 @@ class CapabilityFitReviewTests(unittest.TestCase):
json.dumps(report, sort_keys=True), json.dumps(repeated, sort_keys=True)
)
def test_required_boundary_scope_blocks_admission_until_positive(self) -> None:
catalog, keyring = signed_catalog(self.assessment)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
enforce_required_boundary_scopes(
report, required_scopes=("security", "recovery")
)
self.assertEqual("blocked", report["status"])
self.assertFalse(report["proof_scope"]["admission"]["valid"])
self.assertEqual(
["recovery", "security"],
report["proof_scope"]["admission"]["failed_scopes"],
)
self.assertEqual(
2,
sum(
item["code"] == "required_boundary_scope_unsatisfied"
for item in report["findings"]
),
)
def test_release_drift_identifies_affected_conclusions(self) -> None:
catalog, keyring = signed_catalog(
self.assessment, versions={"campaigns": "0.1.11"}, sequence=202607230001
@@ -360,7 +389,7 @@ class CapabilityFitReviewTests(unittest.TestCase):
self.assertIn("Capability fit rerun: current", rendered)
self.assertIn(
"No installed-composition, installed-release-origin, target-environment, external-provider, production-approval proof",
"No installed-composition, installed-release-origin, target-environment, external-providers, accessibility, privacy, security, operations, recovery, production-approval proof",
rendered,
)
+363
View File
@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""Issue and immediately verify a sanitized target-boundary proof."""
from __future__ import annotations
import argparse
from copy import deepcopy
from datetime import UTC, datetime
import json
from pathlib import Path
import sys
META_ROOT = Path(__file__).resolve().parents[2]
for tools_root in (
META_ROOT / "tools" / "assessments",
META_ROOT / "tools" / "release",
):
if str(tools_root) not in sys.path:
sys.path.insert(0, str(tools_root))
from cryptography.hazmat.primitives import serialization # noqa: E402
from cryptography.hazmat.primitives.asymmetric.ed25519 import ( # noqa: E402
Ed25519PrivateKey,
)
from govoplan_assessment.atomic_io import ( # noqa: E402
AtomicJsonWriteError,
atomic_write_json,
)
from govoplan_assessment.boundary_evidence import ( # noqa: E402
issue_boundary_evidence,
)
from govoplan_assessment.capability_fit import review_capability_fit # noqa: E402
from govoplan_assessment.evidence import ( # noqa: E402
public_key_material_from_keyrings,
validate_payload,
)
MAX_INPUT_BYTES = 16 * 1024 * 1024
MAX_OUTPUT_BYTES = 4 * 1024 * 1024
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--assessment", type=Path, required=True)
parser.add_argument(
"--assessment-schema",
type=Path,
default=META_ROOT / "docs" / "capability-fit.schema.json",
)
parser.add_argument("--catalog", type=Path, required=True)
parser.add_argument("--keyring", type=Path, required=True)
parser.add_argument("--trusted-keyring", type=Path, required=True)
parser.add_argument("--installed-evidence", type=Path, required=True)
parser.add_argument(
"--installed-evidence-schema",
type=Path,
default=META_ROOT / "docs" / "installed-composition-evidence.schema.json",
)
parser.add_argument("--installer-receipt", type=Path, required=True)
parser.add_argument(
"--installer-receipt-schema",
type=Path,
default=META_ROOT / "docs" / "installer-receipt.schema.json",
)
parser.add_argument("--installer-authority-keyring", type=Path, required=True)
parser.add_argument(
"--installer-authority-keyring-schema",
type=Path,
default=META_ROOT / "docs" / "installer-receipt-authority-keyring.schema.json",
)
parser.add_argument(
"--claims",
type=Path,
required=True,
help="Private target-run claim manifest; artifact paths are not published.",
)
parser.add_argument(
"--claims-schema",
type=Path,
default=META_ROOT / "docs" / "capability-fit-boundary-run.schema.json",
)
parser.add_argument("--authority-keyring", type=Path, required=True)
parser.add_argument(
"--authority-keyring-schema",
type=Path,
default=META_ROOT
/ "docs"
/ "capability-fit-proof-authority-keyring.schema.json",
)
parser.add_argument(
"--signing-key",
action="append",
default=[],
required=True,
metavar="KEY_ID=/PATH/PRIVATE.pem",
help="Repeat for independent authorities needed to cover all claim scopes.",
)
parser.add_argument("--expected-external-provider-subject")
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--review-output", type=Path, required=True)
parser.add_argument(
"--boundary-evidence-schema",
type=Path,
default=META_ROOT / "docs" / "capability-fit-boundary-evidence.schema.json",
)
args = parser.parse_args(argv)
if args.output.resolve() == args.review_output.resolve():
parser.error("proof and review output paths must differ")
assessment = read_object(args.assessment, label="assessment")
assessment_schema = read_object(args.assessment_schema, label="assessment schema")
catalog = read_object(args.catalog, label="catalog")
published_keyring = read_object(args.keyring, label="published keyring")
trusted_keyring = read_object(args.trusted_keyring, label="trusted keyring")
installed_evidence = read_object(
args.installed_evidence, label="installed evidence"
)
installed_schema = read_object(
args.installed_evidence_schema, label="installed evidence schema"
)
installer_receipt = read_object(args.installer_receipt, label="installer receipt")
installer_receipt_schema = read_object(
args.installer_receipt_schema, label="installer receipt schema"
)
installer_authorities = read_object(
args.installer_authority_keyring, label="installer authority keyring"
)
installer_authority_schema = read_object(
args.installer_authority_keyring_schema,
label="installer authority keyring schema",
)
claim_manifest = read_object(args.claims, label="target-run claim manifest")
claim_schema = read_object(args.claims_schema, label="target-run claim schema")
authority_keyring = read_object(
args.authority_keyring, label="proof-authority keyring"
)
authority_schema = read_object(
args.authority_keyring_schema, label="proof-authority keyring schema"
)
boundary_schema = read_object(
args.boundary_evidence_schema, label="boundary evidence schema"
)
_require_schema(claim_manifest, claim_schema, label="target-run claim manifest")
_require_schema(
authority_keyring, authority_schema, label="proof-authority keyring"
)
base_report = review_capability_fit(
assessment=assessment,
schema=assessment_schema,
catalog=catalog,
published_keyring=published_keyring,
trusted_keyring=trusted_keyring,
workspace_root=None,
installed_evidence=installed_evidence,
installed_evidence_schema=installed_schema,
installer_receipt=installer_receipt,
installer_receipt_schema=installer_receipt_schema,
installer_authority_keyring=installer_authorities,
installer_authority_keyring_schema=installer_authority_schema,
installed_evidence_mode="imported_unsigned",
)
_require_release_origin(base_report, parser=parser)
claims = resolve_claim_artifact_paths(
claim_manifest.get("claims"), base_dir=args.claims.resolve().parent
)
signing_keys = read_signing_keys(tuple(args.signing_key))
expires_at = parse_datetime(claim_manifest.get("expires_at"), label="expires_at")
forbidden_keys = public_key_material_from_keyrings(
published_keyring,
trusted_keyring,
installer_authorities,
)
try:
proof = issue_boundary_evidence(
assessment=assessment,
installed_evidence=installed_evidence,
proof_id=str(claim_manifest.get("proof_id") or ""),
claims=claims,
authority_keyring=authority_keyring,
signing_keys=signing_keys,
expires_at=expires_at,
expected_external_provider_subject=(
args.expected_external_provider_subject
),
forbidden_authority_public_keys=forbidden_keys,
release_origin_verified=True,
)
except ValueError as exc:
parser.error(str(exc))
_require_schema(proof, boundary_schema, label="issued boundary evidence")
review = review_capability_fit(
assessment=assessment,
schema=assessment_schema,
catalog=catalog,
published_keyring=published_keyring,
trusted_keyring=trusted_keyring,
workspace_root=None,
installed_evidence=installed_evidence,
installed_evidence_schema=installed_schema,
installer_receipt=installer_receipt,
installer_receipt_schema=installer_receipt_schema,
installer_authority_keyring=installer_authorities,
installer_authority_keyring_schema=installer_authority_schema,
boundary_evidence=proof,
boundary_evidence_schema=boundary_schema,
boundary_authority_keyring=authority_keyring,
boundary_authority_keyring_schema=authority_schema,
installed_evidence_mode="imported_unsigned",
expected_external_provider_subject=args.expected_external_provider_subject,
)
supplied_scopes = {str(claim.get("scope")) for claim in proof["claims"]}
unchecked = sorted(
scope
for scope in supplied_scopes
if review.get("proof_scope", {}).get(scope, {}).get("checked") is not True
)
if unchecked:
parser.error(
"issued proof did not pass immediate verification for scopes: "
+ ", ".join(unchecked)
)
write_object(args.output, proof, label="boundary evidence")
write_object(args.review_output, review, label="capability-fit review")
print(
json.dumps(
{
"proof_id": proof["proof_id"],
"scopes": sorted(supplied_scopes),
"reference_readiness": review.get("proof_scope", {}).get(
"reference_readiness"
),
"output": str(args.output),
"review_output": str(args.review_output),
},
sort_keys=True,
)
)
return 0
def read_object(path: Path, *, label: str) -> dict[str, object]:
try:
if path.stat().st_size > MAX_INPUT_BYTES:
raise ValueError("input exceeds size limit")
payload = json.loads(path.read_bytes().decode("utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
raise SystemExit(f"Could not read {label}") from exc
if not isinstance(payload, dict):
raise SystemExit(f"{label.capitalize()} must be a JSON object")
return payload
def read_signing_keys(values: tuple[str, ...]) -> dict[str, Ed25519PrivateKey]:
result: dict[str, Ed25519PrivateKey] = {}
for value in values:
key_id, separator, path_text = value.partition("=")
if not separator or not key_id or not path_text or key_id in result:
raise SystemExit(
"--signing-key must uniquely use KEY_ID=/path/to/private.pem"
)
try:
key = serialization.load_pem_private_key(
Path(path_text).expanduser().read_bytes(), password=None
)
except (OSError, ValueError) as exc:
raise SystemExit("Could not read boundary signing key") from exc
if not isinstance(key, Ed25519PrivateKey):
raise SystemExit("Boundary signing keys must be Ed25519")
result[key_id] = key
return result
def resolve_claim_artifact_paths(
claims: object, *, base_dir: Path
) -> list[dict[str, object]]:
if not isinstance(claims, list):
raise SystemExit("Target-run claim manifest has no claim list")
resolved = deepcopy(claims)
for claim in resolved:
if not isinstance(claim, dict):
continue
artifacts = claim.get("artifacts")
if not isinstance(artifacts, list):
continue
for artifact in artifacts:
if not isinstance(artifact, dict) or not isinstance(
artifact.get("path"), str
):
continue
path = Path(artifact["path"]).expanduser()
artifact["path"] = path if path.is_absolute() else base_dir / path
return resolved
def parse_datetime(value: object, *, label: str) -> datetime:
if not isinstance(value, str):
raise SystemExit(f"{label} must be an RFC 3339 date-time")
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise SystemExit(f"{label} must be an RFC 3339 date-time") from exc
if parsed.tzinfo is None:
raise SystemExit(f"{label} must include a timezone")
return parsed.astimezone(UTC)
def _require_schema(
payload: dict[str, object], schema: dict[str, object], *, label: str
) -> None:
errors = validate_payload(payload=payload, schema=schema)
if errors:
raise SystemExit(f"{label.capitalize()} failed schema validation: {errors[0]}")
def _require_release_origin(
report: dict[str, object], *, parser: argparse.ArgumentParser
) -> None:
proof_scope = report.get("proof_scope")
if not isinstance(proof_scope, dict):
parser.error("capability-fit review did not return proof scope")
required = (
"catalog_signature_and_keyring",
"catalog_signature_and_trusted_keyring",
"published_keyring_hash",
"release_metadata",
"installed_artifacts",
"installed_record_integrity",
"installed_release_origin",
)
failed = sorted(
scope
for scope in required
if not isinstance(proof_scope.get(scope), dict)
or proof_scope[scope].get("valid") is not True
)
observation = proof_scope.get("installed_evidence_observation")
if (
not isinstance(observation, dict)
or observation.get("receipt_authenticated") is not True
):
failed.append("installed_evidence_observation.receipt_authenticated")
if failed:
parser.error(
"trusted release and installer evidence must pass before target proof issuance: "
+ ", ".join(failed)
)
def write_object(path: Path, payload: dict[str, object], *, label: str) -> None:
try:
atomic_write_json(path, payload, max_bytes=MAX_OUTPUT_BYTES)
except AtomicJsonWriteError as exc:
raise SystemExit(f"Could not securely write {label}") from exc
if __name__ == "__main__":
raise SystemExit(main())
+28
View File
@@ -20,9 +20,11 @@ for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
from govoplan_assessment import ( # noqa: E402
collect_installed_composition,
enforce_required_boundary_scopes,
render_review,
review_capability_fit,
)
from govoplan_assessment.evidence import REFERENCE_READINESS_SCOPES # noqa: E402
from govoplan_assessment.atomic_io import ( # noqa: E402
AtomicJsonWriteError,
atomic_write_json,
@@ -169,6 +171,21 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"never supply a URL, credential, or endpoint identifier."
),
)
parser.add_argument(
"--require-reference-readiness",
action="store_true",
help="Fail unless every reference-readiness boundary scope is current and positive.",
)
parser.add_argument(
"--require-external-provider-proof",
action="store_true",
help="Fail unless current positive external-provider evidence is present.",
)
parser.add_argument(
"--require-production-approval",
action="store_true",
help="Fail unless a current explicit production approval is present.",
)
parser.add_argument(
"--json", action="store_true", help="Print the machine-readable review report."
)
@@ -377,6 +394,17 @@ def main(argv: list[str] | None = None) -> int:
),
expected_external_provider_subject=args.expected_external_provider_subject,
)
required_boundary_scopes: set[str] = set()
if args.require_reference_readiness:
required_boundary_scopes.update(REFERENCE_READINESS_SCOPES)
if args.require_external_provider_proof:
required_boundary_scopes.add("external_providers")
if args.require_production_approval:
required_boundary_scopes.add("production_approval")
if required_boundary_scopes:
enforce_required_boundary_scopes(
report, required_scopes=required_boundary_scopes
)
encoded = json.dumps(report, indent=2, sort_keys=True) + "\n"
if args.output is not None:
write_object(
@@ -1,10 +1,15 @@
"""Repeatable GovOPlaN product-assessment tooling."""
from .capability_fit import review_capability_fit, render_review
from .capability_fit import (
enforce_required_boundary_scopes,
render_review,
review_capability_fit,
)
from .evidence import collect_installed_composition
__all__ = (
"collect_installed_composition",
"enforce_required_boundary_scopes",
"render_review",
"review_capability_fit",
)
@@ -0,0 +1,270 @@
"""Issue sanitized, release-bound target evidence from retained result files."""
from __future__ import annotations
import base64
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any, Mapping, Sequence
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from .evidence import (
MAX_HASHED_FILE_BYTES,
NEGATIVE_RESULTS,
OPAQUE_ID_PATTERN,
POSITIVE_RESULTS,
PROOF_REQUIREMENTS,
_active_authority_keys,
_bounded_file_sha256,
canonical_bytes,
canonical_sha256,
)
MAX_BOUNDARY_ARTIFACT_BYTES = MAX_HASHED_FILE_BYTES
MAX_BOUNDARY_ARTIFACT_TOTAL_BYTES = 512 * 1024 * 1024
def issue_boundary_evidence(
*,
assessment: Mapping[str, Any],
installed_evidence: Mapping[str, Any],
proof_id: str,
claims: Sequence[Mapping[str, Any]],
authority_keyring: dict[str, Any],
signing_keys: Mapping[str, Ed25519PrivateKey],
expires_at: datetime,
issued_at: datetime | None = None,
expected_external_provider_subject: str | None = None,
forbidden_authority_public_keys: frozenset[bytes] = frozenset(),
release_origin_verified: bool = False,
) -> dict[str, Any]:
"""Build and sign one bounded proof after its release origin was verified.
The public CLI is responsible for deriving ``release_origin_verified`` from
the signed catalog, installed observation and installer receipt. Artifact
paths are consumed here and never copied into the resulting receipt.
"""
if release_origin_verified is not True:
raise ValueError(
"boundary evidence requires independently verified installed release origin"
)
_require_opaque_id(proof_id, label="proof ID")
now = issued_at or datetime.now(tz=UTC)
now = _aware_utc(now, label="issuance time")
expiry = _aware_utc(expires_at, label="expiry time")
if now >= expiry:
raise ValueError("boundary evidence expiry must be after issuance")
assessment_id = assessment.get("assessment_id")
release = assessment.get("release")
assessment_release = release.get("ref") if isinstance(release, Mapping) else None
deployment = assessment.get("deployment_profile")
deployment_subject = (
deployment.get("id") if isinstance(deployment, Mapping) else None
)
_require_opaque_id(assessment_id, label="assessment ID")
_require_opaque_id(assessment_release, label="assessment release")
_require_opaque_id(deployment_subject, label="deployment subject")
if expected_external_provider_subject is not None:
_require_opaque_id(
expected_external_provider_subject,
label="external-provider subject",
)
normalized_claims = _normalize_claims(
claims=claims,
deployment_subject=str(deployment_subject),
expected_external_provider_subject=expected_external_provider_subject,
)
active_at_issue, issue_findings = _active_authority_keys(
authority_keyring=authority_keyring,
verification_time=now,
forbidden_public_keys=forbidden_authority_public_keys,
)
active_until_expiry, expiry_findings = _active_authority_keys(
authority_keyring=authority_keyring,
verification_time=expiry - timedelta(microseconds=1),
forbidden_public_keys=forbidden_authority_public_keys,
)
if issue_findings or expiry_findings:
raise ValueError(
"proof-authority keyring is malformed, reused, or not valid for the full proof interval"
)
if not signing_keys:
raise ValueError(
"at least one independently provisioned signing key is required"
)
covered_scopes: set[str] = set()
verified_signers: list[tuple[str, Ed25519PrivateKey]] = []
for key_id, private_key in sorted(signing_keys.items()):
_require_opaque_id(key_id, label="signing key ID")
if not isinstance(private_key, Ed25519PrivateKey):
raise ValueError("boundary signing keys must be Ed25519")
authority = active_at_issue.get(key_id)
authority_at_expiry = active_until_expiry.get(key_id)
public_bytes = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
if (
authority is None
or authority_at_expiry is None
or authority[0] != public_bytes
or authority_at_expiry[0] != public_bytes
):
raise ValueError(
f"signing key {key_id!r} is not independently authorized for the full proof interval"
)
covered_scopes.update(authority[1])
verified_signers.append((key_id, private_key))
claim_scopes = {str(claim["scope"]) for claim in normalized_claims}
uncovered_scopes = sorted(claim_scopes - covered_scopes)
if uncovered_scopes:
raise ValueError(
"no supplied signing key is authorized for scopes: "
+ ", ".join(uncovered_scopes)
)
proof: dict[str, Any] = {
"$schema": "./capability-fit-boundary-evidence.schema.json",
"schema_version": "0.1.0",
"evidence_kind": "govoplan.capability-fit-boundary-proof",
"proof_id": proof_id,
"assessment_id": assessment_id,
"assessment_release": assessment_release,
"installed_evidence_sha256": canonical_sha256(installed_evidence),
"issued_at": _iso_datetime(now),
"expires_at": _iso_datetime(expiry),
"claims": normalized_claims,
}
signature_payload = canonical_bytes(proof)
proof["signatures"] = [
{
"algorithm": "ed25519",
"key_id": key_id,
"value": base64.b64encode(private_key.sign(signature_payload)).decode(
"ascii"
),
}
for key_id, private_key in verified_signers
]
return proof
def _normalize_claims(
*,
claims: Sequence[Mapping[str, Any]],
deployment_subject: str,
expected_external_provider_subject: str | None,
) -> list[dict[str, Any]]:
if not 1 <= len(claims) <= len(PROOF_REQUIREMENTS):
raise ValueError("boundary evidence requires between one and eight claims")
normalized: list[dict[str, Any]] = []
seen_scopes: set[str] = set()
total_bytes = 0
for claim in claims:
scope = str(claim.get("scope") or "")
if scope not in PROOF_REQUIREMENTS or scope in seen_scopes:
raise ValueError("boundary claim scopes must be known and unique")
seen_scopes.add(scope)
result = str(claim.get("result") or "")
if result not in {POSITIVE_RESULTS[scope], NEGATIVE_RESULTS[scope]}:
raise ValueError(f"boundary claim result is invalid for {scope!r}")
controls = _opaque_id_list(
claim.get("control_ids"),
label=f"{scope} control IDs",
maximum=256,
)
raw_artifacts = claim.get("artifacts")
if not isinstance(raw_artifacts, Sequence) or isinstance(
raw_artifacts, (str, bytes)
):
raise ValueError(f"{scope} artifacts must be a non-empty list")
if not 1 <= len(raw_artifacts) <= 256:
raise ValueError(f"{scope} must reference between one and 256 artifacts")
artifacts: list[dict[str, str]] = []
seen_artifacts: set[str] = set()
for artifact in raw_artifacts:
if not isinstance(artifact, Mapping):
raise ValueError(f"{scope} contains a malformed artifact")
artifact_id = artifact.get("artifact_id")
_require_opaque_id(artifact_id, label=f"{scope} artifact ID")
artifact_id = str(artifact_id)
if artifact_id in seen_artifacts:
raise ValueError(f"{scope} contains duplicate artifact IDs")
seen_artifacts.add(artifact_id)
raw_path = artifact.get("path")
if not isinstance(raw_path, (str, Path)) or not str(raw_path):
raise ValueError(f"{scope} artifact {artifact_id!r} has no file path")
try:
digest, size, changed = _bounded_file_sha256(
Path(raw_path), max_bytes=MAX_BOUNDARY_ARTIFACT_BYTES
)
except OSError as exc:
raise ValueError(
f"could not securely hash {scope} artifact {artifact_id!r}"
) from exc
if changed:
raise ValueError(
f"{scope} artifact {artifact_id!r} changed or exceeded the size limit while hashing"
)
total_bytes += size
if total_bytes > MAX_BOUNDARY_ARTIFACT_TOTAL_BYTES:
raise ValueError(
"boundary evidence artifacts exceed the total size limit"
)
artifacts.append({"artifact_id": artifact_id, "sha256": digest.hex()})
normalized.append(
{
"scope": scope,
"result": result,
"subject_id": (
expected_external_provider_subject
if scope == "external_providers"
else deployment_subject
),
"control_ids": controls,
"artifacts": artifacts,
}
)
if "external_providers" in seen_scopes and not expected_external_provider_subject:
raise ValueError(
"external-provider evidence requires its expected opaque subject ID"
)
return sorted(normalized, key=lambda item: str(item["scope"]))
def _opaque_id_list(value: object, *, label: str, maximum: int) -> list[str]:
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
raise ValueError(f"{label} must be a non-empty list")
result = [str(item) for item in value]
if not 1 <= len(result) <= maximum or len(result) != len(set(result)):
raise ValueError(f"{label} must contain unique bounded IDs")
for item in result:
_require_opaque_id(item, label=label)
return result
def _require_opaque_id(value: object, *, label: str) -> None:
if (
not isinstance(value, str)
or len(value) > 160
or OPAQUE_ID_PATTERN.fullmatch(value) is None
):
raise ValueError(f"{label} must be a bounded opaque ID")
def _aware_utc(value: datetime, *, label: str) -> datetime:
if value.tzinfo is None:
raise ValueError(f"{label} must include a timezone")
return value.astimezone(UTC)
def _iso_datetime(value: datetime) -> str:
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
@@ -930,10 +930,7 @@ def build_report(
boundary_scope = (
boundary_review.proof_scope
if boundary_review is not None
else {
scope: {"checked": False, "valid": None}
for scope in PROOF_REQUIREMENTS
}
else {scope: {"checked": False, "valid": None} for scope in PROOF_REQUIREMENTS}
)
proof_scope = {
"assessment_schema": {"checked": True, "valid": schema_valid},
@@ -1002,6 +999,63 @@ def build_report(
}
def enforce_required_boundary_scopes(
report: dict[str, Any], *, required_scopes: Iterable[str]
) -> dict[str, Any]:
"""Turn missing or negative external proof into an admission blocker."""
requested = tuple(dict.fromkeys(sorted(required_scopes)))
unknown = sorted(set(requested) - set(PROOF_REQUIREMENTS))
if unknown:
raise ValueError("unknown boundary scopes: " + ", ".join(unknown))
proof_scope = report.get("proof_scope")
if not isinstance(proof_scope, dict):
raise ValueError("capability-fit report has no proof scope")
failed = tuple(
scope
for scope in requested
if not isinstance(proof_scope.get(scope), dict)
or proof_scope[scope].get("checked") is not True
or proof_scope[scope].get("valid") is not True
)
proof_scope["admission"] = {
"checked": True,
"valid": not failed,
"required_scopes": list(requested),
"failed_scopes": list(failed),
}
if not failed:
return report
findings = report.setdefault("findings", [])
existing = {
(item.get("code"), item.get("message"))
for item in findings
if isinstance(item, dict)
}
for scope in failed:
message = (
f"Admission requires current positive boundary evidence for {scope!r}."
)
if ("required_boundary_scope_unsatisfied", message) not in existing:
findings.append(
{
"severity": "blocker",
"code": "required_boundary_scope_unsatisfied",
"message": message,
"assessment_ids": ["assessment.external_proof"],
}
)
findings.sort(
key=lambda item: (
_severity_rank(str(item.get("severity") or "")),
str(item.get("code") or ""),
str(item.get("message") or ""),
)
)
report["status"] = "blocked"
return report
def render_review(report: dict[str, Any]) -> str:
proof_scope = report.get("proof_scope", {})
installed_scope = proof_scope.get("installed_artifacts", {})
@@ -1062,8 +1116,7 @@ def render_review(report: dict[str, Any]) -> str:
unchecked_boundaries = [
label
for key, label in (
(scope, scope.replace("_", "-"))
for scope in PROOF_REQUIREMENTS
(scope, scope.replace("_", "-")) for scope in PROOF_REQUIREMENTS
)
if report.get("proof_scope", {}).get(key, {}).get("checked") is not True
]