Repository navigation
MAC error / CryptographicException on .NET Framework in 2026.0.0: _serverMac is reused across packets without Initialize() #1829
Description
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.
Activity
nallejacobsson commentedon Aug 21, 2026
Running into this one after upgrading from 2024.1.0 to 2026.0.0 on .NET Framework 4.7.2.
Downgrading until this is resolved.
Reset server MAC after TransformFinalBlock on .NET FW
Rob-Hague commentedon Aug 22, 2026
I can't find any documentation about this, unfortunately
It's not dangerous from a security perspective. It causes false positives (valid message rejected) rather than false negatives (invalid message accepted)
It wouldn't because almost all tests go through this path and it would have been caught already. This feels more like a runtime bug that is simple enough to work around, but hard to ensure it doesn't regress the future.
Can somebody please verify #1830?
WojciechNagorski commentedon Sep 1, 2026
@Rob-Hague Hi, when do you plan to release next version?
devesh1207 commentedon Sep 2, 2026
I see the PR for the fix is merged. When can we expect the next release?
mus65 commentedon Sep 2, 2026
You can use a pre-release NuGet package if you don't want to wait for the next release.
devesh1207 commentedon Sep 10, 2026
Hey, when is the next release scheduled where this fix will be carried forward? We can't use the pre-release NuGet package.
maximrykhlik commentedon Sep 10, 2026
Hi, thank you for addressing #1829.
We use SSH.NET in our product, and it has been very valuable to us. One of our supported product branches still targets an older .NET Framework version. Because of this regression, we had to roll back to an older SSH.NET release, which is now flagged for CVE-2026-48798. Although we do not use the vulnerable API, remaining on the older version is not a desirable long-term solution.
Would it be possible to prioritize a patch release containing the final fix for #1829? Do you have even a rough indication of whether such a release is planned?
Is there anything we can do to help move it forward?
scott-xu commentedon Sep 10, 2026
It would be good if someone can verify the fix before release.
nblair2 commentedon Sep 18, 2026
I have done some light testing with cmu-sei/GHOSTS#717, it seems to have resolved the issue for me.
devesh1102 commentedon Sep 24, 2026
I also did some testing, and the issue was resolved for me with the prerelease package. Can we have a new release soon? Or is there a timeline for when the next release will happen, since in our production environment, due to policies, we cannot use the prerelease version?