Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
90 lines
5.1 KiB
Python
90 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Attended independent-key recovery of the new data-bearing off-host bundle."""
|
|
import argparse
|
|
from datetime import datetime,timezone
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import signal
|
|
import subprocess
|
|
import tempfile
|
|
import uuid
|
|
|
|
HERE=Path(__file__).resolve().parent
|
|
|
|
|
|
def load(name,path=None):
|
|
spec=importlib.util.spec_from_file_location(name,path or HERE/(name+'.py'))
|
|
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
|
|
|
|
|
|
bundle=load('identity-bundle');pg=load('check-privacyidea-database')
|
|
custody=load('sops_private',HERE.parents[1]/'sso-mfa/bootstrap/sops-private.py')
|
|
|
|
|
|
def verify(transfer,keyfd,receipt):
|
|
if (transfer.get('schema')!='netkingdom.identity-offsite.v1' or transfer.get('status')!='readback_verified'
|
|
or transfer.get('independent_owner_retrieval_verified') is not True
|
|
or transfer.get('ciphertext_bytes',0)>bundle.LIMIT):raise bundle.Refused()
|
|
path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT)
|
|
with path.open('rb') as source:digest=hashlib.file_digest(source,'sha256').hexdigest()
|
|
if path.stat().st_size!=transfer['ciphertext_bytes'] or digest!=transfer['ciphertext_sha256']:raise bundle.Refused()
|
|
derived=pg.run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()
|
|
if derived!=bundle.profile()['public_recipient'].encode():raise bundle.Refused()
|
|
receipt.update(ciphertext_sha256=digest,artifact=transfer['artifact'],registered_recipient_match=True)
|
|
with tempfile.TemporaryDirectory(prefix='nk49-identity-restore-',dir='/tmp') as temporary:
|
|
work=Path(temporary);env={'PATH':os.environ.get('PATH',os.defpath),'HOME':temporary,'XDG_CONFIG_HOME':temporary}
|
|
wrong=work/'wrong.key';pg.run(['age-keygen','-o',str(wrong)],env=env)
|
|
negative=subprocess.run(['age','-d','-i',str(wrong),str(path)],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env=env,timeout=120)
|
|
if negative.returncode==0:raise bundle.Refused()
|
|
receipt['wrong_key_refused']=True
|
|
plain=work/'archive.tar'
|
|
with plain.open('xb') as output:
|
|
result=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output,
|
|
stderr=subprocess.DEVNULL,env=env,pass_fds=(keyfd,),timeout=180)
|
|
if result.returncode or plain.stat().st_size>bundle.LIMIT+4*1024*1024:raise bundle.Refused()
|
|
restored=work/'restored';restored.mkdir(mode=0o700)
|
|
manifest,checks=bundle.restore(plain,restored)
|
|
receipt.update(checks=checks,manifest_file_count=len(manifest['files']),all_manifest_hashes_verified=True,
|
|
profile_sha256=manifest['profile_sha256'])
|
|
container='nk49-identity-db-'+uuid.uuid4().hex
|
|
try:
|
|
receipt['database_restore']=pg.restore(restored/'databases/privacyidea.dump',container)
|
|
finally:
|
|
pg.run(['docker','rm','--force','--volumes',container])
|
|
absent=subprocess.run(['docker','container','inspect',container],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=15)
|
|
pg.run(['docker','info','--format','{{.ServerVersion}}'])
|
|
if absent.returncode==0:raise bundle.Refused()
|
|
receipt['container_cleanup_verified']=True
|
|
if work.exists():raise bundle.Refused()
|
|
receipt.update(temporary_plaintext_cleanup_verified=True,status='identity_archive_verified',
|
|
full_service_authentication_proven=False,existing_token_decryption_proven=False,audit_signature_continuity_proven=False)
|
|
|
|
|
|
def main():
|
|
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--transfer-receipt',required=True);p.add_argument('--receipt',required=True)
|
|
a=p.parse_args();os.umask(0o077)
|
|
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
r={'schema':'netkingdom.identity-recovery.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(),
|
|
'production_contacted':False,'secret_values_exported':False,'container_cleanup_verified':False,
|
|
'temporary_plaintext_cleanup_verified':False,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
|
|
keyfd=None
|
|
try:
|
|
transfer=json.loads(Path(a.transfer_receipt).read_text());keyfd=custody.private_key_fd()
|
|
verify(transfer,keyfd,r)
|
|
except Exception:r.update(status='failed',error='identity_recovery_failed_private_diagnostics_suppressed')
|
|
finally:
|
|
if keyfd is not None:os.close(keyfd)
|
|
r['finished_at']=datetime.now(timezone.utc).isoformat()
|
|
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
|
|
if r['status']=='identity_archive_verified':
|
|
print('OK: off-host identity bundle decrypted; complete manifest, key pairs, SQLite integrity and PostgreSQL restore passed; disposable data removed. Service authentication remains separate acceptance.')
|
|
return 0
|
|
print('ERROR: identity recovery failed; review safe receipt. Private diagnostics suppressed.');return 1
|
|
|
|
|
|
if __name__=='__main__':
|
|
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,custody.interrupted)
|
|
raise SystemExit(main())
|