diff options
-rw-r--r-- | gemato/openpgp.py | 44 | ||||
-rw-r--r-- | tests/test_openpgp.py | 28 |
2 files changed, 66 insertions, 6 deletions
diff --git a/gemato/openpgp.py b/gemato/openpgp.py index 051cfc1..42b2949 100644 --- a/gemato/openpgp.py +++ b/gemato/openpgp.py @@ -3,6 +3,7 @@ # (c) 2017-2018 Michał Górny # Licensed under the terms of 2-clause BSD license +import datetime import errno import os.path import shutil @@ -12,6 +13,18 @@ import tempfile import gemato.exceptions +class OpenPGPSignatureData(object): + __slots__ = ['fingerprint', 'timestamp', 'expire_timestamp', + 'primary_key_fingerprint'] + + def __init__(self, fingerprint, timestamp, expire_timestamp, + primary_key_fingerprint): + self.fingerprint = fingerprint + self.timestamp = timestamp + self.expire_timestamp = expire_timestamp + self.primary_key_fingerprint = primary_key_fingerprint + + class OpenPGPSystemEnvironment(object): """ OpenPGP environment class that uses the global OpenPGP environment @@ -50,6 +63,21 @@ class OpenPGPSystemEnvironment(object): raise NotImplementedError('refresh_keys() is not implemented by this OpenPGP provider') + def _parse_gpg_ts(self, ts): + """ + Parse GnuPG status timestamp that can either be time_t value + or ISO 8601 timestamp. + """ + # that's how upstream tells us to detect this + if 'T' in ts: + # TODO: is this correct for all cases? is it localtime? + return datetime.datetime.strptime(ts, '%Y%m%dT%H%M%S') + elif ts == '0': + # no timestamp + return None + else: + return datetime.datetime.fromtimestamp(int(ts)) + def verify_file(self, f): """ Perform an OpenPGP verification of Manifest data in open file @f. @@ -64,6 +92,7 @@ class OpenPGPSystemEnvironment(object): raise gemato.exceptions.OpenPGPVerificationFailure(err.decode('utf8')) is_good = False + sig_data = None # process the output of gpg to find the exact result for l in out.splitlines(): @@ -73,9 +102,20 @@ class OpenPGPSystemEnvironment(object): raise gemato.exceptions.OpenPGPExpiredKeyFailure(err.decode('utf8')) elif l.startswith(b'[GNUPG:] REVKEYSIG'): raise gemato.exceptions.OpenPGPRevokedKeyFailure(err.decode('utf8')) - - if not is_good: + elif l.startswith(b'[GNUPG:] VALIDSIG'): + spl = l.split(b' ') + assert len(spl) >= 12 + fp = spl[2].decode('utf8') + ts = self._parse_gpg_ts(spl[4].decode('utf8')) + expts = self._parse_gpg_ts(spl[5].decode('utf8')) + pkfp = spl[11].decode('utf8') + + sig_data = OpenPGPSignatureData(fp, ts, expts, pkfp) + + # require both GOODSIG and VALIDSIG + if not is_good or sig_data is None: raise gemato.exceptions.OpenPGPUnknownSigFailure(err.decode('utf8')) + return sig_data def clear_sign_file(self, f, outf, keyid=None): """ diff --git a/tests/test_openpgp.py b/tests/test_openpgp.py index f42ad85..6f09014 100644 --- a/tests/test_openpgp.py +++ b/tests/test_openpgp.py @@ -4,6 +4,7 @@ # Licensed under the terms of 2-clause BSD license import base64 +import datetime import io import os.path import shutil @@ -210,6 +211,9 @@ mkkhTd2Auao4D2K74BePBuiZ9+eDQA== -----END PGP SIGNATURE----- ''' +KEY_FINGERPRINT = '81E12C16BD8DCD60BE180845136880E72A7B1384' +SIG_TIMESTAMP = datetime.datetime(2017, 11, 8, 10, 1, 26) + def strip_openpgp(text): lines = text.lstrip().splitlines() @@ -355,11 +359,19 @@ class OpenPGPCorrectKeyTest(unittest.TestCase): def test_verify_manifest(self): with io.StringIO(SIGNED_MANIFEST) as f: - self.env.verify_file(f) + sig = self.env.verify_file(f) + self.assertEqual(sig.fingerprint, KEY_FINGERPRINT) + self.assertEqual(sig.timestamp, SIG_TIMESTAMP) + self.assertIsNone(sig.expire_timestamp) + self.assertEqual(sig.primary_key_fingerprint, KEY_FINGERPRINT) def test_verify_dash_escaped_manifest(self): with io.StringIO(DASH_ESCAPED_SIGNED_MANIFEST) as f: - self.env.verify_file(f) + sig = self.env.verify_file(f) + self.assertEqual(sig.fingerprint, KEY_FINGERPRINT) + self.assertEqual(sig.timestamp, SIG_TIMESTAMP) + self.assertIsNone(sig.expire_timestamp) + self.assertEqual(sig.primary_key_fingerprint, KEY_FINGERPRINT) def test_verify_modified_manifest(self): with io.StringIO(MODIFIED_SIGNED_MANIFEST) as f: @@ -628,7 +640,11 @@ class OpenPGPContextManagerTest(unittest.TestCase): except RuntimeError: raise unittest.SkipTest('Unable to import OpenPGP key') - env.verify_file(f) + sig = env.verify_file(f) + self.assertEqual(sig.fingerprint, KEY_FINGERPRINT) + self.assertEqual(sig.timestamp, SIG_TIMESTAMP) + self.assertIsNone(sig.expire_timestamp) + self.assertEqual(sig.primary_key_fingerprint, KEY_FINGERPRINT) except gemato.exceptions.OpenPGPNoImplementation as e: raise unittest.SkipTest(str(e)) @@ -666,7 +682,11 @@ class OpenPGPPrivateKeyTest(unittest.TestCase): def test_verify_manifest(self): with io.StringIO(SIGNED_MANIFEST) as f: - self.env.verify_file(f) + sig = self.env.verify_file(f) + self.assertEqual(sig.fingerprint, KEY_FINGERPRINT) + self.assertEqual(sig.timestamp, SIG_TIMESTAMP) + self.assertIsNone(sig.expire_timestamp) + self.assertEqual(sig.primary_key_fingerprint, KEY_FINGERPRINT) def test_sign_data(self): with io.StringIO(self.TEST_STRING) as f: |