+ * This method starts two-way byte channel that allows the client and the server to perform authentication.
+ * The current supported implementation is based on SSH public key authentication that mutually authenticates
+ * clients and servers.
+ *
+ * @param protocol
+ * Currently only "ssh" is supported.
+ * @throws UnsupportedOperationException
+ * If the specified protocol is not supported by the server.
+ */
+ void authenticate(String protocol, Pipe c2s, Pipe s2c);
+
int VERSION = 1;
}
diff --git a/cli/src/main/java/hudson/cli/Connection.java b/cli/src/main/java/hudson/cli/Connection.java
new file mode 100644
index 0000000000000000000000000000000000000000..543e6acb3e1d504bbf353156ebee6e9bb9e0b196
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/Connection.java
@@ -0,0 +1,211 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2011, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.cli;
+
+import hudson.remoting.SocketInputStream;
+import hudson.remoting.SocketOutputStream;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.KeyAgreement;
+import javax.crypto.interfaces.DHPublicKey;
+import javax.crypto.spec.DHParameterSpec;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.security.AlgorithmParameterGenerator;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.X509EncodedKeySpec;
+
+public class Connection {
+ public final InputStream in;
+ public final OutputStream out;
+
+ public final DataInputStream din;
+ public final DataOutputStream dout;
+
+ public Connection(Socket socket) throws IOException {
+ this(new SocketInputStream(socket),new SocketOutputStream(socket));
+ }
+
+ public Connection(InputStream in, OutputStream out) {
+ this.in = in;
+ this.out = out;
+ this.din = new DataInputStream(in);
+ this.dout = new DataOutputStream(out);
+ }
+
+//
+//
+// Convenience methods
+//
+//
+ public void writeUTF(String msg) throws IOException {
+ dout.writeUTF(msg);
+ }
+
+ public String readUTF() throws IOException {
+ return din.readUTF();
+ }
+
+ public void writeBoolean(boolean b) throws IOException {
+ dout.writeBoolean(b);
+ }
+
+ public boolean readBoolean() throws IOException {
+ return din.readBoolean();
+ }
+
+ /**
+ * Sends a serializable object.
+ */
+ public void writeObject(Object o) throws IOException {
+ ObjectOutputStream oos = new ObjectOutputStream(out);
+ oos.writeObject(o);
+ // don't close oss, which will close the underlying stream
+ // no need to flush either, given the way oos is implemented
+ }
+
+ /**
+ * Receives an object sent by {@link #writeObject(Object)}
+ */
+ public
+ * DH is also useful as a coin-toss algorithm. Two parties get the same random number without trusting
+ * each other.
+ */
+ public KeyAgreement diffieHellman(boolean side) throws IOException, GeneralSecurityException {
+ KeyPair keyPair;
+ PublicKey otherHalf;
+
+ if (side) {
+ AlgorithmParameterGenerator paramGen = AlgorithmParameterGenerator.getInstance("DH");
+ paramGen.init(512);
+
+ KeyPairGenerator dh = KeyPairGenerator.getInstance("DH");
+ dh.initialize(paramGen.generateParameters().getParameterSpec(DHParameterSpec.class));
+ keyPair = dh.generateKeyPair();
+
+ // send a half and get a half
+ writeKey(keyPair.getPublic());
+ otherHalf = KeyFactory.getInstance("DH").generatePublic(readKey());
+ } else {
+ otherHalf = KeyFactory.getInstance("DH").generatePublic(readKey());
+
+ KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("DH");
+ keyPairGen.initialize(((DHPublicKey) otherHalf).getParams());
+ keyPair = keyPairGen.generateKeyPair();
+
+ // send a half and get a half
+ writeKey(keyPair.getPublic());
+ }
+
+ KeyAgreement ka = KeyAgreement.getInstance("DH");
+ ka.init(keyPair.getPrivate());
+ ka.doPhase(otherHalf, true);
+
+ return ka;
+ }
+
+ private String detectKeyAlgorithm(KeyPair kp) {
+ return detectKeyAlgorithm(kp.getPublic());
+ }
+
+ private String detectKeyAlgorithm(PublicKey kp) {
+ if (kp instanceof RSAPublicKey) return "RSA";
+ if (kp instanceof DSAPublicKey) return "DSA";
+ throw new IllegalArgumentException("Unknown public key type: "+kp);
+ }
+
+ /**
+ * Used in conjunction with {@link #verifyIdentity(byte[])} to prove
+ * that we actually own the private key of the given key pair.
+ */
+ public void proveIdentity(byte[] sharedSecret, KeyPair key) throws IOException, GeneralSecurityException {
+ String algorithm = detectKeyAlgorithm(key);
+ writeUTF(algorithm);
+ writeKey(key.getPublic());
+
+ Signature sig = Signature.getInstance("SHA1with"+algorithm);
+ sig.initSign(key.getPrivate());
+ sig.update(key.getPublic().getEncoded());
+ sig.update(sharedSecret);
+ writeObject(sig.sign());
+ }
+
+ /**
+ * Verifies that we are talking to a peer that actually owns the private key corresponding to the public key we get.
+ */
+ public PublicKey verifyIdentity(byte[] sharedSecret) throws IOException, GeneralSecurityException {
+ try {
+ String serverKeyAlgorithm = readUTF();
+ PublicKey spk = KeyFactory.getInstance(serverKeyAlgorithm).generatePublic(readKey());
+
+ // verify the identity of the server
+ Signature sig = Signature.getInstance("SHA1with"+serverKeyAlgorithm);
+ sig.initVerify(spk);
+ sig.update(spk.getEncoded());
+ sig.update(sharedSecret);
+ sig.verify((byte[]) readObject());
+
+ return spk;
+ } catch (ClassNotFoundException e) {
+ throw new Error(e); // impossible
+ }
+ }
+
+ public void close() throws IOException {
+ in.close();
+ out.close();
+ }
+}
diff --git a/cli/src/main/resources/hudson/cli/client/Messages.properties b/cli/src/main/resources/hudson/cli/client/Messages.properties
index 7da44d342b111f77e426b715af445ab60b59fdce..327b04a6f436150d8688e9d335909b81c32cf360 100644
--- a/cli/src/main/resources/hudson/cli/client/Messages.properties
+++ b/cli/src/main/resources/hudson/cli/client/Messages.properties
@@ -7,3 +7,4 @@ CLI.Usage=Jenkins CLI\n\
see the list.
CLI.NoURL=Neither -s nor the JENKINS_URL env var is specified.
CLI.VersionMismatch=Version mismatch. This CLI cannot work with this Hudson server
+CLI.NoSuchFileExists=No such file exists: {0}
diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java
index 2f59a91e0dbcb2a8bd5a9212e80b9b83ea74b5ce..ab3b163754e28ca176d756d1a933cecbb58d1d2d 100644
--- a/core/src/main/java/hudson/TcpSlaveAgentListener.java
+++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java
@@ -217,7 +217,7 @@ public final class TcpSlaveAgentListener extends Thread {
Computer.threadPoolForRemoting, Mode.BINARY,
new BufferedInputStream(new SocketInputStream(this.s)),
new BufferedOutputStream(new SocketOutputStream(this.s)), null, true);
- channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl());
+ channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel));
channel.join();
}
diff --git a/core/src/main/java/hudson/cli/CliManagerImpl.java b/core/src/main/java/hudson/cli/CliManagerImpl.java
index 3f8e0b3c145fcb2abe85bb7d6ea6687711a669eb..8bef506b45b9773e2854e2d3f1a42d1265ff54c1 100644
--- a/core/src/main/java/hudson/cli/CliManagerImpl.java
+++ b/core/src/main/java/hudson/cli/CliManagerImpl.java
@@ -23,7 +23,10 @@
*/
package hudson.cli;
+import hudson.model.User;
import hudson.remoting.Channel;
+import hudson.remoting.Pipe;
+import hudson.util.IOUtils;
import jenkins.model.Jenkins;
import org.apache.commons.discovery.resource.ClassLoaders;
import org.apache.commons.discovery.resource.classes.DiscoverClasses;
@@ -34,6 +37,10 @@ import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.CmdLineParser;
import org.jvnet.tiger_types.Types;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PublicKey;
import java.util.List;
import java.util.Locale;
import java.util.Collections;
@@ -41,6 +48,10 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Serializable;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.*;
/**
* {@link CliEntryPoint} implementation exposed to the remote CLI.
@@ -48,7 +59,10 @@ import java.io.Serializable;
* @author Kohsuke Kawaguchi
*/
public class CliManagerImpl implements CliEntryPoint, Serializable {
- public CliManagerImpl() {
+ private final Channel channel;
+
+ public CliManagerImpl(Channel channel) {
+ this.channel = channel;
}
public int main(List
+ * Implementing this extension point requires changes in the CLI module, as during authentication
+ * neither side trusts each other enough to start code-transfer. But it does allow us to
+ * use different implementations of the same protocol.
+ *
+ *
+ * Starting 1.419, CLI supports SSH public key based client/server mutual authentication.
+ * The protocol name of this is "ssh".
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.419
+ */
+public abstract class CliTransportAuthenticator implements ExtensionPoint {
+ /**
+ * Checks if this implementation supports the specified protocol.
+ *
+ * @param protocol
+ * Identifier. CLI.jar is hard-coded with the built-in knowledge about a specific protocol.
+ * @return
+ * true if this implementation supports the specified protocol,
+ * in which case {@link #authenticate(String, Channel, Connection)} would be called next.
+ */
+ public abstract boolean supportsProtocol(String protocol);
+
+ /**
+ * Performs authentication.
+ *
+ *
+ * The authentication
+ *
+ * @param protocol
+ * Protocol identifier that {@link #supportsProtocol(String)} returned true.
+ * @param channel
+ * Communication channel to the client.
+ * @param con
+ */
+ public abstract void authenticate(String protocol, Channel channel, Connection con);
+
+ public static ExtensionListCurrent Implementations
+ *