Repository: mina-sshd Updated Branches: refs/heads/master 330d17c81 -> e12434d0d
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java new file mode 100644 index 0000000..6421dc0 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java @@ -0,0 +1,335 @@ +/* + * 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.sshd.server.config.keys; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StreamCorruptedException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.io.NoCloseInputStream; +import org.apache.sshd.common.util.io.NoCloseReader; +import org.apache.sshd.server.PublickeyAuthenticator; + +/** + * Represents an entry in the user's {@code authorized_keys} file according + * to the <A HREF="http://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys">OpenSSH format</A>. + * <B>Note:</B> {@code equals/hashCode} check only the key type and data - the + * comment and/or login options are not considered part of equality + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class AuthorizedKeyEntry extends PublicKeyEntry { + private static final long serialVersionUID = -9007505285002809156L; + + private String comment; + // for options that have no value, "true" is used + private Map<String,String> loginOptions=Collections.<String,String>emptyMap(); + + public AuthorizedKeyEntry() { + super(); + } + + public String getComment() { + return comment; + } + + public void setComment(String value) { + this.comment = value; + } + + public Map<String,String> getLoginOptions() { + return loginOptions; + } + + public void setLoginOptions(Map<String,String> value) { + if ((this.loginOptions=value) == null) { + this.loginOptions = Collections.<String,String>emptyMap(); + } + } + + @Override + public String toString() { + String entry = super.toString(); + String kc = getComment(); + Map<?,?> ko=getLoginOptions(); + return (GenericUtils.isEmpty(ko) ? "" : ko.toString() + " ") + + entry + + (GenericUtils.isEmpty(kc) ? "" : " " + kc) + ; + } + + public static final PublickeyAuthenticator fromAuthorizedEntries(Collection<? extends AuthorizedKeyEntry> entries) throws IOException, GeneralSecurityException { + Collection<PublicKey> keys = resolveAuthorizedKeys(entries); + if (GenericUtils.isEmpty(keys)) { + return PublickeyAuthenticator.RejectAllPublickeyAuthenticator.INSTANCE; + } else { + return new PublickeyAuthenticator.KeySetPublickeyAuthenticator(keys); + } + } + + public static final List<PublicKey> resolveAuthorizedKeys(Collection<? extends AuthorizedKeyEntry> entries) throws IOException, GeneralSecurityException { + if (GenericUtils.isEmpty(entries)) { + return Collections.emptyList(); + } + + List<PublicKey> keys = new ArrayList<PublicKey>(entries.size()); + for (AuthorizedKeyEntry e : entries) { + PublicKey k = e.resolvePublicKey(); + keys.add(k); + } + + return keys; + } + + /** + * Standard OpenSSH authorized keys file name + */ + public static final String STD_AUTHORIZED_KEYS_FILENAME="authorized_keys"; + private static final class LazyDefaultAuthorizedKeysFileHolder { + private static final File keysFile=new File(PublicKeyEntry.getDefaultKeysFolder(), STD_AUTHORIZED_KEYS_FILENAME); + } + + /** + * @return The default {@link File} location of the OpenSSH authorized keys file + */ + @SuppressWarnings("synthetic-access") + public static final File getDefaultAuthorizedKeysFile() { + return LazyDefaultAuthorizedKeysFileHolder.keysFile; + } + /** + * Reads read the contents of the default OpenSSH <code>authorized_keys</code> file + * @return A {@link Collection} of all the {@link AuthorizedKeyEntry}-ies found there - + * or empty if file does not exist + * @throws IOException If failed to read keys from file + */ + public static final Collection<AuthorizedKeyEntry> readDefaultAuthorizedKeys() throws IOException { + File keysFile=getDefaultAuthorizedKeysFile(); + if (keysFile.exists()) { + return readAuthorizedKeys(keysFile); + } else { + return Collections.emptyList(); + } + } + + /** + * Reads read the contents of an <code>authorized_keys</code> file + * @param url The {@link URL} to read from + * @return A {@link Collection} of all the {@link AuthorizedKeyEntry}-ies found there + * @throws IOException If failed to read or parse the entries + * @see #readAuthorizedKeys(InputStream, boolean) + */ + public static final Collection<AuthorizedKeyEntry> readAuthorizedKeys(URL url) throws IOException { + return readAuthorizedKeys(url.openStream(), true); + } + + /** + * Reads read the contents of an <code>authorized_keys</code> file + * @param file The {@link File} to read from + * @return A {@link Collection} of all the {@link AuthorizedKeyEntry}-ies found there + * @throws IOException If failed to read or parse the entries + * @see #readAuthorizedKeys(InputStream, boolean) + */ + public static final Collection<AuthorizedKeyEntry> readAuthorizedKeys(File file) throws IOException { + return readAuthorizedKeys(new FileInputStream(file), true); + } + + /** + * Reads read the contents of an <code>authorized_keys</code> file + * @param path {@link Path} to read from + * @param options The {@link OpenOption}s to use - if unspecified then appropriate + * defaults assumed + * @return A {@link Collection} of all the {@link AuthorizedKeyEntry}-ies found there + * @throws IOException If failed to read or parse the entries + * @see #readAuthorizedKeys(InputStream, boolean) + * @see Files#newInputStream(Path, OpenOption...) + */ + public static final Collection<AuthorizedKeyEntry> readAuthorizedKeys(Path path, OpenOption ... options) throws IOException { + return readAuthorizedKeys(Files.newInputStream(path, options), true); + } + + /** + * Reads read the contents of an <code>authorized_keys</code> file + * @param filePath The file path to read from + * @return A {@link Collection} of all the {@link AuthorizedKeyEntry}-ies found there + * @throws IOException If failed to read or parse the entries + * @see #readAuthorizedKeys(InputStream, boolean) + */ + public static final Collection<AuthorizedKeyEntry> readAuthorizedKeys(String filePath) throws IOException { + return readAuthorizedKeys(new FileInputStream(filePath), true); + } + + /** + * Reads read the contents of an <code>authorized_keys</code> file + * @param in The {@link InputStream} + * @param okToClose <code>true</code> if method may close the input stream + * regardless of whether successful or failed + * @return A {@link Collection} of all the {@link AuthorizedKeyEntry}-ies found there + * @throws IOException If failed to read or parse the entries + * @see #readAuthorizedKeys(Reader, boolean) + */ + public static final Collection<AuthorizedKeyEntry> readAuthorizedKeys(InputStream in, boolean okToClose) throws IOException { + try(Reader rdr=new InputStreamReader(NoCloseInputStream.resolveInputStream(in, okToClose), StandardCharsets.UTF_8)) { + return readAuthorizedKeys(rdr, true); + } + } + + /** + * Reads read the contents of an <code>authorized_keys</code> file + * @param rdr The {@link Reader} + * @param okToClose <code>true</code> if method may close the input stream + * regardless of whether successful or failed + * @return A {@link Collection} of all the {@link AuthorizedKeyEntry}-ies found there + * @throws IOException If failed to read or parse the entries + * @see #readAuthorizedKeys(BufferedReader) + */ + public static final Collection<AuthorizedKeyEntry> readAuthorizedKeys(Reader rdr, boolean okToClose) throws IOException { + try(BufferedReader buf=new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) { + return readAuthorizedKeys(buf); + } + } + + /** + * @param rdr The {@link BufferedReader} to use to read the contents of + * an <code>authorized_keys</code> file + * @return A {@link Collection} of all the {@link AuthorizedKeyEntry}-ies found there + * @throws IOException If failed to read or parse the entries + * @see #parseAuthorizedKeyEntry(String) + */ + public static final Collection<AuthorizedKeyEntry> readAuthorizedKeys(BufferedReader rdr) throws IOException { + Collection<AuthorizedKeyEntry> entries=null; + + for (String line=rdr.readLine(); line != null; line=rdr.readLine()) { + final AuthorizedKeyEntry entry; + try { + if ((entry=parseAuthorizedKeyEntry(line.trim())) == null) { + continue; + } + } catch(IllegalArgumentException e) { + throw new StreamCorruptedException(e.getMessage()); + } + + if (entries == null) { + entries = new LinkedList<AuthorizedKeyEntry>(); + } + + entries.add(entry); + } + + if (entries == null) { + return Collections.emptyList(); + } else { + return entries; + } + } + + /** + * @param line Original line from an <code>authorized_keys</code> file + * @return {@link AuthorizedKeyEntry} or <code>null</code> if the line is + * <code>null</code>/empty or a comment line + * @throws IllegalArgumentException If failed to parse/decode the line + * @see #COMMENT_CHAR + */ + public static final AuthorizedKeyEntry parseAuthorizedKeyEntry(String line) throws IllegalArgumentException { + if (GenericUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) { + return null; + } + + int startPos=line.indexOf(' '); + if (startPos <= 0) { + throw new IllegalArgumentException("Bad format (no key data delimiter): " + line); + } + + int endPos=line.indexOf(' ', startPos + 1); + if (endPos <= startPos) { + endPos = line.length(); + } + + String keyType = line.substring(0, startPos); + PublicKeyEntryDecoder<? extends PublicKey> decoder = KeyUtils.getPublicKeyEntryDecoder(keyType); + final AuthorizedKeyEntry entry; + if (decoder == null) { // assume this is due to the fact that it starts with login options + if ((entry=parseAuthorizedKeyEntry(line.substring(startPos + 1).trim())) == null) { + throw new IllegalArgumentException("Bad format (no key data after login options): " + line); + } + + entry.setLoginOptions(parseLoginOptions(keyType)); + } else { + String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line; + String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null; + entry = parsePublicKeyEntry(new AuthorizedKeyEntry(), encData); + entry.setComment(comment); + } + + return entry; + } + + public static final Map<String,String> parseLoginOptions(String options) { + // TODO add support if quoted values contain ',' + String[] pairs=GenericUtils.split(options, ','); + if (GenericUtils.isEmpty(pairs)) { + return Collections.emptyMap(); + } + + Map<String,String> optsMap=new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); + for (String p : pairs) { + p = GenericUtils.trimToEmpty(p); + if (GenericUtils.isEmpty(p)) { + continue; + } + + int pos=p.indexOf('='); + String name=(pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos)); + CharSequence value=(pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1)); + value = GenericUtils.stripQuotes(value); + if (value == null) { + value = Boolean.TRUE.toString(); + } + + String prev=optsMap.put(name, value.toString()); + if (prev != null) { + throw new IllegalArgumentException("Multiple values for key=" + name + ": old=" + prev + ", new=" + value); + } + } + + return optsMap; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java new file mode 100644 index 0000000..177686e --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java @@ -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.sshd.server.config.keys; + +import java.io.File; +import java.io.IOException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.IoUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.io.ModifiableFileWatcher; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; + +/** + * Uses the authorized keys file to implement {@link PublickeyAuthenticator} + * while automatically re-loading the keys if the file has changed when a + * new authentication request is received. <B>Note:</B> by default, the only + * validation of the username is that it is not {@code null}/empty - see + * {@link #isValidUsername(String, ServerSession)} + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class AuthorizedKeysAuthenticator extends ModifiableFileWatcher implements PublickeyAuthenticator { + private final AtomicReference<PublickeyAuthenticator> delegateHolder = // assumes initially reject-all + new AtomicReference<PublickeyAuthenticator>(PublickeyAuthenticator.RejectAllPublickeyAuthenticator.INSTANCE); + + public AuthorizedKeysAuthenticator(File file) { + this(ValidateUtils.checkNotNull(file, "No file to watch", GenericUtils.EMPTY_OBJECT_ARRAY).toPath()); + } + + public AuthorizedKeysAuthenticator(Path file) { + this(file, IoUtils.getLinkOptions(false)); + } + + public AuthorizedKeysAuthenticator(Path file, LinkOption... options) { + super(file, options); + } + + @Override + public boolean authenticate(String username, PublicKey key, ServerSession session) { + if (!isValidUsername(username, session)) { + if (log.isDebugEnabled()) { + log.debug("authenticate(" + username + ")[" + session + "][" + key.getAlgorithm() + "] invalid user name - file = " + getPath()); + } + return false; + } + + try { + PublickeyAuthenticator delegate = + ValidateUtils.checkNotNull(resolvePublickeyAuthenticator(username, session), "No delegate", GenericUtils.EMPTY_OBJECT_ARRAY); + boolean accepted = delegate.authenticate(username, key, session); + if (log.isDebugEnabled()) { + log.debug("authenticate(" + username + ")[" + session + "][" + key.getAlgorithm() + "] accepted " + accepted + " from " + getPath()); + } + + return accepted; + } catch(Exception e) { + if (log.isDebugEnabled()) { + log.debug("authenticate(" + username + ")[" + session + "][" + getPath() + "]" + + " failed (" + e.getClass().getSimpleName() + ")" + + " to resolve delegate: " + e.getMessage()); + } + + return false; + } + } + + protected boolean isValidUsername(String username, ServerSession session) { + if (GenericUtils.isEmpty(username)) { + return false; + } else { + return true; + } + } + + protected PublickeyAuthenticator resolvePublickeyAuthenticator(String username, ServerSession session) throws IOException, GeneralSecurityException { + if (checkReloadRequired()) { + /* Start fresh - NOTE: if there is any error then we want to reject all attempts + * since we don't want to remain with the previous data - safer that way + */ + delegateHolder.set(PublickeyAuthenticator.RejectAllPublickeyAuthenticator.INSTANCE); + + Path path = getPath(); + if (exists()) { + Collection<AuthorizedKeyEntry> entries = reloadAuthorizedKeys(path, username, session); + if (GenericUtils.size(entries) > 0) { + delegateHolder.set(AuthorizedKeyEntry.fromAuthorizedEntries(entries)); + } + } else { + log.info("resolvePublickeyAuthenticator(" + username + ")[" + session + "] no authorized keys file at " + path); + } + } + + return delegateHolder.get(); + } + + protected Collection<AuthorizedKeyEntry> reloadAuthorizedKeys(Path path, String username, ServerSession session) throws IOException { + Collection<AuthorizedKeyEntry> entries = AuthorizedKeyEntry.readAuthorizedKeys(path, options); + log.info("reloadAuthorizedKeys(" + username + ")[" + session + "] loaded " + GenericUtils.size(entries) + " keys from " + path); + updateReloadAttributes(); + return entries; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java new file mode 100644 index 0000000..6bad436 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java @@ -0,0 +1,161 @@ +/* + * 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.sshd.server.config.keys; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystemException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.IoUtils; +import org.apache.sshd.common.util.OsUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.server.session.ServerSession; + +/** + * Monitors the {@code ~/.ssh/authorized_keys} file of the user currently running + * the server, re-loading it if necessary. It also (optionally) enforces the same + * permissions regime as {@code OpenSSH} does for the file permissions. By default + * also compares the current username with the authenticated one. + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DefaultAuthorizedKeysAuthenticator extends AuthorizedKeysAuthenticator { + /** + * The {@link Set} of {@link PosixFilePermission} <U>not</U> allowed if strict + * permissions are enforced + */ + public static final Set<PosixFilePermission> STRICTLY_PROHIBITED_FILE_PERMISSION = + Collections.unmodifiableSet( + EnumSet.of(PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE)); + + /** + * The default instance that enforces the same permissions regime as {@code OpenSSH} + */ + public static final DefaultAuthorizedKeysAuthenticator INSTANCE = new DefaultAuthorizedKeysAuthenticator(true); + + private final boolean strict; + private final String user; + + /** + * @param strict If {@code true} then makes sure that the containing folder + * has 0700 access and the file 0600. <B>Note:</B> for <I>Windows</I> it + * does not check these permissions + */ + public DefaultAuthorizedKeysAuthenticator(boolean strict) { + this(System.getProperty("user.name"), strict); + } + + public DefaultAuthorizedKeysAuthenticator(String user, boolean strict) { + this(user, AuthorizedKeyEntry.getDefaultAuthorizedKeysFile(), strict); + } + + public DefaultAuthorizedKeysAuthenticator(File file, boolean strict) { + this(ValidateUtils.checkNotNull(file, "No file provided", GenericUtils.EMPTY_OBJECT_ARRAY).toPath(), strict, IoUtils.getLinkOptions(false)); + } + + public DefaultAuthorizedKeysAuthenticator(String user, File file, boolean strict) { + this(user, ValidateUtils.checkNotNull(file, "No file provided", GenericUtils.EMPTY_OBJECT_ARRAY).toPath(), strict, IoUtils.getLinkOptions(false)); + } + + public DefaultAuthorizedKeysAuthenticator(Path path, boolean strict, LinkOption ... options) { + this(System.getProperty("user.name"), path, strict, options); + } + + public DefaultAuthorizedKeysAuthenticator(String user, Path path, boolean strict, LinkOption ... options) { + super(path, options); + this.user = ValidateUtils.checkNotNullAndNotEmpty(user, "No username provided", GenericUtils.EMPTY_OBJECT_ARRAY); + this.strict = strict; + } + + public final String getUsername() { + return user; + } + + public final boolean isStrict() { + return strict; + } + + @Override + protected boolean isValidUsername(String username, ServerSession session) { + if (!super.isValidUsername(username, session)) { + return false; + } + + String expected = getUsername(); + if (username.equals(expected)) { + return true; + } else { + return false; // debug breakpoint + } + } + + @Override + protected Collection<AuthorizedKeyEntry> reloadAuthorizedKeys(Path path, String username, ServerSession session) throws IOException { + if (isStrict()) { + if (log.isDebugEnabled()) { + log.info("reloadAuthorizedKeys(" + username + ")[" + session + "] check permissions of " + path); + } + + Collection<PosixFilePermission> perms = IoUtils.getPermissions(path); + // this is true for Windows as well + if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) { + throw new FileSystemException(path.toString(), path.toString(), "File is not allowed to have e(x)ecute permission"); + } + + if (OsUtils.isUNIX()) { + validateFilePath(path, perms, STRICTLY_PROHIBITED_FILE_PERMISSION); + + Path parent=path.getParent(); + validateFilePath(parent, IoUtils.getPermissions(parent), STRICTLY_PROHIBITED_FILE_PERMISSION); + } + } + + return super.reloadAuthorizedKeys(path, username, session); + } + + /** + * @param path The {@link Path} to be validated + * @param perms The current {@link PosixFilePermission}s + * @param excluded The permissions <U>not</U> allowed to exist + * @return The original path + * @throws IOException If an excluded permission appears in the current ones + */ + protected Path validateFilePath(Path path, Collection<PosixFilePermission> perms, Collection<PosixFilePermission> excluded) throws IOException { + if (GenericUtils.isEmpty(perms) || GenericUtils.isEmpty(excluded)) { + return path; + } + + for (PosixFilePermission p : excluded) { + if (perms.contains(p)) { + throw new FileSystemException(path.toString(), path.toString(), "File is not allowed to have permission=" + p); + } + } + + return path; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/test/java/org/apache/sshd/SinglePublicKeyAuthTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/SinglePublicKeyAuthTest.java b/sshd-core/src/test/java/org/apache/sshd/SinglePublicKeyAuthTest.java index e0ce922..61fa2c1 100644 --- a/sshd-core/src/test/java/org/apache/sshd/SinglePublicKeyAuthTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/SinglePublicKeyAuthTest.java @@ -24,8 +24,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.keyprovider.KeyPairProvider; -import org.apache.sshd.common.util.KeyUtils; import org.apache.sshd.server.Command; import org.apache.sshd.server.CommandFactory; import org.apache.sshd.server.PublickeyAuthenticator; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/test/java/org/apache/sshd/common/util/GenericUtilsTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/util/GenericUtilsTest.java b/sshd-core/src/test/java/org/apache/sshd/common/util/GenericUtilsTest.java index 1b594f5..1983f9b 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/util/GenericUtilsTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/util/GenericUtilsTest.java @@ -54,4 +54,63 @@ public class GenericUtilsTest extends BaseTestSupport { } } } + + @Test + public void testStripQuotes() { + String expected = getCurrentTestName(); + assertSame("Unexpected un-quoted stripping", expected, GenericUtils.stripQuotes(expected)); + + StringBuilder sb = new StringBuilder(2 + expected.length()).append('|').append(expected).append('|'); + for (int index=0; index < GenericUtils.QUOTES.length(); index++) { + char delim = GenericUtils.QUOTES.charAt(index); + sb.setCharAt(0, delim); + sb.setCharAt(sb.length() - 1, delim); + + CharSequence actual = GenericUtils.stripQuotes(sb); + assertEquals("Mismatched result for delim (" + delim + ")", expected, actual.toString()); + } + } + + @Test + public void testStripOnlyFirstLayerQuotes() { + StringBuilder sb = new StringBuilder().append("||").append(getCurrentTestName()).append("||"); + char[] delims = { '\'', '"', '"', '\'' }; + for (int index=0; index < delims.length; index += 2) { + char topDelim = delims[index], innerDelim = delims[index + 1]; + sb.setCharAt(0, topDelim); + sb.setCharAt(1, innerDelim); + sb.setCharAt(sb.length() - 2, innerDelim); + sb.setCharAt(sb.length() - 1, topDelim); + + CharSequence expected = sb.subSequence(1, sb.length() - 1); + CharSequence actual = GenericUtils.stripQuotes(sb); + assertEquals("Mismatched result for delim (" + topDelim + "/" + innerDelim + ")", expected.toString(), actual.toString()); + } + } + + @Test + public void testStripDelimiters() { + String expected = getCurrentTestName(); + final char delim = '|'; + assertSame("Unexpected un-delimited stripping", expected, GenericUtils.stripDelimiters(expected, delim)); + + CharSequence actual = GenericUtils.stripDelimiters( + new StringBuilder(2 + expected.length()).append(delim).append(expected).append(delim), delim); + assertEquals("Mismatched stripped values", expected, actual.toString()); + } + + @Test + public void testStripDelimitersOnlyIfOnBothEnds() { + final char delim = '$'; + StringBuilder expected=new StringBuilder().append(delim).append(getCurrentTestName()).append(delim); + for (int index : new int[] { 0, expected.length() - 1 }) { + // restore original delimiters + expected.setCharAt(0, delim); + expected.setCharAt(expected.length() - 1, delim); + // trash one end + expected.setCharAt(index, (char) (delim + 1)); + + assertSame("Mismatched result for delim at index=" + index, expected, GenericUtils.stripDelimiters(expected, delim)); + } + } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthAgent.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthAgent.java b/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthAgent.java index efcd610..51a31f0 100644 --- a/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthAgent.java +++ b/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthAgent.java @@ -25,7 +25,7 @@ import java.util.Iterator; import org.apache.sshd.agent.SshAgent; import org.apache.sshd.client.session.ClientSessionImpl; import org.apache.sshd.common.SshConstants; -import org.apache.sshd.common.util.KeyUtils; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthPublicKey.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthPublicKey.java b/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthPublicKey.java index a8b7e21..ab0dd66 100644 --- a/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthPublicKey.java +++ b/sshd-core/src/test/java/org/apache/sshd/deprecated/UserAuthPublicKey.java @@ -25,7 +25,7 @@ import org.apache.sshd.client.session.ClientSessionImpl; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.Signature; import org.apache.sshd.common.SshConstants; -import org.apache.sshd.common.util.KeyUtils; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java new file mode 100644 index 0000000..008e682 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java @@ -0,0 +1,108 @@ +/* + * 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.sshd.server.config.keys; + +import java.io.File; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.PublicKey; +import java.util.Collection; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.IoUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.util.BaseTestSupport; +import org.junit.Ignore; +import org.junit.Test; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class AuthorizedKeyEntryTest extends BaseTestSupport { + public AuthorizedKeyEntryTest() { + super(); + } + + @Test + public void testReadAuthorizedKeysFile() throws Exception { + URL url = getClass().getResource(AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME); + assertNotNull("Missing " + AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME + " resource", url); + + runAuthorizedKeysTests(AuthorizedKeyEntry.readAuthorizedKeys(url)); + } + + @Test + @Ignore("It might cause some exceptions if user's file contains unsupported keys") + public void testReadDefaultAuthorizedKeysFile() throws Exception { + File file = AuthorizedKeyEntry.getDefaultAuthorizedKeysFile(); + assertNotNull("No default location", file); + + Path path = file.toPath(); + LinkOption[] options = IoUtils.getLinkOptions(false); + if (!Files.exists(path, options)) { + System.out.append(getCurrentTestName()).append(": verify non-existing ").println(path); + Collection<AuthorizedKeyEntry> entries = AuthorizedKeyEntry.readDefaultAuthorizedKeys(); + assertTrue("Non-empty keys even though file not found: " + entries, GenericUtils.isEmpty(entries)); + } else { + assertFalse("Not a file: " + path, Files.isDirectory(path, options)); + runAuthorizedKeysTests(AuthorizedKeyEntry.readDefaultAuthorizedKeys()); + } + } + + private void runAuthorizedKeysTests(Collection<AuthorizedKeyEntry> entries) throws Exception { + testReadAuthorizedKeys(entries); + testAuthorizedKeysAuth(entries); + } + + private static Collection<AuthorizedKeyEntry> testReadAuthorizedKeys(Collection<AuthorizedKeyEntry> entries) throws Exception { + assertFalse("No entries read", GenericUtils.isEmpty(entries)); + + Exception err = null; + for (AuthorizedKeyEntry entry : entries) { + try { + ValidateUtils.checkNotNull(entry.resolvePublicKey(), "No public key resolved from %s", entry); + } catch(Exception e) { + System.err.append("Failed (").append(e.getClass().getSimpleName()).append(')') + .append(" to resolve key of entry=").append(entry.toString()) + .append(": ").println(e.getMessage()); + err = e; + } + } + + if (err != null) { + throw err; + } + + return entries; + } + + private PublickeyAuthenticator testAuthorizedKeysAuth(Collection<AuthorizedKeyEntry> entries) throws Exception { + Collection<PublicKey> keySet = AuthorizedKeyEntry.resolveAuthorizedKeys(entries); + PublickeyAuthenticator auth = AuthorizedKeyEntry.fromAuthorizedEntries(entries); + for (PublicKey key : keySet) { + assertTrue("Failed to authenticate with key=" + key.getAlgorithm(), auth.authenticate(getCurrentTestName(), key, null)); + } + + return auth; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java new file mode 100644 index 0000000..262d013 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java @@ -0,0 +1,124 @@ +/* + * 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.sshd.server.config.keys; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Writer; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.util.BaseTestSupport; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class AuthorizedKeysAuthenticatorTest extends BaseTestSupport { + public AuthorizedKeysAuthenticatorTest() { + super(); + } + + @Test + public void testAutomaticReload() throws Exception { + final Path file=new File(new File(detectTargetFolder(), TEMP_SUBFOLDER_NAME), getCurrentTestName()).toPath(); + if (Files.exists(file)) { + Files.delete(file); + } + + final AtomicInteger reloadCount = new AtomicInteger(0); + PublickeyAuthenticator auth = new AuthorizedKeysAuthenticator(file) { + @Override + protected Collection<AuthorizedKeyEntry> reloadAuthorizedKeys(Path path, String username, ServerSession session) throws IOException { + assertSame("Mismatched reload path", file, path); + reloadCount.incrementAndGet(); + return super.reloadAuthorizedKeys(path, username, session); + } + }; + assertFalse("Unexpected authentication success for missing file " + file, auth.authenticate(getCurrentTestName(), Mockito.mock(PublicKey.class), null)); + + URL url = getClass().getResource(AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME); + assertNotNull("Missing " + AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME + " resource", url); + + List<String> lines = new ArrayList<String>(); + try(BufferedReader rdr = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) { + for (String l = rdr.readLine(); l != null; l = rdr.readLine()) { + l = GenericUtils.trimToEmpty(l); + // filter out empty and comment lines + if (GenericUtils.isEmpty(l) || (l.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) { + continue; + } else { + lines.add(l); + } + } + } + + assertHierarchyTargetFolderExists(file.getParent()); + + final String EOL = System.getProperty("line.separator"); + Random rnd = new Random(System.nanoTime()); + List<String> removed = new ArrayList<String>(lines.size()); + for ( ; ; ) { + try(Writer w = Files.newBufferedWriter(file)) { + for (String l : lines) { + w.append(l).append(EOL); + } + } + + Collection<AuthorizedKeyEntry> entries = AuthorizedKeyEntry.readAuthorizedKeys(file); + Collection<PublicKey> keySet = AuthorizedKeyEntry.resolveAuthorizedKeys(entries); + + reloadCount.set(0); + for (PublicKey k : keySet) { + assertTrue("Failed to authenticate with key=" + k.getAlgorithm() + " on file=" + file, auth.authenticate(getCurrentTestName(), k, null)); + // we expect EXACTLY ONE re-load call since we did not modify the file during the authentication + assertEquals("Unexpected extra calls to keys re-loading", 1, reloadCount.get()); + } + + if (lines.isEmpty()) { + break; + } + + int nextSize = rnd.nextInt(lines.size()); + while (lines.size() > nextSize) { + String l = lines.remove(0); + removed.add(l); + } + } + + assertTrue("File no longer exists: " + file, Files.exists(file)); + assertFalse("Unexpected authentication success for empty file " + file, auth.authenticate(getCurrentTestName(), Mockito.mock(PublicKey.class), null)); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java new file mode 100644 index 0000000..ca700d7 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java @@ -0,0 +1,67 @@ +/* + * 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.sshd.server.config.keys; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PublicKey; +import java.util.Collection; + +import org.apache.sshd.common.util.IoUtils; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.util.BaseTestSupport; +import org.junit.Test; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DefaultAuthorizedKeysAuthenticatorTest extends BaseTestSupport { + public DefaultAuthorizedKeysAuthenticatorTest() { + super(); + } + + @Test + public void testUsernameValidation() throws Exception { + Path file=new File(new File(detectTargetFolder(), TEMP_SUBFOLDER_NAME), getCurrentTestName()).toPath(); + URL url = getClass().getResource(AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME); + assertNotNull("Missing " + AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME + " resource", url); + + try(InputStream input = url.openStream(); + OutputStream output = Files.newOutputStream(file)) { + IoUtils.copy(input, output); + } + + Collection<AuthorizedKeyEntry> entries = AuthorizedKeyEntry.readAuthorizedKeys(file); + Collection<PublicKey> keySet = AuthorizedKeyEntry.resolveAuthorizedKeys(entries); + PublickeyAuthenticator auth = new DefaultAuthorizedKeysAuthenticator(file, false); + String thisUser = System.getProperty("user.name"); + for (String username : new String[] { null, "", thisUser, getClass().getName() + "#" + getCurrentTestName() }) { + boolean expected = thisUser.equals(username); + for (PublicKey key : keySet) { + boolean actual = auth.authenticate(username, key, null); + assertEquals("Mismatched authentication results for user='" + username + "'", expected, actual); + } + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e12434d0/sshd-core/src/test/resources/org/apache/sshd/server/config/keys/authorized_keys ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/resources/org/apache/sshd/server/config/keys/authorized_keys b/sshd-core/src/test/resources/org/apache/sshd/server/config/keys/authorized_keys new file mode 100644 index 0000000..3a12f32 --- /dev/null +++ b/sshd-core/src/test/resources/org/apache/sshd/server/config/keys/authorized_keys @@ -0,0 +1,16 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2KFr3GqL/3yXY2bAwRGGDxl/qLuE9qdx20+DMh5oAZPpwprlUnlxLm+ikimwn65Z0KeUyfofYKt+vc3rl1k2mDqyG8DqHeH0C+uFBbom0fthX7PRiQr2T9SOzSodjowZuBHlWIfgtcZI0bygX+GlKaAq00l4yCoe1xUTCRd2ZVyNuB1nozcFI+sUzdeKfaxvuyvbccG4tOx06HDryNdxW2e99bsAhLAg7d8xciOeb4PCAI1USg83dt0wVZE9VJbnRnoZ2y/DaQCJtBJ8t8uNLVdggakydDzQuglyd4dYRxeU7t4TEw6wsfXPB0kqdecd0Llspjx0ciEY/BbycdiApw== lgoldstein@LGOLDSTEIN-WIN7 +ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoiPvv8awFmA1iIwNlWD/29gfNyiTnkpjCVtFOqPGM8YTGF2G5FRrAwJyehdJu8qFJSUaNMzrjz4qlP4OyP4qcd16TE3FkIwd22+sXo0K9oEbOF307FVtBGrqnp+m5aFPQIZNhX86Rd9m0zrE4eSkZQ0qOjhp0Q60+G2lleaHSYvdc0IOcOJsWI43+ytlzJ2gKoVwPtttsXVtycjt2ZmD99V/lk3G7sdXQGL5S+lxn7rMtxamOSy+VR1eVu2ZagOCp2XZM1eFNWIRCH0KbRJh2mDrk08pIN9yCh2q/5BF+oh/CQyS8W8754MJuQ+0U0qHBH/wNtogIomedxpW8hG1jQ== [email protected] + +# some empty lines + + +# no comment for key +ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBVdEJBWozLd41Huv2LZOglgE3HpRfcxvKBUB/poMAfRY6nMoStxaVCc/dPzpzE6nOAbX+tuRLL611H3Ooby9quSlKsSfx4/Dk76tPokY7T6eLTmoJP6S3c9OMdNFLQ31UKbW4RlBRFYFFMMQGr3PMzXRKDpQhl0Bvts+N5qQ2eaQ== + +# dummy login options +command="/usr/bin/tinyfugue",environment="PATH=/bin:/usr/bin/:/opt/gtm/bin" ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBVdEJBWozLd41Huv2LZOglgE3HpRfcxvKBUB/poMAfRY6nMoStxaVCc/dPzpzE6nOAbX+tuRLL611H3Ooby9quSlKsSfx4/Dk76tPokY7T6eLTmoJP6S3c9OMdNFLQ31UKbW4RlBRFYFFMMQGr3PMzXRKDpQhl0Bvts+N5qQ2eaQ== +hosts="git.eng.vmware.com" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvQK9DhBSStbi2CaZNOo5vHy9Nga/hLCnO19tL6i5U/5Nhh5Y8W+tL+AA8hqD/doLBnyaEv2xjfzECwKlStc9HWx6EcJ+9B1rA4+5HztuUEWxuozNnkvcScjTBBqEd7fPPt0INI+pSZRYa2InEBBUUHTt1YaDEXamM/j4RVKovEH7Efgq9VUti148ZG90/w9V5ZT1o6yhuOw9UbME/eHIbS2E9P/Gy33OhkAgTLyOfCZAdJiYvcvFXqNWSKVFx3H5hSolh9ppxVFVFj2hW6QtvgYTphLU0ccHOWTBd/UToG4Xd9GjgSoD1pAI9e0NwBOsUfiqSzO99wqpzs6erfwelQ== + +# ECDSA keys +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCbZVVpqEHGLNWMqMeyU1VbWb91XteoamVcgpy4yxNVbZffb5IDdbo1ons/y9KAhcub6LZeLrvXzVUZbXCZiUkg= +host=10.23.222.240 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCMOm8SqJPJqvSa5Q6r/pGhkp3aBc4Evf9wF8DjhSy13m+wwQwQCENQ8V+5bpI58Z0jjB8O3lmuOLils+Nx9AFc=
