This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 84196d2bef1444048645787caaa2764b54dca0cc Author: Lyor Goldstein <[email protected]> AuthorDate: Wed Feb 13 15:38:51 2019 +0200 [SSHD-896] Add support for KEX extension negotiation --- CHANGES.md | 6 +- README.md | 24 +++ docs/event-listeners.md | 6 +- .../java/org/apache/sshd/common/SshConstants.java | 1 + .../org/apache/sshd/common/cipher/ECCurves.java | 2 +- .../common/kex/extension/KexExtensionParser.java | 52 ++++++ .../sshd/common/kex/extension/KexExtensions.java | 192 +++++++++++++++++++++ .../parser/AbstractKexExtensionParser.java | 54 ++++++ .../kex/extension/parser/DelayCompression.java | 52 ++++++ .../parser/DelayedCompressionAlgorithms.java | 95 ++++++++++ .../common/kex/extension/parser/Elevation.java | 48 ++++++ .../common/kex/extension/parser/NoFlowControl.java | 48 ++++++ .../parser/ServerSignatureAlgorithms.java | 49 ++++++ .../org/apache/sshd/common/util/GenericUtils.java | 85 ++++++++- .../org/apache/sshd/common/util/buffer/Buffer.java | 103 +++++++++++ .../sshd/common/util/buffer/BufferUtils.java | 31 ++-- .../sshd/common/util/functors/UnaryEquator.java | 119 +++++++++++++ .../auth/keyboard/UserAuthKeyboardInteractive.java | 4 +- .../sshd/client/auth/keyboard/UserInteraction.java | 2 +- .../org/apache/sshd/common/channel/Channel.java | 2 +- .../sshd/common/kex/AbstractKexFactoryManager.java | 14 ++ .../apache/sshd/common/kex/KexFactoryManager.java | 3 +- .../common/kex/extension/KexExtensionHandler.java | 102 +++++++++++ .../kex/extension/KexExtensionHandlerManager.java | 31 ++++ .../session/ReservedSessionMessagesHandler.java | 3 +- .../common/session/SessionDisconnectHandler.java | 25 +++ .../common/session/helpers/AbstractSession.java | 147 ++++++++++++++-- .../sshd/common/session/helpers/SessionHelper.java | 28 +-- .../auth/hostbased/HostBasedAuthenticator.java | 2 +- .../server/auth/keyboard/InteractiveChallenge.java | 2 +- .../keyboard/KeyboardInteractiveAuthenticator.java | 2 +- .../auth/keyboard/UserAuthKeyboardInteractive.java | 2 +- .../password/PasswordChangeRequiredException.java | 2 +- .../sshd/server/session/AbstractServerSession.java | 17 ++ .../sshd/common/kex/KexFactoryManagerTest.java | 12 ++ .../kex/extension/KexExtensionHandlerTest.java | 104 +++++++++++ .../java/org/apache/sshd/server/ServerTest.java | 6 +- 37 files changed, 1400 insertions(+), 77 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3c76f48..07b4430 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,8 @@ and also return an `Iterable<Path>`. * The SFTP command line client provides a `kex` command that displays the KEX parameters of the current sesssion - client/server proposals and what has been negotiated. +* The `Session` object provides a `KexExtensionHandler` for usage with [KEX extension negotiation](https://tools.wordtothewise.com/rfc/rfc8308) + ## Behavioral changes and enhancements * [SSHD-882](https://issues.apache.org/jira/browse/SSHD-882) - Provide hooks to allow users to register a consumer @@ -24,4 +26,6 @@ for STDERR data sent via the `ChannelSession` - especially for the SFTP subsyste * [SSHD=892](https://issues.apache.org/jira/browse/SSHD-882) - Inform user about possible session disconnect prior to disconnecting and allow intervention via `SessionDisconnectHandler`. -* [SSHD-893] Using Path(s) instead of String(s) as DirectoryScanner results +* [SSHD-893](https://issues.apache.org/jira/browse/SSHD-893) - Using Path(s) instead of String(s) as DirectoryScanner results + +* [SSHD-896](https://issues.apache.org/jira/browse/SSHD-896) - Added support for [KEX extension negotiation](https://tools.wordtothewise.com/rfc/rfc8308) diff --git a/README.md b/README.md index 22f24c0..5e2d881 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,30 @@ leverage [Apache MINA](http://mina.apache.org), a scalable and high performance aim at being a replacement for the SSH client or SSH server from Unix operating systems, but rather provides support for Java based applications requiring SSH support. +# Supported standards + +* [RFC 4251 - The Secure Shell (SSH) Protocol Architecture](https://tools.ietf.org/html/rfc4251) +* [RFC 4252 - The Secure Shell (SSH) Authentication Protocol](https://tools.ietf.org/html/rfc4252) +* [RFC 4253 - The Secure Shell (SSH) Transport Layer Protocol](https://tools.ietf.org/html/rfc4253) +* [RFC 4254 - The Secure Shell (SSH) Connection Protocol](https://tools.ietf.org/html/rfc4254) +* [RFC 4256 - Generic Message Exchange Authentication for the Secure Shell Protocol (SSH)](https://tools.ietf.org/html/rfc4256) +* [RFC 5480 - Elliptic Curve Cryptography Subject Public Key Information](https://tools.ietf.org/html/rfc5480) +* [RFC 8308 - Extension Negotiation in the Secure Shell (SSH) Protocol](https://tools.ietf.org/html/rfc8308) + * **Note:** - the code contains **hooks** for implementing the RFC but beyond allowing convenient + access to the required protocol details, it does not implement any logic that handles the messages. +* SFTP version 3-6 + extensions + * `supported` - [DRAFT 05 - section 4.4](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-05.tx) + * `supported2` - [DRAFT 13 section 5.4](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-10) + * `versions` - [DRAFT 09 Section 4.6](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt) + * `vendor-id` - [DRAFT 09 - section 4.4](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt) + * `acl-supported` - [DRAFT 11 - section 5.4](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-11) + * `newline` - [DRAFT 09 Section 4.3](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt) + * `md5-hash`, `md5-hash-handle` - [DRAFT 09 - section 9.1.1](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt) + * `check-file-handle`, `check-file-name` - [DRAFT 09 - section 9.1.2](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt) + * `copy-file`, `copy-data` - [DRAFT 00 - sections 6, 7](http://tools.ietf.org/id/draft-ietf-secsh-filexfer-extensions-00.txt) + * `space-available` - [DRAFT 09 - section 9.3](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt) + * Several [OpenSSH SFTP extensions](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL) + # [Release notes](./CHANGES.md) # Core requirements diff --git a/docs/event-listeners.md b/docs/event-listeners.md index 3b9beef..dd6230e 100644 --- a/docs/event-listeners.md +++ b/docs/event-listeners.md @@ -67,7 +67,6 @@ In this context, it is worth mentioning that one can attach to sessions **arbitr ### `ChannelListener` - Informs about channel related events - as with sessions, once can influence the channel to some extent, depending on the channel's **state**. The ability to influence channels is much more limited than sessions. In this context, it is worth mentioning that one can attach to channels **arbitrary attributes** that can be retrieved by the user's code later on - same was as it is done for sessions. @@ -75,12 +74,15 @@ The ability to influence channels is much more limited than sessions. In this co ### `UnknownChannelReferenceHandler` - Invoked whenever a message intended for an unknown channel is received. By default, the code **ignores** the vast majority of such messages and logs them at DEBUG level. For a select few types of messages the code generates an `SSH_CHANNEL_MSG_FAILURE` packet that is sent to the peer session - see `DefaultUnknownChannelReferenceHandler` implementation. The user may register handlers at any level - client/server, session and/or connection service - the one registered "closest" to connection service will be used. +### `KexExtensionHandler` + +Provides hook for implementing [KEX extension negotiation](https://tools.wordtothewise.com/rfc/rfc8308) + ### `ReservedSessionMessagesHandler` Can be used to handle the following cases: diff --git a/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java b/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java index 57f3c17..7e73edf 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java @@ -45,6 +45,7 @@ public final class SshConstants { public static final byte SSH_MSG_DEBUG = 4; public static final byte SSH_MSG_SERVICE_REQUEST = 5; public static final byte SSH_MSG_SERVICE_ACCEPT = 6; + public static final byte SSH_MSG_KEXINIT = 20; public static final int MSG_KEX_COOKIE_SIZE = 16; public static final byte SSH_MSG_NEWKEYS = 21; diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java index 3ccb671..2102cfa 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java @@ -434,7 +434,7 @@ public enum ECCurves implements KeyTypeIndicator, KeySizeIndicator, NamedResourc * The various {@link ECPoint} representation compression indicators * * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> - * @see <A HREF="https://www.ietf.org/rfc/rfc5480.txt">RFC-5480 - section 2.2</A> + * @see <A HREF="https://tools.ietf.org/html/rfc5480#section-2.2">RFC-5480 - section 2.2</A> */ public enum ECPointCompression { // see http://tools.ietf.org/html/draft-jivsov-ecc-compact-00 diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionParser.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionParser.java new file mode 100644 index 0000000..6f70960 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionParser.java @@ -0,0 +1,52 @@ +/* + * 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.common.kex.extension; + +import java.io.IOException; + +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; + +/** + * Parses a known KEX extension + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface KexExtensionParser<T> extends NamedResource { + default T parseExtension(byte[] data) throws IOException { + return parseExtension(data, 0, data.length); + } + + default T parseExtension(byte[] data, int off, int len) throws IOException { + return parseExtension(new ByteArrayBuffer(data, off, len)); + } + + T parseExtension(Buffer buffer) throws IOException; + + /** + * Adds the name + value to the buffer + * + * @param value The value of the extension + * @param buffer The target {@link Buffer} + * @throws IOException If failed to encode + */ + void putExtension(T value, Buffer buffer) throws IOException; +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java new file mode 100644 index 0000000..41fbce0 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java @@ -0,0 +1,192 @@ +/* + * 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.common.kex.extension; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.kex.extension.parser.DelayCompression; +import org.apache.sshd.common.kex.extension.parser.Elevation; +import org.apache.sshd.common.kex.extension.parser.NoFlowControl; +import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.Readable; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.Buffer; + +/** + * Provides some helpers for <A HREF="https://tools.ietf.org/html/rfc8308">RFC 8308</A> + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class KexExtensions { + public static final byte SSH_MSG_EXT_INFO = 7; + public static final byte SSH_MSG_NEWCOMPRESS = 8; + + public static final String CLIENT_KEX_EXTENSION = "ext-info-c"; + public static final String SERVER_KEX_EXTENSION = "ext-info-s"; + + /** + * A case <U>insensitive</U> map of all the default known {@link KexExtensionParser} + * where key=the extension name + */ + private static final NavigableMap<String, KexExtensionParser<?>> EXTENSION_PARSERS = + Stream.of( + ServerSignatureAlgorithms.INSTANCE, + NoFlowControl.INSTANCE, + Elevation.INSTANCE, + DelayCompression.INSTANCE) + .collect(Collectors.toMap( + NamedResource::getName, Function.identity(), + GenericUtils.throwingMerger(), () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))); + + private KexExtensions() { + throw new UnsupportedOperationException("No instance allowed"); + } + + /** + * @return A case <U>insensitive</U> copy of the currently registered + * {@link KexExtensionParser}s names + */ + public static NavigableSet<String> getRegisteredExtensionParserNames() { + synchronized (EXTENSION_PARSERS) { + return EXTENSION_PARSERS.isEmpty() + ? Collections.emptyNavigableSet() + : GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, EXTENSION_PARSERS.keySet()); + } + } + + /** + * @param name The (never {@code null}/empty) extension name + * @return The registered {@code KexExtensionParser} for the (case <U>insensitive</U>) + * extension name - {@code null} if no match found + */ + public static KexExtensionParser<?> getRegisteredExtensionParser(String name) { + ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name provided"); + synchronized (EXTENSION_PARSERS) { + return EXTENSION_PARSERS.get(name); + } + } + + /** + * Registers a {@link KexExtensionParser} for a named extension + * + * @param parser The (never {@code null}) parser to register + * @return The replaced parser for the named extension (case <U>insensitive</U>) + * - {@code null} if no previous parser registered for this extension + */ + public static KexExtensionParser<?> registerExtensionParser(KexExtensionParser<?> parser) { + Objects.requireNonNull(parser, "No parser provided"); + String name = ValidateUtils.checkNotNullAndNotEmpty(parser.getName(), "No extension name provided"); + synchronized (EXTENSION_PARSERS) { + return EXTENSION_PARSERS.put(name, parser); + } + } + + /** + * Registers {@link KexExtensionParser} for a named extension + * + * @param name The (never {@code null}/empty) extension name + * @return The removed {@code KexExtensionParser} for the (case <U>insensitive</U>) + * extension name - {@code null} if no match found + */ + public static KexExtensionParser<?> unregisterExtensionParser(String name) { + ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name provided"); + synchronized (EXTENSION_PARSERS) { + return EXTENSION_PARSERS.remove(name); + } + } + + /** + * Attempts to parse an {@code SSH_MSG_EXT_INFO} message + * + * @param buffer The {@link Buffer} containing the message + * @return A {@link List} of key/value "pairs" where key=the extension + * name, value=the parsed value using the matching registered {@link KexExtensionParser}. + * If no such parser found then the raw value bytes are set as the extension value. + * @throws IOException If failed to parse one of the extensions + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.3">RFC-8308 - section 2.3</A> + */ + public static List<Map.Entry<String, ?>> parseExtensions(Buffer buffer) throws IOException { + int count = buffer.getInt(); + if (count == 0) { + return Collections.emptyList(); + } + + List<Map.Entry<String, ?>> entries = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + String name = buffer.getString(); + byte[] data = buffer.getBytes(); + KexExtensionParser<?> parser = getRegisteredExtensionParser(name); + Object value = (parser == null) ? data : parser.parseExtension(data); + entries.add(new SimpleImmutableEntry<>(name, value)); + } + + return entries; + } + + /** + * Creates an {@code SSH_MSG_EXT_INFO} message using the provided extensions. + * + * @param exts A {@link Collection} of key/value "pairs" where key=the extension + * name, value=the extension value. <B>Note:</B> if a registered {@link KexExtensionParser} + * exists for the name, then it is assumed that the value is of the correct type. If + * no registered parser found the value is assumed to be either the encoded value as an + * array of bytes or as another {@link Readable} (e.g., another {@link Buffer}) + * or a {@link ByteBuffer}. + * @param buffer The target {@link Buffer} - assumed to already contain the + * {@code SSH_MSG_EXT_INFO} opcode + * @throws IOException If failed to encode + */ + public static void putExtensions(Collection<? extends Map.Entry<String, ?>> exts, Buffer buffer) throws IOException { + int count = GenericUtils.size(exts); + buffer.putInt(count); + if (count <= 0) { + return; + } + + for (Map.Entry<String, ?> ee : exts) { + String name = ee.getKey(); + Object value = ee.getValue(); + @SuppressWarnings("unchecked") + KexExtensionParser<Object> parser = + (KexExtensionParser<Object>) getRegisteredExtensionParser(name); + if (parser != null) { + parser.putExtension(value, buffer); + } else { + buffer.putOptionalBufferedData(value); + } + } + } +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/AbstractKexExtensionParser.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/AbstractKexExtensionParser.java new file mode 100644 index 0000000..1d1a3ba --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/AbstractKexExtensionParser.java @@ -0,0 +1,54 @@ +/* + * 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.common.kex.extension.parser; + +import java.io.IOException; + +import org.apache.sshd.common.kex.extension.KexExtensionParser; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractKexExtensionParser<T> implements KexExtensionParser<T> { + private final String name; + + protected AbstractKexExtensionParser(String name) { + this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No name provided"); + } + + @Override + public String getName() { + return name; + } + + @Override + public void putExtension(T value, Buffer buffer) throws IOException { + buffer.putString(getName()); + int lenPos = buffer.wpos(); + buffer.putInt(0); // placeholder for the encoded value length + encode(value, buffer); + BufferUtils.updateLengthPlaceholder(buffer, lenPos); + } + + protected abstract void encode(T value, Buffer buffer) throws IOException; +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayCompression.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayCompression.java new file mode 100644 index 0000000..21763f8 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayCompression.java @@ -0,0 +1,52 @@ +/* + * 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.common.kex.extension.parser; + +import java.io.IOException; + +import org.apache.sshd.common.util.buffer.Buffer; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.2">RFC-8308 - section 3.2</A> + */ +public class DelayCompression extends AbstractKexExtensionParser<DelayedCompressionAlgorithms> { + public static final String NAME = "delay-compression"; + + public static final DelayCompression INSTANCE = new DelayCompression(); + + public DelayCompression() { + super(NAME); + } + + @Override + public DelayedCompressionAlgorithms parseExtension(Buffer buffer) throws IOException { + DelayedCompressionAlgorithms algos = new DelayedCompressionAlgorithms(); + algos.setClient2Server(buffer.getNameList()); + algos.setServer2Client(buffer.getNameList()); + return algos; + } + + @Override + protected void encode(DelayedCompressionAlgorithms algos, Buffer buffer) throws IOException { + buffer.putNameList(algos.getClient2Server()); + buffer.putNameList(algos.getServer2Client()); + } +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayedCompressionAlgorithms.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayedCompressionAlgorithms.java new file mode 100644 index 0000000..7c01dbb --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/DelayedCompressionAlgorithms.java @@ -0,0 +1,95 @@ +/* + * 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.common.kex.extension.parser; + +import java.util.List; + +import org.apache.sshd.common.util.GenericUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.2">RFC-8308 - section 3.2</A> + */ +public class DelayedCompressionAlgorithms { + private List<String> client2server; + private List<String> server2client; + + public DelayedCompressionAlgorithms() { + super(); + } + + public List<String> getClient2Server() { + return client2server; + } + + public DelayedCompressionAlgorithms withClient2Server(List<String> client2server) { + setClient2Server(client2server); + return this; + } + + public void setClient2Server(List<String> client2server) { + this.client2server = client2server; + } + + public List<String> getServer2Client() { + return server2client; + } + + public DelayedCompressionAlgorithms withServer2Client(List<String> server2client) { + setServer2Client(server2client); + return this; + } + + public void setServer2Client(List<String> server2client) { + this.server2client = server2client; + } + + @Override + public int hashCode() { + // Order might differ + return 31 * GenericUtils.size(getClient2Server()) + + 37 * GenericUtils.size(getServer2Client()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + + DelayedCompressionAlgorithms other = (DelayedCompressionAlgorithms) obj; + return (GenericUtils.findFirstDifferentValueIndex(getClient2Server(), other.getClient2Server()) < 0) + && (GenericUtils.findFirstDifferentValueIndex(getServer2Client(), other.getServer2Client()) < 0); + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "[client2server=" + getClient2Server() + + ", server2client=" + getServer2Client() + + "]"; + } +} \ No newline at end of file diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/Elevation.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/Elevation.java new file mode 100644 index 0000000..abaa15c --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/Elevation.java @@ -0,0 +1,48 @@ +/* + * 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.common.kex.extension.parser; + +import java.io.IOException; + +import org.apache.sshd.common.util.buffer.Buffer; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.4">RFC-8308 - section 3.4</A> + */ +public class Elevation extends AbstractKexExtensionParser<String> { + public static final String NAME = "elevation"; + + public static final Elevation INSTANCE = new Elevation(); + + public Elevation() { + super(NAME); + } + + @Override + public String parseExtension(Buffer buffer) throws IOException { + return buffer.getString(); + } + + @Override + protected void encode(String value, Buffer buffer) throws IOException { + buffer.putString(value); + } +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/NoFlowControl.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/NoFlowControl.java new file mode 100644 index 0000000..f883131 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/NoFlowControl.java @@ -0,0 +1,48 @@ +/* + * 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.common.kex.extension.parser; + +import java.io.IOException; + +import org.apache.sshd.common.util.buffer.Buffer; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + * @see <A HREF="">https://tools.ietf.org/html/rfc8308#section-3.3">RFC-8308 - section 3.3</A> + */ +public class NoFlowControl extends AbstractKexExtensionParser<String> { + public static final String NAME = "no-flow-control"; + + public static final NoFlowControl INSTANCE = new NoFlowControl(); + + public NoFlowControl() { + super(NAME); + } + + @Override + public String parseExtension(Buffer buffer) throws IOException { + return buffer.getString(); + } + + @Override + protected void encode(String value, Buffer buffer) throws IOException { + buffer.putString(value); + } +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/ServerSignatureAlgorithms.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/ServerSignatureAlgorithms.java new file mode 100644 index 0000000..72b1364 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/ServerSignatureAlgorithms.java @@ -0,0 +1,49 @@ +/* + * 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.common.kex.extension.parser; + +import java.io.IOException; +import java.util.List; + +import org.apache.sshd.common.util.buffer.Buffer; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.1">RFC-8308 - section 3.1</A> + */ +public class ServerSignatureAlgorithms extends AbstractKexExtensionParser<List<String>> { + public static final String NAME = "server-sig-algs"; + + public static final ServerSignatureAlgorithms INSTANCE = new ServerSignatureAlgorithms(); + + public ServerSignatureAlgorithms() { + super(NAME); + } + + @Override + public List<String> parseExtension(Buffer buffer) throws IOException { + return buffer.getNameList(); + } + + @Override + protected void encode(List<String> names, Buffer buffer) throws IOException { + buffer.putNameList(names); + } +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java index e7c0ab3..294148d 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java @@ -55,6 +55,8 @@ import java.util.stream.StreamSupport; import javax.management.MBeanException; import javax.management.ReflectionException; +import org.apache.sshd.common.util.functors.UnaryEquator; + /** * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ @@ -404,6 +406,72 @@ public final class GenericUtils { return result; } + public static <T> int findFirstDifferentValueIndex(List<? extends T> c1, List<? extends T> c2) { + return findFirstDifferentValueIndex(c1, c2, UnaryEquator.defaultEquality()); + } + + public static <T> int findFirstDifferentValueIndex( + List<? extends T> c1, List<? extends T> c2, UnaryEquator<? super T> equator) { + Objects.requireNonNull(equator, "No equator provided"); + + int l1 = size(c1); + int l2 = size(c2); + for (int index = 0, count = Math.min(l1, l2); index < count; index++) { + T v1 = c1.get(index); + T v2 = c2.get(index); + if (!equator.test(v1, v2)) { + return index; + } + } + + // all common length items are equal - check length + if (l1 < l2) { + return l1; + } else if (l2 < l1) { + return l2; + } else { + return -1; + } + } + + public static <T> int findFirstDifferentValueIndex(Iterable<? extends T> c1, Iterable<? extends T> c2) { + return findFirstDifferentValueIndex(c1, c2, UnaryEquator.defaultEquality()); + } + + public static <T> int findFirstDifferentValueIndex( + Iterable<? extends T> c1, Iterable<? extends T> c2, UnaryEquator<? super T> equator) { + return findFirstDifferentValueIndex(iteratorOf(c1), iteratorOf(c2), equator); + } + + public static <T> int findFirstDifferentValueIndex(Iterator<? extends T> i1, Iterator<? extends T> i2) { + return findFirstDifferentValueIndex(i1, i2, UnaryEquator.defaultEquality()); + } + + public static <T> int findFirstDifferentValueIndex( + Iterator<? extends T> i1, Iterator<? extends T> i2, UnaryEquator<? super T> equator) { + Objects.requireNonNull(equator, "No equator provided"); + + i1 = iteratorOf(i1); + i2 = iteratorOf(i2); + for (int index = 0;; index++) { + if (i1.hasNext()) { + if (i2.hasNext()) { + T v1 = i1.next(); + T v2 = i2.next(); + if (!equator.test(v1, v2)) { + return index; + } + } else { + return index; + } + } else if (i2.hasNext()) { + return index; + } else { + return -1; // neither has a next value - both exhausted at the same time + } + } + } + public static <T> boolean containsAny(Collection<? extends T> coll, Iterable<? extends T> values) { if (isEmpty(coll)) { return false; @@ -418,28 +486,29 @@ public final class GenericUtils { return false; } - public static <T> void forEach(Iterable<T> values, Consumer<T> consumer) { + public static <T> void forEach(Iterable<? extends T> values, Consumer<? super T> consumer) { if (isNotEmpty(values)) { values.forEach(consumer); } } - public static <T, U> List<U> map(Collection<T> values, Function<? super T, ? extends U> mapper) { + public static <T, U> List<U> map(Collection<? extends T> values, Function<? super T, ? extends U> mapper) { return stream(values).map(mapper).collect(Collectors.toList()); } public static <T, U> NavigableSet<U> mapSort( - Collection<T> values, Function<? super T, ? extends U> mapper, Comparator<U> comparator) { + Collection<? extends T> values, Function<? super T, ? extends U> mapper, Comparator<? super U> comparator) { return stream(values).map(mapper).collect(toSortedSet(comparator)); } public static <T, K, U> NavigableMap<K, U> toSortedMap( - Iterable<T> values, Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Comparator<K> comparator) { + Iterable<? extends T> values, Function<? super T, ? extends K> keyMapper, + Function<? super T, ? extends U> valueMapper, Comparator<? super K> comparator) { return stream(values).collect(toSortedMap(keyMapper, valueMapper, comparator)); } public static <T, K, U> Collector<T, ?, NavigableMap<K, U>> toSortedMap( - Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Comparator<K> comparator) { + Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Comparator<? super K> comparator) { return Collectors.toMap(keyMapper, valueMapper, throwingMerger(), () -> new TreeMap<>(comparator)); } @@ -449,7 +518,7 @@ public final class GenericUtils { }; } - public static <T> Collector<T, ?, NavigableSet<T>> toSortedSet(Comparator<T> comparator) { + public static <T> Collector<T, ?, NavigableSet<T>> toSortedSet(Comparator<? super T> comparator) { return Collectors.toCollection(() -> new TreeSet<>(comparator)); } @@ -629,8 +698,8 @@ public final class GenericUtils { */ public static <T> List<T> selectMatchingMembers(Predicate<? super T> acceptor, Collection<? extends T> values) { return GenericUtils.stream(values) - .filter(acceptor) - .collect(Collectors.toList()); + .filter(acceptor) + .collect(Collectors.toList()); } /** diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java index 8f0dace..5afa514 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java @@ -283,6 +283,39 @@ public abstract class Buffer implements Readable { } /** + * According to <A HREF="https://tools.ietf.org/html/rfc4251#page-10">RFC 4251</A>: + * + * A name-list is represented as a uint32 containing its length (number of bytes + * that follow) followed by a comma-separated list of zero or more names. + * + * @return The parsed result + */ + public List<String> getNameList() { + return getNameList(StandardCharsets.UTF_8); + } + + public List<String> getNameList(Charset charset) { + return getNameList(charset, ','); + } + + public List<String> getNameList(char separator) { + return getNameList(StandardCharsets.UTF_8, separator); + } + + /** + * Parses a string that contains values separated by a delimiter + * + * @param charset The {@link Charset} to use to read the string + * @param separator The separator + * @return A {@link List} of the parsed values + */ + public List<String> getNameList(Charset charset, char separator) { + String list = getString(charset); + String[] values = GenericUtils.split(list, separator); + return GenericUtils.isEmpty(values) ? Collections.emptyList() : Arrays.asList(values); + } + + /** * @param usePrependedLength If {@code true} then there is a 32-bit * value indicating the number of strings to read. Otherwise, the * method will use a "greedy" reading of strings while more @@ -550,6 +583,46 @@ public abstract class Buffer implements Readable { putRawBytes(workBuf, 0, Byte.BYTES); } + /** + * Checks if the <tt>buffer</tt> argument is an array of bytes, + * a {@link Readable} instance or a {@link ByteBuffer} and invokes + * the appropriate {@code putXXX} method. If {@code null} then + * puts an empty byte array value + * + * @param buffer The buffered data object to inspect + * @see #putBufferedData(Object) + */ + public void putOptionalBufferedData(Object buffer) { + if (buffer == null) { + putBytes(GenericUtils.EMPTY_BYTE_ARRAY); + } else { + putBufferedData(buffer); + } + } + + /** + * Checks if the <tt>buffer</tt> argument is an array of bytes, + * a {@link Readable} instance or a {@link ByteBuffer} and invokes + * the appropriate {@code putXXX} method. + * + * @param buffer The (never {@code null}) buffer object to put + * @throws IllegalArgumentException If <tt>buffer</tt> is none of the + * supported types + */ + public void putBufferedData(Object buffer) { + Objects.requireNonNull(buffer, "No buffered data to encode"); + if (buffer instanceof byte[]) { + putBytes((byte[]) buffer); + } else if (buffer instanceof Readable) { + putBuffer((Readable) buffer); + } else if (buffer instanceof ByteBuffer) { + putBuffer((ByteBuffer) buffer); + } else { + throw new IllegalArgumentException("No buffered overload found for " + + ((buffer == null) ? null : buffer.getClass().getName())); + } + } + public void putBuffer(Readable buffer) { putBuffer(buffer, true); } @@ -670,6 +743,36 @@ public abstract class Buffer implements Readable { } } + /** + * According to <A HREF="https://tools.ietf.org/html/rfc4251#page-10">RFC 4251</A>: + * + * A name-list is represented as a uint32 containing its length (number of bytes + * that follow) followed by a comma-separated list of zero or more names. + */ + public void putNameList(Collection<String> names) { + putNameList(names, StandardCharsets.UTF_8); + } + + public void putNameList(Collection<String> names, Charset charset) { + putNameList(names, charset, ','); + } + + public void putNameList(Collection<String> names, char separator) { + putNameList(names, StandardCharsets.UTF_8, separator); + } + + /** + * Adds a string that contains values separated by a delimiter + * + * @param names The names to set + * @param charset The {@link Charset} to use to encode the string + * @param separator The separator + */ + public void putNameList(Collection<String> names, Charset charset, char separator) { + String list = GenericUtils.join(names, separator); + putString(list, charset); + } + public void putString(String string) { putString(string, StandardCharsets.UTF_8); } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java index 31387db..c7f3ed2 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java @@ -238,7 +238,9 @@ public final class BufferUtils { * @throws NumberFormatException If invalid HEX characters found * @see #decodeHex(OutputStream, char, CharSequence, int, int) */ - public static <S extends OutputStream> int decodeHex(S stream, char separator, CharSequence csq) throws IOException { + public static <S extends OutputStream> int decodeHex( + S stream, char separator, CharSequence csq) + throws IOException { return decodeHex(stream, separator, csq, 0, GenericUtils.length(csq)); } @@ -293,7 +295,7 @@ public final class BufferUtils { * @param input The {@link InputStream} * @param buf Work buffer to use * @return The read 32-bit value - * @throws IOException If failed to read 4 bytes or not enough room in + * @throws IOException If failed to read 4 bytes or not enough room in work buffer * @see #readInt(InputStream, byte[], int, int) */ public static int readInt(InputStream input, byte[] buf) throws IOException { @@ -308,8 +310,7 @@ public final class BufferUtils { * @param offset Offset in buffer to us * @param len Available length - must have at least 4 bytes available * @return The read 32-bit value - * @throws IOException If failed to read 4 bytes or not enough room in - * work buffer + * @throws IOException If failed to read 4 bytes or not enough room in work buffer * @see #readUInt(InputStream, byte[], int, int) */ public static int readInt(InputStream input, byte[] buf, int offset, int len) throws IOException { @@ -322,7 +323,7 @@ public final class BufferUtils { * @param input The {@link InputStream} * @param buf Work buffer to use * @return The read 32-bit value - * @throws IOException If failed to read 4 bytes or not enough room in + * @throws IOException If failed to read 4 bytes or not enough room in work buffer * @see #readUInt(InputStream, byte[], int, int) */ public static long readUInt(InputStream input, byte[] buf) throws IOException { @@ -337,8 +338,7 @@ public final class BufferUtils { * @param offset Offset in buffer to us * @param len Available length - must have at least 4 bytes available * @return The read 32-bit value - * @throws IOException If failed to read 4 bytes or not enough room in - * work buffer + * @throws IOException If failed to read 4 bytes or not enough room in work buffer * @see #getUInt(byte[], int, int) */ public static long readUInt(InputStream input, byte[] buf, int offset, int len) throws IOException { @@ -357,8 +357,8 @@ public final class BufferUtils { /** * @param buf A buffer holding a 32-bit unsigned integer in <B>big endian</B> - * format. <B>Note:</B> if more than 4 bytes are available, then only the - * <U>first</U> 4 bytes in the buffer will be used + * format. <B>Note:</B> if more than 4 bytes are available, then only the + * <U>first</U> 4 bytes in the buffer will be used * @return The result as a {@code long} whose 32 high-order bits are zero * @see #getUInt(byte[], int, int) */ @@ -367,12 +367,11 @@ public final class BufferUtils { } /** - * @param buf A buffer holding a 32-bit unsigned integer in <B>big endian</B> - * format. + * @param buf A buffer holding a 32-bit unsigned integer in <B>big endian</B> format. * @param off The offset of the data in the buffer * @param len The available data length. <B>Note:</B> if more than 4 bytes - * are available, then only the <U>first</U> 4 bytes in the buffer will be - * used (starting at the specified <tt>offset</tt>) + * are available, then only the <U>first</U> 4 bytes in the buffer will be + * used (starting at the specified <tt>offset</tt>) * @return The result as a {@code long} whose 32 high-order bits are zero */ public static long getUInt(byte[] buf, int off, int len) { @@ -393,7 +392,7 @@ public final class BufferUtils { * @param output The {@link OutputStream} to write the value * @param value The 32-bit value * @param buf A work buffer to use - must have enough space to contain 4 bytes - * @throws IOException If failed to write the value or work buffer to small + * @throws IOException If failed to write the value or work buffer too small * @see #writeInt(OutputStream, int, byte[], int, int) */ public static void writeInt(OutputStream output, int value, byte[] buf) throws IOException { @@ -408,7 +407,7 @@ public final class BufferUtils { * @param buf A work buffer to use - must have enough space to contain 4 bytes * @param off The offset to write the value * @param len The available space - * @throws IOException If failed to write the value or work buffer to small + * @throws IOException If failed to write the value or work buffer too small * @see #writeUInt(OutputStream, long, byte[], int, int) */ public static void writeInt( @@ -423,7 +422,7 @@ public final class BufferUtils { * @param output The {@link OutputStream} to write the value * @param value The 32-bit value * @param buf A work buffer to use - must have enough space to contain 4 bytes - * @throws IOException If failed to write the value or work buffer to small + * @throws IOException If failed to write the value or work buffer too small * @see #writeUInt(OutputStream, long, byte[], int, int) */ public static void writeUInt(OutputStream output, long value, byte[] buf) throws IOException { diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/functors/UnaryEquator.java b/sshd-common/src/main/java/org/apache/sshd/common/util/functors/UnaryEquator.java new file mode 100644 index 0000000..4834065 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/functors/UnaryEquator.java @@ -0,0 +1,119 @@ +/* + * 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.common.util.functors; + +import java.util.Comparator; +import java.util.Objects; +import java.util.function.BiPredicate; + +import org.apache.sshd.common.util.GenericUtils; + +/** + * Checks equality between 2 entities of same type + * @param <T> Type of compared entity + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface UnaryEquator<T> extends BiPredicate<T, T> { + /** + * Returns a composed equator that represents a short-circuiting logical + * AND of this equator and another. When evaluating the composed + * equator, if this equator is {@code false}, then the {@code other} + * equator is not evaluated. + * + * @param other The other (never {@code null} equator + * @return The compound equator + */ + default UnaryEquator<T> and(UnaryEquator<? super T> other) { + Objects.requireNonNull(other, "No other equator to compose"); + return (t1, t2) -> this.test(t1, t2) && other.test(t1, t2); + } + + /** + * Returns a composed equator that represents a short-circuiting logical + * AND of this equator and another. When evaluating the composed + * equator, if this equator is {@code true}, then the {@code other} + * equator is not evaluated. + * + * @param other The other (never {@code null} equator + * @return The compound equator + */ + default UnaryEquator<T> or(UnaryEquator<? super T> other) { + Objects.requireNonNull(other, "No other equator to compose"); + return (t1, t2) -> this.test(t1, t2) || other.test(t1, t2); + } + + /** + * @return an equator that represents the logical negation of this one + */ + @Override + default UnaryEquator<T> negate() { + return (t1, t2) -> !this.test(t1, t2); + } + + /** + * @param <T> Type of entity + * @return The default equality checker + * @see EqualityUtils#isEqualValue(Object, Object) + */ + static <T> UnaryEquator<T> defaultEquality() { + return Objects::equals; + } + + /** + * @param <T> Type of entity + * @return An equator that checks reference equality + * @see EqualityUtils#isSameReference(Object, Object) + */ + static <T> UnaryEquator<T> referenceEquality() { + return GenericUtils::isSameReference; + } + + /** + * Converts a {@link Comparator} into a {@link UnaryEquator} that + * returns {@code true} if the comparator returns zero + * + * @param <T> Type of entity + * @param c The (never {@code null}) comparator + * @return The equivalent equator + */ + static <T> UnaryEquator<T> comparing(Comparator<? super T> c) { + Objects.requireNonNull(c, "No comparator"); + return (o1, o2) -> c.compare(o1, o2) == 0; + } + + /** + * @param <T> Type of evaluated entity + * @return A {@link UnaryEquator} that returns always {@code true} + * @see <A HREF="https://en.wikipedia.org/wiki/Tee_(symbol)">verum</A> + */ + static <T> UnaryEquator<T> verum() { + return (o1, o2) -> true; + } + + /** + * @param <T> Type of evaluated entity + * @return A {@link UnaryEquator} that returns always {@code false} + * @see <A HREF="https://en.wikipedia.org/wiki/Up_tack">falsum</A> + */ + static <T> UnaryEquator<T> falsum() { + return (o1, o2) -> false; + } +} diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java index 96a204f..677906e 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java @@ -33,7 +33,7 @@ import org.apache.sshd.common.util.buffer.Buffer; /** * Manages a "keyboard-interactive" exchange according to - * <A HREF="https://www.ietf.org/rfc/rfc4256.txt">RFC4256</A> + * <A HREF="https://tools.ietf.org/html/rfc4256">RFC4256</A> * * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ @@ -249,7 +249,7 @@ public class UserAuthKeyboardInteractive extends AbstractUserAuth { * length as the prompts * @return The response for each prompt - if {@code null} then the assumption * is that some internal error occurred and no response is sent. <B>Note:</B> - * according to <A HREF="https://www.ietf.org/rfc/rfc4256.txt">RFC4256</A> + * according to <A HREF="https://tools.ietf.org/html/rfc4256">RFC4256</A> * the number of responses should be <U>exactly</U> the same as the number * of prompts. However, since it is the <U>server's</U> responsibility to * enforce this we do not validate the response (other than logging it as diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java index 56f5e1d..5c294fa 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java @@ -26,7 +26,7 @@ import org.apache.sshd.client.session.ClientSession; * Interface used by the ssh client to communicate with the end user. * * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> - * @see <a href="https://www.ietf.org/rfc/rfc4256.txt">RFC 4256</A> + * @see <a href="https://tools.ietf.org/html/rfc4256">RFC 4256</A> */ public interface UserInteraction { /** diff --git a/sshd-core/src/main/java/org/apache/sshd/common/channel/Channel.java b/sshd-core/src/main/java/org/apache/sshd/common/channel/Channel.java index 4fe1dbe..4f89ce8 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/channel/Channel.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/channel/Channel.java @@ -169,7 +169,7 @@ public interface Channel /** * @return {@code true} if the peer signaled that it will not send any * more data - * @see <A HREF="https://www.ietf.org/rfc/rfc4254.txt">RFC 4254 - section 5.3 - SSH_MSG_CHANNEL_EOE</A> + * @see <A HREF="https://tools.ietf.org/html/rfc4254#section-5.3">RFC 4254 - section 5.3 - SSH_MSG_CHANNEL_EOF</A> */ boolean isEofSignalled(); diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java index eb502e4..da4ed0e 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java @@ -25,6 +25,7 @@ import java.util.List; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.cipher.Cipher; import org.apache.sshd.common.compression.Compression; +import org.apache.sshd.common.kex.extension.KexExtensionHandler; import org.apache.sshd.common.mac.Mac; import org.apache.sshd.common.signature.Signature; import org.apache.sshd.common.util.GenericUtils; @@ -42,6 +43,7 @@ public abstract class AbstractKexFactoryManager private List<NamedFactory<Compression>> compressionFactories; private List<NamedFactory<Mac>> macFactories; private List<NamedFactory<Signature>> signatureFactories; + private KexExtensionHandler kexExtensionHandler; protected AbstractKexFactoryManager() { this(null); @@ -115,6 +117,18 @@ public abstract class AbstractKexFactoryManager this.signatureFactories = signatureFactories; } + @Override + public KexExtensionHandler getKexExtensionHandler() { + KexFactoryManager parent = getDelegate(); + return resolveEffectiveProvider( + KexExtensionHandler.class, kexExtensionHandler, (parent == null) ? null : parent.getKexExtensionHandler()); + } + + @Override + public void setKexExtensionHandler(KexExtensionHandler kexExtensionHandler) { + this.kexExtensionHandler = kexExtensionHandler; + } + protected <V> List<NamedFactory<V>> resolveEffectiveFactories( Class<V> factoryType, List<NamedFactory<V>> local, List<NamedFactory<V>> inherited) { if (GenericUtils.isEmpty(local)) { diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/KexFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/KexFactoryManager.java index 6e46154..6c873d2 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/KexFactoryManager.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/KexFactoryManager.java @@ -30,6 +30,7 @@ import org.apache.sshd.common.cipher.BuiltinCiphers; import org.apache.sshd.common.cipher.Cipher; import org.apache.sshd.common.compression.BuiltinCompressions; import org.apache.sshd.common.compression.Compression; +import org.apache.sshd.common.kex.extension.KexExtensionHandlerManager; import org.apache.sshd.common.mac.BuiltinMacs; import org.apache.sshd.common.mac.Mac; import org.apache.sshd.common.signature.SignatureFactoriesManager; @@ -40,7 +41,7 @@ import org.apache.sshd.common.util.ValidateUtils; * Holds KEX negotiation stage configuration * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ -public interface KexFactoryManager extends SignatureFactoriesManager { +public interface KexFactoryManager extends SignatureFactoriesManager, KexExtensionHandlerManager { /** * Retrieve the list of named factories for <code>KeyExchange</code>. * diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java new file mode 100644 index 0000000..0464bc3 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java @@ -0,0 +1,102 @@ +/* + * 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.common.kex.extension; + +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.util.buffer.Buffer; + +/** + * Used to support <A HREF="https://tools.ietf.org/html/rfc8308">RFC 8308</A> + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface KexExtensionHandler { + enum KexPhase { + NEWKEYS, + AUTHOK; + + public static final Set<KexPhase> VALUES = + Collections.unmodifiableSet(EnumSet.allOf(KexPhase.class)); + } + + /** + * @param session The {@link Session} about to execute KEX + * @return {@code true} whether to declare KEX extensions availability for the session + * @throws IOException If failed to process the request + */ + default boolean isKexExtensionsAvailable(Session session) throws IOException { + return true; + } + + /** + * Invoked in order to allow the handler to send an {@code SSH_MSG_EXT_INFO} message. + * + * @param session The {@link Session} + * @param phase The phase at which the handler is invoked + * @throws IOException If failed to handle the invocation + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.4">RFC-8308 - section 2.4</A> + */ + default void sendKexExtensions(Session session, KexPhase phase) throws IOException { + // do nothing + } + + /** + * Parses the {@code SSH_MSG_EXT_INFO} message + * + * @param session The {@link Session} through which the message was received + * @param buffer The message buffer + * @throws IOException If failed to handle the message + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.3">RFC-8308 - section 2.3</A> + * @see #handleKexExtensionRequest(Session, int, int, String, byte[]) + */ + default void handleKexExtensionsMessage(Session session, Buffer buffer) throws IOException { + int count = buffer.getInt(); + for (int index = 0; index < count; index++) { + String name = buffer.getString(); + byte[] data = buffer.getBytes(); + if (!handleKexExtensionRequest(session, index, count, name, data)) { + return; + } + } + } + + /** + * Invoked by {@link #handleKexExtensionsMessage(Session, Buffer)} in order to + * handle a specific extension. + * + * @param session The {@link Session} through which the message was received + * @param index The 0-based extension index + * @param count The total extensions in the message + * @param name The extension name + * @param data The extension data + * @return {@code true} whether to proceed to the next extension or + * stop processing the rest + * @throws IOException If failed to handle the extension + */ + default boolean handleKexExtensionRequest( + Session session, int index, int count, String name, byte[] data) throws IOException { + return true; + } +} diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerManager.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerManager.java new file mode 100644 index 0000000..5eb87ef --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerManager.java @@ -0,0 +1,31 @@ +/* + * 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.common.kex.extension; + +/** + * TODO Add javadoc + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface KexExtensionHandlerManager { + KexExtensionHandler getKexExtensionHandler(); + + void setKexExtensionHandler(KexExtensionHandler handler); +} diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java index 48ab970..e229eb0 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java @@ -24,7 +24,8 @@ import org.apache.sshd.common.util.buffer.Buffer; /** * Provides a way to listen and handle the {@code SSH_MSG_IGNORE} and - * {@code SSH_MSG_DEBUG} messages that are received by a session. + * {@code SSH_MSG_DEBUG} messages that are received by a session, as well + * as proprietary and/or extension messages. * * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java index d1e7dab..76a0d85 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionDisconnectHandler.java @@ -20,8 +20,10 @@ package org.apache.sshd.common.session; import java.io.IOException; +import java.util.Map; import org.apache.sshd.common.Service; +import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.session.Session.TimeoutStatus; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.server.ServerFactoryManager; @@ -129,4 +131,27 @@ public interface SessionDisconnectHandler { throws IOException { return false; } + + /** + * Invoked if after KEX negotiation parameters resolved one of the options + * violates some internal constraint (e.g., cannot negotiate a value, or + * <A HREF="https://tools.ietf.org/html/rfc8308#section-2.2">RFC 8308 - section 2.2</A>). + * + * @param session The session where the violation occurred + * @param c2sOptions The client options + * @param s2cOptions The server options + * @param negotiatedGuess The negotiated KEX options + * @param option The violating {@link KexProposalOption} + * @return {@code true} if disregard the violation - if {@code false} then + * session will disconnect + */ + default boolean handleKexDisconnectReason( + Session session, Map<KexProposalOption, String> c2sOptions, Map<KexProposalOption, String> s2cOptions, + Map<KexProposalOption, String> negotiatedGuess, KexProposalOption option) { + if (KexProposalOption.S2CLANG.equals(option) || KexProposalOption.C2SLANG.equals(option)) { + return true; // OK if cannot agree on a language + } + + return false; + } } diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java index ef345fe..6a26457 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java @@ -63,10 +63,14 @@ import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.kex.KexState; import org.apache.sshd.common.kex.KeyExchange; +import org.apache.sshd.common.kex.extension.KexExtensionHandler; +import org.apache.sshd.common.kex.extension.KexExtensionHandler.KexPhase; +import org.apache.sshd.common.kex.extension.KexExtensions; import org.apache.sshd.common.mac.Mac; import org.apache.sshd.common.mac.MacInformation; import org.apache.sshd.common.random.Random; import org.apache.sshd.common.session.ReservedSessionMessagesHandler; +import org.apache.sshd.common.session.SessionDisconnectHandler; import org.apache.sshd.common.session.SessionListener; import org.apache.sshd.common.session.SessionWorkBuffer; import org.apache.sshd.common.util.EventListenerUtils; @@ -407,6 +411,9 @@ public abstract class AbstractSession extends SessionHelper { case SshConstants.SSH_MSG_NEWKEYS: handleNewKeys(cmd, buffer); break; + case KexExtensions.SSH_MSG_EXT_INFO: + handleKexExtension(cmd, buffer); + break; default: if ((cmd >= SshConstants.SSH_MSG_KEX_FIRST) && (cmd <= SshConstants.SSH_MSG_KEX_LAST)) { if (firstKexPacketFollows != null) { @@ -468,12 +475,54 @@ public abstract class AbstractSession extends SessionHelper { return true; } + /** + * Compares the specified {@link KexProposalOption} option value for client vs. server + * + * @param option The option to check + * @return {@code null} if option is equal, otherwise a kex/value pair where key=client + * option value and value=the server-side one + */ protected SimpleImmutableEntry<String, String> comparePreferredKexProposalOption(KexProposalOption option) { String[] clientPreferences = GenericUtils.split(clientProposal.get(option), ','); String clientValue = clientPreferences[0]; String[] serverPreferences = GenericUtils.split(serverProposal.get(option), ','); String serverValue = serverPreferences[0]; - return clientValue.equals(serverValue) ? null : new SimpleImmutableEntry<>(clientValue, serverValue); + return Objects.equals(clientValue, serverValue) ? null : new SimpleImmutableEntry<>(clientValue, serverValue); + } + + /** + * Send a message to put new keys into use. + * + * @return An {@link IoWriteFuture} that can be used to wait and + * check the result of sending the packet + * @throws IOException if an error occurs sending the message + */ + protected IoWriteFuture sendNewKeys() throws IOException { + if (log.isDebugEnabled()) { + log.debug("sendNewKeys({}) Send SSH_MSG_NEWKEYS", this); + } + + Buffer buffer = createBuffer(SshConstants.SSH_MSG_NEWKEYS, Byte.SIZE); + IoWriteFuture future = writePacket(buffer); + /* + * According to https://tools.ietf.org/html/rfc8308#section-2.4: + * + * + * If a client sends SSH_MSG_EXT_INFO, it MUST send it as the next packet + * following the client's first SSH_MSG_NEWKEYS message to the server. + * + * If a server sends SSH_MSG_EXT_INFO, it MAY send it at zero, one, or + * both of the following opportunities: + * + * + As the next packet following the server's first SSH_MSG_NEWKEYS. + */ + KexExtensionHandler extHandler = getKexExtensionHandler(); + if ((extHandler == null) || (!extHandler.isKexExtensionsAvailable(this))) { + return future; + } + + extHandler.sendKexExtensions(this, KexPhase.NEWKEYS); + return future; } protected void handleKexMessage(int cmd, Buffer buffer) throws Exception { @@ -494,6 +543,16 @@ public abstract class AbstractSession extends SessionHelper { } } + protected void handleKexExtension(int cmd, Buffer buffer) throws Exception { + KexExtensionHandler extHandler = getKexExtensionHandler(); + if ((extHandler == null) || (!extHandler.isKexExtensionsAvailable(this))) { + notImplemented(cmd, buffer); + return; + } + + extHandler.handleKexExtensionsMessage(this, buffer); + } + protected void handleServiceRequest(Buffer buffer) throws Exception { String serviceName = buffer.getString(); handleServiceRequest(serviceName, buffer); @@ -1417,8 +1476,9 @@ public abstract class AbstractSession extends SessionHelper { * the {@link #negotiationResult} property. * * @return The negotiated options {@link Map} + * @throws IOException If negotiation failed */ - protected Map<KexProposalOption, String> negotiate() { + protected Map<KexProposalOption, String> negotiate() throws IOException { Map<KexProposalOption, String> c2sOptions = getClientKexProposals(); Map<KexProposalOption, String> s2cOptions = getServerKexProposals(); signalNegotiationStart(c2sOptions, s2cOptions); @@ -1426,12 +1486,21 @@ public abstract class AbstractSession extends SessionHelper { Map<KexProposalOption, String> guess = new EnumMap<>(KexProposalOption.class); Map<KexProposalOption, String> negotiatedGuess = Collections.unmodifiableMap(guess); try { + boolean debugEnabled = log.isDebugEnabled(); boolean traceEnabled = log.isTraceEnabled(); + SessionDisconnectHandler discHandler = getSessionDisconnectHandler(); for (KexProposalOption paramType : KexProposalOption.VALUES) { String clientParamValue = c2sOptions.get(paramType); String serverParamValue = s2cOptions.get(paramType); String[] c = GenericUtils.split(clientParamValue, ','); String[] s = GenericUtils.split(serverParamValue, ','); + /* + * According to https://tools.ietf.org/html/rfc8308#section-2.2: + * + * Implementations MAY disconnect if the counterpart sends an incorrect (KEX extension) indicator + * + * TODO - for now we do not enforce this + */ for (String ci : c) { for (String si : s) { if (ci.equals(si)) { @@ -1448,25 +1517,55 @@ public abstract class AbstractSession extends SessionHelper { // check if reached an agreement String value = guess.get(paramType); - if (value == null) { - String message = "Unable to negotiate key exchange for " + paramType.getDescription() - + " (client: " + clientParamValue + " / server: " + serverParamValue + ")"; - // OK if could not negotiate languages - if (KexProposalOption.S2CLANG.equals(paramType) || KexProposalOption.C2SLANG.equals(paramType)) { - if (traceEnabled) { - log.trace("negotiate({}) {}", this, message); - } - } else { - throw new IllegalStateException(message); + if (value != null) { + if (traceEnabled) { + log.trace("negotiate({})[{}] guess={} (client={} / server={})", + this, paramType.getDescription(), value, clientParamValue, serverParamValue); } - } else { + continue; + } + + if ((discHandler != null) + && discHandler.handleKexDisconnectReason( + this, c2sOptions, s2cOptions, negotiatedGuess, paramType)) { + if (debugEnabled) { + log.debug("negotiate({}) ignore missing value for KEX option={}", this, paramType); + } + continue; + } + + String message = "Unable to negotiate key exchange for " + paramType.getDescription() + + " (client: " + clientParamValue + " / server: " + serverParamValue + ")"; + // OK if could not negotiate languages + if (KexProposalOption.S2CLANG.equals(paramType) || KexProposalOption.C2SLANG.equals(paramType)) { if (traceEnabled) { - log.trace("negotiate(" + this + ")[" + paramType.getDescription() + "] guess=" + value - + " (client: " + clientParamValue + " / server: " + serverParamValue + ")"); + log.trace("negotiate({}) {}", this, message); + } + } else { + throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, message); + } + } + + /* + * According to https://tools.ietf.org/html/rfc8308#section-2.2: + * + * If "ext-info-c" or "ext-info-s" ends up being negotiated as a + * key exchange method, the parties MUST disconnect. + */ + String kexOption = guess.get(KexProposalOption.ALGORITHMS); + if (KexExtensions.CLIENT_KEX_EXTENSION.equalsIgnoreCase(kexOption) + || KexExtensions.SERVER_KEX_EXTENSION.equalsIgnoreCase(kexOption)) { + if ((discHandler != null) + && discHandler.handleKexDisconnectReason( + this, c2sOptions, s2cOptions, negotiatedGuess, KexProposalOption.ALGORITHMS)) { + if (debugEnabled) { + log.debug("negotiate({}) ignore violating {} KEX option={}", this, KexProposalOption.ALGORITHMS, kexOption); } + } else { + throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, "Illegal KEX option negotiated: " + kexOption); } } - } catch (RuntimeException | Error e) { + } catch (IOException | RuntimeException | Error e) { signalNegotiationEnd(c2sOptions, s2cOptions, negotiatedGuess, e); throw e; } @@ -1803,6 +1902,22 @@ public abstract class AbstractSession extends SessionHelper { return rekey; } + @Override + protected String resolveSessionKexProposal(String hostKeyTypes) throws IOException { + String proposal = super.resolveSessionKexProposal(hostKeyTypes); + KexExtensionHandler extHandler = getKexExtensionHandler(); + if ((extHandler == null) || (!extHandler.isKexExtensionsAvailable(this))) { + return proposal; + } + + String extType = isServerSession() ? KexExtensions.SERVER_KEX_EXTENSION : KexExtensions.CLIENT_KEX_EXTENSION; + if (GenericUtils.isEmpty(proposal)) { + return extType; + } else { + return proposal + "," + extType; + } + } + protected byte[] sendKexInit() throws IOException, GeneralSecurityException { String resolvedAlgorithms = resolveAvailableSignaturesProposal(); if (GenericUtils.isEmpty(resolvedAlgorithms)) { diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java index 2846157..1bc8aea 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java @@ -777,17 +777,22 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements } } + protected String resolveSessionKexProposal(String hostKeyTypes) throws IOException { + return NamedResource.getNames( + ValidateUtils.checkNotNullAndNotEmpty(getKeyExchangeFactories(), "No KEX factories")); + } + /** * Create our proposal for SSH negotiation * * @param hostKeyTypes The comma-separated list of supported host key types * @return The proposal {@link Map} + * @throws IOException If internal problem - e.g., KEX extensions negotiation issue */ - protected Map<KexProposalOption, String> createProposal(String hostKeyTypes) { + protected Map<KexProposalOption, String> createProposal(String hostKeyTypes) throws IOException { Map<KexProposalOption, String> proposal = new EnumMap<>(KexProposalOption.class); - proposal.put(KexProposalOption.ALGORITHMS, - NamedResource.getNames( - ValidateUtils.checkNotNullAndNotEmpty(getKeyExchangeFactories(), "No KEX factories"))); + String kexProposal = resolveSessionKexProposal(hostKeyTypes); + proposal.put(KexProposalOption.ALGORITHMS, kexProposal); proposal.put(KexProposalOption.SERVERKEYS, hostKeyTypes); String ciphers = NamedResource.getNames( @@ -888,21 +893,6 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements listener.sessionNegotiationEnd(this, c2sOptions, s2cOptions, negotiatedGuess, null); } - /** - * Send a message to put new keys into use. - * - * @return An {@link IoWriteFuture} that can be used to wait and - * check the result of sending the packet - * @throws IOException if an error occurs sending the message - */ - protected IoWriteFuture sendNewKeys() throws IOException { - if (log.isDebugEnabled()) { - log.debug("sendNewKeys({}) Send SSH_MSG_NEWKEYS", this); - } - Buffer buffer = createBuffer(SshConstants.SSH_MSG_NEWKEYS, Byte.SIZE); - return writePacket(buffer); - } - @Override public void disconnect(int reason, String msg) throws IOException { log.info("Disconnecting({}): {} - {}", this, SshConstants.getDisconnectReasonName(reason), msg); diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/hostbased/HostBasedAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/hostbased/HostBasedAuthenticator.java index 3f069c7..64fe3f5 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/hostbased/HostBasedAuthenticator.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/hostbased/HostBasedAuthenticator.java @@ -28,7 +28,7 @@ import org.apache.sshd.server.session.ServerSession; /** * Invoked when "hostbased" authentication is used * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> - * @see <A HREF="https://www.ietf.org/rfc/rfc4252.txt">RFC 4252 - section 9</A> + * @see <A HREF="https://tools.ietf.org/html/rfc4252#section-9">RFC 4252 - section 9</A> */ @FunctionalInterface public interface HostBasedAuthenticator { diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/InteractiveChallenge.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/InteractiveChallenge.java index 94c0be2..a2f1b24 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/InteractiveChallenge.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/InteractiveChallenge.java @@ -29,7 +29,7 @@ import org.apache.sshd.common.util.buffer.Buffer; /** * Represents a server "challenge" as per - * <A HREF="https://www.ietf.org/rfc/rfc4256.txt">RFC-4256</A> + * <A HREF="https://tools.ietf.org/html/rfc4256">RFC-4256</A> * * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/KeyboardInteractiveAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/KeyboardInteractiveAuthenticator.java index 5b78ead..75f5a76 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/KeyboardInteractiveAuthenticator.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/KeyboardInteractiveAuthenticator.java @@ -25,7 +25,7 @@ import org.apache.sshd.server.session.ServerSession; /** * Provides pluggable authentication using the "keyboard-interactive" - * method as specified by <A HREF="https://www.ietf.org/rfc/rfc4256.txt">RFC-4256</A>? + * method as specified by <A HREF="https://tools.ietf.org/html/rfc4256">RFC-4256</A>? * * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/UserAuthKeyboardInteractive.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/UserAuthKeyboardInteractive.java index e032473..3571837 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/UserAuthKeyboardInteractive.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/keyboard/UserAuthKeyboardInteractive.java @@ -31,7 +31,7 @@ import org.apache.sshd.server.auth.AbstractUserAuth; import org.apache.sshd.server.session.ServerSession; /** - * Issue a "keyboard-interactive" command according to <A HREF="https://www.ietf.org/rfc/rfc4256.txt">RFC4256</A> + * Issue a "keyboard-interactive" command according to <A HREF="https://tools.ietf.org/html/rfc4256">RFC4256</A> * * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordChangeRequiredException.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordChangeRequiredException.java index a0fc27d..678dcaa 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordChangeRequiredException.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordChangeRequiredException.java @@ -23,7 +23,7 @@ package org.apache.sshd.server.auth.password; * A special exception that can be thrown by the {@link PasswordAuthenticator} * to indicate that the password requires changing or is not string enough * - * @see <A HREF="https://www.ietf.org/rfc/rfc4252.txt">RFC-4252 section 8 - SSH_MSG_USERAUTH_PASSWD_CHANGEREQ</A> + * @see <A HREF="https://tools.ietf.org/html/rfc4252#section-8">RFC-4252 section 8 - SSH_MSG_USERAUTH_PASSWD_CHANGEREQ</A> * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ public class PasswordChangeRequiredException extends RuntimeException { diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java index 2aafa06..79b7bb7 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java @@ -43,6 +43,8 @@ import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.kex.KexFactoryManager; import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.kex.KexState; +import org.apache.sshd.common.kex.extension.KexExtensionHandler; +import org.apache.sshd.common.kex.extension.KexExtensionHandler.KexPhase; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.ConnectionService; import org.apache.sshd.common.session.SessionContext; @@ -265,10 +267,25 @@ public abstract class AbstractServerSession extends AbstractSession implements S "Authentication success signalled though KEX state=" + curState); } + KexExtensionHandler extHandler = getKexExtensionHandler(); + if ((extHandler != null) && extHandler.isKexExtensionsAvailable(this)) { + extHandler.sendKexExtensions(this, KexPhase.AUTHOK); + } + Buffer response = createBuffer(SshConstants.SSH_MSG_USERAUTH_SUCCESS, Byte.SIZE); IoWriteFuture future; IoSession networkSession = getIoSession(); synchronized (encodeLock) { + /* + * According to https://tools.ietf.org/html/rfc8308#section-2.4 + * + * If a server sends SSH_MSG_EXT_INFO, it MAY send it at zero, one, or + * both of the following opportunities: + * + * ... + * + * + Immediately preceding the server's SSH_MSG_USERAUTH_SUCCESS + */ Buffer packet = resolveOutputPacket(response); setUsername(username); diff --git a/sshd-core/src/test/java/org/apache/sshd/common/kex/KexFactoryManagerTest.java b/sshd-core/src/test/java/org/apache/sshd/common/kex/KexFactoryManagerTest.java index 8242a62..0832ab4 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/kex/KexFactoryManagerTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/kex/KexFactoryManagerTest.java @@ -27,6 +27,7 @@ import org.apache.sshd.common.cipher.BuiltinCiphers; import org.apache.sshd.common.cipher.Cipher; import org.apache.sshd.common.compression.BuiltinCompressions; import org.apache.sshd.common.compression.Compression; +import org.apache.sshd.common.kex.extension.KexExtensionHandler; import org.apache.sshd.common.mac.BuiltinMacs; import org.apache.sshd.common.mac.Mac; import org.apache.sshd.common.signature.BuiltinSignatures; @@ -122,6 +123,7 @@ public class KexFactoryManagerTest extends BaseTestSupport { private List<NamedFactory<Cipher>> ciphers; private List<NamedFactory<Mac>> macs; private List<NamedFactory<Signature>> signatures; + private KexExtensionHandler kexExtensionHandler; TestKexFactoryManager() { super(); @@ -176,5 +178,15 @@ public class KexFactoryManagerTest extends BaseTestSupport { public void setMacFactories(List<NamedFactory<Mac>> macFactories) { macs = macFactories; } + + @Override + public KexExtensionHandler getKexExtensionHandler() { + return kexExtensionHandler; + } + + @Override + public void setKexExtensionHandler(KexExtensionHandler handler) { + this.kexExtensionHandler = handler; + } } } diff --git a/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java new file mode 100644 index 0000000..cd08eba --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java @@ -0,0 +1,104 @@ +/* + * 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.common.kex.extension; + +import java.io.IOException; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.sshd.common.kex.extension.parser.DelayCompression; +import org.apache.sshd.common.kex.extension.parser.DelayedCompressionAlgorithms; +import org.apache.sshd.common.kex.extension.parser.Elevation; +import org.apache.sshd.common.kex.extension.parser.NoFlowControl; +import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.util.test.JUnitTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.MethodSorters; +import org.mockito.Mockito; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Category({ NoIoTestCase.class }) +public class KexExtensionHandlerTest extends JUnitTestSupport { + public KexExtensionHandlerTest() { + super(); + } + + @Test + public void testEncodeDecodeExtensionMessage() throws IOException { + List<Map.Entry<String, ?>> expected = Arrays.asList( + new SimpleImmutableEntry<>(DelayCompression.NAME, + new DelayedCompressionAlgorithms() + .withClient2Server( + Arrays.asList(getClass().getSimpleName(), getCurrentTestName())) + .withServer2Client( + Arrays.asList(getClass().getPackage().getName(), getCurrentTestName()))), + new SimpleImmutableEntry<>(ServerSignatureAlgorithms.NAME, + Arrays.asList(getClass().getPackage().getName(), getClass().getSimpleName(), getCurrentTestName())), + new SimpleImmutableEntry<>(NoFlowControl.NAME, getCurrentTestName()), + new SimpleImmutableEntry<>(Elevation.NAME, getCurrentTestName())); + Buffer buffer = new ByteArrayBuffer(); + KexExtensions.putExtensions(expected, buffer); + + List<Map.Entry<String, ?>> actual = new ArrayList<>(expected.size()); + KexExtensionHandler handler = new KexExtensionHandler() { + @Override + public boolean handleKexExtensionRequest(Session session, int index, int count, String name, byte[] data) + throws IOException { + KexExtensionParser<?> parser = KexExtensions.getRegisteredExtensionParser(name); + assertNotNull("No parser found for extension=" + name, parser); + + Object value = parser.parseExtension(data); + assertNotNull("No value extracted for extension=" + name, value); + actual.add(new SimpleImmutableEntry<>(name, value)); + return true; + } + }; + Session session = Mockito.mock(Session.class); + handler.handleKexExtensionsMessage(session, buffer); + + assertEquals("Mismatched recovered extensions count", expected.size(), actual.size()); + for (int index = 0; index < actual.size(); index++) { + Map.Entry<String, ?> expEntry = expected.get(index); + String name = expEntry.getKey(); + Map.Entry<String, ?> actEntry = actual.get(index); + assertEquals("Mismatched extension name at index=" + index, name, actEntry.getKey()); + + Object expValue = expEntry.getValue(); + Object actValue = actEntry.getValue(); + if (expValue instanceof List<?>) { + assertListEquals(name, (List<?>) expValue, (List<?>) actValue); + } else { + assertEquals("Mismatched values for extension=" + name, expValue, actValue); + } + } + } +} diff --git a/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java b/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java index 224bfa2..73cfc25 100644 --- a/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/server/ServerTest.java @@ -428,7 +428,7 @@ public class ServerTest extends BaseTestSupport { protected ClientSessionImpl doCreateSession(IoSession ioSession) throws Exception { return new ClientSessionImpl(getClient(), ioSession) { @Override - protected Map<KexProposalOption, String> createProposal(String hostKeyTypes) { + protected Map<KexProposalOption, String> createProposal(String hostKeyTypes) throws IOException { Map<KexProposalOption, String> proposal = super.createProposal(hostKeyTypes); proposal.put(KexProposalOption.S2CLANG, "en-US"); proposal.put(KexProposalOption.C2SLANG, "en-US"); @@ -491,7 +491,7 @@ public class ServerTest extends BaseTestSupport { protected ServerSessionImpl doCreateSession(IoSession ioSession) throws Exception { return new ServerSessionImpl(getServer(), ioSession) { @Override - protected Map<KexProposalOption, String> createProposal(String hostKeyTypes) { + protected Map<KexProposalOption, String> createProposal(String hostKeyTypes) throws IOException { Map<KexProposalOption, String> proposal = super.createProposal(hostKeyTypes); proposal.put(KexProposalOption.C2SCOMP, getCurrentTestName()); proposal.put(KexProposalOption.S2CCOMP, getCurrentTestName()); @@ -507,7 +507,7 @@ public class ServerTest extends BaseTestSupport { protected ClientSessionImpl doCreateSession(IoSession ioSession) throws Exception { return new ClientSessionImpl(getClient(), ioSession) { @Override - protected Map<KexProposalOption, String> createProposal(String hostKeyTypes) { + protected Map<KexProposalOption, String> createProposal(String hostKeyTypes) throws IOException { Map<KexProposalOption, String> proposal = super.createProposal(hostKeyTypes); proposal.put(KexProposalOption.C2SCOMP, getCurrentTestName()); proposal.put(KexProposalOption.S2CCOMP, getCurrentTestName());
