test_signature.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. # test_signature.py -- tests for signature.py
  2. # Copyright (C) 2025 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as published by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Tests for signature vendors."""
  22. import shutil
  23. import subprocess
  24. import unittest
  25. from dulwich.signature import GPGCliSignatureVendor, GPGSignatureVendor, SignatureVendor
  26. try:
  27. import gpg
  28. except ImportError:
  29. gpg = None
  30. class SignatureVendorTests(unittest.TestCase):
  31. """Tests for SignatureVendor base class."""
  32. def test_sign_not_implemented(self) -> None:
  33. """Test that sign raises NotImplementedError."""
  34. vendor = SignatureVendor()
  35. with self.assertRaises(NotImplementedError):
  36. vendor.sign(b"test data")
  37. def test_verify_not_implemented(self) -> None:
  38. """Test that verify raises NotImplementedError."""
  39. vendor = SignatureVendor()
  40. with self.assertRaises(NotImplementedError):
  41. vendor.verify(b"test data", b"fake signature")
  42. @unittest.skipIf(gpg is None, "gpg not available")
  43. class GPGSignatureVendorTests(unittest.TestCase):
  44. """Tests for GPGSignatureVendor."""
  45. def test_sign_and_verify(self) -> None:
  46. """Test basic sign and verify cycle.
  47. Note: This test requires a GPG key to be configured in the test
  48. environment. It may be skipped in environments without GPG setup.
  49. """
  50. vendor = GPGSignatureVendor()
  51. test_data = b"test data to sign"
  52. try:
  53. # Sign the data
  54. signature = vendor.sign(test_data)
  55. self.assertIsInstance(signature, bytes)
  56. self.assertGreater(len(signature), 0)
  57. # Verify the signature
  58. vendor.verify(test_data, signature)
  59. except gpg.errors.GPGMEError as e:
  60. # Skip test if no GPG key is available
  61. self.skipTest(f"GPG key not available: {e}")
  62. def test_verify_invalid_signature(self) -> None:
  63. """Test that verify raises an error for invalid signatures."""
  64. vendor = GPGSignatureVendor()
  65. test_data = b"test data"
  66. invalid_signature = b"this is not a valid signature"
  67. with self.assertRaises(gpg.errors.GPGMEError):
  68. vendor.verify(test_data, invalid_signature)
  69. def test_sign_with_keyid(self) -> None:
  70. """Test signing with a specific key ID.
  71. Note: This test requires a GPG key to be configured in the test
  72. environment. It may be skipped in environments without GPG setup.
  73. """
  74. vendor = GPGSignatureVendor()
  75. test_data = b"test data to sign"
  76. try:
  77. # Try to get a key from the keyring
  78. with gpg.Context() as ctx:
  79. keys = list(ctx.keylist(secret=True))
  80. if not keys:
  81. self.skipTest("No GPG keys available for testing")
  82. key = keys[0]
  83. signature = vendor.sign(test_data, keyid=key.fpr)
  84. self.assertIsInstance(signature, bytes)
  85. self.assertGreater(len(signature), 0)
  86. # Verify the signature
  87. vendor.verify(test_data, signature)
  88. except gpg.errors.GPGMEError as e:
  89. self.skipTest(f"GPG key not available: {e}")
  90. class GPGCliSignatureVendorTests(unittest.TestCase):
  91. """Tests for GPGCliSignatureVendor."""
  92. def setUp(self) -> None:
  93. """Check if gpg command is available."""
  94. if shutil.which("gpg") is None:
  95. self.skipTest("gpg command not available")
  96. def test_sign_and_verify(self) -> None:
  97. """Test basic sign and verify cycle using CLI."""
  98. vendor = GPGCliSignatureVendor()
  99. test_data = b"test data to sign"
  100. try:
  101. # Sign the data
  102. signature = vendor.sign(test_data)
  103. self.assertIsInstance(signature, bytes)
  104. self.assertGreater(len(signature), 0)
  105. self.assertTrue(signature.startswith(b"-----BEGIN PGP SIGNATURE-----"))
  106. # Verify the signature
  107. vendor.verify(test_data, signature)
  108. except subprocess.CalledProcessError as e:
  109. # Skip test if no GPG key is available or configured
  110. self.skipTest(f"GPG signing failed: {e}")
  111. def test_verify_invalid_signature(self) -> None:
  112. """Test that verify raises an error for invalid signatures."""
  113. vendor = GPGCliSignatureVendor()
  114. test_data = b"test data"
  115. invalid_signature = b"this is not a valid signature"
  116. with self.assertRaises(subprocess.CalledProcessError):
  117. vendor.verify(test_data, invalid_signature)
  118. def test_sign_with_keyid(self) -> None:
  119. """Test signing with a specific key ID using CLI."""
  120. vendor = GPGCliSignatureVendor()
  121. test_data = b"test data to sign"
  122. try:
  123. # Try to get a key from the keyring
  124. result = subprocess.run(
  125. ["gpg", "--list-secret-keys", "--with-colons"],
  126. capture_output=True,
  127. check=True,
  128. text=True,
  129. )
  130. # Parse output to find a key fingerprint
  131. keyid = None
  132. for line in result.stdout.split("\n"):
  133. if line.startswith("fpr:"):
  134. keyid = line.split(":")[9]
  135. break
  136. if not keyid:
  137. self.skipTest("No GPG keys available for testing")
  138. signature = vendor.sign(test_data, keyid=keyid)
  139. self.assertIsInstance(signature, bytes)
  140. self.assertGreater(len(signature), 0)
  141. # Verify the signature
  142. vendor.verify(test_data, signature)
  143. except subprocess.CalledProcessError as e:
  144. self.skipTest(f"GPG key not available: {e}")
  145. def test_verify_with_keyids(self) -> None:
  146. """Test verifying with specific trusted key IDs."""
  147. vendor = GPGCliSignatureVendor()
  148. test_data = b"test data to sign"
  149. try:
  150. # Sign without specifying a key (use default)
  151. signature = vendor.sign(test_data)
  152. # Get the primary key fingerprint from the keyring
  153. result = subprocess.run(
  154. ["gpg", "--list-secret-keys", "--with-colons"],
  155. capture_output=True,
  156. check=True,
  157. text=True,
  158. )
  159. primary_keyid = None
  160. for line in result.stdout.split("\n"):
  161. if line.startswith("fpr:"):
  162. primary_keyid = line.split(":")[9]
  163. break
  164. if not primary_keyid:
  165. self.skipTest("No GPG keys available for testing")
  166. # Verify with the correct primary keyid - should succeed
  167. # (GPG shows primary key fingerprint even if signed by subkey)
  168. vendor.verify(test_data, signature, keyids=[primary_keyid])
  169. # Verify with a different keyid - should fail
  170. fake_keyid = "0" * 40 # Fake 40-character fingerprint
  171. with self.assertRaises(ValueError):
  172. vendor.verify(test_data, signature, keyids=[fake_keyid])
  173. except subprocess.CalledProcessError as e:
  174. self.skipTest(f"GPG key not available: {e}")
  175. def test_custom_gpg_command(self) -> None:
  176. """Test using a custom GPG command path."""
  177. vendor = GPGCliSignatureVendor(gpg_command="gpg")
  178. test_data = b"test data"
  179. try:
  180. signature = vendor.sign(test_data)
  181. self.assertIsInstance(signature, bytes)
  182. except subprocess.CalledProcessError as e:
  183. self.skipTest(f"GPG not available: {e}")