Bind wrapped issuance to native metadata and preserve immutable recipient attempts
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.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
tegwick 2026-10-12 00:11:49 +02:00
parent 58129de8f1
commit 0db6adef69
8 changed files with 288 additions and 46 deletions

View file

@ -235,3 +235,12 @@ secrets-engine verify whynot-design-npm-publish --field npm_token --positive --n
# 6. publish with the token injected into the child only # 6. publish with the token injected into the child only
secrets-engine exec --catalog whynot-design-npm-publish -- npm publish secrets-engine exec --catalog whynot-design-npm-publish -- npm publish
``` ```
Wrapped handoff files must be fresh and inside an owned private directory outside
Git. Existing files, symlinks and shared directories refuse before minting. A
failed or uncertain mint leaves its private reservation for owner reconciliation;
never delete it merely to retry. The backend must return the exact native KV-v2
data/AppRole creation path, requested bounded TTL and an accessor. Handoff-file
creation verifies those native fields; a requested path alone is not evidence.
These checks do not enable production wrapping or the separate execute-ccr
metadata entitlement.

View file

@ -0,0 +1,54 @@
{
"schema": "wrapped-issuer-source-validation/v1",
"task": "SECRETS-WP-0013-T01",
"captured_at": "2026-10-11T22:07:09.996760+00:00",
"source_sha256": {
"src/secrets_engine/openbao.py": "c1d17ca30367fd2c856927e8ce4021599a992c6c22509f82a6d8d4588ea46199",
"src/secrets_engine/wrap.py": "a37c491a93c8803015d2f1fca6ea502b8da28a9279900404d8f9ebf06a0c0a70",
"tests/test_wrap.py": "40b4511f6f5c2515e8ff70095e8239a4f589a97697fa1827298ec3be89ba57d4",
"tests/test_wrapped_issuer_native.py": "e24d86da4e9b0c971aa39c81f34c1bced76d3def1727ee38fe69a3769c80e5c1"
},
"full_suite": {
"collected": 654,
"passed": 653,
"skipped": 1,
"exit": 0,
"execution": "isolated tracked-source Git checkout, real filesystem ownership, disposable local OpenBao"
},
"final_wrap_and_native_tests": {
"passed": 24,
"exit": 0
},
"native_local_semantics": {
"creation_path": "secret/data/d3-wrap-fixture",
"ttl_seconds": 60,
"first_unwrap_success": true,
"consumed_token_replay": "specific native HTTP400 wrapping-token-invalid denial"
},
"production_lane_proven": false,
"production_authority_enabled": false,
"raw_secret_values_in_receipt": false,
"negative_cases": [
"changed/missing native path",
"wrong/missing/noninteger TTL",
"missing accessor",
"unwrapped material alongside wrap",
"overwrite",
"symlink leaf/ancestor",
"shared parent",
"uncertain mint remint",
"concurrent duplicate",
"reservation replacement"
],
"preparation_failures": [
{
"kind": "missing local clock module in isolated test collection",
"recovery": "explicit owner clock source dependency"
},
{
"kind": "full sandbox suite had17 unrelated custody/path failures",
"recovery": "created actual isolated Git worktree and reran with real filesystem ownership; no custody guard relaxed"
}
],
"pending": "Independent Platform/identity/approval/PDP review, separate exact actor/path/audience delivery entitlement and estate-native proof"
}

View file

