3

I am trying to generate an X509Certificate2 object using the Microsoft AES Cryptographic Provider:

CALG_AES_256 (0x00006610) 256 bit AES. This algorithm is supported by the Microsoft AES Cryptographic Provider.

My problem is that my call to CryptGenKey(providerContext, 0x6610, 0x4000001, out cryptKey) fails with the following error:

An unhandled exception of type 'System.Runtime.InteropServices.COMException' occurred in mscorlib.dll

Additional information: Invalid flags specified. (Exception from HRESULT: 0x80090009)

The flags I am using are (1024 << 16) | 1). Unless I am mistaken, should this not produce a 1024-bit exportable key according to the documentation on MSDN? What is the problem with my approach here?

My code is as follows:

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

namespace WebSockets
{
    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 CheckReturnValue(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.CheckReturnValue(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, uint providerType, uint 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 = "", DateTime? startTime = null, DateTime? endTime = null)
        {
            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.CheckReturnValue(CryptAcquireContextW(out providerContext, containerName, null, 0x18, 0x8));
                MarshalHelper.CheckReturnValue(CryptGenKey(providerContext, 0x6610, 0x4000001, out cryptKey));
                IntPtr errorStringPtr;
                int nameDataLength = 0;
                byte[] nameData;
                dataHandle = GCHandle.Alloc(name, GCHandleType.Pinned);
                if (!CertStrToNameW(0x10001, 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(0x10001, 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 = (uint)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.CheckReturnValue(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 uint cbData;
            public IntPtr pbData;
        }

        struct CryptAlgorithmIdentifier {
            [MarshalAs(UnmanagedType.LPStr)]
            public String pszObjId;
            public CryptoApiBlob Parameters;
        }

        struct CryptKeyProviderInformation
        {
            [MarshalAs(UnmanagedType.LPWStr)]
            public String pwszContainerName;
            [MarshalAs(UnmanagedType.LPWStr)]
            public String pwszProvName;
            public uint dwProvType;
            public uint dwFlags;
            public uint cProvParam;
            public IntPtr rgProvParam;
            public uint dwKeySpec;
        }
    }
}

To call it, you may use: X509Certificate2Helper.GenerateSelfSignedCertificate("CN = Example");.

Update:

If I use 0x1 for the flags:

CryptAcquireContextW(out providerContext, containerName, "Microsoft Enhanced RSA and AES Cryptographic Provider", 0x18, 0x8);
CryptGenKey(providerContext, 0x6610, 0x1, out cryptKey);

It gets past CryptGenKey, but then fails on CertCreateSelfSignCertificate with:

An unhandled exception of type 'System.Runtime.InteropServices.COMException' occurred in mscorlib.dll

Additional information: Key does not exist. (Exception from HRESULT: 0x8009000D)

Must this key-set be passed in differently? What is wrong with the way I have created it above?

Alexandru
  • 12,264
  • 17
  • 113
  • 208

2 Answers2

2

The problem is with CryptGenKey function call. In the Algid parameter, you should pass either 0x1 (for RSA key exchange) or 0x2 (RSA digital signature). You don't need other values. And key length value should be 0x4000001 (with exportable key). Also, I noticed that you pass incorrect provider type when you instantiate CryptKeyProviderInformation object. Replace this line:

CryptKeyProviderInformation keyProvider = new CryptKeyProviderInformation {
    pwszContainerName = containerName,
    dwProvType = 1,
    dwKeySpec = 1
};

with this line:

CryptKeyProviderInformation keyProvider = new CryptKeyProviderInformation {
    pwszContainerName = containerName,
    dwProvType = 0x18,
    dwKeySpec = 1
};
Crypt32
  • 12,850
  • 2
  • 41
  • 70
  • Why are you suggesting I use RSA for the ALG_ID on CryptGenKey? I would like to use `CALG_AES_256` for the ALG_ID if its possible, which has a maximum of 1024 bits (https://msdn.microsoft.com/en-us/library/windows/desktop/aa375549(v=vs.85).aspx). There is something not right here, because passing the flags `0x8000001` successfully generates a 2048-bit key, but shouldn't this provider only support a maximum of 1024 bits? Also, when I inspect the key after the function call, I see that `KeyExchangeAlgorithm = "RSA-PKCS1-KeyEx"` – Alexandru Jun 12 '15 at 11:16
  • I want to use AES for my encryption and authentication algorithm and RSA for key exchange. – Alexandru Jun 12 '15 at 11:33
  • For the mostpart, the key exchange algorithm is RSA, which is what I want, sorry for the confusion here. I guess you were trying to explain that when you generate a TLS 1.2 certificate you can only create it with RSA for the key exchange and that you don't need anything else? But how do I specify AES for my encryption and authentication? – Alexandru Jun 12 '15 at 11:45
  • I think, you need to learn a bit more about public key cryptography. No offense. Digital certificate must have an asymmetric key generated by using asymmetric algorithm. For example, RSA. And signature is provided by using asymmetric algorithms: sha256RSA. AES is symmetric algorithm and cannot be used for authentication purposes. Encryption is application-specific task and target application is responsible to generate suitable session symmtric key (DES/3DES/AES). – Crypt32 Jun 12 '15 at 12:34
  • "But how do I specify AES for my encryption and authentication?" -- this information is not included in the public certificate. And, as I said, AES cannot be used for authentication. – Crypt32 Jun 12 '15 at 12:43
  • Okay, I get PKI, but this API is extremely poorly documented IMHO, and I don't appreciate that rudeness, so I will respond in kind. You still haven't answered my question. With the code you provided, I can generate a 2048-bit key and yet MSDN says the maximum is 1024 bits. How does that make sense, unless the provider is wrong? Furthermore, explain this: When I browse to https://ca.yahoo.com/?p=us, Chrome tells me that "The connection is encrypted and authenticated using AES_128_GCM and uses RSA as the key exchange mechanism." – Alexandru Jun 12 '15 at 12:44
  • Furthermore, what is then the purpose of allowing people to specify the provider if ultimately their keys will be RSA anyways, no matter what provider they use (Microsoft Enhanced Cryptographic Provider vs. Microsoft AES Cryptographic Provider)? This doesn't make sense to me. Please elaborate and share your knowledge so that people can learn why this API is the way it is. – Alexandru Jun 12 '15 at 12:47
  • 1
    @Alexandru See SSL initialization phase [here](http://iwillgetthatjobatgoogle.tumblr.com/post/19123958486/https-request-encryption-and-ssl-handshake). Certificate is used to safely agree on a symmetric key (i.e. aes) which is used to encrypt the data. That's because asymmetric cryptography has a limit how long could the data to encrypt be and is cpu intensive. As for the differences between CSPs - each can implement different set of algorhitms or support different key types, sizes etc. – pepo Jun 12 '15 at 12:55
  • 1
    I wasn't rude. I'm sorry if I offended you. Regarding key size: it is my fault, I didn't check actual value. Unfortunately, the documentation is outdated. Specified algorithm supports RSA keys up to 16384 bits. About Chrome, initial authentication is performed by using RSA. AES is used to encrypt session traffic and renew keys. Authentication in this context is data authentication, not peer authentication. Each packet is signed with AES, but peers are authenticated by using RSA.. – Crypt32 Jun 12 '15 at 13:01
  • What you guys said makes perfect sense, but *how do I force encryption of session traffic and key renewal to use AES if I'm using this certificate inside of an `SslStream` from a TCP socket with TLS 1.2?* Like @pepo said, the certificate is used to agree on a safe symmetric key because RSA has a limit on the size of data it can encrypt (probably you don't want to use big types here, for anyone interested see my answer here: http://stackoverflow.com/questions/24267332/how-to-encrypt-a-string-using-public-key-cryptography). Is there not a way to set the symmetric algorithm you want to use? – Alexandru Jun 12 '15 at 13:22
  • During SSL handshake, peers exchange their supported ciphersuites with session key algorithm information and they select the most appropriate and supported by both peers. It depends on context and has nothing to do with public key certificate in this question. – Crypt32 Jun 12 '15 at 13:25
  • @CryptoGuy I see. Okay, so, for my purpose, using `0x18` as the provider is pointless because `0x1` will suffice in both the call to `CryptAcquireContextW` and for `CryptKeyProviderInformation`, and I may as well use RSA which is an industry standard for the symmetric algorithm (`0x1` for `CryptGenKey`). I was wrong because all this time I thought the symmetric algorithm was a property of the certificate, but you two helped me to realize it isn't; it is something negotiated by the client and the server. Are there any other parameters I should consider using to make this as secure as possible? – Alexandru Jun 12 '15 at 13:36
  • 1
    One clarification: RSA is Asymmetric algorithm, AES is symmetric. You are right, symmetric algorithm is not a member of the certificate, it is selected by an underlying app. Regarding other parameters, I would increase key length to 2048 at a minimum and put the following extensions: Subject Key Identifier, KeyUsages and Enhanced Key Usages. And keep your private key safe. – Crypt32 Jun 12 '15 at 13:45
  • Typo, I meant "RSA which is an industry standard for the asymmetric* algorithm" – Alexandru Jun 12 '15 at 14:01
  • @CryptoGuy Just out of curiosity, how will Subject Key Identifier, KeyUsages and Enhanced Key Usages increase security? It narrows down the usage of the certificate in case the private certificate is stolen? – Alexandru Jun 26 '15 at 12:45
0

Use this formula

(keySize * 65536) | 1;

For a 2048bit key it is 0x08000001. According to documentation of CryptGenKey method you can use RSA1024BIT_KEY to generate 1024bit key. I've tried looking for the define and found this (although it was on Adobe site :) )

#define RSA1024BIT_KEY                   0x04000000 
pepo
  • 8,644
  • 2
  • 27
  • 42
  • Actually, OP correctly define key size as he is generating a 1024-bit key. And if you look at this page: https://msdn.microsoft.com/en-us/library/windows/desktop/aa386979(v=vs.85).aspx you will notice that specified provider do not support 2048-bit keys, max is 1024. – Crypt32 Jun 12 '15 at 09:06
  • @CryptoGuy Where did I write that he should use 2048bit key? All I wrote was that the formula gives for 2048bit key a value of 0x80000001. I fail to understand what in my answer is so wrong or incorrect or misleading or ... to get me down vote? – pepo Jun 12 '15 at 11:20
  • @CryptoGuy In what way? I think that it would be better to delete it (again) :) – pepo Jun 12 '15 at 12:57
  • I just checked actual values for this provider and found that documentation is outdated. This provider supports keys from 384 up to 16384 bits. I need to be more careful further. – Crypt32 Jun 12 '15 at 13:07