You'll see SEC_I_CONTINUE_NEEDED (0x00090312) most often when a piece of software is mid-TLS handshake and something upstream decides the conversation is over. Classic trigger: a .NET service does an LDAPS bind to a domain controller, or a WinHTTP client talks to an internal API behind a load balancer, and the app logs the code as if it were a fatal error. The connection drops, the retry storm starts, and someone files a ticket titled "0x00090312 keeps appearing in event log." That's the scenario I get paged for.
What 0x00090312 actually means
The name is the giveaway: SEC_I_CONTINUE_NEEDED. The I stands for informational, not error. SSPI's InitializeSecurityContext and AcceptSecurityContext are state machines. They don't do the whole handshake in one call. Each call consumes input buffers, produces output buffers, and tells you what to do next:
SEC_E_OK(0x00000000) — done, context is established.SEC_I_CONTINUE_NEEDED(0x00090312) — more tokens to exchange, call me again.SEC_I_COMPLETE_NEEDED/SEC_I_COMPLETE_AND_CONTINUE— finish the context, sometimes callCompleteAuthToken.SEC_E_INCOMPLETE_MESSAGE(0x80090318) — the buffer you fed me is short. Read more bytes and retry.
So 0x00090312 by itself is normal. It only becomes a problem when code doesn't loop. Common culprits: a custom SspiClient wrapper that treats anything non-zero as fatal, a WinHTTP callback that bails on the first ERROR_WINHTTP_SECURE_FAILURE after the continue, or a shim DLL that swallows the informational code and returns it up the stack as a hard failure. Microsoft's own docs say it plainly: if you get this, feed the output token to the peer and call again.
Where you'll actually run into it
Three real-world spots I've hit in the last year:
- Custom SSPI/LDAP code. Someone wrote a Kerberos or NTLM binder against
secur32.dlland didn't loop onSEC_I_CONTINUE_NEEDED. Works with NTLM on the LAN, breaks the moment a smart card or PKINIT is involved, because PKINIT needs multiple token round-trips. - .NET
SslStreammisuse. People callAuthenticateAsClienton a stream that's already been partially negotiated by a proxy, or wrap aNetworkStreamwithout draining pending buffered bytes. You'll get the 0x90312 surfaced throughWin32ExceptionfromSspiHandle. - WinHTTP with a TLS-terminating proxy. The proxy returns
407, WinHTTP tries to renegotiate, and the app'sWINHTTP_CALLBACK_STATUS_SECURE_FAILUREhandler doesn't distinguish "need more data" from "cert is garbage."
The fix
There's no registry key for this one. The fix is in code — yours or the vendor's. Walk it in order.
1. Confirm it's informational, not fatal
Before you touch anything, check whether 0x90312 is the failing code or just the last code logged before a different failure. In .NET, catch the exception and dump the full Win32Exception.NativeErrorCode and the inner one. If the outer error is SEC_E_UNTRUSTED_ROOT (0x80090325) or SEC_E_WRONG_PRINCIPAL (0x80090322), the 0x90312 is a red herring. Trace with Wireshark on 443 and look for the last TLS alert before the disconnect. That tells you the real story.
2. Fix the calling code to loop
If it's your code, the pattern is straightforward. Don't return on non-zero unless it's a real error:
SECURITY_STATUS status;
CtxtHandle hCtx = {0};
SecBuffer outBuf = {0};
SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
PBYTE pIn = NULL; ULONG cbIn = 0;
for (;;) {
status = InitializeSecurityContext(
&cred, hCtx.dwLower ? &hCtx : NULL,
targetSpn, ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM,
0, 0, NULL, 0, &hCtx, &outDesc, &attrs, NULL);
if (status == SEC_I_CONTINUE_NEEDED) {
SendTokenToPeer(outBuf.pvBuffer, outBuf.cbBuffer);
SecFree(outBuf.pvBuffer);
RecvTokenFromPeer(&pIn, &cbIn); // read the peer's token
continue;
}
if (status == SEC_E_INCOMPLETE_MESSAGE) {
ReadMoreBytes();
continue;
}
if (status == SEC_E_OK) break;
// anything else is a real failure
return status;
}
Two things bite people here: you must free the output buffer if you passed ISC_REQ_ALLOCATE_MEMORY, and you must pass the correct input buffer on the next call. A null input buffer on the second iteration is the single most common bug I've seen.
3. Fix .NET SslStream setups
If you're using SslStream and seeing this, stop wrapping streams manually. Use the async overloads and let the framework own the state machine:
var ssl = new SslStream(tcp.GetStream(), false,
(s, cert, chain, errors) => errors == SslPolicyErrors.None);
await ssl.AuthenticateAsClientAsync(targetHost, null,
SslProtocols.Tls12 | SslProtocols.Tls13, true);
Also make sure you're on .NET 4.8 or .NET 6+. A batch of older 4.6.x builds mishandled SEC_I_COMPLETE_NEEDED with smart cards. If you're pinned to 4.6.x for some legacy reason, update — you'll save yourself a week.
4. Check for an intercepting proxy or TLS inspection
If neither your code nor the framework is obviously at fault, something in the path is doing TLS renegotiation or MITM. Common sources:
- Zscaler, Netskope, Palo Alto decryption policy, or a FortiGate doing SSL inspection.
- An internal load balancer that renegotiates client certs and doesn't pass the intermediate token cleanly.
- A shim DLL from a third-party VPN or DLP agent that hooks
secur32!InitializeSecurityContextW.
Test from a machine on the same subnet that isn't behind the proxy. If the error vanishes, it's a path problem, not your app.
If it still fails
Work down this list before opening a case with Microsoft — they'll ask for all of it anyway.
- Grab a network trace with Wireshark on 443. Filter
tls.handshakeand look at the last alert. If you seehandshake_failure (40)right after a renegotiation, it's the proxy. - Turn on Schannel logging:
HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\EventLogging = 1, then check System log for event 36871/36874/36888. Those give you the underlying crypto reason. - Update the root CA store. An outdated
Rootstore is a silent killer on Server 2016 boxes that haven't patched since 2021. - Test with
curl.exe -v https://targetover the same network path. Modern Windows ships a real curl. If curl succeeds and your app fails, the bug is in your code or your TLS stack. - Check TLS versions. If the peer only offers TLS 1.3 and you're on Server 2012 R2, you'll get a cascade that ends in odd SSPI codes. Enable TLS 1.2 client-side via
HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Clientand setEnabled = 1,DisabledByDefault = 0.
The short version: 0x00090312 is almost never the real problem. It's the last handshake step before something else went wrong. Find that something else and this code disappears on its own.