test_signature.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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.config import ConfigDict
  26. from dulwich.signature import (
  27. GPGCliSignatureVendor,
  28. GPGSignatureVendor,
  29. SignatureVendor,
  30. get_signature_vendor,
  31. )
  32. try:
  33. import gpg
  34. except ImportError:
  35. gpg = None
  36. class SignatureVendorTests(unittest.TestCase):
  37. """Tests for SignatureVendor base class."""
  38. def test_sign_not_implemented(self) -> None:
  39. """Test that sign raises NotImplementedError."""
  40. vendor = SignatureVendor()
  41. with self.assertRaises(NotImplementedError):
  42. vendor.sign(b"test data")
  43. def test_verify_not_implemented(self) -> None:
  44. """Test that verify raises NotImplementedError."""
  45. vendor = SignatureVendor()
  46. with self.assertRaises(NotImplementedError):
  47. vendor.verify(b"test data", b"fake signature")
  48. @unittest.skipIf(gpg is None, "gpg not available")
  49. class GPGSignatureVendorTests(unittest.TestCase):
  50. """Tests for GPGSignatureVendor."""
  51. def test_min_trust_level_from_config(self) -> None:
  52. """Test reading gpg.minTrustLevel from config."""
  53. config = ConfigDict()
  54. config.set((b"gpg",), b"minTrustLevel", b"marginal")
  55. vendor = GPGSignatureVendor(config=config)
  56. self.assertEqual(vendor.min_trust_level, "marginal")
  57. def test_min_trust_level_default(self) -> None:
  58. """Test default when gpg.minTrustLevel not in config."""
  59. vendor = GPGSignatureVendor()
  60. self.assertIsNone(vendor.min_trust_level)
  61. def test_sign_and_verify(self) -> None:
  62. """Test basic sign and verify cycle.
  63. Note: This test requires a GPG key to be configured in the test
  64. environment. It may be skipped in environments without GPG setup.
  65. """
  66. vendor = GPGSignatureVendor()
  67. test_data = b"test data to sign"
  68. try:
  69. # Sign the data
  70. signature = vendor.sign(test_data)
  71. self.assertIsInstance(signature, bytes)
  72. self.assertGreater(len(signature), 0)
  73. # Verify the signature
  74. vendor.verify(test_data, signature)
  75. except gpg.errors.GPGMEError as e:
  76. # Skip test if no GPG key is available
  77. self.skipTest(f"GPG key not available: {e}")
  78. def test_verify_invalid_signature(self) -> None:
  79. """Test that verify raises an error for invalid signatures."""
  80. vendor = GPGSignatureVendor()
  81. test_data = b"test data"
  82. invalid_signature = b"this is not a valid signature"
  83. with self.assertRaises(gpg.errors.GPGMEError):
  84. vendor.verify(test_data, invalid_signature)
  85. def test_sign_with_keyid(self) -> None:
  86. """Test signing with a specific key ID.
  87. Note: This test requires a GPG key to be configured in the test
  88. environment. It may be skipped in environments without GPG setup.
  89. """
  90. vendor = GPGSignatureVendor()
  91. test_data = b"test data to sign"
  92. try:
  93. # Try to get a key from the keyring
  94. with gpg.Context() as ctx:
  95. keys = list(ctx.keylist(secret=True))
  96. if not keys:
  97. self.skipTest("No GPG keys available for testing")
  98. key = keys[0]
  99. signature = vendor.sign(test_data, keyid=key.fpr)
  100. self.assertIsInstance(signature, bytes)
  101. self.assertGreater(len(signature), 0)
  102. # Verify the signature
  103. vendor.verify(test_data, signature)
  104. except gpg.errors.GPGMEError as e:
  105. self.skipTest(f"GPG key not available: {e}")
  106. class GPGCliSignatureVendorTests(unittest.TestCase):
  107. """Tests for GPGCliSignatureVendor."""
  108. def setUp(self) -> None:
  109. """Check if gpg command is available."""
  110. if shutil.which("gpg") is None:
  111. self.skipTest("gpg command not available")
  112. def test_sign_and_verify(self) -> None:
  113. """Test basic sign and verify cycle using CLI."""
  114. vendor = GPGCliSignatureVendor()
  115. test_data = b"test data to sign"
  116. try:
  117. # Sign the data
  118. signature = vendor.sign(test_data)
  119. self.assertIsInstance(signature, bytes)
  120. self.assertGreater(len(signature), 0)
  121. self.assertTrue(signature.startswith(b"-----BEGIN PGP SIGNATURE-----"))
  122. # Verify the signature
  123. vendor.verify(test_data, signature)
  124. except subprocess.CalledProcessError as e:
  125. # Skip test if no GPG key is available or configured
  126. self.skipTest(f"GPG signing failed: {e}")
  127. def test_verify_invalid_signature(self) -> None:
  128. """Test that verify raises an error for invalid signatures."""
  129. vendor = GPGCliSignatureVendor()
  130. test_data = b"test data"
  131. invalid_signature = b"this is not a valid signature"
  132. with self.assertRaises(subprocess.CalledProcessError):
  133. vendor.verify(test_data, invalid_signature)
  134. def test_sign_with_keyid(self) -> None:
  135. """Test signing with a specific key ID using CLI."""
  136. vendor = GPGCliSignatureVendor()
  137. test_data = b"test data to sign"
  138. try:
  139. # Try to get a key from the keyring
  140. result = subprocess.run(
  141. ["gpg", "--list-secret-keys", "--with-colons"],
  142. capture_output=True,
  143. check=True,
  144. text=True,
  145. )
  146. # Parse output to find a key fingerprint
  147. keyid = None
  148. for line in result.stdout.split("\n"):
  149. if line.startswith("fpr:"):
  150. keyid = line.split(":")[9]
  151. break
  152. if not keyid:
  153. self.skipTest("No GPG keys available for testing")
  154. signature = vendor.sign(test_data, keyid=keyid)
  155. self.assertIsInstance(signature, bytes)
  156. self.assertGreater(len(signature), 0)
  157. # Verify the signature
  158. vendor.verify(test_data, signature)
  159. except subprocess.CalledProcessError as e:
  160. self.skipTest(f"GPG key not available: {e}")
  161. def test_verify_with_keyids(self) -> None:
  162. """Test verifying with specific trusted key IDs."""
  163. vendor = GPGCliSignatureVendor()
  164. test_data = b"test data to sign"
  165. try:
  166. # Sign without specifying a key (use default)
  167. signature = vendor.sign(test_data)
  168. # Get the primary key fingerprint from the keyring
  169. result = subprocess.run(
  170. ["gpg", "--list-secret-keys", "--with-colons"],
  171. capture_output=True,
  172. check=True,
  173. text=True,
  174. )
  175. primary_keyid = None
  176. for line in result.stdout.split("\n"):
  177. if line.startswith("fpr:"):
  178. primary_keyid = line.split(":")[9]
  179. break
  180. if not primary_keyid:
  181. self.skipTest("No GPG keys available for testing")
  182. # Verify with the correct primary keyid - should succeed
  183. # (GPG shows primary key fingerprint even if signed by subkey)
  184. vendor.verify(test_data, signature, keyids=[primary_keyid])
  185. # Verify with a different keyid - should fail
  186. fake_keyid = "0" * 40 # Fake 40-character fingerprint
  187. with self.assertRaises(ValueError):
  188. vendor.verify(test_data, signature, keyids=[fake_keyid])
  189. except subprocess.CalledProcessError as e:
  190. self.skipTest(f"GPG key not available: {e}")
  191. def test_custom_gpg_command(self) -> None:
  192. """Test using a custom GPG command path."""
  193. vendor = GPGCliSignatureVendor(gpg_command="gpg")
  194. test_data = b"test data"
  195. try:
  196. signature = vendor.sign(test_data)
  197. self.assertIsInstance(signature, bytes)
  198. except subprocess.CalledProcessError as e:
  199. self.skipTest(f"GPG not available: {e}")
  200. def test_gpg_program_from_config(self) -> None:
  201. """Test reading gpg.program from config."""
  202. # Create a config with gpg.program set
  203. config = ConfigDict()
  204. config.set((b"gpg",), b"program", b"gpg2")
  205. vendor = GPGCliSignatureVendor(config=config)
  206. self.assertEqual(vendor.gpg_command, "gpg2")
  207. def test_gpg_program_override(self) -> None:
  208. """Test that gpg_command parameter overrides config."""
  209. config = ConfigDict()
  210. config.set((b"gpg",), b"program", b"gpg2")
  211. vendor = GPGCliSignatureVendor(config=config, gpg_command="gpg")
  212. self.assertEqual(vendor.gpg_command, "gpg")
  213. def test_gpg_program_default(self) -> None:
  214. """Test default gpg command when no config provided."""
  215. vendor = GPGCliSignatureVendor()
  216. self.assertEqual(vendor.gpg_command, "gpg")
  217. def test_gpg_program_default_when_not_in_config(self) -> None:
  218. """Test default gpg command when config doesn't have gpg.program."""
  219. config = ConfigDict()
  220. vendor = GPGCliSignatureVendor(config=config)
  221. self.assertEqual(vendor.gpg_command, "gpg")
  222. class GetSignatureVendorTests(unittest.TestCase):
  223. """Tests for get_signature_vendor function."""
  224. def test_default_format(self) -> None:
  225. """Test that default format is openpgp."""
  226. vendor = get_signature_vendor()
  227. self.assertIsInstance(vendor, (GPGSignatureVendor, GPGCliSignatureVendor))
  228. def test_explicit_openpgp_format(self) -> None:
  229. """Test explicitly requesting openpgp format."""
  230. vendor = get_signature_vendor(format="openpgp")
  231. self.assertIsInstance(vendor, (GPGSignatureVendor, GPGCliSignatureVendor))
  232. def test_format_from_config(self) -> None:
  233. """Test reading format from config."""
  234. config = ConfigDict()
  235. config.set((b"gpg",), b"format", b"openpgp")
  236. vendor = get_signature_vendor(config=config)
  237. self.assertIsInstance(vendor, (GPGSignatureVendor, GPGCliSignatureVendor))
  238. def test_format_case_insensitive(self) -> None:
  239. """Test that format is case-insensitive."""
  240. vendor = get_signature_vendor(format="OpenPGP")
  241. self.assertIsInstance(vendor, (GPGSignatureVendor, GPGCliSignatureVendor))
  242. def test_x509_not_supported(self) -> None:
  243. """Test that x509 format raises ValueError."""
  244. with self.assertRaises(ValueError) as cm:
  245. get_signature_vendor(format="x509")
  246. self.assertIn("X.509", str(cm.exception))
  247. def test_ssh_not_supported(self) -> None:
  248. """Test that ssh format raises ValueError."""
  249. with self.assertRaises(ValueError) as cm:
  250. get_signature_vendor(format="ssh")
  251. self.assertIn("SSH", str(cm.exception))
  252. def test_invalid_format(self) -> None:
  253. """Test that invalid format raises ValueError."""
  254. with self.assertRaises(ValueError) as cm:
  255. get_signature_vendor(format="invalid")
  256. self.assertIn("Unsupported", str(cm.exception))
  257. def test_config_passed_to_vendor(self) -> None:
  258. """Test that config is passed to the vendor."""
  259. config = ConfigDict()
  260. config.set((b"gpg",), b"program", b"gpg2")
  261. vendor = get_signature_vendor(format="openpgp", config=config)
  262. # If CLI vendor is used, check that config was passed
  263. if isinstance(vendor, GPGCliSignatureVendor):
  264. self.assertEqual(vendor.gpg_command, "gpg2")