Skip to content

MAC error / CryptographicException on .NET Framework in 2026.0.0: _serverMac is reused across packets without Initialize() #1829

Description

@sortgnz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions