Repository: freedomofpress/securedrop

Issue:
using the API to retrieve a source immediately after source creation returns *all* source keys
## Description



## Steps to Reproduce

### Steps to reproduce
- boot SecureDrop dev env (server)
- retrieve API token as journalist
- login to web as source
- as source, submit message to web
- as journalist, immediately (important! as in < 2 seconds) go `GET /api/v1/sources`

## Expected Behavior

Exactly one or zero keys are returned for the new source.

## Actual Behavior

All keys are returned for the new source.

Example:

```json
{
    "sources": [
        {
            "add_star_url": "/api/v1/sources/d55335b0-5d7a-4151-bc3c-212f344e584a/add_star", 
            "interaction_count": 4, 
            "is_flagged": false, 
            "is_starred": false, 
            "journalist_designation": "rallying zoologist", 
            "key": {
                "public": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFGRfoABEADJ56nCxMuQeyp/LMhzEyRCQZ6UrYS3KVHtOPz7Q+N/4rWsS+Wu\nMO8X606xVA43JFVJgqeD+a4pcknmw9k0O7WzD+peswLCKulQ0s+yaZjP0G08CQH8\nS3nwG/zoCBxX+FXNnThtN+2u+XhGcKNACzWaiH8KiO9YUSUmiexCP3REBgMLucFF\nTnQK6RUcuVmYYS+VS87hYrEN4F6ANw5EeuENj++a36AJNFrzPO+QAfqZfe8UY64w\niyoXaELKCPKUMq3vka8oR6wUwPGw+053Epmd8BEy/7Ol6BdNbthB0bSrgMtnfEjX\nlU1LuusSPXOTAIXHr63+45oxAJ3MO8qoNXznWbPwElQAR/2xrsy2z20TDuvHCS2f\nrbITmB6oN76505YDFPjc19dXKWUurapQHsTkG1Bv/P/31/g46LA9TrULu+ZN7Dxz\nvvxDEvpPnrjKMjeew21v2w1P29DBhpoZwaxUhpMC7SF4yiEotaUXiZkaxCNkxT/l\n1UPQkS6XhuuGhsEv81BOuibPsI388nRNx5OUaCnXH9FxaFbyJyfmDek9IaymEXjV\nfD9gr1fePG0Wsa94D29nQ/GCegw4UhbDCZD6WX/Fi4kK8SBOOg0fU/5cW0eSZ8bp\nfTEXM++T0mKpbD+pz0QQMJHJuo46hZ7nWGslt7taCmqIPHI0KNNCKYPAfwARAQAB\ntHxBdXRvZ2VuZXJhdGVkIEtleSA8RkdWTUJPWFc2UVBSTDRJMlpBWjZUWktEV0Iy\nUEFNNjY3UE5HQ0tTRVROSEdBR1RVS1NHT1FNT0xJTFVTSk5YR0RZNUZPUjRJQUxP\nQ01TUjJXWE5GWlFKVFU2R01HVkpQR09SR0RQST0+iQI5BBMBCgAjBQJRkX6AAhsv\nBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQPaif/ttqPFxozg/+LgcbcStF\nBRxSzui0gZI2OEIdhL2ELZRSflhjOx650AfJO1hFZw3gHKrCv2AYwOXRqWhZHpwZ\n67qtkOTbwP2ENs2JNJodzk3CLRXm4YgoCABbH0QFd85osareTKA8qt1dIfzFwHup\nrJRCoc0cOI0vtzyK1fyWxAriRkn+Df2ue4FWJRI3LfvvCccr0+xkfac9RkgAUVIl\n46NyplnfRCwiHH0UkAzXYxOCSZ7NbT63HImZ8pkQifReFgANwljjtMpuNKw33otp\n7FMBK/VyeBcsHj/KhotMLbdmJvQ3Jwur5zT9/jR1F6Bh+JTHk8+sQZtefA9DTpFI\n6mBQl56toCqWMWfXB58tlv/YYb7GPhJI3dhc6UBJ8AHYA3Sgv698hITmAM4Yy+RB\nrGfAVKPpCwKr4wNh58BtHQ4J0oT4cLEgOr03QbnLIfg8EFBkIEfA+GP4/kwVrdAQ\nxqoRaksbWHXRUbXfaoraglYszAB9n004J5B0g0JwSgRRK0RpSrFLgW50bfCX6Nqe\nYTrxVL2tUXlZD8nKKu/tbmVymEgZameAVpR4sSU++9uBU3tsvqqqSR7SAKZDDz2k\nX8ofyvvqPeFXPQA4wyVcP9STgPUfYrn7tlSz6oq/fJHJcSw4TgY/zt7UZZxm0fJQ\ngN8384mxsBfcHYyblx4kMPnjwQ/B5067IuA=\n=3F/H\n-----END PGP PUBLIC KEY BLOCK-----\n", 
                "type": "PGP"
            }, 
            "last_updated": "2018-12-22T13:00:54.524845Z", 
            "number_of_documents": 0, 
            "number_of_messages": 2, 
            "remove_star_url": "/api/v1/sources/d55335b0-5d7a-4151-bc3c-212f344e584a/remove_star", 
            "replies_url": "/api/v1/sources/d55335b0-5d7a-4151-bc3c-212f344e584a/replies", 
            "submissions_url": "/api/v1/sources/d55335b0-5d7a-4151-bc3c-212f344e584a/submissions", 
            "url": "/api/v1/sources/d55335b0-5d7a-4151-bc3c-212f344e584a", 
            "uuid": "d55335b0-5d7a-4151-bc3c-212f344e584a"
        }, 
        {
            "add_star_url": "/api/v1/sources/dc17df20-6459-4317-8b3c-67b51997d3ce/add_star", 
            "interaction_count": 1, 
            "is_flagged": false, 
            "is_starred": false, 
            "journalist_designation": "atomistic leviathan", 
            "key": {
                "public": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFJZi2ABEACZJJA53+pEAdkZyD99nxB995ZVTBw60SQ/6E/gws4kInv+YS7t\nwSMXGa5bR4SD9voWxzLgyulqbM93jUFKn5GcsSh2O/lxAvEDKsPmXCRP1eBg3pjU\n+8DRLm0TEFiywC+w6HF4PsOh+JlBWafUfL3vwrGKTXvrlKBsosvDmoogLjkMWomM\nKBF/97OKyQiMQf1BDJqZ88nScJEqwo0xz0PfcB04GAtfR7N6Qa8HpFc0VDQcILFB\n0aJx5+p7nw1LyR37LLoK8JbEY6QZd277Y0/U+O4v6WfH/2H5kQ8sC+P8hPwr3rSg\nu3SVbNRasB4ZHFpJZR9Kv21zmQb9U3rrCk2yg3Wm0qtZ0S5CECAAwG2LQkKouRw2\nak+Y8aolHDt6a785eF0AaAtgbPX4THMum/CNMksHO0PBBqxR+C9z7WSHXFHvv+8B\n5nRccS4m4klyYTbZOOJ45DuC3xDjTRwzzpkYhqf4pLAhwF3spKZsAczAFPmDyxFf\nCyIBiMZSK/j8PMJT1X5tgpL1NXImNdVIPV2Fy+W7PkNfG2FL/FQIUnK6ntukLW/7\nhV6VHcx52mMn1pVUc6v80LEb4BMDz41vlj9R8YVv8hycPtnN0QL5gIME1n7jbKJf\nyfWxkvBXMINDgHK/RysRMP6FXA6Mw65BGNIuO0Il0FTy12HuKI/coEsG2QARAQAB\ntDZTZWN1cmVEcm9wIFRlc3QvRGV2ZWxvcG1lbnQgKERPIE5PVCBVU0UgSU4gUFJP\nRFVDVElPTimJAjsEEwECACUCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheABQJS\nm8UzAhkBAAoJEMxA7xIoJxRB1hAP/jVoFRi1R3i4P3EhmaYg9VQUo5SRyfMDoE6r\nFyzOv2x3vRqPM1Bm4ihLQePfwKsJLDo7UVgjmTNEY4bpSYmKus/uo6Kx6yrxm6d/\nJzY0BER+LJi0iA0iyLTqYk3eXyxQmHmy6my8zVyag5k/f/DejSUQgckJZ9pAhr7r\nq4aTCWYapo/6fDM0XAo1T5Upt/iSqHet6NZR15JCDHIvGJYGAxVemccSNKFb1tsn\n5aIMuGDbNivCUIFav+eo2JIEy60BokcZCy68qWwtlO5nIao79MoNMNz2EFSOomOg\nb1sNadEj2vAkLfU4+dOVbYsFGUzOaV0mUHcaTNPYwnK+PgyOi5M05BX55a9FSBgi\nAsEwEnDK1lvzLfWEQxVQvsw9A9vnCbSX8PwC4/uUtokkKxVN9ICl8AfaT38+OUHW\niNl4NCgd26iRgTLhfMXpTjRyOb2RvFdzLByDEWIbvu5kCh247UFYSL0llk+suNh3\ncm0mOUdL1nZuEo4EyEF1dq+1opMfDMF98q0660wZdwvwUQIXBt/yK3FH0BGA66ai\nR78Z4pH1JqtYvzfDJx+XP8O2N9GYGd7kpak/5C2BTJzLVyzagB1yi8SmiYna5yQj\nEqW5Txeq0GGd2H4KtUETUevU4x0Rw3luHToaDd9d5sioF48o87PlGwk+OCofPfLj\nLnwFPNZcuQINBFJZi2ABEADzfv+9Ogb4KEWFom9zMF+xg8bcd/Ct72/sWLQW6Pz6\n+SkmLEHuklTO+k7xiQ6jdzXzj1rTfy317L7G51naBSb6Ekfv8mu2ogOwrvtgYnGC\nvfCpooUSxcfi+aEJzIJL29TAi1RCLZm15KRbkvEl8wS93BSLiag5w4/8eP1vXebq\n95GrCZwiNZdhdQs3qn4j3VRvTW/SZHIAdJY+mMfUMPjq4c4sA82os6kVrEnWeLGf\nT9d+knfm9J/2Rumy90bLAY6SFmRZ9/DxwKwbIsVy8CRvU3RVFSX8HCBQepRCQkls\n9r7KVBqYE2Wh+0a+9wHHHNI7VBxKGXPflrirxY1AB5vjLcX1hmXbCoyf4ytgdHyC\nKDz9Oc+xkgJeyVW6XwSqc5EhuNFXp3+C7BF7eQZ1REJLbL6CtEkeF0jHBaTeKM/p\nN4fVhjPiU/FsNmZGKxxLyxDnnDI5pY8bhphVxwBRZ5GtVNqiVNDw+rRACQalpT21\nOcAgLP+Rz+qf3TPyEZN6WPEx8/76ILuSHb8mpOH7W/514f5NuFaAlgmUnO3cT10h\nh4IwOQ+kvj0qMww8fASI9DJExXUYb3xDSCmOkJPhu1/Drr3gdFBha4/jAz7jBWls\nVr2RLJzilf8Mi9j8WpHIfP+WXtwWz3+iYPS0SPoB7g9DA0+Ei760pJJf73AEjD+f\nFwARAQABiQIfBBgBAgAJBQJSWYtgAhsMAAoJEMxA7xIoJxRBp/cP/3lJx9z5yzZA\n6UvLQR6pK+V1iy2hvZ+S+EwYRCiTgYTXekHzLXWwjWGfUYDTHMeaS9O9BMRMGOU3\ninyb47GZSoQ0N0bRVTzrY6/0ifhUSJ00MemOodI1bz4pAMk3uR8iWyhlaGn7JAIA\nKmCm+K0qkeJd61S9iyrx7s9QmaNPnupm5pc+bpOAkbKyq7sEFpWM5Qx82n1tVMtn\nIW2OoRPbz80JkkQB2pl6SjskXqZ89jcFWGI6IChYENKc65xafDt4uFuHU+5j4j2f\n4ySYSwfoWC97MOgJLqA/WimxeeNCYFhykUDWrL5mKBTgMXgH/sYk3GDo7fssaYbK\nn1xbbX4GXQl3+ru4zT6/F7CxZErjLb+evShyf4itM+5AdbKRiRzoraqKblBa4TfJ\nBSqHisdcxdZeBe19+jyY6a8ZMcGhrQeksiKxTRh7ylAk7CLVgLEIHLxXzHoZ0oAF\nz2ulG+zH9KS9Pe8MQxHCrlyfoQElQuJoYbrYBOu28itvGPgz6+5xgvZROvPoqIkI\nk8DYt9lJqUFBeZuFJd5W1TuHKLxueVYvSKeG+e3TjOYdJFvDZInM4cNWr8N92mYS\niphljiHAKVTQeIf1ma07QUH/ul3YC+g07F+BLonIIXA6uQVebv5iLxTgOzIQwHTJ\nVu4MPiQNn1h4dk1RonfV/aJ+de1+qjA8mQINBFGRfoABEADJ56nCxMuQeyp/LMhz\nEyRCQZ6UrYS3KVHtOPz7Q+N/4rWsS+WuMO8X606xVA43JFVJgqeD+a4pcknmw9k0\nO7WzD+peswLCKulQ0s+yaZjP0G08CQH8S3nwG/zoCBxX+FXNnThtN+2u+XhGcKNA\nCzWaiH8KiO9YUSUmiexCP3REBgMLucFFTnQK6RUcuVmYYS+VS87hYrEN4F6ANw5E\neuENj++a36AJNFrzPO+QAfqZfe8UY64wiyoXaELKCPKUMq3vka8oR6wUwPGw+053\nEpmd8BEy/7Ol6BdNbthB0bSrgMtnfEjXlU1LuusSPXOTAIXHr63+45oxAJ3MO8qo\nNXznWbPwElQAR/2xrsy2z20TDuvHCS2frbITmB6oN76505YDFPjc19dXKWUurapQ\nHsTkG1Bv/P/31/g46LA9TrULu+ZN7DxzvvxDEvpPnrjKMjeew21v2w1P29DBhpoZ\nwaxUhpMC7SF4yiEotaUXiZkaxCNkxT/l1UPQkS6XhuuGhsEv81BOuibPsI388nRN\nx5OUaCnXH9FxaFbyJyfmDek9IaymEXjVfD9gr1fePG0Wsa94D29nQ/GCegw4UhbD\nCZD6WX/Fi4kK8SBOOg0fU/5cW0eSZ8bpfTEXM++T0mKpbD+pz0QQMJHJuo46hZ7n\nWGslt7taCmqIPHI0KNNCKYPAfwARAQABtHxBdXRvZ2VuZXJhdGVkIEtleSA8RkdW\nTUJPWFc2UVBSTDRJMlpBWjZUWktEV0IyUEFNNjY3UE5HQ0tTRVROSEdBR1RVS1NH\nT1FNT0xJTFVTSk5YR0RZNUZPUjRJQUxPQ01TUjJXWE5GWlFKVFU2R01HVkpQR09S\nR0RQST0+iQI5BBMBCgAjBQJRkX6AAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgEC\nF4AACgkQPaif/ttqPFxozg/+LgcbcStFBRxSzui0gZI2OEIdhL2ELZRSflhjOx65\n0AfJO1hFZw3gHKrCv2AYwOXRqWhZHpwZ67qtkOTbwP2ENs2JNJodzk3CLRXm4Ygo\nCABbH0QFd85osareTKA8qt1dIfzFwHuprJRCoc0cOI0vtzyK1fyWxAriRkn+Df2u\ne4FWJRI3LfvvCccr0+xkfac9RkgAUVIl46NyplnfRCwiHH0UkAzXYxOCSZ7NbT63\nHImZ8pkQifReFgANwljjtMpuNKw33otp7FMBK/VyeBcsHj/KhotMLbdmJvQ3Jwur\n5zT9/jR1F6Bh+JTHk8+sQZtefA9DTpFI6mBQl56toCqWMWfXB58tlv/YYb7GPhJI\n3dhc6UBJ8AHYA3Sgv698hITmAM4Yy+RBrGfAVKPpCwKr4wNh58BtHQ4J0oT4cLEg\nOr03QbnLIfg8EFBkIEfA+GP4/kwVrdAQxqoRaksbWHXRUbXfaoraglYszAB9n004\nJ5B0g0JwSgRRK0RpSrFLgW50bfCX6NqeYTrxVL2tUXlZD8nKKu/tbmVymEgZameA\nVpR4sSU++9uBU3tsvqqqSR7SAKZDDz2kX8ofyvvqPeFXPQA4wyVcP9STgPUfYrn7\ntlSz6oq/fJHJcSw4TgY/zt7UZZxm0fJQgN8384mxsBfcHYyblx4kMPnjwQ/B5067\nIuA=\n=5Nq3\n-----END PGP PUBLIC KEY BLOCK-----\n", 
                "type": "PGP"
            }, 
            "last_updated": "2018-12-22T13:14:02.588646Z", 
            "number_of_documents": 0, 
            "number_of_messages": 1, 
            "remove_star_url": "/api/v1/sources/dc17df20-6459-4317-8b3c-67b51997d3ce/remove_star", 
            "replies_url": "/api/v1/sources/dc17df20-6459-4317-8b3c-67b51997d3ce/replies", 
            "submissions_url": "/api/v1/sources/dc17df20-6459-4317-8b3c-67b51997d3ce/submissions", 
            "url": "/api/v1/sources/dc17df20-6459-4317-8b3c-67b51997d3ce", 
            "uuid": "dc17df20-6459-4317-8b3c-67b51997d3ce"
        }
    ]
}
```

