test_signature.py 10.0 KB

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