"""Use real SOPS with disposable age custody; no repository secret inputs.""" import json import os from pathlib import Path import shutil import subprocess import pytest 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 def case(tmp_path): repo = tmp_path / 'repo'; (repo / 'sso-mfa/bootstrap').mkdir(parents=True) shutil.copyfile(ROOT / 'Makefile', repo / 'Makefile') shutil.copyfile(ROOT / 'sso-mfa/bootstrap/sops-private.py', repo / 'sso-mfa/bootstrap/sops-private.py') (repo / 'keys').mkdir() key = tmp_path / 'disposable.key' subprocess.run(['age-keygen', '-o', str(key)], capture_output=True, check=True) public = subprocess.check_output(['age-keygen', '-y', str(key)], text=True).strip() (repo / 'keys/age.pub').write_text(public + '\n') home = tmp_path / 'home'; home.mkdir(mode=0o700) env = {'HOME': str(home), 'PATH': os.environ['PATH'], 'NETKINGDOM_CUSTODY_AGE_KEY_FILE': str(key)} clear = tmp_path / 'input.json'; clear.write_text(json.dumps({'credential': 'synthetic-secret-payload'})) cipher = repo / 'cipher.json' encrypted = subprocess.run([SOPS, '--encrypt', '--age', public, str(clear)], cwd=tmp_path, env=env, capture_output=True, check=True) cipher.write_bytes(encrypted.stdout) output_dir = tmp_path / 'private-output'; output_dir.mkdir(mode=0o700) return repo, key, env, cipher, output_dir / 'restored.json' def make(case, target='sops-decrypt', *, env=None, source=None, output=None): repo, _, default_env, cipher, destination = case return subprocess.run(['make', '-s', target, 'FILE=' + str(source or cipher), 'OUTPUT=' + str(output or destination)], env=env or default_env, cwd=repo, capture_output=True, text=True, timeout=15) def ambient_key(case): _, key, env, _, _ = case path = Path(env['HOME']) / '.config/sops/age/keys.txt' path.parent.mkdir(parents=True); path.write_bytes(key.read_bytes()); path.chmod(0o600) def test_setup_never_installs_private_key(case): _, key, env, _, _ = case legacy = Path(env['HOME']) / '.config/age/key.txt' legacy.parent.mkdir(parents=True); legacy.write_bytes(key.read_bytes()) result = make(case, 'sops-setup') assert result.returncode == 0 assert not (Path(env['HOME']) / '.config/sops').exists() def test_decrypt_real_sops_to_private_output_without_transcript(case): ambient_key(case) # Old target succeeds through ambient discovery and leaks. result = make(case) assert result.returncode == 0 assert 'synthetic-secret-payload' not in result.stdout + result.stderr 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-*')) def test_wrong_explicit_key_never_falls_back_to_home_or_inline_keys(case, tmp_path): ambient_key(case) wrong = tmp_path / 'wrong.key' subprocess.run(['age-keygen', '-o', str(wrong)], capture_output=True, check=True) env = {**case[2], 'NETKINGDOM_CUSTODY_AGE_KEY_FILE': str(wrong), 'SOPS_AGE_KEY': case[1].read_text()} result = make(case, env=env) assert result.returncode != 0 assert 'synthetic-secret-payload' not in result.stdout + result.stderr assert not case[4].exists() def test_absent_explicit_custody_refused_even_if_home_has_key(case): ambient_key(case) env = dict(case[2]); del env['NETKINGDOM_CUSTODY_AGE_KEY_FILE'] assert make(case, env=env).returncode != 0 assert not case[4].exists() @pytest.mark.parametrize('kind', ['file', 'symlink']) def test_existing_output_is_not_replaced(case, kind): ambient_key(case) if kind == 'file': case[4].write_bytes(b'original-output') else: case[4].symlink_to(case[4].parent / 'absent') assert make(case).returncode != 0 if kind == 'file': assert case[4].read_bytes() == b'original-output' else: assert case[4].is_symlink() def test_git_worktree_output_is_refused(case): ambient_key(case) subprocess.run(['git', 'init', str(case[0])], env=case[2], capture_output=True, check=True) assert make(case, output=case[0] / 'plaintext.json').returncode != 0 assert not (case[0] / 'plaintext.json').exists() def test_shared_output_directory_is_refused(case): ambient_key(case) case[4].parent.chmod(0o755) assert make(case).returncode != 0 assert not case[4].exists() def noisy_tool(case, tmp_path): bin_dir = tmp_path / 'bin'; bin_dir.mkdir() tool = bin_dir / 'sops' tool.write_text('#!/bin/sh\necho synthetic-tool-secret\necho synthetic-error-secret >&2\nexit 23\n') tool.chmod(0o755) return {**case[2], 'PATH': str(bin_dir) + ':' + case[2]['PATH']} def test_child_output_and_errors_are_not_exposed_on_failure(case, tmp_path): result = make(case, env=noisy_tool(case, tmp_path)) assert result.returncode != 0 assert 'synthetic-tool-secret' not in result.stdout + result.stderr assert 'synthetic-error-secret' not in result.stdout + result.stderr assert not case[4].exists() assert not list(case[4].parent.glob('.netkingdom-sops-*')) def test_non_age_metadata_refuses_before_tool(case, tmp_path): data = json.loads(case[3].read_text()); data['sops']['kms'] = [{'arn': 'synthetic'}] case[3].write_text(json.dumps(data)) result = make(case, env=noisy_tool(case, tmp_path)) assert result.returncode != 0 assert 'age-only metadata' in result.stderr assert 'synthetic-tool-secret' not in result.stdout def test_filename_is_not_shell_code(case): ambient_key(case) odd = case[0] / 'cipher; touch SHELL_EXECUTED; #.json' shutil.copyfile(case[3], odd) result = make(case, source=odd) assert result.returncode == 0 assert not (case[0] / 'SHELL_EXECUTED').exists() assert case[4].exists() def test_yaml_roundtrip(case, tmp_path): public = (case[0] / 'keys/age.pub').read_text().strip() clear = tmp_path / 'input.yaml'; clear.write_text('credential: synthetic-yaml-secret\n') cipher = case[0] / 'cipher.yaml' result = subprocess.run([SOPS, '--encrypt', '--age', public, str(clear)], cwd=tmp_path, env=case[2], capture_output=True, check=True) cipher.write_bytes(result.stdout) result = make(case, source=cipher) assert result.returncode == 0 assert 'synthetic-yaml-secret' not in result.stdout + result.stderr assert 'synthetic-yaml-secret' in case[4].read_text() def test_parser_error_never_echoes_input(case): case[3].write_text('synthetic-malformed-secret') result = make(case) assert result.returncode != 0 assert 'synthetic-malformed-secret' not in result.stdout + result.stderr assert not case[4].exists() def test_shared_custody_key_is_refused(case): case[1].chmod(0o644) result = make(case) assert result.returncode != 0 assert not case[4].exists() def test_creds_init_has_no_permanent_key_requirement(case): subprocess.run(['git', 'init', str(case[0])], env=case[2], capture_output=True, check=True) hooks = case[0] / '.githooks'; hooks.mkdir() (hooks / 'pre-commit').write_text('#!/bin/sh\nexit 0\n') result = make(case, 'creds-init') assert result.returncode == 0 assert not (Path(case[2]['HOME']) / '.config/sops').exists() def test_real_sops_does_not_execute_ambient_key_commands(case, tmp_path): marker = tmp_path / 'ambient-command-executed' command = tmp_path / 'ambient-key-command' command.write_text('#!/bin/sh\ntouch ' + str(marker) + '\nexit 1\n') command.chmod(0o755) env = {**case[2], 'SOPS_AGE_KEY_CMD': str(command), 'SOPS_AGE_SSH_PRIVATE_KEY_CMD': str(command)} result = make(case, env=env) assert result.returncode == 0 assert not marker.exists() def test_yaml_parser_error_is_sanitized(case): bad = case[0] / 'broken.yaml' bad.write_text('secret: [synthetic-malformed-yaml-secret\n') result = make(case, source=bad) assert result.returncode != 0 assert 'synthetic-malformed-yaml-secret' not in result.stdout + result.stderr assert 'Traceback' not in result.stderr @pytest.mark.parametrize('kind', ['json', 'yaml']) def test_duplicate_metadata_refused_before_sops(case, kind): source = case[0] / ('duplicate.' + kind) source.write_text('{"sops": {}, "sops": {}}' if kind == 'json' else 'sops: {}\nsops: {}\n') result = make(case, source=source) assert result.returncode != 0 assert 'duplicate' in result.stderr @pytest.mark.parametrize('kind', ['bare', 'linked']) def test_bare_and_linked_git_outputs_are_refused(case, kind): location = case[4].parent / 'git-output' if kind == 'bare': subprocess.run(['git', 'init', '--bare', str(location)], env=case[2], capture_output=True, check=True) else: subprocess.run(['git', 'init', str(case[0])], env=case[2], capture_output=True, check=True) subprocess.run(['git', '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '--allow-empty', '-m', 'fixture'], cwd=case[0], env=case[2], capture_output=True, check=True) subprocess.run(['git', 'worktree', 'add', '--detach', str(location)], cwd=case[0], env=case[2], capture_output=True, check=True) location.chmod(0o700) result = make(case, output=location / 'plaintext.json') assert result.returncode != 0 assert not (location / 'plaintext.json').exists()