fix(auth): authenticate producer-signed decisions before authorization
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 07:51:44 +02:00
parent 6bd7129eac
commit c80a944105
30 changed files with 2059 additions and 32 deletions

View file

@ -36,6 +36,13 @@ point, not an identity provider, and not a general secrets API.
### Decision-gated planning and OpenBao metadata apply
- Source now requires independently configured responder trust and a separately
installed Go producer-format verifier on the serving authorization path.
Signatures bind admitted keys to signed evaluator/mode and exact policy
package/version/artifact digest; unsigned responses fail closed. This requires
a reviewed native deployment/key admission under WP14, still outstanding.
- Resolves a catalog lane by catalog id or `approval.decision_ref`.
- Resolves legacy lane decisions from State Hub by id, with tracked local YAML
mirrors retained only for non-production and explicit throwaway demos. This is

View file

@ -132,3 +132,26 @@ The lesson worth keeping: the replay fixtures could not catch this, because
`_request_from()` rebuilds the request from the binding, so the tests hashed the
evaluator's output and compared it to the evaluator's output. Only a real
request through this access path exposed it.
## Mandatory responder authentication (WP14 source preparation)
Every serving `authorize_action` path requires `SECRETS_ENGINE_PDP_TRUST_FILE`
and `SECRETS_ENGINE_PDP_VERIFIER`. These fail closed before approval/PDP credential
access when a PDP path is configured. Trust must be an absolute regular JSON file,
not group/world writable, admitted independently of the responder or its keys
endpoint. Neither a loopback URL nor installing source admits production keys.
See `verifier/README.md` for build, source pin/update and installation requirements.
Trust schema `secrets-engine.pdp-trust.v1` requires exactly `schema`, `evaluator`,
`mode`, `policy_package`, `policy_version`, `policy_package_digest`, and `keys`
(a map of signer kid to base64url-unpadded Ed25519 public key). The producer has
no separate issuer or instance identifier: issuer binding is the admitted signer
plus signed provenance evaluator/mode; the exact policy coordinate and artifact
digest identify the admitted pin. Deployment key custody must scope the signer to
that responder; shared signing keys cannot prove a distinct pod identity.
Rotation is an explicit reviewed admission update: admit old/new keys for a bounded
overlap, switch producer signer, verify the actual consumer, then remove the old
kid and prove refusal. Keys are reloaded each call; no endpoint discovery, cached
key fallback, unsigned exception, or automatic admission exists. These source
checks do not constitute Platform key admission or native rollout acceptance.

View file

@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""Verify exact vendored producer bytes and optionally a prospective upstream.
Updates require reviewed replacement of API/sign files, fixture refresh from the
producer, and reviewed manifest hashes/revision; this checker never updates them.
"""
import argparse
import hashlib
import json
from pathlib import Path
parser=argparse.ArgumentParser()
parser.add_argument('--upstream',type=Path)
args=parser.parse_args()
root=Path(__file__).resolve().parents[1]
manifest=json.loads((root/'verifier/producer-source.json').read_text())
for name,digest in manifest['files'].items():
assert hashlib.sha256((root/'verifier'/name).read_bytes()).hexdigest()==digest, name
if args.upstream:
assert hashlib.sha256((args.upstream/name).read_bytes()).hexdigest()==digest, 'upstream drift: '+name
for name,digest in manifest['fixtures'].items():
assert hashlib.sha256((root/'tests/fixtures/pdp-signature'/name).read_bytes()).hexdigest()==digest, name
print('producer source and fixture hashes match the admitted source snapshot')

View file

@ -22,7 +22,8 @@ from secrets_engine.approval_auth import approval_auth_configured, approval_toke
from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
from secrets_engine.approval_claim import observe_pdp_approval_claim, validate_approval_claim
from secrets_engine.catalog import human_control_required
from secrets_engine.decision_check import check_decision
from secrets_engine.decision_check import check_decision, require_supported_pdp_address
from secrets_engine.decision_signature import load_trust
from secrets_engine.authorization import (
build_action_request,
validate_decision_envelope,
@ -300,6 +301,16 @@ def authorize_action(
configured at all, which leaves production fail-closed. A configured but
failing path raises: a partial deployment must not read as an absent one.
"""
serving = (getattr(cfg, "approval_url", "") and approval_auth_configured(cfg)
and _authorization_id(entry, decision))
if serving or (getattr(cfg, "pdp_url", "") and getattr(cfg, "pdp_token_file", None)):
if not getattr(cfg, "pdp_url", "") or not getattr(cfg, "pdp_token_file", None):
raise DecisionError("production action requires configured access-engine URL/token and responder trust")
require_supported_pdp_address(str(cfg.pdp_url))
load_trust(getattr(cfg, "pdp_trust_file", None))
verifier = getattr(cfg, "pdp_verifier", None)
if verifier is None or not Path(verifier).is_absolute() or not Path(verifier).is_file():
raise DecisionError("access-engine signature verifier is not installed")
observation = resolve_approval_observation(
cfg, entry, action, decision,
fields=fields,
@ -333,6 +344,8 @@ def authorize_action(
token_file=Path(pdp_token),
request=expected_request,
opener=pdp_opener,
trust_file=getattr(cfg, "pdp_trust_file", None),
verifier=getattr(cfg, "pdp_verifier", None),
)
if not getattr(cfg, "clock_trust_file", None):
wait_for_decision_start(envelope)

View file

@ -10,6 +10,10 @@ from dataclasses import dataclass
from pathlib import Path
# Public State Hub domain identifier, not an authentication credential.
DEFAULT_TOPIC_ID = "cee7bedf-2b48-46ef-8601-006474f2ad7a"
def repo_root() -> Path:
"""Repo root = nearest ancestor containing pyproject.toml (fallback: cwd)."""
here = Path(__file__).resolve()
@ -52,6 +56,8 @@ class Config:
authorization_min_approvals: int = 1
pdp_url: str = ""
pdp_token_file: Path | None = None
pdp_trust_file: Path | None = None
pdp_verifier: Path | None = None
clock_trust_file: Path | None = None
@classmethod
@ -69,7 +75,7 @@ class Config:
hub_url=os.environ.get("SECRETS_ENGINE_HUB_URL", "http://127.0.0.1:8000"),
bao_addr=os.environ.get("BAO_ADDR", os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200")),
topic_id=os.environ.get(
"SECRETS_ENGINE_TOPIC_ID", "cee7bedf-2b48-46ef-8601-006474f2ad7a"
"SECRETS_ENGINE_TOPIC_ID", DEFAULT_TOPIC_ID
),
approval_url=os.environ.get("SECRETS_ENGINE_APPROVAL_URL", ""),
approval_token_file=Path(token_file) if token_file else None,
@ -95,5 +101,7 @@ class Config:
),
pdp_url=os.environ.get("SECRETS_ENGINE_PDP_URL", ""),
pdp_token_file=Path(pdp_token) if pdp_token else None,
pdp_trust_file=Path(os.environ["SECRETS_ENGINE_PDP_TRUST_FILE"]) if os.environ.get("SECRETS_ENGINE_PDP_TRUST_FILE") else None,
pdp_verifier=Path(os.environ["SECRETS_ENGINE_PDP_VERIFIER"]) if os.environ.get("SECRETS_ENGINE_PDP_VERIFIER") else None,
clock_trust_file=Path(os.environ["SECRETS_ENGINE_CLOCK_TRUST_FILE"]) if os.environ.get("SECRETS_ENGINE_CLOCK_TRUST_FILE") else None,
)

View file

