Skip to content

Add Windows SSPI Negotiate#147

Open
micha102 wants to merge 1 commit intoversat:masterfrom
micha102:master
Open

Add Windows SSPI Negotiate#147
micha102 wants to merge 1 commit intoversat:masterfrom
micha102:master

Conversation

@micha102
Copy link
Copy Markdown

In this PR, I add support of SSPI Negotiate for Windows. (Previously only NTLM is supported)
SSPI Negotiate mode directly generates a Kerberos ticket to inherit the user session. No login or password are needed.
Also, the authenticate is done on one-shot (no 407 Proxy Authentication Required received but directly 200 Connection Established)
I tested it with our corporate proxy successfully.

Kindly review it.

Note: I didn't integrate it to the -M switch (automatic configuration detection)

@fralken
Copy link
Copy Markdown
Collaborator

fralken commented Apr 14, 2026

Hello @micha102 , thanks, this is a nice addition. I cannot test it myself right now, but it looks well written and documented. I added a couple of comments in proxy.c.

Also, since now sspi supports both ntlm and negotiate, I would add few changes in the sspi code:

  1. add a new method int sspi_is_ntlm(void) similar to sspi_is_negotiate(void), and use this method in ntlm.c. This is because sspi_enable now doesn't guarantee that it refers to ntlm.

  2. rename sspi_request and sspi_response to sspi_ntlm_request and sspi_ntlm_response so it is more clear that they implement this protocol.

  3. rebase your code on versat:master so that the merge commit is not necessary

Thanks

@micha102
Copy link
Copy Markdown
Author

Thanks.
Will work on the modification this week.

@micha102 micha102 marked this pull request as draft April 15, 2026 12:36
@micha102 micha102 closed this May 1, 2026
@micha102 micha102 reopened this May 1, 2026
@micha102 micha102 force-pushed the master branch 2 times, most recently from 2d4975b to 23f404c Compare May 1, 2026 07:50
@micha102
Copy link
Copy Markdown
Author

micha102 commented May 1, 2026

I did the renaming of the old functions which assumed SSPI is only NTLM to explicitly say they are NTLM.
But after I force pushed, I couldn't find anymore the comments you did on proxy.c
Can you please re-post them ?

@micha102 micha102 marked this pull request as ready for review May 1, 2026 08:02
@fralken
Copy link
Copy Markdown
Collaborator

fralken commented May 1, 2026

Hello @micha102 I guess you can still view the comments if you go to the "Files changed" tab. There you can see my comments in main.c and proxy.c.

Are sspi_negotiate_response() and sspi_negotiate_cleanup() used anywhere?

@micha102
Copy link
Copy Markdown
Author

micha102 commented May 3, 2026

I wanted to add a proper cleanup method for the allocated sspi_negotiate_state object, but it seems that the thread is terminated at an earlier stage at the bailout of forward.c where it exits the thread, so it's not necessary.
For the sspi_negotiate_response method, I first cloned the function sspi_ntlm_response but then when I realize it's one-shot authentication it's not needed anymore.

I cleanup both 2 functions as well as the variables storing the initialization and termination states.

But for the comments in proxy.c I still can't see them sorry
image

@fralken
Copy link
Copy Markdown
Collaborator

fralken commented May 3, 2026

This is weird, in the same view I see my comments (see below).
Also, int sspi_negotiate_request can be renamed to int sspi_negotiate since there is not a corresponding response. In sspi.h you should put extern like the other function declarations, since they are referenced in other files (proxy.c). Same for sspi_get_scheme.

Again, thanks a lot for this PR, it is a nice addition to the SSPI support.

Screenshot 2026-05-03 alle 18 01 01 Screenshot 2026-05-03 alle 18 01 24 Screenshot 2026-05-03 alle 18 01 43 Screenshot 2026-05-03 alle 18 02 00

@fralken fralken self-requested a review May 3, 2026 18:06
@versat versat requested a review from Copilot May 4, 2026 05:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends CNTLM’s Windows (Cygwin) SSPI authentication support by adding an SSPI Negotiate mode alongside the existing SSPI NTLM mode, enabling Kerberos/Negotiate-based proxy authentication using the current Windows logon session.

Changes:

  • Added SSPI Negotiate mode helpers/APIs and scheme reporting.
  • Updated proxy authentication flow to emit Proxy-Authorization: Negotiate … using a per-thread Negotiate state.
  • Updated documentation to mention both NTLM and Negotiate SSPI modes.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
sspi.h Adds Negotiate state struct and new SSPI API declarations.
sspi.c Adds mode checks (NTLM/Negotiate), scheme getter, Negotiate token generation, and renames SSPI NTLM functions.
proxy.c Adds thread-local Negotiate state and uses SSPI Negotiate in proxy_authenticate().
ntlm.c Updates SSPI NTLM function call names and gates response path on sspi_is_ntlm().
main.c Adjusts password/hashes requirement logic for SSPI Negotiate mode.
doc/cntlm.conf Updates SSPI comment to reflect Negotiate support.
doc/cntlm.1 Updates manpage to document SSPI ... Negotiate.
Comments suppressed due to low confidence (1)

sspi.c:223

  • In UNICODE builds, sspi_mode allocation/conversion does not leave room for the terminating NUL (zmalloc(sizeof(wchar_t) * strlen(mode)) and mbstowcs(..., strlen(mode))). This can leave sspi_mode unterminated and make later wcscmp read past the buffer. Allocate strlen(mode)+1 and convert/copy including the terminator.
	if ((!strcasecmp("NTLM", mode) || !strcasecmp("Negotiate", mode)) && sspi_dll)  // Only NTLM and Negotiate supported for now
	{
#ifdef UNICODE
		sspi_mode = zmalloc(sizeof(wchar_t) * strlen(mode));
		mbstowcs(sspi_mode, mode, strlen(mode));
#else

Comment thread sspi.c
Comment on lines +187 to +198
#ifdef UNICODE
return (wcscmp(sspi_mode, L"Negotiate") == 0);
#else
return (strcasecmp(sspi_mode, "Negotiate") == 0);
#endif
}

