How to Call Schannel Functions from .Net/C#
Asked Answered
E

1

5

I am trying to re-order/remove cipher suites due to compliance reasons (I want to use 256 bit AES and ephemeral keys) in .Net. However, using WCF TCP Transport Security, I cede all control over the security to Windows' TLS implementation and its preferred ciphers. I don't want to change the system-level ciphers.

I found this great Microsoft page that lists how to do it at the application level using BCryptEnumContextFunctions, BCryptAddContextFunction, BCryptRemoveContextFunction, but it's all C and I don't have enough P/Invoke experience to even know where to begin. I googled but didn't find anyone doing it.

The C++ code snippets on the MSDN page are below, and I need help converting them to .Net P/Invoke calls:

BCryptEnumContextFunctions

#include <stdio.h>
#include <windows.h>
#include <bcrypt.h>


void main()
{

   HRESULT Status = ERROR_SUCCESS;
   DWORD   cbBuffer = 0;
   PCRYPT_CONTEXT_FUNCTIONS pBuffer = NULL;

    Status = BCryptEnumContextFunctions(
        CRYPT_LOCAL,
        L"SSL",
        NCRYPT_SCHANNEL_INTERFACE,
        &cbBuffer,
        &pBuffer);
    if(FAILED(Status))
    {
        printf_s("\n**** Error 0x%x returned by BCryptEnumContextFunctions\n", Status);
        goto Cleanup;
    }

    if(pBuffer == NULL)
    {
        printf_s("\n**** Error pBuffer returned from BCryptEnumContextFunctions is null");
        goto Cleanup;
    }

    printf_s("\n\n Listing Cipher Suites ");
    for(UINT index = 0; index < pBuffer->cFunctions; ++index)
    {
        printf_s("\n%S", pBuffer->rgpszFunctions[index]);
    }

Cleanup:
    if (pBuffer != NULL)
    {
        BCryptFreeBuffer(pBuffer);
    }
}

BCryptAddContextFunction

#include <stdio.h>
#include <windows.h>
#include <bcrypt.h>


void main()
{

    SECURITY_STATUS Status = ERROR_SUCCESS;
    LPWSTR wszCipher = (L"RSA_EXPORT1024_DES_CBC_SHA");

    Status = BCryptAddContextFunction(
                CRYPT_LOCAL,
                L"SSL",
                NCRYPT_SCHANNEL_INTERFACE,
                wszCipher,
                CRYPT_PRIORITY_TOP);
}

BCryptRemoveContextFunction

#include <stdio.h>
#include <windows.h>
#include <bcrypt.h>


void main()
{

    SECURITY_STATUS Status = ERROR_SUCCESS;
      LPWSTR wszCipher = (L"TLS_RSA_WITH_RC4_128_SHA");

    Status = BCryptRemoveContextFunction(
                CRYPT_LOCAL,
                L"SSL",
                NCRYPT_SCHANNEL_INTERFACE,
                wszCipher);
}

Could someone please help me convert these to .Net so I can call them from managed code to adjust the ciphers? Thanks!

Edit:

