From ea1fd599d808f0b3370269363824690be9b630dd Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Thu, 18 Apr 2024 16:14:15 +1000 Subject: [PATCH] temporary work with ServerKeyVerifier but not convince Signed-off-by: Olivier Lamy --- pom.xml | 20 +- .../sshd/JenkinsSshdSessionFactory.java | 259 ++++++++++++++++++ .../plugins/gitclient/JGitAPIImpl.java | 5 +- .../verifier/AbstractJGitHostKeyVerifier.java | 3 + .../AcceptFirstConnectionVerifier.java | 8 + .../verifier/KnownHostsFileVerifier.java | 8 + .../verifier/ManuallyProvidedKeyVerifier.java | 17 ++ .../gitclient/verifier/NoHostKeyVerifier.java | 7 + 8 files changed, 313 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/eclipse/jgit/transport/sshd/JenkinsSshdSessionFactory.java diff --git a/pom.xml b/pom.xml index f3d8cf516d..92421e8ac7 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ io.jenkins.tools.bom bom-2.414.x - 2907.vcb_35d6f2f7de + 2961.v1f472390972e pom import @@ -103,6 +103,12 @@ io.jenkins.plugins.mina-sshd-api mina-sshd-api-core + + + net.i2p.crypto + eddsa + 0.3.0 + org.eclipse.jgit org.eclipse.jgit @@ -202,12 +208,6 @@ org.jenkins-ci.plugins ssh-credentials - - - org.jenkins-ci.plugins - trilead-api - - org.jenkins-ci.plugins @@ -239,12 +239,6 @@ org.jenkins-ci.plugins git-server test - - - org.jenkins-ci.plugins - trilead-api - - diff --git a/src/main/java/org/eclipse/jgit/transport/sshd/JenkinsSshdSessionFactory.java b/src/main/java/org/eclipse/jgit/transport/sshd/JenkinsSshdSessionFactory.java new file mode 100644 index 0000000000..ee0542b651 --- /dev/null +++ b/src/main/java/org/eclipse/jgit/transport/sshd/JenkinsSshdSessionFactory.java @@ -0,0 +1,259 @@ +package org.eclipse.jgit.transport.sshd; + +import java.io.File; +import java.io.IOException; +import java.nio.file.InvalidPathException; +import java.security.KeyPair; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import org.apache.sshd.client.ClientBuilder; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.UserAuthFactory; +import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory; +import org.apache.sshd.client.auth.password.UserAuthPasswordFactory; +import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; +import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.compression.BuiltinCompressions; +import org.apache.sshd.common.keyprovider.KeyIdentityProvider; +import org.apache.sshd.common.signature.BuiltinSignatures; +import org.apache.sshd.common.signature.Signature; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory; +import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory; +import org.eclipse.jgit.internal.transport.sshd.JGitSshClient; +import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig; +import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction; +import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper; +import org.eclipse.jgit.internal.transport.sshd.SshdText; +import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; +import org.eclipse.jgit.util.FS; +import org.jenkinsci.plugins.gitclient.jgit.SmartCredentialsProvider; +import org.jenkinsci.plugins.gitclient.verifier.SshHostKeyVerificationStrategy; + +public class JenkinsSshdSessionFactory extends SshdSessionFactory { + + private final ServerKeyVerifier serverKeyVerifier; + private final SmartCredentialsProvider credentialsProvider; + private final Map defaultHostConfigEntryResolver = new ConcurrentHashMap<>(); + private final AtomicBoolean closing = new AtomicBoolean(); + + private final Set sessions = new HashSet<>(); + + public JenkinsSshdSessionFactory( + ServerKeyVerifier serverKeyVerifier, SmartCredentialsProvider credentialsProvider) { + super(); + this.serverKeyVerifier = serverKeyVerifier; + this.credentialsProvider = credentialsProvider; + } + + @Override + public SshdSession getSession(URIish uri, CredentialsProvider credentialsProvider, FS fs, int tms) + throws TransportException { + SshdSession session = null; + try { + session = new SshdSession(uri, () -> { + File home = getHomeDirectory(); + if (home == null) { + // Always use the detected filesystem for the user home! + // It makes no sense to have different "user home" + // directories depending on what file system a repository + // is. + home = FS.DETECTED.userHome(); + } + File sshDir = getSshDirectory(); + if (sshDir == null) { + sshDir = new File(home, SshConstants.SSH_DIR); + } + HostConfigEntryResolver configFile = getHostConfigEntryResolver(home, sshDir); + KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider(getDefaultKeys(sshDir)); + Supplier keyPasswordProvider = + () -> createKeyPasswordProvider(credentialsProvider); + ServerKeyVerifier serverKeyVerifier = new KnownHostsServerKeyVerifier( + JenkinsSshdSessionFactory.this.serverKeyVerifier, + SshHostKeyVerificationStrategy.JGIT_KNOWN_HOSTS_FILE.toPath()); + SshClient client = ClientBuilder.builder() + .factory(JGitSshClient::new) + .filePasswordProvider(new PasswordProviderWrapper(keyPasswordProvider)) + .hostConfigEntryResolver(configFile) + .serverKeyVerifier(serverKeyVerifier) + .signatureFactories(getSignatureFactories()) + .compressionFactories(new ArrayList<>(BuiltinCompressions.VALUES)) + .build(); + client.setUserInteraction(new JGitUserInteraction(credentialsProvider)); + client.setUserAuthFactories(getUserAuthFactories()); + client.setKeyIdentityProvider(defaultKeysProvider); + ConnectorFactory connectors = getConnectorFactory(); + if (connectors != null) { + client.setAgentFactory(new JGitSshAgentFactory(connectors, home)); + } + // JGit-specific things: + JGitSshClient jgitClient = (JGitSshClient) client; + jgitClient.setKeyCache(getKeyCache()); + jgitClient.setCredentialsProvider(credentialsProvider); + // FIXME Jenkins proxies? + jgitClient.setProxyDatabase(new DefaultProxyDataFactory()); + jgitClient.setKeyPasswordProviderFactory(keyPasswordProvider); + String defaultAuths = getDefaultPreferredAuthentications(); + if (defaultAuths != null) { + jgitClient.setAttribute(JGitSshClient.PREFERRED_AUTHENTICATIONS, defaultAuths); + } + if (home != null) { + try { + jgitClient.setAttribute( + JGitSshClient.HOME_DIRECTORY, + home.getAbsoluteFile().toPath()); + } catch (SecurityException | InvalidPathException e) { + // Ignore + } + } + // Other things? + return client; + }); + session.addCloseListener(s -> unregister(s)); + register(session); + session.connect(Duration.ofMillis(tms)); + return session; + } catch (Exception e) { + unregister(session); + if (e instanceof TransportException) { + throw (TransportException) e; + } + throw new TransportException(uri, e.getMessage(), e); + } + } + + @NonNull + private HostConfigEntryResolver getHostConfigEntryResolver(@NonNull File homeDir, @NonNull File sshDir) { + return defaultHostConfigEntryResolver.computeIfAbsent( + new Tuple(new Object[] {homeDir, sshDir}), + t -> new JGitSshConfig(createSshConfigStore(homeDir, getSshConfig(sshDir), getLocalUserName()))); + } + + /** A simple general map key. */ + private static final class Tuple { + private Object[] objects; + + public Tuple(Object[] objects) { + this.objects = objects; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj != null && obj.getClass() == JenkinsSshdSessionFactory.Tuple.class) { + JenkinsSshdSessionFactory.Tuple other = (JenkinsSshdSessionFactory.Tuple) obj; + return Arrays.equals(objects, other.objects); + } + return false; + } + + @Override + public int hashCode() { + return Arrays.hashCode(objects); + } + } + + private KeyIdentityProvider toKeyIdentityProvider(Iterable keys) { + if (keys instanceof KeyIdentityProvider) { + return (KeyIdentityProvider) keys; + } + return (session) -> keys; + } + + private void register(SshdSession newSession) throws IOException { + if (newSession == null) { + return; + } + if (closing.get()) { + throw new IOException(SshdText.get().sshClosingDown); + } + synchronized (this) { + sessions.add(newSession); + } + } + + private void unregister(SshdSession oldSession) { + boolean cleanKeys = false; + synchronized (this) { + sessions.remove(oldSession); + cleanKeys = closing.get() && sessions.isEmpty(); + } + if (cleanKeys) { + KeyCache cache = getKeyCache(); + if (cache != null) { + cache.close(); + } + } + } + + /** + * Apache MINA sshd 2.6.0 has removed DSA, DSA_CERT and RSA_CERT. We have to + * set it up explicitly to still allow users to connect with DSA keys. + * + * @return a list of supported signature factories + */ + @SuppressWarnings("deprecation") + private static List> getSignatureFactories() { + // @formatter:off + // FIXME Check FIPS? + return Arrays.asList( + BuiltinSignatures.nistp256_cert, + BuiltinSignatures.nistp384_cert, + BuiltinSignatures.nistp521_cert, + BuiltinSignatures.ed25519_cert, + BuiltinSignatures.rsaSHA512_cert, + BuiltinSignatures.rsaSHA256_cert, + BuiltinSignatures.rsa_cert, + BuiltinSignatures.nistp256, + BuiltinSignatures.nistp384, + BuiltinSignatures.nistp521, + BuiltinSignatures.ed25519, + BuiltinSignatures.sk_ecdsa_sha2_nistp256, + BuiltinSignatures.sk_ssh_ed25519, + BuiltinSignatures.rsaSHA512, + BuiltinSignatures.rsaSHA256, + BuiltinSignatures.rsa, + BuiltinSignatures.dsa_cert, + BuiltinSignatures.dsa); + // @formatter:on + } + + /** + * Gets the user authentication mechanisms (or rather, factories for them). + * By default this returns gssapi-with-mic, public-key, password, and + * keyboard-interactive, in that order. The order is only significant if the + * ssh config does not set {@code PreferredAuthentications}; if it + * is set, the order defined there will be taken. + * + * @return the non-empty list of factories. + */ + @NonNull + private List getUserAuthFactories() { + // About the order of password and keyboard-interactive, see upstream + // bug https://issues.apache.org/jira/projects/SSHD/issues/SSHD-866 . + // Password auth doesn't have this problem. + return Collections.unmodifiableList(Arrays.asList( + GssApiWithMicAuthFactory.INSTANCE, + JGitPublicKeyAuthFactory.FACTORY, + UserAuthPasswordFactory.INSTANCE, + UserAuthKeyboardInteractiveFactory.INSTANCE)); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 5aaedfa12b..6c8851a8fe 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -179,8 +179,11 @@ public class JGitAPIImpl extends LegacyCompatibleGitAPIImpl { // to avoid rogue plugins from clobbering what we use, always // make a point of overwriting it with ours. + // SshdSessionFactoryBuilder builder = new SshdSessionFactoryBuilder(); SshSessionFactory.setInstance( buildSshdSessionFactory(hostKeyFactory.forJGit(listener), asSmartCredentialsProvider())); + // SshSessionFactory.setInstance(new JenkinsSshdSessionFactory( + // hostKeyFactory.forJGit(listener).getServerKeyVerifier(), asSmartCredentialsProvider())); if (httpConnectionFactory != null) { httpConnectionFactory.setCredentialsProvider(asSmartCredentialsProvider()); // allow override of HttpConnectionFactory to avoid JENKINS-37934 @@ -190,7 +193,6 @@ public class JGitAPIImpl extends LegacyCompatibleGitAPIImpl { private SshdSessionFactory buildSshdSessionFactory( AbstractJGitHostKeyVerifier abstractJGitHostKeyVerifier, SmartCredentialsProvider credentialsProvider) { - if (Files.notExists(SshHostKeyVerificationStrategy.JGIT_KNOWN_HOSTS_FILE.toPath())) { try { Files.createDirectories(SshHostKeyVerificationStrategy.JGIT_KNOWN_HOSTS_FILE @@ -232,6 +234,7 @@ public SshdSession getSession(URIish uri, CredentialsProvider credentialsProvide sshdSession.addCloseListener(sshdSession1 -> { if (tmpKey != null) { try { + listener.getLogger().println("deleting"); Files.deleteIfExists(tmpKey); } catch (IOException e) { if (LOGGER.isLoggable(Level.FINE)) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/AbstractJGitHostKeyVerifier.java b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/AbstractJGitHostKeyVerifier.java index 4dcf0bb825..4b42a96ccd 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/AbstractJGitHostKeyVerifier.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/AbstractJGitHostKeyVerifier.java @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.gitclient.verifier; import hudson.model.TaskListener; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; import org.jenkinsci.remoting.SerializableOnlyOverRemoting; @@ -17,4 +18,6 @@ public TaskListener getTaskListener() { } public abstract OpenSshConfigFile.HostEntry customizeHostEntry(OpenSshConfigFile.HostEntry hostEntry); + + public abstract ServerKeyVerifier getServerKeyVerifier(); } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionVerifier.java b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionVerifier.java index 6888b26f47..1084493530 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionVerifier.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionVerifier.java @@ -5,6 +5,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.logging.Logger; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; import org.eclipse.jgit.transport.SshConstants; @@ -50,5 +53,10 @@ public OpenSshConfigFile.HostEntry customizeHostEntry(OpenSshConfigFile.HostEntr return hostEntry; } + + @Override + public ServerKeyVerifier getServerKeyVerifier() { + return new DefaultKnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, false); + } } } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/KnownHostsFileVerifier.java b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/KnownHostsFileVerifier.java index 1738ff1d4f..519e5ade64 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/KnownHostsFileVerifier.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/KnownHostsFileVerifier.java @@ -8,6 +8,9 @@ import java.nio.file.Path; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; import org.eclipse.jgit.transport.SshConstants; @@ -56,6 +59,11 @@ public OpenSshConfigFile.HostEntry customizeHostEntry(OpenSshConfigFile.HostEntr hostEntry.setValue(SshConstants.STRICT_HOST_KEY_CHECKING, SshConstants.YES); return hostEntry; } + + @Override + public ServerKeyVerifier getServerKeyVerifier() { + return new DefaultKnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, true); + } } private void logHint(TaskListener listener) { diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerifier.java b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerifier.java index 54e4444807..fe1e06fd64 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerifier.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerifier.java @@ -8,6 +8,9 @@ import java.nio.file.Path; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; import org.eclipse.jgit.transport.SshConstants; @@ -89,5 +92,19 @@ public OpenSshConfigFile.HostEntry customizeHostEntry(OpenSshConfigFile.HostEntr throw new RuntimeException(e); } } + + @Override + public ServerKeyVerifier getServerKeyVerifier() { + try { + Path tempKnownHosts = Files.createTempFile("known_hosts", ""); + Files.write( + tempKnownHosts, (approvedHostKeys + System.lineSeparator()).getBytes(StandardCharsets.UTF_8)); + return new DefaultKnownHostsServerKeyVerifier( + AcceptAllServerKeyVerifier.INSTANCE, true, tempKnownHosts); + } catch (IOException e) { + getTaskListener().error("cannot write temporary know_hosts file: " + e.getMessage()); + throw new RuntimeException(e); + } + } } } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerifier.java b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerifier.java index 445e010b73..b29929232b 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerifier.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerifier.java @@ -2,6 +2,8 @@ import hudson.model.TaskListener; import java.util.logging.Logger; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; import org.eclipse.jgit.transport.SshConstants; @@ -22,6 +24,11 @@ public OpenSshConfigFile.HostEntry customizeHostEntry(OpenSshConfigFile.HostEntr hostEntry.setValue(SshConstants.STRICT_HOST_KEY_CHECKING, SshConstants.NO); return hostEntry; } + + @Override + public ServerKeyVerifier getServerKeyVerifier() { + return AcceptAllServerKeyVerifier.INSTANCE; + } }; } }