If you import the above key into GPG, you will see that it contains:
- Journalist's pub key
- First source's pub key
- **Not** the second source's pub key

## Comments

The root cause is here:

```python
    def export_pubkey(self, name):
        fingerprint = self.getkey(name)
        return self.gpg.export_keys(fingerprint)
```

if `None` is passed to `export_keys`, then it will return *all* pub keys.

This bug will cause the following fatal error in the Qt client.

```
Traceback (most recent call last):
  File "/home/heartsucker/code/freedomofpress/securedrop-client/securedrop_client/logic.py", line 189, in <lambda>
    lambda: self.completed_api_call(new_thread_id, callback))
  File "/home/heartsucker/code/freedomofpress/securedrop-client/securedrop_client/logic.py", line 242, in completed_api_call
    user_callback(result_data)
  File "/home/heartsucker/code/freedomofpress/securedrop-client/securedrop_client/logic.py", line 419, in on_synced
    self.gpg.import_key(source.uuid, pub_key)
  File "/home/heartsucker/code/freedomofpress/securedrop-client/securedrop_client/crypto.py", line 116, in import_key
    raise RuntimeError('Expected exactly one fingerprint. Found: {}'
RuntimeError: Expected exactly one fingerprint.
```

In additional to the error in Qt, it seems likely this will cause errors in the web UI too (both interfaces), but I haven't tested that yet.


Discussion:
There is a related bit of code in `crypto_util` that makes that may be worth resolving along side this.

```python
    def getkey(self, name):
        for key in self.gpg.list_keys():
            for uid in key['uids']:
                if name in uid:
                    return key['fingerprint']
        return None
```

If we call this like `app.crypto_util.getekey('a')`, the letter `a` will match many keys but only the first will be returned. This will most likely lead to us using the wrong key.

Instead of listing all the keys and iterating through them, it may be better to use the GPG wrapper to extract exactly the key we want, then check that the extraction returned only one key. I'm fairly sure the wrapper supports this.

If we do not make the change above, when we call `export_pubkey`, we may be able to just directly use the `filesystem_id` since this is unique enough to be able to extract the pub key. It also would avoid the error originally reported in the ticket. The new function would look like:

```python
def export_pubkey(self, identifier):
    # 40 is the len of a fingerprint, scrypt filesystem_ids are longer than that
    if not identifier and not len(identifier) > 40:
        raise Exception("not today!")
    return self.gpg.export_keys(identifier)
As a related issue, if we use the code snippet above of exporting by identifier, this happens


```
    def test_export_pubkey(source_app, test_source):
        export = source_app.crypto_util.export_pubkey
        begin_pgp = '-----BEGIN PGP PUBLIC KEY BLOCK----'
    
        # check that a filesystem_id exports the pubkey
        exported_by_fid = export(test_source['filesystem_id'])
        assert exported_by_fid.startswith(begin_pgp)
    
        # check that a fingerprint exports the pubkey
        test_source_fpr = source_app.crypto_util.getkey(
            test_source['filesystem_id'])
        assert test_source_fpr  # precondition
        exported_by_fpr = export(test_source_fpr)
        assert exported_by_fid.startswith(begin_pgp)
    
        # both exports should be equal
