浏览代码

Merge branch 'tag-verify' of git://github.com/pmrowla/dulwich

Jelmer Vernooij 3 年之前
父节点
当前提交
d69ccebfd9
共有 4 个文件被更改,包括 155 次插入18 次删除
  1. 39 0
      dulwich/objects.py
  2. 1 0
      dulwich/tests/compat/__init__.py
  3. 97 0
      dulwich/tests/compat/test_porcelain.py
  4. 18 18
      dulwich/tests/test_porcelain.py

+ 39 - 0
dulwich/objects.py

@@ -30,6 +30,7 @@ import stat
 from typing import (
     Optional,
     Dict,
+    Iterable,
     Union,
     Type,
 )
@@ -881,6 +882,44 @@ class Tag(ShaFile):
                     self.as_raw_string(), mode=gpg.constants.sig.mode.DETACH
                 )
 
+    def verify(self, keyids: Optional[Iterable[str]] = None):
+        """Verify GPG signature for this tag (if it is signed).
+
+        Args:
+          keyids: Optional iterable of trusted keyids for this tag.
+            If this tag is not signed by any key in keyids verification will
+            fail. If not specified, this function only verifies that the tag
+            has a valid signature.
+
+        Raises:
+          gpg.errors.BadSignatures: if GPG signature verification fails
+          gpg.errors.MissingSignatures: if tag was not signed by a key
+            specified in keyids
+        """
+        if self._signature is None:
+            return
+
+        import gpg
+
+        with gpg.Context() as ctx:
+            data, result = ctx.verify(
+                self.as_raw_string()[: -len(self._signature)],
+                signature=self._signature,
+            )
+            if keyids:
+                keys = [
+                    ctx.get_key(key)
+                    for key in keyids
+                ]
+                for key in keys:
+                    for subkey in keys:
+                        for sig in result.signatures:
+                            if subkey.can_sign and subkey.fpr == sig.fpr:
+                                return
+                raise gpg.errors.MissingSignatures(
+                    result, keys, results=(data, result)
+                )
+
 
 class TreeEntry(namedtuple("TreeEntry", ["path", "mode", "sha"])):
     """Named tuple encapsulating a single tree entry."""

+ 1 - 0
dulwich/tests/compat/__init__.py

@@ -28,6 +28,7 @@ def test_suite():
         "client",
         "pack",
         "patch",
+        "porcelain",
         "repository",
         "server",
         "utils",

+ 97 - 0
dulwich/tests/compat/test_porcelain.py

@@ -0,0 +1,97 @@
+# test_porcelain .py -- Tests for dulwich.porcelain/CGit compatibility
+# Copyright (C) 2010 Google, Inc.
+#
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Compatibility tests for dulwich.porcelain."""
+
+import os
+import platform
+import sys
+from unittest import skipIf
+
+from dulwich import porcelain
+from dulwich.tests.utils import (
+    build_commit_graph,
+)
+from dulwich.tests.compat.utils import (
+    run_git_or_fail,
+    CompatTestCase,
+)
+from dulwich.tests.test_porcelain import (
+    PorcelainGpgTestCase,
+)
+
+
+@skipIf(platform.python_implementation() == "PyPy" or sys.platform == "win32", "gpgme not easily available or supported on Windows and PyPy")
+class TagCreateSignTestCase(PorcelainGpgTestCase, CompatTestCase):
+    def setUp(self):
+        super(TagCreateSignTestCase, self).setUp()
+
+    def test_sign(self):
+        # Test that dulwich signatures can be verified by CGit
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        self.import_default_key()
+
+        porcelain.tag_create(
+            self.repo.path,
+            b"tryme",
+            b"foo <foo@bar.com>",
+            b"bar",
+            annotated=True,
+            sign=True,
+        )
+
+        run_git_or_fail(
+            [
+                "--git-dir={}".format(self.repo.controldir()),
+                "tag",
+                "-v",
+                "tryme"
+            ],
+            env=os.environ.copy(),
+        )
+
+    def test_verify(self):
+        # Test that CGit signatures can be verified by dulwich
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+        self.import_default_key()
+
+        run_git_or_fail(
+            [
+                "--git-dir={}".format(self.repo.controldir()),
+                "tag",
+                "-u",
+                PorcelainGpgTestCase.DEFAULT_KEY_ID,
+                "-m",
+                "foo",
+                "verifyme",
+            ],
+            env=os.environ.copy(),
+        )
+        tag = self.repo[b"refs/tags/verifyme"]
+        self.assertNotEqual(tag.signature, None)
+        tag.verify()

+ 18 - 18
dulwich/tests/test_porcelain.py

@@ -1105,6 +1105,8 @@ class RevListTests(PorcelainTestCase):
 @skipIf(platform.python_implementation() == "PyPy" or sys.platform == "win32", "gpgme not easily available or supported on Windows and PyPy")
 class TagCreateSignTests(PorcelainGpgTestCase):
     def test_default_key(self):
+        import gpg
+
         c1, c2, c3 = build_commit_graph(
             self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
         )
@@ -1129,16 +1131,22 @@ class TagCreateSignTests(PorcelainGpgTestCase):
         self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
         self.assertEqual(b"bar\n", tag.message)
         self.assertLess(time.time() - tag.tag_time, 5)
-        # GPG Signatures aren't deterministic, so we can't do a static assertion.
-        # Instead we need to check the signature can be verified by git
         tag = self.repo[b'refs/tags/tryme']
-        # TODO(jelmer): Remove calls to C git, and call tag.verify() instead -
-        # perhaps moving git call to compat testsuite?
-        subprocess.run(
-            ["git", "--git-dir={}".format(self.repo.controldir()), "tag", "-v", "tryme"],
-            check=True,
-            stderr=subprocess.DEVNULL,
-            stdout=subprocess.DEVNULL,
+        # GPG Signatures aren't deterministic, so we can't do a static assertion.
+        tag.verify()
+        tag.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
+
+        self.import_non_default_key()
+        self.assertRaises(
+            gpg.errors.MissingSignatures,
+            tag.verify,
+            keyids=[PorcelainGpgTestCase.NON_DEFAULT_KEY_ID],
+        )
+
+        tag._chunked_text = [b"bad data", tag._signature]
+        self.assertRaises(
+            gpg.errors.BadSignatures,
+            tag.verify,
         )
 
     def test_non_default_key(self):
@@ -1168,15 +1176,7 @@ class TagCreateSignTests(PorcelainGpgTestCase):
         self.assertLess(time.time() - tag.tag_time, 5)
         tag = self.repo[b'refs/tags/tryme']
         # GPG Signatures aren't deterministic, so we can't do a static assertion.
-        # Instead we need to check the signature can be verified by git
-        # TODO(jelmer): Remove calls to C git, and call tag.verify() instead -
-        # perhaps moving git call to compat testsuite?
-        subprocess.run(
-            ["git", "--git-dir={}".format(self.repo.controldir()), "tag", "-v", "tryme"],
-            check=True,
-            stderr=subprocess.DEVNULL,
-            stdout=subprocess.DEVNULL,
-        )
+        tag.verify()
 
 
 class TagCreateTests(PorcelainTestCase):