diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md
index 141cce6..f945190 100644
--- a/WORK-RECORDS.md
+++ b/WORK-RECORDS.md
@@ -171,6 +171,7 @@
| task | FLEX-WP-0024-T04 | done | — | workplans/FLEX-WP-0024-decision-envelope-authenticity.md |
| task | FLEX-WP-0024-T05 | done | — | workplans/FLEX-WP-0024-decision-envelope-authenticity.md |
| task | FLEX-WP-0024-T06 | progress | — | workplans/FLEX-WP-0024-decision-envelope-authenticity.md |
+| task | FLEX-WP-0024-T07 | wait | — | workplans/FLEX-WP-0024-decision-envelope-authenticity.md |
| task | FLEX-WP-0025-T01 | done | — | workplans/FLEX-WP-0025-fact-versus-assertion.md |
| task | FLEX-WP-0025-T02 | done | — | workplans/FLEX-WP-0025-fact-versus-assertion.md |
| task | FLEX-WP-0025-T03 | done | — | workplans/FLEX-WP-0025-fact-versus-assertion.md |
diff --git a/docs/decision-envelope-signature.md b/docs/decision-envelope-signature.md
index 9d79b23..d2be986 100644
--- a/docs/decision-envelope-signature.md
+++ b/docs/decision-envelope-signature.md
@@ -166,3 +166,17 @@ owner-bound loopback tunnel and caller audience; installation of the reviewed
consumer verifier binary (the Python wheel alone omits it); exact consumer source
and hash drift check; and native positive/negative/rotation plus record-custody
receipts. Local fixtures and chart renders do not establish those outcomes.
+
+## Public receipt for consumer admission (2026-10-21)
+
+The consumer's trust file has a closed field set (`schema`, `evaluator`, `mode`,
+`policy_package`, `policy_version`, `policy_package_digest`, `keys{kid: pubkey}`), so
+provenance travels beside it, not in it. `tools/prepare_signer_public_receipt.py`
+reads only the public fields of the Platform `railiance-platform.signer-cas-receipt.v1`
+plus `docs/native-signer-public-pin.json`, refuses non-final custody, a kid other
+than the reviewed rollout kid, a malformed key or fingerprint, or any private-looking
+field, and writes two files: the exact trust bytes the founder signs, and
+`access-engine.signer-public-receipt.v1` carrying kid, public key, fingerprint, pin,
+the trust file's SHA-256 and the custody/CCR/plan/pin hashes. The Access owner
+authenticates the receipt by committing it under `docs/evidence/`. Neither file
+admits trust; the founder signature does (Secrets `docs/founder-admission.md`).
diff --git a/docs/native-signer-public-pin.json b/docs/native-signer-public-pin.json
new file mode 100644
index 0000000..4c501b0
--- /dev/null
+++ b/docs/native-signer-public-pin.json
@@ -0,0 +1,24 @@
+{
+ "schema": "access-engine.signer-public-pin.v1",
+ "purpose": "Exact issuer/policy pin the retained Secrets lifecycle PDP signs under. Input to tools/prepare_signer_public_receipt.py; never a key and never consumer trust by itself.",
+ "expected_kid": "flex-auth-lifecycle-20261016-r1",
+ "pin": {
+ "evaluator": "flex-auth/local",
+ "mode": "standalone",
+ "policy_package": "secrets-engine.catalog-lane.lifecycle",
+ "policy_version": "v2",
+ "policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4"
+ },
+ "derivation": {
+ "date": "2026-10-21",
+ "source_revision": "84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0",
+ "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f",
+ "policy_file": "examples/secrets-engine/policy_package.md",
+ "policy_file_sha256": "sha256:657fa9312c9dcabee059ec31c89ee13396fe9aa54d852edc9e6559a74bbc139a",
+ "command": "git archive 84c78e0 | tar -x -C
; cd ; go run ./cmd/access-engine check -policy examples/secrets-engine/policy_package.md -registry examples/secrets-engine/registry_snapshot.json -request examples/secrets-engine/check_request_allow_rotate.json",
+ "result": "provenance fields above; signature {mode: unsigned} (no key in source)",
+ "rollout_manifest": "the-custodian docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r2/manifest.json sha256:5be7a7f438624dcb35b7dbc96e7c9e5a8e2d5d1b1966cb4338aa5aecbe3a8b59",
+ "rollout_activation_values_sha256": "sha256:e76a30967add800ed8b588368757e7a5fa0ce4d8305ef658b5a4a08e377eb5b8",
+ "confirmation_required": "The first native signed allow/deny must carry these exact provenance values; tools/verify_native_signer_receipt.py refuses otherwise. A changed policy package, image or source is a new pin and a new receipt."
+ }
+}
diff --git a/tests/test_signer_public_receipt.py b/tests/test_signer_public_receipt.py
new file mode 100644
index 0000000..d7031b5
--- /dev/null
+++ b/tests/test_signer_public_receipt.py
@@ -0,0 +1,46 @@
+"""Offline regression for the signer public receipt projection (synthetic values only)."""
+import base64
+import hashlib
+import importlib.util
+import json
+from pathlib import Path
+import unittest
+
+ROOT = Path(__file__).resolve().parents[1]
+spec = importlib.util.spec_from_file_location('public_receipt', ROOT / 'tools/prepare_signer_public_receipt.py')
+MODULE = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(MODULE)
+PIN = (ROOT / 'docs/native-signer-public-pin.json').read_bytes()
+KEY = base64.urlsafe_b64encode(bytes(range(32))).decode().rstrip('=') # synthetic, not a real key
+
+
+def custody(**changes):
+ value = {'schema': MODULE.CUSTODY_SCHEMA, 'path': MODULE.CUSTODY_PATH, 'kv_version': 1,
+ 'status': 'custody-verified-pending-independent-public-admission',
+ 'kid': 'flex-auth-lifecycle-20261016-r1', 'public_key': KEY,
+ 'public_fingerprint': 'sha256:' + hashlib.sha256(KEY.encode()).hexdigest()}
+ value.update(changes)
+ return json.dumps(value).encode()
+
+
+class PublicReceiptTests(unittest.TestCase):
+ def test_trust_matches_secrets_schema(self):
+ trust_bytes, receipt = MODULE.build(custody(), PIN, b'ccr', b'plan')
+ trust = json.loads(trust_bytes)
+ self.assertEqual(set(trust), {'schema', 'evaluator', 'mode', 'policy_package', 'policy_version',
+ 'policy_package_digest', 'keys'})
+ self.assertEqual(trust['keys'], {'flex-auth-lifecycle-20261016-r1': KEY})
+ self.assertEqual(receipt['trust_file_sha256'], 'sha256:' + hashlib.sha256(trust_bytes).hexdigest())
+ self.assertNotIn(b'PRIVATE', json.dumps(receipt).encode())
+
+ def test_refusals(self):
+ for bad in (custody(kv_version=None), custody(status='write-intent-unknown'),
+ custody(kid='other'), custody(public_fingerprint='sha256:00'),
+ custody(public_key=KEY + '='), custody(ED25519_PRIVATE_KEY='x'),
+ custody(source_ccr_sha256='sha256:other')):
+ with self.assertRaises(ValueError):
+ MODULE.build(bad, PIN, b'ccr', b'plan')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/prepare_signer_public_receipt.py b/tools/prepare_signer_public_receipt.py
new file mode 100644
index 0000000..fb83e6c
--- /dev/null
+++ b/tools/prepare_signer_public_receipt.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+"""Project a Platform custody receipt into the Secrets trust file plus a public receipt.
+
+FLEX-WP-0024-T06. Offline only: no network, token, key generation or effect.
+Reads only the allowlisted public fields of `railiance-platform.signer-cas-receipt.v1`
+(kid, public_key, public_fingerprint, kv_version, path, status) and the committed
+Access pin (`docs/native-signer-public-pin.json`). Emits:
+
+* the exact `secrets-engine.pdp-trust.v1` bytes the founder signs
+ (`ssh-keygen -Y sign -n secrets-engine-pdp-trust pdp-trust.json`), and
+* `access-engine.signer-public-receipt.v1`: kid, public key, pin and provenance
+ hashes only. Owner authentication is the Access owner's commit of that receipt
+ under `docs/evidence/`; the receipt never admits trust on its own.
+"""
+from __future__ import annotations
+import argparse
+import base64
+import hashlib
+import json
+from pathlib import Path
+import re
+
+CUSTODY_SCHEMA = 'railiance-platform.signer-cas-receipt.v1'
+CUSTODY_PATH = 'platform/data/workloads/flex-auth/envelope-signing'
+FINAL = {'custody-verified-pending-independent-public-admission',
+ 'custody-reconciled-pending-independent-public-admission'}
+PIN_FIELDS = ('evaluator', 'mode', 'policy_package', 'policy_version', 'policy_package_digest')
+FORBIDDEN = re.compile(r'private|seed|secret_value|token', re.I)
+
+
+def sha(data: bytes) -> str:
+ return 'sha256:' + hashlib.sha256(data).hexdigest()
+
+
+def read(path: str, limit: int = 65536) -> bytes:
+ target = Path(path)
+ if not target.is_absolute() or target.is_symlink() or not target.is_file():
+ raise ValueError(f'absolute regular input required: {path}')
+ data = target.read_bytes()
+ if len(data) > limit:
+ raise ValueError(f'input too large: {path}')
+ return data
+
+
+def check_public_key(key: str, fingerprint: str) -> None:
+ if not isinstance(key, str) or not re.fullmatch(r'[A-Za-z0-9_-]{43}', key):
+ raise ValueError('public key must be unpadded base64url, 43 characters')
+ if len(base64.urlsafe_b64decode(key + '=')) != 32:
+ raise ValueError('public key must decode to 32 bytes')
+ if fingerprint != sha(key.encode()):
+ raise ValueError('public fingerprint does not match public key')
+
+
+def build(custody_raw: bytes, pin_raw: bytes, ccr_raw: bytes, plan_raw: bytes) -> tuple[bytes, dict]:
+ custody, pin_doc = json.loads(custody_raw), json.loads(pin_raw)
+ if any(FORBIDDEN.search(k) for k in custody):
+ raise ValueError('custody receipt carries a non-public field; refuse without echoing it')
+ if custody.get('schema') != CUSTODY_SCHEMA or custody.get('path') != CUSTODY_PATH:
+ raise ValueError('not the Platform envelope-signing custody receipt')
+ if custody.get('status') not in FINAL or custody.get('kv_version') != 1:
+ raise ValueError('custody not verified at exact KV version 1')
+ if custody.get('kid') != pin_doc['expected_kid']:
+ raise ValueError('custody kid differs from the reviewed rollout kid')
+ check_public_key(custody.get('public_key'), custody.get('public_fingerprint'))
+ if custody.get('source_ccr_sha256') not in (None, sha(ccr_raw)):
+ raise ValueError('custody receipt names a different CCR revision')
+ pin = pin_doc['pin']
+ if set(pin) != set(PIN_FIELDS) or any(not isinstance(pin[k], str) or not pin[k] for k in PIN_FIELDS):
+ raise ValueError('incomplete issuer/policy pin')
+ trust = {'schema': 'secrets-engine.pdp-trust.v1', **{k: pin[k] for k in PIN_FIELDS},
+ 'keys': {custody['kid']: custody['public_key']}}
+ trust_bytes = (json.dumps(trust, indent=2, sort_keys=True) + '\n').encode()
+ receipt = {
+ 'schema': 'access-engine.signer-public-receipt.v1',
+ 'kid': custody['kid'],
+ 'public_key': custody['public_key'],
+ 'public_fingerprint': custody['public_fingerprint'],
+ 'pin': {k: pin[k] for k in PIN_FIELDS},
+ 'trust_file_sha256': sha(trust_bytes),
+ 'provenance': {
+ 'custody_receipt_sha256': sha(custody_raw),
+ 'custody_status': custody['status'],
+ 'custody_kv_path': CUSTODY_PATH,
+ 'custody_kv_version': 1,
+ 'ccr_sha256': sha(ccr_raw),
+ 'native_plan_sha256': sha(plan_raw),
+ 'pin_file_sha256': sha(pin_raw),
+ 'source_revision': pin_doc['derivation']['source_revision'],
+ 'image': pin_doc['derivation']['image'],
+ 'rollout_manifest': pin_doc['derivation']['rollout_manifest'],
+ },
+ 'admission': 'none: founder signs the trust bytes; Secrets owner installs them (SECRETS-WP-0014-T04)',
+ }
+ return trust_bytes, receipt
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
+ for name in ('custody-receipt', 'pin', 'ccr', 'native-plan', 'trust-out', 'receipt-out'):
+ parser.add_argument('--' + name, required=True)
+ args = parser.parse_args()
+ trust_bytes, receipt = build(read(args.custody_receipt), read(args.pin), read(args.ccr),
+ read(args.native_plan, 262144))
+ for out, data in ((args.trust_out, trust_bytes),
+ (args.receipt_out, (json.dumps(receipt, indent=2, sort_keys=True) + '\n').encode())):
+ target = Path(out)
+ if not target.is_absolute() or target.exists():
+ raise SystemExit(f'refusing existing or relative output: {out}')
+ target.write_bytes(data)
+ print(json.dumps({'kid': receipt['kid'], 'public_fingerprint': receipt['public_fingerprint'],
+ 'trust_file_sha256': receipt['trust_file_sha256']}))
+ return 0
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
index 5c18d4a..467248b 100644
--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
+++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
@@ -15,7 +15,7 @@ related_workplans:
- FLEX-WP-0019
- FLEX-WP-0023
created: "2026-09-06"
-updated: "2026-10-18"
+updated: "2026-10-21"
state_hub_workstream_id: "90577acd-6910-548d-a13e-1dbfdfb8ed27"
---
@@ -365,3 +365,37 @@ Existing rollout R2/image84 and their approvals remain frozen; they do not admit
this source or a new image. T06 and all native custody/trust/rollout/recovery gates
remain open. Forgejo's complete resource/operation/inverse and session-revocation
coverage remains NK48-T43 / RAPPS16-T09 work.
+
+
+2026-10-21 Access owner review of CCR-2026-0050 (railiance-platform fd2f808; CCR
+sha256 66b3ea12d3f3bb870fa0aa0e7598e32abdb9bf39efe1adcfac286407abeb98e4, policy
+b9f816da…85ef, native plan 22306d50…06e5): **approved for Access scope** and sent to
+`railiance-platform` via State Hub. Secret name/key/mount/0440/kid in the CCR
+projection match frozen rollout R2 activation values byte for byte; candidate
+84c78e0 carries `--envelope-signing-key-file`. Public receipt prepared:
+`docs/native-signer-public-pin.json` (pin derived by running `check` at 84c78e0:
+`flex-auth/local`, `standalone`, `secrets-engine.catalog-lane.lifecycle`, `v2`,
+`sha256:bd11c5fe…b8b4`) and `tools/prepare_signer_public_receipt.py`, which projects
+the Platform `signer-cas-receipt.v1` into the exact `secrets-engine.pdp-trust.v1`
+bytes plus `access-engine.signer-public-receipt.v1` (kid, public key, pin,
+provenance hashes only). Rollout proposal stays R2; no parallel one. **No key exists
+yet**; no rollout, trust or admission happened. Founder go is T07.
+
+## 7. Founder go for the attended signer rollout
+
+```task
+id: FLEX-WP-0024-T07
+status: wait
+priority: high
+needs_human: true
+decision_id: "4fab263d-686b-4a06-9cea-8df619c87c03"
+```
+
+Founder go/no-go for one attended window, after Platform custody exists under
+CCR-2026-0050 (Platform-owned approval, not this task). Executes frozen R2 exactly:
+stop to 0, Platform CAS0 under fence, activation values (image `sha256:09a83b27…`,
+kid `flex-auth-lifecycle-20261016-r1`). Rollback to the stopped revision, then the
+reviewed unsigned `05a03a87` restore; never re-CAS or restore a seed. Afterwards:
+`prepare_signer_public_receipt.py` → commit receipt under `docs/evidence/` → founder
+signs `pdp-trust.json` (SECRETS-WP-0014-T04, decision e4eee965) →
+`verify_native_signer_receipt.py` on one native allow and deny. Then T06 closes.