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