提交 0d4117e4 编写于 作者: K Kohsuke Kawaguchi

Implemented SecurityRealm-independent authentication mechanism,

and an implementation around SSH keys.

changelog will be updated when the server-side authentication module
is added.
上级 2109e28d
......@@ -72,5 +72,10 @@
<artifactId>localizer</artifactId>
<version>1.10</version>
</dependency>
<dependency>
<groupId>org.jvnet.hudson</groupId>
<artifactId>trilead-ssh2</artifactId>
<version>build212-hudson-5</version>
</dependency>
</dependencies>
</project>
......@@ -23,30 +23,46 @@
*/
package hudson.cli;
import com.trilead.ssh2.crypto.Base64;
import com.trilead.ssh2.crypto.PEMDecoder;
import com.trilead.ssh2.signature.DSASHA1Verify;
import com.trilead.ssh2.signature.RSASHA1Verify;
import hudson.cli.client.Messages;
import hudson.remoting.Channel;
import hudson.remoting.PingThread;
import hudson.remoting.Pipe;
import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.PingThread;
import hudson.remoting.SocketInputStream;
import hudson.remoting.SocketOutputStream;
import hudson.cli.client.Messages;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.DataOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.util.logging.Logger;
import static java.util.logging.Level.*;
/**
* CLI entry point to Jenkins.
......@@ -139,7 +155,7 @@ public class CLI {
}
public int execute(List<String> args) {
return execute(args,System.in,System.out,System.err);
return execute(args, System.in, System.out, System.err);
}
public int execute(String... args) {
......@@ -154,7 +170,13 @@ public class CLI {
}
public static void main(final String[] _args) throws Exception {
System.exit(_main(_args));
}
public static int _main(String[] _args) throws Exception {
List<String> args = Arrays.asList(_args);
List<KeyPair> candidateKeys = new ArrayList<KeyPair>();
boolean sshAuthRequestedExplicitly = false;
String url = System.getenv("JENKINS_URL");
......@@ -168,32 +190,169 @@ public class CLI {
args = args.subList(2,args.size());
continue;
}
if(head.equals("-i") && args.size()>=2) {
File f = new File(args.get(1));
if (!f.exists()) {
printUsage(Messages.CLI_NoSuchFileExists(f));
return -1;
}
try {
candidateKeys.add(loadKey(f));
} catch (IOException e) {
throw new Exception("Failed to load key: "+f,e);
} catch (GeneralSecurityException e) {
throw new Exception("Failed to load key: "+f,e);
}
args = args.subList(2,args.size());
sshAuthRequestedExplicitly = true;
continue;
}
break;
}
if(url==null) {
printUsageAndExit(Messages.CLI_NoURL());
return;
printUsage(Messages.CLI_NoURL());
return -1;
}
if(args.isEmpty())
args = Arrays.asList("help"); // default to help
if (candidateKeys.isEmpty())
addDefaultPrivateKeyLocations(candidateKeys);
CLI cli = new CLI(new URL(url));
try {
if (!candidateKeys.isEmpty()) {
try {
// TODO: server verification
cli.authenticate(candidateKeys);
} catch (UnsupportedOperationException e) {
if (sshAuthRequestedExplicitly) {
System.err.println("The server doesn't support public key authentication");
return -1;
}
} catch (GeneralSecurityException e) {
System.err.println("Failed to authenticate with your SSH keys. Proceeding with anonymous access");
LOGGER.log(FINE,"Failed to authenticate with your SSH keys. Proceeding with anonymous access",e);
}
}
// execute the command
// Arrays.asList is not serializable --- see 6835580
args = new ArrayList<String>(args);
System.exit(cli.execute(args, System.in, System.out, System.err));
return cli.execute(args, System.in, System.out, System.err);
} finally {
cli.close();
}
}
private static void printUsageAndExit(String msg) {
private static KeyPair loadKey(File f) throws IOException, GeneralSecurityException {
DataInputStream dis = new DataInputStream(new FileInputStream(f));
byte[] bytes = new byte[(int) f.length()];
dis.readFully(bytes);
dis.close();
return loadKey(new String(bytes));
}
public static KeyPair loadKey(String pemString) throws IOException, GeneralSecurityException {
Object key = PEMDecoder.decode(pemString.toCharArray(), null);
if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) {
com.trilead.ssh2.signature.RSAPrivateKey x = (com.trilead.ssh2.signature.RSAPrivateKey)key;
System.out.println("ssh-rsa " + new String(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey(x.getPublicKey()))));
// this doesn't work
// System.out.println("ssh-rsa " + new String(Base64.encode(x.toJCEKeyPair().getPublic().getEncoded())));
return x.toJCEKeyPair();
}
if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) {
com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key;
KeyFactory kf = KeyFactory.getInstance("DSA");
System.out.println("ssh-dsa " + new String(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey(x.getPublicKey()))));
return new KeyPair(
kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())),
kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG())));
}
throw new UnsupportedOperationException("Unrecognizable key format: "+key);
}
/**
* try all the default key locations
*/
private static void addDefaultPrivateKeyLocations(List<KeyPair> keyFileCandidates) {
File home = new File(System.getProperty("user.home"));
for (String path : new String[]{".ssh/id_rsa",".ssh/id_dsa",".ssh/identity"}) {
File key = new File(home,path);
if (key.exists()) {
try {
keyFileCandidates.add(loadKey(key));
} catch (IOException e) {
// don't report an error. the user can still see it by using the -i option
LOGGER.log(FINE, "Failed to load "+key,e);
} catch (GeneralSecurityException e) {
LOGGER.log(FINE, "Failed to load " + key, e);
}
}
}
}
/**
* Authenticate ourselves against the server.
*
* @return
* identity of the server represented as a public key.
*/
public PublicKey authenticate(Iterable<KeyPair> privateKeys) throws IOException, GeneralSecurityException {
Pipe c2s = Pipe.createLocalToRemote();
Pipe s2c = Pipe.createRemoteToLocal();
entryPoint.authenticate("ssh",c2s, s2c);
Connection c = new Connection(s2c.getIn(), c2s.getOut());
try {
byte[] sharedSecret = c.diffieHellman(false).generateSecret();
PublicKey serverIdentity = c.verifyIdentity(sharedSecret);
// try all the public keys
for (KeyPair key : privateKeys) {
c.proveIdentity(sharedSecret,key);
if (c.readBoolean())
return serverIdentity; // succeeded
}
if (privateKeys.iterator().hasNext())
throw new GeneralSecurityException("Authentication failed. No private key accepted.");
else
throw new GeneralSecurityException("No private key is available for use in authentication");
} finally {
c.close();
}
}
private static KeyPair readKeyFile(File f) throws IOException, GeneralSecurityException {
DataInputStream dis = new DataInputStream(new FileInputStream(f));
byte[] b = new byte[(int) f.length()];
dis.readFully(b);
dis.close();
Object key = PEMDecoder.decode(new String(b).toCharArray(), null);
if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) {
return ((com.trilead.ssh2.signature.RSAPrivateKey)key).toJCEKeyPair();
}
if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) {
com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key;
KeyFactory kf = KeyFactory.getInstance("DSA");
return new KeyPair(
kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())),
kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG())));
}
throw new UnsupportedOperationException("Unrecognizable key format: "+key);
}
private static void printUsage(String msg) {
if(msg!=null) System.out.println(msg);
System.err.println(Messages.CLI_Usage());
System.exit(-1);
}
private static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
......
......@@ -23,6 +23,8 @@
*/
package hudson.cli;
import hudson.remoting.Pipe;
import java.io.OutputStream;
import java.io.InputStream;
import java.util.List;
......@@ -53,5 +55,19 @@ public interface CliEntryPoint {
*/
int protocolVersion();
/**
* Initiates authentication out of band.
* <p>
* 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;
}
/*
* 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 <T> T readObject() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(in);
return (T)ois.readObject();
}
public void writeKey(Key key) throws IOException {
writeUTF(new String(Base64.encodeBase64(key.getEncoded())));
}
public X509EncodedKeySpec readKey() throws IOException {
byte[] otherHalf = Base64.decodeBase64(readUTF());
return new X509EncodedKeySpec(otherHalf);
}
/**
* Performs a Diffie-Hellman key exchange and produce a common secret between two ends of the connection.
*
* <p>
* 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();
}
}
......@@ -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}
......@@ -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();
}
......
......@@ -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<String> args, Locale locale, InputStream stdin, OutputStream stdout, OutputStream stderr) {
......@@ -76,6 +90,21 @@ public class CliManagerImpl implements CliEntryPoint, Serializable {
return -1;
}
public void authenticate(final String protocol, final Pipe c2s, final Pipe s2c) {
for (final CliTransportAuthenticator cta : CliTransportAuthenticator.all()) {
if (cta.supportsProtocol(protocol)) {
new Thread() {
@Override
public void run() {
cta.authenticate(protocol,channel,new Connection(c2s.getIn(), s2c.getOut()));
}
}.start();
return;
}
}
throw new UnsupportedOperationException("Unsupported authentication protocol: "+protocol);
}
public boolean hasCommand(String name) {
return CLICommand.clone(name)!=null;
}
......@@ -104,4 +133,6 @@ public class CliManagerImpl implements CliEntryPoint, Serializable {
CmdLineParser.registerHandler(c,h);
}
}
private static final Logger LOGGER = Logger.getLogger(CliManagerImpl.class.getName());
}
package hudson.cli;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.remoting.Channel;
import hudson.security.SecurityRealm;
import jenkins.model.Jenkins;
/**
* Perform {@link SecurityRealm} independent authentication.
*
* <p>
* 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.
*
* <h2>Current Implementations</h2>
* <p>
* 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.
*
* <p>
* 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 ExtensionList<CliTransportAuthenticator> all() {
return Jenkins.getInstance().getExtensionList(CliTransportAuthenticator.class);
}
}
......@@ -36,13 +36,18 @@ import hudson.model.listeners.SaveableListener;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.security.SecurityRealm;
import hudson.util.RunList;
import hudson.util.XStream2;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.acegisecurity.userdetails.UserDetails;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
......@@ -236,6 +241,20 @@ public class User extends AbstractModelObject implements AccessControlled, Savea
return null;
}
/**
* Creates an {@link Authentication} object that represents this user.
*/
public Authentication impersonate() {
try {
UserDetails u = Jenkins.getInstance().getSecurityRealm().loadUserByUsername(id);
return new UsernamePasswordAuthenticationToken(u.getUsername(), u.getPassword(), u.getAuthorities());
} catch (AuthenticationException e) {
// TODO: use the stored GrantedAuthorities
return new UsernamePasswordAuthenticationToken(id, "",
new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY});
}
}
/**
* Accepts the new description.
*/
......
......@@ -2836,7 +2836,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableItemGroup<TopLe
protected void main(Channel channel) throws IOException, InterruptedException {
// capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator()
channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION,getAuthentication());
channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl());
channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel));
}
});
try {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册