Repository: buildbot/buildbot

Issue:
Gitpoller not working well with secret providers that strip newlines from secrets
Hello and happy new year!

In my `master.cfg` file, i have a `SecretInAFile` provider...

```python
c["secretsProviders"] = [
    buildbot.plugins.secrets.SecretInAFile(dirname="/workspace/secrets/")    # note that strip==True by default
]
```

... and a `GitPoller` which uses a secret as ssh private key:

```python
c["change_source"] = [
    buildbot.plugins.changes.GitPoller(
        ...
        ssh_privkey=buildbot.plugins.util.Secret("id_ed25519"),
    )
]
```

However, the `GitPoller` fails to authenticate: 

```
File "/usr/local/lib/python3.11/dist-packages/buildbot/changes/gitpoller.py", line 478, in _dovccmdImpl
    raise GitError(f'command {full_args} in {path} on repourl {self.repourl} failed '
buildbot.changes.gitpoller.GitError: command ['-c', 'core.sshCommand=ssh -o "BatchMode=yes" -i "/workspace/buildbot/./gitpoller/default/********/.buildbot-ssh9ffxzc86/ssh-key" -o "UserKnownHostsFile=/workspace/buildbot/./gitpoller/default/********/.buildbot-ssh9ffxzc86/ssh-known-hosts"', 'ls-remote', '--refs', 'git@github.com:********/********.git'] in None on repourl git@github.com:********/********.git failed with exit code 128: Load key "/workspace/buildbot/./gitpoller/default/********/.buildbot-ssh9ffxzc86/ssh-key": error in libcrypto
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
```

The private key itself is working (i have verified that it is) but it seems that when buildbot is copying the key, **it strips a trailing newline and then OpenSSH cannot read it anymore.** 

It seems that the `Git` BuildStep has already solved the same problem with [a function called ensureSshKeyNewline](https://github.com/buildbot/buildbot/blob/03f3395caed18b3d772585c0006803f79fa3eca8/master/buildbot/util/git.py#L128), and i think that `GitPoller` should do the same thing... 

In my `master.cfg` file, I could work around this issue by passing `strip=False` to the `SecretInAFile` provider. 

In case you need it, here are the buildbot/python/openssh/git versions i am using:

```
Buildbot version: 3.10.1
Twisted version: 23.10.0

Python 3.11.7
OpenSSH_8.9p1 Ubuntu-3ubuntu0.5, OpenSSL 3.0.2 15 Mar 2022
git version 2.34.1
```


Discussion:

Resolving patch:
diff --git a/master/buildbot/changes/gitpoller.py b/master/buildbot/changes/gitpoller.py
--- a/master/buildbot/changes/gitpoller.py
+++ b/master/buildbot/changes/gitpoller.py
@@ -27,6 +27,7 @@
 from buildbot.util import private_tempdir
 from buildbot.util import runprocess
 from buildbot.util.git import GitMixin
+from buildbot.util.git import ensureSshKeyNewline
 from buildbot.util.git import getSshKnownHostsContents
 from buildbot.util.misc import writeLocalFile
 from buildbot.util.state import StateMixin
@@ -423,7 +424,7 @@ def _downloadSshPrivateKey(self, keyPath):
         # We change the permissions of the key file to be user-readable only so
         # that ssh does not complain. This is not used for security because the
         # parent directory will have proper permissions.
-        writeLocalFile(keyPath, self.sshPrivateKey, mode=stat.S_IRUSR)
+        writeLocalFile(keyPath, ensureSshKeyNewline(self.sshPrivateKey), mode=stat.S_IRUSR)
 
     def _downloadSshKnownHosts(self, path):
         if self.sshKnownHosts is not None:


Test patch:
diff --git a/master/buildbot/test/unit/changes/test_gitpoller.py b/master/buildbot/test/unit/changes/test_gitpoller.py
--- a/master/buildbot/test/unit/changes/test_gitpoller.py
+++ b/master/buildbot/test/unit/changes/test_gitpoller.py
@@ -1567,7 +1567,7 @@ def test_poll_initial_2_10(self, write_local_file_mock, temp_dir_mock):
         self.assertEqual(temp_dir_mock.dirs,
                          [(temp_dir_path, 0o700),
                           (temp_dir_path, 0o700)])
-        write_local_file_mock.assert_called_with(key_path, 'ssh-key',
+        write_local_file_mock.assert_called_with(key_path, 'ssh-key\n',
                                                  mode=0o400)
 
     @mock.patch('buildbot.util.private_tempdir.PrivateTemporaryDirectory',
@@ -1611,7 +1611,7 @@ def test_poll_initial_2_3(self, write_local_file_mock, temp_dir_mock):
         self.assertEqual(temp_dir_mock.dirs,
                          [(temp_dir_path, 0o700),
                           (temp_dir_path, 0o700)])
-        write_local_file_mock.assert_called_with(key_path, 'ssh-key',
+        write_local_file_mock.assert_called_with(key_path, 'ssh-key\n',
                                                  mode=0o400)
 
     @mock.patch('buildbot.util.private_tempdir.PrivateTemporaryDirectory',
@@ -1647,7 +1647,7 @@ def test_poll_failFetch_git_2_10(self, write_local_file_mock,
         self.assertEqual(temp_dir_mock.dirs,
                          [(temp_dir_path, 0o700),
                           (temp_dir_path, 0o700)])
-        write_local_file_mock.assert_called_with(key_path, 'ssh-key',
+        write_local_file_mock.assert_called_with(key_path, 'ssh-key\n',
                                                  mode=0o400)
 
 
@@ -1706,9 +1706,9 @@ def test_poll_initial_2_10(self, write_local_file_mock, temp_dir_mock):
                           (temp_dir_path, 0o700)])
 
         expected_file_writes = [
-            mock.call(key_path, 'ssh-key', mode=0o400),
+            mock.call(key_path, 'ssh-key\n', mode=0o400),
             mock.call(known_hosts_path, '* ssh-host-key'),
-            mock.call(key_path, 'ssh-key', mode=0o400),
+            mock.call(key_path, 'ssh-key\n', mode=0o400),
             mock.call(known_hosts_path, '* ssh-host-key'),
         ]
 
@@ -1717,9 +1717,8 @@ def test_poll_initial_2_10(self, write_local_file_mock, temp_dir_mock):
 
 
 class TestGitPollerWithSshKnownHosts(TestGitPollerBase):
-
     def createPoller(self):
-        return gitpoller.GitPoller(self.REPOURL, sshPrivateKey='ssh-key',
+        return gitpoller.GitPoller(self.REPOURL, sshPrivateKey='ssh-key\n',
                                    sshKnownHosts='ssh-known-hosts')
 
     @mock.patch('buildbot.util.private_tempdir.PrivateTemporaryDirectory',
@@ -1771,9 +1770,9 @@ def test_poll_initial_2_10(self, write_local_file_mock, temp_dir_mock):
                           (temp_dir_path, 0o700)])
 
         expected_file_writes = [
-            mock.call(key_path, 'ssh-key', mode=0o400),
+            mock.call(key_path, 'ssh-key\n', mode=0o400),
             mock.call(known_hosts_path, 'ssh-known-hosts'),
-            mock.call(key_path, 'ssh-key', mode=0o400),
+            mock.call(key_path, 'ssh-key\n', mode=0o400),
             mock.call(known_hosts_path, 'ssh-known-hosts'),
         ]