Skip to content

Commit 7cfe62b

Browse files
authored
Support ETM (Encrypt-then-MAC) variants for HMAC (#1316)
* Support ETM (Encrypt-then-MAC) variants for HMAC * Support ETM (Encrypt-then-MAC) variants for HMAC * Remove `ETM` property from `HashInfo` * Add support for HmacMd5Etm, HmacMd5_96_Etm, HmacSha1Etm and HmacSha1_96_Etm Explicitly specify etm even if false * Add Encrypt-then-MAC variants to README.md * Add back empty span to prevent auto link * Store ETM in `HashInfo`; Change `HMAC Create[...]Hash()` to `HashAlgorithm Create[...]Hash(out bool isEncryptThenMAC)`
1 parent d07827b commit 7cfe62b

File tree

10 files changed

+181
-38
lines changed

10 files changed

+181
-38
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ Private keys can be encrypted using one of the following cipher methods:
115115
* hmac-sha2-512-96
116116
* hmac-ripemd160
117117
* hmac-ripemd160<span></span>@openssh.com
118+
* hmac-md5-etm<span></span>@openssh.com
119+
* hmac-md5-96-etm<span></span>@openssh.com
120+
* hmac-sha1-etm<span></span>@openssh.com
121+
* hmac-sha1-96-etm<span></span>@openssh.com
122+
* hmac-sha2-256-etm<span></span>@openssh.com
123+
* hmac-sha2-512-etm<span></span>@openssh.com
118124

119125
## Framework Support
120126
**SSH.NET** supports the following target frameworks:

src/Renci.SshNet/ConnectionInfo.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -376,16 +376,24 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
376376
#pragma warning disable IDE0200 // Remove unnecessary lambda expression; We want to prevent instantiating the HashAlgorithm objects.
377377
HmacAlgorithms = new Dictionary<string, HashInfo>
378378
{
379-
{ "hmac-sha2-256", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key)) },
380-
{ "hmac-sha2-512", new HashInfo(64 * 8, key => CryptoAbstraction.CreateHMACSHA512(key)) },
381-
{ "hmac-sha2-512-96", new HashInfo(64 * 8, key => CryptoAbstraction.CreateHMACSHA512(key, 96)) },
382-
{ "hmac-sha2-256-96", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key, 96)) },
383-
{ "hmac-ripemd160", new HashInfo(160, key => CryptoAbstraction.CreateHMACRIPEMD160(key)) },
384-
{ "[email protected]", new HashInfo(160, key => CryptoAbstraction.CreateHMACRIPEMD160(key)) },
385-
{ "hmac-sha1", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key)) },
386-
{ "hmac-sha1-96", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key, 96)) },
387-
{ "hmac-md5", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key)) },
388-
{ "hmac-md5-96", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key, 96)) },
379+
/* Encrypt-and-MAC (encrypt-and-authenticate) variants */
380+
{ "hmac-sha2-256", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key), isEncryptThenMAC: false) },
381+
{ "hmac-sha2-512", new HashInfo(64*8, key => CryptoAbstraction.CreateHMACSHA512(key), isEncryptThenMAC: false) },
382+
{ "hmac-sha2-512-96", new HashInfo(64*8, key => CryptoAbstraction.CreateHMACSHA512(key, 96), isEncryptThenMAC: false) },
383+
{ "hmac-sha2-256-96", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key, 96), isEncryptThenMAC: false) },
384+
{ "hmac-ripemd160", new HashInfo(160, key => CryptoAbstraction.CreateHMACRIPEMD160(key), isEncryptThenMAC: false) },
385+
{ "[email protected]", new HashInfo(160, key => CryptoAbstraction.CreateHMACRIPEMD160(key), isEncryptThenMAC: false) },
386+
{ "hmac-sha1", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key), isEncryptThenMAC: false) },
387+
{ "hmac-sha1-96", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key, 96), isEncryptThenMAC: false) },
388+
{ "hmac-md5", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key), isEncryptThenMAC: false) },
389+
{ "hmac-md5-96", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key, 96), isEncryptThenMAC: false) },
390+
/* Encrypt-then-MAC variants */
391+
{ "[email protected]", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key), isEncryptThenMAC: true) },
392+
{ "[email protected]", new HashInfo(64*8, key => CryptoAbstraction.CreateHMACSHA512(key), isEncryptThenMAC: true) },
393+
{ "[email protected]", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key), isEncryptThenMAC: true) },
394+
{ "[email protected]", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key, 96), isEncryptThenMAC: true) },
395+
{ "[email protected]", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key), isEncryptThenMAC: true) },
396+
{ "[email protected]", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key, 96), isEncryptThenMAC: true) },
389397
};
390398
#pragma warning restore IDE0200 // Remove unnecessary lambda expression
391399