@ -19,6 +19,7 @@ from urllib.error import HTTPError, URLError
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
from secrets_engine.errors import DecisionError
from secrets_engine.decision_signature import strict_json, load_trust, authenticate_response
from secrets_engine.openbao import read_strict_token_file
_MAX_BODY = 512 * 1024
@ -95,6 +96,9 @@ def check_decision(
base_url: str,
token_file: Path,
request: dict[str, Any],
trust_file: Path | None = None,
verifier: Path | None = None,
allow_unsigned_test_fixture: bool = False,
timeout_seconds: float = 3,
opener: Callable[..., Any] | None = None,
) -> dict[str, Any]:
@ -103,9 +107,13 @@ def check_decision(
Silence is never permission: an unreachable PDP raises rather than
returning a permissive default.
"""
require_signature = not allow_unsigned_test_fixture
if not base_url or not base_url.startswith(("http://", "https://")):
raise DecisionError("access-engine check URL is missing or invalid")
require_supported_pdp_address(base_url)
trust = load_trust(trust_file) if require_signature else None
if require_signature and (verifier is None or not Path(verifier).is_absolute() or not Path(verifier).is_file()):
raise DecisionError("access-engine signature verifier is not installed")
token = read_strict_token_file(Path(token_file), purpose="access-engine credential")
encoded = json.dumps(request).encode("utf-8")
http_request = Request(
@ -123,13 +131,15 @@ def check_decision(
body = response.read(_MAX_BODY + 1)
if len(body) > _MAX_BODY:
raise DecisionError("access-engine check body is too large")
payload = json.loads(body.decode("utf-8"))
payload = strict_json(body.decode("utf-8"))
except HTTPError as e:
raise DecisionError(f"access-engine check refused: {_status_message(e.code)}") from e
except URLError as e:
raise DecisionError("access-engine is unreachable for check") from e
except (json.JSONDecodeError, UnicodeDecodeError) as e:
except (ValueError, UnicodeDecodeError, RecursionError) as e:
raise DecisionError("access-engine check returned a non-JSON body") from e
if not isinstance(payload, dict):
raise DecisionError("access-engine check returned a non-object body")
if require_signature:
return authenticate_response(body, trust=trust, verifier=verifier)
return payload

View file

@ -0,0 +1,83 @@
"""Independently admitted Access Engine responder keys, never responder discovery.
The bundled Go helper uses the exact producer types and verifier. No Python
canonicalization or unsigned compatibility mode participates in authorization.
"""
from __future__ import annotations
import base64
import json
from pathlib import Path
import stat
import subprocess
from typing import Any
from secrets_engine.errors import DecisionError
def _unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate JSON member")
result[key] = value
return result
def strict_json(data):
return json.loads(data, object_pairs_hook=_unique_object,
parse_constant=lambda value: (_ for _ in ()).throw(ValueError("invalid number")))
def load_trust(path: Path | None) -> dict[str, Any]:
"""Read an operator-owned public admission file; missing trust is a refusal.
Admission config is a local authority input like the existing policy pin.
File provenance/approval and the executable's installation are rollout gates,
not established by a successful local signature test.
"""
try:
if path is None or not Path(path).is_absolute() or Path(path).is_symlink():
raise ValueError("trust path must be absolute and regular")
with Path(path).open('rb') as stream:
import os
info = os.fstat(stream.fileno())
if not stat.S_ISREG(info.st_mode) or info.st_mode & 0o022:
raise ValueError("trust must not be group/world writable")
raw = stream.read(65537)
if len(raw) > 65536:
raise ValueError("trust is oversized")
trust = strict_json(raw)
if set(trust) != {'schema', 'evaluator', 'mode', 'policy_package', 'policy_version', 'policy_package_digest', 'keys'}:
raise ValueError("invalid trust fields")
if trust['schema'] != 'secrets-engine.pdp-trust.v1':
raise ValueError("unsupported trust schema")
if any(not isinstance(trust[k], str) or not trust[k] for k in trust if k != 'keys'):
raise ValueError("missing issuer/pin")
if not isinstance(trust['keys'], dict) or not trust['keys'] or len(trust['keys']) > 16:
raise ValueError("missing admitted keys")
if any(not isinstance(k,str) or not k or not isinstance(v,str) or not v for k,v in trust['keys'].items()):
raise ValueError("invalid keys")
for key in trust["keys"].values():
if len(key) != 43 or len(base64.b64decode(key + "=", altchars=b"-_", validate=True)) != 32:
raise ValueError("invalid Ed25519 public key")
return trust
except (OSError, ValueError, TypeError, KeyError, RecursionError) as exc:
raise DecisionError("access-engine responder trust is missing or invalid") from exc
def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) -> dict:
if verifier is None or not Path(verifier).is_absolute():
raise DecisionError("access-engine signature verifier is not installed")
try:
result = subprocess.run([str(verifier)], input=b'{"envelope":'+body+b',"keys":'+json.dumps(trust['keys']).encode()+b'}',
capture_output=True, env={}, timeout=3, check=False)
if result.returncode != 0 or len(result.stdout) > 524288:
raise ValueError("signature refused")
authenticated = strict_json(result.stdout)
provenance = authenticated['provenance']
if any(provenance.get(k) != trust[k] for k in ('evaluator','mode','policy_package','policy_version','policy_package_digest')):
raise ValueError("issuer/pin mismatch")
return authenticated
except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError) as exc:
raise DecisionError("access-engine responder authentication failed") from exc

View file

@ -0,0 +1,118 @@
{
"id": "decision:414734bb30381ff7",
"contract_version": "flex-auth.decision-record.v1",
"request_id": "check:secrets-engine-rotate",
"effect": "allow",
"reason": "catalog_lane_policy_matched",
"matched_policy_version": "v2",
"matched_rule": "catalog_lane_policy_matched",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:platform",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"binding": {
"tenant": "tenant:platform",
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"action": "rotate",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:platform",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"request_digest": "sha256:de67324f54187055307a833235f83ced9fcd3a20952a27b3d19493ed39734345",
"submitted_request_digest": "sha256:41c8fc084e58c46554ccb6afe9943a99906e5986668c923811721f66d9b30a6a"
},
"lifetime": {
"kind": "ttl",
"ttl": "15m",
"not_before": "2026-09-07T07:10:23Z",
"expires_at": "2026-09-07T07:25:23Z"
},
"diagnostics": {
"action": "rotate",
"matched_relationship": "",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_status": "ready",
"registry_overrode": [],
"registry_resource": false,
"registry_subject": true
},
"provenance": {
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
"decision_time": "2026-09-07T07:10:23Z"
},
"signature": {
"mode": "signed",
"alg": "ed25519",
"kid": "testdata-ed25519",
"value": "G45R7eb_7Dl7B8RO5HTgefcys6QabYwe7NZcF4ju_zpEAfEBWKVK5T5e5rP1GNJ5uuVusrw6Fd90tttg3_-CAQ"
},
"caring": {
"profile": "caring-0.4.0-rc2",
"conformance_findings": [
{
"code": "CARING-DESCRIPTOR-MISSING",
"severity": "warning",
"message": "no CARING descriptor matched the request",
"fields": [
"caring_context"
]
}
]
}
}

View file

@ -0,0 +1,118 @@
{
"id": "decision:414734bb30381ff7",
"contract_version": "flex-auth.decision-record.v1",
"request_id": "check:secrets-engine-rotate",
"effect": "deny",
"reason": "tampered_after_signing",
"matched_policy_version": "v2",
"matched_rule": "catalog_lane_policy_matched",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:platform",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"binding": {
"tenant": "tenant:platform",
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"action": "rotate",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:platform",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"request_digest": "sha256:de67324f54187055307a833235f83ced9fcd3a20952a27b3d19493ed39734345",
"submitted_request_digest": "sha256:41c8fc084e58c46554ccb6afe9943a99906e5986668c923811721f66d9b30a6a"
},
"lifetime": {
"kind": "ttl",
"ttl": "15m",
"not_before": "2026-09-07T07:10:23Z",
"expires_at": "2026-09-07T07:25:23Z"
},
"diagnostics": {
"action": "rotate",
"matched_relationship": "",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_status": "ready",
"registry_overrode": [],
"registry_resource": false,
"registry_subject": true
},
"provenance": {
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
"decision_time": "2026-09-07T07:10:23Z"
},
"signature": {
"mode": "signed",
"alg": "ed25519",
"kid": "testdata-ed25519",
"value": "G45R7eb_7Dl7B8RO5HTgefcys6QabYwe7NZcF4ju_zpEAfEBWKVK5T5e5rP1GNJ5uuVusrw6Fd90tttg3_-CAQ"
},
"caring": {
"profile": "caring-0.4.0-rc2",
"conformance_findings": [
{
"code": "CARING-DESCRIPTOR-MISSING",
"severity": "warning",
"message": "no CARING descriptor matched the request",
"fields": [
"caring_context"
]
}
]
}
}

View file

@ -0,0 +1,35 @@
"""Native producer signing fixtures, never an authentication bypass."""
import json
import os
from pathlib import Path
import subprocess
import pytest
PUBLIC='IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'
@pytest.fixture(scope='session')
def signature_tools(tmp_path_factory):
directory=tmp_path_factory.mktemp('signature-tools')
# Private writable build cache keeps tests runnable in the workspace sandbox.
env={**os.environ,'GOCACHE':os.environ.get('GOCACHE',str(directory/'cache'))}
root=Path(__file__).parents[1]/'verifier'
for name,package in [('verify','.'),('sign','./testsupport/signer')]:
subprocess.run(['go','build','-o',str(directory/name),package],cwd=root,env=env,check=True,timeout=120)
return directory
def admit_tools(cfg, directory, tools):
trust={'schema':'secrets-engine.pdp-trust.v1','evaluator':'stub/local','mode':'standalone',
'policy_package':cfg.authorization_policy_package,'policy_version':cfg.authorization_policy_version,
'policy_package_digest':'sha256:'+'1'*64,'keys':{'testdata-ed25519':PUBLIC}}
path=directory/'pdp-trust.json';path.write_text(json.dumps(trust))
from dataclasses import is_dataclass, replace
if is_dataclass(cfg):
return replace(cfg,pdp_trust_file=path,pdp_verifier=tools/'verify')
cfg.pdp_trust_file=path;cfg.pdp_verifier=tools/'verify'
return cfg
def sign_envelope(envelope,tools,next_key=False):
result=subprocess.run([str(tools/'sign')]+(['next'] if next_key else []),input=json.dumps(envelope),text=True,capture_output=True,check=True,timeout=3)
return json.loads(result.stdout)

View file

@ -194,8 +194,11 @@ def test_environment_selects_separate_approval_secret(monkeypatch):
assert cfg.keycape_client_secret_file == Path("/protected/openbao.secret")
from tests.signature_support import signature_tools, admit_tools, sign_envelope
@pytest.mark.parametrize("failure", [None, "exchange", "conflict", "wrong-action"])
def test_cli_gate_exchanges_for_claim_and_consume(tmp_path, monkeypatch, failure):
def test_cli_gate_exchanges_for_claim_and_consume(tmp_path, monkeypatch, failure, signature_tools):
raw = copy.deepcopy(VALID)
raw["stage"] = "prod"
raw["approval"] = {"model": "bootstrap-only", "authorization_id": "approval-test", "purpose": "synthetic proof"}
@ -205,6 +208,9 @@ def test_cli_gate_exchanges_for_claim_and_consume(tmp_path, monkeypatch, failure
cfg.authorization_subject_type = "service"
cfg.authorization_policy_package = "secrets-engine.catalog-lane.lifecycle"
cfg.authorization_policy_version = "v2"
admit_tools(cfg,tmp_path,signature_tools)
original_decision=AuthorizationStub.decision
monkeypatch.setattr(AuthorizationStub,"decision",lambda self,request:sign_envelope(original_decision(self,request),signature_tools))
cfg.evidence_dir = tmp_path / "evidence"
cfg.pdp_token_file = tmp_path / "pdp.token"
cfg.pdp_token_file.write_text("synthetic-pdp-token")

View file

@ -24,7 +24,19 @@ from secrets_engine.catalog import validate_entry
from secrets_engine.errors import DecisionError
from tests.test_catalog import VALID
AUTH_ID = "8bfc20be-47a4-4fb0-97a2-bf0a920afad8"
from tests.signature_support import signature_tools, admit_tools, sign_envelope
@pytest.fixture(autouse=True)
def admitted_native_signer(monkeypatch, tmp_path, signature_tools):
original = _Cfg.__init__
def configured(self, *a, **kw):
original(self,*a,**kw)
admit_tools(self,tmp_path,signature_tools)
monkeypatch.setattr(_Cfg,'__init__',configured)
original_decision=AuthorizationStub.decision
monkeypatch.setattr(AuthorizationStub,'decision',lambda self,request:sign_envelope(original_decision(self,request),signature_tools))
APPROVAL_ID = "8bfc20be-47a4-4fb0-97a2-bf0a920afad8"
class _Cfg:
@ -45,7 +57,7 @@ class _Cfg:
def _entry():
raw = copy.deepcopy(VALID)
raw["approval"] = dict(raw.get("approval") or {})
raw["approval"]["authorization_id"] = AUTH_ID
raw["approval"]["authorization_id"] = APPROVAL_ID
raw["approval"]["purpose"] = "contract-test"
return validate_entry(raw)
@ -76,7 +88,7 @@ def _served(entry=None, action="deactivate", fields=("api_token",), **over):
"schema_version": "0.1",
"kind": "approval-claim",
"issuer": "approval-engine",
"approval_id": AUTH_ID,
"approval_id": APPROVAL_ID,
"state": "valid",
"valid_now": True,
"consumed": False,
@ -116,7 +128,7 @@ def _opener(envelope, status=200):
def _resolve(cfg, entry, envelope, action="deactivate"):
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
pdp = AuthorizationStub(approval_id=APPROVAL_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
def check(request, timeout=None):
submitted = json.loads(request.data)
return _opener(pdp.decision(submitted))(request)
@ -136,7 +148,7 @@ def test_valid_authorization_yields_binding_with_canonical_digest(tmp_path):
cfg = _Cfg(_token(tmp_path))
binding = _resolve(cfg, entry, _served())
assert binding is not None
assert binding.binding.approval_id == AUTH_ID
assert binding.binding.approval_id == APPROVAL_ID
assert binding.binding.request_digest != request_digest(_expected_request(entry))

View file

@ -176,7 +176,7 @@ def test_the_guard_runs_before_the_token_is_read(tmp_path):
"""
missing = tmp_path / "never-read.token"
with pytest.raises(DecisionError, match="in-cluster Service name"):
check_decision(
check_decision(allow_unsigned_test_fixture=True,
base_url="http://flex-auth-secrets-engine.flex-auth.svc.cluster.local:8080",
token_file=missing,
request={},

View file

@ -0,0 +1,110 @@
"""Producer-signed native replay and real consumer boundary regression checks."""
import copy
import json
from pathlib import Path
import subprocess
from types import SimpleNamespace
import pytest
from secrets_engine import approval_consume
from secrets_engine.authorization import validate_decision_envelope
from secrets_engine.decision_check import check_decision
from secrets_engine.decision_signature import load_trust
from secrets_engine.errors import DecisionError
from tests.test_decision_transport import server, credential, clear_proxies
FIXTURE = Path(__file__).parent / 'fixtures/pdp-signature/decision_rotate_signed.json'
PUBLIC = 'IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'
from tests.signature_support import signature_tools
@pytest.fixture(scope='module')
def verifier(signature_tools):
return signature_tools/'verify'
@pytest.fixture
def trust_file(tmp_path):
envelope = json.loads(FIXTURE.read_bytes())
provenance = envelope['provenance']
trust = {k:provenance[k] for k in ('evaluator','mode','policy_package','policy_version','policy_package_digest')}
trust.update(schema='secrets-engine.pdp-trust.v1', keys={'testdata-ed25519':PUBLIC})
path = tmp_path/'trust.json'; path.write_text(json.dumps(trust)); return path
def checked(body, credential, trust_file, verifier):
with server(body=body) as (origin, received):
result = check_decision(base_url=origin, token_file=credential, request={},
trust_file=trust_file, verifier=verifier)
assert received == [True]
return result
def test_genuine_native_signature_at_consumer_boundary(credential, trust_file, verifier):
result = checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
assert result['effect']=='allow'
assert result['binding']==json.loads(FIXTURE.read_bytes())['binding']
@pytest.mark.parametrize('change', ['allow_forgery','tampered','unknown_key','unsigned','absent','algorithm','unknown_field','duplicate','null','case_alias'])
def test_response_authentication_refuses(credential, trust_file, verifier, change):
env=json.loads(FIXTURE.read_bytes())
if change=='allow_forgery': env['signature']['value']='A'*86
elif change=='tampered': env['binding']['action']='destroy'
elif change=='unknown_key': env['signature']['kid']='new-key'
elif change=='unsigned': env['signature']={'mode':'unsigned'}
elif change=='absent': del env['signature']
elif change=='algorithm': env['signature']['alg']='none'
elif change=='unknown_field': env['unbound_authority']='allow'
elif change=='null': env['binding']=None
elif change=='case_alias': env['Effect']='deny'
body=json.dumps(env).encode()
if change=='duplicate': body=body[:-1]+b',"effect":"deny"}'
with pytest.raises(DecisionError): checked(body,credential,trust_file,verifier)
@pytest.mark.parametrize('field',['evaluator','mode','policy_package','policy_version','policy_package_digest'])
def test_independent_issuer_and_pin_refusal(credential, trust_file, verifier, field):
trust=json.loads(trust_file.read_text()); trust[field]='wrong'; trust_file.write_text(json.dumps(trust))
with pytest.raises(DecisionError): checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
def test_rotation_overlap_then_removal(credential, trust_file, verifier, signature_tools):
trust=json.loads(trust_file.read_text()); trust['keys']['next-admitted']='Ivwpd5Lwtv_Av8_bftsMCqFOAlo2XsDjQuhuOCnLdLY'
trust_file.write_text(json.dumps(trust)); checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
from tests.signature_support import sign_envelope
next_body=json.dumps(sign_envelope(json.loads(FIXTURE.read_bytes()),signature_tools,next_key=True)).encode()
checked(next_body,credential,trust_file,verifier)
del trust['keys']['testdata-ed25519']; trust_file.write_text(json.dumps(trust))
with pytest.raises(DecisionError): checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
checked(next_body,credential,trust_file,verifier)
def test_missing_trust_precedes_credential_read(tmp_path):
with pytest.raises(DecisionError,match='trust'):
check_decision(base_url='http://127.0.0.1:1',token_file=tmp_path/'must-not-read',request={})
def test_production_authorization_has_no_unsigned_path(monkeypatch, credential, trust_file, verifier):
observation=SimpleNamespace(request={})
monkeypatch.setattr(approval_consume,'resolve_approval_observation',lambda *a,**kw:observation)
with server(body=b'{"effect":"allow"}') as (origin,received):
cfg=SimpleNamespace(pdp_url=origin,pdp_token_file=credential,pdp_trust_file=trust_file,pdp_verifier=verifier,
authorization_policy_package='package',authorization_policy_version='v1')
with pytest.raises(DecisionError,match='authentication'):
approval_consume.authorize_action(cfg,None,'rotate')
assert received == [True]
def test_missing_trust_precedes_approval_observation(monkeypatch):
def forbidden(*a,**kw): raise AssertionError('approval credential path reached')
monkeypatch.setattr(approval_consume,'resolve_approval_observation',forbidden)
with pytest.raises(DecisionError,match='trust'):
approval_consume.authorize_action(SimpleNamespace(pdp_url='http://127.0.0.1:1',pdp_token_file=Path('/must-not-read')),None,'rotate')
@pytest.mark.parametrize('body', [b'{"effect":NaN}',b'{"effect":Infinity}',b'['*2000+b'0'+b']'*2000,b'{"x":1,"x":2}',b'\xff'])
def test_malformed_body_refuses_as_decision_error(credential,trust_file,verifier,body):
with pytest.raises(DecisionError): checked(body,credential,trust_file,verifier)
def test_default_check_requires_authentication_before_credential_read(tmp_path):
with pytest.raises(DecisionError,match='trust'):
check_decision(base_url='http://127.0.0.1:1',token_file=tmp_path/'must-not-read',request={})

View file

@ -67,7 +67,7 @@ def test_redirect_never_contacts_another_origin(credential, status):
with server() as (destination, received):
with server(status=status, location=destination + '/sink') as (source, sent):
with pytest.raises(DecisionError):
check_decision(base_url=source, token_file=credential, request={})
check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={})
assert sent == [True]
assert received == []
@ -77,18 +77,22 @@ def test_environment_proxy_never_receives_request(credential, monkeypatch):
with server() as (source, direct):
monkeypatch.setenv('http_proxy', proxy)
monkeypatch.setenv('no_proxy', '')
assert check_decision(base_url=source, token_file=credential, request={}) == {}
assert check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={}) == {}
assert direct == [True]
assert proxied == []
def test_approval_flow_uses_the_same_safe_transport(credential, monkeypatch):
from tests.signature_support import signature_tools, admit_tools
def test_approval_flow_uses_the_same_safe_transport(credential, monkeypatch, signature_tools):
observation = SimpleNamespace(request={})
monkeypatch.setattr(approval_consume, 'resolve_approval_observation', lambda *a, **kw: observation)
with server() as (destination, received):
with server(status=302, location=destination + '/sink') as (source, sent):
cfg = SimpleNamespace(pdp_url=source, pdp_token_file=credential,
authorization_policy_package='test', authorization_policy_version='v1')
admit_tools(cfg, credential.parent, signature_tools)
with pytest.raises(DecisionError):
approval_consume.authorize_action(cfg, object(), 'rotate', None)
assert sent == [True]
@ -105,7 +109,7 @@ def test_approval_flow_uses_the_same_safe_transport(credential, monkeypatch):
])
def test_invalid_address_refused_before_credential_read(url, tmp_path):
with pytest.raises(DecisionError, match='URL'):
check_decision(base_url=url, token_file=tmp_path / 'missing', request={})
check_decision(allow_unsigned_test_fixture=True, base_url=url, token_file=tmp_path / 'missing', request={})
@pytest.mark.parametrize('body,reason', [
@ -115,4 +119,4 @@ def test_invalid_address_refused_before_credential_read(url, tmp_path):
def test_response_limits_and_encoding_fail_closed(credential, body, reason):
with server(body=body) as (source, _):
with pytest.raises(DecisionError, match=reason):
check_decision(base_url=source, token_file=credential, request={})
check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={})

View file

@ -12,7 +12,7 @@ from secrets_engine.catalog import validate_entry
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, catalog_target_digest
from tests.test_catalog import VALID
from tests.test_consume_binding_join import _Cfg, _token, _opener, AUTH_ID
from tests.test_consume_binding_join import _Cfg, _token, _opener, APPROVAL_ID
from tests.authorization_stub import AuthorizationStub
H = 'sha256:' + 'a' * 64
@ -21,7 +21,7 @@ H = 'sha256:' + 'a' * 64
def entry():
raw = copy.deepcopy(VALID)
raw['stage'] = 'prod'
raw['approval'] = {'model': 'bootstrap-only', 'authorization_id': AUTH_ID, 'purpose': 'metadata-test'}
raw['approval'] = {'model': 'bootstrap-only', 'authorization_id': APPROVAL_ID, 'purpose': 'metadata-test'}
return validate_entry(raw)
@ -59,7 +59,7 @@ def test_four_builders_preserve_context_and_binding(tmp_path):
expected = request(e, contract)
assert expected['context']['human_control'] is True
assert _expected_request(cfg,e,'execute-ccr',execution_contract=contract) == expected
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
pdp = AuthorizationStub(approval_id=APPROVAL_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
pdp.bind_request(expected); claim = served(pdp)
observed = resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=contract,opener=_opener(claim))
assert observed.request['context']['execution_contract'] == raw
@ -76,7 +76,7 @@ def test_four_builders_preserve_context_and_binding(tmp_path):
consume_bodies = []
def consume(req, timeout=None):
value = json.loads(req.data); consume_bodies.append(value)
return _Response(200, {'status':'consumed', 'approval_id':AUTH_ID,
return _Response(200, {'status':'consumed', 'approval_id':APPROVAL_ID,
'request_digest':value['request_digest'], 'decision_id':value['decision_id'],
'consumed_at':datetime.now(timezone.utc).isoformat(), 'idempotent':False})
confirmed = consume_approval(base_url=cfg.approval_url, token_file=cfg.approval_token_file,
@ -92,7 +92,7 @@ def test_four_builders_preserve_context_and_binding(tmp_path):
@pytest.mark.parametrize('change', ['operation_id','ccr','grant','source','body','method','path','budget','deadline','state','cluster'])
def test_changed_binding_cannot_use_original_claim(tmp_path,change):
e=entry(); original=descriptor(e); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp=AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp=AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,ExecutionContract.parse(original))); claim=served(pdp)
changed=copy.deepcopy(original)
if change=='operation_id': changed[change]='775ab8f4-4731-47c8-bd98-342af22885dd'
@ -164,7 +164,7 @@ def test_expired_contract_refuses_before_claim(tmp_path):
def test_request_correspondence_changes_refuse_before_authorization(tmp_path, changed):
e = entry(); contract = ExecutionContract.parse(descriptor(e))
cfg = _Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp = AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp = AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,contract)); claim = served(pdp)
if changed == 'subject': cfg.authorization_subject_id = 'foreign-service'
elif changed == 'purpose': e = replace(e,approval={**e.approval, 'purpose':'different-purpose'})
@ -190,8 +190,13 @@ def test_raw_dictionary_and_direct_invalid_type_cannot_bypass_validation():
def test_deadline_beyond_claim_refuses_before_pdp(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)+timedelta(hours=2)).isoformat()
contract=ExecutionContract.parse(raw); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp=AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp=AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,contract)); calls=[]
with pytest.raises(DecisionError,match='exceeds served authority'):
authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(served(pdp)),pdp_opener=lambda *a,**k:calls.append('check'))
assert not calls
# Reuse admitted native producer signing, never bypass responder verification.
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools

View file

@ -17,7 +17,7 @@ from secrets_engine.errors import CatalogError, DecisionError
from tests.authorization_stub import AuthorizationStub
from tests.test_catalog import VALID
from tests.test_consume_binding_join import (
AUTH_ID, _Cfg, _entry, _opener, _resolve, _served, _token,
APPROVAL_ID, _Cfg, _entry, _opener, _resolve, _served, _token,
)
from tests.test_exec_owner import bound
from tests.test_privileged_cli_evidence import _config, _records
@ -98,7 +98,7 @@ def test_consume_requires_observed_human_control_even_with_demo_enabled(tmp_path
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
with pytest.raises(DecisionError, match="human_control"):
require_production_consume(_config(tmp_path), entry,
binding=ConsumeBinding(AUTH_ID, "sha256:"+"a"*64),
binding=ConsumeBinding(APPROVAL_ID, "sha256:"+"a"*64),
opener=lambda *a, **k: pytest.fail("must not consume"))
@ -108,7 +108,7 @@ def test_declared_control_still_consumes_with_demo_enabled(tmp_path, monkeypatch
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
cfg = replace(_config(tmp_path), approval_url="https://approval.example",
approval_token_file=_token(tmp_path))
binding = ConsumeBinding(AUTH_ID, "sha256:"+"a"*64, human_control=True)
binding = ConsumeBinding(APPROVAL_ID, "sha256:"+"a"*64, human_control=True)
calls = []
def consume(request, **kwargs):
calls.append(request.full_url)
@ -116,8 +116,8 @@ def test_declared_control_still_consumes_with_demo_enabled(tmp_path, monkeypatch
response.getcode = lambda: 200
return response
result = require_production_consume(cfg, entry, binding=binding, opener=consume)
assert result.approval_id == AUTH_ID
assert calls == [f"https://approval.example/v1/approvals/{AUTH_ID}/consume"]
assert result.approval_id == APPROVAL_ID
assert calls == [f"https://approval.example/v1/approvals/{APPROVAL_ID}/consume"]
def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch):
@ -127,7 +127,7 @@ def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch):
claim["binding"]["human_control"] = True
monkeypatch.setattr(approval_consume, "fetch_approval_claim", lambda **k: claim)
cfg = _Cfg(_token(tmp_path))
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package,
pdp = AuthorizationStub(approval_id=APPROVAL_ID, package=cfg.authorization_policy_package,
version=cfg.authorization_policy_version)
original_validate = approval_consume.validate_decision_envelope
def invalidate_after_check(*args, **kwargs):
@ -145,7 +145,7 @@ def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch):
def test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child(bound, tmp_path, monkeypatch):
raw, _, _ = bound
raw["stage"] = "prod"
raw["approval"].update(model="decision", authorization_id=AUTH_ID,
raw["approval"].update(model="decision", authorization_id=APPROVAL_ID,
purpose="contract-test", human_control=True)
entry = validate_entry(raw)
cfg = replace(_config(tmp_path), approval_url="https://approval.example",
@ -169,3 +169,8 @@ def test_configured_factory_catalog_still_requires_human_control():
assert entry.approval["human_control"] is True
assert entry.delivery_config["exec_owner"]["status"] == "configured"
assert not entry.approval.get("authorization_id")
# Reuse admitted native producer signing, never bypass responder verification.
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools

View file

@ -234,3 +234,15 @@ def test_unconfigured_engine_still_fails_closed(bao_dev, lane, tmp_path):
)
with pytest.raises(DecisionError, match="live production remains disabled"):
cli._require_lane_approval(cfg, entry, "apply")
from tests.signature_support import signature_tools, admit_tools, sign_envelope
@pytest.fixture(autouse=True)
def admitted_native_signer(monkeypatch,signature_tools):
original_cfg=_cfg
def configured(tmp_path,*args,**kwargs):
return admit_tools(original_cfg(tmp_path,*args,**kwargs),tmp_path,signature_tools)
monkeypatch.setattr(__import__(__name__,fromlist=['_cfg']),'_cfg',configured)
original_decision=AuthorizationStub.decision
monkeypatch.setattr(AuthorizationStub,'decision',lambda self,request:sign_envelope(original_decision(self,request),signature_tools))

View file

@ -0,0 +1,151 @@
# Target Revenue Source License
**Version 1.0, Candidate 1 (V1C1)**
---
> **PRELIMINARY CANDIDATE — SUBJECT TO CHANGE — NOT FINAL**
>
> This repository is governed by the Target Revenue Source License
> (TRSL), Version 1, Candidate 1. This is the framework's first working
> candidate, adopted as the org's preliminary operating license across
> all repos (maintainer decision, 2026-07-29) during the build/alpha
> stage — see `SCOPE.md` §1 and Appendix A of the canonical text (link
> below) for the alpha/beta risk-acceptance decision this reflects and
> what it does and does not mean. Full specialist legal review is
> explicitly deferred until the framework moves out of beta.
>
> **Canonical source, full candidate-status banner, and Appendix A
> (non-normative candidate notes tracking every open item):** the
> `coulomb` org's `target-revenue` repository,
> `specs/TargetRevenueSourceLicense-V1C1.md` — this file is the operative
> legal text (Preamble through Section 11) only; the canonical document
> is authoritative if this copy and that document ever diverge.
---
## Preamble
This Target Revenue Source License ("**License**") governs the Software identified in the applicable Phase Manifest. It implements the Target Revenue Framework: a defined development Phase accumulates Development Credit and Remission Credit against an immutable Initial Target until the Milestone Release automatically and irrevocably converts to a declared permissive Future License.
Commercial beneficiaries fund the creation and early availability of a software improvement; once the declared target is satisfied, the governed release becomes permissively open source.
## 1. Definitions
Capitalized terms used in this License have the meanings given below. Where a term is also defined in the Phase Manifest or Target Ledger for a specific Phase, the Phase Manifest and Target Ledger govern the *values* (amounts, dates, identifiers) and this License governs the *legal effect* of those values — the two must not be read as conflicting definitions of the same concept.
**"Commercial Entitlement"** means a right, purchased or otherwise granted under a Commercial Use Agreement, to make Commercial Use of the Software during a Phase.
**"Commercial Use"** means billing, invoicing, or otherwise charging any customer a fee, subscription, license fee, or other consideration for or in connection with use of the Software, at any time before the Conversion Event for the applicable Phase, regardless of whether the person or organization billed would otherwise qualify for Noncommercial Use. Commercial Use occurs by virtue of such billing alone, whether or not the resulting payment is registered with the Trust Service; in particular, billing a customer for pre-conversion use of the Software without recording the corresponding payment in the applicable Phase's Target Ledger is Commercial Use without a valid Commercial Entitlement — a violation of Section 3, addressed under Section 7 and, where applicable, the Enforcement Network described in the canonical repository's `specs/EnforcementNetworkConcept.md`.
**"Commercial Use Agreement"** means the separate agreement, referenced by the applicable Phase Manifest, under which a Commercial Entitlement is purchased or granted. This License does not itself set pricing, metering, or payment terms — those are governed by the Commercial Use Agreement.
**"Conversion Event"** means the moment the Outstanding Target for a Phase reaches zero, as computed from the Phase Manifest and Target Ledger per the Target Ledger Specification. The Conversion Event occurs automatically and is not conditioned on any declaration, attestation, or other act by the Licensor or any Trust Service.
**"Development Credit"** means the portion of a collected and settled payment explicitly allocated toward satisfying the Initial Target of a specific Phase, as recorded in that Phase's Target Ledger.
**"Future License"** means the permissive license identified in the applicable Phase Manifest, being either the MIT License or the Apache License, Version 2.0, which applies to the Milestone Release upon the Conversion Event.
**"Initial Target"** means the immutable monetary target declared for a Phase in its Phase Manifest.
**"Licensor"** means **Binky Hedgehog GmbH**, the party that publishes the Phase Manifest and holds the rights necessary to grant this License and the Future License for the Milestone Release.
**"Milestone Release"** means the precisely identified software release designated in the applicable Phase Manifest, identified by an immutable source revision, release artifact, or cryptographic digest.
**"Noncommercial Use"** means use of the Software for personal purposes, private study, hobby or amateur projects; use by any charitable organization, educational institution, public research organization, or government institution acting in a non-revenue-generating capacity; or other use of a materially similar character.
**"Outstanding Target"** means, at any time, `max(0, Initial Target − cumulative Development Credit − cumulative Remission Credit)` for a Phase, as computed from that Phase's Target Ledger.
**"Phase"** means a bounded development undertaking governed by one Initial Target, one Milestone Release, one degeneration policy, and one Future License declaration, as declared in a Phase Manifest.
**"Phase Manifest"** means the published, immutable declaration identifying a Phase, its Milestone Release, Initial Target, Future License, degeneration policy, and Target Ledger location, as specified in the Phase Manifest Specification.
**"Remission Credit"** means a transparent, non-revenue reduction of a Phase's Outstanding Target, generated under that Phase's published degeneration policy and recorded in the Target Ledger.
**"Settled Payment"** means a payment that has cleared through its payment processor and is no longer subject to reversal in the ordinary course (chargeback, dispute, or equivalent), as further specified by the applicable Commercial Use Agreement or monetization extension.
**"Software"** means the source code, object code, and associated documentation of the Milestone Release identified in the applicable Phase Manifest.
**"Target Ledger"** means the append-only record of Development Credit, Remission Credit, and correction entries for a Phase, as specified in the Target Ledger Specification.
**"You"** or **"Licensee"** means the individual or entity exercising rights under this License.
## 2. Grant of Rights for Noncommercial Use
Subject to the terms of this License, the Licensor grants You a worldwide, royalty-free, non-exclusive license, during the applicable Phase, to:
(a) use, reproduce, and study the Software for any Noncommercial Use;
(b) modify the Software and create derivative works of it for any Noncommercial Use; and
(c) redistribute the Software and Your modifications, in source or object form, for any Noncommercial Use, provided that You include this License, unmodified, with any such redistribution, and that You do not remove or alter any copyright, patent, trademark, or attribution notices contained in the Software.
This grant does not extend to Commercial Use. Commercial Use requires a Commercial Entitlement under Section 3.
## 3. Commercial Use
You may not make Commercial Use of the Software during the applicable Phase unless You hold a valid, current Commercial Entitlement under a Commercial Use Agreement with the Licensor covering the applicable Phase. A Commercial Entitlement granted under one Phase's Commercial Use Agreement does not extend to a later Phase's Milestone Release unless the Commercial Use Agreement expressly says so.
This Section 3 states the existence and boundary of the commercial-use restriction. It does not itself set pricing, invoicing, metering, audit rights, or payment terms — those are governed exclusively by the applicable Commercial Use Agreement.
## 4. Patent License
Subject to the terms of this License, each contributor to the Software grants You, during the applicable Phase and solely to the extent of rights granted under Sections 2 and 3, a perpetual (subject to the termination below), worldwide, non-exclusive, no-charge, royalty-free patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Software, limited to those patent claims licensable by that contributor that are necessarily infringed by their contribution(s) alone or by combination of their contribution(s) with the Software.
If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Software or a contribution incorporated within it constitutes direct or contributory patent infringement, then any patent licenses granted to You under this Section 4 for the Software shall terminate as of the date such litigation is filed.
## 5. Automatic Conversion to the Future License
**5.1 Automatic effect.** Upon the Conversion Event for a Phase, the rights and restrictions in Sections 3 (Commercial Use) of this License, as they apply to that Phase's Milestone Release, terminate automatically. In their place, the Milestone Release is licensed under the Future License identified in that Phase's Phase Manifest, effective as of the Conversion Event, without any further act, declaration, or attestation required by the Licensor, any Trust Service, or any other party.
**5.2 Irrevocability.** Once a valid Conversion Event has occurred for a Phase, no subsequent refund, chargeback, accounting correction, dispute, or termination of this License for an unrelated breach shall revoke, suspend, or otherwise impair the Future License grant for that Phase's Milestone Release. Any shortfall or dispute arising after a Conversion Event is a commercial or accounting matter between the relevant parties and does not reinstate a commercial-use restriction over already-converted Software.
**5.3 Prior freedom preserved.** A later Phase covering subsequent improvements to the Software does not restrict, withdraw, or otherwise affect the rights granted under the Future License for an earlier Phase's already-converted Milestone Release.
**5.4 Evidence, not cause.** A Trust Service may publish a Conversion Attestation documenting a Conversion Event. Such an attestation is evidence that the Conversion Event occurred; it is not a condition of, and its absence or delay does not postpone, the automatic effect described in Section 5.1. Any person may independently verify whether a Conversion Event has occurred directly from the Phase Manifest and Target Ledger.
## 6. Successive Phases
The Licensor may declare a new Phase covering subsequent improvements to the Software following a Milestone Release's Conversion Event. Each Phase is independently governed by its own Phase Manifest, Initial Target, degeneration policy, and Target Ledger. Nothing in a later Phase's Phase Manifest may be construed to reduce or withdraw rights already granted under Section 5 for an earlier Phase's Milestone Release.
## 7. Term and Termination
**7.1 Term.** This License applies to the Software for the duration of the applicable Phase, and, for the Milestone Release, indefinitely following that Phase's Conversion Event under the Future License.
**7.2 Termination for breach.** If You breach Section 3 (Commercial Use) or Section 2(c) (redistribution notice requirement), the Licensor may terminate this License as to You. Before such termination becomes effective, the Licensor shall provide You written notice of the breach; if You cure the breach within thirty (30) days of that notice, this License continues in effect. A second breach of the same provision within twelve (12) months may be terminated immediately without a further cure opportunity.
**7.3 Effect of termination.** Termination under this Section 7 affects only Your rights under Sections 2 and 3 for the Phase in which the breach occurred. It does not affect any rights already vested under Section 5 (Automatic Conversion) for a Milestone Release whose Conversion Event has already occurred, per Section 5.2.
**7.4 Public record of breach and resolution.** The Licensor shall cause the Trust Service to publish, as part of the public record for the affected Phase, notice of: (a) any breach notice issued under Section 7.2, stating the general nature of the breach and the date of notice; (b) whether the breach was cured within the applicable cure period, and the date of cure; and (c) any termination determination made under this Section 7, including its effective date and scope. This public record exists to give the ecosystem a transparent, verifiable conformity signal for the Phase, distinct from and in addition to the Development Credit and Remission Credit facts already published under Section 5.4 and the Target Ledger Specification.
A breach that You dispute, and that has not been finally determined, shall be recorded as **alleged**; it shall be recorded as **determined** only once the cure period has run without cure, or the dispute has been resolved against You under the applicable Commercial Use Agreement's dispute process, if any. The Trust Service shall update the record promptly upon resolution in either direction. Recording an alleged or determined breach under this Section 7.4 is a ministerial act of publishing the Licensor's determination (or a dispute process's outcome); it does not give the Trust Service discretionary authority to decide whether a breach occurred, consistent with Section 5.4's evidence-not-cause principle.
Whether, and under what conditions, the public record identifies a Commercial Entitlement holder by name is governed exclusively by the applicable Commercial Use Agreement, which the Licensor and that Commercial Entitlement holder negotiate and agree to directly. This License does not itself set a naming default. Where no Commercial Use Agreement addresses the question, or where the affected party has no Commercial Use Agreement at all (for example, a Section 2(c) breach by a Noncommercial Use licensee), the public record states the Phase and breach category only, without naming the party.
## 8. Disclaimer of Warranty
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE LICENSOR DOES NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR THAT ANY PHASE WILL REACH ITS CONVERSION EVENT.
## 9. Limitation of Liability
IN NO EVENT SHALL THE LICENSOR OR ANY CONTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE, EXCEPT TO THE EXTENT SUCH LIMITATION IS PROHIBITED BY APPLICABLE LAW.
## 10. Trademarks
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary attribution.
## 11. General Provisions
**11.1 Governing law and venue.** Adopted for alpha/beta 2026-07-29: disputes arising under this License shall be resolved by binding arbitration, seated at a neutral, arbitration-mature venue (Singapore or London are the two candidates concretely supported by current research), rather than by litigation in a national court. The specific arbitral institution and substantive governing law remain a per-deployment blank pending final selection; they are not fixed by this candidate. See the canonical repository's `history/260729-TRSL-Jurisdiction-Synthesis.md` §2.
**11.2 Severability.** If any provision of this License is held unenforceable, the remaining provisions remain in full force, and the unenforceable provision shall be reformed to the minimum extent necessary to make it enforceable.
**11.3 No waiver.** Failure to enforce any provision of this License is not a waiver of future enforcement of that or any other provision.
**11.4 Entire agreement (as to licensing).** This License, together with the applicable Phase Manifest and, where applicable, the Commercial Use Agreement, constitutes the entire agreement between You and the Licensor regarding the Software's licensing terms. Operations, service, and consulting arrangements are governed by separate agreements, if any, and are not part of this License.
**11.5 Definitions control.** Marketing materials, documentation, or other non-normative communications about the Software must not describe pre-Conversion-Event Software as "Open Source," "free software," or "open core." Pre-conversion Noncommercial Use is **source-available**; pre-conversion Commercial Use requires a **Commercial Entitlement**; only post-conversion Software may be described as Open Source, under the Future License.
---
**No Phase is currently declared for this repository under this License.** Until a Phase Manifest is published and registered with the Trust Service for a Milestone Release in this repository, Sections 2–7 above have no operative subject matter here — this License establishes the governing framework in advance of that declaration, consistent with the org-wide rollout decision recorded in `target-revenue`'s `workplans/TREV-WP-0008-governance-and-pilot-rollout.md`.

30
verifier/README.md Normal file
View file

@ -0,0 +1,30 @@
# Access Engine decision verifier
The API types and `internal/sign/sign.go` are unchanged producer source, pinned
by revision and SHA-256 in `producer-source.json`. License attribution and the
original license text are in `LICENSE.access-engine`. This is source vendoring
necessitated by the producer's Go `internal` package boundary, not a separately
maintained crypto implementation. There are no external Go dependencies.
Build in this directory: `go build -trimpath -o /tmp/secrets-engine-pdp-verify .`.
Install that binary through the deployment's reviewed package/image path and set
`SECRETS_ENGINE_PDP_VERIFIER` to its absolute path. Do not compile at request time.
The Python wheel alone is insufficient: deployments must install this helper.
The `testsupport/signer` executable is test-only and must not be installed.
From repository root, run `python3 scripts/check_pdp_producer_source.py` before
building, and optionally add `--upstream /path/to/access-engine` to detect producer
drift. To update: select/review an upstream revision, copy these exact source
files and native replay fixtures, update the manifest, run both upstream signature
checks and downstream consumer tests, then review the complete diff. Never change
canonicalization locally. Producer contract additions fail closed until reviewed.
The helper reads the original JSON envelope, verifies using Go's producer verifier,
and returns only the authenticated typed envelope. The Python boundary rejects
duplicate keys, nonfinite values and invalid UTF-8 before invoking it. Unknown
typed fields refuse. The unsigned producer mode is never a production fallback.
The Check API defaults to mandatory authentication. Its deliberately named
`allow_unsigned_test_fixture` argument is used only by synthetic transport tests;
production `authorize_action` never passes it. The helper runs with an empty
environment, so ambient backend and identity credentials cannot reach it.

3
verifier/go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/netkingdom/access-engine
go 1.26.7

View file

@ -0,0 +1,76 @@
// Package sign attaches a detached Ed25519 signature to a decision envelope
// (FLEX-WP-0024). The signed material is json.Marshal of the envelope with
// the signature field omitted.
package sign
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/netkingdom/access-engine/pkg/api"
)
const AlgorithmEd25519 = "ed25519"
// CanonicalBytes is the exact payload that is signed and verified: the
// envelope as emitted, with signature absent.
func CanonicalBytes(envelope api.DecisionEnvelope) ([]byte, error) {
envelope.Signature = nil
data, err := json.Marshal(envelope)
if err != nil {
return nil, fmt.Errorf("marshal canonical envelope: %w", err)
}
return data, nil
}
func Sign(envelope *api.DecisionEnvelope, private ed25519.PrivateKey, kid string) error {
if envelope == nil {
return fmt.Errorf("envelope is required")
}
if len(private) != ed25519.PrivateKeySize {
return fmt.Errorf("ed25519 private key has length %d", len(private))
}
if kid == "" {
return fmt.Errorf("kid is required")
}
payload, err := CanonicalBytes(*envelope)
if err != nil {
return err
}
sig := ed25519.Sign(private, payload)
envelope.Signature = &api.EnvelopeSignature{
Mode: api.EnvelopeSignatureSigned,
Alg: AlgorithmEd25519,
Kid: kid,
Value: base64.RawURLEncoding.EncodeToString(sig),
}
return nil
}
func Verify(envelope api.DecisionEnvelope, keys map[string]ed25519.PublicKey) error {
sig := envelope.Signature
if sig == nil || sig.Mode != api.EnvelopeSignatureSigned {
return fmt.Errorf("envelope is not signed")
}
if sig.Alg != AlgorithmEd25519 {
return fmt.Errorf("unsupported signature alg %q", sig.Alg)
}
pub, ok := keys[sig.Kid]
if !ok {
return fmt.Errorf("unknown signature kid %q", sig.Kid)
}
raw, err := base64.RawURLEncoding.DecodeString(sig.Value)
if err != nil {
return fmt.Errorf("decode signature: %w", err)
}
payload, err := CanonicalBytes(envelope)
if err != nil {
return err
}
if !ed25519.Verify(pub, payload, raw) {
return fmt.Errorf("signature does not match envelope")
}
return nil
}

52
verifier/main.go Normal file
View file

@ -0,0 +1,52 @@
// Decision verification uses the unchanged Access Engine producer implementation.
package main
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/pkg/api"
"io"
"os"
)
type input struct {
Envelope json.RawMessage `json:"envelope"`
Keys map[string]string `json:"keys"`
}
func run() error {
var in input
if err := json.NewDecoder(io.LimitReader(os.Stdin, 1048577)).Decode(&in); err != nil {
return err
}
var envelope api.DecisionEnvelope
decoder := json.NewDecoder(bytes.NewReader(in.Envelope))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&envelope); err != nil {
return err
}
keys := map[string]ed25519.PublicKey{}
for kid, encoded := range in.Keys {
raw, err := base64.RawURLEncoding.Strict().DecodeString(encoded)
if err != nil || len(raw) != ed25519.PublicKeySize || kid == "" {
return fmt.Errorf("invalid admitted key")
}
keys[kid] = ed25519.PublicKey(raw)
}
if err := sign.Verify(envelope, keys); err != nil {
return err
}
// Return the exact typed interpretation authenticated by the producer; Python
// must never act on a second interpretation of unverified response fields.
return json.NewEncoder(os.Stdout).Encode(envelope)
}
func main() {
if run() != nil {
fmt.Fprintln(os.Stderr, "decision signature verification refused")
os.Exit(1)
}
}

View file

@ -0,0 +1,685 @@
package api
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
)
// ProtectedSystemManifest describes a system that delegates authorization to
// flex-auth.
type ProtectedSystemManifest struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
ClientBinding *ClientBinding `json:"client_binding,omitempty" yaml:"client_binding,omitempty"`
ResourceTypes []ResourceType `json:"resource_types,omitempty" yaml:"resource_types,omitempty"`
Actions []ActionDefinition `json:"actions,omitempty" yaml:"actions,omitempty"`
CaringProfiles []string `json:"caring_profiles,omitempty" yaml:"caring_profiles,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// ClientBinding names the key-cape client a protected system's users sign in
// through. It is descriptive only: the readback registry projection lists it,
// and it never decides an allow (FLEX-DEC-2026-019).
type ClientBinding struct {
Issuer string `json:"issuer" yaml:"issuer"`
ClientID string `json:"client_id" yaml:"client_id"`
}
// ResourceType describes a resource namespace entry owned by a protected system.
type ResourceType struct {
Name string `json:"name" yaml:"name"`
ParentTypes []string `json:"parent_types,omitempty" yaml:"parent_types,omitempty"`
ScopeLevel ScopeLevel `json:"scope_level,omitempty" yaml:"scope_level,omitempty"`
Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// ActionDefinition maps a protected-system action to CARING capabilities.
type ActionDefinition struct {
Name string `json:"name" yaml:"name"`
Capabilities []Capability `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// SubjectManifest declares subjects, groups, teams, and tenants for local
// registry loading.
type SubjectManifest struct {
ID string `json:"id" yaml:"id"`
Subjects []Subject `json:"subjects,omitempty" yaml:"subjects,omitempty"`
Groups []Group `json:"groups,omitempty" yaml:"groups,omitempty"`
Teams []Team `json:"teams,omitempty" yaml:"teams,omitempty"`
Tenants []Tenant `json:"tenants,omitempty" yaml:"tenants,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Subject is a human, service, automation, agent, or other acting identity.
type Subject struct {
ID string `json:"id" yaml:"id"`
Type SubjectType `json:"type" yaml:"type"`
DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"`
OrganizationRelation OrganizationRelation `json:"organization_relation,omitempty" yaml:"organization_relation,omitempty"`
Roles []CanonicalRole `json:"roles,omitempty" yaml:"roles,omitempty"`
Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Claims map[string]any `json:"claims,omitempty" yaml:"claims,omitempty"`
CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Group is an assignment convenience, not a canonical role.
type Group struct {
ID string `json:"id" yaml:"id"`
DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"`
Members []string `json:"members,omitempty" yaml:"members,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Team is a group-like ownership unit used by protected systems.
type Team struct {
ID string `json:"id" yaml:"id"`
DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"`
Members []string `json:"members,omitempty" yaml:"members,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Tenant is a structural isolation boundary.
type Tenant struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// RelationshipFact records a relation between subjects, groups, teams, tenants,
// and resources.
type RelationshipFact struct {
ID string `json:"id" yaml:"id"`
System string `json:"system,omitempty" yaml:"system,omitempty"`
Subject string `json:"subject" yaml:"subject"`
Relation string `json:"relation" yaml:"relation"`
Object string `json:"object" yaml:"object"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Conditions []Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"`
Caring *CaringAccessDescriptor `json:"caring,omitempty" yaml:"caring,omitempty"`
Provenance map[string]any `json:"provenance,omitempty" yaml:"provenance,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// PolicyPackageMetadata is the frontmatter contract for Rego-in-Markdown
// policy packages.
type PolicyPackageMetadata struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
Version string `json:"version" yaml:"version"`
Status string `json:"status,omitempty" yaml:"status,omitempty"`
Package string `json:"package" yaml:"package"`
Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"`
Owner string `json:"owner,omitempty" yaml:"owner,omitempty"`
Fixtures []string `json:"fixtures,omitempty" yaml:"fixtures,omitempty"`
Caring CaringPolicyMetadata `json:"caring" yaml:"caring"`
Activation map[string]any `json:"activation,omitempty" yaml:"activation,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
// AllowTTL is a Go duration (for example "15m") that bounds every allow
// this package produces. Omit to use DefaultAllowTTL. "none" or "0s"
// means no stated end; the engine denies those allows (§9.7.1).
AllowTTL string `json:"allow_ttl,omitempty" yaml:"allow_ttl,omitempty"`
}
// CaringPolicyMetadata declares the CARING envelope a policy governs.
type CaringPolicyMetadata struct {
Profile string `json:"profile" yaml:"profile"`
Enforce bool `json:"enforce,omitempty" yaml:"enforce,omitempty"`
CanonicalRoles []CanonicalRole `json:"canonical_roles,omitempty" yaml:"canonical_roles,omitempty"`
OrganizationRelations []OrganizationRelation `json:"organization_relations,omitempty" yaml:"organization_relations,omitempty"`
Scopes []CaringScope `json:"scopes,omitempty" yaml:"scopes,omitempty"`
Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"`
Capabilities []Capability `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
Conditions []Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"`
Restrictions []Restriction `json:"restrictions,omitempty" yaml:"restrictions,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// PolicyFixture binds a check request to an expected decision.
type PolicyFixture struct {
ID string `json:"id" yaml:"id"`
Request CheckRequest `json:"request" yaml:"request"`
Expect DecisionExpectation `json:"expect" yaml:"expect"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// DecisionExpectation is the compact fixture expectation for policy tests.
type DecisionExpectation struct {
Effect DecisionEffect `json:"effect" yaml:"effect"`
Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
Obligations []Obligation `json:"obligations,omitempty" yaml:"obligations,omitempty"`
ConformanceFindings []CaringConformanceFinding `json:"conformance_findings,omitempty" yaml:"conformance_findings,omitempty"`
}
// CheckRequest is the stable protected-system-facing decision request.
type CheckRequest struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Action string `json:"action" yaml:"action"`
Resource ResourceRef `json:"resource" yaml:"resource"`
Context map[string]any `json:"context,omitempty" yaml:"context,omitempty"`
CaringContext *CaringAccessDescriptor `json:"caring_context,omitempty" yaml:"caring_context,omitempty"`
PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
// Purpose is empty on every enforcement request. The readback pin sets it
// to PurposeReadback, and it is hashed into every request digest, so an
// advisory readback decision can never pass a PEP's §6.4.2 replay test for
// an enforcement request (FLEX-DEC-2026-019). /v1/check and /v1/batch_check
// refuse a request that carries it.
Purpose string `json:"purpose,omitempty" yaml:"purpose,omitempty"`
}
// PurposeReadback marks an advisory application-access readback decision.
const PurposeReadback = "readback"
// BatchCheckRequest evaluates one subject/action against multiple resources.
type BatchCheckRequest struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Action string `json:"action" yaml:"action"`
Resources []ResourceRef `json:"resources" yaml:"resources"`
Context map[string]any `json:"context,omitempty" yaml:"context,omitempty"`
PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
}
// SubjectRef is a normalized subject reference in request and decision shapes.
type SubjectRef struct {
ID string `json:"id" yaml:"id"`
Type SubjectType `json:"type,omitempty" yaml:"type,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"`
}
// ResourceRef is a normalized resource reference in request and decision shapes.
type ResourceRef struct {
ID string `json:"id" yaml:"id"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
System string `json:"system,omitempty" yaml:"system,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"`
}
// DecisionEffect is the stable decision outcome vocabulary.
type DecisionEffect string
const (
DecisionEffectAllow DecisionEffect = "allow"
DecisionEffectDeny DecisionEffect = "deny"
DecisionEffectRedact DecisionEffect = "redact"
DecisionEffectAuditOnly DecisionEffect = "audit_only"
DecisionEffectNotApplicable DecisionEffect = "not_applicable"
)
// DecisionEffects returns the complete effect vocabulary. Every effect is an
// emission class in cadence.yaml; a test fails if a constant above is missing
// here, so a new effect cannot ship unclassified (FLEX-WP-0031-T03).
func DecisionEffects() []DecisionEffect {
return []DecisionEffect{
DecisionEffectAllow,
DecisionEffectDeny,
DecisionEffectRedact,
DecisionEffectAuditOnly,
DecisionEffectNotApplicable,
}
}
// Restricts reports whether an effect withholds authority. This classification
// does not exempt an effect from required durable emission (GH-DEC-2026-024).
func (e DecisionEffect) Restricts() bool {
switch e {
case DecisionEffectDeny, DecisionEffectRedact, DecisionEffectNotApplicable:
return true
}
return false
}
// DecisionRecordContractV1 is the published decision-record contract identifier.
const DecisionRecordContractV1 = "flex-auth.decision-record.v1"
// DecisionEnvelope is the stable response produced by standalone and delegated
// evaluators. It is flex-auth's published decision-record contract (§17).
type DecisionEnvelope struct {
ID string `json:"id" yaml:"id"`
ContractVersion string `json:"contract_version,omitempty" yaml:"contract_version,omitempty"`
RequestID string `json:"request_id,omitempty" yaml:"request_id,omitempty"`
Effect DecisionEffect `json:"effect" yaml:"effect"`
Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
MatchedPolicyVersion string `json:"matched_policy_version,omitempty" yaml:"matched_policy_version,omitempty"`
MatchedRule string `json:"matched_rule,omitempty" yaml:"matched_rule,omitempty"`
Resource ResourceRef `json:"resource" yaml:"resource"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Binding *DecisionBinding `json:"binding,omitempty" yaml:"binding,omitempty"`
Lifetime *DecisionLifetime `json:"lifetime,omitempty" yaml:"lifetime,omitempty"`
Obligations []Obligation `json:"obligations,omitempty" yaml:"obligations,omitempty"`
Diagnostics map[string]any `json:"diagnostics,omitempty" yaml:"diagnostics,omitempty"`
Provenance DecisionProvenance `json:"provenance" yaml:"provenance"`
Signature *EnvelopeSignature `json:"signature,omitempty" yaml:"signature,omitempty"`
Caring *CaringDecisionMetadata `json:"caring,omitempty" yaml:"caring,omitempty"`
}
// EnvelopeSignatureMode is how the responder authenticates the envelope.
type EnvelopeSignatureMode string
const (
EnvelopeSignatureSigned EnvelopeSignatureMode = "signed"
EnvelopeSignatureUnsigned EnvelopeSignatureMode = "unsigned"
)
// EnvelopeSignature is a detached signature over the canonical envelope with
// this field omitted (FLEX-WP-0024). It is not decision material and is not
// part of request_digest.
type EnvelopeSignature struct {
Mode EnvelopeSignatureMode `json:"mode" yaml:"mode"`
Alg string `json:"alg,omitempty" yaml:"alg,omitempty"`
Kid string `json:"kid,omitempty" yaml:"kid,omitempty"`
Value string `json:"value,omitempty" yaml:"value,omitempty"`
}
func UnsignedEnvelopeSignature() *EnvelopeSignature {
return &EnvelopeSignature{Mode: EnvelopeSignatureUnsigned}
}
// DecisionLifetimeKind identifies how an allow ends.
type DecisionLifetimeKind string
const (
DecisionLifetimeTTL DecisionLifetimeKind = "ttl"
)
// DefaultAllowTTL is the engine default when a policy package omits allow_ttl.
const DefaultAllowTTL = 15 * time.Minute
// ReasonAllowLifetimeUnstated is the deny reason for an allow with no stated end.
const ReasonAllowLifetimeUnstated = "allow_lifetime_unstated"
// DecisionLifetime bounds an allow (§9.7.1). flex-auth has no session concept,
// so the first honest shape is a policy-package-declared TTL.
type DecisionLifetime struct {
Kind DecisionLifetimeKind `json:"kind" yaml:"kind"`
TTL string `json:"ttl,omitempty" yaml:"ttl,omitempty"`
NotBefore string `json:"not_before,omitempty" yaml:"not_before,omitempty"`
ExpiresAt string `json:"expires_at" yaml:"expires_at"`
}
// DecisionBinding is the exact normalized authorization request evaluated by
// a decision. It lets a consumer verify structured action, target, actor, and
// context fields without parsing reason or diagnostic prose.
type DecisionBinding struct {
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Action string `json:"action" yaml:"action"`
Resource ResourceRef `json:"resource" yaml:"resource"`
Context map[string]any `json:"context,omitempty" yaml:"context,omitempty"`
// Purpose is "readback" on an advisory readback decision and absent on an
// enforcement decision (FLEX-DEC-2026-019).
Purpose string `json:"purpose,omitempty" yaml:"purpose,omitempty"`
RequestDigest string `json:"request_digest" yaml:"request_digest"`
// ApprovalBindingDigest is RequestDigest computed with the approval claim
// removed from context. It exists to break a circularity: an approval's
// pdp_digest is recorded at issue time, but the request that later carries
// the claim inside its hashed context has a different RequestDigest by
// construction, so a claim can never name the request that carries it.
//
// This digest is stable across attaching the claim, so it is the value
// claim.binding.pdp_digest must equal (GH-DEC-2026-008). It is NOT a replay
// identity and must not be used as one: two requests differing only in
// which approval was presented share it, and the decision does not.
ApprovalBindingDigest string `json:"approval_binding_digest,omitempty" yaml:"approval_binding_digest,omitempty"`
// SubmittedRequestDigest is RequestDigest over the request exactly as the
// consumer sent it, before the evaluator overlays registry facts onto the
// subject and resource.
//
// It exists because RequestDigest is not consumer-computable and was
// published as though it were. The evaluator hashes the ENRICHED request —
// subject.tenant, subject.attributes, and resource attributes the registry
// contributed — and the registry is flex-auth's, so a consumer recomputing
// the digest over what it sent gets a different value on every real allow.
// secrets-engine found this on its first live request (FLEX-DEC-2026-012).
//
// This is the digest a PEP compares for §6.4 obligation 2's replay test.
// Nothing is lost by hashing the pre-enrichment form: enrichment is a
// function of the request and the registry snapshot, and
// provenance.registry_snapshot_digest already pins the snapshot, so
// SubmittedRequestDigest together with that digest identifies the evaluated
// request completely.
SubmittedRequestDigest string `json:"submitted_request_digest,omitempty" yaml:"submitted_request_digest,omitempty"`
}
// ApprovalContextKey is the context key carrying an approval-engine
// approval-claim. It is excluded from ApprovalBindingDigest and from nothing
// else.
const ApprovalContextKey = "approval"
// requestDigestMaterial is the exact tuple hashed for §6.4.2 replay. Request
// id, policy version, and caring_context are excluded: id is correlation, the
// version is provenance, and caring_context is an input-claim digest.
type requestDigestMaterial struct {
Tenant string `json:"tenant,omitempty"`
Subject SubjectRef `json:"subject"`
Action string `json:"action"`
Resource ResourceRef `json:"resource"`
Context map[string]any `json:"context,omitempty"`
Purpose string `json:"purpose,omitempty"`
}
// NewDecisionBinding returns a stable structured binding for the exact request
// an evaluator consumed.
//
// Prefer NewDecisionBindingFor, which also records the digest of the request as
// submitted. This form leaves SubmittedRequestDigest empty, which reads as "the
// evaluator did not record one" rather than "the two are equal".
func NewDecisionBinding(request CheckRequest) *DecisionBinding {
contextCopy := make(map[string]any, len(request.Context))
for key, value := range request.Context {
contextCopy[key] = value
}
binding := &DecisionBinding{
Tenant: request.Tenant,
Subject: request.Subject,
Action: request.Action,
Resource: request.Resource,
Context: contextCopy,
Purpose: request.Purpose,
RequestDigest: RequestDigest(request),
}
if _, carries := request.Context[ApprovalContextKey]; carries {
binding.ApprovalBindingDigest = ApprovalBindingDigest(request)
}
return binding
}
// NewDecisionBindingFor records the binding of the evaluated request together
// with the digest of the request as submitted.
//
// The two arguments are the same request before and after the evaluator
// overlaid registry facts. Where a request named nothing the registry knows they
// are equal, and the field is emitted anyway: a consumer that only sees it on
// enriched decisions would build a check that passes by absence.
func NewDecisionBindingFor(evaluated, submitted CheckRequest) *DecisionBinding {
binding := NewDecisionBinding(evaluated)
binding.SubmittedRequestDigest = RequestDigest(submitted)
return binding
}
// ApprovalBindingDigest is RequestDigest over the same material with the
// approval claim removed from context.
//
// When a request carries no approval claim the two are identical, which is the
// point: an approval issued against a claim-free Check records that Check's
// request_digest, and the later claim-bearing request reproduces the same value
// here. Comparing it to claim.binding.pdp_digest establishes correspondence by
// identity rather than by translating between two action vocabularies
// (GH-DEC-2026-008).
func ApprovalBindingDigest(request CheckRequest) string {
if _, carries := request.Context[ApprovalContextKey]; !carries {
return RequestDigest(request)
}
stripped := make(map[string]any, len(request.Context))
for key, value := range request.Context {
if key == ApprovalContextKey {
continue
}
stripped[key] = value
}
if len(stripped) == 0 {
stripped = nil
}
return CanonicalDigest(requestDigestMaterial{
Tenant: request.Tenant,
Subject: request.Subject,
Action: request.Action,
Resource: request.Resource,
Context: stripped,
Purpose: request.Purpose,
})
}
// RequestDigest is the mechanical §6.4.2 replay test: SHA-256 over canonical
// JSON of tenant, subject, action, resource, context, and purpose. Purpose is
// omitted when empty, so enforcement digests are unchanged by its existence.
func RequestDigest(request CheckRequest) string {
return CanonicalDigest(requestDigestMaterial{
Tenant: request.Tenant,
Subject: request.Subject,
Action: request.Action,
Resource: request.Resource,
Context: request.Context,
Purpose: request.Purpose,
})
}
// CanonicalDigest returns "sha256:" plus the hex SHA-256 of canonical JSON.
// encoding/json sorts map keys, so two equal Go values agree.
func CanonicalDigest(value any) string {
data, err := json.Marshal(value)
if err != nil {
sum := sha256.Sum256(nil)
return "sha256:" + hex.EncodeToString(sum[:])
}
sum := sha256.Sum256(data)
return "sha256:" + hex.EncodeToString(sum[:])
}
// InputClaimDigests hashes the request-time claim classes the evaluator joined.
func InputClaimDigests(request CheckRequest) map[string]string {
digests := make(map[string]string)
if len(request.Context) > 0 {
digests["context"] = CanonicalDigest(request.Context)
}
if request.CaringContext != nil {
digests["caring_context"] = CanonicalDigest(request.CaringContext)
}
if len(digests) == 0 {
return nil
}
return digests
}
// DecisionCompletion carries evaluator-side inputs used to finish an envelope.
type DecisionCompletion struct {
AllowTTL string
Now time.Time
}
// CompleteDecision stamps contract version, input-claim digests, decision time,
// and an explicit allow lifetime. An allow with no stated end becomes a deny.
func CompleteDecision(envelope *DecisionEnvelope, request CheckRequest, completion DecisionCompletion) {
if envelope == nil {
return
}
if envelope.ContractVersion == "" {
envelope.ContractVersion = DecisionRecordContractV1
}
if envelope.Provenance.InputClaimDigests == nil {
envelope.Provenance.InputClaimDigests = InputClaimDigests(request)
}
ApplyAllowLifetime(envelope, completion.AllowTTL, completion.Now)
}
// ParseAllowTTL resolves a package-declared TTL. ok is false when the allow
// would have no stated end. Invalid strings return an error so package
// validation can reject them.
func ParseAllowTTL(declared string) (time.Duration, error) {
trimmed := strings.TrimSpace(declared)
if trimmed == "" {
return DefaultAllowTTL, nil
}
if strings.EqualFold(trimmed, "none") {
return 0, nil
}
ttl, err := time.ParseDuration(trimmed)
if err != nil {
return 0, fmt.Errorf("allow_ttl %q is not a Go duration: %w", declared, err)
}
if ttl <= 0 {
return 0, nil
}
return ttl, nil
}
// ApplyAllowLifetime sets DecisionTime and, for allows, an explicit TTL. A
// missing or zero TTL denies the allow rather than mint a standing grant.
func ApplyAllowLifetime(envelope *DecisionEnvelope, declaredTTL string, now time.Time) {
if envelope == nil {
return
}
if now.IsZero() {
now = time.Now().UTC()
} else {
now = now.UTC()
}
if envelope.Provenance.DecisionTime == "" {
envelope.Provenance.DecisionTime = now.Format(time.RFC3339)
}
if envelope.Effect != DecisionEffectAllow {
return
}
ttl, err := ParseAllowTTL(declaredTTL)
if err != nil || ttl <= 0 {
if envelope.Diagnostics == nil {
envelope.Diagnostics = map[string]any{}
}
if envelope.Reason != "" {
envelope.Diagnostics["unstated_allow_reason"] = envelope.Reason
}
envelope.Effect = DecisionEffectDeny
envelope.Reason = ReasonAllowLifetimeUnstated
envelope.MatchedRule = ReasonAllowLifetimeUnstated
envelope.Lifetime = nil
return
}
display := strings.TrimSpace(declaredTTL)
if display == "" {
display = "15m"
}
envelope.Lifetime = &DecisionLifetime{
Kind: DecisionLifetimeTTL,
TTL: display,
NotBefore: now.Format(time.RFC3339),
ExpiresAt: now.Add(ttl).Format(time.RFC3339),
}
}
// ActionAuthorizationStatus is the lifecycle state of a durable authorization.
type ActionAuthorizationStatus string
const (
ActionAuthorizationPending ActionAuthorizationStatus = "pending"
ActionAuthorizationApproved ActionAuthorizationStatus = "approved"
ActionAuthorizationDenied ActionAuthorizationStatus = "denied"
ActionAuthorizationSuperseded ActionAuthorizationStatus = "superseded"
ActionAuthorizationExpired ActionAuthorizationStatus = "expired"
ActionAuthorizationRevoked ActionAuthorizationStatus = "revoked"
)
// ActionAuthorization joins a durable approval lifecycle to one exact
// flex-auth request and decision. Storage and approval collection remain the
// responsibility of the organizational decision authority.
type ActionAuthorization struct {
SchemaVersion string `json:"schema_version" yaml:"schema_version"`
ID string `json:"id" yaml:"id"`
Status ActionAuthorizationStatus `json:"status" yaml:"status"`
SupersededBy string `json:"superseded_by,omitempty" yaml:"superseded_by,omitempty"`
Request CheckRequest `json:"request" yaml:"request"`
Validity ActionAuthorizationValidity `json:"validity" yaml:"validity"`
Approvals ActionAuthorizationApprovals `json:"approvals" yaml:"approvals"`
Decision DecisionEnvelope `json:"decision" yaml:"decision"`
Provenance map[string]any `json:"provenance,omitempty" yaml:"provenance,omitempty"`
}
// ActionAuthorizationValidity bounds execution of an approved action.
type ActionAuthorizationValidity struct {
NotBefore string `json:"not_before,omitempty" yaml:"not_before,omitempty"`
ExpiresAt string `json:"expires_at" yaml:"expires_at"`
}
// ActionAuthorizationApprovals declares the approval threshold and evidence.
type ActionAuthorizationApprovals struct {
RequiredCount int `json:"required_count" yaml:"required_count"`
Entries []ActionAuthorizationApprovalEntry `json:"entries" yaml:"entries"`
}
// ActionAuthorizationApprovalEntry is one authenticated approver's evidence.
type ActionAuthorizationApprovalEntry struct {
SubjectID string `json:"subject_id" yaml:"subject_id"`
ApprovedAt string `json:"approved_at" yaml:"approved_at"`
Assurance string `json:"assurance,omitempty" yaml:"assurance,omitempty"`
EvidenceRef string `json:"evidence_ref,omitempty" yaml:"evidence_ref,omitempty"`
}
// Obligation describes a follow-up behavior required by a decision.
type Obligation struct {
Type string `json:"type" yaml:"type"`
Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"`
}
// DecisionProvenance captures evaluator and policy provenance.
type DecisionProvenance struct {
Evaluator string `json:"evaluator" yaml:"evaluator"`
Mode string `json:"mode" yaml:"mode"`
PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
}
// CallerProvenance records how the request was authenticated to flex-auth.
// It is not decision material: the same request from a different authenticated
// caller must decide identically. Mode is required. Under disabled the object
// is {"mode":"disabled"} with no principal (FLEX-DEC-2026-009).
type CallerProvenance struct {
Mode string `json:"mode" yaml:"mode"`
Principal string `json:"principal,omitempty" yaml:"principal,omitempty"`
Audience string `json:"audience,omitempty" yaml:"audience,omitempty"`
NotAfter string `json:"not_after,omitempty" yaml:"not_after,omitempty"`
}
// CaringDecisionMetadata carries CARING descriptor and conformance details in
// a decision envelope.
type CaringDecisionMetadata struct {
Profile string `json:"profile" yaml:"profile"`
Descriptor *CaringAccessDescriptor `json:"descriptor,omitempty" yaml:"descriptor,omitempty"`
RestrictionsEvaluated []Restriction `json:"restrictions_evaluated,omitempty" yaml:"restrictions_evaluated,omitempty"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
DerivedCapabilities []CaringDerivedCapability `json:"derived_capabilities,omitempty" yaml:"derived_capabilities,omitempty"`
ConformanceFindings []CaringConformanceFinding `json:"conformance_findings,omitempty" yaml:"conformance_findings,omitempty"`
ExposureEvent *CaringExposureEvent `json:"exposure_event,omitempty" yaml:"exposure_event,omitempty"`
}
// AuditEvent is the local log shape for decisions and exposure events.
type AuditEvent struct {
ID string `json:"id" yaml:"id"`
Type string `json:"type" yaml:"type"`
DecisionID string `json:"decision_id,omitempty" yaml:"decision_id,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Resource ResourceRef `json:"resource,omitempty" yaml:"resource,omitempty"`
Action string `json:"action,omitempty" yaml:"action,omitempty"`
Effect DecisionEffect `json:"effect,omitempty" yaml:"effect,omitempty"`
Timestamp string `json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
ExposureEvent *CaringExposureEvent `json:"exposure_event,omitempty" yaml:"exposure_event,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}

335
verifier/pkg/api/caring.go Normal file
View file

@ -0,0 +1,335 @@
package api
// CaringProfileCaring040RC2 is the executable profile identifier for the
// CARING 0.4.0-RC2 standard pinned by flex-auth.
const CaringProfileCaring040RC2 = "caring-0.4.0-rc2"
// SubjectType is the CARING subject dimension.
type SubjectType string
const (
SubjectTypeHuman SubjectType = "Human"
SubjectTypeGroup SubjectType = "Group"
SubjectTypeOrganization SubjectType = "Organization"
SubjectTypeService SubjectType = "Service"
SubjectTypeAutomation SubjectType = "Automation"
SubjectTypeAgent SubjectType = "Agent"
SubjectTypeSystem SubjectType = "System"
SubjectTypeDevice SubjectType = "Device"
SubjectTypeProcess SubjectType = "Process"
SubjectTypeAnonymous SubjectType = "Anonymous"
SubjectTypeUnknown SubjectType = "Unknown"
)
// OrganizationRelation is the CARING organization-relation dimension.
type OrganizationRelation string
const (
OrganizationRelationVendor OrganizationRelation = "Vendor"
OrganizationRelationServiceProvider OrganizationRelation = "ServiceProvider"
OrganizationRelationDistributor OrganizationRelation = "Distributor"
OrganizationRelationConsultant OrganizationRelation = "Consultant"
OrganizationRelationCustomer OrganizationRelation = "Customer"
OrganizationRelationCommunity OrganizationRelation = "Community"
OrganizationRelationAuthority OrganizationRelation = "Authority"
OrganizationRelationUnknown OrganizationRelation = "Unknown"
)
// CanonicalRole is the CARING lifecycle responsibility posture.
type CanonicalRole string
const (
CanonicalRoleCreator CanonicalRole = "Creator"
CanonicalRoleBuilder CanonicalRole = "Builder"
CanonicalRoleVerifier CanonicalRole = "Verifier"
CanonicalRoleMaintainer CanonicalRole = "Maintainer"
CanonicalRoleIntegrator CanonicalRole = "Integrator"
CanonicalRoleOperator CanonicalRole = "Operator"
CanonicalRoleManager CanonicalRole = "Manager"
CanonicalRoleCoach CanonicalRole = "Coach"
CanonicalRoleDoer CanonicalRole = "Doer"
)
// ScopeLevel is the CARING scope ladder.
type ScopeLevel string
const (
ScopeLevelEcosystem ScopeLevel = "Ecosystem"
ScopeLevelProduct ScopeLevel = "Product"
ScopeLevelPlatform ScopeLevel = "Platform"
ScopeLevelCluster ScopeLevel = "Cluster"
ScopeLevelEnvironment ScopeLevel = "Environment"
ScopeLevelTenant ScopeLevel = "Tenant"
ScopeLevelNamespace ScopeLevel = "Namespace"
ScopeLevelDomain ScopeLevel = "Domain"
ScopeLevelWorkspace ScopeLevel = "Workspace"
ScopeLevelProject ScopeLevel = "Project"
ScopeLevelProcess ScopeLevel = "Process"
ScopeLevelDataset ScopeLevel = "Dataset"
ScopeLevelResource ScopeLevel = "Resource"
ScopeLevelSubresource ScopeLevel = "Subresource"
ScopeLevelRecord ScopeLevel = "Record"
ScopeLevelField ScopeLevel = "Field"
ScopeLevelAction ScopeLevel = "Action"
)
// Plane is the CARING access-surface dimension.
type Plane string
const (
PlaneIntent Plane = "Intent"
PlaneBuild Plane = "Build"
PlaneRuntime Plane = "Runtime"
PlaneExecution Plane = "Execution"
PlaneConfiguration Plane = "Configuration"
PlaneData Plane = "Data"
PlaneIdentity Plane = "Identity"
PlanePolicy Plane = "Policy"
PlaneSecret Plane = "Secret"
PlaneAudit Plane = "Audit"
PlaneCommercial Plane = "Commercial"
PlaneCommunity Plane = "Community"
)
// Capability is a CARING capability verb.
type Capability string
const (
CapabilityView Capability = "View"
CapabilityViewCollection Capability = "ViewCollection"
CapabilityObserve Capability = "Observe"
CapabilityCreate Capability = "Create"
CapabilityEditOwn Capability = "EditOwn"
CapabilityEditAssigned Capability = "EditAssigned"
CapabilityEditAny Capability = "EditAny"
CapabilityDeleteOwn Capability = "DeleteOwn"
CapabilityDeleteAny Capability = "DeleteAny"
CapabilityBulkDelete Capability = "BulkDelete"
CapabilitySubmit Capability = "Submit"
CapabilityComment Capability = "Comment"
CapabilityReview Capability = "Review"
CapabilityApprove Capability = "Approve"
CapabilityReject Capability = "Reject"
CapabilityPublish Capability = "Publish"
CapabilityArchive Capability = "Archive"
CapabilityRestore Capability = "Restore"
CapabilityExecute Capability = "Execute"
CapabilityConfigure Capability = "Configure"
CapabilityOperate Capability = "Operate"
CapabilityDeploy Capability = "Deploy"
CapabilityIntegrate Capability = "Integrate"
CapabilityGrant Capability = "Grant"
CapabilityRevoke Capability = "Revoke"
CapabilityDelegate Capability = "Delegate"
CapabilityImpersonate Capability = "Impersonate"
CapabilityExport Capability = "Export"
CapabilityImport Capability = "Import"
CapabilityReplicate Capability = "Replicate"
CapabilityEncrypt Capability = "Encrypt"
CapabilityDecrypt Capability = "Decrypt"
CapabilityMask Capability = "Mask"
CapabilityInspect Capability = "Inspect"
CapabilityAudit Capability = "Audit"
CapabilityOverride Capability = "Override"
CapabilityEscalate Capability = "Escalate"
CapabilityBind Capability = "Bind"
CapabilityUse Capability = "Use"
)
// ExposureMode describes how much information becomes visible or extractable.
type ExposureMode string
const (
ExposureModeNone ExposureMode = "None"
ExposureModeMetadata ExposureMode = "Metadata"
ExposureModeMasked ExposureMode = "Masked"
ExposureModeAggregated ExposureMode = "Aggregated"
ExposureModeSynthetic ExposureMode = "Synthetic"
ExposureModePseudonymous ExposureMode = "Pseudonymous"
ExposureModeEncrypted ExposureMode = "Encrypted"
ExposureModePlaintext ExposureMode = "Plaintext"
ExposureModeSecretMaterial ExposureMode = "SecretMaterial"
ExposureModeExportable ExposureMode = "Exportable"
ExposureModeCrossTenantAggregate ExposureMode = "CrossTenantAggregate"
)
// Condition is a CARING runtime or governance condition.
type Condition string
const (
ConditionMFARequired Condition = "MFARequired"
ConditionDeviceTrusted Condition = "DeviceTrusted"
ConditionNetworkTrusted Condition = "NetworkTrusted"
ConditionTicketRequired Condition = "TicketRequired"
ConditionTenantConsentRequired Condition = "TenantConsentRequired"
ConditionCustomerApprovalRequired Condition = "CustomerApprovalRequired"
ConditionDualApprovalRequired Condition = "DualApprovalRequired"
ConditionTimeLimited Condition = "TimeLimited"
ConditionBusinessHoursOnly Condition = "BusinessHoursOnly"
ConditionEmergencyOnly Condition = "EmergencyOnly"
ConditionTrainingRequired Condition = "TrainingRequired"
ConditionContractRequired Condition = "ContractRequired"
ConditionNDARequired Condition = "NDARequired"
ConditionPurposeBound Condition = "PurposeBound"
ConditionCaseBound Condition = "CaseBound"
ConditionEnvironmentBound Condition = "EnvironmentBound"
ConditionNamespaceBound Condition = "NamespaceBound"
ConditionPipelineBound Condition = "PipelineBound"
ConditionChangeWindowBound Condition = "ChangeWindowBound"
ConditionLogged Condition = "Logged"
ConditionRecorded Condition = "Recorded"
ConditionNotificationRequired Condition = "NotificationRequired"
ConditionPostReviewRequired Condition = "PostReviewRequired"
ConditionHumanReviewRequired Condition = "HumanReviewRequired"
ConditionPolicyReviewRequired Condition = "PolicyReviewRequired"
ConditionWorkloadIdentityRequired Condition = "WorkloadIdentityRequired"
)
// LifecycleState describes why access exists now.
type LifecycleState string
const (
LifecycleStateDesign LifecycleState = "Design"
LifecycleStateBuild LifecycleState = "Build"
LifecycleStateTest LifecycleState = "Test"
LifecycleStateReview LifecycleState = "Review"
LifecycleStateRelease LifecycleState = "Release"
LifecycleStateOnboard LifecycleState = "Onboard"
LifecycleStateIntegrate LifecycleState = "Integrate"
LifecycleStateMigrate LifecycleState = "Migrate"
LifecycleStateOperate LifecycleState = "Operate"
LifecycleStateSupport LifecycleState = "Support"
LifecycleStateImprove LifecycleState = "Improve"
LifecycleStateDeprecate LifecycleState = "Deprecate"
LifecycleStateArchive LifecycleState = "Archive"
LifecycleStateIncident LifecycleState = "Incident"
LifecycleStateLegal LifecycleState = "Legal"
LifecycleStateTerminate LifecycleState = "Terminate"
)
// Restriction is an overriding CARING deny or limiting policy effect.
type Restriction string
const (
RestrictionNoAccess Restriction = "NoAccess"
RestrictionSuspended Restriction = "Suspended"
RestrictionTerminated Restriction = "Terminated"
RestrictionQuarantined Restriction = "Quarantined"
RestrictionScopeExcluded Restriction = "ScopeExcluded"
RestrictionDataClassRestricted Restriction = "DataClassRestricted"
RestrictionLegalHold Restriction = "LegalHold"
RestrictionExportBlocked Restriction = "ExportBlocked"
RestrictionImpersonationBlocked Restriction = "ImpersonationBlocked"
RestrictionCrossTenantBlocked Restriction = "CrossTenantBlocked"
RestrictionSecretAccessBlocked Restriction = "SecretAccessBlocked"
RestrictionPolicyFrozen Restriction = "PolicyFrozen"
RestrictionEmergencyLocked Restriction = "EmergencyLocked"
RestrictionRiskDenied Restriction = "RiskDenied"
RestrictionExecutionBlocked Restriction = "ExecutionBlocked"
RestrictionWorkloadCreationBlocked Restriction = "WorkloadCreationBlocked"
RestrictionPrivilegeEscalationBlocked Restriction = "PrivilegeEscalationBlocked"
)
// ExposureEventType is a CARING exceptional or irregular access class.
type ExposureEventType string
const (
ExposureEventSupport ExposureEventType = "X-Support"
ExposureEventBreakGlass ExposureEventType = "X-BreakGlass"
ExposureEventSecurityTest ExposureEventType = "X-SecurityTest"
ExposureEventIncident ExposureEventType = "X-Incident"
ExposureEventLegalDemand ExposureEventType = "X-LegalDemand"
ExposureEventComplianceAudit ExposureEventType = "X-ComplianceAudit"
ExposureEventMigration ExposureEventType = "X-Migration"
ExposureEventRecovery ExposureEventType = "X-Recovery"
ExposureEventAdversarial ExposureEventType = "X-Adversarial"
ExposureEventMisconfiguration ExposureEventType = "X-Misconfiguration"
ExposureEventInducedAccess ExposureEventType = "X-InducedAccess"
ExposureEventPrivilegeEscalation ExposureEventType = "X-PrivilegeEscalation"
)
// AccessPath describes how access is exercised.
type AccessPath string
const (
AccessPathDirect AccessPath = "direct"
AccessPathDelegated AccessPath = "delegated"
AccessPathMediated AccessPath = "mediated"
AccessPathInduced AccessPath = "induced"
)
// CaringScope identifies where a CARING descriptor applies.
type CaringScope struct {
Level ScopeLevel `json:"level" yaml:"level"`
ID string `json:"id" yaml:"id"`
Parent string `json:"parent,omitempty" yaml:"parent,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Resource string `json:"resource,omitempty" yaml:"resource,omitempty"`
Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"`
}
// CaringAccessDescriptor is the executable flex-auth representation of a
// CARING access assignment.
type CaringAccessDescriptor struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Profile string `json:"profile" yaml:"profile"`
SubjectType SubjectType `json:"subject_type" yaml:"subject_type"`
OrganizationRelation OrganizationRelation `json:"organization_relation" yaml:"organization_relation"`
CanonicalRole CanonicalRole `json:"canonical_role" yaml:"canonical_role"`
Scope CaringScope `json:"scope" yaml:"scope"`
Planes []Plane `json:"planes" yaml:"planes"`
Capabilities []Capability `json:"capabilities" yaml:"capabilities"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
Conditions []Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"`
LifecycleState LifecycleState `json:"lifecycle_state,omitempty" yaml:"lifecycle_state,omitempty"`
Restrictions []Restriction `json:"restrictions,omitempty" yaml:"restrictions,omitempty"`
ExposureEvent ExposureEventType `json:"exposure_event,omitempty" yaml:"exposure_event,omitempty"`
DerivedCapabilities []CaringDerivedCapability `json:"derived_capabilities,omitempty" yaml:"derived_capabilities,omitempty"`
AccessPath AccessPath `json:"access_path,omitempty" yaml:"access_path,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// CaringDerivedCapability records effective authority created by another grant.
type CaringDerivedCapability struct {
Capability Capability `json:"capability" yaml:"capability"`
Reason string `json:"reason" yaml:"reason"`
Source string `json:"source,omitempty" yaml:"source,omitempty"`
Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
}
// CaringConformanceFinding is a diagnostic emitted by descriptive or
// prescriptive CARING validation.
type CaringConformanceFinding struct {
Code string `json:"code" yaml:"code"`
Severity string `json:"severity" yaml:"severity"`
Message string `json:"message" yaml:"message"`
Fields []string `json:"fields,omitempty" yaml:"fields,omitempty"`
Descriptor string `json:"descriptor,omitempty" yaml:"descriptor,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// CaringExposureEvent records exceptional or irregular information exposure.
type CaringExposureEvent struct {
ID string `json:"id" yaml:"id"`
Type ExposureEventType `json:"type" yaml:"type"`
Actor string `json:"actor" yaml:"actor"`
Subject string `json:"subject" yaml:"subject"`
Descriptor *CaringAccessDescriptor `json:"descriptor,omitempty" yaml:"descriptor,omitempty"`
Scope *CaringScope `json:"scope,omitempty" yaml:"scope,omitempty"`
Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"`
CapabilitiesUsed []Capability `json:"capabilities_used,omitempty" yaml:"capabilities_used,omitempty"`
DerivedCapabilities []CaringDerivedCapability `json:"derived_capabilities,omitempty" yaml:"derived_capabilities,omitempty"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
Reason string `json:"reason" yaml:"reason"`
AuthoritySource string `json:"authority_source,omitempty" yaml:"authority_source,omitempty"`
Approval string `json:"approval,omitempty" yaml:"approval,omitempty"`
StartTime string `json:"start_time,omitempty" yaml:"start_time,omitempty"`
EndTime string `json:"end_time,omitempty" yaml:"end_time,omitempty"`
ResourcesAccessed []string `json:"resources_accessed,omitempty" yaml:"resources_accessed,omitempty"`
Evidence []string `json:"evidence,omitempty" yaml:"evidence,omitempty"`
NotificationStatus string `json:"notification_status,omitempty" yaml:"notification_status,omitempty"`
PostReview string `json:"post_review,omitempty" yaml:"post_review,omitempty"`
ConformanceFindings []CaringConformanceFinding `json:"conformance_findings,omitempty" yaml:"conformance_findings,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}

10
verifier/pkg/api/doc.go Normal file
View file

@ -0,0 +1,10 @@
// Package api defines flex-auth's public, backend-neutral types:
// resource manifests, subject manifests, relationship facts, policy
// package metadata, check requests, and decision envelopes.
//
// These types are the stable contract consumed by protected systems
// (Markitect first) and produced by every adapter. Schema artifacts
// in schemas/ are generated from or validated against these types.
//
// Concrete type definitions land in FLEX-WP-0002 P2.1.
package api

View file

@ -0,0 +1,32 @@
package api
// ResourceManifest is the shape a protected system publishes to register
// its resources with flex-auth. The shape is pinned against the
// Markitect-side emitter in markitect-tool (MKTT-WP-0014); see
// schemas/resource_manifest.schema.json for the JSON Schema and
// examples/markitect/resource_manifest.yaml for the canonical example.
type ResourceManifest struct {
ID string `json:"id" yaml:"id"`
System string `json:"system" yaml:"system"`
Resources []Resource `json:"resources" yaml:"resources"`
Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"`
CaringProfile string `json:"caring_profile,omitempty" yaml:"caring_profile,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Resource is one entry in a ResourceManifest.
type Resource struct {
ID string `json:"id" yaml:"id"`
Type string `json:"type" yaml:"type"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Parent string `json:"parent,omitempty" yaml:"parent,omitempty"`
Labels []string `json:"labels,omitempty" yaml:"labels,omitempty"`
TrustZone string `json:"trust_zone,omitempty" yaml:"trust_zone,omitempty"`
Owner string `json:"owner,omitempty" yaml:"owner,omitempty"`
Caring *CaringAccessDescriptor `json:"caring,omitempty" yaml:"caring,omitempty"`
Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"`
}
// FlexAuthContractV0 is the metadata.flex_auth_contract value that
// signals the v0 resource-registration contract.
const FlexAuthContractV0 = "resource-registration-v0"

View file

@ -0,0 +1,15 @@
{
"upstream": "https://github.com/netkingdom/access-engine",
"revision": "62ac2b4e99dec2005c7c407fe633612578a7907d",
"files": {
"pkg/api/canonical.go": "21e754617c2b82d673cef31b7075f8d177e4367782b63b20b37125587bf9e300",
"pkg/api/caring.go": "1907156f0f889e7b9e9c14970cf7a86a20d00478a496efaf92c7231f27fec5e3",
"pkg/api/resource_manifest.go": "f0fa4080287d389ed48c2b1d6af96cc62e57ef72c902333f677f964420be133f",
"pkg/api/doc.go": "0638f06ae47369899dd1e2dd5dd23a7200e7dbb3b505b04f4d3c78c0a074def9",
"internal/sign/sign.go": "dad2d0c9f6d1fb2fd68e774949ccdf954382acd075191b5ddfea2c3cc2f4cb62"
},
"fixtures": {
"decision_rotate_signed.json": "a7bc499f3583f53551cf163109a3c108188345100176092adc31eccefc222fd8",
"decision_rotate_signed_tampered.json": "7f2d2e9372a9617bd4b197033f09a5e33a08b202046de8109582da1dc90cb527"
}
}

View file

@ -0,0 +1,28 @@
// Synthetic test-only signer with the producer's published nonproduction seed.
package main
import (
"bytes"
"crypto/ed25519"
"encoding/json"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/pkg/api"
"os"
)
func main() {
seed := byte(0x42)
kid := "testdata-ed25519"
if len(os.Args) > 1 {
seed = 0x43
kid = "next-admitted"
}
var env api.DecisionEnvelope
if json.NewDecoder(os.Stdin).Decode(&env) != nil {
os.Exit(1)
}
if sign.Sign(&env, ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, 32)), kid) != nil {
os.Exit(1)
}
json.NewEncoder(os.Stdout).Encode(env)
}

