Search code examples
cschannel

InitializeSecurityContextA returns SEC_E_INVALID_HANDLE


I am trying to use SChannel for https request. When I call InitializeSecurityContextA for the second time, I am getting SEC_E_INVALID_HANDLE although I am supplying the same CtxHandle from first call. Here is the complete source code; (don't mind the sloppy coding practices)

#include <windows.h>
#include <winternl.h>
#include <security.h>
#include <schannel.h>
#include <stdio.h>
#include <winsock2.h>
#include <WS2tcpip.h>

// call this function to create credhandle for a client
SECURITY_STATUS create_default_client_cred_handle(PCredHandle handle)
{
    SCH_CREDENTIALS cred;
    ZeroMemory(&cred, sizeof(cred));
    cred.dwVersion = SCH_CREDENTIALS_VERSION;
    return AcquireCredentialsHandle(
        NULL,
        UNISP_NAME,
        SECPKG_CRED_OUTBOUND,
        NULL,
        &cred,
        NULL,
        NULL,
        handle,
        0
    );
}

// Open a socket and connect to a host
SOCKET connect_to_host(TCHAR *hostname, ULONG port)
{
    SOCKET s = INVALID_SOCKET;
    
    // create tcp socket (implied by sock_stream)
    // with ipv6 support
    s = socket(AF_INET6, SOCK_STREAM, 0);
    
    if(s == INVALID_SOCKET)
        return s;
    
    // disable IPV6 ONLY
    DWORD ipv6only = 0;
    int res = setsockopt(s,
    IPPROTO_IPV6,
    IPV6_V6ONLY,
    (char*)&ipv6only,
    sizeof(ipv6only));

    if(res == SOCKET_ERROR)
    {
        closesocket(s);
        return INVALID_SOCKET;
    }

    // connect to host
    BOOL bSuccess = WSAConnectByNameA(
        s,
        "www.google.com",
        "https",
        0,
        NULL,
        0,
        NULL,
        NULL,
        NULL);

    if (!bSuccess){
        closesocket(s);
        return INVALID_SOCKET;
    }

    return s;
}

// Call this function after socket is connected
// to do TLS handshake
void client_socket_create_security_ctx(SOCKET s, SEC_CHAR *hostname, PCredHandle h)
{
#define TLS_BUFFER_SIZE (1<<14)

    ULONG flags = 
          ISC_REQ_ALLOCATE_MEMORY
        | ISC_REQ_CONFIDENTIALITY
        | ISC_REQ_INTEGRITY
        | ISC_REQ_REPLAY_DETECT
        | ISC_REQ_STREAM;

    CtxtHandle ctxHandle = { 0 };

    // setup buffer descriptors
    char buf[TLS_BUFFER_SIZE];

    SecBuffer outbuffer[1] = { 0 };
    SecBufferDesc outdesc = {SECBUFFER_VERSION, ARRAYSIZE(outbuffer), outbuffer};

    SecBuffer inbuffer[2];
    inbuffer[0].BufferType = SECBUFFER_TOKEN;
    inbuffer[0].pvBuffer = buf;
    inbuffer[0].cbBuffer = 0;

    inbuffer[1].BufferType = SECBUFFER_EMPTY;
    inbuffer[1].cbBuffer = 0;
    inbuffer[1].pvBuffer = NULL;

    SecBufferDesc indesc = {SECBUFFER_VERSION, ARRAYSIZE(inbuffer), inbuffer};
    
    // First call
    SECURITY_STATUS sec_status = InitializeSecurityContextA(
        h, // phCredential,
        NULL, // phContext, (must be null on first call)
        hostname, // *pszTargetName,
        flags, // fContextReq,
        0, // Reserved1
        0, // TargetDataRep
        NULL, // pInput,(must be null on first call)
        0, // Reserved2
        &ctxHandle, // phNewContext 
        &outdesc, // pfContextAttr 
        &flags, //
        NULL
    );

    while(sec_status != SEC_E_OK)
    {
        // if there was leftover data from previous call
        // move it to start of buffer
        if(inbuffer[1].BufferType == SECBUFFER_EXTRA)
        {
            printf("Found extra data, moving to start of buffer\r\n");
            MoveMemory(
                buf,
                buf + (inbuffer[0].cbBuffer - inbuffer[1].cbBuffer),
                inbuffer[1].cbBuffer
            );
            inbuffer[0].cbBuffer = inbuffer[1].cbBuffer;
        } else
        {
            inbuffer[0].cbBuffer = 0;
        }

        switch(sec_status)
        {
            // SUCCESS RETURN CODES
            case SEC_I_COMPLETE_AND_CONTINUE:
            case SEC_I_COMPLETE_NEEDED:
            {
                // Although these mean InitializeSecurityContextA
                // returned successfully, in the context of https
                // these return codes are meaningless
                printf("SEC_I_COMPLETE_NEEDED meaningless, returning...\r\n");
            } return;
            case SEC_I_CONTINUE_NEEDED:
            {
                printf("SEC_I_CONTINUE_NEEDED...\r\n");
                // client must send output token
                // and read input token

                printf("Send output token...\r\n");
                // sending client token
                char *buffer = outbuffer[0].pvBuffer;
                DWORD size = outbuffer[0].cbBuffer;
                while(size > 0)
                {
                    int d = send(s, buffer, size, 0);
                    if(d <= 0)
                        break;
                    size -= d;
                    buffer += d;
                }
                FreeContextBuffer(outbuffer[0].pvBuffer);

                printf("read server token...\r\n");
                // read server token
                buffer = buf + inbuffer[0].cbBuffer;
                size = TLS_BUFFER_SIZE - inbuffer[0].cbBuffer;
                inbuffer[0].cbBuffer += recv(s, buffer, size, 0);
            } break;
            case SEC_I_INCOMPLETE_CREDENTIALS:
            {
                printf("Incomplete credentials...\r\n");
                // server requested client certificate
                // it shouldn't for https...
            } return;
            case SEC_E_INCOMPLETE_MESSAGE:
            {
                printf("Incomplete message...\r\n");
                // we didn't get enough data from server
                // we should read more
                char *buffer = buf + inbuffer[0].cbBuffer;
                DWORD size;
                if(inbuffer[1].BufferType == SECBUFFER_MISSING)
                {
                    // we know how much data we need (approx.)
                    size = inbuffer[1].cbBuffer;
                    printf("Attemting to read %lu more bytes...\r\n", size);
                    while(size)
                    {
                        int incoming = recv(s, buffer, size, 0);
                        if(incoming <= 0)
                            break;
                        size -= incoming;
                        buffer += incoming;
                        inbuffer[0].cbBuffer += incoming;
                    }
                } else {
                    // This shouldn't happen, but just in case
                    printf("Reading as much data as we can...\r\n");
                    size = TLS_BUFFER_SIZE - inbuffer[0].cbBuffer;
                    int incoming = recv(s, buffer, size, 0);
                    inbuffer[0].cbBuffer += incoming;
                }
            } break;
            // ERROR RETURN CODES
            default:
            {
                printf("Got error code 0x%X\r\n", sec_status);
                // dont care for individual codes,
                // error is error
            } return;
        }

        // prepare buffers
        ZeroMemory(outbuffer, sizeof(outbuffer));
        ZeroMemory(&inbuffer[1], sizeof(inbuffer[1]));
        printf("Calling InitializeSecurityContextA again..\r\n");
        // call again
        sec_status = InitializeSecurityContextA(
            NULL, // phCredential,
            &ctxHandle, // phContext
            NULL, // *pszTargetName,
            flags, // fContextReq,
            0, // Reserved1
            0, // TargetDataRep
            &indesc, // pInput,(must be null on first call)
            0, // Reserved2
            &ctxHandle, // phNewContext 
            &outdesc, // pOutput 
            &flags, //pfContextAttr
            NULL // ptsExpiry
        );
    }

    printf("Handshake done ...\r\n");
#undef TLS_BUFFER_SIZE
}

int main()
{
    char buffer[512];
    CredHandle h;

    WSADATA wsaData;
    WSAStartup(MAKEWORD(2,2), &wsaData);

    SECURITY_STATUS s = create_default_client_cred_handle(&h);
    if (s != SEC_E_OK)
        return -1;
    
    SOCKET sock = connect_to_host("www.google.com", 443);
    if(sock == INVALID_SOCKET)
        return -1;
        
    client_socket_create_security_ctx(sock, "www.google.com", &h);
    // close socket
    shutdown(sock, SD_SEND);
    // drain rest
    while(recv(sock, buffer, 512, 0) > 0)
        ;
    closesocket(sock);

}

I am using this Cmake file to compile;

cmake_minimum_required (VERSION 3.8)


project ("SChannelTest" LANGUAGES C)

add_executable (SChannelTest  "main.c")
if(WIN32)
    add_definitions(/DSECURITY_WIN32 /DSCHANNEL_USE_BLACKLISTS /D_CONSOLE /DUNICODE /D_UNICODE /DWIN32_LEAN_AND_MEAN /D_CRT_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE /W3 /wd4005 /wd4996 /nologo )
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /MANIFEST:NO")
endif()

target_link_libraries(SChannelTest PRIVATE Secur32 ws2_32)


Solution

  • Although relevant msdn documentatation states that;

    phCredential [in, optional]

    A handle to the credentials returned by AcquireCredentialsHandle (Schannel). This handle is used to build the security context. The InitializeSecurityContext (Schannel) function requires at least OUTBOUND credentials on the first call. On subsequent calls, this can be NULL.

    Supplying NULL for phCredential parameter causes InitializeSecurityContext to fail with SEC_E_INVALID_HANDLE.

    Supplying same CredHandle for subsequent calls fixes the issue.