--- a/tests/test_sops_private.py +++ b/tests/test_sops_private.py @@ -10,6 +10,40 @@ ROOT = Path(__file__).resolve().parents[1] SOPS = shutil.which('sops') pytestmark = pytest.mark.skipif(SOPS is None, reason='SOPS required for real crypto regression') + + +@pytest.mark.parametrize('wrong_recipient', [False, True]) +def test_real_sops_decrypt_with_only_sealed_descriptor(case, wrong_recipient): + import fcntl + original = case[1].read_bytes() + material = (subprocess.run(['age-keygen'], capture_output=True, check=True).stdout + if wrong_recipient else original) + fd = os.memfd_create('generated-sops-custody', os.MFD_ALLOW_SEALING) + try: + os.fchmod(fd, 0o600); os.write(fd, material); os.lseek(fd, 0, os.SEEK_SET) + fcntl.fcntl(fd, fcntl.F_ADD_SEALS, + fcntl.F_SEAL_WRITE | fcntl.F_SEAL_GROW | fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_SEAL) + case[1].unlink() + env = {**case[2], 'NETKINGDOM_CUSTODY_AGE_KEY_FD': str(fd)} + env.pop('NETKINGDOM_CUSTODY_AGE_KEY_FILE') + if wrong_recipient: + # A correct ambient key must never rescue the wrong explicit FD. + env['SOPS_AGE_KEY'] = original.decode() + result = subprocess.run(['make', '-s', 'sops-decrypt', 'FILE=' + str(case[3]), + 'OUTPUT=' + str(case[4])], cwd=case[0], env=env, + pass_fds=(fd,), capture_output=True, text=True, timeout=15) + assert not case[1].exists() + assert 'synthetic-secret-payload' not in result.stdout + result.stderr + if wrong_recipient: + assert result.returncode != 0 + assert not case[4].exists() + else: + assert result.returncode == 0 + assert json.loads(case[4].read_text()) == {'credential': 'synthetic-secret-payload'} + assert case[4].stat().st_mode & 0o077 == 0 + assert not list(case[4].parent.glob('.netkingdom-sops-*')) + finally: + os.close(fd) @pytest.fixture --- a/tests/test_sops_write.py +++ b/tests/test_sops_write.py @@ -11,6 +11,36 @@ SOPS=shutil.which('sops') pytestmark=pytest.mark.skipif(SOPS is None,reason='real SOPS required') PLAIN='synthetic-sops-write-value' + + +@pytest.mark.parametrize('action', ['rotate', 'edit']) +def test_real_sops_write_with_only_sealed_descriptor(case, action): + import fcntl + fd = os.memfd_create('generated-sops-write-custody', os.MFD_ALLOW_SEALING) + try: + os.fchmod(fd, 0o600); os.write(fd, case['key'].read_bytes()); os.lseek(fd, 0, os.SEEK_SET) + fcntl.fcntl(fd, fcntl.F_ADD_SEALS, + fcntl.F_SEAL_WRITE | fcntl.F_SEAL_GROW | fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_SEAL) + case['key'].unlink() + env = {**case['env'], 'NETKINGDOM_CUSTODY_AGE_KEY_FD': str(fd)} + env.pop('NETKINGDOM_CUSTODY_AGE_KEY_FILE'); env.pop('SOPS_AGE_KEY_FILE') + before = case['cipher'].read_bytes() + result = subprocess.run(['make', '-s', 'sops-' + action, + 'FILE=' + str(case['cipher']), 'INPUT=' + str(case['input'])], + cwd=case['repo'], env=env, pass_fds=(fd,), + capture_output=True, text=True, timeout=15) + assert result.returncode == 0 + assert PLAIN not in result.stdout + result.stderr + assert not case['key'].exists() + assert before != case['cipher'].read_bytes() + clear = subprocess.run([SOPS, '--decrypt', str(case['cipher'])], + env={**env, 'SOPS_AGE_KEY_FILE': '/proc/self/fd/' + str(fd)}, + pass_fds=(fd,), capture_output=True, check=True) + assert json.loads(clear.stdout) == {'credential': PLAIN + ('-edited' if action == 'edit' else '')} + assert case['cipher'].stat().st_mode & 0o077 == 0 + assert not list(case['cipher'].parent.glob('.netkingdom-sops-write-*')) + finally: + os.close(fd) @pytest.fixture