int sspi_is_ntlm(void) {
#ifdef UNICODE
return (wcscmp(sspi_mode, L"NTLM") == 0);
#else
return (strcasecmp(sspi_mode, "NTLM") == 0);
Comment thread sspi.c
Comment on lines +349 to +412
// Step 1: Acquire credentials handle with Negotiate package
status = _AcquireCredentialsHandle(
NULL,
TEXT("Negotiate"),
SECPKG_CRED_OUTBOUND,
NULL, NULL, NULL, NULL,
&state->credentials,
&expiry);

if (status != SEC_E_OK) {
syslog(LOG_ERR, "AcquireCredentialsHandle failed: 0x%08x", status);
return 0;
}

// Build SPN from proxy hostname
char hostname_only[256];
strcpy(hostname_only, proxy_hostname ? proxy_hostname : "");
char *colon = strchr(hostname_only, ':');
if (colon) *colon = '\0';

// Step 2: Initialize security context
output_desc.ulVersion = SECBUFFER_VERSION;
output_desc.cBuffers = 1;
output_desc.pBuffers = &output_buffer;
output_buffer.cbBuffer = TOKEN_BUFSIZE;
output_buffer.BufferType = SECBUFFER_TOKEN;
output_buffer.pvBuffer = zmalloc(TOKEN_BUFSIZE);

#ifdef UNICODE
wchar_t target_name[261]; // 256 + 5 chars
wchar_t whostname[256];
mbstowcs(whostname, hostname_only, 256);
swprintf(target_name, 256, L"HTTP/%s", whostname);
#else
char target_name[261]; // 256 + 5 chars
snprintf(target_name, sizeof(target_name), "HTTP/%s", hostname_only);
#endif

status = _InitializeSecurityContext(
&state->credentials,
NULL,
target_name,
ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION,
0,
SECURITY_NETWORK_DREP,
NULL,
0,
&state->context,
&output_desc,
&context_attr,
&expiry);

syslog(LOG_ERR, "InitializeSecurityContext status: 0x%08x", status);

if (status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED) {
syslog(LOG_ERR, "InitializeSecurityContext failed: 0x%08x", status);
_FreeCredentialsHandle(&state->credentials);
free(output_buffer.pvBuffer);
return 0;
}
*dst = output_buffer.pvBuffer;

return output_buffer.cbBuffer;
}
Comment thread proxy.c
Comment on lines +566 to +578
#ifdef __CYGWIN__
if (sspi_is_negotiate()) {
// Get scheme from SSPI (returns "NTLM" or "Negotiate")
snprintf(buf, bufsize, "%s ", sspi_get_scheme());
int prefix_len = strlen(buf);

pthread_once(&key_once, make_negotiate_key);
struct sspi_negotiate_state *negotiate_state = pthread_getspecific(negotiate_state_key);
if (!negotiate_state) {
negotiate_state = zmalloc(sizeof(struct sspi_negotiate_state));
pthread_setspecific(negotiate_state_key, negotiate_state);
}
len = sspi_negotiate_request(&tmp, negotiate_state, curr_proxy->hostname);
Comment thread proxy.c
Comment on lines +692 to +705
if (sspi_is_negotiate()) {
// Get scheme from SSPI (returns "NTLM" or "Negotiate")
snprintf(buf, bufsize, "%s ", sspi_get_scheme());
int prefix_len = strlen(buf);

pthread_once(&key_once, make_negotiate_key);
struct sspi_negotiate_state *negotiate_state = pthread_getspecific(negotiate_state_key);
if (!negotiate_state) {
negotiate_state = zmalloc(sizeof(struct sspi_negotiate_state));
memset(negotiate_state, 0, sizeof(struct sspi_negotiate_state));
pthread_setspecific(negotiate_state_key, negotiate_state);
}
len = sspi_negotiate_request(&tmp, negotiate_state, curr_proxy->hostname);
if (len) {
Comment thread ntlm.c
@@ -224,7 +224,7 @@ int ntlm_request(char **dst, struct auth_s *creds) {
#ifdef __CYGWIN__
if (sspi_enabled())
Comment thread sspi.c
Comment on lines +387 to +408
status = _InitializeSecurityContext(
&state->credentials,
NULL,
target_name,
ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION,
0,
SECURITY_NETWORK_DREP,
NULL,
0,
&state->context,
&output_desc,
&context_attr,
&expiry);

syslog(LOG_ERR, "InitializeSecurityContext status: 0x%08x", status);

if (status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED) {
syslog(LOG_ERR, "InitializeSecurityContext failed: 0x%08x", status);
_FreeCredentialsHandle(&state->credentials);
free(output_buffer.pvBuffer);
return 0;
}
Comment thread sspi.c
Comment on lines +401 to +402
syslog(LOG_ERR, "InitializeSecurityContext status: 0x%08x", status);

Comment thread proxy.c
Comment on lines +43 to +47

// Helper function to create the key
static void make_negotiate_key(void) {
pthread_key_create(&negotiate_state_key, NULL);
}
Comment thread proxy.c
Comment on lines +692 to +696
if (sspi_is_negotiate()) {
// Get scheme from SSPI (returns "NTLM" or "Negotiate")
snprintf(buf, bufsize, "%s ", sspi_get_scheme());
int prefix_len = strlen(buf);

Comment thread main.c
#endif
#ifdef __CYGWIN__
!sspi_enabled() &&
(!sspi_is_negotiate() && sspi_enabled()) &&
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants