Merge branch '3.1.x'

This commit is contained in:
Ryan Baxter
2022-07-15 17:18:45 -04:00
19 changed files with 796 additions and 303 deletions

View File

@@ -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.

View File

@@ -6,16 +6,16 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>4.0.0-SNAPSHOT</version>
<version>3.1.4-SNAPSHOT</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-config-dependencies</artifactId>
<version>4.0.0-SNAPSHOT</version>
<version>3.1.4-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-config-dependencies</name>
<description>Spring Cloud Config Dependencies</description>
<properties>
<jgit.version>5.12.0.202106070339-r</jgit.version>
<jgit.version>5.13.1.202206130422-r</jgit.version>
<spring-vault.version>3.0.0-M1</spring-vault.version>
<spring-credhub.version>2.1.1.RELEASE</spring-credhub.version>
</properties>

View File

@@ -82,7 +82,7 @@
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
<artifactId>org.eclipse.jgit.ssh.apache</artifactId>
</dependency>
<dependency>
<groupId>org.yaml</groupId>

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;
}
};
}
}

View File

@@ -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));
}
}
}

View File

@@ -48,8 +48,8 @@ public class HostKeyAlgoSupportedValidator
private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git.";
private static final Set<String> 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<String> 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();

View File

@@ -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<KeyPair> 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<String> {
protected StringResource(String resourceValue) {
super(String.class, resourceValue);
}
@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(this.getResourceValue().getBytes());
}
}
}

View File

@@ -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<PrivateKeyIsVali
private boolean isPrivateKeyFormatCorrect(JGitEnvironmentProperties sshUriProperties,
ConstraintValidatorContext context) {
try {
KeyPair.load(new JSch(), sshUriProperties.getPrivateKey().getBytes(), null);
if (KeyPairUtils.isValid(sshUriProperties.getPrivateKey())) {
return true;
}
catch (JSchException e) {
context.buildConstraintViolationWithTemplate(
format("Property '%sprivateKey' is not a valid private key", GIT_PROPERTY_PREFIX))
.addConstraintViolation();
return false;
}
context.buildConstraintViolationWithTemplate(
format("Property '%sprivateKey' is not a valid private key", GIT_PROPERTY_PREFIX))
.addConstraintViolation();
return false;
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.cloud.config.server.ssh;
import com.jcraft.jsch.JSch;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.Transport;
@@ -44,9 +43,8 @@ public class PropertiesBasedSshTransportConfigCallback implements TransportConfi
@Override
public void configure(Transport transport) {
if (transport instanceof SshTransport) {
SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(new PropertyBasedSshSessionFactory(
new SshUriPropertyProcessor(this.sshUriProperties).getSshKeysByHostname(), new JSch()));
((SshTransport) transport).setSshSessionFactory(new PropertyBasedSshSessionFactory(
new SshUriPropertyProcessor(this.sshUriProperties).getSshKeysByHostname()));
}
}

View File

@@ -16,20 +16,39 @@
package org.springframework.cloud.config.server.ssh;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.jcraft.jsch.HostKey;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.ProxyHTTP;
import com.jcraft.jsch.Session;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig.Host;
import org.eclipse.jgit.util.Base64;
import org.eclipse.jgit.util.FS;
import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConfigStore;
import org.eclipse.jgit.transport.sshd.JGitKeyCache;
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.springframework.cloud.config.server.environment.JGitEnvironmentProperties;
import org.springframework.cloud.config.server.proxy.ProxyHostProperties;
import org.springframework.util.StringUtils;
/**
* In a cloud environment local SSH config files such as `.known_hosts` may not be
@@ -39,7 +58,7 @@ import org.springframework.cloud.config.server.proxy.ProxyHostProperties;
* @author William Tran
* @author Ollie Hughes
*/
public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory {
public class PropertyBasedSshSessionFactory extends SshdSessionFactory {
private static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
@@ -49,62 +68,179 @@ public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory {
private static final String NO_OPTION = "no";
private static final String SERVER_HOST_KEY = "server_host_key";
private final Map<String, JGitEnvironmentProperties> sshKeysByHostname;
private final JSch jSch;
public PropertyBasedSshSessionFactory(Map<String, JGitEnvironmentProperties> sshKeysByHostname) {
super(new JGitKeyCache(), new HttpProxyDataFactory(sshKeysByHostname));
public PropertyBasedSshSessionFactory(Map<String, JGitEnvironmentProperties> 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<PublicKey> lookup(String connectAddress, InetSocketAddress remoteAddress,
Configuration config) {
JGitEnvironmentProperties sshProperties = sshKeysByHostname.get(remoteAddress.getHostName());
if (sshProperties == null) {
return Collections.emptyList();
}
List<Path> knownHostFiles = getKnownHostFiles(sshProperties);
List<PublicKey> 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<PublicKey> 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<Path> getKnownHostFiles(JGitEnvironmentProperties sshProperties) {
if (sshProperties.getKnownHostsFile() == null) {
return Collections.emptyList();
}
else {
return Collections.singletonList(Paths.get(sshProperties.getKnownHostsFile()));
}
}
};
}
@Override
protected Iterable<KeyPair> getDefaultKeys(File dir) {
return new SingleKeyIdentityProvider(sshKeysByHostname);
}
private final static class SingleKeyIdentityProvider implements KeyIdentityProvider, Iterable<KeyPair> {
private final Map<String, JGitEnvironmentProperties> sshKeysByHostname;
private SingleKeyIdentityProvider(Map<String, JGitEnvironmentProperties> sshKeysByHostname) {
this.sshKeysByHostname = sshKeysByHostname;
}
throw new JSchException("no keys configured for hostname " + host);
@Override
public Iterator<KeyPair> iterator() {
throw new UnsupportedOperationException("Should not be called");
}
@Override
public Iterable<KeyPair> 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<String, JGitEnvironmentProperties> sshKeysByHostname;
private HttpProxyDataFactory(Map<String, JGitEnvironmentProperties> 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());
}
}
}

View File

@@ -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<String> keyCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> 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<String> keyCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<HostKey> 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<String> 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<ProxyHostProperties.ProxyForScheme, ProxyHostProperties> 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<ProxyHTTP> 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<KeyPair> 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<KeyPair> kayPairs;
try {
Spliterator<KeyPair> 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<PublicKey> 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<String, JGitEnvironmentProperties> 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);
}
}

View File

@@ -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<ConstraintViolation<MultipleJGitEnvironmentProperties>> 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);

View File

@@ -0,0 +1 @@
gitlab.example.local ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMzCa0AcNbahUFjFYJHIilhJOhKFHuDOOuY+/HqV9kALftitwNYo6dQ+tC9IK5JVZCZfqKfDWVMxspcPDf9eMoE=

View File

@@ -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-----

View File

@@ -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-----