def __init__(self, gpg_key_dir: Path, journalist_key_fingerprint: str) -> None: self._gpg_key_dir = gpg_key_dir self._journalist_key_fingerprint = journalist_key_fingerprint self._redis = Redis(decode_responses=True) # Instantiate the "main" GPG binary gpg = gnupg.GPG( binary="gpg2", homedir=str(self._gpg_key_dir), options=["--trust-model direct"] ) if StrictVersion(gpg.binary_version) >= StrictVersion("2.1"): # --pinentry-mode, required for SecureDrop on GPG 2.1.x+, was added in GPG 2.1. self._gpg = gnupg.GPG( binary="gpg2", homedir=str(gpg_key_dir), options=["--pinentry-mode loopback", "--trust-model direct"], ) else: self._gpg = gpg # Instantiate the GPG binary to be used for key deletion: always delete keys without # invoking pinentry-mode=loopback # see: https://lists.gnupg.org/pipermail/gnupg-users/2016-May/055965.html self._gpg_for_key_deletion = gnupg.GPG( binary="gpg2", homedir=str(self._gpg_key_dir), options=["--yes", "--trust-model direct"] ) # Ensure that the journalist public key has been previously imported in GPG try: self.get_journalist_public_key() except GpgKeyNotFoundError: raise EnvironmentError( f"The journalist public key with fingerprint {journalist_key_fingerprint}" f" has not been imported into GPG." )
def config(tmpdir): '''Clone the module so we can modify it per test.''' cnf = SDConfig() data = tmpdir.mkdir('data') keys = data.mkdir('keys') os.chmod(str(keys), 0o700) store = data.mkdir('store') tmp = data.mkdir('tmp') sqlite = data.join('db.sqlite') # gpg 2.1+ requires gpg-agent, see #4013 gpg_agent_config = str(keys.join('gpg-agent.conf')) with open(gpg_agent_config, 'w+') as f: f.write('allow-loopback-pinentry') gpg = gnupg.GPG('gpg2', homedir=str(keys)) for ext in ['sec', 'pub']: with io.open( path.join(path.dirname(__file__), 'files', 'test_journalist_key.{}'.format(ext))) as f: gpg.import_keys(f.read()) cnf.SECUREDROP_DATA_ROOT = str(data) cnf.GPG_KEY_DIR = str(keys) cnf.STORE_DIR = str(store) cnf.TEMP_DIR = str(tmp) cnf.DATABASE_FILE = str(sqlite) # create the db file subprocess.check_call(['sqlite3', cnf.DATABASE_FILE, '.databases']) return cnf
def setup_journalist_key_and_gpg_folder( ) -> Generator[Tuple[str, Path], None, None]: """Set up the journalist test key and the key folder, and reduce source key length for speed. This fixture takes about 2s to complete hence we use the "session" scope to only run it once. """ # This path matches the GPG_KEY_DIR defined in the config.py used for the tests # If they don't match, it can make the tests flaky and very hard to debug tmp_gpg_dir = Path("/tmp") / "securedrop" / "keys" tmp_gpg_dir.mkdir(parents=True, exist_ok=True, mode=0o0700) try: # GPG 2.1+ requires gpg-agent, see #4013 gpg_agent_config = tmp_gpg_dir / "gpg-agent.conf" gpg_agent_config.write_text( "allow-loopback-pinentry\ndefault-cache-ttl 0") # Import the journalist public key in GPG # WARNING: don't import the journalist secret key; it will make the decryption tests # unreliable gpg = gnupg.GPG("gpg2", homedir=str(tmp_gpg_dir)) journalist_public_key_path = Path( __file__).parent / "files" / "test_journalist_key.pub" journalist_public_key = journalist_public_key_path.read_text() journalist_key_fingerprint = gpg.import_keys( journalist_public_key).fingerprints[0] # Reduce source GPG key length to speed up tests at the expense of security with mock.patch.object(EncryptionManager, "GPG_KEY_LENGTH", PropertyMock(return_value=1024)): yield journalist_key_fingerprint, tmp_gpg_dir finally: shutil.rmtree(tmp_gpg_dir, ignore_errors=True)
def __init__(self, scrypt_params: Dict[str, int], scrypt_id_pepper: str, scrypt_gpg_pepper: str, securedrop_root: str, word_list: str, nouns_file: str, adjectives_file: str, gpg_key_dir: str) -> None: self.__securedrop_root = securedrop_root self.__word_list = word_list if os.environ.get('SECUREDROP_ENV') in ('dev', 'test'): # Optimize crypto to speed up tests (at the expense of security # DO NOT use these settings in production) self.__gpg_key_length = 1024 self.scrypt_params = dict(N=2**1, r=1, p=1) else: # pragma: no cover self.__gpg_key_length = 4096 self.scrypt_params = scrypt_params self.scrypt_id_pepper = scrypt_id_pepper self.scrypt_gpg_pepper = scrypt_gpg_pepper self.do_runtime_tests() # --pinentry-mode, required for SecureDrop on GPG 2.1.x+, was # added in GPG 2.1. self.gpg_key_dir = gpg_key_dir gpg_binary = gnupg.GPG(binary='gpg2', homedir=self.gpg_key_dir) if StrictVersion(gpg_binary.binary_version) >= StrictVersion('2.1'): self.gpg = gnupg.GPG(binary='gpg2', homedir=gpg_key_dir, options=['--pinentry-mode loopback']) else: self.gpg = gpg_binary # map code for a given language to a localized wordlist self.__language2words = {} # type: Dict[str, List[str]] with io.open(nouns_file) as f: self.nouns = f.read().splitlines() with io.open(adjectives_file) as f: self.adjectives = f.read().splitlines() self.redis = Redis(decode_responses=True)
def delete_reply_keypair(self, source_filesystem_id): key = self.getkey(source_filesystem_id) # If this source was never flagged for review, they won't have a reply # keypair if not key: return # Always delete keys without invoking pinentry-mode = loopback # see: https://lists.gnupg.org/pipermail/gnupg-users/2016-May/055965.html temp_gpg = gnupg.GPG(binary='gpg2', homedir=self.gpg_key_dir) # The subkeys keyword argument deletes both secret and public keys. temp_gpg.delete_keys(key, secret=True, subkeys=True) self.keycache.delete(source_filesystem_id)
def init_gpg(): """Initialize the GPG keyring and import the journalist key for testing. """ # gpg 2.1+ requires gpg-agent, see #4013 gpg_agent_config = os.path.join(config.GPG_KEY_DIR, 'gpg-agent.conf') with open(gpg_agent_config, 'w+') as f: f.write('allow-loopback-pinentry') gpg_binary = gnupg.GPG(binary='gpg2', homedir=config.GPG_KEY_DIR) if StrictVersion(gpg_binary.binary_version) >= StrictVersion('2.1'): gpg = gnupg.GPG(binary='gpg2', homedir=config.GPG_KEY_DIR, options=['--pinentry-mode loopback']) else: gpg = gpg_binary # Faster to import a pre-generated key than to gen a new one every time. for keyfile in (join(FILES_DIR, "test_journalist_key.pub"), join(FILES_DIR, "test_journalist_key.sec")): gpg.import_keys(io.open(keyfile).read()) return gpg
def gpg_key_dir() -> Generator[Path, None, None]: """Set up the journalist test key in GPG and the parent folder. This fixture takes about 2s to complete hence we use the "session" scope to only run it once. """ with TemporaryDirectory() as tmp_gpg_dir_name: tmp_gpg_dir = Path(tmp_gpg_dir_name) # GPG 2.1+ requires gpg-agent, see #4013 gpg_agent_config = tmp_gpg_dir / "gpg-agent.conf" gpg_agent_config.write_text("allow-loopback-pinentry") # Import the test key in GPG gpg = gnupg.GPG("gpg2", homedir=str(tmp_gpg_dir)) test_keys_dir = Path(__file__).parent / "files" for ext in ["sec", "pub"]: key_file = test_keys_dir / "test_journalist_key.{}".format(ext) gpg.import_keys(key_file.read_text()) yield tmp_gpg_dir
def delete_reply_keypair(self, source_filesystem_id: str) -> None: fingerprint = self.get_fingerprint(source_filesystem_id) # If this source was never flagged for review, they won't have a reply # keypair if not fingerprint: return # verify that the key with the given fingerprint belongs to a source key = self.find_source_key(fingerprint) if not key: raise ValueError("source key not found") # Always delete keys without invoking pinentry-mode = loopback # see: https://lists.gnupg.org/pipermail/gnupg-users/2016-May/055965.html temp_gpg = gnupg.GPG(binary='gpg2', homedir=self.gpg_key_dir) # The subkeys keyword argument deletes both secret and public keys. temp_gpg.delete_keys(fingerprint, secret=True, subkeys=True) self.redis.hdel(self.REDIS_KEY_HASH, self.get_fingerprint(source_filesystem_id)) self.redis.hdel(self.REDIS_FINGERPRINT_HASH, source_filesystem_id)