build: package CLI with pinned producer verifier
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
c80a944105
commit
ebca4f5adb
9 changed files with 433 additions and 1 deletions
86
scripts/build_release_bundle.py
Normal file
86
scripts/build_release_bundle.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build wheel + pinned producer verifier; install is a separate reviewed step."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run(command, **kwargs):
|
||||
return subprocess.check_output(command,text=True,**kwargs).strip()
|
||||
|
||||
|
||||
def build(root, output, dependency_wheel, uv, allow_build_downloads=False):
|
||||
if platform.system() != 'Linux':
|
||||
raise ValueError('this bundle supports Linux CLI only')
|
||||
# No discovered keys, source rewriting, or runtime compilation.
|
||||
subprocess.run([sys.executable,str(root/'scripts/check_pdp_producer_source.py')],check=True,stdout=sys.stderr)
|
||||
locked=next(p for p in tomllib.loads((root/'uv.lock').read_text())['package'] if p['name']=='pyyaml')
|
||||
admitted={Path(item['url']).name:item['hash'].removeprefix('sha256:') for item in locked['wheels']}
|
||||
if dependency_wheel.name not in admitted or digest(dependency_wheel)!=admitted[dependency_wheel.name]:
|
||||
raise ValueError('supplied PyYAML wheel does not match uv.lock')
|
||||
output.mkdir(mode=0o700,parents=False,exist_ok=False)
|
||||
epoch=run(['git','log','-1','--format=%ct'],cwd=root)
|
||||
commit=run(['git','rev-parse','HEAD'],cwd=root)
|
||||
architecture={'x86_64':'amd64','aarch64':'arm64'}.get(platform.machine())
|
||||
if architecture is None:
|
||||
raise ValueError('unsupported Linux architecture')
|
||||
env={**os.environ,'CGO_ENABLED':'0','GOOS':'linux','GOARCH':architecture,
|
||||
'GOFLAGS':'','GOWORK':'off','SOURCE_DATE_EPOCH':epoch}
|
||||
(output/'bin').mkdir();(output/'wheels').mkdir()
|
||||
subprocess.run(['go','build','-trimpath','-buildvcs=false','-ldflags=-buildid=',
|
||||
'-o',str(output/'bin/secrets-engine-pdp-verify'),'.'],cwd=root/'verifier',env=env,check=True)
|
||||
command=[uv,'--no-config','build','--wheel','--out-dir',str(output/'wheels'),str(root)]
|
||||
if not allow_build_downloads:command.append('--offline')
|
||||
subprocess.run(command,env=env,check=True)
|
||||
wheel,=list((output/'wheels').glob('secrets_engine-*.whl'))
|
||||
shutil.copyfile(dependency_wheel,output/'wheels'/dependency_wheel.name)
|
||||
helper_hash=digest(output/'bin/secrets-engine-pdp-verify')
|
||||
launcher=(root/'scripts/release_cli.py').read_text().replace('@HELPER_SHA256@',helper_hash)
|
||||
(output/'release_cli.py').write_text(launcher)
|
||||
shutil.copyfile(root/'scripts/install_release_bundle.py',output/'install.py')
|
||||
shutil.copyfile(root/'verifier/LICENSE.access-engine',output/'LICENSE.access-engine')
|
||||
sources=[root/'pyproject.toml',root/'uv.lock',root/'verifier/producer-source.json']
|
||||
sources+=list((root/'src/secrets_engine').glob('*.py'))
|
||||
sources+=list((root/'verifier/pkg/api').glob('*.go'))+list((root/'verifier/internal/sign').glob('*.go'))
|
||||
sources+=[root/'verifier/main.go',root/'scripts/check_pdp_producer_source.py',
|
||||
root/'scripts/build_release_bundle.py',root/'scripts/install_release_bundle.py',root/'scripts/release_cli.py']
|
||||
source_hashes={str(path.relative_to(root)):digest(path) for path in sorted(sources)}
|
||||
manifest={"schema":"secrets-engine.cli-bundle.v1","source_commit":commit,"source_files":source_hashes,
|
||||
"source_date_epoch":int(epoch),"system":platform.system(),"machine":platform.machine(),
|
||||
"python":platform.python_version(),"go":run(['go','version']),"uv":run([uv,'--version']),
|
||||
"build_backend":"hatchling==1.27.0","wheel":str(wheel.relative_to(output)),
|
||||
"producer":json.loads((root/'verifier/producer-source.json').read_text()),
|
||||
"artifacts":{str(path.relative_to(output)):digest(path) for path in sorted(output.rglob('*')) if path.is_file()},
|
||||
"admission":"checksums are integrity evidence; owner must admit the bundle/prefix independently"}
|
||||
(output/'manifest.json').write_text(json.dumps(manifest,sort_keys=True,indent=2)+'\n')
|
||||
return {'bundle':str(output),'manifest_sha256':digest(output/'manifest.json'),'helper_sha256':helper_hash}
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output',type=Path,required=True)
|
||||
parser.add_argument('--dependency-wheel',type=Path,required=True)
|
||||
parser.add_argument('--uv',default='uv')
|
||||
parser.add_argument('--allow-build-downloads',action='store_true',help='permit uv to fetch existing build-system dependencies')
|
||||
args=parser.parse_args()
|
||||
try:
|
||||
print(json.dumps(build(Path(__file__).resolve().parents[1],args.output.resolve(),args.dependency_wheel.resolve(),
|
||||
args.uv,args.allow_build_downloads),sort_keys=True))
|
||||
except (OSError,ValueError,KeyError,subprocess.SubprocessError) as exc:
|
||||
print('bundle build refused: '+str(exc),file=sys.stderr);return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
raise SystemExit(main())
|
||||
82
scripts/install_release_bundle.py
Normal file
82
scripts/install_release_bundle.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Install one reviewed CLI bundle offline into a new owner-controlled prefix."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def install(bundle, prefix, expected_manifest, python, uv):
|
||||
manifest_path = bundle / "manifest.json"
|
||||
if digest(manifest_path) != expected_manifest:
|
||||
raise ValueError("bundle manifest differs from reviewed digest")
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
if manifest["schema"] != "secrets-engine.cli-bundle.v1":
|
||||
raise ValueError("unsupported bundle")
|
||||
if manifest['system'] != platform.system() or manifest['machine'] != platform.machine():
|
||||
raise ValueError("bundle platform differs from installer host")
|
||||
expected_files = set(manifest['artifacts']) | {'manifest.json'}
|
||||
actual_files = {str(p.relative_to(bundle)) for p in bundle.rglob('*') if p.is_file()}
|
||||
if actual_files != expected_files:
|
||||
raise ValueError("bundle file set differs")
|
||||
for name, expected in manifest['artifacts'].items():
|
||||
relative = Path(name)
|
||||
if relative.is_absolute() or '..' in relative.parts:
|
||||
raise ValueError("invalid artifact path")
|
||||
artifact = bundle / relative
|
||||
if any(parent.is_symlink() for parent in (artifact,*artifact.parents)) or digest(artifact) != expected:
|
||||
raise ValueError("bundle artifact missing or changed")
|
||||
# Reserve once; never replace an installed prefix or follow an existing link.
|
||||
prefix.mkdir(mode=0o700, parents=False, exist_ok=False)
|
||||
try:
|
||||
subprocess.run([uv,'--no-config','venv','--python',python,'--no-python-downloads',str(prefix/'venv')],check=True)
|
||||
subprocess.run([uv,'--no-config','pip','install','--python',str(prefix/'venv/bin/python'),
|
||||
'--no-index','--find-links',str(bundle/'wheels'),str(bundle/manifest['wheel'])],check=True)
|
||||
(prefix/'libexec').mkdir(mode=0o700)
|
||||
helper=prefix/'libexec/secrets-engine-pdp-verify'
|
||||
shutil.copyfile(bundle/'bin/secrets-engine-pdp-verify',helper);helper.chmod(0o700)
|
||||
(prefix/'bin').mkdir(mode=0o700)
|
||||
launcher=prefix/'bin/secrets-engine'
|
||||
# An absolute venv interpreter fixes the installed dependency environment.
|
||||
launcher.write_text('#!'+str(prefix/'venv/bin/python')+'\n'+(bundle/'release_cli.py').read_text())
|
||||
launcher.chmod(0o700)
|
||||
receipt={"schema":"secrets-engine.cli-install.v1","manifest_sha256":expected_manifest,
|
||||
"helper_sha256":digest(helper),"launcher_sha256":digest(launcher),"prefix":str(prefix),
|
||||
"admission":"artifact integrity only; trust and keys not admitted"}
|
||||
(prefix/'install-receipt.json').write_text(json.dumps(receipt,sort_keys=True,indent=2)+'\n')
|
||||
return receipt
|
||||
except BaseException:
|
||||
# Preserve a failed prefix as evidence; never retry into a partial install.
|
||||
(prefix/'INSTALL-FAILED').write_text('Incomplete installation; use a new prefix.\n')
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--bundle',type=Path,default=Path(__file__).resolve().parent)
|
||||
parser.add_argument('--prefix',type=Path,required=True)
|
||||
parser.add_argument('--manifest-sha256',required=True)
|
||||
parser.add_argument('--python',default=sys.executable)
|
||||
parser.add_argument('--uv',default='uv')
|
||||
args=parser.parse_args()
|
||||
try:
|
||||
if not args.prefix.is_absolute() or args.prefix.parent.is_symlink():
|
||||
raise ValueError('prefix must be absolute with an admitted parent')
|
||||
receipt=install(args.bundle.resolve(),args.prefix,args.manifest_sha256,args.python,args.uv)
|
||||
print(json.dumps(receipt,sort_keys=True))
|
||||
except (OSError,ValueError,KeyError,subprocess.SubprocessError) as exc:
|
||||
print('bundle install refused: '+str(exc),file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
raise SystemExit(main())
|
||||
29
scripts/release_cli.py
Normal file
29
scripts/release_cli.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Narrow installed CLI entry point; rendered by build_release_bundle.py."""
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
EXPECTED_HELPER_SHA256 = "@HELPER_SHA256@"
|
||||
|
||||
|
||||
def main():
|
||||
prefix = Path(__file__).resolve().parent.parent
|
||||
helper = prefix / "libexec" / "secrets-engine-pdp-verify"
|
||||
try:
|
||||
if helper.is_symlink() or not helper.is_file():
|
||||
raise ValueError("missing helper")
|
||||
if hashlib.sha256(helper.read_bytes()).hexdigest() != EXPECTED_HELPER_SHA256:
|
||||
raise ValueError("changed helper")
|
||||
except (OSError, ValueError):
|
||||
print("secrets-engine: installed verifier missing or changed", file=sys.stderr)
|
||||
return 1
|
||||
# The bundle always uses its reviewed helper; ambient overrides do not select
|
||||
# another executable. The owner admits runtime trust separately.
|
||||
os.environ["SECRETS_ENGINE_PDP_VERIFIER"] = str(helper)
|
||||
from secrets_engine.cli import main as cli_main
|
||||
return cli_main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue