feat(admission): founder-signed trust and recovery-attempt admission (WP14 T02, WP15 T05)
Reuse the gated-executor approval pattern: ssh-keygen -Y signatures over exact bytes, verified against a runtime-unwritable allowed_signers anchor with a fixed namespace per purpose. load_trust now refuses responder trust without a founder admission; the KeyCape selector preflight gains --admission for one exact, bounded recovery attempt. WP14 re-pointed to CUST-WP-0082-T06 and split founder key admission into human-gated T04; WP15 T04 now requires the signed admission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 809600@bnt-lap001 Assistant-Session: 40ba228f-c8a0-4ec9-bb29-3129951afdc4
This commit is contained in:
parent
497040c58c
commit
ebd8e49aff
15 changed files with 469 additions and 29 deletions
8
SCOPE.md
8
SCOPE.md
|
|
@ -39,8 +39,12 @@ point, not an identity provider, and not a general secrets API.
|
|||
- 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.
|
||||
package/version/artifact digest; unsigned responses fail closed. The trust
|
||||
file itself is admitted only by a founder `ssh-keygen -Y` signature over its
|
||||
exact bytes, checked against a runtime-unwritable `allowed_signers` anchor.
|
||||
This reuses the gated-executor approval pattern (`docs/founder-admission.md`).
|
||||
The real key's founder admission and the native rollout are still outstanding
|
||||
(WP14 T04/T03).
|
||||
|
||||
|
||||
- Resolves a catalog lane by catalog id or `approval.decision_ref`.
|
||||
|
|
|
|||
|
|
@ -89,12 +89,14 @@
|
|||
| task | SECRETS-WP-0013-T02 | done | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
|
||||
| task | SECRETS-WP-0013-T03 | wait | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
|
||||
| task | SECRETS-WP-0014-T01 | done | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
|
||||
| task | SECRETS-WP-0014-T02 | progress | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
|
||||
| task | SECRETS-WP-0014-T02 | done | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
|
||||
| task | SECRETS-WP-0014-T03 | wait | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
|
||||
| task | SECRETS-WP-0014-T04 | wait | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
|
||||
| task | SECRETS-WP-0015-T01 | done | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
|
||||
| task | SECRETS-WP-0015-T02 | done | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
|
||||
| task | SECRETS-WP-0015-T03 | done | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
|
||||
| task | SECRETS-WP-0015-T04 | wait | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
|
||||
| task | SECRETS-WP-0015-T05 | done | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
|
||||
| intake | SECRETS-IN-0001 | closed | — | intakes/intakes.md |
|
||||
| intake | SECRETS-IN-0002 | closed | — | intakes/intakes.md |
|
||||
| intake | SECRETS-IN-0003 | closed | — | intakes/intakes.md |
|
||||
|
|
|
|||
68
docs/founder-admission.md
Normal file
68
docs/founder-admission.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Founder admission (SSH signature over exact bytes)
|
||||
|
||||
Two inputs need authority from outside this process: the Access Engine
|
||||
responder trust file (SECRETS-WP-0014) and a KeyCape recovery attempt
|
||||
(SECRETS-WP-0015). The runtime itself must not be able to grant either. Both
|
||||
now use the approval pattern the estate already accepted in **gated-executor**
|
||||
(GEXEC-WP-0001, installed on railiance01 2026-10-20). The founder signs exact
|
||||
bytes with the passphrase-protected key, and the verifier trusts only an
|
||||
`allowed_signers` file that the runtime account cannot write.
|
||||
|
||||
Implementation: `src/secrets_engine/founder_admission.py`. No new approval
|
||||
service, network call or key type is involved. `ssh-keygen -Y` works offline,
|
||||
so the recovery case still works when KeyCape/OpenBao are down.
|
||||
|
||||
## Anchor
|
||||
|
||||
```
|
||||
/etc/secrets-engine/admission/ root:root 0755
|
||||
/etc/secrets-engine/admission/allowed_signers root:root 0644
|
||||
```
|
||||
|
||||
`SECRETS_ENGINE_ADMISSION_ANCHOR` may name another directory. The checks stay
|
||||
the same either way: the directory and file are not symlinks, not group/world
|
||||
writable, and not owned by the running UID unless that UID is root. The signer
|
||||
principal is read from the signature (`find-principals`), and it must match
|
||||
exactly one `allowed_signers` entry.
|
||||
|
||||
Namespaces are fixed per purpose, so a signature for one purpose cannot admit
|
||||
another. That includes a gexec action approval (`gexec-approval`).
|
||||
|
||||
| Purpose | Namespace | Signed file | Signature |
|
||||
| --- | --- | --- | --- |
|
||||
| PDP responder trust | `secrets-engine-pdp-trust` | `pdp-trust.json` | `pdp-trust.json.sig` |
|
||||
| Recovery attempt | `secrets-engine-recovery-attempt` | attempt admission JSON | `<file>.sig` |
|
||||
|
||||
## Admit or rotate responder trust
|
||||
|
||||
```bash
|
||||
ssh-keygen -Y sign -f ~/.ssh/<founder-key> -n secrets-engine-pdp-trust pdp-trust.json
|
||||
```
|
||||
|
||||
`load_trust` verifies the signature over the same bytes it parses. Any edit
|
||||
needs a new signature, so rotation means a newly signed file. Overlap is two
|
||||
keys in one signed file, and removal is a signed file without the old key.
|
||||
Neither the file's presence nor a loopback URL counts as admission.
|
||||
|
||||
## Admit one recovery attempt
|
||||
|
||||
```json
|
||||
{"schema": "secrets-engine.recovery-attempt-admission.v1",
|
||||
"selector_sha256": "sha256:<exact selector file bytes>",
|
||||
"source_sha256": "sha256:<service_auth.py>",
|
||||
"issuer": "https://kc.coulomb.social",
|
||||
"case": "secrets15-nk49-readiness",
|
||||
"not_before": "YYYY-MM-DDTHH:MM:SSZ", "not_after": "YYYY-MM-DDTHH:MM:SSZ"}
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh-keygen -Y sign -f ~/.ssh/<founder-key> -n secrets-engine-recovery-attempt attempt.json
|
||||
tools/check-keycape-recovery-selector.py --selector docs/keycape-recovery-selector.json \
|
||||
--source-sha256 sha256:<...> --admission attempt.json
|
||||
```
|
||||
|
||||
The window may be at most 24 hours. A pass reports
|
||||
`founder_admitted_attempt_not_native_acceptance`. That status proves the
|
||||
founder admitted this exact attempt. It does not prove the attempt ran, that it
|
||||
ran only once, or that the native result passed. Single-use consumption and
|
||||
cleanup stay with the bounded NK-WP-0049 execution process.
|
||||
|
|
@ -14,6 +14,9 @@ import subprocess
|
|||
from typing import Any
|
||||
|
||||
from secrets_engine.errors import DecisionError
|
||||
from secrets_engine.founder_admission import read_signature, verify_admission
|
||||
|
||||
TRUST_NAMESPACE = "secrets-engine-pdp-trust"
|
||||
|
||||
|
||||
def _unique_object(pairs):
|
||||
|
|
@ -38,11 +41,13 @@ def strict_json(data):
|
|||
|
||||
|
||||
def load_trust(path: Path | None) -> dict[str, Any]:
|
||||
"""Read an operator-owned public admission file; missing trust is a refusal.
|
||||
"""Read a founder-admitted public trust 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.
|
||||
The exact file bytes must carry a sibling ``<trust>.sig`` made with
|
||||
``ssh-keygen -Y sign -n secrets-engine-pdp-trust`` by a principal in the
|
||||
runtime-unwritable admission anchor (``founder_admission``). Rotation is a
|
||||
new signed file; an unsigned or edited file refuses. The executable's
|
||||
installation remains a rollout gate.
|
||||
"""
|
||||
try:
|
||||
if path is None or not Path(path).is_absolute() or Path(path).is_symlink():
|
||||
|
|
@ -55,6 +60,7 @@ def load_trust(path: Path | None) -> dict[str, Any]:
|
|||
raw = stream.read(65537)
|
||||
if len(raw) > 65536:
|
||||
raise ValueError("trust is oversized")
|
||||
admission = verify_admission(raw, read_signature(Path(str(path) + ".sig")), namespace=TRUST_NAMESPACE)
|
||||
trust = strict_json(raw)
|
||||
if set(trust) != {'schema', 'evaluator', 'mode', 'policy_package', 'policy_version', 'policy_package_digest', 'keys'}:
|
||||
raise ValueError("invalid trust fields")
|
||||
|
|
@ -69,11 +75,18 @@ def load_trust(path: Path | None) -> dict[str, Any]:
|
|||
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:
|
||||
return AdmittedTrust(trust, admission)
|
||||
except (OSError, ValueError, TypeError, KeyError, RecursionError, DecisionError) as exc:
|
||||
raise DecisionError("access-engine responder trust is missing or invalid") from exc
|
||||
|
||||
|
||||
class AdmittedTrust(dict):
|
||||
"""Trust fields plus the non-secret founder admission (principal, key fingerprint)."""
|
||||
def __init__(self, values, admission):
|
||||
super().__init__(values)
|
||||
self.admission = admission
|
||||
|
||||
|
||||
class AuthenticatedEnvelope(dict):
|
||||
"""Verified typed values with exact producer JSON retained for reauthentication."""
|
||||
def __init__(self, values, authenticated_json):
|
||||
|
|
|
|||
96
src/secrets_engine/founder_admission.py
Normal file
96
src/secrets_engine/founder_admission.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""Founder SSH-signature admission over exact bytes, never runtime self-approval.
|
||||
|
||||
Reuses the gated-executor approval pattern (GEXEC-WP-0001, accepted on
|
||||
railiance01 2026-10-20): ``ssh-keygen -Y verify`` against an ``allowed_signers``
|
||||
file the running account cannot write, with a fixed namespace per purpose so a
|
||||
signature for one purpose can never admit another (including a gexec action).
|
||||
|
||||
The anchor is ``/etc/secrets-engine/admission/allowed_signers`` unless
|
||||
``SECRETS_ENGINE_ADMISSION_ANCHOR`` names another directory. Relocating the
|
||||
anchor does not weaken it: the directory and file must still be owned by root
|
||||
or another account than the runtime, and not group/world writable. The signer
|
||||
principal is derived from the signature and must be listed in that file.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from secrets_engine.errors import DecisionError
|
||||
|
||||
DEFAULT_ANCHOR = Path("/etc/secrets-engine/admission")
|
||||
NAMESPACES = frozenset({"secrets-engine-pdp-trust", "secrets-engine-recovery-attempt"})
|
||||
SIG_HEADER = b"-----BEGIN SSH SIGNATURE-----"
|
||||
SIG_LIMIT = 8192
|
||||
PRINCIPAL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@-]{0,127}$")
|
||||
FINGERPRINT_RE = re.compile(r"key (SHA256:[A-Za-z0-9+/=]+)")
|
||||
SSH_ENV = {"PATH": "/usr/bin:/bin", "LC_ALL": "C"}
|
||||
|
||||
|
||||
def anchor_dir() -> Path:
|
||||
return Path(os.environ.get("SECRETS_ENGINE_ADMISSION_ANCHOR") or DEFAULT_ANCHOR)
|
||||
|
||||
|
||||
def _writable_by_runtime(info: os.stat_result) -> bool:
|
||||
"""The runtime must not be able to rewrite its own trust anchor."""
|
||||
euid = os.geteuid()
|
||||
return bool(info.st_mode & 0o022) or (info.st_uid == euid and euid != 0)
|
||||
|
||||
|
||||
def _read_anchor() -> Path:
|
||||
directory = anchor_dir()
|
||||
signers = directory / "allowed_signers"
|
||||
if not directory.is_absolute() or directory.is_symlink() or signers.is_symlink():
|
||||
raise ValueError("anchor must be absolute and not a symlink")
|
||||
for path, kind in ((directory, stat.S_ISDIR), (signers, stat.S_ISREG)):
|
||||
info = os.stat(path, follow_symlinks=False)
|
||||
if not kind(info.st_mode) or _writable_by_runtime(info):
|
||||
raise ValueError("anchor is writable by the runtime")
|
||||
return signers
|
||||
|
||||
|
||||
def read_signature(path: Path) -> bytes:
|
||||
if path.is_symlink():
|
||||
raise DecisionError("founder admission signature is missing or invalid")
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
raw = stream.read(SIG_LIMIT + 1)
|
||||
except OSError as exc:
|
||||
raise DecisionError("founder admission signature is missing or invalid") from exc
|
||||
if len(raw) > SIG_LIMIT or not raw.startswith(SIG_HEADER):
|
||||
raise DecisionError("founder admission signature is missing or invalid")
|
||||
return raw
|
||||
|
||||
|
||||
def verify_admission(data: bytes, signature: bytes, *, namespace: str) -> dict[str, str]:
|
||||
"""Verify an exact-byte founder signature; returns principal and key fingerprint."""
|
||||
try:
|
||||
if namespace not in NAMESPACES:
|
||||
raise ValueError("unknown admission namespace")
|
||||
if len(signature) > SIG_LIMIT or not signature.startswith(SIG_HEADER):
|
||||
raise ValueError("unsigned admission")
|
||||
signers = _read_anchor()
|
||||
with tempfile.TemporaryDirectory(prefix="se-admission-") as tmp:
|
||||
sig = Path(tmp) / "admission.sig"
|
||||
sig.write_bytes(signature)
|
||||
found = subprocess.run(
|
||||
["ssh-keygen", "-Y", "find-principals", "-f", str(signers), "-n", namespace, "-s", str(sig)],
|
||||
capture_output=True, env=SSH_ENV, timeout=5, check=False,
|
||||
)
|
||||
principals = found.stdout.decode("utf-8", "replace").split()
|
||||
if found.returncode != 0 or len(principals) != 1 or not PRINCIPAL_RE.fullmatch(principals[0]):
|
||||
raise ValueError("signer is not exactly one admitted principal")
|
||||
checked = subprocess.run(
|
||||
["ssh-keygen", "-Y", "verify", "-f", str(signers), "-I", principals[0], "-n", namespace, "-s", str(sig)],
|
||||
input=data, capture_output=True, env=SSH_ENV, timeout=5, check=False,
|
||||
)
|
||||
match = FINGERPRINT_RE.search(checked.stdout.decode("utf-8", "replace"))
|
||||
if checked.returncode != 0 or not match:
|
||||
raise ValueError("signature verification failed")
|
||||
return {"principal": principals[0], "key_fingerprint": match.group(1), "namespace": namespace}
|
||||
except (OSError, ValueError, subprocess.SubprocessError) as exc:
|
||||
raise DecisionError("founder admission is missing or invalid") from exc
|
||||
21
tests/conftest.py
Normal file
21
tests/conftest.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""Synthetic founder admission anchor for every test; never a verification bypass."""
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.signature_support import FOUNDER, admission_anchor
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def founder_key(tmp_path_factory):
|
||||
return admission_anchor(tmp_path_factory.mktemp('founder-admission'))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def founder_anchor(founder_key, monkeypatch):
|
||||
import secrets_engine.founder_admission as admission
|
||||
monkeypatch.setenv('SECRETS_ENGINE_ADMISSION_ANCHOR', str(founder_key['anchor']))
|
||||
# Tests run as the anchor's owner; keep the mode-bit half of the rule.
|
||||
real_rule = admission._writable_by_runtime
|
||||
monkeypatch.setattr(admission, '_writable_by_runtime', lambda info: bool(info.st_mode & 0o022))
|
||||
return {**founder_key, 'real_rule': real_rule}
|
||||
|
|
@ -6,6 +6,26 @@ import subprocess
|
|||
import pytest
|
||||
|
||||
PUBLIC='IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'
|
||||
FOUNDER='founder@synthetic'
|
||||
|
||||
|
||||
def admission_anchor(directory):
|
||||
"""Synthetic founder key plus allowed_signers anchor, like /etc/gexec."""
|
||||
key=directory/'founder'
|
||||
subprocess.run(['ssh-keygen','-q','-t','ed25519','-N','','-C',FOUNDER,'-f',str(key)],check=True,timeout=30)
|
||||
anchor=directory/'anchor';anchor.mkdir(mode=0o755)
|
||||
(anchor/'allowed_signers').write_text(FOUNDER+' '+(directory/'founder.pub').read_text())
|
||||
(anchor/'allowed_signers').chmod(0o644)
|
||||
return {'key':key,'anchor':anchor}
|
||||
|
||||
|
||||
def admit(path,namespace='secrets-engine-pdp-trust',key=None):
|
||||
"""Founder-sign exact file bytes as `<path>.sig` (ssh-keygen -Y sign)."""
|
||||
import os as _os
|
||||
key=key or Path(_os.environ['SECRETS_ENGINE_ADMISSION_ANCHOR']).parent/'founder'
|
||||
Path(str(path)+'.sig').unlink(missing_ok=True)
|
||||
subprocess.run(['ssh-keygen','-q','-Y','sign','-f',str(key),'-n',namespace,str(path)],check=True,timeout=30,capture_output=True)
|
||||
return path
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def signature_tools(tmp_path_factory):
|
||||
|
|
@ -22,7 +42,7 @@ 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))
|
||||
path=directory/'pdp-trust.json';path.write_text(json.dumps(trust));admit(path)
|
||||
from dataclasses import is_dataclass, replace
|
||||
if is_dataclass(cfg):
|
||||
return replace(cfg,pdp_trust_file=path,pdp_verifier=tools/'verify')
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ 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
|
||||
from tests.signature_support import signature_tools, admit
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def verifier(signature_tools):
|
||||
|
|
@ -29,7 +29,7 @@ def trust_file(tmp_path):
|
|||
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
|
||||
path = tmp_path/'trust.json'; path.write_text(json.dumps(trust)); return admit(path)
|
||||
|
||||
|
||||
def checked(body, credential, trust_file, verifier):
|
||||
|
|
@ -63,17 +63,17 @@ def test_response_authentication_refuses(credential, trust_file, verifier, chang
|
|||
|
||||
@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))
|
||||
trust=json.loads(trust_file.read_text()); trust[field]='wrong'; trust_file.write_text(json.dumps(trust)); admit(trust_file)
|
||||
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)
|
||||
trust_file.write_text(json.dumps(trust)); admit(trust_file); 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))
|
||||
del trust['keys']['testdata-ed25519']; trust_file.write_text(json.dumps(trust)); admit(trust_file)
|
||||
with pytest.raises(DecisionError): checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
|
||||
checked(next_body,credential,trust_file,verifier)
|
||||
|
||||
|
|
|
|||
68
tests/test_founder_admission.py
Normal file
68
tests/test_founder_admission.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"""Founder admission of the responder trust file (SECRETS-WP-0014-T02)."""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from secrets_engine import founder_admission
|
||||
from secrets_engine.decision_signature import load_trust
|
||||
from secrets_engine.errors import DecisionError
|
||||
from tests.signature_support import PUBLIC, FOUNDER, admit
|
||||
|
||||
TRUST = {'schema':'secrets-engine.pdp-trust.v1','evaluator':'stub/local','mode':'standalone',
|
||||
'policy_package':'p','policy_version':'v','policy_package_digest':'sha256:'+'1'*64,
|
||||
'keys':{'testdata-ed25519':PUBLIC}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def trust(tmp_path):
|
||||
path=tmp_path/'pdp-trust.json';path.write_text(json.dumps(TRUST));return path
|
||||
|
||||
|
||||
def test_admitted_trust_carries_founder_identity_only(trust):
|
||||
loaded=load_trust(admit(trust))
|
||||
assert dict(loaded)==TRUST
|
||||
assert loaded.admission['principal']==FOUNDER
|
||||
assert loaded.admission['key_fingerprint'].startswith('SHA256:')
|
||||
assert loaded.admission['namespace']=='secrets-engine-pdp-trust'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('change',['unsigned','edited','other-namespace','gexec-namespace','garbage-signature'])
|
||||
def test_unadmitted_trust_refuses(trust,change):
|
||||
if change=='unsigned':pass
|
||||
elif change=='edited':
|
||||
admit(trust);trust.write_text(json.dumps({**TRUST,'keys':{'attacker':PUBLIC}}))
|
||||
elif change=='other-namespace':admit(trust,namespace='secrets-engine-recovery-attempt')
|
||||
elif change=='gexec-namespace':admit(trust,namespace='gexec-approval')
|
||||
else:(trust.parent/(trust.name+'.sig')).write_text('-----BEGIN SSH SIGNATURE-----\nAAAA\n')
|
||||
with pytest.raises(DecisionError):load_trust(trust)
|
||||
|
||||
|
||||
def test_key_outside_anchor_refuses(trust,tmp_path):
|
||||
key=tmp_path/'intruder'
|
||||
subprocess.run(['ssh-keygen','-q','-t','ed25519','-N','','-f',str(key)],check=True,timeout=30)
|
||||
with pytest.raises(DecisionError):load_trust(admit(trust,key=key))
|
||||
|
||||
|
||||
def test_runtime_writable_anchor_refuses(trust,monkeypatch,founder_anchor):
|
||||
admit(trust)
|
||||
# The anchor is owned by the test's own uid, so the production rule must refuse.
|
||||
monkeypatch.setattr(founder_admission,'_writable_by_runtime',founder_anchor['real_rule'])
|
||||
assert os.stat(founder_anchor['anchor']).st_uid==os.geteuid()!=0
|
||||
with pytest.raises(DecisionError):load_trust(trust)
|
||||
|
||||
|
||||
def test_group_writable_anchor_refuses(trust,founder_anchor):
|
||||
admit(trust);signers=founder_anchor['anchor']/'allowed_signers'
|
||||
signers.chmod(0o664)
|
||||
try:
|
||||
with pytest.raises(DecisionError):load_trust(trust)
|
||||
finally:signers.chmod(0o644)
|
||||
|
||||
|
||||
def test_relocated_anchor_without_signers_refuses(trust,tmp_path,monkeypatch):
|
||||
admit(trust);empty=tmp_path/'empty';empty.mkdir()
|
||||
monkeypatch.setenv('SECRETS_ENGINE_ADMISSION_ANCHOR',str(empty))
|
||||
with pytest.raises(DecisionError):load_trust(trust)
|
||||
|
||||
|
|
@ -29,3 +29,39 @@ def test_oversize_public_input_refused(tmp_path):
|
|||
def test_symlink_public_input_refused(tmp_path):
|
||||
p=tmp_path/'selector.json';p.write_text('{}');link=tmp_path/'link';link.symlink_to(p)
|
||||
with pytest.raises(ValueError):m.read(link)
|
||||
|
||||
# Founder attempt admission (SECRETS-WP-0015-T05), gated-executor signature pattern.
|
||||
from datetime import datetime, timezone
|
||||
from secrets_engine.errors import DecisionError
|
||||
from tests.signature_support import admit, FOUNDER
|
||||
NOW=datetime(2026,10,21,12,0,0,tzinfo=timezone.utc)
|
||||
ISSUER='https://kc.coulomb.social'
|
||||
SELECTOR_RAW=json.dumps({'issuer':ISSUER,'consumer':SELECTOR}).encode()
|
||||
ADMISSION={'schema':'secrets-engine.recovery-attempt-admission.v1','selector_sha256':'sha256:'+hashlib.sha256(SELECTOR_RAW).hexdigest(),
|
||||
'source_sha256':DIGEST,'issuer':ISSUER,'case':'secrets15-nk49-readiness',
|
||||
'not_before':'2026-10-21T11:00:00Z','not_after':'2026-10-21T13:00:00Z'}
|
||||
def signed(tmp_path,payload=ADMISSION,namespace='secrets-engine-recovery-attempt'):
|
||||
p=tmp_path/'attempt.json';p.write_text(json.dumps(payload));admit(p,namespace=namespace)
|
||||
return p.read_bytes(),(tmp_path/'attempt.json.sig').read_bytes()
|
||||
def attempt(data,sig,selector_raw=SELECTOR_RAW,now=NOW):
|
||||
return m.verify_attempt(data,sig,selector_raw=selector_raw,issuer=ISSUER,source_sha256=DIGEST,now=now)
|
||||
def test_founder_admitted_attempt_binds_exact_inputs(tmp_path):
|
||||
result=attempt(*signed(tmp_path))
|
||||
assert result['principal']==FOUNDER and result['case']=='secrets15-nk49-readiness'
|
||||
assert result['namespace']=='secrets-engine-recovery-attempt'
|
||||
@pytest.mark.parametrize('field,value',[('selector_sha256','sha256:'+'0'*64),('source_sha256','sha256:'+'0'*64),
|
||||
('issuer','https://other'),('case','Bad Case'),('schema','other'),('not_after','2026-10-22T12:00:01Z'),
|
||||
('not_before','2026-10-21T12:00:01Z'),('not_after','2026-10-21T12:00:00Z')])
|
||||
def test_attempt_binding_mismatch_refused(tmp_path,field,value):
|
||||
with pytest.raises((ValueError,DecisionError)):attempt(*signed(tmp_path,{**ADMISSION,field:value}))
|
||||
def test_attempt_signature_negatives_refused(tmp_path):
|
||||
data,sig=signed(tmp_path)
|
||||
for bad in [(data.replace(b'secrets15',b'secrets16'),sig),signed(tmp_path,namespace='secrets-engine-pdp-trust'),
|
||||
signed(tmp_path,namespace='gexec-approval'),(data,b'')]:
|
||||
with pytest.raises((ValueError,DecisionError)):attempt(*bad)
|
||||
def test_attempt_refuses_changed_selector(tmp_path):
|
||||
with pytest.raises((ValueError,DecisionError)):attempt(*signed(tmp_path),selector_raw=SELECTOR_RAW+b' ')
|
||||
def test_cli_admission_refusal_is_suppressed(tmp_path,capsys,monkeypatch):
|
||||
sel=tmp_path/'selector.json';sel.write_bytes(SELECTOR_RAW);(tmp_path/'a.json').write_text('{}')
|
||||
monkeypatch.setattr('sys.argv',['x','--selector',str(sel),'--source-sha256',DIGEST,'--admission',str(tmp_path/'a.json')])
|
||||
assert m.main()==1 and 'refused' in capsys.readouterr().out
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import pytest
|
|||
|
||||
from tests.test_decision_signature import FIXTURE, PUBLIC
|
||||
from tests.test_decision_transport import server
|
||||
from tests.signature_support import admit
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
|
@ -74,11 +75,13 @@ def test_clean_install_uses_installed_wheel_and_signed_boundary(installed,tmp_pa
|
|||
envelope=json.loads(FIXTURE.read_bytes())
|
||||
trust={name:envelope['provenance'][name] for name in ('evaluator','mode','policy_package','policy_version','policy_package_digest')}
|
||||
trust.update(schema='secrets-engine.pdp-trust.v1',keys={'testdata-ed25519':PUBLIC})
|
||||
trust_file=tmp_path/'synthetic-trust.json';trust_file.write_text(json.dumps(trust))
|
||||
trust_file=tmp_path/'synthetic-trust.json';trust_file.write_text(json.dumps(trust));admit(trust_file)
|
||||
token=tmp_path/'synthetic.token';token.write_text('synthetic-pdp-credential');token.chmod(0o600)
|
||||
script='''import pathlib,sys
|
||||
import secrets_engine.decision_check as installed
|
||||
from secrets_engine.errors import DecisionError
|
||||
import secrets_engine.founder_admission as admission
|
||||
admission._writable_by_runtime=lambda info: bool(info.st_mode & 0o022) # test runs as anchor owner
|
||||
assert str(pathlib.Path(installed.__file__).resolve()).startswith(sys.argv[1]+'/venv/')
|
||||
try:
|
||||
result=installed.check_decision(base_url=sys.argv[2],token_file=pathlib.Path(sys.argv[3]),request={},trust_file=pathlib.Path(sys.argv[4]),verifier=pathlib.Path(sys.argv[1]+'/libexec/secrets-engine-pdp-verify'))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from secrets_engine.execution_contract import ExecutionContract
|
|||
from secrets_engine.task_execution_handoff import (authorize_task_execution,consume_task_execution,
|
||||
TaskExecutionAuthorization)
|
||||
from tests.authorization_stub import AuthorizationStub
|
||||
from tests.signature_support import signature_tools, admit_tools, sign_envelope
|
||||
from tests.signature_support import signature_tools, admit_tools, sign_envelope, admit
|
||||
from tests.test_consume_binding_join import _Cfg,_token,APPROVAL_ID
|
||||
from tests.test_execution_context import entry,descriptor
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ def test_changed_owner_coordinates_refuse_before_cas(lane,field):
|
|||
def test_removed_signer_refuses_before_cas(lane):
|
||||
cfg,e,contract,state=lane;result=prepared(lane)
|
||||
trust=json.loads(cfg.pdp_trust_file.read_text());trust['keys']={'other':next(iter(trust['keys'].values()))}
|
||||
cfg.pdp_trust_file.write_text(json.dumps(trust))
|
||||
cfg.pdp_trust_file.write_text(json.dumps(trust));admit(cfg.pdp_trust_file)
|
||||
with pytest.raises(DecisionError):consume_task_execution(cfg,result)
|
||||
assert 'consume' not in state['calls']
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,16 @@
|
|||
|
||||
Checks current consumer source constants and the producer's already-deployed
|
||||
public env/denial selector. This is source metadata, not runtime/owner assent.
|
||||
|
||||
With --admission, additionally requires a founder SSH signature (gated-executor
|
||||
pattern, namespace secrets-engine-recovery-attempt) over an attempt admission
|
||||
that binds these exact selector bytes, source hash, issuer, case and a bounded
|
||||
window. That is checkable founder authority for one attempt, still not native
|
||||
acceptance; single-use consumption stays with the bounded NK49 execution.
|
||||
"""
|
||||
import argparse
|
||||
import ast
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
|
@ -15,6 +22,8 @@ import stat
|
|||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
LIMIT=128*1024
|
||||
ADMISSION_FIELDS={'schema','selector_sha256','source_sha256','issuer','case','not_before','not_after'}
|
||||
MAX_WINDOW=86400
|
||||
|
||||
def read(path):
|
||||
path=Path(os.path.abspath(path))
|
||||
|
|
@ -51,13 +60,38 @@ def verify(selector,source,source_sha256):
|
|||
return {'schema':'secrets-engine.recovery-selector-preflight.v1','status':'source_selector_matches_not_native_assent',
|
||||
'source_sha256':source_sha256,'consumer':expected,'credential_source_exercised':False,'network_contacted':False}
|
||||
|
||||
def ts(value):
|
||||
if not isinstance(value,str) or not re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z',value):raise ValueError()
|
||||
return datetime.strptime(value,'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
|
||||
|
||||
def verify_attempt(admission,signature,*,selector_raw,issuer,source_sha256,now=None):
|
||||
"""Founder-signed exact attempt binding; the signature covers the admission bytes."""
|
||||
import sys;sys.path.insert(0,str(ROOT/'src'))
|
||||
from secrets_engine.founder_admission import verify_admission
|
||||
payload=json.loads(admission,object_pairs_hook=unique)
|
||||
if not isinstance(payload,dict) or set(payload)!=ADMISSION_FIELDS:raise ValueError()
|
||||
if payload['schema']!='secrets-engine.recovery-attempt-admission.v1':raise ValueError()
|
||||
if payload['selector_sha256']!='sha256:'+hashlib.sha256(selector_raw).hexdigest():raise ValueError()
|
||||
if payload['source_sha256']!=source_sha256 or payload['issuer']!=issuer:raise ValueError()
|
||||
if not isinstance(payload['case'],str) or not re.fullmatch(r'[a-z0-9][a-z0-9.-]{0,127}',payload['case']):raise ValueError()
|
||||
start,end=ts(payload['not_before']),ts(payload['not_after']);now=now or datetime.now(timezone.utc)
|
||||
if not start<=now<end or (end-start).total_seconds()>MAX_WINDOW:raise ValueError()
|
||||
signer=verify_admission(admission,signature,namespace='secrets-engine-recovery-attempt')
|
||||
return {'case':payload['case'],'not_after':payload['not_after'],**signer}
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--selector',required=True);p.add_argument('--source-sha256',required=True)
|
||||
p.add_argument('--admission',help='founder-signed attempt admission JSON; signature at <admission>.sig')
|
||||
args=p.parse_args()
|
||||
try:
|
||||
proposal=json.loads(read(args.selector),object_pairs_hook=unique)
|
||||
selector_raw=read(args.selector)
|
||||
proposal=json.loads(selector_raw,object_pairs_hook=unique)
|
||||
if not isinstance(proposal,dict) or not isinstance(proposal.get('consumer'),dict):raise ValueError()
|
||||
result=verify(proposal['consumer'],read(ROOT/'src/secrets_engine/service_auth.py'),args.source_sha256)
|
||||
if args.admission:
|
||||
result['attempt_admission']=verify_attempt(read(args.admission),read(args.admission+'.sig'),
|
||||
selector_raw=selector_raw,issuer=proposal.get('issuer'),source_sha256=args.source_sha256)
|
||||
result['status']='founder_admitted_attempt_not_native_acceptance'
|
||||
except Exception:print('{"status":"source_selector_refused","private_diagnostics_suppressed":true}');return 1
|
||||
print(json.dumps(result,indent=2));return 0
|
||||
|
||||
|
|
|
|||
|
|
@ -8,18 +8,29 @@ status: active
|
|||
owner: codex
|
||||
topic_slug: secrets-engine
|
||||
created: "2026-10-12"
|
||||
updated: "2026-10-16"
|
||||
updated: "2026-10-21"
|
||||
flavor: implementation
|
||||
depends_on: []
|
||||
related: [CUST-WP-0082, FLEX-WP-0024, GEXEC-WP-0001, SECRETS-WP-0013]
|
||||
state_hub_workstream_id: "7252469d-d7a5-5c18-8911-b511b8c6a736"
|
||||
---
|
||||
|
||||
Native owner work for CUST-WP-0080 G13. Reuses the accepted Check contract and
|
||||
Native owner work for G13, now tracked as CUST-WP-0082-T06. CUST-WP-0080 finished
|
||||
on 2026-10-19 and transferred its T21 native acceptance obligation to WP-82 without
|
||||
implementing it. Reuses the accepted Check contract and
|
||||
FLEX-WP-0024 producer signature surface; does not create another PDP or grant
|
||||
any catalog-lane execution authority. SECRETS-WP-0013 still owns task-scoped
|
||||
protected execution. Transport review is complete; key admission and native
|
||||
rollout remain separate gates below.
|
||||
|
||||
2026-10-21 review against gated-executor (GEXEC-WP-0001, accepted on railiance01
|
||||
2026-10-20): the estate now has a working, founder-held exact-byte approval
|
||||
mechanism (`ssh-keygen -Y`, runtime-unwritable `allowed_signers`, per-purpose
|
||||
namespace). T02's open "independent admission" gap had no concrete anchor before
|
||||
this; it now reuses that pattern. T02 closes at source scope. The founder's actual
|
||||
signature over the real producer trust file is split out as human-gated T04, and
|
||||
T03's native rollout depends on it.
|
||||
|
||||
## Prevent the Check credential from leaving the admitted local transport
|
||||
|
||||
```task
|
||||
|
|
@ -44,7 +55,7 @@ This closes the source transport defects only; T02/T03 remain mandatory.
|
|||
|
||||
```task
|
||||
id: SECRETS-WP-0014-T02
|
||||
status: progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "877782e5-045b-5186-8b95-82e25db21a5a"
|
||||
```
|
||||
|
|
@ -86,21 +97,52 @@ artifacts, signed HTTP consumption and missing/tampered artifact refusal; they d
|
|||
not admit packages, keys or a production prefix. T02 remains progress and T03
|
||||
wait; no native installation, trust change or protected entitlement is claimed.
|
||||
|
||||
2026-10-21 founder admission anchor (closes T02 source scope): `load_trust` now
|
||||
refuses any trust file without a sibling `<trust>.sig`. The signature must be made
|
||||
with `ssh-keygen -Y sign -n secrets-engine-pdp-trust` by exactly one principal in
|
||||
`/etc/secrets-engine/admission/allowed_signers`, and is verified over the exact
|
||||
bytes that are parsed. That anchor and directory must not be writable by, or owned
|
||||
by, the running UID (gated-executor rule), which closes the same-UID trust
|
||||
self-admission path recorded above. `src/secrets_engine/founder_admission.py`;
|
||||
operator procedure `docs/founder-admission.md`. Tests cover unsigned, edited after
|
||||
signing, wrong namespace (including `gexec-approval`), unknown signer key,
|
||||
runtime-owned or group-writable anchor and missing anchor, plus re-signed
|
||||
rotation overlap/removal in the existing signature and installed-bundle tests.
|
||||
No production key, anchor installation or trust admission occurred. That work is T04.
|
||||
|
||||
## Admit the real producer verification key by founder signature
|
||||
|
||||
```task
|
||||
id: SECRETS-WP-0014-T04
|
||||
status: wait
|
||||
priority: high
|
||||
needs_human: true
|
||||
depends_on: [SECRETS-WP-0014-T02]
|
||||
blocking_reason: "Needs Access's real producer kid/public key/custody receipt (FLEX-WP-0024) and the founder's signature over the exact trust file; the anchor install under /etc needs root."
|
||||
```
|
||||
|
||||
Install `/etc/secrets-engine/admission/allowed_signers` (root-owned) with the
|
||||
founder's approver public key, the same key that signs gexec approvals. Write
|
||||
the exact `pdp-trust.json` for the real producer kid/key and pin. The founder
|
||||
signs it in namespace `secrets-engine-pdp-trust`. Record only the principal, key
|
||||
fingerprint and trust-file hash. This is founder action, not worker
|
||||
work. A local signature test is not admission.
|
||||
|
||||
## Verify the admitted native consumer and rotation path
|
||||
|
||||
```task
|
||||
id: SECRETS-WP-0014-T03
|
||||
status: wait
|
||||
priority: high
|
||||
depends_on: [SECRETS-WP-0014-T02]
|
||||
blocking_reason: "Requires reviewed verification-key admission, source enforcement and exact native rollout receipt; no production trust change is proposed yet."
|
||||
depends_on: [SECRETS-WP-0014-T02, SECRETS-WP-0014-T04]
|
||||
blocking_reason: "Requires the founder-signed key admission (T04) and an exact native rollout receipt; source enforcement is done (T02)."
|
||||
state_hub_task_id: "85aa4c78-b66d-5201-8e31-0ed50e7eaeec"
|
||||
```
|
||||
|
||||
Under an exact reviewed rollout, prove the real pin and consumer accept the
|
||||
admitted signer, reject forgery/downgrade, rotate without accepting removed keys,
|
||||
and preserve request/approval binding, lifetime and single-use consumption.
|
||||
Retain rollback and failure evidence. Update CUST-WP-0080 G13 only with these
|
||||
Retain rollback and failure evidence. Update CUST-WP-0082-T06 (G13) only with these
|
||||
native receipts; do not equate passing transport tests with closure.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ status: active
|
|||
owner: codex
|
||||
topic_slug: custodian
|
||||
created: "2026-10-17"
|
||||
updated: "2026-10-17"
|
||||
updated: "2026-10-21"
|
||||
flavor: implementation
|
||||
depends_on: []
|
||||
related: [CUST-WP-0080, NK-WP-0049, SECRETS-WP-0012, SECRETS-WP-0008, SECRETS-WP-0013]
|
||||
related: [CUST-WP-0082, NK-WP-0049, SECRETS-WP-0012, SECRETS-WP-0008, SECRETS-WP-0013, SECRETS-WP-0014, GEXEC-WP-0001]
|
||||
state_hub_workstream_id: "68a0642e-4908-5260-89c9-3adce60e6a4d"
|
||||
---
|
||||
|
||||
|
|
@ -22,6 +22,14 @@ it is not native assent. Finished0012 remains finished; functional adoption/back
|
|||
entitlement stay0008T06/0013. No new grant,
|
||||
secret fetch, provider login, production redirect or external message occurred.
|
||||
|
||||
2026-10-21 review against gated-executor (GEXEC-WP-0001, accepted on railiance01
|
||||
2026-10-20): the T03 plan requires "surviving offline founder authorization" bound
|
||||
to the exact source/issuer/case/deadline, with "no new auth or approval service".
|
||||
The gexec approval pattern (founder `ssh-keygen -Y` signature, runtime-unwritable
|
||||
`allowed_signers`) is that existing mechanism, and it verifies offline during an
|
||||
outage. T05 makes it a checkable precondition of the selector preflight, and T04
|
||||
now requires it. CUST-WP-0080 finished; its remaining native gates live in CUST-WP-0082.
|
||||
|
||||
## Establish exact current intended source selector
|
||||
|
||||
```task
|
||||
|
|
@ -94,6 +102,29 @@ original OpenBao and its custody/OIDC/MFA graph for full consumer continuity;
|
|||
exact native action approval and resource-server negative/revocation receipts.
|
||||
Protected backend operation entitlement stays in existing0008T06/0013.
|
||||
|
||||
## Require founder-signed exact attempt admission
|
||||
|
||||
```task
|
||||
id: SECRETS-WP-0015-T05
|
||||
status: done
|
||||
priority: high
|
||||
assignee: claude
|
||||
depends_on: [SECRETS-WP-0015-T03]
|
||||
```
|
||||
|
||||
Completed 2026-10-21 as source. `tools/check-keycape-recovery-selector.py --admission`
|
||||
verifies a `secrets-engine.recovery-attempt-admission.v1` document. It binds the
|
||||
exact selector-file SHA-256, the consumer source hash, the issuer, the case id and
|
||||
a window of at most 24 hours. The founder signs it in namespace
|
||||
`secrets-engine-recovery-attempt` against the shared runtime-unwritable anchor
|
||||
(`src/secrets_engine/founder_admission.py`, `docs/founder-admission.md`; same
|
||||
anchor as SECRETS-WP-0014). A pass reports
|
||||
`founder_admitted_attempt_not_native_acceptance`. Synthetic tests cover every
|
||||
binding mismatch, the expired/early/overlong window, edits after signing, the
|
||||
trust/gexec namespaces, an empty signature, a changed selector and suppressed CLI
|
||||
diagnostics. No private archive, token, endpoint or real founder signature was
|
||||
touched. Single-use consumption and cleanup remain in the bounded NK49 execution.
|
||||
|
||||
## Verify the separately admitted actual isolated consumer result
|
||||
|
||||
```task
|
||||
|
|
@ -102,11 +133,13 @@ status: wait
|
|||
priority: high
|
||||
assignee: codex
|
||||
needs_human: true
|
||||
depends_on: [SECRETS-WP-0015-T03, NK-WP-0049-T33]
|
||||
blocking_reason: "Source plan delivered in T03. Await NK-WP-0049-T33 original TLS/archive/private admission, exact native owner authority, and original OpenBao/custody recovery for actual consumer continuity."
|
||||
depends_on: [SECRETS-WP-0015-T03, SECRETS-WP-0015-T05, NK-WP-0049-T33]
|
||||
blocking_reason: "Source plan (T03) and signed-admission check (T05) delivered. Await NK-WP-0049-T33 original TLS/archive/private admission, the founder's signed attempt admission for the exact case, and original OpenBao/custody recovery for actual consumer continuity."
|
||||
state_hub_task_id: "aeec6eb0-39e2-5f7c-8f5d-778133edfc73"
|
||||
```
|
||||
|
||||
Before any key prompt, the selector preflight must pass with `--admission` on the
|
||||
founder-signed attempt document for this exact case (T05).
|
||||
Only a separately admitted owner action can establish actual archived registration,
|
||||
issuer trust, native negative checks and consumer continuity. Source selector and
|
||||
historical provider proof do not fill this gate. No production consumer redirect,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue