Ver Fonte

Add SSH revocation file and default key command support

Jelmer Vernooij há 3 semanas atrás
pai
commit
87872a4b00
2 ficheiros alterados com 100 adições e 3 exclusões
  1. 52 3
      dulwich/signature.py
  2. 48 0
      tests/test_signature.py

+ 52 - 3
dulwich/signature.py

@@ -476,6 +476,7 @@ class SSHSigSignatureVendor(SignatureVendor):
 
     Supports git config options:
     - gpg.ssh.allowedSignersFile: File containing allowed SSH public keys
+    - gpg.ssh.revocationFile: File containing revoked SSH public keys
     - gpg.ssh.defaultKeyCommand: Command to get default SSH key (currently unused)
     """
 
@@ -489,6 +490,7 @@ class SSHSigSignatureVendor(SignatureVendor):
 
         # Parse SSH-specific config
         self.allowed_signers_file = None
+        self.revocation_file = None
         self.default_key_command = None
 
         if config is not None:
@@ -499,6 +501,13 @@ class SSHSigSignatureVendor(SignatureVendor):
             except KeyError:
                 pass
 
+            try:
+                revoc_file = config.get((b"gpg", b"ssh"), b"revocationFile")
+                if revoc_file:
+                    self.revocation_file = revoc_file.decode("utf-8")
+            except KeyError:
+                pass
+
             try:
                 key_command = config.get((b"gpg", b"ssh"), b"defaultKeyCommand")
                 if key_command:
@@ -613,7 +622,9 @@ class SSHCliSignatureVendor(SignatureVendor):
 
     Supports git config options:
     - gpg.ssh.allowedSignersFile: File containing allowed SSH public keys
+    - gpg.ssh.revocationFile: File containing revoked SSH public keys
     - gpg.ssh.program: Path to ssh-keygen command
+    - gpg.ssh.defaultKeyCommand: Command to get default SSH key for signing
     """
 
     def __init__(
@@ -641,6 +652,9 @@ class SSHCliSignatureVendor(SignatureVendor):
 
         # Parse SSH-specific config
         self.allowed_signers_file = None
+        self.revocation_file = None
+        self.default_key_command = None
+
         if config is not None:
             try:
                 signers_file = config.get((b"gpg", b"ssh"), b"allowedSignersFile")
@@ -649,13 +663,27 @@ class SSHCliSignatureVendor(SignatureVendor):
             except KeyError:
                 pass
 
+            try:
+                revoc_file = config.get((b"gpg", b"ssh"), b"revocationFile")
+                if revoc_file:
+                    self.revocation_file = revoc_file.decode("utf-8")
+            except KeyError:
+                pass
+
+            try:
+                key_command = config.get((b"gpg", b"ssh"), b"defaultKeyCommand")
+                if key_command:
+                    self.default_key_command = key_command.decode("utf-8")
+            except KeyError:
+                pass
+
     def sign(self, data: bytes, keyid: str | None = None) -> bytes:
         """Sign data with an SSH key using ssh-keygen.
 
         Args:
           data: The data to sign
-          keyid: Path to SSH private key. If not specified, ssh-keygen will
-                use the default key (typically from ssh-agent)
+          keyid: Path to SSH private key. If not specified, will try to get
+                default key from gpg.ssh.defaultKeyCommand
 
         Returns:
           The signature as bytes
@@ -668,8 +696,25 @@ class SSHCliSignatureVendor(SignatureVendor):
         import subprocess
         import tempfile
 
+        # If no keyid specified, try to get default key from command
         if keyid is None:
-            raise ValueError("SSH signing requires a key to be specified")
+            if self.default_key_command:
+                # Run the default key command to get the key
+                result = subprocess.run(
+                    self.default_key_command,
+                    shell=True,
+                    capture_output=True,
+                    check=True,
+                    text=True,
+                )
+                keyid = result.stdout.strip()
+                if not keyid:
+                    raise ValueError("gpg.ssh.defaultKeyCommand returned empty key")
+            else:
+                raise ValueError(
+                    "SSH signing requires a key to be specified via keyid parameter "
+                    "or gpg.ssh.defaultKeyCommand configuration"
+                )
 
         # Create a temporary directory to hold both data and signature files
         # ssh-keygen creates the signature file with .sig suffix
@@ -754,6 +799,10 @@ class SSHCliSignatureVendor(SignatureVendor):
                 sig_filename,
             ]
 
+            # Add revocation file if configured
+            if self.revocation_file:
+                args.extend(["-r", self.revocation_file])
+
             subprocess.run(
                 args,
                 stdin=open(data_filename, "rb"),

+ 48 - 0
tests/test_signature.py

@@ -592,6 +592,54 @@ class SSHCliSignatureVendorTests(unittest.TestCase):
             # Verify the signature
             vendor.verify(test_data, signature)
 
+    def test_default_key_command(self) -> None:
+        """Test gpg.ssh.defaultKeyCommand support."""
+        import os
+        import tempfile
+
+        # Generate a test SSH key
+        with tempfile.TemporaryDirectory() as tmpdir:
+            private_key = os.path.join(tmpdir, "test_key")
+
+            # Generate Ed25519 key (no passphrase)
+            subprocess.run(
+                [
+                    "ssh-keygen",
+                    "-t",
+                    "ed25519",
+                    "-f",
+                    private_key,
+                    "-N",
+                    "",
+                    "-C",
+                    "test@example.com",
+                ],
+                capture_output=True,
+                check=True,
+            )
+
+            # Create config with defaultKeyCommand that echoes the key path
+            config = ConfigDict()
+            config.set(
+                (b"gpg", b"ssh"), b"defaultKeyCommand", f"echo {private_key}".encode()
+            )
+
+            vendor = SSHCliSignatureVendor(config=config)
+            test_data = b"test data"
+
+            # Sign without providing keyid - should use defaultKeyCommand
+            signature = vendor.sign(test_data)
+            self.assertIsInstance(signature, bytes)
+            self.assertGreater(len(signature), 0)
+
+    def test_revocation_file_config(self) -> None:
+        """Test that revocation file is read from config."""
+        config = ConfigDict()
+        config.set((b"gpg", b"ssh"), b"revocationFile", b"/path/to/revoked_keys")
+
+        vendor = SSHCliSignatureVendor(config=config)
+        self.assertEqual(vendor.revocation_file, "/path/to/revoked_keys")
+
 
 class DetectSignatureFormatTests(unittest.TestCase):
     """Tests for detect_signature_format function."""