41
41
import java .util .concurrent .ScheduledFuture ;
42
42
import java .util .concurrent .TimeUnit ;
43
43
import java .util .concurrent .TimeoutException ;
44
+ import java .util .concurrent .atomic .AtomicBoolean ;
44
45
import java .util .concurrent .atomic .AtomicLong ;
45
46
import java .util .concurrent .atomic .AtomicReference ;
46
47
import java .util .function .LongConsumer ;
@@ -188,6 +189,8 @@ public abstract class AbstractSession extends SessionHelper {
188
189
protected final SessionWorkBuffer decoderBuffer ;
189
190
protected int decoderState ;
190
191
protected int decoderLength ;
192
+ protected final AtomicBoolean newKeysSignaledHolder = new AtomicBoolean ();
193
+ protected final AtomicBoolean strictKexSignalled = new AtomicBoolean ();
191
194
protected final Object encodeLock = new Object ();
192
195
protected final Object decodeLock = new Object ();
193
196
protected final Object requestLock = new Object ();
@@ -540,8 +543,27 @@ protected void handleMessage(Buffer buffer) throws Exception {
540
543
541
544
protected void doHandleMessage (Buffer buffer ) throws Exception {
542
545
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
+
543
565
if (log .isDebugEnabled ()) {
544
- log .debug ("doHandleMessage({}) process #{} {}" , this , seqi - 1 ,
566
+ log .debug ("doHandleMessage({}) process #{} {}" , this , seqi - 1L ,
545
567
SshConstants .getCommandMessageName (cmd ));
546
568
}
547
569
@@ -655,6 +677,73 @@ protected Map.Entry<String, String> comparePreferredKexProposalOption(KexProposa
655
677
return null ;
656
678
}
657
679
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
+
658
747
/**
659
748
* Send a message to put new keys into use.
660
749
*
@@ -674,6 +763,9 @@ protected IoWriteFuture sendNewKeys() throws Exception {
674
763
// initiate a new KEX, and thus would never try to get the kexLock monitor. If it did, we might get a
675
764
// deadlock due to lock inversion. It seems safer to push this out directly, though.
676
765
future = doWritePacket (buffer );
766
+
767
+ newKeysSignalled (true );
768
+
677
769
// Use the new settings from now on for any outgoing packet
678
770
setOutputEncoding ();
679
771
}
@@ -901,6 +993,16 @@ protected void handleNewKeys(int cmd, Buffer buffer) throws Exception {
901
993
this , SshConstants .getCommandMessageName (cmd ));
902
994
}
903
995
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
+
904
1006
// It is guaranteed that we handle the peer's SSH_MSG_NEWKEYS after having sent our own.
905
1007
// prepareNewKeys() was already called in sendNewKeys().
906
1008
//
@@ -1118,9 +1220,17 @@ protected IoWriteFuture doWritePacket(Buffer buffer) throws IOException {
1118
1220
}
1119
1221
1120
1222
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
+ */
1121
1230
if ((ignorePacketDataLength <= 0 )
1122
1231
|| (ignorePacketsFrequency <= 0L )
1123
- || (ignorePacketsVariance < 0 )) {
1232
+ || (ignorePacketsVariance < 0 )
1233
+ || isStrictKexSignalled () && (!isNewKeysSignalled ())) {
1124
1234
return 0 ;
1125
1235
}
1126
1236
@@ -2683,7 +2793,7 @@ public MessageCodingSettings(Cipher cipher, Mac mac, Compression compression, Ci
2683
2793
this .iv = iv .clone ();
2684
2794
}
2685
2795
2686
- private void initCipher (long packetSequenceNumber ) throws Exception {
2796
+ protected void initCipher (long packetSequenceNumber ) throws Exception {
2687
2797
if (key != null ) {
2688
2798
if (cipher .getAlgorithm ().startsWith ("ChaCha" )) {
2689
2799
BufferUtils .putLong (packetSequenceNumber , iv , 0 , iv .length );
0 commit comments