Skip to content

Commit 9e4fa67

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

File tree

8 files changed

+163
-9
lines changed

8 files changed

+163
-9
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

+9-2
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@
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)
35-
* [SSH proxy jumps](./internals.md#ssh-jumps)
36-
* SFTP version 3-6 + extensions
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
3740
* `supported` - [DRAFT 05 - section 4.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-4.4)
3841
* `supported2` - [DRAFT 13 section 5.4](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-5.4)
3942
* `versions` - [DRAFT 09 Section 4.6](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-4.6)
@@ -46,6 +49,10 @@
4649
* `space-available` - [DRAFT 09 - section 9.2](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-9.2)
4750
* `filename-charset`, `filename-translation-control` - [DRAFT 13 - section 6](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-6) - only client side
4851
* Several [OpenSSH SFTP extensions](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
52+
53+
## Miscellaneous
54+
55+
* [SSH proxy jumps](./internals.md#ssh-jumps)
4956
* [Endless tarpit](https://nullprogram.com/blog/2019/03/22/) - see [HOWTO(s)](./howto.md) section.
5057

5158
## 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

+44-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;
@@ -540,8 +536,27 @@ protected void handleMessage(Buffer buffer) throws Exception {
540536

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

@@ -655,6 +670,7 @@ protected Map.Entry<String, String> comparePreferredKexProposalOption(KexProposa
655670
return null;
656671
}
657672

673+
658674
/**
659675
* Send a message to put new keys into use.
660676
*
@@ -674,6 +690,9 @@ protected IoWriteFuture sendNewKeys() throws Exception {
674690
// initiate a new KEX, and thus would never try to get the kexLock monitor. If it did, we might get a
675691
// deadlock due to lock inversion. It seems safer to push this out directly, though.
676692
future = doWritePacket(buffer);
693+
694+
newKeysSignalled(true);
695+
677696
// Use the new settings from now on for any outgoing packet
678697
setOutputEncoding();
679698
}
@@ -901,6 +920,16 @@ protected void handleNewKeys(int cmd, Buffer buffer) throws Exception {
901920
this, SshConstants.getCommandMessageName(cmd));
902921
}
903922
validateKexState(cmd, KexState.KEYS);
923+
924+
/*
925+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
926+
* section 1.9: transport: strict key exchange extension
927+
*
928+
* After sending or receiving a SSH2_MSG_NEWKEYS message,
929+
* reset the packet sequence number to zero.
930+
*/
931+
newKeysSignalled(false);
932+
904933
// It is guaranteed that we handle the peer's SSH_MSG_NEWKEYS after having sent our own.
905934
// prepareNewKeys() was already called in sendNewKeys().
906935
//
@@ -1118,9 +1147,17 @@ protected IoWriteFuture doWritePacket(Buffer buffer) throws IOException {
11181147
}
11191148

11201149
protected int resolveIgnoreBufferDataLength() {
1150+
/*
1151+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
1152+
* section 1.9: transport: strict key exchange extension
1153+
*
1154+
* We need to defer sending any stuffing SSH_MSG_IGNORE message so that
1155+
* the peer does not close the connection
1156+
*/
11211157
if ((ignorePacketDataLength <= 0)
11221158
|| (ignorePacketsFrequency <= 0L)
1123-
|| (ignorePacketsVariance < 0)) {
1159+
|| (ignorePacketsVariance < 0)
1160+
|| isStrictKexSignalled() && (!isNewKeysSignalled())) {
11241161
return 0;
11251162
}
11261163

@@ -2683,7 +2720,7 @@ public MessageCodingSettings(Cipher cipher, Mac mac, Compression compression, Ci
26832720
this.iv = iv.clone();
26842721
}
26852722

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

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

+59
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,65 @@ public void setAuthenticated() throws IOException {
224224
}
225225
}
226226

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

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

+6
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ 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: strict key exchange extension</A>
152+
*/
153+
public static final Property<Boolean> USE_STRICT_KEX
154+
= Property.bool("use-strict-kex", false);
155+
150156
/**
151157
* Key used to set the heartbeat interval in milliseconds (0 to disable = default)
152158
*/

0 commit comments

Comments
 (0)