2

I'm not the most familiar with the unmanaged cryptography library in the Windows API, but alas I am trying to generate a self-signed X509Certificate2 certificate.

Here is the complete code:

using System;
using System.Security;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;

namespace Example
{

    [StructLayout(LayoutKind.Sequential)]
    public struct SystemTime
    {
        public short Year;
        public short Month;
        public short DayOfWeek;
        public short Day;
        public short Hour;
        public short Minute;
        public short Second;
        public short Milliseconds;
    }

    public static class MarshalHelper
    {
        public static void ErrorCheck(bool nativeCallSucceeded)
        {
            if (!nativeCallSucceeded)
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        }
    }

    public static class DateTimeExtensions
    {

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool FileTimeToSystemTime(ref long fileTime, out SystemTime systemTime);

        public static SystemTime ToSystemTime(this DateTime dateTime)
        {
            long fileTime = dateTime.ToFileTime();
            SystemTime systemTime;
            MarshalHelper.ErrorCheck(FileTimeToSystemTime(ref fileTime, out systemTime));
            return systemTime;
        }
    }

    class X509Certificate2Helper
    {

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        static extern bool CryptAcquireContextW(out IntPtr providerContext, string container, string provider, int providerType, int flags);

        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool CryptReleaseContext(IntPtr providerContext, int flags);

        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool CryptGenKey(IntPtr providerContext, int algorithmId, int flags, out IntPtr cryptKeyHandle);

        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool CryptDestroyKey(IntPtr cryptKeyHandle);

        [DllImport("crypt32.dll", SetLastError = true)]
        static extern bool CertStrToNameW(int certificateEncodingType, IntPtr x500, int strType, IntPtr reserved, byte[] encoded, ref int encodedLength, out IntPtr errorString);

        [DllImport("crypt32.dll", SetLastError = true)]
        static extern IntPtr CertCreateSelfSignCertificate(IntPtr providerHandle, ref CryptoApiBlob subjectIssuerBlob, int flags, ref CryptKeyProviderInformation keyProviderInformation, IntPtr signatureAlgorithm, ref SystemTime startTime, ref SystemTime endTime, IntPtr extensions);

        [DllImport("crypt32.dll", SetLastError = true)]
        static extern bool CertFreeCertificateContext(IntPtr certificateContext);

        [DllImport("crypt32.dll", SetLastError = true)]
        static extern bool CertSetCertificateContextProperty(IntPtr certificateContext, int propertyId, int flags, ref CryptKeyProviderInformation data);

        public static X509Certificate2 GenerateSelfSignedCertificate(String name = "CN = Example", DateTime? startTime = null, DateTime? endTime = null)
        {
            if (name == null)
                name = String.Empty;
            var startSystemTime = default(SystemTime);
            if (startTime == null || (DateTime)startTime < DateTime.FromFileTimeUtc(0))
                startTime = DateTime.FromFileTimeUtc(0);
            var startSystemTime = ((DateTime)startTime).ToSystemTime();
            if (endTime == null)
                endTime = DateTime.MaxValue;
            var endSystemTime = ((DateTime)endTime).ToSystemTime();
            string containerName = Guid.NewGuid().ToString();
            GCHandle dataHandle = new GCHandle();
            IntPtr providerContext = IntPtr.Zero;
            IntPtr cryptKey = IntPtr.Zero;
            IntPtr certificateContext = IntPtr.Zero;
            IntPtr algorithmPointer = IntPtr.Zero;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                MarshalHelper.ErrorCheck(CryptAcquireContextW(out providerContext, containerName, null, 1, 0x8));
                MarshalHelper.ErrorCheck(CryptGenKey(providerContext, 1, 0x8000001, out cryptKey));
                IntPtr errorStringPtr;
                int nameDataLength = 0;
                byte[] nameData;
                dataHandle = GCHandle.Alloc(name, GCHandleType.Pinned);
                if (!CertStrToNameW(0x00010001, dataHandle.AddrOfPinnedObject(), 3, IntPtr.Zero, null, ref nameDataLength, out errorStringPtr))
                {
                    string error = Marshal.PtrToStringUni(errorStringPtr);
                    throw new ArgumentException(error);
                }
                nameData = new byte[nameDataLength];
                if (!CertStrToNameW(0x00010001, dataHandle.AddrOfPinnedObject(), 3, IntPtr.Zero, nameData, ref nameDataLength, out errorStringPtr))
                {
                    string error = Marshal.PtrToStringUni(errorStringPtr);
                    throw new ArgumentException(error);
                }
                dataHandle.Free();
                dataHandle = GCHandle.Alloc(nameData, GCHandleType.Pinned);
                CryptoApiBlob nameBlob = new CryptoApiBlob { cbData = nameData.Length, pbData = dataHandle.AddrOfPinnedObject() };
                dataHandle.Free();
                CryptKeyProviderInformation keyProvider = new CryptKeyProviderInformation { pwszContainerName = containerName, dwProvType = 1, dwKeySpec = 1 };
                CryptAlgorithmIdentifier algorithm = new CryptAlgorithmIdentifier { pszObjId = "1.2.840.113549.1.1.13", Parameters = new CryptoApiBlob() };
                algorithmPointer = Marshal.AllocHGlobal(Marshal.SizeOf(algorithm));
                Marshal.StructureToPtr(algorithm, algorithmPointer, false);
                certificateContext = CertCreateSelfSignCertificate(providerContext, ref nameBlob, 0, ref keyProvider, algorithmPointer, ref startSystemTime, ref endSystemTime, IntPtr.Zero);
                MarshalHelper.ErrorCheck(certificateContext != IntPtr.Zero);
                return new X509Certificate2(certificateContext);
            }
            finally
            {
                if (dataHandle.IsAllocated)
                    dataHandle.Free();
                if (certificateContext != IntPtr.Zero)
                    CertFreeCertificateContext(certificateContext);
                if (cryptKey != IntPtr.Zero)
                    CryptDestroyKey(cryptKey);
                if (providerContext != IntPtr.Zero)
                    CryptReleaseContext(providerContext, 0);
                if (algorithmPointer != IntPtr.Zero)
                {
                    Marshal.DestroyStructure(algorithmPointer, typeof(CryptAlgorithmIdentifier));
                    Marshal.FreeHGlobal(algorithmPointer);
                }
            }
        }

