diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 1bbf605c..e96656ad 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -428,7 +428,7 @@ The following table describes the SSH configuration properties. |Valid SSH host key. Must be set if `hostKeyAlgorithm` is also set. |*hostKeyAlgorithm* -|One of `ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521`. Must be set if `hostKey` is also set. +|One of `ssh-dss, ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521`. Must be set if `hostKey` is also set. |*strictHostKeyChecking* |`true` or `false`. If false, ignore errors with host key. diff --git a/spring-cloud-config-dependencies/pom.xml b/spring-cloud-config-dependencies/pom.xml index 1dce9296..e43a830c 100644 --- a/spring-cloud-config-dependencies/pom.xml +++ b/spring-cloud-config-dependencies/pom.xml @@ -6,16 +6,16 @@ spring-cloud-dependencies-parent org.springframework.cloud - 4.0.0-SNAPSHOT + 3.1.4-SNAPSHOT spring-cloud-config-dependencies - 4.0.0-SNAPSHOT + 3.1.4-SNAPSHOT pom spring-cloud-config-dependencies Spring Cloud Config Dependencies - 5.12.0.202106070339-r + 5.13.1.202206130422-r 3.0.0-M1 2.1.1.RELEASE diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml index 548e7531..0d18879d 100644 --- a/spring-cloud-config-server/pom.xml +++ b/spring-cloud-config-server/pom.xml @@ -82,7 +82,7 @@ org.eclipse.jgit - org.eclipse.jgit.ssh.jsch + org.eclipse.jgit.ssh.apache org.yaml diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentProperties.java index aa2505ee..7861839f 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentProperties.java @@ -97,7 +97,7 @@ public class JGitEnvironmentProperties extends AbstractScmAccessorProperties private String privateKey; /** - * One of ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or + * One of ssh-dss, ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or * ecdsa-sha2-nistp521. Must be set if hostKey is also set. */ private String hostKeyAlgorithm; diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java index e7b1bd9d..64de2249 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java @@ -25,7 +25,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import com.jcraft.jsch.Session; import io.micrometer.observation.ObservationRegistry; import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CloneCommand; @@ -152,11 +151,8 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository private boolean tryMasterBranch; - private final ObservationRegistry observationRegistry; - - public JGitEnvironmentRepository(ConfigurableEnvironment environment, JGitEnvironmentProperties properties, - ObservationRegistry observationRegistry) { - super(environment, properties, observationRegistry); + public JGitEnvironmentRepository(ConfigurableEnvironment environment, JGitEnvironmentProperties properties) { + super(environment, properties); this.cloneOnStart = properties.isCloneOnStart(); this.defaultLabel = properties.getDefaultLabel(); this.forcePull = properties.isForcePull(); @@ -166,7 +162,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository this.skipSslValidation = properties.isSkipSslValidation(); this.gitFactory = new JGitFactory(properties.isCloneSubmodules()); this.tryMasterBranch = properties.isTryMasterBranch(); - this.observationRegistry = observationRegistry; } public boolean isTryMasterBranch() { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/FileBasedSshSessionFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/FileBasedSshSessionFactory.java new file mode 100644 index 00000000..94d81a90 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/FileBasedSshSessionFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.ssh; + +import java.io.File; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; +import org.eclipse.jgit.transport.SshConfigStore; +import org.eclipse.jgit.transport.sshd.SshdSessionFactory; + +import org.springframework.cloud.config.server.environment.JGitEnvironmentProperties; + +public class FileBasedSshSessionFactory extends SshdSessionFactory { + + private static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking"; + + private static final String YES_OPTION = "yes"; + + private static final String NO_OPTION = "no"; + + private final JGitEnvironmentProperties sshUriProperties; + + public FileBasedSshSessionFactory(JGitEnvironmentProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + @Override + protected SshConfigStore createSshConfigStore(File homeDir, File configFile, String localUserName) { + return configFile == null ? null : new OpenSshConfigFile(homeDir, configFile, localUserName) { + + @Override + public HostEntry lookup(@NonNull String hostName, int port, String userName) { + HostEntry hostEntry = super.lookup(hostName, port, userName); + + hostEntry.setValue(STRICT_HOST_KEY_CHECKING, + sshUriProperties.isStrictHostKeyChecking() ? YES_OPTION : NO_OPTION); + + return hostEntry; + } + }; + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/FileBasedSshTransportConfigCallback.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/FileBasedSshTransportConfigCallback.java index 49c42500..fa408c64 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/FileBasedSshTransportConfigCallback.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/FileBasedSshTransportConfigCallback.java @@ -16,11 +16,8 @@ package org.springframework.cloud.config.server.ssh; -import com.jcraft.jsch.Session; import org.eclipse.jgit.api.TransportConfigCallback; -import org.eclipse.jgit.transport.JschConfigSessionFactory; -import org.eclipse.jgit.transport.OpenSshConfig; -import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.SshTransport; import org.eclipse.jgit.transport.Transport; import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentProperties; @@ -33,7 +30,7 @@ import org.springframework.cloud.config.server.environment.MultipleJGitEnvironme */ public class FileBasedSshTransportConfigCallback implements TransportConfigCallback { - private MultipleJGitEnvironmentProperties sshUriProperties; + private final MultipleJGitEnvironmentProperties sshUriProperties; public FileBasedSshTransportConfigCallback(MultipleJGitEnvironmentProperties sshUriProperties) { this.sshUriProperties = sshUriProperties; @@ -45,14 +42,9 @@ public class FileBasedSshTransportConfigCallback implements TransportConfigCallb @Override public void configure(Transport transport) { - SshSessionFactory.setInstance(new JschConfigSessionFactory() { - @Override - protected void configure(OpenSshConfig.Host hc, Session session) { - session.setConfig("StrictHostKeyChecking", - FileBasedSshTransportConfigCallback.this.sshUriProperties.isStrictHostKeyChecking() ? "yes" - : "no"); - } - }); + if (transport instanceof SshTransport) { + ((SshTransport) transport).setSshSessionFactory(new FileBasedSshSessionFactory(sshUriProperties)); + } } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java index 64144d3b..32780dc5 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java @@ -48,8 +48,8 @@ public class HostKeyAlgoSupportedValidator private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git."; - private static final Set VALID_HOST_KEY_ALGORITHMS = new LinkedHashSet<>( - Arrays.asList("ssh-dss", "ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521")); + private static final Set VALID_HOST_KEY_ALGORITHMS = new LinkedHashSet<>(Arrays.asList("ssh-dss", "ssh-rsa", + "ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521")); private final SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KeyPairUtils.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KeyPairUtils.java new file mode 100644 index 00000000..2f890e62 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KeyPairUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.ssh; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Collection; + +import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.io.resource.AbstractIoResource; +import org.apache.sshd.common.util.security.SecurityUtils; + +final class KeyPairUtils { + + private static final KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser(); + + private KeyPairUtils() { + + } + + static Collection load(SessionContext session, String privateKey) + throws IOException, GeneralSecurityException { + return loader.loadKeyPairs(session, new StringResource(privateKey), null); + } + + static boolean isValid(String privateKey) { + try { + return !KeyPairUtils.load(null, privateKey).isEmpty(); + } + catch (IOException | GeneralSecurityException ignored) { + return false; + } + } + + private static class StringResource extends AbstractIoResource { + + protected StringResource(String resourceValue) { + super(String.class, resourceValue); + } + + @Override + public InputStream openInputStream() { + return new ByteArrayInputStream(this.getResourceValue().getBytes()); + } + + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyValidator.java index d361f9f0..d775b0ce 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyValidator.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyValidator.java @@ -20,9 +20,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.KeyPair; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; @@ -37,7 +34,7 @@ import static org.springframework.util.StringUtils.hasText; /** * JSR-303 Cross Field validator that ensures that an * {@link MultipleJGitEnvironmentProperties} bean for the constraints: - Private key is - * present and can be correctly parsed using {@link com.jcraft.jsch.KeyPair} + * present and can be correctly parsed using {@link java.security.KeyPair} * * Beans annotated with {@link PrivateKeyValidator} and {@link Validated} will have the * constraints applied. @@ -86,16 +83,14 @@ public class PrivateKeyValidator implements ConstraintValidator sshKeysByHostname; - private final JSch jSch; + public PropertyBasedSshSessionFactory(Map sshKeysByHostname) { + super(new JGitKeyCache(), new HttpProxyDataFactory(sshKeysByHostname)); - public PropertyBasedSshSessionFactory(Map sshKeysByHostname, JSch jSch) { this.sshKeysByHostname = sshKeysByHostname; - this.jSch = jSch; + assert this.sshKeysByHostname.entrySet().size() > 0; } @Override - protected void configure(Host hc, Session session) { - JGitEnvironmentProperties sshProperties = this.sshKeysByHostname.get(hc.getHostName()); - String hostKeyAlgorithm = sshProperties.getHostKeyAlgorithm(); - if (hostKeyAlgorithm != null) { - session.setConfig(SERVER_HOST_KEY, hostKeyAlgorithm); - } - if (sshProperties.getHostKey() == null || !sshProperties.isStrictHostKeyChecking()) { - session.setConfig(STRICT_HOST_KEY_CHECKING, NO_OPTION); - } - else { - session.setConfig(STRICT_HOST_KEY_CHECKING, YES_OPTION); - } - String preferredAuthentications = sshProperties.getPreferredAuthentications(); - if (preferredAuthentications != null) { - session.setConfig(PREFERRED_AUTHENTICATIONS, preferredAuthentications); - } + protected SshConfigStore createSshConfigStore(File homeDir, File configFile, String localUserName) { + return new SshConfigStore() { - ProxyHostProperties proxyHostProperties = sshProperties.getProxy().get(ProxyHostProperties.ProxyForScheme.HTTP); - if (proxyHostProperties != null && proxyHostProperties.connectionInformationProvided()) { - ProxyHTTP proxy = createProxy(proxyHostProperties); - proxy.setUserPasswd(proxyHostProperties.getUsername(), proxyHostProperties.getPassword()); - session.setProxy(proxy); - } - } + @Override + public HostConfig lookup(@NonNull String hostName, int port, String userName) { + OpenSshConfigFile.HostEntry hostEntry = new OpenSshConfigFile.HostEntry(); - protected ProxyHTTP createProxy(ProxyHostProperties proxyHostProperties) { - return new ProxyHTTP(proxyHostProperties.getHost(), proxyHostProperties.getPort()); + return updateIfNeeded(hostEntry, hostName); + } + + private OpenSshConfigFile.HostEntry updateIfNeeded(OpenSshConfigFile.HostEntry hostEntry, String hostName) { + JGitEnvironmentProperties sshProperties = sshKeysByHostname.get(hostName); + if (sshProperties == null) { + return hostEntry; + } + + if (sshProperties.getHostKey() == null || !sshProperties.isStrictHostKeyChecking()) { + hostEntry.setValue(STRICT_HOST_KEY_CHECKING, NO_OPTION); + } + else { + hostEntry.setValue(STRICT_HOST_KEY_CHECKING, YES_OPTION); + } + + String preferredAuthentications = sshProperties.getPreferredAuthentications(); + if (preferredAuthentications != null) { + hostEntry.setValue(PREFERRED_AUTHENTICATIONS, preferredAuthentications); + } + return hostEntry; + } + }; } @Override - protected Session createSession(Host hc, String user, String host, int port, FS fs) throws JSchException { - if (this.sshKeysByHostname.containsKey(host)) { - JGitEnvironmentProperties sshUriProperties = this.sshKeysByHostname.get(host); - this.jSch.addIdentity(host, sshUriProperties.getPrivateKey().getBytes(), null, null); - if (sshUriProperties.getKnownHostsFile() != null) { - this.jSch.setKnownHosts(sshUriProperties.getKnownHostsFile()); + protected File getSshConfig(File dir) { + // Do not use a config file. + return null; + } + + @Override + protected ServerKeyDatabase getServerKeyDatabase(File homeDir, File dir) { + return new ServerKeyDatabase() { + @Override + public List lookup(String connectAddress, InetSocketAddress remoteAddress, + Configuration config) { + + JGitEnvironmentProperties sshProperties = sshKeysByHostname.get(remoteAddress.getHostName()); + if (sshProperties == null) { + return Collections.emptyList(); + } + + List knownHostFiles = getKnownHostFiles(sshProperties); + List publicKeys = new OpenSshServerKeyDatabase(false, knownHostFiles).lookup(connectAddress, + remoteAddress, config); + + PublicKey publicKey = getHostKey(sshProperties); + if (publicKey != null) { + publicKeys.add(publicKey); + } + + return publicKeys; } - if (sshUriProperties.getHostKey() != null) { - HostKey hostkey = new HostKey(host, Base64.decode(sshUriProperties.getHostKey())); - this.jSch.getHostKeyRepository().add(hostkey, null); + + @Override + public boolean accept(String connectAddress, InetSocketAddress remoteAddress, PublicKey serverKey, + Configuration config, CredentialsProvider provider) { + if (isNotStrictHostKeyChecking(remoteAddress.getHostName())) { + return true; + } + + List knownServerKeys = lookup(connectAddress, remoteAddress, config); + + return KeyUtils.findMatchingKey(serverKey, knownServerKeys) != null; } - return this.jSch.getSession(user, host, port); + + private boolean isNotStrictHostKeyChecking(String hostName) { + JGitEnvironmentProperties sshProperties = sshKeysByHostname.get(hostName); + if (sshProperties == null) { + return false; + } + return !sshProperties.isStrictHostKeyChecking(); + } + + private PublicKey getHostKey(JGitEnvironmentProperties sshProperties) { + String hostKey = sshProperties.getHostKey(); + String hostKeyAlgorithm = sshProperties.getHostKeyAlgorithm(); + if (!StringUtils.hasText(hostKey) || !StringUtils.hasText(hostKeyAlgorithm)) { + return null; + } + + try { + return AuthorizedKeyEntry.parseAuthorizedKeyEntry(hostKeyAlgorithm + " " + hostKey) + .resolvePublicKey(null, null); + } + catch (IOException | GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + private List getKnownHostFiles(JGitEnvironmentProperties sshProperties) { + if (sshProperties.getKnownHostsFile() == null) { + return Collections.emptyList(); + } + else { + return Collections.singletonList(Paths.get(sshProperties.getKnownHostsFile())); + } + } + + }; + } + + @Override + protected Iterable getDefaultKeys(File dir) { + return new SingleKeyIdentityProvider(sshKeysByHostname); + } + + private final static class SingleKeyIdentityProvider implements KeyIdentityProvider, Iterable { + + private final Map sshKeysByHostname; + + private SingleKeyIdentityProvider(Map sshKeysByHostname) { + this.sshKeysByHostname = sshKeysByHostname; } - throw new JSchException("no keys configured for hostname " + host); + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public Iterable loadKeys(SessionContext session) throws IOException, GeneralSecurityException { + SshdSocketAddress remoteAddress = SshdSocketAddress.toSshdSocketAddress(session.getRemoteAddress()); + JGitEnvironmentProperties sshProperties = sshKeysByHostname.get(remoteAddress.getHostName()); + + return sshProperties == null ? Collections.emptyList() + : KeyPairUtils.load(session, sshProperties.getPrivateKey()); + } + + } + + private final static class HttpProxyDataFactory implements ProxyDataFactory { + + private final Map sshKeysByHostname; + + private HttpProxyDataFactory(Map sshKeysByHostname) { + this.sshKeysByHostname = sshKeysByHostname; + } + + @Override + public ProxyData get(InetSocketAddress remoteAddress) { + JGitEnvironmentProperties sshProperties = sshKeysByHostname.get(remoteAddress.getHostName()); + ProxyHostProperties proxyHostProperties = sshProperties.getProxy() + .get(ProxyHostProperties.ProxyForScheme.HTTP); + + if (proxyHostProperties == null || !proxyHostProperties.connectionInformationProvided()) { + return null; + } + + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(proxyHostProperties.getHost(), proxyHostProperties.getPort())); + return new ProxyData(proxy, proxyHostProperties.getUsername(), + proxyHostProperties.getPassword().toCharArray()); + } + } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java index 13bf1711..40fcf444 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java @@ -18,31 +18,32 @@ package org.springframework.cloud.config.server; import java.io.File; import java.lang.reflect.Method; +import java.net.URISyntaxException; -import com.jcraft.jsch.Session; import org.eclipse.jgit.api.TransportConfigCallback; -import org.eclipse.jgit.transport.JschConfigSessionFactory; -import org.eclipse.jgit.transport.OpenSshConfig; -import org.eclipse.jgit.transport.SshSessionFactory; -import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.transport.FetchConnection; +import org.eclipse.jgit.transport.PushConnection; +import org.eclipse.jgit.transport.SshConfigStore; +import org.eclipse.jgit.transport.SshTransport; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.sshd.SshdSessionFactory; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentProperties; import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository; +import org.springframework.cloud.config.server.ssh.FileBasedSshSessionFactory; import org.springframework.cloud.config.server.ssh.FileBasedSshTransportConfigCallback; import org.springframework.cloud.config.server.ssh.PropertiesBasedSshTransportConfigCallback; +import org.springframework.cloud.config.server.ssh.PropertyBasedSshSessionFactory; import org.springframework.cloud.config.server.ssh.SshPropertyValidator; import org.springframework.cloud.config.server.test.TestConfigServerApplication; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Integration tests for property based SSH config support. @@ -65,12 +66,22 @@ public class TransportConfigurationIntegrationTests { private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; @Test - public void propertyBasedTransportCallbackIsConfigured() throws Exception { + public void propertyBasedTransportCallbackIsConfigured() { TransportConfigCallback transportConfigCallback = this.jGitEnvironmentRepository .getTransportConfigCallback(); assertThat(transportConfigCallback).isInstanceOf(PropertiesBasedSshTransportConfigCallback.class); } + @Test + public void propertyBasedSessionFactoryIsUsedForSshTransports() throws Exception { + this.jGitEnvironmentRepository.afterPropertiesSet(); + + SshTransport sshTransport = DummySshTransport.newInstance(); + this.jGitEnvironmentRepository.getTransportConfigCallback().configure(sshTransport); + + assertThat(sshTransport.getSshSessionFactory()).isInstanceOf(PropertyBasedSshSessionFactory.class); + } + } @RunWith(SpringRunner.class) @@ -85,12 +96,22 @@ public class TransportConfigurationIntegrationTests { private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; @Test - public void propertyBasedTransportCallbackIsConfigured() throws Exception { + public void propertyBasedTransportCallbackIsConfigured() { TransportConfigCallback transportConfigCallback = this.jGitEnvironmentRepository .getTransportConfigCallback(); assertThat(transportConfigCallback).isInstanceOf(PropertiesBasedSshTransportConfigCallback.class); } + @Test + public void propertyBasedSessionFactoryIsUsedForSshTransports() throws Exception { + this.jGitEnvironmentRepository.afterPropertiesSet(); + + SshTransport sshTransport = DummySshTransport.newInstance(); + this.jGitEnvironmentRepository.getTransportConfigCallback().configure(sshTransport); + + assertThat(sshTransport.getSshSessionFactory()).isInstanceOf(PropertyBasedSshSessionFactory.class); + } + } } @@ -109,7 +130,7 @@ public class TransportConfigurationIntegrationTests { private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; @Test - public void privateKeyPropertyWithLineBreaks() throws Exception { + public void privateKeyPropertyWithLineBreaks() { TransportConfigCallback callback = this.jGitEnvironmentRepository.getTransportConfigCallback(); assertThat(callback).isInstanceOf(PropertiesBasedSshTransportConfigCallback.class); @@ -132,7 +153,7 @@ public class TransportConfigurationIntegrationTests { private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; @Test - public void privateKeyPropertyWithLineBreaks() throws Exception { + public void privateKeyPropertyWithLineBreaks() { TransportConfigCallback callback = this.jGitEnvironmentRepository.getTransportConfigCallback(); assertThat(callback).isInstanceOf(PropertiesBasedSshTransportConfigCallback.class); @@ -159,7 +180,7 @@ public class TransportConfigurationIntegrationTests { private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; @Test - public void sshPropertiesWithinNestedRepo() throws Exception { + public void sshPropertiesWithinNestedRepo() { TransportConfigCallback callback = this.jGitEnvironmentRepository.getTransportConfigCallback(); assertThat(callback).isInstanceOf(PropertiesBasedSshTransportConfigCallback.class); @@ -187,7 +208,7 @@ public class TransportConfigurationIntegrationTests { private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; @Test - public void sshPropertiesWithinNestedRepo() throws Exception { + public void sshPropertiesWithinNestedRepo() { TransportConfigCallback callback = this.jGitEnvironmentRepository.getTransportConfigCallback(); assertThat(callback).isInstanceOf(PropertiesBasedSshTransportConfigCallback.class); @@ -219,42 +240,44 @@ public class TransportConfigurationIntegrationTests { private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; @Test - public void fileBasedTransportCallbackIsConfigured() throws Exception { + public void fileBasedTransportCallbackIsConfigured() { TransportConfigCallback transportConfigCallback = this.jGitEnvironmentRepository .getTransportConfigCallback(); assertThat(transportConfigCallback).isInstanceOf(FileBasedSshTransportConfigCallback.class); } + @Test + public void fileBasedSessionFactoryIsUsedForSshTransports() throws Exception { + this.jGitEnvironmentRepository.afterPropertiesSet(); + + SshTransport sshTransport = DummySshTransport.newInstance(); + this.jGitEnvironmentRepository.getTransportConfigCallback().configure(sshTransport); + + assertThat(sshTransport.getSshSessionFactory()).isInstanceOf(FileBasedSshSessionFactory.class); + } + @Test public void strictHostKeyCheckShouldCheck() throws Exception { - String uri = "git+ssh://git@somegitserver/somegitrepo"; - SshSessionFactory.setInstance(null); - this.jGitEnvironmentRepository.setUri(uri); - this.jGitEnvironmentRepository.setBasedir(new File("./mybasedir")); assertThat(this.jGitEnvironmentRepository.isStrictHostKeyChecking()).isTrue(); - this.jGitEnvironmentRepository.setCloneOnStart(true); - try { - // this will throw but we don't care about connecting. - this.jGitEnvironmentRepository.afterPropertiesSet(); - } - catch (Exception e) { - final OpenSshConfig.Host hc = OpenSshConfig.get(FS.detect()).lookup("github.com"); - JschConfigSessionFactory factory = (JschConfigSessionFactory) SshSessionFactory.getInstance(); - // There's no public method that can be used to inspect the ssh - // configuration, so we'll reflect - // the configure method to allow us to check that the config - // property is set as expected. - Method configure = factory.getClass().getDeclaredMethod("configure", OpenSshConfig.Host.class, - Session.class); - configure.setAccessible(true); - Session session = mock(Session.class); - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); - configure.invoke(factory, hc, session); - verify(session).setConfig(keyCaptor.capture(), valueCaptor.capture()); - configure.setAccessible(false); - assertThat("yes".equals(valueCaptor.getValue())).isTrue(); - } + + this.jGitEnvironmentRepository.afterPropertiesSet(); + SshTransport sshTransport = DummySshTransport.newInstance(); + this.jGitEnvironmentRepository.getTransportConfigCallback().configure(sshTransport); + + SshdSessionFactory factory = (SshdSessionFactory) sshTransport.getSshSessionFactory(); + // There's no public method that can be used to inspect the ssh + // configuration, so we'll reflect + // the createSshConfigStore method to allow us to check that the config + // property is set as expected. + Method createSshConfigStore = factory.getClass().getDeclaredMethod("createSshConfigStore", File.class, + File.class, String.class); + createSshConfigStore.setAccessible(true); + SshConfigStore configStore = (SshConfigStore) createSshConfigStore.invoke(factory, new File("."), + new File("."), "local-username"); + createSshConfigStore.setAccessible(false); + assertThat("yes" + .equals(configStore.lookup("gitserver.com", 22, "username").getValue("StrictHostKeyChecking"))) + .isTrue(); } } @@ -272,44 +295,73 @@ public class TransportConfigurationIntegrationTests { private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; @Test - public void fileBasedTransportCallbackIsConfigured() throws Exception { + public void fileBasedTransportCallbackIsConfigured() { TransportConfigCallback transportConfigCallback = this.jGitEnvironmentRepository .getTransportConfigCallback(); assertThat(transportConfigCallback).isInstanceOf(FileBasedSshTransportConfigCallback.class); } @Test - public void strictHostKeyCheckShouldCheck() throws Exception { - String uri = "git+ssh://git@somegitserver/somegitrepo"; - SshSessionFactory.setInstance(null); - this.jGitEnvironmentRepository.setUri(uri); - this.jGitEnvironmentRepository.setBasedir(new File("./mybasedir")); - assertThat(this.jGitEnvironmentRepository.isStrictHostKeyChecking()).isTrue(); - this.jGitEnvironmentRepository.setCloneOnStart(true); - try { - // this will throw but we don't care about connecting. - this.jGitEnvironmentRepository.afterPropertiesSet(); - } - catch (Exception e) { - final OpenSshConfig.Host hc = OpenSshConfig.get(FS.detect()).lookup("github.com"); - JschConfigSessionFactory factory = (JschConfigSessionFactory) SshSessionFactory.getInstance(); - // There's no public method that can be used to inspect the ssh - // configuration, so we'll reflect - // the configure method to allow us to check that the config - // property is set as expected. - Method configure = factory.getClass().getDeclaredMethod("configure", OpenSshConfig.Host.class, - Session.class); - configure.setAccessible(true); - Session session = mock(Session.class); - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); - configure.invoke(factory, hc, session); - verify(session).setConfig(keyCaptor.capture(), valueCaptor.capture()); - configure.setAccessible(false); - assertThat("yes".equals(valueCaptor.getValue())).isTrue(); - } + public void fileBasedSessionFactoryIsUsedForSshTransports() throws Exception { + this.jGitEnvironmentRepository.afterPropertiesSet(); + + SshTransport sshTransport = DummySshTransport.newInstance(); + this.jGitEnvironmentRepository.getTransportConfigCallback().configure(sshTransport); + + assertThat(sshTransport.getSshSessionFactory()).isInstanceOf(FileBasedSshSessionFactory.class); } + @Test + public void strictHostKeyCheckShouldCheck() throws Exception { + assertThat(this.jGitEnvironmentRepository.isStrictHostKeyChecking()).isTrue(); + + this.jGitEnvironmentRepository.afterPropertiesSet(); + SshTransport sshTransport = DummySshTransport.newInstance(); + this.jGitEnvironmentRepository.getTransportConfigCallback().configure(sshTransport); + + SshdSessionFactory factory = (SshdSessionFactory) sshTransport.getSshSessionFactory(); + // There's no public method that can be used to inspect the ssh + // configuration, so we'll reflect + // the createSshConfigStore method to allow us to check that the config + // property is set as expected. + Method createSshConfigStore = factory.getClass().getDeclaredMethod("createSshConfigStore", File.class, + File.class, String.class); + createSshConfigStore.setAccessible(true); + SshConfigStore configStore = (SshConfigStore) createSshConfigStore.invoke(factory, new File("."), + new File("."), "local-username"); + createSshConfigStore.setAccessible(false); + assertThat("yes" + .equals(configStore.lookup("gitserver.com", 22, "username").getValue("StrictHostKeyChecking"))) + .isTrue(); + } + + } + + } + + private static class DummySshTransport extends SshTransport { + + DummySshTransport(String uri) throws URISyntaxException { + super(new URIish(uri)); + } + + @Override + public FetchConnection openFetch() { + return null; + } + + @Override + public PushConnection openPush() { + return null; + } + + public static SshTransport newInstance() { + try { + return new DummySshTransport("git+ssh://git@uri"); + } + catch (URISyntaxException e) { + throw new IllegalStateException("Not expected", e); + } } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/FileBasedSshSessionFactoryTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/FileBasedSshSessionFactoryTest.java new file mode 100644 index 00000000..17dcbc26 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/FileBasedSshSessionFactoryTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.ssh; + +import java.io.File; + +import org.eclipse.jgit.transport.SshConfigStore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.cloud.config.server.environment.JGitEnvironmentProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for file based SSH config processor. + */ +@RunWith(MockitoJUnitRunner.class) +public class FileBasedSshSessionFactoryTest { + + private FileBasedSshSessionFactory factory; + + @Test + public void strictHostKeyCheckingIsOptional() { + JGitEnvironmentProperties sshKey = new JGitEnvironmentProperties(); + sshKey.setUri("ssh://gitlab.example.local:3322/somerepo.git"); + sshKey.setStrictHostKeyChecking(false); + setupSessionFactory(sshKey); + + SshConfigStore.HostConfig sshConfig = getSshHostConfig("gitlab.example.local"); + + assertThat(sshConfig.getValue("StrictHostKeyChecking")).isEqualTo("no"); + } + + @Test + public void strictHostKeyCheckingIsUsed() { + JGitEnvironmentProperties sshKey = new JGitEnvironmentProperties(); + sshKey.setUri("ssh://gitlab.example.local:3322/somerepo.git"); + setupSessionFactory(sshKey); + + SshConfigStore.HostConfig sshConfig = getSshHostConfig("gitlab.example.local"); + + assertThat(sshConfig.getValue("StrictHostKeyChecking")).isEqualTo("yes"); + } + + @Test + public void handlesNullConfigFile() { + JGitEnvironmentProperties sshKey = new JGitEnvironmentProperties(); + sshKey.setUri("ssh://gitlab.example.local:3322/somerepo.git"); + setupSessionFactory(sshKey); + + SshConfigStore configStore = factory.createSshConfigStore(new File("dummy"), null, "localUserName"); + + assertThat(configStore).isNull(); + } + + private SshConfigStore.HostConfig getSshHostConfig(String hostName) { + return factory.createSshConfigStore(new File("dummy"), new File("dummy"), "localUserName").lookup(hostName, 22, + "userName"); + } + + private void setupSessionFactory(JGitEnvironmentProperties sshKey) { + this.factory = new FileBasedSshSessionFactory(sshKey); + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java index 789c28c3..7b060505 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java @@ -17,21 +17,35 @@ package org.springframework.cloud.config.server.ssh; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Spliterator; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; -import com.jcraft.jsch.HostKey; -import com.jcraft.jsch.HostKeyRepository; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.ProxyHTTP; -import com.jcraft.jsch.Session; -import org.eclipse.jgit.transport.OpenSshConfig.Host; +import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder; +import org.apache.sshd.common.keyprovider.KeyIdentityProvider; +import org.apache.sshd.common.session.SessionContext; +import org.eclipse.jgit.transport.SshConfigStore; +import org.eclipse.jgit.transport.sshd.ProxyData; +import org.eclipse.jgit.transport.sshd.ProxyDataFactory; +import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; +import org.eclipse.jgit.transport.sshd.SshdSessionFactory; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.cloud.config.server.environment.JGitEnvironmentProperties; @@ -40,9 +54,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** @@ -63,21 +75,6 @@ public class PropertyBasedSshSessionFactoryTest { private PropertyBasedSshSessionFactory factory; - @Mock - private Host hc; - - @Mock - private Session session; - - @Mock - private JSch jSch; - - @Mock - private HostKeyRepository hostKeyRepository; - - @Mock - private ProxyHTTP proxyMock; - public static String getResourceAsString(String path) { try { Resource resource = new ClassPathResource(path); @@ -102,10 +99,9 @@ public class PropertyBasedSshSessionFactoryTest { sshKey.setPrivateKey(PRIVATE_KEY); setupSessionFactory(sshKey); - this.factory.configure(this.hc, this.session); + SshConfigStore.HostConfig sshConfig = getSshHostConfig("gitlab.example.local"); - verify(this.session).setConfig("StrictHostKeyChecking", "no"); - verifyNoMoreInteractions(this.session); + assertThat(sshConfig.getValue("StrictHostKeyChecking")).isEqualTo("no"); } @Test @@ -116,10 +112,25 @@ public class PropertyBasedSshSessionFactoryTest { sshKey.setPrivateKey(PRIVATE_KEY); setupSessionFactory(sshKey); - this.factory.configure(this.hc, this.session); + SshConfigStore.HostConfig sshConfig = getSshHostConfig("gitlab.example.local"); - verify(this.session).setConfig("StrictHostKeyChecking", "yes"); - verifyNoMoreInteractions(this.session); + assertThat(sshConfig.getValue("StrictHostKeyChecking")).isEqualTo("yes"); + } + + @Test + public void sshConfigIsUsedForRelevantHostOnly() { + JGitEnvironmentProperties sshKey = new JGitEnvironmentProperties(); + sshKey.setUri("ssh://gitlab.example.local:3322/somerepo.git"); + sshKey.setHostKeyAlgorithm(HOST_KEY_ALGORITHM); + sshKey.setHostKey(HOST_KEY); + sshKey.setPrivateKey(PRIVATE_KEY); + setupSessionFactory(sshKey); + PublicKey configuredKey = toPublicKey(HOST_KEY, HOST_KEY_ALGORITHM); + + SshConfigStore.HostConfig sshConfig = getSshHostConfig("another.host"); + + assertThat(sshConfig.getValue("StrictHostKeyChecking")).isNull(); + assertThat(isKnownKeyForHost(configuredKey, "another.host")).isFalse(); } @Test @@ -131,37 +142,40 @@ public class PropertyBasedSshSessionFactoryTest { sshKey.setPrivateKey(PRIVATE_KEY); setupSessionFactory(sshKey); - this.factory.configure(this.hc, this.session); - verify(this.session).setConfig("server_host_key", HOST_KEY_ALGORITHM); - verify(this.session).setConfig("StrictHostKeyChecking", "yes"); - verifyNoMoreInteractions(this.session); + PublicKey hostKey = getSshHostKey("gitlab.example.local"); + + assertThat(hostKey).isNotNull(); + assertThat(hostKey.getAlgorithm()).isEqualTo(toPublicKey(HOST_KEY, HOST_KEY_ALGORITHM).getAlgorithm()); } @Test - public void privateKeyIsUsed() throws Exception { + public void privateKeyIsUsed() { JGitEnvironmentProperties sshKey = new JGitEnvironmentProperties(); sshKey.setUri("git@gitlab.example.local:someorg/somerepo.git"); sshKey.setPrivateKey(PRIVATE_KEY); setupSessionFactory(sshKey); - this.factory.createSession(this.hc, null, SshUriPropertyProcessor.getHostname(sshKey.getUri()), 22, null); - verify(this.jSch).addIdentity("gitlab.example.local", PRIVATE_KEY.getBytes(), null, null); + PrivateKey privateKey = getSshPrivateKey("gitlab.example.local"); + + assertThat(privateKey).isNotNull(); + assertThat(privateKey).isEqualTo(toPrivateKey(PRIVATE_KEY)); } @Test - public void hostKeyIsUsed() throws Exception { + public void hostKeyIsUsed() { JGitEnvironmentProperties sshKey = new JGitEnvironmentProperties(); sshKey.setUri("git@gitlab.example.local:someorg/somerepo.git"); + sshKey.setHostKeyAlgorithm(HOST_KEY_ALGORITHM); sshKey.setHostKey(HOST_KEY); sshKey.setPrivateKey(PRIVATE_KEY); setupSessionFactory(sshKey); + PublicKey configuredKey = toPublicKey(HOST_KEY, HOST_KEY_ALGORITHM); - this.factory.createSession(this.hc, null, SshUriPropertyProcessor.getHostname(sshKey.getUri()), 22, null); - ArgumentCaptor captor = ArgumentCaptor.forClass(HostKey.class); - verify(this.hostKeyRepository).add(captor.capture(), isNull()); - HostKey hostKey = captor.getValue(); - assertThat(hostKey.getHost()).isEqualTo("gitlab.example.local"); - assertThat(hostKey.getKey()).isEqualTo(HOST_KEY); + PublicKey knownHostKey = getSshHostKey("gitlab.example.local"); + + assertThat(knownHostKey).isNotNull(); + assertThat(knownHostKey).isEqualTo(configuredKey); + assertThat(isKnownKeyForHost(configuredKey, "gitlab.example.local")).isTrue(); } @Test @@ -172,30 +186,32 @@ public class PropertyBasedSshSessionFactoryTest { sshKey.setPreferredAuthentications("password,keyboard-interactive"); setupSessionFactory(sshKey); - this.factory.configure(this.hc, this.session); - verify(this.session).setConfig("PreferredAuthentications", "password,keyboard-interactive"); - verify(this.session).setConfig("StrictHostKeyChecking", "no"); - verifyNoMoreInteractions(this.session); + SshConfigStore.HostConfig sshConfig = getSshHostConfig("gitlab.example.local"); + + assertThat(sshConfig.getValue("PreferredAuthentications")).isEqualTo("password,keyboard-interactive"); + assertThat(sshConfig.getValue("StrictHostKeyChecking")).isEqualTo("no"); } @Test - public void customKnownHostsFileIsUsed() throws Exception { + public void customKnownHostsFileIsUsed() throws IOException { JGitEnvironmentProperties sshKey = new JGitEnvironmentProperties(); sshKey.setUri("git@gitlab.example.local:someorg/somerepo.git"); sshKey.setPrivateKey(PRIVATE_KEY); - sshKey.setKnownHostsFile("/ssh/known_hosts"); + sshKey.setKnownHostsFile(new ClassPathResource("/ssh/known_hosts").getFile().getPath()); setupSessionFactory(sshKey); + PublicKey configuredKey = toPublicKey(HOST_KEY, HOST_KEY_ALGORITHM); - this.factory.createSession(this.hc, null, SshUriPropertyProcessor.getHostname(sshKey.getUri()), 22, null); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + PublicKey knownHostKey = getSshHostKey("gitlab.example.local"); - verify(this.jSch).setKnownHosts(captor.capture()); - assertThat(captor.getValue()).isEqualTo("/ssh/known_hosts"); + assertThat(knownHostKey).isNotNull(); + assertThat(knownHostKey).isEqualTo(configuredKey); + assertThat(isKnownKeyForHost(configuredKey, "gitlab.example.local")).isTrue(); } @Test - public void proxySettingsIsUsed() { + public void proxySettingsIsUsed() throws Exception { JGitEnvironmentProperties sshProperties = new JGitEnvironmentProperties(); + sshProperties.setUri("ssh://gitlab.example.local:3322/somerepo.git"); sshProperties.setPrivateKey(PRIVATE_KEY); Map map = new HashMap<>(); ProxyHostProperties proxyHostProperties = new ProxyHostProperties(); @@ -204,30 +220,99 @@ public class PropertyBasedSshSessionFactoryTest { proxyHostProperties.setUsername("user"); proxyHostProperties.setPassword("password"); map.put(ProxyHostProperties.ProxyForScheme.HTTP, proxyHostProperties); - sshProperties.setProxy(map); setupSessionFactory(sshProperties); - this.factory.configure(this.hc, this.session); - ArgumentCaptor captor = ArgumentCaptor.forClass(ProxyHTTP.class); + ProxyData proxyData = getSshProxyData("gitlab.example.local"); - verify(this.session).setProxy(captor.capture()); - assertThat(captor.getValue()).isNotNull(); - verify(this.proxyMock).setUserPasswd("user", "password"); + assertThat(proxyData.getUser()).isEqualTo("user"); + assertThat(new String(proxyData.getPassword())).isEqualTo("password"); + assertThat(proxyData.getProxy().type().toString()).isEqualTo("HTTP"); + assertThat(proxyData.getProxy().address().toString()).containsPattern("host\\.domain.*:8080"); + } + + @Test + public void sshConfigFileIsNotUsed() { + setupSessionFactory(new JGitEnvironmentProperties()); + + assertThat(factory.getSshConfig(new File("."))).isNull(); + } + + private ProxyData getSshProxyData(String hostname) { + try { + Field proxies = SshdSessionFactory.class.getDeclaredField("proxies"); + proxies.setAccessible(true); + ProxyDataFactory proxyDataFactory = (ProxyDataFactory) proxies.get(factory); + proxies.setAccessible(false); + return proxyDataFactory.get(new InetSocketAddress(hostname, 22)); + } + catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private PublicKey toPublicKey(String key, String algorithm) { + try { + return new ECDSAPublicKeyEntryDecoder().decodePublicKey(null, algorithm, Base64.getDecoder().decode(key), + Collections.emptyMap()); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + private PrivateKey toPrivateKey(String key) { + try { + Collection keyPairs = KeyPairUtils.load(null, key); + + return keyPairs.isEmpty() ? null : keyPairs.iterator().next().getPrivate(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + private PrivateKey getSshPrivateKey(String hostName) { + SessionContext session = mock(SessionContext.class); + when(session.getRemoteAddress()).thenReturn(new InetSocketAddress(hostName, 22)); + + List kayPairs; + try { + Spliterator spliterator = ((KeyIdentityProvider) factory.getDefaultKeys(new File("."))) + .loadKeys(session).spliterator(); + + kayPairs = StreamSupport.stream(spliterator, false).collect(Collectors.toList()); + } + catch (IOException | GeneralSecurityException e) { + throw new RuntimeException(e); + } + + return kayPairs.isEmpty() ? null : kayPairs.get(0).getPrivate(); + } + + private PublicKey getSshHostKey(String hostName) { + InetSocketAddress address = new InetSocketAddress(hostName, 22); + List publicKeys = factory.getServerKeyDatabase(null, null).lookup("address", address, + mock(ServerKeyDatabase.Configuration.class)); + + return publicKeys.isEmpty() ? null : publicKeys.get(0); + } + + private boolean isKnownKeyForHost(PublicKey publicKey, String hostName) { + InetSocketAddress address = new InetSocketAddress(hostName, 22); + return factory.getServerKeyDatabase(null, null).accept("address", address, publicKey, + mock(ServerKeyDatabase.Configuration.class), null); + } + + private SshConfigStore.HostConfig getSshHostConfig(String hostName) { + return factory.createSshConfigStore(new File("dummy"), new File("dummy"), "localUserName").lookup(hostName, 22, + "userName"); } private void setupSessionFactory(JGitEnvironmentProperties sshKey) { Map sshKeysByHostname = new HashMap<>(); sshKeysByHostname.put(SshUriPropertyProcessor.getHostname(sshKey.getUri()), sshKey); - this.factory = new PropertyBasedSshSessionFactory(sshKeysByHostname, this.jSch) { - - @Override - protected ProxyHTTP createProxy(ProxyHostProperties proxyHostProperties) { - return proxyMock; - } - }; - when(this.hc.getHostName()).thenReturn(SshUriPropertyProcessor.getHostname(sshKey.getUri())); - when(this.jSch.getHostKeyRepository()).thenReturn(this.hostKeyRepository); + this.factory = new PropertyBasedSshSessionFactory(sshKeysByHostname); } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java index ad87afca..16058458 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java @@ -81,7 +81,7 @@ public class SshPropertyValidatorTest { } @Test - public void supportedParametersSuccesful() throws Exception { + public void supportedParametersSuccessful() { MultipleJGitEnvironmentProperties validSettings = new MultipleJGitEnvironmentProperties(); validSettings.setUri(SSH_URI); validSettings.setIgnoreLocalSshSettings(true); @@ -95,7 +95,19 @@ public class SshPropertyValidatorTest { } @Test - public void invalidPrivateKeyFails() throws Exception { + public void invalidPrivateKeyFails() { + MultipleJGitEnvironmentProperties invalidKey = new MultipleJGitEnvironmentProperties(); + invalidKey.setUri(SSH_URI); + invalidKey.setIgnoreLocalSshSettings(true); + invalidKey.setPrivateKey("-----BEGIN OPENSSH PRIVATE KEY-----\nFOOBAR"); + + Set> constraintViolations = validator + .validate(invalidKey); + assertThat(constraintViolations).hasSize(1); + } + + @Test + public void dummyPrivateKeyFails() { MultipleJGitEnvironmentProperties invalidKey = new MultipleJGitEnvironmentProperties(); invalidKey.setUri(SSH_URI); invalidKey.setIgnoreLocalSshSettings(true); @@ -107,7 +119,7 @@ public class SshPropertyValidatorTest { } @Test - public void missingPrivateKeyFails() throws Exception { + public void missingPrivateKeyFails() { MultipleJGitEnvironmentProperties missingKey = new MultipleJGitEnvironmentProperties(); missingKey.setUri(SSH_URI); missingKey.setIgnoreLocalSshSettings(true); @@ -118,7 +130,7 @@ public class SshPropertyValidatorTest { } @Test - public void hostKeyWithMissingAlgoFails() throws Exception { + public void hostKeyWithMissingAlgoFails() { MultipleJGitEnvironmentProperties missingAlgo = new MultipleJGitEnvironmentProperties(); missingAlgo.setUri(SSH_URI); missingAlgo.setIgnoreLocalSshSettings(true); @@ -131,7 +143,7 @@ public class SshPropertyValidatorTest { } @Test - public void algoWithMissingHostKeyFails() throws Exception { + public void algoWithMissingHostKeyFails() { MultipleJGitEnvironmentProperties missingHostKey = new MultipleJGitEnvironmentProperties(); missingHostKey.setUri(SSH_URI); missingHostKey.setIgnoreLocalSshSettings(true); @@ -144,7 +156,7 @@ public class SshPropertyValidatorTest { } @Test - public void unsupportedAlgoFails() throws Exception { + public void unsupportedAlgoFails() { MultipleJGitEnvironmentProperties unsupportedAlgo = new MultipleJGitEnvironmentProperties(); unsupportedAlgo.setUri(SSH_URI); unsupportedAlgo.setIgnoreLocalSshSettings(true); @@ -158,7 +170,7 @@ public class SshPropertyValidatorTest { } @Test - public void validatorNotRunIfIgnoreLocalSettingsFalse() throws Exception { + public void validatorNotRunIfIgnoreLocalSettingsFalse() { MultipleJGitEnvironmentProperties useLocal = new MultipleJGitEnvironmentProperties(); useLocal.setUri(SSH_URI); useLocal.setIgnoreLocalSshSettings(false); @@ -170,7 +182,7 @@ public class SshPropertyValidatorTest { } @Test - public void validatorNotRunIfHttpsUri() throws Exception { + public void validatorNotRunIfHttpsUri() { MultipleJGitEnvironmentProperties httpsUri = new MultipleJGitEnvironmentProperties(); httpsUri.setUri("https://somerepo.com/team/project.git"); httpsUri.setIgnoreLocalSshSettings(true); @@ -182,7 +194,7 @@ public class SshPropertyValidatorTest { } @Test - public void preferredAuthenticationsIsValidated() throws Exception { + public void preferredAuthenticationsIsValidated() { MultipleJGitEnvironmentProperties sshUriProperties = new MultipleJGitEnvironmentProperties(); assertThat(validator.validate(sshUriProperties)).hasSize(0); @@ -194,7 +206,7 @@ public class SshPropertyValidatorTest { } @Test - public void knowHostsFileIsValidated() throws Exception { + public void knowHostsFileIsValidated() { MultipleJGitEnvironmentProperties sshUriProperties = new MultipleJGitEnvironmentProperties(); assertThat(validator.validate(sshUriProperties)).hasSize(0); diff --git a/spring-cloud-config-server/src/test/resources/ssh/known_hosts b/spring-cloud-config-server/src/test/resources/ssh/known_hosts new file mode 100644 index 00000000..3ba67a7e --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/known_hosts @@ -0,0 +1 @@ +gitlab.example.local ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMzCa0AcNbahUFjFYJHIilhJOhKFHuDOOuY+/HqV9kALftitwNYo6dQ+tC9IK5JVZCZfqKfDWVMxspcPDf9eMoE= diff --git a/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block-list.yml b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block-list.yml index a277f1f2..e4b19af6 100644 --- a/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block-list.yml +++ b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block-list.yml @@ -7,30 +7,41 @@ spring: uri: git@gitserver.com:team/repo.git ignoreLocalSshSettings: true privateKey: | - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI - yyxp38YcuZG9cUDBAxby+K2TKmwHaC1Wf1QTwbPuCRdIPrDwRz+FLoegm3iDLCmn - uP6rjZDneYsqfU1sSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km - ayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq - TOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir - +diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk - jfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt - lDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI - xdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V - 4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG - 7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu - 3reKgm0CgYEA03Sx8JgF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8 - q4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq - mRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa - lKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M - ZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv - ZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit - BT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO - zf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw - uC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF - 54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS - E1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx - 4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr - /syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq - 2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ== - -----END RSA PRIVATE KEY----- + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn + NhAAAAAwEAAQAAAYEAw9e+uwqtqnXCuxqeCoterQzT95Mx1XwAlIEsee+0xyzbQ54hlXTw + p/8kFRue0eq08VYOIFt/CkpDNvGiNZDYa1HPzYRbxgU1Mi73c58VbRdghg8hGEF8hs29tQ + bAyi1E7KoPfXn4Jhqp57PoUB2StuBwj9VU8DR7t4QvTx1XZmIh6ZND7pRKjyvzDCFSG7cO + bMpiWgVkDmmZWbRGY/el/GHfqah5cTEyBN8/HYXOL5opymQfmhd66t1/Yh0sacEBnbvHjD + Q1yIhf/tFXbuunacGLzP30XimexQOJ8CnSgFBBgu360saS5xfGJVoIgvnm44Irbw/Mtuxj + i6VFaWEfJNvGvhFIAVMf98cHZ+mdLpQIxFm+AUBcAPVGoI6kiXr6LucSWigz+TmeQ3T/Y1 + hSlofgN4KudZWpxUPJKDU3kQ2WAdVIo22/tO7egJsnd9os+fzxaas+yaaZ2QtLDCaHBxO0 + 78o2TPlAqxzgt+yvm0NYZ+G0XBazJMn49qCL91T5AAAFmHD1ZR9w9WUfAAAAB3NzaC1yc2 + EAAAGBAMPXvrsKrap1wrsangqLXq0M0/eTMdV8AJSBLHnvtMcs20OeIZV08Kf/JBUbntHq + tPFWDiBbfwpKQzbxojWQ2GtRz82EW8YFNTIu93OfFW0XYIYPIRhBfIbNvbUGwMotROyqD3 + 15+CYaqeez6FAdkrbgcI/VVPA0e7eEL08dV2ZiIemTQ+6USo8r8wwhUhu3DmzKYloFZA5p + mVm0RmP3pfxh36moeXExMgTfPx2Fzi+aKcpkH5oXeurdf2IdLGnBAZ27x4w0NciIX/7RV2 + 7rp2nBi8z99F4pnsUDifAp0oBQQYLt+tLGkucXxiVaCIL55uOCK28PzLbsY4ulRWlhHyTb + xr4RSAFTH/fHB2fpnS6UCMRZvgFAXAD1RqCOpIl6+i7nElooM/k5nkN0/2NYUpaH4DeCrn + WVqcVDySg1N5ENlgHVSKNtv7Tu3oCbJ3faLPn88WmrPsmmmdkLSwwmhwcTtO/KNkz5QKsc + 4Lfsr5tDWGfhtFwWsyTJ+Pagi/dU+QAAAAMBAAEAAAGAAc1LYPcxL99Tgls1Vw1/OoJitO + Vy0O8KJlOl8B1HgYmlHtMmpfRkfnc3gsY1SOMq9QmAqcWNvq9+PNQuVOXXR+2BxvdPzNuh + aKvL9RFiphVP+wvKlymLFsZv12mPfoy6FJ9f8xybLuaR56LdIVeUUQBxqLEize79sGuT79 + tqQXPnsfl754cPxI939gWcdsrRZCjcjM195TANjCi/eQ3/Rfo0j6AQNf1o42iWcRQZGJEH + j2gqbkWHCQAuut1f2j0/XXApnY2YVnD6brfTDo0P6AX1DWV9DvCkWwiI6/xYWUPmyjmE2x + JiDDeGWkI6WihnMlMObc8N8y/C5r2Oaw/WVtNSxiP2JaLBHq7lF3cGOzTfKrwS0lXtw1z6 + nJS/OwKiplvTDcNKUIFAKlryb6psHSFuHKneFPB3hNz0Pznth3EmXoxoAgGD3vMJQD40sF + BIsSS6AuQVHstbLg6AZ7g6eTCMt+JH9Rbpt3hhznR5SZz4rgi49txFV0x/fN6JOm+NAAAA + wQCuAd8WCe4sQkTkpaSt0qEvqifGxLFnRLoLpVBQvBiJAFDn6f9JhpWHoND6YBYg9T8OXp + vJBED+v6TIfmynxt53S8MhadzwnSaqhf+arW+Q+H56acOtmEnlxv+epbSdFraGT6XExsdG + 9zeVV2FTpaOG8s9Dye5zg7FYUAazjlxEnj8IavfkSV5htF7k6nuNADauw35v9jfEPbLvGN + L0jY277Bj55a2ytRuHqbZHm1lrOe9iK4XOddehBlXEsvIGWuUAAADBAOQcUn7EtxSYmUL+ + mqn8+biEC8+7dgsv6mJvOETJhcRE3mbSgYO03UxrtOhcylUf0ks0mYvZugvTHN2dsdG9eT + 1ds0nZvOoKXwkSSJCH83+J7+jnI9PxK+nhqppZ3RtjKaaRDHmaS5zQZi6ZahSKGPx0KszB + +SmrzUJLpUq9cD7kDC3TydN2prGIF7RSgl6KhiHXm5UbYmFctNhMhyb1GPPDY9RWeo5zFX + VcXjY8MCXWiBYLCtbEPfcIAzHQcpiv+wAAAMEA28l23xIkQnMDkoiJqiK0BjT6G5joRzcj + VDD+qZCOR5z7d617mvYQM7B+3Mu8h4jXk4aQPyHMF3riRj8f2pvWBVqVl5EAAvKsywm/9l + LuyVFDDd3GlJRYuO/1XYKM8i7iXa4nKrQVcxiyF8gkiyafrqYSxuM0UTMc5vX4fCxQSqgh + 8WUG6n1OTwJ/x9grdXLi8rbjXaZbZfax4eOBJeEb9gKqE8+Ak0aRyXOMw8ygiUTIQELr7T + xLwZE6Y5ULy9ibAAAAHm1zaGFtc2lAbXNoYW1zaS1hMDIudm13YXJlLmNvbQECAwQ= + -----END OPENSSH PRIVATE KEY----- diff --git a/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml index 021f9572..dbfd2291 100644 --- a/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml +++ b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml @@ -6,30 +6,41 @@ spring: uri: git@gitserver.com:team/repo.git ignoreLocalSshSettings: true privateKey: | - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI - yyxp38YcuZG9cUDBAxby+K2TKmwHaC1Wf1QTwbPuCRdIPrDwRz+FLoegm3iDLCmn - uP6rjZDneYsqfU1sSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km - ayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq - TOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir - +diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk - jfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt - lDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI - xdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V - 4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG - 7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu - 3reKgm0CgYEA03Sx8JgF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8 - q4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq - mRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa - lKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M - ZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv - ZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit - BT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO - zf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw - uC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF - 54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS - E1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx - 4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr - /syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq - 2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ== - -----END RSA PRIVATE KEY----- + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn + NhAAAAAwEAAQAAAYEAw9e+uwqtqnXCuxqeCoterQzT95Mx1XwAlIEsee+0xyzbQ54hlXTw + p/8kFRue0eq08VYOIFt/CkpDNvGiNZDYa1HPzYRbxgU1Mi73c58VbRdghg8hGEF8hs29tQ + bAyi1E7KoPfXn4Jhqp57PoUB2StuBwj9VU8DR7t4QvTx1XZmIh6ZND7pRKjyvzDCFSG7cO + bMpiWgVkDmmZWbRGY/el/GHfqah5cTEyBN8/HYXOL5opymQfmhd66t1/Yh0sacEBnbvHjD + Q1yIhf/tFXbuunacGLzP30XimexQOJ8CnSgFBBgu360saS5xfGJVoIgvnm44Irbw/Mtuxj + i6VFaWEfJNvGvhFIAVMf98cHZ+mdLpQIxFm+AUBcAPVGoI6kiXr6LucSWigz+TmeQ3T/Y1 + hSlofgN4KudZWpxUPJKDU3kQ2WAdVIo22/tO7egJsnd9os+fzxaas+yaaZ2QtLDCaHBxO0 + 78o2TPlAqxzgt+yvm0NYZ+G0XBazJMn49qCL91T5AAAFmHD1ZR9w9WUfAAAAB3NzaC1yc2 + EAAAGBAMPXvrsKrap1wrsangqLXq0M0/eTMdV8AJSBLHnvtMcs20OeIZV08Kf/JBUbntHq + tPFWDiBbfwpKQzbxojWQ2GtRz82EW8YFNTIu93OfFW0XYIYPIRhBfIbNvbUGwMotROyqD3 + 15+CYaqeez6FAdkrbgcI/VVPA0e7eEL08dV2ZiIemTQ+6USo8r8wwhUhu3DmzKYloFZA5p + mVm0RmP3pfxh36moeXExMgTfPx2Fzi+aKcpkH5oXeurdf2IdLGnBAZ27x4w0NciIX/7RV2 + 7rp2nBi8z99F4pnsUDifAp0oBQQYLt+tLGkucXxiVaCIL55uOCK28PzLbsY4ulRWlhHyTb + xr4RSAFTH/fHB2fpnS6UCMRZvgFAXAD1RqCOpIl6+i7nElooM/k5nkN0/2NYUpaH4DeCrn + WVqcVDySg1N5ENlgHVSKNtv7Tu3oCbJ3faLPn88WmrPsmmmdkLSwwmhwcTtO/KNkz5QKsc + 4Lfsr5tDWGfhtFwWsyTJ+Pagi/dU+QAAAAMBAAEAAAGAAc1LYPcxL99Tgls1Vw1/OoJitO + Vy0O8KJlOl8B1HgYmlHtMmpfRkfnc3gsY1SOMq9QmAqcWNvq9+PNQuVOXXR+2BxvdPzNuh + aKvL9RFiphVP+wvKlymLFsZv12mPfoy6FJ9f8xybLuaR56LdIVeUUQBxqLEize79sGuT79 + tqQXPnsfl754cPxI939gWcdsrRZCjcjM195TANjCi/eQ3/Rfo0j6AQNf1o42iWcRQZGJEH + j2gqbkWHCQAuut1f2j0/XXApnY2YVnD6brfTDo0P6AX1DWV9DvCkWwiI6/xYWUPmyjmE2x + JiDDeGWkI6WihnMlMObc8N8y/C5r2Oaw/WVtNSxiP2JaLBHq7lF3cGOzTfKrwS0lXtw1z6 + nJS/OwKiplvTDcNKUIFAKlryb6psHSFuHKneFPB3hNz0Pznth3EmXoxoAgGD3vMJQD40sF + BIsSS6AuQVHstbLg6AZ7g6eTCMt+JH9Rbpt3hhznR5SZz4rgi49txFV0x/fN6JOm+NAAAA + wQCuAd8WCe4sQkTkpaSt0qEvqifGxLFnRLoLpVBQvBiJAFDn6f9JhpWHoND6YBYg9T8OXp + vJBED+v6TIfmynxt53S8MhadzwnSaqhf+arW+Q+H56acOtmEnlxv+epbSdFraGT6XExsdG + 9zeVV2FTpaOG8s9Dye5zg7FYUAazjlxEnj8IavfkSV5htF7k6nuNADauw35v9jfEPbLvGN + L0jY277Bj55a2ytRuHqbZHm1lrOe9iK4XOddehBlXEsvIGWuUAAADBAOQcUn7EtxSYmUL+ + mqn8+biEC8+7dgsv6mJvOETJhcRE3mbSgYO03UxrtOhcylUf0ks0mYvZugvTHN2dsdG9eT + 1ds0nZvOoKXwkSSJCH83+J7+jnI9PxK+nhqppZ3RtjKaaRDHmaS5zQZi6ZahSKGPx0KszB + +SmrzUJLpUq9cD7kDC3TydN2prGIF7RSgl6KhiHXm5UbYmFctNhMhyb1GPPDY9RWeo5zFX + VcXjY8MCXWiBYLCtbEPfcIAzHQcpiv+wAAAMEA28l23xIkQnMDkoiJqiK0BjT6G5joRzcj + VDD+qZCOR5z7d617mvYQM7B+3Mu8h4jXk4aQPyHMF3riRj8f2pvWBVqVl5EAAvKsywm/9l + LuyVFDDd3GlJRYuO/1XYKM8i7iXa4nKrQVcxiyF8gkiyafrqYSxuM0UTMc5vX4fCxQSqgh + 8WUG6n1OTwJ/x9grdXLi8rbjXaZbZfax4eOBJeEb9gKqE8+Ak0aRyXOMw8ygiUTIQELr7T + xLwZE6Y5ULy9ibAAAAHm1zaGFtc2lAbXNoYW1zaS1hMDIudm13YXJlLmNvbQECAwQ= + -----END OPENSSH PRIVATE KEY-----