>       assert exported_by_fid == exported_by_fpr
E       AssertionError: assert '-----BEGIN P...Y BLOCK-----\n' == '-----BEGIN PG...Y BLOCK-----\n'
E           -----BEGIN PGP PUBLIC KEY BLOCK-----
E           
E         + mI0EUZF+gAEEANB0+sUg2oIC1gTupLac8JDAn89aEwZ1wdUoa6S1iXl0/G9Ez8Gk
E         + fwKHtREnYi9O2HzjKTnZmMTa2HOwrX5H+P4OhAhUIC5iWmA8UfBTg8iFtGK9OXy/
E         + PEcEHRtTvNOn/b+tqlMiyWGv3AGpW4jaWbMmjKL6AEYcwUP2j4WayxlzABEBAAG0
E         + fEF1dG9nZW5lcmF0ZWQgS2V5IDw0QzdESDNJRFBBUk9ONTVVWTJRVldCVkRZQ1hV
E         + RktHUVhWRURHTVZIM09WSE40RllSWlJRV0NaNDY3RlEyS0pBQ1pUWVBFRVRSN1dN
E         + R1ozQzRMWllQQUVJUzVMMlE2TlRXV0syRDVZPT6IuQQTAQoAIwUCUZF+gAIbLwcL
E         + CQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEHpVhzvUWhgO5OUEAL5+s1cPSDFB
E         + jq4Huk6SuKtS0BRY0vfKbPiyRUlKibxu+Gc/7ACBoeVq74SadxXhJEUeF/rD99qY
E         + 6jP933gHG2XYdaYOab/H0aHynaEqGH1EXgqv8dSpKcRqBzka1KkaWUhVQJIx2TVz
E         + fCZFbuMKbnSLiJgg9B1gbLNQrk5/lhxx
E         + =pQMj
E         - mQINBFJZi2ABEACZJJA53+pEAdkZyD99nxB995ZVTBw60SQ/6E/gws4kInv+YS7t
E         - wSMXGa5bR4SD9voWxzLgyulqbM93jUFKn5GcsSh2O/lxAvEDKsPmXCRP1eBg3pjU
E         - +8DRLm0TEFiywC+w6HF4PsOh+JlBWafUfL3vwrGKTXvrlKBsosvDmoogLjkMWomM
E         - KBF/97OKyQiMQf1BDJqZ88nScJEqwo0xz0PfcB04GAtfR7N6Qa8HpFc0VDQcILFB
E         - 0aJx5+p7nw1LyR37LLoK8JbEY6QZd277Y0/U+O4v6WfH/2H5kQ8sC+P8hPwr3rSg
E         - u3SVbNRasB4ZHFpJZR9Kv21zmQb9U3rrCk2yg3Wm0qtZ0S5CECAAwG2LQkKouRw2
E         - ak+Y8aolHDt6a785eF0AaAtgbPX4THMum/CNMksHO0PBBqxR+C9z7WSHXFHvv+8B
E         - 5nRccS4m4klyYTbZOOJ45DuC3xDjTRwzzpkYhqf4pLAhwF3spKZsAczAFPmDyxFf
E         - CyIBiMZSK/j8PMJT1X5tgpL1NXImNdVIPV2Fy+W7PkNfG2FL/FQIUnK6ntukLW/7
E         - hV6VHcx52mMn1pVUc6v80LEb4BMDz41vlj9R8YVv8hycPtnN0QL5gIME1n7jbKJf
E         - yfWxkvBXMINDgHK/RysRMP6FXA6Mw65BGNIuO0Il0FTy12HuKI/coEsG2QARAQAB
E         - tDZTZWN1cmVEcm9wIFRlc3QvRGV2ZWxvcG1lbnQgKERPIE5PVCBVU0UgSU4gUFJP
E         - RFVDVElPTimJAjgEEwECACIFAlKbxQMCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B
E         - AheAAAoJEMxA7xIoJxRBF4QP/i4aTU4ZIkeKnYSyAA+vAvovdpP0Uwq355AIuiPk
E         - N2gfe1jSZcYw//Qz+girZj6roERdU0Rg7LZDNOec+qf3rm2o39JkXUI278hibEIm
E         - PtNImL8y5a2Dsh5tHwq9YsomNbg9TnJuYHaHPI7eNMTNdjePxuhwZ/HdDwHqi38x
E         - 0ryyzkmstIg+WBAeHJeWh7gJNOTRqVBu23woSnSQxeaqOvProCyh/CPNx7M6fU9J
E         - zRLi1XvQ6QgRql68FyvtydhC/eBZJekrjNk2D01nl0tHveDrgu/4aorRQGSy54x8
E         - QSbH+4MF3v+7GbPuNYVvcP0wEsKMAxD6zwqHxslD8c8yMiHxP3Lx6T90jkiXt6EV
E         - pyLXLNP6og+3tICuPergpHmUI14grsG7wq67ZWB8JGyR7Y8kBCVq67uM7ddlwtK6
E         - KHBN0sux75Yh1bqaLJzMk0C3WkeScex1cA16AmHt6gDL6CEWiTuRmlgxNH+LL3kS
E         - o3EC3qUuK9h7LNS18+MeMOmo9S2tlEaKmqYXAhxOfkT8eiSRWJRKLPHRJ3YvIna2
E         - jHpr5rOWt5biFFjlZU9yVV5PKe8C0ytKXUMQStGhUr8BkqcGBQR4MJ32D8SJHM4u
E         - tflrNv2cKIWUsx5ZZWZt5++cJetcmpVA2tVl16VJlqQmkn+hCkrK//mdT2lyB5Wv
E         - M/rxiQI7BBMBAgAlAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAUCUpvFMwIZ
E         - AQAKCRDMQO8SKCcUQdYQD/41aBUYtUd4uD9xIZmmIPVUFKOUkcnzA6BOqxcszr9s
E         - d70ajzNQZuIoS0Hj38CrCSw6O1FYI5kzRGOG6UmJirrP7qOisesq8Zunfyc2NARE
E         - fiyYtIgNIsi06mJN3l8sUJh5supsvM1cmoOZP3/w3o0lEIHJCWfaQIa+66uGkwlm
E         - GqaP+nwzNFwKNU+VKbf4kqh3rejWUdeSQgxyLxiWBgMVXpnHEjShW9bbJ+WiDLhg
E         - 2zYrwlCBWr/nqNiSBMutAaJHGQsuvKlsLZTuZyGqO/TKDTDc9hBUjqJjoG9bDWnR
E         - I9rwJC31OPnTlW2LBRlMzmldJlB3GkzT2MJyvj4MjouTNOQV+eWvRUgYIgLBMBJw
E         - ytZb8y31hEMVUL7MPQPb5wm0l/D8AuP7lLaJJCsVTfSApfAH2k9/PjlB1ojZeDQo
E         - HduokYEy4XzF6U40cjm9kbxXcywcgxFiG77uZAoduO1BWEi9JZZPrLjYd3JtJjlH
E         - S9Z2bhKOBMhBdXavtaKTHwzBffKtOutMGXcL8FECFwbf8itxR9ARgOumoke/GeKR
E         - 9SarWL83wycflz/DtjfRmBne5KWpP+QtgUycy1cs2oAdcovEpomJ2uckIxKluU8X
E         - qtBhndh+CrVBE1Hr1OMdEcN5bh06Gg3fXebIqBePKPOz5RsJPjgqHz3y4y58BTzW
E         - XLkCDQRSWYtgARAA837/vToG+ChFhaJvczBfsYPG3Hfwre9v7Fi0Fuj8+vkpJixB
E         - 7pJUzvpO8YkOo3c1849a038t9ey+xudZ2gUm+hJH7/JrtqIDsK77YGJxgr3wqaKF
E         - EsXH4vmhCcyCS9vUwItUQi2ZteSkW5LxJfMEvdwUi4moOcOP/Hj9b13m6veRqwmc
E         - IjWXYXULN6p+I91Ub01v0mRyAHSWPpjH1DD46uHOLAPNqLOpFaxJ1nixn0/XfpJ3
E         - 5vSf9kbpsvdGywGOkhZkWffw8cCsGyLFcvAkb1N0VRUl/BwgUHqUQkJJbPa+ylQa
E         - mBNloftGvvcBxxzSO1QcShlz35a4q8WNQAeb4y3F9YZl2wqMn+MrYHR8gig8/TnP
E         - sZICXslVul8EqnORIbjRV6d/guwRe3kGdURCS2y+grRJHhdIxwWk3ijP6TeH1YYz
E         - 4lPxbDZmRiscS8sQ55wyOaWPG4aYVccAUWeRrVTaolTQ8Pq0QAkGpaU9tTnAICz/
E         - kc/qn90z8hGTeljxMfP++iC7kh2/JqTh+1v+deH+TbhWgJYJlJzt3E9dIYeCMDkP
E         - pL49KjMMPHwEiPQyRMV1GG98Q0gpjpCT4btfw6694HRQYWuP4wM+4wVpbFa9kSyc
E         - 4pX/DIvY/FqRyHz/ll7cFs9/omD0tEj6Ae4PQwNPhIu+tKSSX+9wBIw/nxcAEQEA
E         - AYkCHwQYAQIACQUCUlmLYAIbDAAKCRDMQO8SKCcUQaf3D/95Scfc+cs2QOlLy0Ee
E         - qSvldYstob2fkvhMGEQok4GE13pB8y11sI1hn1GA0xzHmkvTvQTETBjlN4p8m+Ox
E         - mUqENDdG0VU862Ov9In4VEidNDHpjqHSNW8+KQDJN7kfIlsoZWhp+yQCACpgpvit
E         - KpHiXetUvYsq8e7PUJmjT57qZuaXPm6TgJGysqu7BBaVjOUMfNp9bVTLZyFtjqET
E         - 28/NCZJEAdqZeko7JF6mfPY3BVhiOiAoWBDSnOucWnw7eLhbh1PuY+I9n+MkmEsH
E         - 6FgvezDoCS6gP1opsXnjQmBYcpFA1qy+ZigU4DF4B/7GJNxg6O37LGmGyp9cW21+
E         - Bl0Jd/q7uM0+vxewsWRK4y2/nr0ocn+IrTPuQHWykYkc6K2qim5QWuE3yQUqh4rH
E         - XMXWXgXtffo8mOmvGTHBoa0HpLIisU0Ye8pQJOwi1YCxCBy8V8x6GdKABc9rpRvs
E         - x/SkvT3vDEMRwq5cn6EBJULiaGG62ATrtvIrbxj4M+vucYL2UTrz6KiJCJPA2LfZ
E         - SalBQXmbhSXeVtU7hyi8bnlWL0inhvnt04zmHSRbw2SJzOHDVq/DfdpmEoqYZY4h
E         - wClU0HiH9ZmtO0FB/7pd2AvoNOxfgS6JyCFwOrkFXm7+Yi8U4DsyEMB0yVbuDD4k
E         - DZ9YeHZNUaJ31f2ifnXtfqowPJiNBFGRfoABBADQdPrFINqCAtYE7qS2nPCQwJ/P
E         - WhMGdcHVKGuktYl5dPxvRM/BpH8Ch7URJ2IvTth84yk52ZjE2thzsK1+R/j+DoQI
E         - VCAuYlpgPFHwU4PIhbRivTl8vzxHBB0bU7zTp/2/rapTIslhr9wBqVuI2lmzJoyi
E         - +gBGHMFD9o+FmssZcwARAQABtHxBdXRvZ2VuZXJhdGVkIEtleSA8NEM3REgzSURQ
E         - QVJPTjU1VVkyUVZXQlZEWUNYVUZLR1FYVkVER01WSDNPVkhONEZZUlpSUVdDWjQ2
E         - N0ZRMktKQUNaVFlQRUVUUjdXTUdaM0M0TFpZUEFFSVM1TDJRNk5UV1dLMkQ1WT0+
E         - iLkEEwEKACMFAlGRfoACGy8HCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRB6
E         - VYc71FoYDuTlBAC+frNXD0gxQY6uB7pOkrirUtAUWNL3ymz4skVJSom8bvhnP+wA
E         - gaHlau+EmncV4SRFHhf6w/famOoz/d94Bxtl2HWmDmm/x9Gh8p2hKhh9RF4Kr/HU
E         - qSnEagc5GtSpGllIVUCSMdk1c3wmRW7jCm50i4iYIPQdYGyzUK5Of5YccQ==
E         - =4OIG
E           -----END PGP PUBLIC KEY BLOCK-----

