-
Notifications
You must be signed in to change notification settings - Fork 685
Proposal: Replace pretty_bad_privacy with Sequoia for PGP operations in SecureDrop server
This was originally approved by the SecureDrop team on 2023-02-27
@legoktm
SecureDrop Server
@legoktm, ?
- pretty_bad_privacy is unmaintained, forcing us to monkey patch things so it works
- we shell out to gpg because it doesn't expose a proper library API
- We historically have had issues with gpg, like https://github.com/freedomofpress/securedrop/issues/6389 and https://github.com/freedomofpress/securedrop/pull/6270
- The security stance of gpg upstream is dubious/unclear, see e.g. https://www.openwall.com/lists/oss-security/2022/06/30/1 (from the Qubes team)
- <insert generic gpg complaint>
On the flip side, Sequoia is a Rust project to make using PGP less terrible. It is library-first, stable, and being picked up by other significant projects to replace their usage of gpg, most notably RPM. The Sequoia project is also working to move the ecosystem forward through the IETF RFC process, pushing the ecosystem to switch from broken SHA-1 to (mostly) collision resistant SHA-1 and then dropping support (by default) altogether.
It is worth noting that this is a shortish-term problem, as the next-gen SD work is most likely not going to use OpenPGP in favor of modern crypto stuff.
- We will be shipping written-here Rust code as part of SecureDrop (related: #10).
- Developers feel a lot better about not relying on unmaintained and hacky code for probably one of the most critical parts of SecureDrop - the encryption. We would be in a much better position to deal with any future potential security issues in this area of code.
- If successful, it is likely that we will pursue replacing gpg with Sequoia in SDW and other places.
- Messages and files will be encrypted with a non-gpg implementation of OpenPGP. There is some possibility for interop issues if journalists are using GPG on the Tails workstation/SDW, however the Sequoia team appears to care and has active testing for interop.
- We need to be able to do the following:
- Generate a new OpenPGP key pair, for a specified user ID and encrypted using the provided passphrase
- Encrypt a message (text is in a variable), and store the encrypted version on disk
- Encrypt a file (contents are stored on disk) and store the encrypted version on disk
- Decrypt arbitrary bytes, given a secret key and passphrase (assuming the decrypted contents are valid UTF-8)
- The migration/switching process should be entirely invisible to sources and administrators (no manual interventions required)
- We no longer have
pretty_bad_privacy
as a dependency (if we still need to interact with gpg for migration purposes, we can inline the parts we still need) - Avoid vendor lock-in, we should aim to use only standardized parts of OpenPGP, so we don't find ourselves again where we are today, with private keys locked in a custom GPG format.
See prior discussion at:
- https://github.com/freedomofpress/securedrop/issues/6399
- https://github.com/freedomofpress/securedrop/issues/6499
Unlike gpg, Sequoia-as-a-library does not store keys in a on-disk keyring, it leaves that up to the caller. We could continue to store keys on disk, but doing so creates extra metadata and means there is an additional place to keep in sync.
Instead, I propose we store the keys in the database, in their armored formats. We will add 3 columns to the source table:
public_key = Column(Text, nullable=True)
private_key = Column(Text, nullable=True)
fingerprint = Column(String(40), nullable=True)
Keys will be created when the source is inserted into the database table, not at a later time, so all new sources will always have a keypair (it is possible legacy sources will not have a keypair yet, in which case it'll be generated the next time they log in).
During source deletion we don't need to take any extra steps since they key will be deleted as part of the database row.
Source private keys are currently stored in the gpg keyring, encrypted with the source passphrase. The keys are stored in a format specific to GPG, and Sequoia (as of this writing), cannot read those files.
There will be two stages to this migration, the "offline" part in which the public key material is migrated, and an "online" part when secret key material is migrated when sources log in.
The offline migration will first be a standard alembic schema change to add the new database columns discussed above. Then we will iterate through all the sources in the database, export their public key from GPG, ensure Sequoia can parse it, and save the armored public key and fingerprint in the database.
The online migration will take place each time a source logs in. If a public key is set for the source but the secret key is missing, it will use the passphrase to export the secret key from GPG, validate it with Sequoia (see "SHA-1 migration and key linting" below for more details) and save the armored + encrypted private key in the database and delete it from the GPG keyring. If the source has no public key nor private key, a new keypair will be generated for them.
In the future, if Sequoia or other Python/Rust code is able to read the GPG secret key format, we can do another offline migration to move all the remaining keys into the database, and write code that, when the secret key is needed, will decrypt and convert it to a standard OpenPGP key and update what is in the database.
Sequoia, by default, no longer supports SHA-1 signatures (see https://sequoia-pgp.org/blog/2023/02/01/202302-happy-sha1-day/). It seems possible that there are old SecureDrops out there with old sources with SHA-1 keys (I'm just assuming this based on the age of the project and https://gitlab.com/sequoia-pgp/sequoia/-/issues/595 specifically calling us out).
The Sequoia keyring-linter will detect this issue and can automatically fix it, but it needs the secret key. During the online migration, we can check each key with the linter and fix if necessary.
This is being called out separately from the gpg->Sequoia migration steps above because it seems like something we should continue doing whenever a source logs in, upgrading their key based on what the linter says and can do, just like we opportunistically rehash passwords to be stronger upon login if they're still stored using an older/weaker hash.
TBD if we should shell out to the linter or can call it via Python/Rust code.
Because we only need 4 specific functions, we will create a small Rust library that exposes only those functions and use pyO3 to make those functions callable from Python. The Rust code will be built into a wheel that can be installed into a virtualenv.
An initial version of this interface is at https://github.com/freedomofpress/securedrop/blob/oxidize/redwood/src/lib.rs; the Python interface looks like: https://github.com/freedomofpress/securedrop/blob/oxidize/redwood/redwood.pyi.
The development environment will build this wheel on the fly, but automatic reload will likely not be supported in the initial implementation.
The package build process will also build the wheel (it will not be stored with other wheels in securedrop-builder) and install it into the virtualenv we ship in the deb.
Note that Sequoia supports multiple crypto backends, we should use OpenSSL rather than Nettle since it's already part of our trust model (RPM is also planning to use it with OpenSSL). The pure-Rust crypto is still considered experimental.
No response