        struct CryptoApiBlob
        {
            public Int32 cbData;
            public IntPtr pbData;
        }

        struct CryptAlgorithmIdentifier {
            public String pszObjId;
            public CryptoApiBlob Parameters;
        }

        struct CryptKeyProviderInformation
        {
            public String pwszContainerName;
            public String pwszProvName;
            public Int32 dwProvType;
            public Int32 dwFlags;
            public Int32 cProvParam;
            public IntPtr rgProvParam;
            public Int32 dwKeySpec;
        }
    }
}

Here is how you can generate a new X509Certificate2 using it:

var certificate = X509Certificate2Helper.GenerateSelfSignedCertificate();

However, you can see that trying to get the private key through certificate.PrivateKey throws Keyset does not exist. I've tried to consult the documentation but I couldn't figure out why the certificate context doesn't have its private key set when its loaded as an X509Certificate2. Does anyone have any ideas? Are there problems with the implementation that cause the key to not be set? I mean, I'm a little bit confused here because I would expect a self-signed certificate to always carry its private key since its signed itself using it, or is this not the case?

Alexandru
  • 12,264
  • 17
  • 113
  • 208

1 Answers1

1

The problem is with CryptKeyProviderInformation structure signature. It is missing CharSet (with either, Auto or Unicode) attribute, because container and provider names are expected to be unicode (after marshalling). Update structure definition as follows:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct CryptKeyProviderInformation
{
    public String pwszContainerName;
    public String pwszProvName;
    public Int32 dwProvType;
    public Int32 dwFlags;
    public Int32 cProvParam;
    public IntPtr rgProvParam;
    public Int32 dwKeySpec;
}

and the key should be accessbile after that.

Crypt32
  • 12,850
  • 2
  • 41
  • 70
  • Thank you, I can't believe I missed that. That would have been very tricky for me to figure out. Just out of curiosity, if this happens in the future, do you happen to know what the best way is to find the problem with these types of marshaling issues? – Alexandru Jun 11 '15 at 11:25
  • it is impossible without having native sources. However, there might be indications in parameter names and/or documentation. Since CryptoAPI uses hungarian notation in their names, it does provide some clue. For example, LPSTR type is ANSI string, while LPWSTR (note 'W' character) means Unicode. And parameter names contain this information: "psz" or "pwsz". First is ANSI, second one is Unicode. And documentation (in this example) says that it expects Unicode string. HTH – Crypt32 Jun 11 '15 at 11:57
  • sometimes, structures may have conflictring string types. One field is ANSI and another is Unicode. You can solve this by attributing fields undividually: `[MarshalAs(UnmanagedType.LPStr]` for ANSI and `[MarshalAs(UnmanagedType.LPWStr)]` for Unicode strings, – Crypt32 Jun 11 '15 at 12:02
  • I mean to ask, besides checking the presence of a return value, is there any other way to detect when marshaling fails to marshal a value back? – Alexandru Jun 11 '15 at 12:21
  • 1
    It didn't fail actually, the question is about input data and marshalling rules. If the function do not strictly check input data, then there is no universal way to determine issue source. – Crypt32 Jun 11 '15 at 13:03
  • Thanks Vadims, you've helped me out significantly today and yesterday. True, marshaling didn't fail, I think that "fail" was a bad word to choose, since marshaling did its job, and did it well by following its rules and not marshaling a wide string back to an ANSI container. I guess a better expression would have been, "Can you detect a character set mismatch when marshaling without checking return values of a type?", but to that question you have already provider an answer. Thanks again! – Alexandru Jun 11 '15 at 13:41
  • With WinAPI you never can be sure and should always do double checks before you make your code stable. Again, the answer is "no". – Crypt32 Jun 11 '15 at 14:43