import fcntl import importlib.util import json import os from pathlib import Path import signal import pty import select import subprocess import sys import tempfile import time import unittest from unittest.mock import patch ROOT=Path(__file__).parents[1] def load(name,path): spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m h=load('fdhelper',ROOT/'sso-mfa/bootstrap/sops-custody-fd.py') c=load('custody',ROOT/'sso-mfa/bootstrap/sops-private.py') class SealedCustodyTests(unittest.TestCase): @classmethod def setUpClass(cls): # Capture generated test key in memory only, never into test output/path. r=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True) cls.material=next(x for x in r.stdout.splitlines() if x.startswith(b'AGE-SECRET-KEY-'))+b'\n' fd=h.sealed_key(cls.material) try:cls.recipient=subprocess.run(['age-keygen','-y',f'/proc/self/fd/{fd}'],pass_fds=(fd,),stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout.decode().strip() finally:os.close(fd) def test_sealed_descriptor_private_duplicate_is_cloexec_and_decrypts(self): encrypted=subprocess.run(['age','-r',self.recipient],input=b'synthetic recovery material',stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout original=h.sealed_key(self.material) try: with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':str(original)},clear=True):fd=c.private_key_fd() try: self.assertFalse(os.get_inheritable(fd));self.assertEqual(os.fstat(fd).st_nlink,0) r=subprocess.run(['age','-d','-i',f'/proc/self/fd/{fd}'],pass_fds=(fd,),input=encrypted,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL) self.assertEqual(r.returncode,0);self.assertEqual(r.stdout,b'synthetic recovery material') finally:os.close(fd) finally:os.close(original) def test_incomplete_seals_or_named_file_refused(self): for named in (False,True): if named: file=tempfile.TemporaryFile();fd=file.fileno();file.write(self.material);file.flush() else:fd=os.memfd_create('synthetic-unsealed',os.MFD_CLOEXEC|os.MFD_ALLOW_SEALING);os.write(fd,self.material) try: with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':str(fd)},clear=True),self.assertRaises((c.Refused,OSError)): c.private_key_fd() finally: if named:file.close() else:os.close(fd) def test_ambiguous_sources_refused(self): with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':'3','NETKINGDOM_CUSTODY_AGE_KEY_FILE':'/not/opened'},clear=True),self.assertRaises(c.Refused):c.private_key_fd() def test_malformed_descriptor_text_refused_before_dup(self): for value in ('-1','2','03',' 3','3 ','1000000000','3/4'): with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':value},clear=True),patch.object(c.os,'dup') as duplicate: with self.subTest(value=value),self.assertRaises(c.Refused):c.private_key_fd() duplicate.assert_not_called() def test_bad_expected_recipient_refused_before_prompt(self): with patch('sys.argv',['sealed','--expected-recipient','age1bad','--','true']),patch.object(h.os,'isatty',return_value=True),patch.object(h.getpass,'getpass') as prompt,patch('builtins.print'): self.assertEqual(h.main(),1);prompt.assert_not_called() def test_existing_file_mode_still_accepts_private_owner_file(self): with tempfile.TemporaryDirectory() as temp: path=Path(temp)/'synthetic-key';path.write_bytes(self.material);path.chmod(0o600) with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FILE':str(path)},clear=True):fd=c.private_key_fd() try:self.assertEqual(os.read(fd,16384),self.material) finally:os.close(fd) def test_helper_passes_exact_fd_and_clears_ambient_sources(self): code="""import os,sys fd=int(os.environ['NETKINGDOM_CUSTODY_AGE_KEY_FD']) assert os.fstat(fd).st_nlink==0 assert all(k not in os.environ for k in ('NETKINGDOM_CUSTODY_AGE_KEY_FILE','SOPS_AGE_KEY_FILE','SOPS_AGE_KEY','SOPS_AGE_KEY_CMD')) """ with patch.dict(os.environ,{'SOPS_AGE_KEY':'synthetic-competing','NETKINGDOM_CUSTODY_AGE_KEY_FILE':'/not/read'}): self.assertEqual(h.run(self.material,self.recipient,[sys.executable,'-c',code]),0) def test_normal_and_error_return_close_parent_fd(self): actual=h.sealed_key for status in (0,7): fds=[] def capture(material): fd=actual(material);fds.append(fd);return fd with patch.object(h,'sealed_key',side_effect=capture):self.assertEqual(h.run(self.material,self.recipient,[sys.executable,'-c',f'raise SystemExit({status})']),status) with self.assertRaises(OSError):os.fstat(fds[0]) def test_wrong_recipient_does_not_start_child_and_closes_fd(self): other=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout other=next(x for x in other.splitlines() if x.startswith(b'AGE-SECRET-KEY-'))+b'\n';fd=h.sealed_key(other) try:recipient=subprocess.run(['age-keygen','-y',f'/proc/self/fd/{fd}'],pass_fds=(fd,),stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout.decode().strip() finally:os.close(fd) with tempfile.TemporaryDirectory() as temp: marker=Path(temp)/'child' with self.assertRaises(h.Refused):h.run(self.material,recipient,[sys.executable,'-c','import pathlib,sys;pathlib.Path(sys.argv[1]).touch()',str(marker)]) self.assertFalse(marker.exists()) def test_actual_attended_shell_sealed_mode_hides_key_and_runs_original_fd_consumer(self): code="import importlib.util,os,sys;spec=importlib.util.spec_from_file_location('c',sys.argv[1]);c=importlib.util.module_from_spec(spec);spec.loader.exec_module(c);fd=c.private_key_fd();assert os.fstat(fd).st_nlink==0;os.close(fd)" master,slave=pty.openpty() child=subprocess.Popen(['bash','-x',str(ROOT/'sso-mfa/bootstrap/sops-custody-unlock.sh'),'--expected-recipient',self.recipient,'--sealed-fd','--prompt','--',sys.executable,'-c',code,str(ROOT/'sso-mfa/bootstrap/sops-private.py')],stdin=slave,stdout=slave,stderr=slave) os.close(slave);captured=b'';sent=False;end=time.monotonic()+5 try: while time.monotonic()