Skip to content

Commit 1f0aacf

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

File tree

1 file changed

+113
-3
lines changed

1 file changed

+113
-3
lines changed

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

+113-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.util.concurrent.ScheduledFuture;
4242
import java.util.concurrent.TimeUnit;
4343
import java.util.concurrent.TimeoutException;
44+
import java.util.concurrent.atomic.AtomicBoolean;
4445
import java.util.concurrent.atomic.AtomicLong;
4546
import java.util.concurrent.atomic.AtomicReference;
4647
import java.util.function.LongConsumer;
@@ -188,6 +189,8 @@ public abstract class AbstractSession extends SessionHelper {
188189
protected final SessionWorkBuffer decoderBuffer;
189190
protected int decoderState;
190191
protected int decoderLength;
192+
protected final AtomicBoolean newKeysSignaledHolder = new AtomicBoolean();
193+
protected final AtomicBoolean strictKexSignalled = new AtomicBoolean();
191194
protected final Object encodeLock = new Object();
192195
protected final Object decodeLock = new Object();
193196
protected final Object requestLock = new Object();
@@ -540,8 +543,27 @@ protected void handleMessage(Buffer buffer) throws Exception {
540543

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

@@ -655,6 +677,73 @@ protected Map.Entry<String, String> comparePreferredKexProposalOption(KexProposa
655677
return null;
656678
}
657679

680+
protected void resetSequenceNumbers(boolean sentNewkeys) {
681+
/*
682+
* We rely on the fact that SSH_MSG_NEWKEYS is symmetric and if we initiated one then an
683+
* incoming one is due from our peer (and vice versa). Therefore:
684+
*
685+
* - if we initiated the message, we can reset our sequence number and
686+
* rely on receiving the peer's response to reset our tracking of
687+
* its counter. We still need it to decode our peer's response and
688+
* thus have to wait for it before resetting out tracking value.
689+
*
690+
* - if we are the peer that received the message then we can reset
691+
* our tracking of the initiator's counter, relying on the fact that
692+
* it did it to its own counter. After (!) we send our response we will
693+
* reset our counter as well.
694+
*/
695+
long prevSeqno;
696+
synchronized (newKeysSignaledHolder) {
697+
if (sentNewkeys) {
698+
prevSeqno = seqo;
699+
seqo = 0L;
700+
} else {
701+
prevSeqno = seqi;
702+
seqi = 0L;
703+
}
704+
705+
}
706+
707+
if (log.isDebugEnabled()) {
708+
log.debug("resetSequenceNumbers({})[sentNewKeys={}] packet couter={}", this, sentNewkeys, prevSeqno);
709+
}
710+
}
711+
712+
protected boolean isNewKeysSignalled() {
713+
return newKeysSignaledHolder.get();
714+
}
715+
716+
protected boolean isStrictKexSignalled() {
717+
return strictKexSignalled.get();
718+
}
719+
720+
/**
721+
* Called to indicate that {@link SshConstants#SSH_MSG_NEWKEYS} was either sent or received
722+
*
723+
* @param sentNewKeys Indicates whether the message was sent or received
724+
* @return The previous state of the signalling holder
725+
* @see #isNewKeysSignalled()
726+
*/
727+
protected boolean newKeysSignalled(boolean sentNewKeys) {
728+
boolean prev = newKeysSignaledHolder.getAndSet(true);
729+
if (log.isDebugEnabled()) {
730+
log.debug("newKeysSignalled({})[sentNewKeys={}] signalState={} -> {}", this, sentNewKeys, prev, true);
731+
}
732+
733+
/*
734+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
735+
* section 1.9: transport: strict key exchange extension
736+
*
737+
* After sending or receiving a SSH2_MSG_NEWKEYS message,
738+
* reset the packet sequence number to zero.
739+
*/
740+
if (isStrictKexSignalled()) {
741+
resetSequenceNumbers(sentNewKeys);
742+
}
743+
744+
return prev;
745+
}
746+
658747
/**
659748
* Send a message to put new keys into use.
660749
*
@@ -674,6 +763,9 @@ protected IoWriteFuture sendNewKeys() throws Exception {
674763
// initiate a new KEX, and thus would never try to get the kexLock monitor. If it did, we might get a
675764
// deadlock due to lock inversion. It seems safer to push this out directly, though.
676765
future = doWritePacket(buffer);
766+
767+
newKeysSignalled(true);
768+
677769
// Use the new settings from now on for any outgoing packet
678770
setOutputEncoding();
679771
}
@@ -901,6 +993,16 @@ protected void handleNewKeys(int cmd, Buffer buffer) throws Exception {
901993
this, SshConstants.getCommandMessageName(cmd));
902994
}
903995
validateKexState(cmd, KexState.KEYS);
996+
997+
/*
998+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
999+
* section 1.9: transport: strict key exchange extension
1000+
*
1001+
* After sending or receiving a SSH2_MSG_NEWKEYS message,
1002+
* reset the packet sequence number to zero.
1003+
*/
1004+
newKeysSignalled(false);
1005+
9041006
// It is guaranteed that we handle the peer's SSH_MSG_NEWKEYS after having sent our own.
9051007
// prepareNewKeys() was already called in sendNewKeys().
9061008
//
@@ -1118,9 +1220,17 @@ protected IoWriteFuture doWritePacket(Buffer buffer) throws IOException {
11181220
}
11191221

11201222
protected int resolveIgnoreBufferDataLength() {
1223+
/*
1224+
* Terrapin attack mitigation - see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
1225+
* section 1.9: transport: strict key exchange extension
1226+
*
1227+
* We need to defer sending any stuffing SSH_MSG_IGNORE message so that
1228+
* the peer does not close the connection
1229+
*/
11211230
if ((ignorePacketDataLength <= 0)
11221231
|| (ignorePacketsFrequency <= 0L)
1123-
|| (ignorePacketsVariance < 0)) {
1232+
|| (ignorePacketsVariance < 0)
1233+
|| isStrictKexSignalled() && (!isNewKeysSignalled())) {
11241234
return 0;
11251235
}
11261236

@@ -2683,7 +2793,7 @@ public MessageCodingSettings(Cipher cipher, Mac mac, Compression compression, Ci
26832793
this.iv = iv.clone();
26842794
}
26852795

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

0 commit comments

Comments
 (0)