Summary
Since 2026.0.0, connections from .NET Framework fail on the second MAC-protected packet
received, on machines whose mscorlib.dll predates the HMAC rework (see Environment below).
2025.1.0 works fine against the same server from the same machine.
The machine reports .NET Framework 4.8 with Release=528049 either way, so the affected
configuration is not distinguishable from the supported one by the usual version check.
Depending on the MAC algorithm the symptom differs:
| MAC |
Symptom |
hmac-sha2-256, hmac-sha2-512 |
SshConnectionException: MAC error (DisconnectReason.MacError) |
hmac-sha1 |
CryptographicException: Hash not valid for use in specified state. |
Typical stack trace:
Renci.SshNet.Common.SshConnectionException: MAC error
at Renci.SshNet.Session.ReceiveMessage(Socket socket)
at Renci.SshNet.Session.MessageListener()
at Renci.SshNet.Session.WaitOnHandle(WaitHandle waitHandle)
at Renci.SshNet.NoneAuthenticationMethod.Authenticate(Session session)
at Renci.SshNet.ClientAuthentication.Authenticate(IConnectionInfoInternal connectionInfo, ISession session)
at Renci.SshNet.Session.ConnectAsync()
The first MAC-protected packet (SSH_MSG_SERVICE_ACCEPT) verifies correctly; the next one
(SSH_MSG_USERAUTH_FAILURE) fails. That is the tell: the failure is not about the packet, it is
about the state of the HashAlgorithm instance after the first use.
Cause
Session.ReceiveMessage changed how the receive MAC is computed.
2025.1.0:
var clientHash = _serverMac.ComputeHash(data, 0, data.Length - serverMacLength);
2026.0.0:
_ = _serverMac.TransformBlock(_inboundPacketSequenceBytes, 0, 4, outputBuffer: null, 0);
_ = _serverMac.TransformBlock(_receiveBuffer.DangerousGetUnderlyingBuffer(),
_receiveBuffer.ActiveStartOffset,
totalPacketLength - serverMacLength,
outputBuffer: null, 0);
_ = _serverMac.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
if (!CryptoAbstraction.FixedTimeEquals(_serverMac.Hash, ...))
{
throw new SshConnectionException("MAC error", DisconnectReason.MacError);
}
ComputeHash resets the algorithm on every call. The new code reuses the same _serverMac
instance for the whole session and never calls Initialize() between packets.
On .NET Framework that is not valid. From Microsoft's reference source:
// mscorlib/system/security/cryptography/hashalgorithm.cs
public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) {
HashCore(inputBuffer, inputOffset, inputCount);
HashValue = HashFinal();
...
// reset the State value
State = 0; // <-- does NOT call Initialize()
return outputBytes;
}
// mscorlib/system/security/cryptography/hmac.cs
public override void Initialize() {
m_hash1.Initialize(); // <-- the only thing that resets the inner hashes
m_hash2.Initialize();
m_hashing = false;
}
TransformFinalBlock only resets State. The inner m_hash1/m_hash2 keep their accumulated
state, so the next packet's data is appended to the previous packet's state and the resulting MAC
is wrong. The CAPI-backed implementations detect the finalized state and throw instead.
This affects both the ETM and the non-ETM branches (the two TransformFinalBlock call sites).
Note this is not a platform quirk to work around: the documented contract requires Initialize()
before reusing a HashAlgorithm. The machines where it currently works are the ones getting away
with it, not the other way round — which is why the failure looks environment-dependent.
Minimal repro — no network, no SSH server
using System;
using System.Linq;
using System.Security.Cryptography;
var key = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var sequence = new byte[] { 0, 0, 0, 7 };
var packet = Enumerable.Range(0, 64).Select(i => (byte)(i * 3)).ToArray();
byte[] expected;
using (var reference = new HMACSHA256(key))
{
expected = reference.ComputeHash(sequence.Concat(packet).ToArray());
}
using var mac = new HMACSHA256(key);
for (int packetNumber = 1; packetNumber <= 3; packetNumber++)
{
mac.TransformBlock(sequence, 0, sequence.Length, null, 0);
mac.TransformBlock(packet, 0, packet.Length, null, 0);
mac.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
Console.WriteLine($"packet #{packetNumber}: {(mac.Hash.SequenceEqual(expected) ? "OK" : "WRONG HASH")}");
}
On an affected machine this prints:
packet #1: OK
packet #2: WRONG HASH
packet #3: WRONG HASH
With HMACSHA1 instead, packet #2 throws
CryptographicException: Hash not valid for use in specified state.
On an unaffected machine all three print OK.
Environment — what decides whether a machine is affected
Two machines, both Windows Server 2019 / .NET Framework 4.8 / net48, both reporting
Release=528049 and Version=4.8.03761, both with FIPS disabled. Same SSH.NET build
(2026.0.0.1+7b2fd3dbf2), same server (SSH-2.0-dropbear_2018.76), same negotiated algorithms
(curve25519-sha256 + aes128-ctr + hmac-sha2-256).
The discriminator is the file version of mscorlib.dll, which the registry Release value does
not reflect — Windows cumulative updates patch these binaries without changing it:
mscorlib.dll |
internal HMAC fields |
repro |
SSH connection |
| 4.8.4739.0 |
(no longer present) |
all packets OK |
connects, 10/10 |
| 4.8.4110.0 |
m_hash1=SHA256Managed, m_hash2=SHA256Managed |
packet #2 wrong hash |
MAC error, 0/10 |
On 4.8.4110.0 the inner hash objects are still the older m_hash1/m_hash2 design, which does not
support reuse after TransformFinalBlock. On 4.8.4739.0 those fields are gone — the HMAC
implementation was reworked at some point between the two, and the reworked one tolerates reuse.
This also explains the two different symptoms on the same machine:
| MAC |
inner implementation |
behaviour on reuse |
hmac-sha1 |
SHA1CryptoServiceProvider (CAPI) |
throws CryptographicException |
hmac-sha2-256 / -512 |
SHA256Managed / SHA512Managed |
silently returns a wrong hash |
The silent case is the dangerous one: it is indistinguishable from a genuine integrity failure.
With 2025.1.0 both machines connect reliably against the same server, across all seven common key
exchange algorithms and every cipher/MAC combination.
Suggested fix
Call Initialize() on the MAC before (or after) each use, in both the ETM and non-ETM branches of
ReceiveMessage, and in the corresponding send path if it uses the same pattern. That restores the
guarantee ComputeHash used to provide, at negligible cost.
Note that on the affected configuration the failure is silent for SHA-2 — a wrong hash rather
than an exception — so a unit test asserting a correct MAC over two consecutive packets on the same
instance would be worth adding.
Summary
Since 2026.0.0, connections from .NET Framework fail on the second MAC-protected packet
received, on machines whose
mscorlib.dllpredates the HMAC rework (see Environment below).2025.1.0 works fine against the same server from the same machine.
The machine reports .NET Framework 4.8 with
Release=528049either way, so the affectedconfiguration is not distinguishable from the supported one by the usual version check.
Depending on the MAC algorithm the symptom differs:
hmac-sha2-256,hmac-sha2-512SshConnectionException: MAC error(DisconnectReason.MacError)hmac-sha1CryptographicException: Hash not valid for use in specified state.Typical stack trace:
The first MAC-protected packet (
SSH_MSG_SERVICE_ACCEPT) verifies correctly; the next one(
SSH_MSG_USERAUTH_FAILURE) fails. That is the tell: the failure is not about the packet, it isabout the state of the
HashAlgorithminstance after the first use.Cause
Session.ReceiveMessagechanged how the receive MAC is computed.2025.1.0:
2026.0.0:
ComputeHashresets the algorithm on every call. The new code reuses the same_serverMacinstance for the whole session and never calls
Initialize()between packets.On .NET Framework that is not valid. From Microsoft's reference source:
TransformFinalBlockonly resetsState. The innerm_hash1/m_hash2keep their accumulatedstate, so the next packet's data is appended to the previous packet's state and the resulting MAC
is wrong. The CAPI-backed implementations detect the finalized state and throw instead.
This affects both the ETM and the non-ETM branches (the two
TransformFinalBlockcall sites).Note this is not a platform quirk to work around: the documented contract requires
Initialize()before reusing a
HashAlgorithm. The machines where it currently works are the ones getting awaywith it, not the other way round — which is why the failure looks environment-dependent.
Minimal repro — no network, no SSH server
On an affected machine this prints:
With
HMACSHA1instead, packet #2 throwsCryptographicException: Hash not valid for use in specified state.On an unaffected machine all three print
OK.Environment — what decides whether a machine is affected
Two machines, both Windows Server 2019 / .NET Framework 4.8 / net48, both reporting
Release=528049andVersion=4.8.03761, both with FIPS disabled. Same SSH.NET build(
2026.0.0.1+7b2fd3dbf2), same server (SSH-2.0-dropbear_2018.76), same negotiated algorithms(
curve25519-sha256+aes128-ctr+hmac-sha2-256).The discriminator is the file version of
mscorlib.dll, which the registryReleasevalue doesnot reflect — Windows cumulative updates patch these binaries without changing it:
mscorlib.dllm_hash1=SHA256Managed, m_hash2=SHA256ManagedMAC error, 0/10On 4.8.4110.0 the inner hash objects are still the older
m_hash1/m_hash2design, which does notsupport reuse after
TransformFinalBlock. On 4.8.4739.0 those fields are gone — the HMACimplementation was reworked at some point between the two, and the reworked one tolerates reuse.
This also explains the two different symptoms on the same machine:
hmac-sha1SHA1CryptoServiceProvider(CAPI)CryptographicExceptionhmac-sha2-256/-512SHA256Managed/SHA512ManagedThe silent case is the dangerous one: it is indistinguishable from a genuine integrity failure.
With 2025.1.0 both machines connect reliably against the same server, across all seven common key
exchange algorithms and every cipher/MAC combination.
Suggested fix
Call
Initialize()on the MAC before (or after) each use, in both the ETM and non-ETM branches ofReceiveMessage, and in the corresponding send path if it uses the same pattern. That restores theguarantee
ComputeHashused to provide, at negligible cost.Note that on the affected configuration the failure is silent for SHA-2 — a wrong hash rather
than an exception — so a unit test asserting a correct MAC over two consecutive packets on the same
instance would be worth adding.