View file

@ -8,7 +8,7 @@ status: active
owner: codex
topic_slug: secrets-engine
created: "2026-10-12"
updated: "2026-10-12"
updated: "2026-10-16"
flavor: implementation
depends_on: []
state_hub_workstream_id: "7252469d-d7a5-5c18-8911-b511b8c6a736"
@ -44,7 +44,7 @@ This closes the source transport defects only; T02/T03 remain mandatory.
```task
id: SECRETS-WP-0014-T02
status: todo
status: progress
priority: high
state_hub_task_id: "877782e5-045b-5186-8b95-82e25db21a5a"
```
@ -59,6 +59,24 @@ wrong issuer/pin, unsigned/downgrade, expired and rotation overlap/refusal cases
Track any temporary exception using all required SEC-DSM-EXC fields and actual
approval; neither source support nor a loopback URL is that approval.
2026-10-16 source preparation: production `authorize_action` now requires an
independently admitted local trust file and installed verifier before approval/PDP
credential reads. The Go helper vendors exact producer API/signature sources,
checks source hashes, authenticates the original response and returns only its
verified typed interpretation. Signed evaluator/mode and exact package/version/
artifact digest are bound to admitted public keys. Unknown/unsigned/altered
responses refuse; existing request/approval/lifetime/CAS checks remain required.
Native replay, real producer signing and consumer-boundary tests cover downgrade,
issuer/pin, request joins, and distinct-key overlap/removal. Build/install and
source update procedures are documented in `verifier/README.md`.
This is an isolated source implementation awaiting integration/independent review.
No production verification keys, native pin signed-mode change, trust admission,
owner assent or rollout has occurred. T02 stays progress because independent
key admission remains part of its scope; T03 stays wait. WP13 execution and
identity-only CCR38 are unchanged. Custodian source/verification handoff:
`docs/changes/netkingdom-target-2026-10-16/decision-auth/` in the Custodian repo.
## Verify the admitted native consumer and rotation path
```task