Repository: freedomofpress/securedrop

Issue:
Make journalist's public keys available to sources
Technically sophisticated sources might want to encrypt their submissions themselves, so the cleartext _never_ exists on the web server. We should add a link to the submission page to optionally download the journalist's public key (perhaps in an "Advanced options" section).

There is an interesting trust problem in this case. We are trying to stop an attacker who has somehow gained access to the source server from accessing submissions in cleartext. However, if they have access to the server then they could just send their own public key to a source who wants to use this, tricking them into encrypting it to the adversary instead of the journalist.

This could be mitigated with any of the standard GPG key verification mechanisms. For example, we could publish the valid GPG key fingerprint on the website and/or in a physical newspaper/magazine so savvy sources can additionally verify the key. We could also run a third party server that periodically checks the public key for unauthorized changes.



Discussion:
If a source submits an encrypted blob of information, will SecureDrop encrypt this blob again or will it just pass it on to the journalist server?

@runasand We could, or we could detect that it is encrypted (via `file` and/or the presence of `.gpg` extension) and not encrypt it again in that case.


Resolving patch:
diff --git a/securedrop/crypto_util.py b/securedrop/crypto_util.py
--- a/securedrop/crypto_util.py
+++ b/securedrop/crypto_util.py
@@ -108,6 +108,11 @@ def getkey(name):
     return None
 
 
+def get_key_by_fingerprint(fingerprint):
+    matches = filter(lambda k: k['fingerprint'] == fingerprint, gpg.list_keys())
+    return matches[0] if matches else None
+
+
 def _shquote(s):
     return "\\'".join("'" + p + "'" for p in s.split("'"))
 
diff --git a/securedrop/source.py b/securedrop/source.py
--- a/securedrop/source.py
+++ b/securedrop/source.py
@@ -5,7 +5,7 @@
 from functools import wraps
 
 from flask import (Flask, request, render_template, session, redirect, url_for,
-                   flash, abort, g)
+                   flash, abort, g, send_file)
 from flask_wtf.csrf import CsrfProtect
 
 import config
@@ -218,6 +218,20 @@ def tor2web_warning():
     return render_template("tor2web-warning.html")
 
 
+@app.route('/journalist-key')
+def download_journalist_pubkey():
+    journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)
+    return send_file(StringIO(journalist_pubkey),
+                     mimetype="application/pgp-keys",
+                     attachment_filename=config.JOURNALIST_KEY + ".asc",
+                     as_attachment=True)
+
+
+@app.route('/why-journalist-key')
+def why_download_journalist_pubkey():
+    return render_template("why-journalist-key.html")
+
+
 @app.errorhandler(404)
 def page_not_found(error):
     return render_template('notfound.html'), 404


Test patch:
diff --git a/securedrop/test.py b/securedrop/test.py
--- a/securedrop/test.py
+++ b/securedrop/test.py
@@ -143,6 +143,17 @@ def _new_codename(self):
             rv = c.post('/create')
         return codename
 
+    def test_lookup(self):
+        """Test various elements on the /lookup page"""
+        codename = self._new_codename()
+        rv = self.client.post('login', data=dict(codename=codename),
+                              follow_redirects=True)
+        # redirects to /lookup
+        self.assertIn("Download journalist's public key", rv.data)
+        # download the public key
+        rv = self.client.get('journalist-key')
+        self.assertIn("BEGIN PGP PUBLIC KEY BLOCK", rv.data)
+
     def test_login(self):
         rv = self.client.get('/login')
         self.assert200(rv)