diff options
author | Gunnar Wagenknecht | 2011-12-13 13:32:53 +0000 |
---|---|---|
committer | Lazar Kirchev | 2011-12-13 13:53:42 +0000 |
commit | 4b2ce1a69ac4fb93921548acc8243889c19ecc96 (patch) | |
tree | 9878a2e11d787a33d673b5b1f114c8292669a46a /bundles/org.eclipse.equinox.console.ssh | |
parent | 0c431ddd8c9c2db4f3b12f15a899f9b15403f42a (diff) | |
download | rt.equinox.bundles-4b2ce1a69ac4fb93921548acc8243889c19ecc96.tar.gz rt.equinox.bundles-4b2ce1a69ac4fb93921548acc8243889c19ecc96.tar.xz rt.equinox.bundles-4b2ce1a69ac4fb93921548acc8243889c19ecc96.zip |
Bug 366188 - Support for authorized_keys filev20111213-1353
If the system property 'ssh.server.authorized_keys' is specified it
may point to a file which contains a list of keys that are allowed to
connect to the SSH console. The file will be read every time a
connection is made so that it can be modified dynamically at runtime.
Diffstat (limited to 'bundles/org.eclipse.equinox.console.ssh')
3 files changed, 281 insertions, 10 deletions
diff --git a/bundles/org.eclipse.equinox.console.ssh/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.console.ssh/META-INF/MANIFEST.MF index 917ea0a7d..9de3d86c6 100755..100644 --- a/bundles/org.eclipse.equinox.console.ssh/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.equinox.console.ssh/META-INF/MANIFEST.MF @@ -13,6 +13,7 @@ Import-Package: javax.security.auth;resolution:=optional, javax.security.auth.login;resolution:=optional, javax.security.auth.spi;resolution:=optional, org.apache.felix.service.command;status=provisional;version="0.8.0", + org.apache.mina.util;version="2.0.0";resolution:=optional, org.apache.sshd;version="0.5.0";resolution:=optional, org.apache.sshd.common;version="0.5.0";resolution:=optional, org.apache.sshd.server;version="0.5.0";resolution:=optional, diff --git a/bundles/org.eclipse.equinox.console.ssh/src/org/eclipse/equinox/console/internal/ssh/AuthorizedKeys.java b/bundles/org.eclipse.equinox.console.ssh/src/org/eclipse/equinox/console/internal/ssh/AuthorizedKeys.java new file mode 100644 index 000000000..1e3284d14 --- /dev/null +++ b/bundles/org.eclipse.equinox.console.ssh/src/org/eclipse/equinox/console/internal/ssh/AuthorizedKeys.java @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2011 Gunnar Wagenknecht and others. + * All rights reserved. + * + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v1.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Gunnar Wagenknecht - initial API and implementation + */ +package org.eclipse.equinox.console.internal.ssh; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Scanner; + +import org.apache.mina.util.Base64; + +/** + * Reader for 'authorized_keys' file as typically found on Unix systems. + */ +public class AuthorizedKeys { + + public static enum KeyType { + RSA, DSA + } + + public static class ParseKeyException extends IOException { + + /** serialVersionUID */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new instance. + * + * @param message + * @param cause + */ + public ParseKeyException(final String message, final Throwable cause) { + super(message, cause); + } + + } + + private static final String PREFIX_KEY_TYPE = "ssh-"; + private static final String PREFIX_KEY_TYPE_DSA = "ssh-dsa "; + private static final String PREFIX_KEY_TYPE_RSA = "ssh-rsa "; + private static final String NEWLINE = "\n"; + + private static byte[] asBytes(final String string) { + try { + return string.getBytes("UTF-8"); + } catch (final UnsupportedEncodingException e) { + return string.getBytes(); + } + } + + private static String asString(final byte[] bytes) { + try { + return new String(bytes, "UTF-8"); + } catch (final UnsupportedEncodingException e) { + return new String(bytes); + } + } + + public static void main(final String[] args) { + try { + final List<PublicKey> keys = new AuthorizedKeys(args[0]).getKeys(); + for (final PublicKey key : keys) { + System.out.println(key); + } + } catch (final Exception e) { + e.printStackTrace(); + } + } + + private final List<PublicKey> keys; + + /** + * Creates a new instance. + * + * @throws FileNotFoundException + */ + public AuthorizedKeys(final String authorizedKeysFile) throws FileNotFoundException, IOException { + // read file line-by-line + final File file = new File(authorizedKeysFile); + final Scanner scanner = new Scanner(file); + scanner.useDelimiter(NEWLINE); + int lineNumber = 0; + final List<PublicKey> keys = new ArrayList<PublicKey>(); + + try { + while (scanner.hasNext()) { + lineNumber++; + + // get line (without leading and trailing blanks) + final String line = scanner.next().trim(); + + // ignore blank line and comments + if ((line.length() == 0) || (line.charAt(0) == '#')) { + continue; + } + + // read key + try { + keys.add(readPublicKey(line)); + } catch (final Exception e) { + throw new ParseKeyException("Line " + lineNumber + ": " + e.getMessage(), e); + } + } + } finally { + scanner.close(); + } + + this.keys = Collections.unmodifiableList(keys); + } + + /** + * Returns the keys. + * + * @return the keys + */ + public List<PublicKey> getKeys() { + return keys; + } + + private BigInteger readBigInteger(final ByteBuffer buffer) { + final int len = buffer.getInt(); + final byte[] bytes = new byte[len]; + buffer.get(bytes); + final BigInteger pubExp = new BigInteger(bytes); + return pubExp; + } + + private PublicKey readPublicKey(String line) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { + // [options] <type> <base64> <comment> + final KeyType type; + final byte[] key; + + // skip options (if any) + if (!line.startsWith(PREFIX_KEY_TYPE)) { + final int keyTypeStart = line.indexOf(PREFIX_KEY_TYPE); + if (keyTypeStart == -1) { + throw new IOException("missing key type"); + } + line = line.substring(keyTypeStart); + } + + // key type + if (line.startsWith(PREFIX_KEY_TYPE_DSA)) { + line = line.substring(PREFIX_KEY_TYPE_DSA.length()); + type = KeyType.DSA; + } else if (line.startsWith(PREFIX_KEY_TYPE_RSA)) { + line = line.substring(PREFIX_KEY_TYPE_RSA.length()); + type = KeyType.RSA; + } else { + throw new IOException("unsupported key type"); + } + + // key + final int keyEndIdx = line.indexOf(' '); + if (keyEndIdx != -1) { + key = Base64.decodeBase64(asBytes(line.substring(0, keyEndIdx))); + line = line.substring(keyEndIdx + 1); + } else { + key = Base64.decodeBase64(asBytes(line)); + } + + // wrap key into byte buffer + final ByteBuffer buffer = ByteBuffer.wrap(key); + + // skip key type + readString(buffer); + + // parse key + switch (type) { + case RSA: + // exponent + modulus + final BigInteger pubExp = readBigInteger(buffer); + final BigInteger mod = readBigInteger(buffer); + return KeyFactory.getInstance(KeyType.RSA.name()).generatePublic(new RSAPublicKeySpec(mod, pubExp)); + case DSA: + // p + q+ g + y + final BigInteger p = readBigInteger(buffer); + final BigInteger q = readBigInteger(buffer); + final BigInteger g = readBigInteger(buffer); + final BigInteger y = readBigInteger(buffer); + return KeyFactory.getInstance(KeyType.DSA.name()).generatePublic(new DSAPublicKeySpec(y, p, q, g)); + default: + throw new IOException("not implemented: " + type); + } + } + + private String readString(final ByteBuffer buffer) { + final int len = buffer.getInt(); + final byte[] bytes = new byte[len]; + buffer.get(bytes); + return asString(bytes); + } +} diff --git a/bundles/org.eclipse.equinox.console.ssh/src/org/eclipse/equinox/console/ssh/SshServ.java b/bundles/org.eclipse.equinox.console.ssh/src/org/eclipse/equinox/console/ssh/SshServ.java index 088a31f5a..09276511c 100755..100644 --- a/bundles/org.eclipse.equinox.console.ssh/src/org/eclipse/equinox/console/ssh/SshServ.java +++ b/bundles/org.eclipse.equinox.console.ssh/src/org/eclipse/equinox/console/ssh/SshServ.java @@ -4,22 +4,31 @@ * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html - * + * * Contributors: * Lazar Kirchev, SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.equinox.console.ssh; +import java.io.FileNotFoundException; import java.io.IOException; +import java.security.PublicKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPublicKey; import java.util.List; +import org.eclipse.equinox.console.internal.ssh.AuthorizedKeys; + +import org.osgi.framework.BundleContext; + import org.apache.felix.service.command.CommandProcessor; import org.apache.sshd.SshServer; import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.jaas.JaasPasswordAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; -import org.osgi.framework.BundleContext; +import org.apache.sshd.server.session.ServerSession; /** * This class configures and start an ssh server @@ -30,17 +39,18 @@ public class SshServ extends Thread { private String host; private SshServer sshServer = null; private SshShellFactory shellFactory = null; - + private static final String SSH_KEYSTORE_PROP = "ssh.server.keystore"; private static final String SSH_KEYSTORE_PROP_DEFAULT = "hostkey.ser"; + private static final String SSH_AUTHORIZED_KEYS_FILE_PROP = "ssh.server.authorized_keys"; private static final String EQUINOX_CONSOLE_DOMAIN = "equinox_console"; - + public SshServ(List<CommandProcessor> processors, BundleContext context, String host, int port) { this.host = host; this.port = port; shellFactory = new SshShellFactory(processors, context); } - + public void run() throws RuntimeException { sshServer = SshServer.setUpDefaultServer(); if (host != null) { @@ -50,32 +60,78 @@ public class SshServ extends Thread { sshServer.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(System.getProperty(SSH_KEYSTORE_PROP, SSH_KEYSTORE_PROP_DEFAULT))); sshServer.setShellFactory(shellFactory); sshServer.setPasswordAuthenticator(createJaasPasswordAuthenticator()); + sshServer.setPublickeyAuthenticator(createSimpleAuthorizedKeysAuthenticator()); try { sshServer.start(); } catch (IOException e) { e.printStackTrace(); } } - - public synchronized void stopSshServer() { + + + public synchronized void stopSshServer() { try { sshServer.stop(true); } catch (InterruptedException e) { e.printStackTrace(); } } - + public synchronized void addCommandProcessor(CommandProcessor processor) { shellFactory.addCommandProcessor(processor); } - + public synchronized void removeCommandProcessor(CommandProcessor processor) { shellFactory.removeCommandProcessor(processor); } - + private PasswordAuthenticator createJaasPasswordAuthenticator() { JaasPasswordAuthenticator jaasPasswordAuthenticator = new JaasPasswordAuthenticator(); jaasPasswordAuthenticator.setDomain(EQUINOX_CONSOLE_DOMAIN); return jaasPasswordAuthenticator; } + + private PublickeyAuthenticator createSimpleAuthorizedKeysAuthenticator() { + // check if property is set + final String authorizedKeysFile = System.getProperty(SSH_AUTHORIZED_KEYS_FILE_PROP); + if (null == authorizedKeysFile) + return null; + + // dynamically read key file at each login attempt + return new PublickeyAuthenticator() { + public boolean authenticate(String username, PublicKey key, ServerSession session) { + try { + AuthorizedKeys keys = new AuthorizedKeys(authorizedKeysFile); + for (PublicKey authorizedKey : keys.getKeys()) { + if (isSameKey(authorizedKey, key)) { + return true; + } + } + } catch (FileNotFoundException e) { + System.err.println("Configured authorized_keys file not found! " + e.getMessage()); + } catch (IOException e) { + System.err.println("Please check authorized_keys file! " + e.getMessage()); + } + return false; + } + + private boolean isSameKey(PublicKey k1, PublicKey k2) throws IOException { + if ((k1 instanceof DSAPublicKey) && (k2 instanceof DSAPublicKey)) { + return isSameDSAKey((DSAPublicKey) k1, (DSAPublicKey) k2); + } else if ((k1 instanceof RSAPublicKey) && (k2 instanceof RSAPublicKey)) { + return isSameRSAKey((RSAPublicKey) k1, (RSAPublicKey) k2); + } else { + throw new IOException("Unsupported key types detected!"); + } + } + + private boolean isSameRSAKey(RSAPublicKey k1, RSAPublicKey k2) { + return k1.getPublicExponent().equals(k2.getPublicExponent()) && k1.getModulus().equals(k2.getModulus()); + } + + private boolean isSameDSAKey(DSAPublicKey k1, DSAPublicKey k2) { + return k1.getY().equals(k2.getY()) && k1.getParams().getG().equals(k2.getParams().getG()) && k1.getParams().getP().equals(k2.getParams().getP()) && k1.getParams().getQ().equals(k2.getParams().getQ()); + } + }; + } } |