Index: src/test/java/org/apache/karaf/shell/ssh/TestAuthorizedKeysParsing.java =================================================================== --- src/test/java/org/apache/karaf/shell/ssh/TestAuthorizedKeysParsing.java (Revision 0) +++ src/test/java/org/apache/karaf/shell/ssh/TestAuthorizedKeysParsing.java (Revision 0) @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.karaf.shell.ssh; + +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Map; + +import junit.framework.TestCase; + +import org.apache.karaf.shell.ssh.KarafPublickeyAuthenticator.AuthorizedKey; + +/** + * Test parsing an authorized_keys file. + */ +public class TestAuthorizedKeysParsing extends TestCase { + + public void testAuthorizedKeysParsing() throws NoSuchAlgorithmException, InvalidKeySpecException, IOException { + + InputStream is = + TestAuthorizedKeysParsing.class.getClassLoader().getResourceAsStream("org/apache/karaf/shell/ssh/authorized_keys"); + + Map keys = KarafPublickeyAuthenticator.parseAuthorizedKeys(is); + + assertEquals(2,keys.size()); + + for (Map.Entry e : keys.entrySet()) { + + assertSame(e.getKey(),e.getValue().getPubKey()); + assertTrue("ssh-dss".equals(e.getValue().getFormat()) || "ssh-rsa".equals(e.getValue().getFormat())); + + if ("ssh-dss".equals(e.getValue().getFormat())) { + assertTrue(e.getKey() instanceof DSAPublicKey); + assertEquals("dsa-test",e.getValue().getAlias()); + } + if ("ssh-rsa".equals(e.getValue().getFormat())) { + assertTrue(e.getKey() instanceof RSAPublicKey); + assertEquals("rsa-test",e.getValue().getAlias()); + } + } + } +} Eigenschaftsänderungen: src/test/java/org/apache/karaf/shell/ssh/TestAuthorizedKeysParsing.java ___________________________________________________________________ Hinzugefügt: svn:keywords + Id Hinzugefügt: svn:eol-style + native Index: src/test/resources/org/apache/karaf/shell/ssh/authorized_keys =================================================================== --- src/test/resources/org/apache/karaf/shell/ssh/authorized_keys (Revision 0) +++ src/test/resources/org/apache/karaf/shell/ssh/authorized_keys (Revision 0) @@ -0,0 +1,2 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxaehvx5wOJ0rzZdzoSOXxbETW6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7StxyltHnXF1YLfKD1G4T6JYrdHYI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3gJq2e7Yisk/gF+1VAAAAFQDb8D5cvwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4KLYk3IwRbXblwXdkPggA4pfdtW9vGfJ0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/FXPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAAvioUPkmdMc0zuWoSOEsSNhVDtX3WdvVcGcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACBAN7CY+KKv1gHpRzFwdQm7HK9bb1LAo2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HSn24VYtYtsMu74qXviYjziVucWKjjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5sY29ouezv4Xz2PuMch5VGPP+CDqzCM4loWgV dsa-test +ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1on8gxCGJJWSRT4uOrR13mUaUk0hRf4RzxSZ1zRbYYFw8pfGesIFoEuVth4HKyF8k1y4mRUnYHP1XNMNMJl1JcEArC2asV8sHf6zSPVffozZ5TT4SfsUu/iKy9lUcCfXzwre4WWZSXXcPff+EHtWshahu3WzBdnGxm5Xoi89zcE= rsa-test Index: src/main/java/org/apache/karaf/shell/ssh/KarafPublickeyAuthenticator.java =================================================================== --- src/main/java/org/apache/karaf/shell/ssh/KarafPublickeyAuthenticator.java (Revision 0) +++ src/main/java/org/apache/karaf/shell/ssh/KarafPublickeyAuthenticator.java (Revision 0) @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.karaf.shell.ssh; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.TreeMap; + +import org.apache.mina.util.Base64; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A public key authenticator, which reads an openssl2 authorized_keys file. + */ +public class KarafPublickeyAuthenticator implements PublickeyAuthenticator { + + private static final Logger log = LoggerFactory.getLogger(KarafPublickeyAuthenticator.class); + + private String authorizedKeys; + private boolean active; + + public static final class AuthorizedKey { + + private final String alias; + private final String format; + private final PublicKey pubKey; + + public AuthorizedKey(String alias, String format, PublicKey pubKey) { + + super(); + this.alias = alias; + this.format = format; + this.pubKey = pubKey; + } + + public String getAlias() { + return alias; + } + + public String getFormat() { + return format; + } + + public PublicKey getPubKey() { + return pubKey; + } + } + + private static final class PublicKeyComparator implements Comparator { + + public int compare(PublicKey a, PublicKey b) { + + if (a instanceof DSAPublicKey) { + if (b instanceof DSAPublicKey) { + + DSAPublicKey da = (DSAPublicKey)a; + DSAPublicKey db = (DSAPublicKey)b; + + int r = da.getParams().getG().compareTo(db.getParams().getG()); + if (r != 0) { + return r; + } + r = da.getParams().getP().compareTo(db.getParams().getP()); + if (r != 0) { + return r; + } + r = da.getParams().getQ().compareTo(db.getParams().getQ()); + if (r != 0) { + return r; + } + return da.getY().compareTo(db.getY()); + } + else { + return -1; + } + } + else if (a instanceof RSAPublicKey) { + if (b instanceof RSAPublicKey) { + + RSAPublicKey da = (RSAPublicKey)a; + RSAPublicKey db = (RSAPublicKey)b; + + int r = da.getPublicExponent().compareTo(db.getPublicExponent()); + if (r != 0) { + return r; + } + return da.getModulus().compareTo(db.getModulus()); + } + else { + return 1; + } + } + else { + throw new IllegalArgumentException("Only RSA and DAS keys are supported."); + } + } + + } + + private final class AuthorizedKeysProvider extends TimerTask { + + private Map keys; + private Long lastModDate; + private Boolean fileAvailable; + + @Override + public void run() { + + try { + File af = new File (KarafPublickeyAuthenticator.this.authorizedKeys); + + if (af.exists()) { + + Long newModDate = Long.valueOf(af.lastModified()); + + if ((this.fileAvailable != null && + !this.fileAvailable.booleanValue()) || + !newModDate.equals(this.lastModDate)) { + + log.info("Parsing authorized keys file [{}]...", + KarafPublickeyAuthenticator.this.authorizedKeys); + + this.fileAvailable = Boolean.TRUE; + this.lastModDate = newModDate; + + Map newKeys = KarafPublickeyAuthenticator.parseAuthorizedKeys(new FileInputStream(af)); + this.setKeys(newKeys); + log.info("Successfully parsed [{}] keys from file [{}].", + newKeys.size(),KarafPublickeyAuthenticator.this.authorizedKeys); + } + } + else { + + if (this.fileAvailable != null && + this.fileAvailable.booleanValue()) { + + log.info("Authorized keys file ["+KarafPublickeyAuthenticator.this.authorizedKeys+"] disappeared, will recheck every minute."); + } + else if (this.fileAvailable == null) { + + log.info("Authorized keys file ["+KarafPublickeyAuthenticator.this.authorizedKeys+"] does not exist, will recheck every minute."); + } + + this.fileAvailable = Boolean.FALSE; + this.lastModDate = null; + this.setKeys(null); + } + + } + catch (Throwable e) { + log.error("Error parsing authorized keys file ["+KarafPublickeyAuthenticator.this.authorizedKeys+"]",e); + this.fileAvailable = Boolean.FALSE; + this.lastModDate = null; + this.setKeys(null); + } + } + + private synchronized void setKeys(Map keys) { + this.keys = keys; + } + + public synchronized AuthorizedKey getKey(PublicKey pk) { + if (this.keys == null) { + return null; + } + else { + return this.keys.get(pk); + } + } + } + + private Timer parseAuthorizedKeysTimer; + private AuthorizedKeysProvider authorizedKeysProvider; + + private static final int getInt(byte[] b, int pos) { + + return (((int)b[pos] & 0xff) << 24) + + (((int)b[pos+1] & 0xff) << 16) + + (((int)b[pos+2] & 0xff) << 8) + + ((int)b[pos+3] & 0xff); + } + + /** + * Parse an authorized_keys file in openssh style. + * @param is The input stream to read. + * @return A map of authorized public keys. + * @throws IOException + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static final Map parseAuthorizedKeys(InputStream is) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + + try { + + Base64 decoder = new Base64(); + + KeyFactory rsaKeyGen = KeyFactory.getInstance("RSA"); + KeyFactory dsaKeyGen = KeyFactory.getInstance("DSA"); + + LineNumberReader reader = new LineNumberReader(new InputStreamReader(is,"UTF-8")); + + Map ret = + new TreeMap(new PublicKeyComparator()); + + String line; + + while ((line = reader.readLine()) != null) { + + String[] tokens = line.split("[ \\t]+",3); + + if (tokens.length != 3) { + throw new IOException("Authorized keys file line ["+reader.getLineNumber()+"] does not contain 3 tokens."); + } + + byte[] rawKey = decoder.decode(tokens[1].getBytes("UTF-8")); + + if (getInt(rawKey,0) != 7 || + !new String(rawKey,4,7,"UTF-8").equals(tokens[0])) { + + throw new IOException("Authorized keys file line ["+reader.getLineNumber()+"] contain a key with a format that does not match the first token."); + } + + PublicKey pk; + + if (tokens[0].equals("ssh-dss")) { + + int pos = 11; + + int n = getInt(rawKey,pos); + pos += 4; + BigInteger p = new BigInteger(1,Arrays.copyOfRange(rawKey,pos,pos+n)); + pos += n; + + n = getInt(rawKey,pos); + pos += 4; + BigInteger q = new BigInteger(1,Arrays.copyOfRange(rawKey,pos,pos+n)); + pos += n; + + n = getInt(rawKey,pos); + pos += 4; + BigInteger g = new BigInteger(1,Arrays.copyOfRange(rawKey,pos,pos+n)); + pos += n; + + n = getInt(rawKey,pos); + pos += 4; + BigInteger y = new BigInteger(1,Arrays.copyOfRange(rawKey,pos,pos+n)); + pos += n; + + if (pos != rawKey.length) { + throw new IOException("Authorized keys file line ["+reader.getLineNumber()+"] contain a DSA key with extra garbage."); + } + + DSAPublicKeySpec ps = new DSAPublicKeySpec(y,p,q,g); + pk = dsaKeyGen.generatePublic(ps); + } + else if (tokens[0].equals("ssh-rsa")) { + + int pos = 11; + + int n = getInt(rawKey,pos); + pos += 4; + BigInteger e = new BigInteger(1,Arrays.copyOfRange(rawKey,pos,pos+n)); + pos += n; + + n = getInt(rawKey,pos); + pos += 4; + BigInteger modulus = new BigInteger(1,Arrays.copyOfRange(rawKey,pos,pos+n)); + pos += n; + + if (pos != rawKey.length) { + throw new IOException("Authorized keys file line ["+reader.getLineNumber()+"] contain a RSA key with extra garbage."); + } + + RSAPublicKeySpec ps = new RSAPublicKeySpec(modulus,e); + pk = rsaKeyGen.generatePublic(ps); + } + else { + throw new IOException("Authorized keys file line ["+reader.getLineNumber()+"] does not start with ssh-dss or ssh-rsa."); + + } + + ret.put(pk,new AuthorizedKey(tokens[2],tokens[0],pk)); + } + return ret; + } + finally { + is.close(); + } + } + + /* (non-Javadoc) + * @see org.apache.sshd.server.PublickeyAuthenticator#authenticate(java.lang.String, java.security.PublicKey, org.apache.sshd.server.session.ServerSession) + */ + public boolean authenticate(String username, PublicKey key, + ServerSession session) { + + AuthorizedKey ak = this.authorizedKeysProvider.getKey(key); + + if (ak == null) { + + log.error("Failed authentication of user [{}] from [{}] with unknown public key.", + username,session.getIoSession().getRemoteAddress()); + return false; + } + else { + + log.info("Successful authentication of user [{}] from [{}] with public key [{}].", + new Object[]{username,session.getIoSession().getRemoteAddress(),ak.getAlias()}); + return true; + } + } + + /** + * @param path The path of the authorized keys file. + */ + public void setAuthorizedKeys(String path) { + + this.authorizedKeys = path; + } + + /** + * @param active Whether the authorized_keys parsing timer should be started. + */ + public void setActive(boolean active) { + this.active = active; + } + + /** + * The blueprint init-method used to start the parsing timer + * if {@link #setActive(boolean)} was called with a value of true. + */ + public void startTimer() { + + if (this.active) { + + this.parseAuthorizedKeysTimer = new Timer(); + this.authorizedKeysProvider = new AuthorizedKeysProvider(); + this.parseAuthorizedKeysTimer.schedule(this.authorizedKeysProvider,10,60000L); + } + } + + /** + * The blueprint destroy-method used to stop the parsing timer. + */ + public void stopTimer() { + if (this.parseAuthorizedKeysTimer != null) { + this.parseAuthorizedKeysTimer.cancel(); + this.parseAuthorizedKeysTimer = null; + } + } +} Eigenschaftsänderungen: src/main/java/org/apache/karaf/shell/ssh/KarafPublickeyAuthenticator.java ___________________________________________________________________ Hinzugefügt: svn:keywords + Id Hinzugefügt: svn:eol-style + native Index: src/main/java/org/apache/karaf/shell/ssh/UserAuthFactoriesFactory.java =================================================================== --- src/main/java/org/apache/karaf/shell/ssh/UserAuthFactoriesFactory.java (Revision 0) +++ src/main/java/org/apache/karaf/shell/ssh/UserAuthFactoriesFactory.java (Revision 0) @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.karaf.shell.ssh; + +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.sshd.SshServer; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.server.UserAuth; +import org.apache.sshd.server.auth.UserAuthPassword; +import org.apache.sshd.server.auth.UserAuthPublicKey; +import org.osgi.service.blueprint.container.ComponentDefinitionException; +import org.osgi.service.blueprint.container.ReifiedType; + +/** + *

A factory for user authentication factories to set on + * {@link SshServer#setUserAuthFactories(java.util.List)} based on a + * comma-separated list of authentication methods.

+ * + *

Currently, the following methods are supported: + *

    + *
  • password + * Password authentication against a given JAAS domain.

  • + *
  • publickey + * Public Key authentication against an openssh authorized_keys file.

  • + *
+ *

+ */ +public class UserAuthFactoriesFactory { + + public static final String PASSWORD_METHOD = "password"; + public static final String PUBLICKEY_METHOD = "publickey"; + + private Set methodSet; + private List> factories; + + public static Converter getConverter() { + return new Converter(); + } + + /** + * This blueprint typ converter silently converts instances of + * class X implements NameFactory<UserAuth> + * to the reified type cNameFactory<UserAuth> + * and therefore helps blueprint to set the returned factories on + * {@link SshServer#setUserAuthFactories(List)} without complaining + * about type conversion errors. + */ + public static class Converter implements org.osgi.service.blueprint.container.Converter { + + public boolean canConvert(Object sourceObject, ReifiedType targetType) { + return NamedFactory.class.isAssignableFrom(sourceObject.getClass()) + && UserAuth.class.equals(((ParameterizedType)sourceObject.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]) + && NamedFactory.class.equals(targetType.getRawClass()) + && UserAuth.class.equals(targetType.getActualTypeArgument(0).getRawClass()); + } + + public Object convert(Object sourceObject, ReifiedType targetType) throws Exception { + return sourceObject; + } + } + + /** + * @param methods A comma-separated list of authentication methods. + */ + public void setAuthMethods(String methods) { + + this.methodSet = new HashSet(); + this.factories = new ArrayList>(); + + String[] ams = methods.split(","); + + for (String am : ams) { + + if (PASSWORD_METHOD.equals(am)) { + + this.factories.add(new UserAuthPassword.Factory()); + } + else if (PUBLICKEY_METHOD.equals(am)) { + + this.factories.add(new UserAuthPublicKey.Factory()); + } + else { + throw new ComponentDefinitionException("Invalid authentication method ["+am+"] specified."); + } + + this.methodSet.add(am); + } + } + + /** + * @return the factories to set on {@link SshServer#setUserAuthFactories(List)} + */ + public List> getFactories() { + return factories; + } + + public boolean isPublickeyEnabled() { + return this.methodSet.contains(PUBLICKEY_METHOD); + } + + public boolean isPasswordEnabled() { + return this.methodSet.contains(PASSWORD_METHOD); + } + +} Eigenschaftsänderungen: src/main/java/org/apache/karaf/shell/ssh/UserAuthFactoriesFactory.java ___________________________________________________________________ Hinzugefügt: svn:keywords + Id Hinzugefügt: svn:eol-style + native Index: src/main/resources/OSGI-INF/blueprint/shell-ssh.xml =================================================================== --- src/main/resources/OSGI-INF/blueprint/shell-ssh.xml (Revision 1237229) +++ src/main/resources/OSGI-INF/blueprint/shell-ssh.xml (Arbeitskopie) @@ -25,6 +25,7 @@ + @@ -41,6 +42,8 @@ + + @@ -64,6 +67,10 @@ + + + + @@ -83,9 +90,16 @@ + + + + + + @@ -96,6 +110,14 @@ + + + + + + + Index: pom.xml =================================================================== --- pom.xml (Revision 1237229) +++ pom.xml (Arbeitskopie) @@ -65,6 +65,11 @@ org.apache.sshd sshd-core + + + org.slf4j + slf4j-simple +