@ -311,3 +311,24 @@ this change does not make them verified human representation. Existing generic
consume receipt validation limitations remain explicitly separate. service-jwt consume receipt validation limitations remain explicitly separate. service-jwt
readiness guard/default and ordinary actions remain unchanged. T01 stays in readiness guard/default and ordinary actions remain unchanged. T01 stays in
progress, T03 stays wait, WP13 stays proposed, and WP8-T06 stays open. progress, T03 stays wait, WP13 stays proposed, and WP8-T06 stays open.
## D3 wrapped-delivery primitive and its separate entitlement
D3 P3=b chooses response-wrapped issuer delivery with one-time unwrap. The
existing wrapped-handoff primitive now verifies native TTL/accessor/creation-path
metadata and reserves a private immutable recipient file before minting. Native
KV-v2 creation paths include the `data/` endpoint; caller-requested CLI paths
cannot substitute for returned metadata. Failed mint/write or an altered
reservation retains an uncertain attempt and never silently overwrites/remints.
A disposable local OpenBao integration proves the exact source path,60-second
TTL, successful first unwrap and specific HTTP400 consumed-token replay denial.
This proves backend semantics, not the estate's first delivered lane, audited
initiating principal, consumer identity, independent owner acceptance or cleanup
under crash/revocation. Production CLI wrapping remains fail-closed. Existing
metadata execute-ccr contracts expressly exclude wrap/KV value delivery and cannot
be borrowed for this purpose. A separate exact owner-admitted delivery operation
must bind actor,path,audience,deadline,authorization/consume state,destination,
audit,replay/crash recovery and rollback before native adoption. The prepared
D2 repository-issuer lane remains attended until its own reviewed execution and
independent destination/adoption proofs pass.

View file

@ -16,6 +16,7 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
import os import os
import re
import shutil import shutil
import stat import stat
import subprocess import subprocess
@ -400,23 +401,34 @@ class OpenBaoClient:
self._run_ok(["lease", "revoke", lease_id]) self._run_ok(["lease", "revoke", lease_id])
def _parse_wrap_response(self, stdout: str, *, ttl: str, creation_path: str) -> WrappedResponse: def _parse_wrap_response(self, stdout: str, *, ttl: str, creation_path: str) -> WrappedResponse:
# Verify native wrapping metadata. Never reconstruct a purported source
# path or TTL from the caller when the backend omitted or changed it.
try: try:
payload = json.loads(stdout) payload = json.loads(stdout)
except json.JSONDecodeError as exc: except (json.JSONDecodeError, TypeError):
raise BackendError("wrapped response is not JSON") from exc raise BackendError("wrapped response is not JSON") from None
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise BackendError("wrapped response is invalid") raise BackendError("wrapped response is invalid")
wrap = payload.get("wrap_info") wrap = payload.get("wrap_info")
if not isinstance(wrap, dict) or not wrap.get("token"): if not isinstance(wrap, dict) or not wrap.get("token"):
raise BackendError("response wrapping was not applied") raise BackendError("response wrapping was not applied")
token = str(wrap["token"]) if payload.get("data") or payload.get("auth"):
accessor = str(wrap.get("accessor") or "") raise BackendError("wrapped response unexpectedly included unwrapped material")
wrap_ttl = wrap.get("ttl") match = re.fullmatch(r"([1-9][0-9]*)([smh])", ttl)
seconds = int(match[1]) * {"s": 1, "m": 60, "h": 3600}[match[2]] if match else 0
if not 0 < seconds <= 900:
raise BackendError("wrap TTL is outside the supported bound")
token, accessor = wrap.get("token"), wrap.get("accessor")
if (not isinstance(token, str) or not token or any(ch.isspace() for ch in token)
or not isinstance(accessor, str) or not accessor
or type(wrap.get("ttl")) is not int or wrap["ttl"] != seconds
or wrap.get("creation_path") != creation_path):
raise BackendError("native wrap identity, TTL or creation path unverified")
return WrappedResponse( return WrappedResponse(
wrap_token=token, wrap_token=token,
accessor_fingerprint=accessor_fingerprint(accessor) if accessor else "", accessor_fingerprint=accessor_fingerprint(accessor),
ttl=str(wrap_ttl if wrap_ttl is not None else ttl), ttl=str(seconds),
creation_path=creation_path, creation_path=wrap["creation_path"],
) )
def wrap_kv_get(self, mount: str, path: str, *, ttl: str) -> WrappedResponse: def wrap_kv_get(self, mount: str, path: str, *, ttl: str) -> WrappedResponse:
@ -425,7 +437,7 @@ class OpenBaoClient:
proc = self._run(["kv", "get", f"-wrap-ttl={ttl}", "-format=json", target]) proc = self._run(["kv", "get", f"-wrap-ttl={ttl}", "-format=json", target])
if proc.returncode != 0: if proc.returncode != 0:
raise BackendError("wrapped KV read failed") raise BackendError("wrapped KV read failed")
return self._parse_wrap_response(proc.stdout, ttl=ttl, creation_path=target) return self._parse_wrap_response(proc.stdout, ttl=ttl, creation_path=f"{mount}/data/{path}")
def wrap_approle_secret_id(self, role_name: str, *, ttl: str) -> WrappedResponse: def wrap_approle_secret_id(self, role_name: str, *, ttl: str) -> WrappedResponse:
"""Wrap a single-use AppRole secret_id. Fail if returned unwrapped.""" """Wrap a single-use AppRole secret_id. Fail if returned unwrapped."""

