diff options
-rw-r--r-- | gemato/exceptions.py | 8 | ||||
-rw-r--r-- | gemato/openpgp.py | 34 | ||||
-rw-r--r-- | tests/test_openpgp.py | 22 |
3 files changed, 53 insertions, 11 deletions
diff --git a/gemato/exceptions.py b/gemato/exceptions.py index 24451dc..aa4e499 100644 --- a/gemato/exceptions.py +++ b/gemato/exceptions.py @@ -195,6 +195,14 @@ class OpenPGPUnknownSigFailure(OpenPGPRuntimeError): f'{self.output}') +class OpenPGPUntrustedSigFailure(OpenPGPRuntimeError): + """OpenPGP verification failed due to untrusted signing key""" + + def __str__(self): + return (f'Good OpenPGP signature made using untrusted key:\n' + f'{self.output}') + + class OpenPGPSigningFailure(OpenPGPRuntimeError): """ An exception raised when OpenPGP signing fails. diff --git a/gemato/openpgp.py b/gemato/openpgp.py index 83d5d05..b1100c1 100644 --- a/gemato/openpgp.py +++ b/gemato/openpgp.py @@ -25,6 +25,7 @@ from gemato.exceptions import ( OpenPGPKeyListingError, OpenPGPKeyRefreshError, OpenPGPUnknownSigFailure, + OpenPGPUntrustedSigFailure, OpenPGPSigningFailure, ) @@ -70,7 +71,7 @@ class OpenPGPSystemEnvironment: def close(self): pass - def import_key(self, keyfile): + def import_key(self, keyfile, trust=True): """ Import a public key from open file @keyfile. The file should be open for reading in binary mode, and oriented @@ -127,6 +128,7 @@ class OpenPGPSystemEnvironment: raise OpenPGPVerificationFailure(err.decode('utf8')) is_good = False + is_trusted = False sig_data = None # process the output of gpg to find the exact result @@ -146,10 +148,18 @@ class OpenPGPSystemEnvironment: pkfp = spl[11].decode('utf8') sig_data = OpenPGPSignatureData(fp, ts, expts, pkfp) + elif line.startswith(b'[GNUPG:] TRUST_'): + spl = line.split(b' ', 2) + if spl[1] in (b'TRUST_MARGINAL', + b'TRUST_FULL', + b'TRUST_ULTIMATE'): + is_trusted = True # require both GOODSIG and VALIDSIG if not is_good or sig_data is None: raise OpenPGPUnknownSigFailure(err.decode('utf8')) + if not is_trusted: + raise OpenPGPUntrustedSigFailure(err.decode('utf8')) return sig_data def clear_sign_file(self, f, outf, keyid=None): @@ -223,8 +233,8 @@ debug-level guru with open(os.path.join(self._home, 'gpg.conf'), 'w') as f: f.write('''# autogenerated by gemato -# we are using an isolated keyring, so always trust our keys -trust-model always +# we set validity directly on keys +trust-model direct ''') with open(os.path.join(self._home, 'gpg-agent.conf'), 'w') as f: f.write(f'''# autogenerated by gemato @@ -273,12 +283,26 @@ debug-level guru f'{self._home}') self._home = None - def import_key(self, keyfile): + def import_key(self, keyfile, trust=True): exitst, out, err = self._spawn_gpg( - [GNUPG, '--batch', '--import'], keyfile.read()) + [GNUPG, '--batch', '--import', '--status-fd', '1'], + keyfile.read()) if exitst != 0: raise OpenPGPKeyImportError(err.decode('utf8')) + if trust: + fprs = set() + for line in out.splitlines(): + if line.startswith(b'[GNUPG:] IMPORT_OK'): + fprs.add(line.split(b' ')[3].decode('ASCII')) + + ownertrust = ''.join(f'{fpr}:6:\n' for fpr in fprs).encode('utf8') + exitst, out, err = self._spawn_gpg( + [GNUPG, '--batch', '--import-ownertrust'], + ownertrust) + if exitst != 0: + raise OpenPGPKeyImportError(err.decode('utf8')) + def list_keys(self): """ List fingerprints and UIDs of all keys in keyring diff --git a/tests/test_openpgp.py b/tests/test_openpgp.py index edeb0b9..dbb71e5 100644 --- a/tests/test_openpgp.py +++ b/tests/test_openpgp.py @@ -22,10 +22,10 @@ from gemato.exceptions import ( OpenPGPKeyImportError, OpenPGPKeyRefreshError, OpenPGPRuntimeError, + OpenPGPUntrustedSigFailure, ) from gemato.manifest import ManifestFile from gemato.openpgp import ( - GNUPG, OpenPGPEnvironment, OpenPGPSystemEnvironment, ) @@ -338,11 +338,8 @@ class OpenPGPMockedSystemEnvironment(OpenPGPSystemEnvironment): self._tmpdir = None os.environ.pop('GNUPGHOME', None) - def import_key(self, keyfile): - exitst, out, err = self._spawn_gpg( - [GNUPG, '--batch', '--import'], keyfile.read()) - if exitst != 0: - raise OpenPGPKeyImportError(err.decode('utf8')) + def import_key(self, keyfile, trust=True): + OpenPGPEnvironment.import_key(self, keyfile, trust=trust) @pytest.fixture(params=[OpenPGPEnvironment, @@ -420,6 +417,19 @@ def test_verify_manifest(openpgp_env, manifest_var, key_var, expected): pytest.skip(str(e)) +def test_verify_untrusted_key(): + try: + openpgp_env = OpenPGPMockedSystemEnvironment() + with io.BytesIO(VALID_PUBLIC_KEY) as f: + openpgp_env.import_key(f, trust=False) + + with io.StringIO(SIGNED_MANIFEST) as f: + with pytest.raises(OpenPGPUntrustedSigFailure): + openpgp_env.verify_file(f) + except OpenPGPNoImplementation as e: + pytest.skip(str(e)) + + @pytest.mark.parametrize('manifest_var,key_var,expected', MANIFEST_VARIANTS) def test_manifest_load(openpgp_env, manifest_var, key_var, expected): |