jstedfast/MailKit · error · SaslException

InvalidChallenge

InvalidChallenge

Error message

Challenge contained an invalid nonce.

What it means

After extracting the SCRAM server nonce ('r'), MailKit verifies that it starts with the client nonce sent in the client-first message. If not, SaslException with SaslErrorCode.InvalidChallenge and "Challenge contained an invalid nonce" is thrown (SaslMechanismScramBase.cs:354). RFC 5802 requires server_nonce = client_nonce + server_nonce, so a mismatch indicates a broken or malicious server.

Solutions

  1. Capture the client-first and server-first messages (enable protocol logging, e.g. IProtocolLogger) and compare nonce prefixes
  2. Confirm the server implements RFC 5802 nonce concatenation; upgrade or patch the server
  3. Rule out MITM/replay by using TLS for the connection
  4. Fall back to another mechanism if the server's SCRAM nonce logic is broken

Example fix

// before
client.Authenticate (new SaslMechanismScramSha256 ("user", "pass")); // non-conformant server nonce
// after
try {
    client.Authenticate (new SaslMechanismScramSha256 ("user", "pass"));
} catch (SaslException ex) when (ex.ErrorCode == SaslErrorCode.InvalidChallenge && ex.Message.Contains ("nonce")) {
    client.Authenticate (new SaslMechanismPlain ("user", "pass")); // over TLS
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Enforce TLS so nonce mismatches cannot come from tampering:
if (!client.IsSecure)
    await client.ConnectAsync (host, port, SecureSocketOptions.SslOnConnect);

Try / catch

try {
    client.Authenticate (scramMechanism);
} catch (SaslException ex) when (ex.ErrorCode == SaslErrorCode.InvalidChallenge &&
                                 ex.Message.Contains ("invalid nonce")) {
    // server nonce does not extend the client nonce
}

Prevention

When it happens

Trigger: Calling Challenge() on a SCRAM mechanism where the server's r=... value does not begin with the exact cnonce the client generated (e.g. server returns a wholly different nonce, or a replayed stale challenge).

Common situations: Custom/buggy SCRAM server implementations that ignore the client nonce, challenge replay by a man-in-the-middle, or caching proxies serving stale challenges.

Related errors


AI-assisted analysis of jstedfast/MailKit@9d3859a785 (2026-09-15). Data as JSON: /api/errors/b58d3c607faf0413. Report an issue: GitHub.

Appendix: source

Thrown at MailKit/Security/SaslMechanismScramBase.cs:354

				if (token == null)
					throw new SaslException (MechanismName, SaslErrorCode.MissingChallenge, "Server response did not contain any authentication data.");

				server = Encoding.UTF8.GetString (token, startIndex, length);
				var tokens = ParseServerChallenge (server);
				string? salt, nonce, iterations;
				int count;

				if (!tokens.TryGetValue ('s', out salt))
					throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain a salt.");

				if (!tokens.TryGetValue ('r', out nonce))
					throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain a nonce.");

				if (!tokens.TryGetValue ('i', out iterations))
					throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain an iteration count.");

				if (!nonce.StartsWith (cnonce!, StringComparison.Ordinal))
					throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid nonce.");

				if (!int.TryParse (iterations, NumberStyles.None, CultureInfo.InvariantCulture, out count) || count < 1)
					throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid iteration count.");

				var password = Encoding.UTF8.GetBytes (SaslPrep (Credentials.Password));
				salted = Hi (password, Convert.FromBase64String (salt), count);
				Array.Clear (password, 0, password.Length);

				input = GetChannelBindingInput (channelBindingKind, AuthorizationId);
				var inputBuffer = Encoding.ASCII.GetBytes (input);
				string base64;

				if (SupportsChannelBinding && channelBindingToken != null) {
					var binding = new byte[inputBuffer.Length + channelBindingToken.Length];

					Buffer.BlockCopy (inputBuffer, 0, binding, 0, inputBuffer.Length);
					Buffer.BlockCopy (channelBindingToken, 0, binding, inputBuffer.Length, channelBindingToken.Length);

View on GitHub (pinned to 9d3859a785)