View file

@ -7,6 +7,7 @@ from __future__ import annotations
import os import os
import re import re
import stat
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@ -43,33 +44,48 @@ def normalize_wrap_ttl(value: str) -> str:
def _validate_output_path(path: Path) -> Path: def _validate_output_path(path: Path) -> Path:
resolved = path.expanduser().resolve() # Do not follow a leaf/parent symlink into another recipient's private file.
worktree = containing_git_worktree(resolved) absolute = Path(os.path.abspath(path.expanduser()))
if absolute.resolve() != absolute or absolute.is_symlink():
raise ProvisioningError("wrap output cannot use redirected paths")
worktree = containing_git_worktree(absolute)
if worktree is not None: if worktree is not None:
raise ProvisioningError( raise ProvisioningError("wrap output is inside a Git worktree; keep tokens outside repos")
f"wrap file {resolved} is inside a Git worktree ({worktree}); " if os.path.lexists(absolute):
"keep wrap tokens outside repos" raise ProvisioningError("wrap output already exists; reconcile the prior attempt, never overwrite or remint")
) return absolute
if resolved.exists() and resolved.stat().st_mode & 0o077:
raise ProvisioningError(
f"wrap file {resolved} is group/other-accessible "
f"(mode {oct(resolved.stat().st_mode & 0o777)}); must be 0600"
)
return resolved
def _write_mode_0600(path: Path, value: str) -> None: def _reserve_mode_0600(path: Path) -> int:
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
fd: int | None = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) directory_fd = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
try: try:
os.fchmod(fd, 0o600) # Walk using directory descriptors so a concurrent symlink swap cannot
with os.fdopen(fd, "w", encoding="utf-8") as fh: # redirect either an ancestor or the final file creation.
fd = None for component in path.parent.parts[1:]:
fh.write(value) next_fd = os.open(component, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
fh.write("\n") dir_fd=directory_fd)
os.close(directory_fd)
directory_fd = next_fd
parent = os.fstat(directory_fd)
if parent.st_uid != os.geteuid() or stat.S_IMODE(parent.st_mode) & 0o077:
raise ProvisioningError("wrap output parent must be owned and private")
# Reserve before the backend call. Failure/crash leaves this private
# marker for reconciliation, never automatic remint.
file_fd = os.open(path.name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=directory_fd)
try:
os.fchmod(file_fd, 0o600)
os.fsync(file_fd)
os.fsync(directory_fd)
except BaseException:
os.close(file_fd)
raise
return file_fd
except OSError:
raise ProvisioningError("wrap output reservation failed; no backend retry") from None
finally: finally:
if fd is not None: os.close(directory_fd)
os.close(fd)
def write_wrapped_handoff( def write_wrapped_handoff(
@ -82,20 +98,26 @@ def write_wrapped_handoff(
"""Mint a wrap token and write it without printing it.""" """Mint a wrap token and write it without printing it."""
ttl = normalize_wrap_ttl(ttl) ttl = normalize_wrap_ttl(ttl)
out_path = _validate_output_path(out_file) out_path = _validate_output_path(out_file)
wrapped: WrappedResponse if entry.kind != "auth-capability" and not entry.stores_kv_value():
if entry.kind == "auth-capability": raise ProvisioningError(f"lane '{entry.id}' kind {entry.kind} has no wrapped handoff")
wrapped = client.wrap_approle_secret_id(entry.role_name, ttl=ttl) fd = _reserve_mode_0600(out_path)
elif entry.stores_kv_value():
wrapped = client.wrap_kv_get(entry.mount, entry.path, ttl=ttl)
else:
raise ProvisioningError(
f"lane '{entry.id}' kind {entry.kind} has no wrapped handoff"
)
token = wrapped.wrap_token
try: try:
_write_mode_0600(out_path, token) if entry.kind == "auth-capability":
wrapped: WrappedResponse = client.wrap_approle_secret_id(entry.role_name, ttl=ttl)
else:
wrapped = client.wrap_kv_get(entry.mount, entry.path, ttl=ttl)
visible, reserved = out_path.lstat(), os.fstat(fd)
if (not stat.S_ISREG(visible.st_mode)
or (visible.st_dev, visible.st_ino) != (reserved.st_dev, reserved.st_ino)):
raise ProvisioningError("wrap output reservation changed; retain and reconcile the mint")
with os.fdopen(fd, "w", encoding="utf-8") as handle:
fd = None
handle.write(wrapped.wrap_token + "\n")
handle.flush()
os.fsync(handle.fileno())
finally: finally:
token = "" if fd is not None:
os.close(fd)
return WrapHandoffResult( return WrapHandoffResult(
catalog_id=entry.id, catalog_id=entry.id,
kind=entry.kind, kind=entry.kind,

View file

@ -65,13 +65,17 @@ def test_wrap_kv_writes_0600_file_and_omits_token_from_result(tmp_path):
assert WRAP_ACCESSOR not in result.wrap_handle assert WRAP_ACCESSOR not in result.wrap_handle
def test_wrap_rejects_repo_paths(): def test_wrap_rejects_repo_paths(tmp_path):
import subprocess
repository = tmp_path / "repository"
repository.mkdir()
subprocess.run(["git", "init", str(repository)], check=True, capture_output=True)
entry = validate_entry(copy.deepcopy(VALID)) entry = validate_entry(copy.deepcopy(VALID))
with pytest.raises(ProvisioningError, match="Git worktree"): with pytest.raises(ProvisioningError, match="Git worktree"):
write_wrapped_handoff( write_wrapped_handoff(
FakeWrapClient(), FakeWrapClient(),
entry, entry,
out_file=__import__("pathlib").Path("wrap.token"), out_file=repository / "wrap.token",
ttl="15m", ttl="15m",
) )
@ -96,6 +100,7 @@ def test_parse_wrap_accepts_wrap_info_only():
"token": WRAP_TOKEN, "token": WRAP_TOKEN,
"accessor": WRAP_ACCESSOR, "accessor": WRAP_ACCESSOR,
"ttl": 900, "ttl": 900,
"creation_path": "secret/x",
} }
} }
) )
@ -116,7 +121,7 @@ def test_wrap_kv_get_uses_wrap_ttl_flag(monkeypatch):
return SimpleNamespace( return SimpleNamespace(
returncode=0, returncode=0,
stdout=json.dumps( stdout=json.dumps(
{"wrap_info": {"token": WRAP_TOKEN, "accessor": WRAP_ACCESSOR}} {"wrap_info": {"token": WRAP_TOKEN, "accessor": WRAP_ACCESSOR, "ttl": 900, "creation_path": "secret/data/test/team/thing"}}
), ),
stderr="", stderr="",
) )
@ -158,3 +163,93 @@ def test_production_wrap_fails_closed(tmp_path, monkeypatch):
) )
with pytest.raises(DecisionError, match="production action 'wrap'"): with pytest.raises(DecisionError, match="production action 'wrap'"):
cli.cmd_wrap(cfg, args) cli.cmd_wrap(cfg, args)
@pytest.mark.parametrize('change', [
{'creation_path':'secret/data/other-recipient'}, {'creation_path':None},
{'ttl':901}, {'ttl':60}, {'ttl':True}, {'ttl':None},
{'token':42}, {'token':'value with whitespace'}, {'accessor':None},
])
def test_native_wrap_binding_cannot_be_inferred_from_request(change):
client=OpenBaoClient(addr='http://example.invalid',token='t',bao_bin='bao')
value={'token':WRAP_TOKEN,'accessor':WRAP_ACCESSOR,'ttl':900,'creation_path':'secret/data/test/team/thing'}
value.update(change)
with pytest.raises(BackendError) as raised:
client._parse_wrap_response(json.dumps({'wrap_info':value}),ttl='15m',creation_path='secret/data/test/team/thing')
assert WRAP_TOKEN not in str(raised.value)
def test_valid_wrap_info_cannot_mask_unwrapped_payload():
client=OpenBaoClient(addr='http://example.invalid',token='t',bao_bin='bao')
value={'wrap_info':{'token':WRAP_TOKEN,'accessor':WRAP_ACCESSOR,'ttl':900,'creation_path':'secret/x'},'data':{'secret':'NEVER-LOG-ME'}}
with pytest.raises(BackendError) as raised:
client._parse_wrap_response(json.dumps(value),ttl='15m',creation_path='secret/x')
assert 'NEVER-LOG-ME' not in str(raised.value)
def test_existing_output_prevents_mint_and_preserves_original(tmp_path):
out=tmp_path/'wrap.token';out.write_text('previous trial');out.chmod(0o600)
client=FakeWrapClient()
with pytest.raises(ProvisioningError,match='already exists'):
write_wrapped_handoff(client,validate_entry(copy.deepcopy(VALID)),out_file=out,ttl='60s')
assert client.calls==[] and out.read_text()=='previous trial'
def test_leaf_and_parent_symlink_cannot_redirect_handoff(tmp_path):
from pathlib import Path
target=tmp_path/'retained';target.write_text('private existing');target.chmod(0o600)
out=tmp_path/'wrap.token';out.symlink_to(target)
client=FakeWrapClient();entry=validate_entry(copy.deepcopy(VALID))
with pytest.raises(ProvisioningError):write_wrapped_handoff(client,entry,out_file=out,ttl='60s')
folder=tmp_path/'private';folder.mkdir(mode=0o700)
parent=tmp_path/'redirect';parent.symlink_to(folder,target_is_directory=True)
with pytest.raises(ProvisioningError):write_wrapped_handoff(client,entry,out_file=parent/'wrap.token',ttl='60s')
assert client.calls==[] and target.read_text()=='private existing'
def test_backend_failure_retains_private_reservation_and_prevents_remint(tmp_path):
class Failed(FakeWrapClient):
def wrap_kv_get(self,*args,**kwargs):
self.calls.append('attempt');raise BackendError('unverified native mint')
client=Failed();out=tmp_path/'wrap.token';entry=validate_entry(copy.deepcopy(VALID))
with pytest.raises(BackendError):write_wrapped_handoff(client,entry,out_file=out,ttl='60s')
assert out.exists() and out.read_bytes()==b'' and out.stat().st_mode&0o077==0
with pytest.raises(ProvisioningError):write_wrapped_handoff(client,entry,out_file=out,ttl='60s')
assert client.calls==['attempt']
def test_shared_output_directory_refuses_before_backend(tmp_path):
parent=tmp_path/'shared';parent.mkdir(mode=0o755);client=FakeWrapClient()
with pytest.raises(ProvisioningError,match='owned and private'):
write_wrapped_handoff(client,validate_entry(copy.deepcopy(VALID)),out_file=parent/'wrap.token',ttl='60s')
assert client.calls==[]
def test_concurrent_handoff_reserves_before_only_one_backend_mint(tmp_path):
from concurrent.futures import ThreadPoolExecutor
import threading
entered,release=threading.Event(),threading.Event()
class Paused(FakeWrapClient):
def wrap_kv_get(self,*args,**kwargs):
entered.set();assert release.wait(5)
return super().wrap_kv_get(*args,**kwargs)
client=Paused();entry=validate_entry(copy.deepcopy(VALID));out=tmp_path/'wrap.token'
with ThreadPoolExecutor(max_workers=2) as pool:
first=pool.submit(write_wrapped_handoff,client,entry,out_file=out,ttl='60s')
assert entered.wait(5)
try:
with pytest.raises(ProvisioningError):write_wrapped_handoff(client,entry,out_file=out,ttl='60s')
finally:release.set()
first.result()
assert len(client.calls)==1 and out.read_text().strip()==WRAP_TOKEN
def test_output_replacement_during_backend_call_does_not_overwrite_other_recipient(tmp_path):
out=tmp_path/'wrap.token';other=tmp_path/'other';other.write_text('retained recipient')
class Replaced(FakeWrapClient):
def wrap_kv_get(self,*args,**kwargs):
out.unlink();out.symlink_to(other)
return super().wrap_kv_get(*args,**kwargs)
client=Replaced()
with pytest.raises(ProvisioningError,match='reservation changed'):
write_wrapped_handoff(client,validate_entry(copy.deepcopy(VALID)),out_file=out,ttl='60s')
assert other.read_text()=='retained recipient' and len(client.calls)==1

View file

@ -0,0 +1,24 @@
"""Real wrapping semantics in a disposable local OpenBao, not estate authority."""
import json
import re
from secrets_engine.openbao import OpenBaoClient
from tests.test_integration_bao import bao_dev, pytestmark # noqa: F401
def test_native_wrap_path_ttl_and_one_time_unwrap(bao_dev):
written=bao_dev._run(['kv','put','-format=json','secret/d3-wrap-fixture',
'marker=synthetic-wrapped-integration-value'])
assert written.returncode==0
wrapped=bao_dev.wrap_kv_get('secret','d3-wrap-fixture',ttl='60s')
assert wrapped.creation_path=='secret/data/d3-wrap-fixture'
assert wrapped.ttl=='60'
assert wrapped.wrap_token not in repr(wrapped) and wrapped.wrap_token not in json.dumps(wrapped.evidence())
recipient=OpenBaoClient(addr=bao_dev.addr,token=wrapped.wrap_token,bao_bin=bao_dev.bao_bin)
first=recipient._run(['unwrap','-format=json'])
assert first.returncode==0
assert json.loads(first.stdout)['data']['data']['marker']=='synthetic-wrapped-integration-value'
replay=recipient._run(['unwrap','-format=json'])
assert replay.returncode==2 and not replay.stdout.strip()
assert re.search(r'Code:\s*400\b',replay.stderr)
assert '* wrapping token is not valid or does not exist' in replay.stderr