tests/test_crypto_util.py:299: AssertionError
```

You can see in the above that I'm exporting once by `filesystem_id` and then once by `fingerprint` and then compare. They are unexpectedly not equal.

@redshiftzero mentioned that this may be an upstream issue, but also we're far behind the latest version of `python-gnupg` (see #3622) so :man_shrugging: 
Hmm, did you confirm that it is indeed the case that we should expect to be able to pass `filesystem_id` to `GPG.export_keys` instead of a fingerprint? Asking because [the docstring for that method](https://github.com/isislovecruft/python-gnupg/blob/2.3.1/gnupg/gnupg.py#L425) says that keyids or fingerprints are expected as args to `GPG.export_keys`

When I tested just now, it looks like the behavior when the identifier actually doesn't match anything is to still return a public-key looking return value (i.e. the underlying problem here could just be passing an unexpected arg to `GPG.export_keys`):

```
(Pdb) source_app.crypto_util.gpg.export_keys('this definitely should not return anything')
u'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFJZi2ABEACZ ... [i snippity snipped some radix64 encoded stuff] ... RM3C5eMH/6s0g6shvlCb+DOCA==\n=IE/M\n-----END PGP PUBLIC KEY BLOCK-----\n'
```
Yeah I just woke up and checked that with `pgpdump` from the command line and this is def related to the lib. lol unexpected computer behavior. I guess we'll have to just traverse the whole list each time.

Resolving patch:
diff --git a/securedrop/crypto_util.py b/securedrop/crypto_util.py
--- a/securedrop/crypto_util.py
+++ b/securedrop/crypto_util.py
@@ -205,7 +205,10 @@ def getkey(self, name):
 
     def export_pubkey(self, name):
         fingerprint = self.getkey(name)
-        return self.gpg.export_keys(fingerprint)
+        if fingerprint:
+            return self.gpg.export_keys(fingerprint)
+        else:
+            return None
 
     def encrypt(self, plaintext, fingerprints, output=None):
         # Verify the output path


Test patch:
diff --git a/securedrop/tests/test_crypto_util.py b/securedrop/tests/test_crypto_util.py
--- a/securedrop/tests/test_crypto_util.py
+++ b/securedrop/tests/test_crypto_util.py
@@ -278,3 +278,19 @@ def test_delete_reply_keypair_no_key(source_app):
 def test_getkey(source_app, test_source):
     assert (source_app.crypto_util.getkey(test_source['filesystem_id'])
             is not None)
+
+    # check that a non-existent key returns None
+    assert source_app.crypto_util.getkey('x' * 50) is None
+
+
+def test_export_pubkey(source_app, test_source):
+    begin_pgp = '-----BEGIN PGP PUBLIC KEY BLOCK----'
+
+    # check that a filesystem_id exports the pubkey
+    exported = source_app.crypto_util.export_pubkey(
+        test_source['filesystem_id'])
+    assert exported.startswith(begin_pgp)
+
+    # check that a non-existent identifer exports None
+    exported = source_app.crypto_util.export_pubkey('x' * 50)
+    assert exported is None