Skip to content

Commit f992796

Browse files
author
Lyor Goldstein
committed
[apacheGH-445] lay down the groundwork for mitigating the Terrapin attack
1 parent f5c63a8 commit f992796

File tree

8 files changed

+183
-20
lines changed

8 files changed

+183
-20
lines changed

CHANGES.md

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@
3636

3737
## Behavioral changes and enhancements
3838

39+
### [GH-445 - Terrapin attack mitigation](https://github.com/apache/mina-sshd/issues/429)
40+
41+
There is a **new** `CoreModuleProperties` property that controls the mitigation for the [Terrapin attach](https://terrapin-attack.com/) via what is known as
42+
"strict-KEX" (see [OpenSSH PROTOCOL - 1.9 transport: strict key exchange extension](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)).
43+
It is **disabled** by default due to its experimental nature and possible interoperability issues, so users who wish to use this feature must turn it on *explicitly*.
44+
3945
### New `ScpTransferEventListener` callback method
4046

4147
Following [GH-428/GH-392](https://github.com/apache/mina-sshd/issues/428) a new `handleReceiveCommandAckInfo` method has been added to enable users to inspect

docs/standards.md

+21-13
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,31 @@
2929
above mentioned hooks for [RFC 8308](https://tools.ietf.org/html/rfc8308).
3030
* [RFC 8731 - Secure Shell (SSH) Key Exchange Method Using Curve25519 and Curve448](https://tools.ietf.org/html/rfc8731)
3131
* [Key Exchange (KEX) Method Updates and Recommendations for Secure Shell](https://tools.ietf.org/html/draft-ietf-curdle-ssh-kex-sha2-03)
32+
33+
## *OpenSSH*
3234
* [OpenSSH support for U2F/FIDO security keys](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f)
3335
* **Note:** the server side supports these keys by default. The client side requires specific initialization
3436
* [OpenSSH public-key certificate authentication system for use by SSH](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys)
37+
* [OpenSSH 1.9 transport: strict key exchange extension](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
38+
39+
## SFTP version 3-6 + extensions
40+
41+
* `supported` - [DRAFT 05 - section 4.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-4.4)
42+
* `supported2` - [DRAFT 13 section 5.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-5.4)
43+
* `versions` - [DRAFT 09 Section 4.6](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-4.6)
44+
* `vendor-id` - [DRAFT 09 - section 4.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-4.4)
45+
* `acl-supported` - [DRAFT 11 - section 5.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-11#section-5.4)
46+
* `newline` - [DRAFT 09 Section 4.3](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-4.3)
47+
* `md5-hash`, `md5-hash-handle` - [DRAFT 09 - section 9.1.1](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-9.1.1)
48+
* `check-file-handle`, `check-file-name` - [DRAFT 09 - section 9.1.2](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-9.1.2)
49+
* `copy-file`, `copy-data` - [DRAFT 00 - sections 6, 7](https://tools.ietf.org/id/draft-ietf-secsh-filexfer-extensions-00.txt)
50+
* `space-available` - [DRAFT 09 - section 9.2](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-9.2)
51+
* `filename-charset`, `filename-translation-control` - [DRAFT 13 - section 6](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-6) - only client side
52+
* Several [OpenSSH SFTP extensions](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
53+
54+
## Miscellaneous
55+
3556
* [SSH proxy jumps](./internals.md#ssh-jumps)
36-
* SFTP version 3-6 + extensions
37-
* `supported` - [DRAFT 05 - section 4.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-4.4)
38-
* `supported2` - [DRAFT 13 section 5.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-5.4)
39-
* `versions` - [DRAFT 09 Section 4.6](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-4.6)
40-
* `vendor-id` - [DRAFT 09 - section 4.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-4.4)
41-
* `acl-supported` - [DRAFT 11 - section 5.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-11#section-5.4)
42-
* `newline` - [DRAFT 09 Section 4.3](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-4.3)
43-
* `md5-hash`, `md5-hash-handle` - [DRAFT 09 - section 9.1.1](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-9.1.1)
44-
* `check-file-handle`, `check-file-name` - [DRAFT 09 - section 9.1.2](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-9.1.2)
45-
* `copy-file`, `copy-data` - [DRAFT 00 - sections 6, 7](https://tools.ietf.org/id/draft-ietf-secsh-filexfer-extensions-00.txt)
46-
* `space-available` - [DRAFT 09 - section 9.2](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-9.2)
47-
* `filename-charset`, `filename-translation-control` - [DRAFT 13 - section 6](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-6) - only client side
48-
* Several [OpenSSH SFTP extensions](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
4957
* [Endless tarpit](https://nullprogram.com/blog/2019/03/22/) - see [HOWTO(s)](./howto.md) section.
5058

5159
## Implemented/available support

docs/technical/kex.md

+8
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,11 @@ thread is not overrun by producers and actually can finish.
129129
Again, "client" and "server" could also be inverted. For instance, a client uploading
130130
files via SFTP might have an application thread pumping data through a channel, which
131131
might be blocked during KEX.
132+
133+
## [OpenSSH 1.9 transport: strict key exchange extension](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
134+
135+
136+
There is a **new** `CoreModuleProperties` property that controls the mitigation for the [Terrapin attack](https://terrapin-attack.com/) via what is known as "strict-KEX"
137+
It is **disabled** by default due to its experimental nature and possible interoperability issues, so users who wish to use this feature must turn it on *explicitly*.
138+
The pseudo KEX values are *appended* to the initial proposals sent to the peer and removed when received before proceeding with the standard KEX proposals negotiation so
139+
as not to interfere with it (other than marking that they were detected).

sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java

+15
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,21 @@ public final class KexExtensions {
5959
public static final String CLIENT_KEX_EXTENSION = "ext-info-c";
6060
public static final String SERVER_KEX_EXTENSION = "ext-info-s";
6161

62+
/**
63+
* Reminder:
64+
*
65+
* These pseudo-algorithms are only valid in the initial SSH2_MSG_KEXINIT and MUST be ignored if they are present in
66+
* subsequent SSH2_MSG_KEXINIT packets.
67+
*
68+
* <B>Note:</B> these values are <U>appended</U> to the initial proposals and removed if received before proceeding
69+
* with the standard KEX proposals negotiation.
70+
*
71+
* @see <A HREF="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL">OpenSSH PROTOCOL - 1.9 transport:
72+
* strict key exchange extension</A>
73+
*/
74+
public static final String STRICT_KEX_CLIENT_EXTENSION = "[email protected]";
75+
public static final String STRICT_KEX_SERVER_EXTENSION = "[email protected]";
76+
6277
@SuppressWarnings("checkstyle:Indentation")
6378
public static final Predicate<String> IS_KEX_EXTENSION_SIGNAL
6479
= n -> CLIENT_KEX_EXTENSION.equalsIgnoreCase(n) || SERVER_KEX_EXTENSION.equalsIgnoreCase(n);

sshd-core/src/main/java/org/apache/sshd/common/kex/AbstractKexFactoryManager.java

+16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Collection;
2323
import java.util.Collections;
2424
import java.util.List;
25+
import java.util.concurrent.atomic.AtomicBoolean;
2526

2627
import org.apache.sshd.common.NamedFactory;
2728
import org.apache.sshd.common.cipher.Cipher;
@@ -38,6 +39,13 @@
3839
public abstract class AbstractKexFactoryManager
3940
extends AbstractInnerCloseable
4041
implements KexFactoryManager {
42+
/** Input packet sequence number. */
43+
protected long seqi;
44+
/** Output packet sequence number. */
45+
protected long seqo;
46+
protected final AtomicBoolean newKeysSignaledHolder = new AtomicBoolean();
47+
protected final AtomicBoolean strictKexSignalled = new AtomicBoolean();
48+
4149
private final KexFactoryManager delegate;
4250
private List<KeyExchangeFactory> keyExchangeFactories;
4351
private List<NamedFactory<Cipher>> cipherFactories;
@@ -130,6 +138,14 @@ public void setKexExtensionHandler(KexExtensionHandler kexExtensionHandler) {
130138
this.kexExtensionHandler = kexExtensionHandler;
131139
}
132140

141+
protected boolean isNewKeysSignalled() {
142+
return newKeysSignaledHolder.get();
143+
}
144+
145+
protected boolean isStrictKexSignalled() {
146+
return strictKexSignalled.get();
147+
}
148+
133149
protected <V, C extends Collection<V>> C resolveEffectiveFactories(C local, C inherited) {
134150
if (GenericUtils.isEmpty(local)) {
135151
return inherited;

sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java

+49-7
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,6 @@ public abstract class AbstractSession extends SessionHelper {
180180
protected byte[] inMacResult;
181181
protected Compression outCompression;
182182
protected Compression inCompression;
183-
/** Input packet sequence number. */
184-
protected long seqi;
185-
/** Output packet sequence number. */
186-
protected long seqo;
187183
protected SessionWorkBuffer uncompressBuffer;
188184
protected final SessionWorkBuffer decoderBuffer;
189185
protected int decoderState;
@@ -202,7 +198,9 @@ public abstract class AbstractSession extends SessionHelper {
202198
* Rekeying
203199
*/
204200
protected final AtomicLong inPacketsCount = new AtomicLong(0L);
201+
protected final AtomicLong totalIncomingPacketCount = new AtomicLong(0L);
205202
protected final AtomicLong outPacketsCount = new AtomicLong(0L);
203+
protected final AtomicLong totalOutgingPacketCount = new AtomicLong(0L);
206204
protected final AtomicLong inBytesCount = new AtomicLong(0L);
207205
protected final AtomicLong outBytesCount = new AtomicLong(0L);
208206
protected final AtomicLong inBlocksCount = new AtomicLong(0L);
@@ -540,8 +538,29 @@ protected void handleMessage(Buffer buffer) throws Exception {
540538

541539
protected void doHandleMessage(Buffer buffer) throws Exception {
542540
int cmd = buffer.getUByte();
541+
542+
/*
543+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
544+
* section 1.9 transport: strict key exchange extension
545+
*
546+
* During initial KEX, terminate the connection if any unexpected or
547+
* out-of-sequence packet is received. This includes terminating the
548+
* connection if the first packet received is not SSH2_MSG_KEXINIT.
549+
*
550+
* Unexpected packets for the purpose of strict KEX include messages
551+
* that are otherwise valid at any time during the connection such as
552+
* SSH2_MSG_DEBUG and SSH2_MSG_IGNORE.
553+
*/
554+
if ((totalIncomingPacketCount.get() == 1L)
555+
&& isStrictKexSignalled()
556+
&& (cmd != SshConstants.SSH_MSG_KEXINIT)) {
557+
log.error("doHandleMessage({}) invalid 1st message: {}",
558+
this, SshConstants.getCommandMessageName(cmd));
559+
throw new SshException(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Strict KEX Error");
560+
}
561+
543562
if (log.isDebugEnabled()) {
544-
log.debug("doHandleMessage({}) process #{} {}", this, seqi - 1,
563+
log.debug("doHandleMessage({}) process #{} {}", this, seqi - 1L,
545564
SshConstants.getCommandMessageName(cmd));
546565
}
547566

@@ -674,6 +693,9 @@ protected IoWriteFuture sendNewKeys() throws Exception {
674693
// initiate a new KEX, and thus would never try to get the kexLock monitor. If it did, we might get a
675694
// deadlock due to lock inversion. It seems safer to push this out directly, though.
676695
future = doWritePacket(buffer);
696+
697+
newKeysSignalled(true);
698+
677699
// Use the new settings from now on for any outgoing packet
678700
setOutputEncoding();
679701
}
@@ -901,6 +923,16 @@ protected void handleNewKeys(int cmd, Buffer buffer) throws Exception {
901923
this, SshConstants.getCommandMessageName(cmd));
902924
}
903925
validateKexState(cmd, KexState.KEYS);
926+
927+
/*
928+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
929+
* section 1.9: transport: strict key exchange extension
930+
*
931+
* After sending or receiving a SSH2_MSG_NEWKEYS message,
932+
* reset the packet sequence number to zero.
933+
*/
934+
newKeysSignalled(false);
935+
904936
// It is guaranteed that we handle the peer's SSH_MSG_NEWKEYS after having sent our own.
905937
// prepareNewKeys() was already called in sendNewKeys().
906938
//
@@ -1118,9 +1150,17 @@ protected IoWriteFuture doWritePacket(Buffer buffer) throws IOException {
11181150
}
11191151

11201152
protected int resolveIgnoreBufferDataLength() {
1153+
/*
1154+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
1155+
* section 1.9: transport: strict key exchange extension
1156+
*
1157+
* We need to defer sending any stuffing SSH_MSG_IGNORE messages so that
1158+
* the peer does not close the connection
1159+
*/
11211160
if ((ignorePacketDataLength <= 0)
11221161
|| (ignorePacketsFrequency <= 0L)
1123-
|| (ignorePacketsVariance < 0)) {
1162+
|| (ignorePacketsVariance < 0)
1163+
|| (isStrictKexSignalled() && (!isNewKeysSignalled()))) {
11241164
return 0;
11251165
}
11261166

@@ -1467,6 +1507,7 @@ protected Buffer encode(Buffer buffer) throws IOException {
14671507

14681508
// Update counters used to track re-keying
14691509
outPacketsCount.incrementAndGet();
1510+
totalOutgingPacketCount.incrementAndGet();
14701511
outBytesCount.addAndGet(len);
14711512

14721513
// Make buffer ready to be read
@@ -1643,6 +1684,7 @@ protected void decode() throws Exception {
16431684

16441685
// Update counters used to track re-keying
16451686
inPacketsCount.incrementAndGet();
1687+
totalIncomingPacketCount.incrementAndGet();
16461688
inBytesCount.addAndGet(packet.available());
16471689

16481690
// Process decoded packet
@@ -2683,7 +2725,7 @@ public MessageCodingSettings(Cipher cipher, Mac mac, Compression compression, Ci
26832725
this.iv = iv.clone();
26842726
}
26852727

2686-
private void initCipher(long packetSequenceNumber) throws Exception {
2728+
protected void initCipher(long packetSequenceNumber) throws Exception {
26872729
if (key != null) {
26882730
if (cipher.getAlgorithm().startsWith("ChaCha")) {
26892731
BufferUtils.putLong(packetSequenceNumber, iv, 0, iv.length);

sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java

+61
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
/**
7878
* Contains split code in order to make {@link AbstractSession} class smaller
7979
*/
80+
@SuppressWarnings("checkstyle:MethodCount")
8081
public abstract class SessionHelper extends AbstractKexFactoryManager implements Session {
8182

8283
// Session timeout measurements
@@ -127,6 +128,7 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
127128
*/
128129
protected SessionHelper(boolean serverSession, FactoryManager factoryManager, IoSession ioSession) {
129130
super(Objects.requireNonNull(factoryManager, "No factory manager provided"));
131+
130132
this.serverSession = serverSession;
131133
this.ioSession = Objects.requireNonNull(ioSession, "No IoSession provided");
132134
}
@@ -224,6 +226,65 @@ public void setAuthenticated() throws IOException {
224226
}
225227
}
226228

229+
/**
230+
* Called to indicate that {@link SshConstants#SSH_MSG_NEWKEYS} was either sent or received
231+
*
232+
* @param sentNewKeys Indicates whether the message was sent or received
233+
* @return The previous state of the signalling holder
234+
* @see #isNewKeysSignalled()
235+
*/
236+
protected boolean newKeysSignalled(boolean sentNewKeys) {
237+
boolean prev = newKeysSignaledHolder.getAndSet(true);
238+
if (log.isDebugEnabled()) {
239+
log.debug("newKeysSignalled({})[sentNewKeys={}] signalState={} -> {}", this, sentNewKeys, prev, true);
240+
}
241+
242+
/*
243+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
244+
* section 1.9: transport: strict key exchange extension
245+
*
246+
* After sending or receiving a SSH2_MSG_NEWKEYS message,
247+
* reset the packet sequence number to zero.
248+
*/
249+
if (isStrictKexSignalled()) {
250+
resetSequenceNumbers(sentNewKeys);
251+
}
252+
253+
return prev;
254+
}
255+
256+
protected void resetSequenceNumbers(boolean sentNewkeys) {
257+
/*
258+
* We rely on the fact that SSH_MSG_NEWKEYS is symmetric and if we initiated one then an
259+
* incoming one is due from our peer (and vice versa). Therefore:
260+
*
261+
* - if we initiated the message, we can reset our sequence number and
262+
* rely on receiving the peer's response to reset our tracking of
263+
* its counter. We still need it to decode our peer's response and
264+
* thus have to wait for it before resetting our tracking value.
265+
*
266+
* - if we are the peer that received the message then we can reset
267+
* our tracking of the initiator's counter, relying on the fact that
268+
* it did it to its own counter. After (!) we send our response we will
269+
* reset our counter as well.
270+
*/
271+
long prevSeqno;
272+
synchronized (newKeysSignaledHolder) {
273+
if (sentNewkeys) {
274+
prevSeqno = seqo;
275+
seqo = 0L;
276+
} else {
277+
prevSeqno = seqi;
278+
seqi = 0L;
279+
}
280+
281+
}
282+
283+
if (log.isDebugEnabled()) {
284+
log.debug("resetSequenceNumbers({})[sentNewKeys={}] packet couter={}", this, sentNewkeys, prevSeqno);
285+
}
286+
}
287+
227288
/**
228289
* Checks whether the session has timed out (both authentication and idle timeouts are checked). If the session has
229290
* timed out, a DISCONNECT message will be sent.

sshd-core/src/main/java/org/apache/sshd/core/CoreModuleProperties.java

+7
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ public final class CoreModuleProperties {
147147
public static final Property<Duration> KEX_PROPOSAL_SETUP_TIMEOUT
148148
= Property.durationSec("kex-proposal-setup-timeout", Duration.ofSeconds(42), Duration.ofSeconds(5));
149149

150+
/**
151+
* @see <A HREF="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL">OpenSSH PROTOCOL - 1.9 transport:
152+
* strict key exchange extension</A>
153+
*/
154+
public static final Property<Boolean> USE_STRICT_KEX
155+
= Property.bool("use-strict-kex", false);
156+
150157
/**
151158
* Key used to set the heartbeat interval in milliseconds (0 to disable = default)
152159
*/

0 commit comments

Comments
 (0)