View file

@ -24,7 +24,10 @@ unchanged identity-only login and is not a protected execution entitlement.
```task ```task
id: SECRETS-WP-0013-T01 id: SECRETS-WP-0013-T01
status: progress status: wait
needs_human: false
blocked_on: message-from:railiance-platform
blocking_reason: "Independent Platform mediated-contract review and identity/approval/PDP owner returns remain required; D3 wrapped primitive source is implemented, no native entitlement is inferred."
priority: high priority: high
state_hub_task_id: "b2f0d462-a542-518b-a26b-b7ec07b3c7c1" state_hub_task_id: "b2f0d462-a542-518b-a26b-b7ec07b3c7c1"
``` ```
@ -45,6 +48,8 @@ in the execution-contract document. No source owner assent, initiating-principal
admission, new policy package, ledger/mediator/backend or native acceptance is admission, new policy package, ledger/mediator/backend or native acceptance is
claimed. Independent source review remains required. Task statuses are unchanged. claimed. Independent source review remains required. Task statuses are unchanged.
2026-10-11: D3 response-wrapped primitive now verifies native creation path and bounded TTL/accessor, rejects overwrite/symlink/shared-parent paths, and reserves/fsyncs the private recipient before mint. Backend/write uncertainty retains a non-replayable reservation. Disposable OpenBao proves one successful unwrap and native consumed-token HTTP400 denial, not a production lane or entitlement. Separate delivery contracts cannot reuse metadata execute-ccr authority. Source design is ready for independent owner review; RPF-IN-0012 requests the Platform return. Identity/approval/PDP returns and the complete native mediated path remain open; no founder decision or foreign owner assent is fabricated.
## Prevent consumption without execution readiness ## Prevent consumption without execution readiness
```task ```task