src/Renci.SshNet/HashInfo.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Security.Cryptography;
3+
34
using Renci.SshNet.Common;
45

56
namespace Renci.SshNet
@@ -17,6 +18,14 @@ public class HashInfo
1718
/// </value>
1819
public int KeySize { get; private set; }
1920

21+
/// <summary>
22+
/// Gets a value indicating whether enable encrypt-then-MAC or use encrypt-and-MAC.
23+
/// </summary>
24+
/// <value>
25+
/// <see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.
26+
/// </value>
27+
public bool IsEncryptThenMAC { get; private set; }
28+
2029
/// <summary>
2130
/// Gets the cipher.
2231
/// </summary>
@@ -27,10 +36,12 @@ public class HashInfo
2736
/// </summary>
2837
/// <param name="keySize">Size of the key.</param>
2938
/// <param name="hash">The hash algorithm to use for a given key.</param>
30-
public HashInfo(int keySize, Func<byte[], HashAlgorithm> hash)
39+
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
40+
public HashInfo(int keySize, Func<byte[], HashAlgorithm> hash, bool isEncryptThenMAC = false)
3141
{
3242
KeySize = keySize;
3343
HashAlgorithm = key => hash(key.Take(KeySize / 8));
44+
IsEncryptThenMAC = isEncryptThenMAC;
3445
}
3546
}
3647
}

src/Renci.SshNet/Messages/Message.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected override void WriteBytes(SshDataStream stream)
3737
base.WriteBytes(stream);
3838
}
3939

40-
internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor)
40+
internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool isEncryptThenMAC = false)
4141
{
4242
const int outboundPacketSequenceSize = 4;
4343

@@ -78,7 +78,9 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor)
7878
var packetLength = messageLength + 4 + 1;
7979

8080
// determine the padding length
81-
var paddingLength = GetPaddingLength(paddingMultiplier, packetLength);
81+
// in Encrypt-then-MAC mode, the length field is not encrypted, so we should keep it out of the
82+
// padding length calculation
83+
var paddingLength = GetPaddingLength(paddingMultiplier, isEncryptThenMAC ? packetLength - 4 : packetLength);
8284

8385
// add padding bytes
8486
var paddingBytes = new byte[paddingLength];
@@ -104,7 +106,9 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor)
104106
var packetLength = messageLength + 4 + 1;
105107

106108
// determine the padding length
107-
var paddingLength = GetPaddingLength(paddingMultiplier, packetLength);
109+
// in Encrypt-then-MAC mode, the length field is not encrypted, so we should keep it out of the
110+
// padding length calculation
111+
var paddingLength = GetPaddingLength(paddingMultiplier, isEncryptThenMAC ? packetLength - 4 : packetLength);
108112

109113
var packetDataLength = GetPacketDataLength(messageLength, paddingLength);
110114

src/Renci.SshNet/Security/IKeyExchange.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,20 @@ public interface IKeyExchange : IDisposable
6666
/// <summary>
6767
/// Creates the server-side hash algorithm to use.
6868
/// </summary>
69+
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
6970
/// <returns>
7071
/// The server hash algorithm.
7172
/// </returns>
72-
HashAlgorithm CreateServerHash();
73+
HashAlgorithm CreateServerHash(out bool isEncryptThenMAC);
7374

