Skip to content

Commit

Permalink
Merge pull request #54 from mc1arke/jenkins-42959-specify-hostkey-alg…
Browse files Browse the repository at this point in the history
…orithms

[FIXED JENKINS-42959] Specify preferred host keys during connect
  • Loading branch information
stephenc committed Jun 12, 2017
2 parents 860a3a9 + 12903d2 commit 67bbbff
Show file tree
Hide file tree
Showing 11 changed files with 324 additions and 25 deletions.
11 changes: 9 additions & 2 deletions pom.xml
Expand Up @@ -16,8 +16,8 @@
<description>Allows to launch agents over SSH, using a Java implementation of the SSH protocol</description>

<properties>
<jenkins.version>1.609.1</jenkins.version>
<java.level>6</java.level>
<jenkins.version>1.625</jenkins.version>
<java.level>7</java.level>
<jenkins-test-harness.version>2.18</jenkins-test-harness.version>
</properties>

Expand Down Expand Up @@ -62,6 +62,13 @@

<dependencies>
<!-- regular dependencies -->
<dependency>
<groupId>org.jenkins-ci</groupId>
<artifactId>trilead-ssh2</artifactId>
<version>build-217-jenkins-11</version>
<scope>provided</scope>
<!-- we only need the newer version for testing, we use the bundled version during execution -->
</dependency>
<!-- plugin dependencies -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
Expand Down
1 change: 1 addition & 0 deletions src/main/java/hudson/plugins/sshslaves/SSHLauncher.java
Expand Up @@ -786,6 +786,7 @@ public synchronized void launch(final SlaveComputer computer, final TaskListener
public Boolean call() throws InterruptedException {
Boolean rval = Boolean.FALSE;
try {
connection.setServerHostKeyAlgorithms(sshHostKeyVerificationStrategy.getPreferredKeyAlgorithms(computer));

openConnection(listener, computer);

Expand Down
@@ -0,0 +1,42 @@
package hudson.plugins.sshslaves.verifiers;

import com.trilead.ssh2.signature.KeyAlgorithm;
import com.trilead.ssh2.signature.KeyAlgorithmManager;
import hudson.plugins.sshslaves.Messages;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* @author Michael Clarke
*/
@Restricted(NoExternalUse.class)
class JenkinsTrilead9VersionSupport extends TrileadVersionSupportManager.TrileadVersionSupport {

@Override
public String[] getSupportedAlgorithms() {
List<String> algorithms = new ArrayList<>();
for (KeyAlgorithm<?, ?> algorithm : KeyAlgorithmManager.getSupportedAlgorithms()) {
algorithms.add(algorithm.getKeyFormat());
}
return algorithms.toArray(new String[algorithms.size()]);
}

@Override
public HostKey parseKey(String algorithm, byte[] keyValue) throws KeyParseException {
for (KeyAlgorithm<?, ?> keyAlgorithm : KeyAlgorithmManager.getSupportedAlgorithms()) {
try {
if (keyAlgorithm.getKeyFormat().equals(algorithm)) {
keyAlgorithm.decodePublicKey(keyValue);
return new HostKey(algorithm, keyValue);
}
} catch (IOException ex) {
throw new KeyParseException(Messages.ManualKeyProvidedHostKeyVerifier_KeyValueDoesNotParse(algorithm), ex);
}
}
throw new KeyParseException("Unexpected key algorithm: " + algorithm);
}
}
@@ -0,0 +1,16 @@
package hudson.plugins.sshslaves.verifiers;

/**
* @author Michael Clarke
* @since 1.18
*/
public class KeyParseException extends Exception {

public KeyParseException(String message) {
super(message);
}

public KeyParseException(String message, Throwable cause) {
super(message, cause);
}
}
Expand Up @@ -24,7 +24,9 @@
package hudson.plugins.sshslaves.verifiers;

import java.io.File;
import java.io.IOException;

import hudson.slaves.ComputerLauncher;
import org.kohsuke.stapler.DataBoundConstructor;

import com.trilead.ssh2.KnownHosts;
Expand Down Expand Up @@ -52,14 +54,18 @@ public KnownHostsFileKeyVerificationStrategy() {

@Override
public boolean verify(SlaveComputer computer, HostKey hostKey, TaskListener listener) throws Exception {
ComputerLauncher launcher = computer.getLauncher();
if (!(launcher instanceof SSHLauncher)) {
return false;
}

if (!KNOWN_HOSTS_FILE.exists()) {
listener.getLogger().println(Messages.KnownHostsFileHostKeyVerifier_NoKnownHostsFile(KNOWN_HOSTS_FILE.getAbsolutePath()));
return false;
}

KnownHosts knownHosts = new KnownHosts(KNOWN_HOSTS_FILE);
int result = knownHosts.verifyHostkey(((SSHLauncher)computer.getLauncher()).getHost(), hostKey.getAlgorithm(), hostKey.getKey());
int result = knownHosts.verifyHostkey(((SSHLauncher)launcher).getHost(), hostKey.getAlgorithm(), hostKey.getKey());

if (KnownHosts.HOSTKEY_IS_OK == result) {
listener.getLogger().println(Messages.KnownHostsFileHostKeyVerifier_KeyTrused(SSHLauncher.getTimestamp()));
Expand All @@ -73,6 +79,19 @@ public boolean verify(SlaveComputer computer, HostKey hostKey, TaskListener list
}

}

@Override
public String[] getPreferredKeyAlgorithms(SlaveComputer computer) throws IOException {
ComputerLauncher launcher = computer.getLauncher();

if (!(launcher instanceof SSHLauncher) || !KNOWN_HOSTS_FILE.exists()) {
return super.getPreferredKeyAlgorithms(computer);
}

KnownHosts knownHosts = new KnownHosts(KNOWN_HOSTS_FILE);
return knownHosts.getPreferredServerHostkeyAlgorithmOrder(((SSHLauncher) launcher).getHost());
}


@Extension
public static class KnownHostsFileKeyVerificationStrategyDescriptor extends SshHostKeyVerificationStrategyDescriptor {
Expand Down
Expand Up @@ -24,13 +24,14 @@
package hudson.plugins.sshslaves.verifiers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

import com.trilead.ssh2.signature.DSASHA1Verify;
import com.trilead.ssh2.signature.RSASHA1Verify;

import hudson.Extension;
import hudson.model.TaskListener;
Expand All @@ -54,7 +55,11 @@ public class ManuallyProvidedKeyVerificationStrategy extends SshHostKeyVerificat
@DataBoundConstructor
public ManuallyProvidedKeyVerificationStrategy(String key) {
super();
this.key = parseKey(key);
try {
this.key = parseKey(key);
} catch (KeyParseException e) {
throw new IllegalArgumentException("Invalid key: " + e.getMessage(), e);
}
}

public String getKey() {
Expand All @@ -75,34 +80,29 @@ public boolean verify(SlaveComputer computer, HostKey hostKey, TaskListener list
return false;
}
}

@Override
public String[] getPreferredKeyAlgorithms(SlaveComputer computer) throws IOException {
List<String> sortedAlgorithms = new ArrayList<>(Arrays.asList(super.getPreferredKeyAlgorithms(computer)));

sortedAlgorithms.remove(key.getAlgorithm());
sortedAlgorithms.add(0, key.getAlgorithm());

return sortedAlgorithms.toArray(new String[sortedAlgorithms.size()]);
}

private static HostKey parseKey(String key) {
private static HostKey parseKey(String key) throws KeyParseException {
if (!key.contains(" ")) {
throw new IllegalArgumentException(Messages.ManualKeyProvidedHostKeyVerifier_TwoPartKey());
}
StringTokenizer tokenizer = new StringTokenizer(key, " ");
String algorithm = tokenizer.nextToken();
byte[] keyValue = Base64.decode(tokenizer.nextToken());
if (null == keyValue) {
throw new IllegalArgumentException(Messages.ManualKeyProvidedHostKeyVerifier_Base64EncodedKeyValueRequired());
}

try {
if ("ssh-rsa".equals(algorithm)) {
RSASHA1Verify.decodeSSHRSAPublicKey(keyValue);
} else if ("ssh-dss".equals(algorithm)) {
DSASHA1Verify.decodeSSHDSAPublicKey(keyValue);
} else {
throw new IllegalArgumentException("Key algorithm should be one of ssh-rsa or ssh-dss");
}
} catch (IOException ex) {
throw new IllegalArgumentException(Messages.ManualKeyProvidedHostKeyVerifier_KeyValueDoesNotParse(algorithm), ex);
} catch (StringIndexOutOfBoundsException ex) {
// can happen in DSASHA1Verifier with certain values (from quick testing)
throw new IllegalArgumentException(Messages.ManualKeyProvidedHostKeyVerifier_KeyValueDoesNotParse(algorithm), ex);
throw new KeyParseException(Messages.ManualKeyProvidedHostKeyVerifier_Base64EncodedKeyValueRequired());
}

return new HostKey(algorithm, keyValue);
return TrileadVersionSupportManager.getTrileadSupport().parseKey(algorithm, keyValue);
}

@Extension
Expand All @@ -117,7 +117,7 @@ public FormValidation doCheckKey(@QueryParameter String key) {
try {
ManuallyProvidedKeyVerificationStrategy.parseKey(key);
return FormValidation.ok();
} catch (IllegalArgumentException ex) {
} catch (KeyParseException ex) {
return FormValidation.error(ex.getMessage());
}
}
Expand Down
Expand Up @@ -33,6 +33,8 @@
import hudson.slaves.SlaveComputer;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -98,6 +100,24 @@ else if (!existingHostKey.equals(hostKey)) {
}
}

@Override
public String[] getPreferredKeyAlgorithms(SlaveComputer computer) throws IOException {
String[] algorithms = super.getPreferredKeyAlgorithms(computer);

HostKey hostKey = HostKeyHelper.getInstance().getHostKey(computer);

if (null != hostKey) {
List<String> sortedAlgorithms = new ArrayList<>(Arrays.asList(algorithms));

sortedAlgorithms.remove(hostKey.getAlgorithm());
sortedAlgorithms.add(0, hostKey.getAlgorithm());

algorithms = sortedAlgorithms.toArray(new String[sortedAlgorithms.size()]);
}

return algorithms;
}

/** TODO replace with {@link Computer#addAction} after core baseline picks up JENKINS-42969 fix */
private static void addAction(@Nonnull Computer c, @Nonnull Action a) {
try {
Expand Down
Expand Up @@ -29,6 +29,8 @@
import hudson.slaves.SlaveComputer;
import jenkins.model.Jenkins;

import java.io.IOException;

/**
* A method for verifying the host key provided by the remote host during the
* initiation of each connection.
Expand All @@ -53,6 +55,10 @@ public SshHostKeyVerificationStrategyDescriptor getDescriptor() {
* @since 1.12
*/
public abstract boolean verify(SlaveComputer computer, HostKey hostKey, TaskListener listener) throws Exception;

public String[] getPreferredKeyAlgorithms(SlaveComputer computer) throws IOException {
return TrileadVersionSupportManager.getTrileadSupport().getSupportedAlgorithms();
}

public static abstract class SshHostKeyVerificationStrategyDescriptor extends Descriptor<SshHostKeyVerificationStrategy> {

Expand Down
@@ -0,0 +1,104 @@
package hudson.plugins.sshslaves.verifiers;

import com.trilead.ssh2.signature.DSASHA1Verify;
import com.trilead.ssh2.signature.RSASHA1Verify;
import hudson.plugins.sshslaves.Messages;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* An abstraction layer to allow handling of feature changes (e.g. new key types) between different Trilead versions.
* @author Michael Clarke
* @since 1.18
*/
@Restricted(NoExternalUse.class)
final class TrileadVersionSupportManager {

private static final Logger LOGGER = Logger.getLogger(TrileadVersionSupportManager.class.getName());

/**
* Craetes an instance of TrileadVersionSupport that can provide functionality relevant to the version of Trilead
* available in the current executing instance of Jenkins.
* @return an instance of TrileadVersionSupport that provides functionality relevant for the version of Trilead
* currently on the classpath
*/
static TrileadVersionSupport getTrileadSupport() {
try {
if (isAfterTrilead8()) {
return createVersion9Instance();
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Could not create Trilead support class. Using legacy Trilead features", e);
}
// We're on an old version of Triilead or couldn't create a new handler, fall back to legacy trilead handler
return new LegacyTrileadVersionSupport();
}

private static boolean isAfterTrilead8() {
try {
Thread.currentThread().getContextClassLoader().loadClass("com.trilead.ssh2.signature.KeyAlgorithmManager");
} catch (ClassNotFoundException ex) {
return false;
}
return true;
}

private static TrileadVersionSupport createVersion9Instance() throws ReflectiveOperationException {
return (TrileadVersionSupport) Thread.currentThread().getContextClassLoader()
.loadClass("hudson.plugins.sshslaves.verifiers.JenkinsTrilead9VersionSupport").newInstance();

}

public abstract static class TrileadVersionSupport {

@Restricted(NoExternalUse.class)
/*package*/ TrileadVersionSupport() {
super();
}

/**
* Returns an array of all Key algorithms supported by Yrilead, e.g. ssh-rsa, ssh-dsa, ssh-eds25519
* @return an array containing all the key algorithms the version of Trilead in use can support.
*/
public abstract String[] getSupportedAlgorithms();

/**
* Parses a raw key into a {@link HostKey} for later storage or comparison.
* @param algorithm the algorithm the key has been generated with, e.h. ssh-rsa, ssh-dss, ssh-ed25519
* @param keyValue the value of the key, typically encoded in PEM format.
* @return the input key in a format that can be compared to other keys
* @throws KeyParseException on any failure parsing the key, such as an unknown algorithm or invalid keyValue
*/
public abstract HostKey parseKey(String algorithm, byte[] keyValue) throws KeyParseException;
}

private static class LegacyTrileadVersionSupport extends TrileadVersionSupport {

@Override
public String[] getSupportedAlgorithms() {
return new String[]{"ssh-rsa", "ssh-dss"};
}

@Override
public HostKey parseKey(String algorithm, byte[] keyValue) throws KeyParseException {
try {
if ("ssh-rsa".equals(algorithm)) {
RSASHA1Verify.decodeSSHRSAPublicKey(keyValue);
} else if ("ssh-dss".equals(algorithm)) {
DSASHA1Verify.decodeSSHDSAPublicKey(keyValue);
} else {
throw new KeyParseException("Key algorithm should be one of ssh-rsa or ssh-dss");
}
} catch (IOException | StringIndexOutOfBoundsException ex) {
throw new KeyParseException(Messages.ManualKeyProvidedHostKeyVerifier_KeyValueDoesNotParse(algorithm), ex);
}

return new HostKey(algorithm, keyValue);
}
}

}

0 comments on commit 67bbbff

Please sign in to comment.