Later last night, I tried the following in a test program (still have no idea what I'm doing in P/Invoke):

// I found this in bcrypt.h
const uint CRYPT_LOCAL = 0x00000001;
// I can't find this anywhere in Microsoft's headers in my SDK,
// but I found some random .c file with it in there. No idea
// what this constant actually is according to Microsoft
const uint NCRYPT_SCHANNEL_INTERFACE = 0x00010002;

public static void DoStuff()
{
    PCRYPT_CONTEXT_FUNCTIONS pBuffer = new PCRYPT_CONTEXT_FUNCTIONS();
    pBuffer.rgpszFunctions = String.Empty.PadRight(1500);
    uint cbBuffer = (uint)Marshal.SizeOf(typeof(PCRYPT_CONTEXT_FUNCTIONS));

    uint Status = BCryptEnumContextFunctions(
            CRYPT_LOCAL,
            "SSL",
            NCRYPT_SCHANNEL_INTERFACE,
            ref cbBuffer,
            ref pBuffer);

    Console.WriteLine(Status);
    Console.WriteLine(pBuffer);
    Console.WriteLine(cbBuffer);
    Console.WriteLine(pBuffer.cFunctions);
    Console.WriteLine(pBuffer.rgpszFunctions);
}
/*
    typedef struct _CRYPT_CONTEXT_FUNCTIONS {
        ULONG cFunctions;
        PWSTR rgpszFunctions;
    } CRYPT_CONTEXT_FUNCTIONS, *PCRYPT_CONTEXT_FUNCTIONS;
*/

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct PCRYPT_CONTEXT_FUNCTIONS
{
    public uint cFunctions;
    public string rgpszFunctions;
}

/*
    NTSTATUS WINAPI BCryptEnumContextFunctions(
    ULONG dwTable,
    LPCWSTR pszContext,
    ULONG dwInterface,
    ULONG *pcbBuffer,
    PCRYPT_CONTEXT_FUNCTIONS *ppBuffer
    );
*/
[DllImport("Bcrypt.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern uint BCryptEnumContextFunctions(uint dwTable, string pszContext, uint dwInterface, ref uint pcbBuffer, ref PCRYPT_CONTEXT_FUNCTIONS ppBuffer);

The output right now of the only method I got even this far with is:

0
MyClass+PCRYPT_CONTEXT_FUNCTIONS
2400
8934576
[1500 spaces that I initialized rgpszFunctions with, not the cipher functions]
Endocranium answered 30/10, 2013 at 23:15 Comment(2)
Can you show what you've tried so far?Sumner
@DavidHeffernan I really don't know much about P/Invoke other than what I've copied from PInvoke.net, but that said, I did give it a try last night and I added to the question what I tried with the retrieval function. Needless to say, it doesn't work.Endocranium
S
11

This is a poorly documented library. For instance, the declaration of CRYPT_CONTEXT_FUNCTION_PROVIDERS is in fact:

typedef struct _CRYPT_CONTEXT_FUNCTION_PROVIDERS
{
    ULONG cProviders;
    PWSTR *rgpszProviders;
}
CRYPT_CONTEXT_FUNCTION_PROVIDERS, *PCRYPT_CONTEXT_FUNCTION_PROVIDERS;

This is lifted directly from bcrypt.h.

Anyway, here's a translation of the C++ code for you:

using System;
using System.Text;
using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    class Program
    {
        [DllImport("Bcrypt.dll", CharSet = CharSet.Unicode)]
        static extern uint BCryptEnumContextFunctions(uint dwTable, string pszContext, uint dwInterface, ref uint pcbBuffer, ref IntPtr ppBuffer);

        [DllImport("Bcrypt.dll")]
        static extern void BCryptFreeBuffer(IntPtr pvBuffer);

        [DllImport("Bcrypt.dll", CharSet = CharSet.Unicode)]
        static extern uint BCryptAddContextFunction(uint dwTable, string pszContext, uint dwInterface, string pszFunction, uint dwPosition);

        [DllImport("Bcrypt.dll", CharSet = CharSet.Unicode)]
        static extern uint BCryptRemoveContextFunction(uint dwTable, string pszContext, uint dwInterface, string pszFunction);

        [StructLayout(LayoutKind.Sequential)]
        public struct CRYPT_CONTEXT_FUNCTIONS
        {
            public uint cFunctions;
            public IntPtr rgpszFunctions;
        }

        const uint CRYPT_LOCAL = 0x00000001;
        const uint NCRYPT_SCHANNEL_INTERFACE = 0x00010002;
        const uint CRYPT_PRIORITY_TOP = 0x00000000;
        const uint CRYPT_PRIORITY_BOTTOM = 0xFFFFFFFF;

        public static void DoStuff()
        {
            uint cbBuffer = 0;
            IntPtr ppBuffer = IntPtr.Zero;
            uint Status = BCryptEnumContextFunctions(
                    CRYPT_LOCAL,
                    "SSL",
                    NCRYPT_SCHANNEL_INTERFACE,
                    ref cbBuffer,
                    ref ppBuffer);
            if (Status == 0)
            {
                CRYPT_CONTEXT_FUNCTIONS functions = (CRYPT_CONTEXT_FUNCTIONS)Marshal.PtrToStructure(ppBuffer, typeof(CRYPT_CONTEXT_FUNCTIONS));
                Console.WriteLine(functions.cFunctions);
                IntPtr pStr = functions.rgpszFunctions;
                for (int i = 0; i < functions.cFunctions; i++)
                {
                    Console.WriteLine(Marshal.PtrToStringUni(Marshal.ReadIntPtr(pStr)));
                    pStr += IntPtr.Size;
                }
                BCryptFreeBuffer(ppBuffer);
            }
        }

        static void Main(string[] args)
        {
            DoStuff();
            Console.ReadLine();
        }
    }
}

On my machine the output is:

30
TLS_RSA_WITH_AES_128_CBC_SHA256
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA256
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_RC4_128_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA_P256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA_P384
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA_P256
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA_P384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256_P256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256_P256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384_P384
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384_P384
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA_P256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA_P384
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA_P256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA_P384
TLS_DHE_DSS_WITH_AES_128_CBC_SHA256
TLS_DHE_DSS_WITH_AES_128_CBC_SHA
TLS_DHE_DSS_WITH_AES_256_CBC_SHA256
TLS_DHE_DSS_WITH_AES_256_CBC_SHA
TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA
TLS_RSA_WITH_RC4_128_MD5
SSL_CK_RC4_128_WITH_MD5
SSL_CK_DES_192_EDE3_CBC_WITH_MD5
TLS_RSA_WITH_NULL_SHA256
TLS_RSA_WITH_NULL_SHA

I urge you to get to grips with some simple C++ to make progress. Take the example from MSDN and compile it. Run it under the debugger and get to know it. Use Visual Studio to locate definitions and declarations. For instance, Visual Studio took me straight to the definition of NCRYPT_SCHANNEL_INTERFACE.

Sumner answered 31/10, 2013 at 14:39 Comment(8)
I did get it to compile in VS in C++ mode after figuring out how to add bcrypt.lib and I found the struct, but I didn't know how to convert the IntPtr to the address of the struct, since I was getting Status==0 and a non-void struct back. I'm getting an error with the pStr += IntPtr.Size: "Operator '+=' cannot be applied to operands of type 'System.IntPtr' and 'int'". Also, can you convert the other 2 in the question: BCryptAddContextFunction and BCryptRemoveContextFunction? I can probably do those myself since they are similar in signature without the return struct but "for google". :)Endocranium
Also I tried to compile with /clr:safe but I got lots of errors about incompatible switches and I wasn't able to get a calling method that worked with it. /clr worked, but the code it generated, when I looked at it in DotPeek, just havd [NativeCppMethodAttribute] around it and no actual P/Invoke code.Endocranium
I'm sure you can do those two yourself. They are much easier and require no marshalling tricks. The parameters are all either plain string or uint. Charset.Unicode and you are golden. I've no idea about /clr:safe and your second comment.Sumner
Did you see the part about the error (SO won't let me have line breaks in a comment)? pStr += IntPtr.Size won't compile for me. I was able to make it work as pStr = new IntPtr(pStr.ToInt64() + IntPtr.Size);, but I'm not sure if that's the best way to do it.Endocranium
Oh, one more thing. Don't use SetLastError = true. These functions don't set Win32 error.Sumner
OK, you need .net 4 for arithmetic on IntPtr. Your solution is the best workaround for older .net, it's a well known issue. Sorry, I did miss that.Sumner
Anyway, you seem very clued up. I'm sure you will get home from here! Good luck! :-)Sumner
Thanks very much (marked answered). I got the add/remove working, but it appears to do it for the whole system, not just the application, but that's outside the scope of this question (e.g. when I removed the ciphers in one run of the program, they remained removed in the next run).Endocranium

© 2022 - 2024 — McMap. All rights reserved.