Repository: freedomofpress/securedrop

Issue:
test_submit_message and test_submit_file failure on Focal container
## Description

On `dev_focal` branch, if we run tests, we will see errors related to gpg decryption operation.

## Steps to Reproduce

- [ ] `make test-focal`


## Expected Behavior

- [ ] all tests should pass

## Actual Behavior

`test_submit_message` and `test_submit_file` are failing with the following error.

```
source_app = <Flask 'source_app'>, journalist_app = <Flask 'journalist_app'>                                                                                                                       
test_journo = {'first_name': None, 'id': 1, 'journalist': <Journalist untrained henna pastime borough olympics cathouse tinfoil okay>, 'last_name': None, ...}                                     
                                                                                                                                                                                                   
    def test_submit_message(source_app, journalist_app, test_journo):                                                                                                                              
        """When a source creates an account, test that a new entry appears                                                                                                                         
        in the journalist interface"""                                                                                                                                                             
        test_msg = "This is a test message."                                                                                                                                                       
                                                                                                                                                                                                   
        with source_app.test_client() as app:                                                                                                                                                      
            app.get('/generate')                                                                                                                                                                   
            tab_id = next(iter(session['codenames'].keys()))                                                                                                                                       
            app.post('/create', data={'tab_id': tab_id}, follow_redirects=True)                                                                                                                    
            filesystem_id = g.filesystem_id                                                                                                                                                        
            # redirected to submission form                                                                                                                                                        
            resp = app.post('/submit', data=dict(                                                                                                                                                  
                msg=test_msg,                                                                                                                                                                      
                fh=(BytesIO(b''), ''),                                                                                                                                                             
            ), follow_redirects=True)                                                                                                                                                              
            assert resp.status_code == 200                                                                                                                                                         
            app.get('/logout')                                                                                                                                                                     
                                                                                                                                                                                                   
        # Request the Journalist Interface index                                                                                                                                                   
        with journalist_app.test_client() as app:                                                                                                                                                  
            _login_user(app, test_journo)                                                                                                                                                          
            resp = app.get('/')                                                                                                                                                                    
            assert resp.status_code == 200                                                                                                                                                         
            text = resp.data.decode('utf-8')                                                                                                                                                       
            assert "Sources" in text                                                                                                                                                               
            soup = BeautifulSoup(text, 'html.parser')

            # The source should have a "download unread" link that
            # says "1 unread"
            col = soup.select('ul#cols > li')[0]
            unread_span = col.select('span.unread a')[0]
            assert "1 unread" in unread_span.get_text()

            col_url = soup.select('ul#cols > li a')[0]['href']
            resp = app.get(col_url)
            assert resp.status_code == 200
            text = resp.data.decode('utf-8')
            soup = BeautifulSoup(text, 'html.parser')
            submission_url = soup.select('ul#submissions li a')[0]['href']
            assert "-msg" in submission_url
            span = soup.select('ul#submissions li span.info span')[0]
            assert re.compile(r'\d+ bytes').match(span['title'])

            resp = app.get(submission_url)
            assert resp.status_code == 200
            decrypted_data = journalist_app.crypto_util.gpg.decrypt(resp.data)
>           assert decrypted_data.ok
E           assert False
E            +  where False = <pretty_bad_protocol._parsers.Crypt object at 0x7f4728da3490>.ok

tests/test_integration.py:98: AssertionError
-------------------------------------------------------------------------------------- Captured stdout setup --------------------------------------------------------------------------------------
main: /tmp/pytest-of-www-data/pytest-0/test_submit_message0/data/db.sqlite
-------------------------------------------------------------------------------------- Captured stderr call ---------------------------------------------------------------------------------------
Exception in thread Thread-2245:
Traceback (most recent call last):
  File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.8/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "/opt/venvs/securedrop-app-code/lib/python3.8/site-packages/pretty_bad_protocol/_meta.py", line 670, in _read_response
    result._handle_status(keyword, value)
  File "/opt/venvs/securedrop-app-code/lib/python3.8/site-packages/pretty_bad_protocol/_parsers.py", line 1772, in _handle_status
    super(Crypt, self)._handle_status(key, value)
  File "/opt/venvs/securedrop-app-code/lib/python3.8/site-packages/pretty_bad_protocol/_parsers.py", line 1671, in _handle_status
    raise ValueError("Unknown status message: %r %r" % (key, value))
ValueError: Unknown status message: 'DECRYPTION_COMPLIANCE_MODE' '23'

```

## Comments

Suggestions to fix, any other relevant information.



Discussion:

Resolving patch:
diff --git a/securedrop/crypto_util.py b/securedrop/crypto_util.py
--- a/securedrop/crypto_util.py
+++ b/securedrop/crypto_util.py
@@ -23,6 +23,9 @@
 
 from models import Source
 
+# monkey patch to work with Focal gnupg.
+# https://github.com/isislovecruft/python-gnupg/issues/250
+gnupg._parsers.Verify.TRUST_LEVELS["DECRYPTION_COMPLIANCE_MODE"] = 23
 
 # to fix GPG error #78 on production
 os.environ['USERNAME'] = 'www-data'


Test patch: