Skip to content

Support ETM (Encrypt-then-MAC) variants for HMAC #1316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 16, 2024
Merged
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ Private keys can be encrypted using one of the following cipher methods:
* hmac-sha2-512-96
* hmac-ripemd160
* hmac-ripemd160<span></span>@openssh.com
* hmac-md5-etm<span></span>@openssh.com
* hmac-md5-96-etm<span></span>@openssh.com
* hmac-sha1-etm<span></span>@openssh.com
* hmac-sha1-96-etm<span></span>@openssh.com
* hmac-sha2-256-etm<span></span>@openssh.com
* hmac-sha2-512-etm<span></span>@openssh.com

## Framework Support
**SSH.NET** supports the following target frameworks:
Expand Down
28 changes: 18 additions & 10 deletions src/Renci.SshNet/ConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,16 +376,24 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
#pragma warning disable IDE0200 // Remove unnecessary lambda expression; We want to prevent instantiating the HashAlgorithm objects.
HmacAlgorithms = new Dictionary<string, HashInfo>
{
{ "hmac-sha2-256", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key)) },
{ "hmac-sha2-512", new HashInfo(64 * 8, key => CryptoAbstraction.CreateHMACSHA512(key)) },
{ "hmac-sha2-512-96", new HashInfo(64 * 8, key => CryptoAbstraction.CreateHMACSHA512(key, 96)) },
{ "hmac-sha2-256-96", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key, 96)) },
{ "hmac-ripemd160", new HashInfo(160, key => CryptoAbstraction.CreateHMACRIPEMD160(key)) },
{ "hmac-ripemd160@openssh.com", new HashInfo(160, key => CryptoAbstraction.CreateHMACRIPEMD160(key)) },
{ "hmac-sha1", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key)) },
{ "hmac-sha1-96", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key, 96)) },
{ "hmac-md5", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key)) },
{ "hmac-md5-96", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key, 96)) },
/* Encrypt-and-MAC (encrypt-and-authenticate) variants */
{ "hmac-sha2-256", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key), isEncryptThenMAC: false) },
{ "hmac-sha2-512", new HashInfo(64*8, key => CryptoAbstraction.CreateHMACSHA512(key), isEncryptThenMAC: false) },
{ "hmac-sha2-512-96", new HashInfo(64*8, key => CryptoAbstraction.CreateHMACSHA512(key, 96), isEncryptThenMAC: false) },
{ "hmac-sha2-256-96", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key, 96), isEncryptThenMAC: false) },
{ "hmac-ripemd160", new HashInfo(160, key => CryptoAbstraction.CreateHMACRIPEMD160(key), isEncryptThenMAC: false) },
{ "hmac-ripemd160@openssh.com", new HashInfo(160, key => CryptoAbstraction.CreateHMACRIPEMD160(key), isEncryptThenMAC: false) },
{ "hmac-sha1", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key), isEncryptThenMAC: false) },
{ "hmac-sha1-96", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key, 96), isEncryptThenMAC: false) },
{ "hmac-md5", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key), isEncryptThenMAC: false) },
{ "hmac-md5-96", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key, 96), isEncryptThenMAC: false) },
/* Encrypt-then-MAC variants */
{ "hmac-sha2-256-etm@openssh.com", new HashInfo(32*8, key => CryptoAbstraction.CreateHMACSHA256(key), isEncryptThenMAC: true) },
{ "hmac-sha2-512-etm@openssh.com", new HashInfo(64*8, key => CryptoAbstraction.CreateHMACSHA512(key), isEncryptThenMAC: true) },
{ "hmac-sha1-etm@openssh.com", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key), isEncryptThenMAC: true) },
{ "hmac-sha1-96-etm@openssh.com", new HashInfo(20*8, key => CryptoAbstraction.CreateHMACSHA1(key, 96), isEncryptThenMAC: true) },
{ "hmac-md5-etm@openssh.com", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key), isEncryptThenMAC: true) },
{ "hmac-md5-96-etm@openssh.com", new HashInfo(16*8, key => CryptoAbstraction.CreateHMACMD5(key, 96), isEncryptThenMAC: true) },
};
#pragma warning restore IDE0200 // Remove unnecessary lambda expression

Expand Down
13 changes: 12 additions & 1 deletion src/Renci.SshNet/HashInfo.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Security.Cryptography;

using Renci.SshNet.Common;

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

/// <summary>
/// Gets a value indicating whether enable encrypt-then-MAC or use encrypt-and-MAC.
/// </summary>
/// <value>
/// <see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.
/// </value>
public bool IsEncryptThenMAC { get; private set; }

/// <summary>
/// Gets the cipher.
/// </summary>
Expand All @@ -27,10 +36,12 @@ public class HashInfo
/// </summary>
/// <param name="keySize">Size of the key.</param>
/// <param name="hash">The hash algorithm to use for a given key.</param>
public HashInfo(int keySize, Func<byte[], HashAlgorithm> hash)
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
public HashInfo(int keySize, Func<byte[], HashAlgorithm> hash, bool isEncryptThenMAC = false)
{
KeySize = keySize;
HashAlgorithm = key => hash(key.Take(KeySize / 8));
IsEncryptThenMAC = isEncryptThenMAC;
}
}
}
10 changes: 7 additions & 3 deletions src/Renci.SshNet/Messages/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected override void WriteBytes(SshDataStream stream)
base.WriteBytes(stream);
}

internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor)
internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool isEncryptThenMAC = false)
{
const int outboundPacketSequenceSize = 4;

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

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

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

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

var packetDataLength = GetPacketDataLength(messageLength, paddingLength);

Expand Down
6 changes: 4 additions & 2 deletions src/Renci.SshNet/Security/IKeyExchange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,20 @@ public interface IKeyExchange : IDisposable
/// <summary>
/// Creates the server-side hash algorithm to use.
/// </summary>
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
/// <returns>
/// The server hash algorithm.
/// </returns>
HashAlgorithm CreateServerHash();
HashAlgorithm CreateServerHash(out bool isEncryptThenMAC);

/// <summary>
/// Creates the client-side hash algorithm to use.
/// </summary>
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
/// <returns>
/// The client hash algorithm.
/// </returns>
HashAlgorithm CreateClientHash();
HashAlgorithm CreateClientHash(out bool isEncryptThenMAC);

/// <summary>
/// Creates the compression algorithm to use to deflate data.
Expand Down
12 changes: 9 additions & 3 deletions src/Renci.SshNet/Security/KeyExchange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ from a in message.MacAlgorithmsClientToServer
select a).FirstOrDefault();
if (string.IsNullOrEmpty(clientHmacAlgorithmName))
{
throw new SshConnectionException("Server HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
throw new SshConnectionException("Client HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
}

session.ConnectionInfo.CurrentClientHmacAlgorithm = clientHmacAlgorithmName;
Expand Down Expand Up @@ -218,11 +218,14 @@ public Cipher CreateClientCipher()
/// <summary>
/// Creates the server side hash algorithm to use.
/// </summary>
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
/// <returns>
/// The server-side hash algorithm.
/// </returns>
public HashAlgorithm CreateServerHash()
public HashAlgorithm CreateServerHash(out bool isEncryptThenMAC)
{
isEncryptThenMAC = _serverHashInfo.IsEncryptThenMAC;

// Resolve Session ID
var sessionId = Session.SessionId ?? ExchangeHash;

Expand All @@ -241,11 +244,14 @@ public HashAlgorithm CreateServerHash()
/// <summary>
/// Creates the client side hash algorithm to use.
/// </summary>
/// <param name="isEncryptThenMAC"><see langword="true"/> to enable encrypt-then-MAC, <see langword="false"/> to use encrypt-and-MAC.</param>
/// <returns>
/// The client-side hash algorithm.
/// </returns>
public HashAlgorithm CreateClientHash()
public HashAlgorithm CreateClientHash(out bool isEncryptThenMAC)
{
isEncryptThenMAC = _clientHashInfo.IsEncryptThenMAC;

// Resolve Session ID
var sessionId = Session.SessionId ?? ExchangeHash;

Expand Down
75 changes: 64 additions & 11 deletions src/Renci.SshNet/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ public class Session : ISession

private HashAlgorithm _clientMac;

private bool _serverEtm;

private bool _clientEtm;

private Cipher _clientCipher;

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

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

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

if (_clientMac != null)
if (_clientMac != null && !_clientEtm)
{
// write outbound packet sequence to start of packet data
Pack.UInt32ToBigEndian(_outboundPacketSequence, packetData);
Expand All @@ -1075,8 +1079,29 @@ internal void SendMessage(Message message)
// Encrypt packet data
if (_clientCipher != null)
{
packetData = _clientCipher.Encrypt(packetData, packetDataOffset, packetData.Length - packetDataOffset);
packetDataOffset = 0;
if (_clientMac != null && _clientEtm)
{
// The length of the "packet length" field in bytes
const int packetLengthFieldLength = 4;

var encryptedData = _clientCipher.Encrypt(packetData, packetDataOffset + packetLengthFieldLength, packetData.Length - packetDataOffset - packetLengthFieldLength);

Array.Resize(ref packetData, packetDataOffset + packetLengthFieldLength + encryptedData.Length);

// write outbound packet sequence to start of packet data
Pack.UInt32ToBigEndian(_outboundPacketSequence, packetData);

// write encrypted data
Buffer.BlockCopy(encryptedData, 0, packetData, packetDataOffset + packetLengthFieldLength, encryptedData.Length);

// calculate packet hash
hash = _clientMac.ComputeHash(packetData);
}
else
{
packetData = _clientCipher.Encrypt(packetData, packetDataOffset, packetData.Length - packetDataOffset);
packetDataOffset = 0;
}
}

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

// Determine the size of the first block, which is 8 or cipher block size (whichever is larger) bytes
var blockSize = _serverCipher is null ? (byte) 8 : Math.Max((byte) 8, _serverCipher.MinimumSize);
int blockSize;

// Determine the size of the first block which is 8 or cipher block size (whichever is larger) bytes
// The "packet length" field is not encrypted in ETM.
if (_serverMac != null && _serverEtm)
{
blockSize = (byte) 4;
}
else if (_serverCipher != null)
{
blockSize = Math.Max((byte) 8, _serverCipher.MinimumSize);
}
else
{
blockSize = (byte) 8;
}

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

Expand All @@ -1215,7 +1254,7 @@ private Message ReceiveMessage(Socket socket)
return null;
}

if (_serverCipher != null)
if (_serverCipher != null && (_serverMac == null || !_serverEtm))
{
firstBlock = _serverCipher.Decrypt(firstBlock);
}
Expand Down Expand Up @@ -1257,6 +1296,20 @@ private Message ReceiveMessage(Socket socket)
}
}

// validate encrypted message against MAC
if (_serverMac != null && _serverEtm)
{
var clientHash = _serverMac.ComputeHash(data, 0, data.Length - serverMacLength);
var serverHash = data.Take(data.Length - serverMacLength, serverMacLength);

// TODO Add IsEqualTo overload that takes left+right index and number of bytes to compare.
// TODO That way we can eliminate the extra allocation of the Take above.
if (!serverHash.IsEqualTo(clientHash))
{
throw new SshConnectionException("MAC error", DisconnectReason.MacError);
}
}

if (_serverCipher != null)
{
var numberOfBytesToDecrypt = data.Length - (blockSize + inboundPacketSequenceLength + serverMacLength);
Expand All @@ -1271,8 +1324,8 @@ private Message ReceiveMessage(Socket socket)
var messagePayloadLength = (int) packetLength - paddingLength - paddingLengthFieldLength;
var messagePayloadOffset = inboundPacketSequenceLength + packetLengthFieldLength + paddingLengthFieldLength;

// validate message against MAC
if (_serverMac != null)
// validate decrypted message against MAC
if (_serverMac != null && !_serverEtm)
{
var clientHash = _serverMac.ComputeHash(data, 0, data.Length - serverMacLength);
var serverHash = data.Take(data.Length - serverMacLength, serverMacLength);
Expand Down Expand Up @@ -1472,8 +1525,8 @@ internal void OnNewKeysReceived(NewKeysMessage message)
// Update negotiated algorithms
_serverCipher = _keyExchange.CreateServerCipher();
_clientCipher = _keyExchange.CreateClientCipher();
_serverMac = _keyExchange.CreateServerHash();
_clientMac = _keyExchange.CreateClientHash();
_serverMac = _keyExchange.CreateServerHash(out _serverEtm);
_clientMac = _keyExchange.CreateClientHash(out _clientEtm);
_clientCompression = _keyExchange.CreateCompressor();
_serverDecompression = _keyExchange.CreateDecompressor();

Expand Down
36 changes: 36 additions & 0 deletions test/Renci.SshNet.IntegrationTests/HmacTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,42 @@ public void HmacSha2_512()
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha2_512);
}

[TestMethod]
public void HmacMd5_Etm()
{
DoTest(MessageAuthenticationCodeAlgorithm.HmacMd5Etm);
}

[TestMethod]
public void HmacMd5_96_Etm()
{
DoTest(MessageAuthenticationCodeAlgorithm.HmacMd5_96_Etm);
}

[TestMethod]
public void HmacSha1_Etm()
{
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha1Etm);
}

[TestMethod]
public void HmacSha1_96_Etm()
{
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha1_96_Etm);
}

[TestMethod]
public void HmacSha2_256_Etm()
{
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha2_256_Etm);
}

[TestMethod]
public void HmacSha2_512_Etm()
{
DoTest(MessageAuthenticationCodeAlgorithm.HmacSha2_512_Etm);
}

private void DoTest(MessageAuthenticationCodeAlgorithm macAlgorithm)
{
_remoteSshdConfig.ClearMessageAuthenticationCodeAlgorithms()
Expand Down
16 changes: 12 additions & 4 deletions test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,18 @@ private void SetupMocks()
.Returns((Cipher) null);
_ = _keyExchangeMock.Setup(p => p.CreateClientCipher())
.Returns((Cipher) null);
_ = _keyExchangeMock.Setup(p => p.CreateServerHash())
.Returns((HashAlgorithm) null);
_ = _keyExchangeMock.Setup(p => p.CreateClientHash())
.Returns((HashAlgorithm) null);
_ = _keyExchangeMock.Setup(p => p.CreateServerHash(out It.Ref<bool>.IsAny))
.Returns((ref bool serverEtm) =>
{
serverEtm = false;
return (HashAlgorithm) null;
});
_ = _keyExchangeMock.Setup(p => p.CreateClientHash(out It.Ref<bool>.IsAny))
.Returns((ref bool clientEtm) =>
{
clientEtm = false;
return (HashAlgorithm) null;
});
_ = _keyExchangeMock.Setup(p => p.CreateCompressor())
.Returns((Compressor) null);
_ = _keyExchangeMock.Setup(p => p.CreateDecompressor())
Expand Down
Loading