7475
/// <summary>
7576
/// Creates the client-side hash algorithm to use.
7677
/// </summary>
78+
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
7779
/// <returns>
7880
/// The client hash algorithm.
7981
/// </returns>
80-
HashAlgorithm CreateClientHash();
82+
HashAlgorithm CreateClientHash(out bool isEncryptThenMAC);
8183

8284
/// <summary>
8385
/// Creates the compression algorithm to use to deflate data.

src/Renci.SshNet/Security/KeyExchange.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ from a in message.MacAlgorithmsClientToServer
103103
select a).FirstOrDefault();
104104
if (string.IsNullOrEmpty(clientHmacAlgorithmName))
105105
{
106-
throw new SshConnectionException("Server HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
106+
throw new SshConnectionException("Client HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
107107
}
108108

109109
session.ConnectionInfo.CurrentClientHmacAlgorithm = clientHmacAlgorithmName;
@@ -218,11 +218,14 @@ public Cipher CreateClientCipher()
218218
/// <summary>
219219
/// Creates the server side hash algorithm to use.
220220
/// </summary>
221+
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
221222
/// <returns>
222223
/// The server-side hash algorithm.
223224
/// </returns>
224-
public HashAlgorithm CreateServerHash()
225+
public HashAlgorithm CreateServerHash(out bool isEncryptThenMAC)
225226
{
227+
isEncryptThenMAC = _serverHashInfo.IsEncryptThenMAC;
228+
226229
// Resolve Session ID
227230
var sessionId = Session.SessionId ?? ExchangeHash;
228231

@@ -241,11 +244,14 @@ public HashAlgorithm CreateServerHash()
241244
/// <summary>
242245
/// Creates the client side hash algorithm to use.
243246
/// </summary>
247+
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
244248
/// <returns>
245249
/// The client-side hash algorithm.
246250
/// </returns>
247-
public HashAlgorithm CreateClientHash()
251+
public HashAlgorithm CreateClientHash(out bool isEncryptThenMAC)
248252
{
253+
isEncryptThenMAC = _clientHashInfo.IsEncryptThenMAC;
254+
249255
// Resolve Session ID
250256
var sessionId = Session.SessionId ?? ExchangeHash;
251257

src/Renci.SshNet/Session.cs

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ public class Session : ISession
176176

177177
private HashAlgorithm _clientMac;
178178

179+
private bool _serverEtm;
180+
181+
private bool _clientEtm;
182+
179183
private Cipher _clientCipher;
180184

181185
private Cipher _serverCipher;
@@ -1054,7 +1058,7 @@ internal void SendMessage(Message message)
10541058
DiagnosticAbstraction.Log(string.Format("[{0}] Sending message '{1}' to server: '{2}'.", ToHex(SessionId), message.GetType().Name, message));
10551059

10561060
var paddingMultiplier = _clientCipher is null ? (byte) 8 : Math.Max((byte) 8, _serverCipher.MinimumSize);
1057-
var packetData = message.GetPacket(paddingMultiplier, _clientCompression);
1061+
var packetData = message.GetPacket(paddingMultiplier, _clientCompression, _clientMac != null && _clientEtm);
10581062

10591063
// take a write lock to ensure the outbound packet sequence number is incremented
10601064
// atomically, and only after the packet has actually been sent
@@ -1063,7 +1067,7 @@ internal void SendMessage(Message message)
10631067
byte[] hash = null;
10641068
var packetDataOffset = 4; // first four bytes are reserved for outbound packet sequence
10651069

1066-
if (_clientMac != null)
1070+
if (_clientMac != null && !_clientEtm)
10671071
{
10681072
// write outbound packet sequence to start of packet data
10691073
Pack.UInt32ToBigEndian(_outboundPacketSequence, packetData);
@@ -1075,8 +1079,29 @@ internal void SendMessage(Message message)
10751079
// Encrypt packet data
10761080
if (_clientCipher != null)
10771081
{
1078-
packetData = _clientCipher.Encrypt(packetData, packetDataOffset, packetData.Length - packetDataOffset);
1079-
packetDataOffset = 0;
1082+
if (_clientMac != null && _clientEtm)
1083+
{
1084+
// The length of the "packet length" field in bytes
1085+
const int packetLengthFieldLength = 4;
1086+
1087+
var encryptedData = _clientCipher.Encrypt(packetData, packetDataOffset + packetLengthFieldLength, packetData.Length - packetDataOffset - packetLengthFieldLength);
1088+
1089+
Array.Resize(ref packetData, packetDataOffset + packetLengthFieldLength + encryptedData.Length);
1090+
1091+
// write outbound packet sequence to start of packet data
1092+
Pack.UInt32ToBigEndian(_outboundPacketSequence, packetData);
1093+
1094+
// write encrypted data
1095+
Buffer.BlockCopy(encryptedData, 0, packetData, packetDataOffset + packetLengthFieldLength, encryptedData.Length);
1096+
1097+
// calculate packet hash
1098+
hash = _clientMac.ComputeHash(packetData);
1099+
}
1100+
else
1101+
{
1102+
packetData = _clientCipher.Encrypt(packetData, packetDataOffset, packetData.Length - packetDataOffset);
1103+
packetDataOffset = 0;
1104+
}
10801105
}
10811106

10821107
if (packetData.Length > MaximumSshPacketSize)
@@ -1194,8 +1219,22 @@ private Message ReceiveMessage(Socket socket)
11941219
// The length of the "padding length" field in bytes
11951220
const int paddingLengthFieldLength = 1;
11961221

1197-
// Determine the size of the first block, which is 8 or cipher block size (whichever is larger) bytes
1198-
var blockSize = _serverCipher is null ? (byte) 8 : Math.Max((byte) 8, _serverCipher.MinimumSize);
1222+
int blockSize;
1223+
1224+
// Determine the size of the first block which is 8 or cipher block size (whichever is larger) bytes
1225+
// The "packet length" field is not encrypted in ETM.
1226+
if (_serverMac != null && _serverEtm)
1227+
{
1228+
blockSize = (byte) 4;
1229+
}
1230+
else if (_serverCipher != null)
1231+
{
1232+
blockSize = Math.Max((byte) 8, _serverCipher.MinimumSize);
1233+
}
1234+
else
1235+
{
1236+
blockSize = (byte) 8;
1237+
}
11991238

12001239
var serverMacLength = _serverMac != null ? _serverMac.HashSize/8 : 0;
12011240

@@ -1215,7 +1254,7 @@ private Message ReceiveMessage(Socket socket)
12151254
return null;
12161255
}
12171256

1218-
if (_serverCipher != null)
1257+
if (_serverCipher != null && (_serverMac == null || !_serverEtm))
12191258
{
12201259
firstBlock = _serverCipher.Decrypt(firstBlock);
12211260
}
@@ -1257,6 +1296,20 @@ private Message ReceiveMessage(Socket socket)
12571296
}
12581297
}
12591298

1299+
// validate encrypted message against MAC
1300+
if (_serverMac != null && _serverEtm)
1301+
{
1302+
var clientHash = _serverMac.ComputeHash(data, 0, data.Length - serverMacLength);
1303+
var serverHash = data.Take(data.Length - serverMacLength, serverMacLength);
1304+
1305+
// TODO Add IsEqualTo overload that takes left+right index and number of bytes to compare.
1306+
// TODO That way we can eliminate the extra allocation of the Take above.
1307+
if (!serverHash.IsEqualTo(clientHash))
1308+
{
1309+
throw new SshConnectionException("MAC error", DisconnectReason.MacError);
1310+
}
1311+
}
1312+
12601313
if (_serverCipher != null)
12611314
{
12621315
var numberOfBytesToDecrypt = data.Length - (blockSize + inboundPacketSequenceLength + serverMacLength);
@@ -1271,8 +1324,8 @@ private Message ReceiveMessage(Socket socket)
12711324
var messagePayloadLength = (int) packetLength - paddingLength - paddingLengthFieldLength;
12721325
var messagePayloadOffset = inboundPacketSequenceLength + packetLengthFieldLength + paddingLengthFieldLength;
12731326

1274-
// validate message against MAC
1275-
if (_serverMac != null)
1327+
// validate decrypted message against MAC
1328+
if (_serverMac != null && !_serverEtm)
12761329
{
12771330
var clientHash = _serverMac.ComputeHash(data, 0, data.Length - serverMacLength);
12781331
var serverHash = data.Take(data.Length - serverMacLength, serverMacLength);
@@ -1472,8 +1525,8 @@ internal void OnNewKeysReceived(NewKeysMessage message)
14721525
// Update negotiated algorithms
14731526
_serverCipher = _keyExchange.CreateServerCipher();
14741527
_clientCipher = _keyExchange.CreateClientCipher();
1475-
_serverMac = _keyExchange.CreateServerHash();
1476-
_clientMac = _keyExchange.CreateClientHash();
1528+
_serverMac = _keyExchange.CreateServerHash(out _serverEtm);
1529+
_clientMac = _keyExchange.CreateClientHash(out _clientEtm);
14771530
_clientCompression = _keyExchange.CreateCompressor();
14781531
_serverDecompression = _keyExchange.CreateDecompressor();
14791532

test/Renci.SshNet.IntegrationTests/HmacTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,42 @@ public void HmacSha2_512()
5858
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha2_512);
5959
}
6060

61+
[TestMethod]
62+
public void HmacMd5_Etm()
63+
{
64+
DoTest(MessageAuthenticationCodeAlgorithm.HmacMd5Etm);
65+
}
66+
67+
[TestMethod]
68+
public void HmacMd5_96_Etm()
69+
{
70+
DoTest(MessageAuthenticationCodeAlgorithm.HmacMd5_96_Etm);
71+
}
72+
73+
[TestMethod]
74+
public void HmacSha1_Etm()
75+
{
76+
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha1Etm);
77+
}
78+
79+
[TestMethod]
80+
public void HmacSha1_96_Etm()
81+
{
82+
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha1_96_Etm);
83+
}
84+
85+
[TestMethod]
86+
public void HmacSha2_256_Etm()
87+
{
88+
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha2_256_Etm);
89+
}
90+
91+
[TestMethod]
92+
public void HmacSha2_512_Etm()
93+
{
94+
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha2_512_Etm);
95+
}
96+
6197
private void DoTest(MessageAuthenticationCodeAlgorithm macAlgorithm)
6298
{
6399
_remoteSshdConfig.ClearMessageAuthenticationCodeAlgorithms()

test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,18 @@ private void SetupMocks()
215215
.Returns((Cipher) null);
216216
_ = _keyExchangeMock.Setup(p => p.CreateClientCipher())
217217
.Returns((Cipher) null);
218-
_ = _keyExchangeMock.Setup(p => p.CreateServerHash())
219-
.Returns((HashAlgorithm) null);
220-
_ = _keyExchangeMock.Setup(p => p.CreateClientHash())
221-
.Returns((HashAlgorithm) null);
218+
_ = _keyExchangeMock.Setup(p => p.CreateServerHash(out It.Ref<bool>.IsAny))
219+
.Returns((ref bool serverEtm) =>
220+
{
221+
serverEtm = false;
222+
return (HashAlgorithm) null;
223+
});
224+
_ = _keyExchangeMock.Setup(p => p.CreateClientHash(out It.Ref<bool>.IsAny))
225+
.Returns((ref bool clientEtm) =>
226+
{
227+
clientEtm = false;
228+
return (HashAlgorithm) null;
229+
});
222230
_ = _keyExchangeMock.Setup(p => p.CreateCompressor())
223231
.Returns((Compressor) null);
224232
_ = _keyExchangeMock.Setup(p => p.CreateDecompressor())

0 commit comments

Comments
 (0)