#!/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())