diff options
author | Michał Górny <mgorny@gentoo.org> | 2017-10-25 12:30:25 +0200 |
---|---|---|
committer | Michał Górny <mgorny@gentoo.org> | 2017-10-25 12:42:17 +0200 |
commit | 6b9ccd25950b69b089e4b15e364c0c2b740dd9ad (patch) | |
tree | d2902339083edecfdc6c51cf14be9510835bd546 | |
parent | f1eae61437faf74ae1b970a2dc323ec33f210031 (diff) | |
download | gemato-6b9ccd25950b69b089e4b15e364c0c2b740dd9ad.tar.gz |
manifest: Support reading files containing OpenPGP signatures
-rw-r--r-- | gemato/exceptions.py | 11 | ||||
-rw-r--r-- | gemato/manifest.py | 45 | ||||
-rw-r--r-- | tests/test_openpgp.py | 131 |
3 files changed, 187 insertions, 0 deletions
diff --git a/gemato/exceptions.py b/gemato/exceptions.py index c9cb646..790ff53 100644 --- a/gemato/exceptions.py +++ b/gemato/exceptions.py @@ -50,3 +50,14 @@ class ManifestCrossDevice(Exception): super(ManifestCrossDevice, self).__init__( "Path {} crosses filesystem boundaries, it must be IGNORE-d explicitly" .format(path)) + + +class ManifestUnsignedData(Exception): + """ + An exception caused by a Manifest file containing non-whitespace + outside the OpenPGP-signed part. + """ + + def __init__(self): + super(ManifestUnsignedData, self).__init__( + "Unsigned data found in an OpenPGP signed Manifest") diff --git a/gemato/manifest.py b/gemato/manifest.py index 96b1110..0fae91f 100644 --- a/gemato/manifest.py +++ b/gemato/manifest.py @@ -273,6 +273,18 @@ MANIFEST_TAG_MAPPING = { } +class ManifestState(object): + """ + FSM constants for loading Manifest. + """ + + DATA = 0 + SIGNED_PREAMBLE = 1 + SIGNED_DATA = 2 + SIGNATURE = 3 + POST_SIGNED_DATA = 4 + + class ManifestFile(object): """ A class encapsulating a single Manifest file. It supports reading @@ -296,11 +308,44 @@ class ManifestFile(object): """ self.entries = [] + state = ManifestState.DATA + for l in f: + if state == ManifestState.DATA: + if l == '-----BEGIN PGP SIGNED MESSAGE-----\n': + if self.entries: + raise gemato.exceptions.ManifestUnsignedData() + state = ManifestState.SIGNED_PREAMBLE + continue + elif state == ManifestState.SIGNED_PREAMBLE: + # skip header lines up to the empty line + if l.strip(): + continue + state = ManifestState.SIGNED_DATA + elif state == ManifestState.SIGNED_DATA: + if l == '-----BEGIN PGP SIGNATURE-----\n': + state = ManifestState.SIGNATURE + continue + # dash-escaping, RFC 4880 says any line can suffer from it + if l.startswith('- '): + l = l[2:] + elif state == ManifestState.SIGNATURE: + if l == '-----END PGP SIGNATURE-----\n': + state = ManifestState.POST_SIGNED_DATA + continue + + if l.startswith('-----') and l.rstrip().endswith('-----'): + raise gemato.exceptions.ManifestSyntaxError( + "Unexpected OpenPGP header: {}".format(l)) + if state in (ManifestState.SIGNED_PREAMBLE, ManifestState.SIGNATURE): + continue + sl = l.strip().split() # skip empty lines if not sl: continue + if state == ManifestState.POST_SIGNED_DATA: + raise gemato.exceptions.ManifestUnsignedData() tag = sl[0] self.entries.append(MANIFEST_TAG_MAPPING[tag].from_list(sl)) diff --git a/tests/test_openpgp.py b/tests/test_openpgp.py new file mode 100644 index 0000000..b829078 --- /dev/null +++ b/tests/test_openpgp.py @@ -0,0 +1,131 @@ +# gemato: OpenPGP signature support tests +# vim:fileencoding=utf-8 +# (c) 2017 Michał Górny +# Licensed under the terms of 2-clause BSD license + +import io +import unittest + +import gemato.manifest + + +SIGNED_MANIFEST = u''' +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +TIMESTAMP 2017-10-22T18:06:41Z +MANIFEST eclass/Manifest 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +IGNORE local +DATA myebuild-0.ebuild 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +MISC metadata.xml 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +OPTIONAL ChangeLog +DIST mydistfile.tar.gz 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +-----BEGIN PGP SIGNATURE----- + +iQGTBAEBCgB9FiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAlnwXQpfFIAAAAAALgAo +aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDgx +RTEyQzE2QkQ4RENENjBCRTE4MDg0NTEzNjg4MEU3MkE3QjEzODQACgkQE2iA5yp7 +E4ScZAf+IF4suRtuN3bJki2zyYV/1VtSekK96tO+IzXxXDY0OKXmf61R6ZuuXcUD +Q+DlBONMILG+CDY+qiDp6snEWPmeLuh57qjkxilTgEX88W7OSCSdvGzSbC5WIRQG +KHtfZWtVVrZHTzQ6MF3u2Vombkpra/CQrf4Yx+8zdkorsoXwZ6ZjriB3W/zTUWIJ +XUy2tNfupdu72q9ske3dhVLhUEjtBzq5MlTf6gUjLBEsIHCGSafO2VG00lii3q4E +14EEilADJlKAOwK5WQUmAOjeeC60ck5EW5tGBotncd954v6n42pwlVXVmqSOJdYy +9F1V8N1m6n9UEUQ7Hhrv/+BTDPJO0A== +=9naF +-----END PGP SIGNATURE----- +''' + +DASH_ESCAPED_SIGNED_MANIFEST = u''' +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +- TIMESTAMP 2017-10-22T18:06:41Z +- MANIFEST eclass/Manifest 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +IGNORE local +- DATA myebuild-0.ebuild 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +MISC metadata.xml 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +- OPTIONAL ChangeLog +- DIST mydistfile.tar.gz 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +-----BEGIN PGP SIGNATURE----- + +iQGTBAEBCgB9FiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAlnwXQpfFIAAAAAALgAo +aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDgx +RTEyQzE2QkQ4RENENjBCRTE4MDg0NTEzNjg4MEU3MkE3QjEzODQACgkQE2iA5yp7 +E4ScZAf+IF4suRtuN3bJki2zyYV/1VtSekK96tO+IzXxXDY0OKXmf61R6ZuuXcUD +Q+DlBONMILG+CDY+qiDp6snEWPmeLuh57qjkxilTgEX88W7OSCSdvGzSbC5WIRQG +KHtfZWtVVrZHTzQ6MF3u2Vombkpra/CQrf4Yx+8zdkorsoXwZ6ZjriB3W/zTUWIJ +XUy2tNfupdu72q9ske3dhVLhUEjtBzq5MlTf6gUjLBEsIHCGSafO2VG00lii3q4E +14EEilADJlKAOwK5WQUmAOjeeC60ck5EW5tGBotncd954v6n42pwlVXVmqSOJdYy +9F1V8N1m6n9UEUQ7Hhrv/+BTDPJO0A== +=9naF +-----END PGP SIGNATURE----- +''' + +MODIFIED_SIGNED_MANIFEST = u''' +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +TIMESTAMP 2017-10-22T18:06:41Z +MANIFEST eclass/Manifest 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +IGNORE local +DATA myebuild-0.ebuild 32 +MISC metadata.xml 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +OPTIONAL ChangeLog +DIST mydistfile.tar.gz 0 MD5 d41d8cd98f00b204e9800998ecf8427e SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 +-----BEGIN PGP SIGNATURE----- + +iQGTBAEBCgB9FiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAlnwXQpfFIAAAAAALgAo +aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDgx +RTEyQzE2QkQ4RENENjBCRTE4MDg0NTEzNjg4MEU3MkE3QjEzODQACgkQE2iA5yp7 +E4ScZAf+IF4suRtuN3bJki2zyYV/1VtSekK96tO+IzXxXDY0OKXmf61R6ZuuXcUD +Q+DlBONMILG+CDY+qiDp6snEWPmeLuh57qjkxilTgEX88W7OSCSdvGzSbC5WIRQG +KHtfZWtVVrZHTzQ6MF3u2Vombkpra/CQrf4Yx+8zdkorsoXwZ6ZjriB3W/zTUWIJ +XUy2tNfupdu72q9ske3dhVLhUEjtBzq5MlTf6gUjLBEsIHCGSafO2VG00lii3q4E +14EEilADJlKAOwK5WQUmAOjeeC60ck5EW5tGBotncd954v6n42pwlVXVmqSOJdYy +9F1V8N1m6n9UEUQ7Hhrv/+BTDPJO0A== +=9naF +-----END PGP SIGNATURE----- +''' + + +class SignedManifestTest(unittest.TestCase): + """ + Test whether signed Manifest is read correctly. + """ + + def test_manifest_load(self): + m = gemato.manifest.ManifestFile() + with io.StringIO(SIGNED_MANIFEST) as f: + m.load(f) + self.assertIsNotNone(m.find_timestamp()) + self.assertIsNotNone(m.find_path_entry('myebuild-0.ebuild')) + + def test_dash_escaped_manifest_load(self): + m = gemato.manifest.ManifestFile() + with io.StringIO(DASH_ESCAPED_SIGNED_MANIFEST) as f: + m.load(f) + self.assertIsNotNone(m.find_timestamp()) + self.assertIsNotNone(m.find_path_entry('myebuild-0.ebuild')) + + def test_modified_manifest_load(self): + """ + Modified Manifest should load correctly since we do not enforce + implicit verification. + """ + m = gemato.manifest.ManifestFile() + with io.StringIO(MODIFIED_SIGNED_MANIFEST) as f: + m.load(f) + self.assertIsNotNone(m.find_timestamp()) + self.assertIsNotNone(m.find_path_entry('myebuild-0.ebuild')) + + def test_junk_before_manifest_load(self): + m = gemato.manifest.ManifestFile() + with io.StringIO('OPTIONAL test\n' + SIGNED_MANIFEST) as f: + self.assertRaises(gemato.exceptions.ManifestUnsignedData, + m.load, f) + + def test_junk_after_manifest_load(self): + m = gemato.manifest.ManifestFile() + with io.StringIO(SIGNED_MANIFEST + 'OPTIONAL test\n') as f: + self.assertRaises(gemato.exceptions.ManifestUnsignedData, + m.load, f) |