Search code examples
c#self-signedcapfx

Export Certificate as PFX with proper chain of signing


I read some posts (that don't exist anymore) and came up with the following code that generates a PFX certificate. It works fine to the part of creating this self-signed certificate.

I'm trying to expand this to crate a self-signed certificate and from that one, create it's "childs". I tryed many things but none of then actually export the certificate with it's chain as result.

The current code get's to a point of exporting a PFX with a containing CA and importing it would include both certificates, but not associate then with each other.

It's kind of a long code, but the action should work on the last "Create" funcion of it.

using Sys = global::System;
using SysInterop = global::System.Runtime.InteropServices;
using SysCry509 = global::System.Security.Cryptography.X509Certificates;
using MdPFX = global::PFX;
public static class PFX
{
    #region NativeCode
    private const string DLL_Kernel = "kernel32.dll";
    private const string DLL_AdvApi = "AdvApi32.dll";
    private const string DLL_Crypt = "Crypt32.dll";

    [SysInterop.StructLayout(SysInterop.LayoutKind.Sequential)] private 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;
    }

    [SysInterop.StructLayout(SysInterop.LayoutKind.Sequential)] private struct CryptoApiBlob
    {
        public int DataLength;
        public Sys.IntPtr Data;

        public CryptoApiBlob(int dataLength, Sys.IntPtr data)
        {
            this.DataLength = dataLength;
            this.Data = data;
        }
    }

    [SysInterop.StructLayout(SysInterop.LayoutKind.Sequential)] private struct CryptKeyProviderInformation
    {
        [SysInterop.MarshalAs(SysInterop.UnmanagedType.LPWStr)] public string ContainerName;
        [SysInterop.MarshalAs(SysInterop.UnmanagedType.LPWStr)] public string ProviderName;
        public int ProviderType;
        public int Flags;
        public int ProviderParameterCount;
        public Sys.IntPtr ProviderParameters;
        public int KeySpec;
    }

    [SysInterop.DllImport(MdPFX.DLL_Kernel, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool FileTimeToSystemTime([SysInterop.In] ref long fileTime, out MdPFX.SystemTime systemTime);
    [SysInterop.DllImport(MdPFX.DLL_AdvApi, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CryptAcquireContextW(out Sys.IntPtr providerContext, [SysInterop.MarshalAs(SysInterop.UnmanagedType.LPWStr)] string container, [SysInterop.MarshalAs(SysInterop.UnmanagedType.LPWStr)] string provider, int providerType, int flags);
    [SysInterop.DllImport(MdPFX.DLL_AdvApi, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CryptReleaseContext(Sys.IntPtr providerContext, int flags);
    [SysInterop.DllImport(MdPFX.DLL_AdvApi, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CryptGenKey(Sys.IntPtr providerContext, int algorithmId, int flags, out Sys.IntPtr cryptKeyHandle);
    [SysInterop.DllImport(MdPFX.DLL_AdvApi, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CryptDestroyKey(Sys.IntPtr cryptKeyHandle);
    [SysInterop.DllImport(MdPFX.DLL_Crypt, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CertStrToNameW(int certificateEncodingType, Sys.IntPtr x500, int strType, Sys.IntPtr reserved, [SysInterop.MarshalAs(SysInterop.UnmanagedType.LPArray)] [SysInterop.Out] byte[] encoded, ref int encodedLength, out Sys.IntPtr errorString);
    [SysInterop.DllImport(MdPFX.DLL_Crypt, SetLastError = true, ExactSpelling = true)] private static extern Sys.IntPtr CertCreateSelfSignCertificate(Sys.IntPtr providerHandle, [SysInterop.In] ref MdPFX.CryptoApiBlob subjectIssuerBlob, int flags, [SysInterop.In] ref MdPFX.CryptKeyProviderInformation keyProviderInformation, Sys.IntPtr signatureAlgorithm, [SysInterop.In] ref MdPFX.SystemTime startTime, [SysInterop.In] ref MdPFX.SystemTime endTime, Sys.IntPtr extensions);
    [SysInterop.DllImport(MdPFX.DLL_Crypt, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CertFreeCertificateContext(Sys.IntPtr certificateContext);
    [SysInterop.DllImport(MdPFX.DLL_Crypt, SetLastError = true, ExactSpelling = true)] private static extern Sys.IntPtr CertOpenStore([SysInterop.MarshalAs(SysInterop.UnmanagedType.LPStr)] string storeProvider, int messageAndCertificateEncodingType, Sys.IntPtr cryptProvHandle, int flags, Sys.IntPtr parameters);
    [SysInterop.DllImport(MdPFX.DLL_Crypt, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CertCloseStore(Sys.IntPtr certificateStoreHandle, int flags);
    [SysInterop.DllImport(MdPFX.DLL_Crypt, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CertAddCertificateContextToStore(Sys.IntPtr certificateStoreHandle, Sys.IntPtr certificateContext, int addDisposition, out Sys.IntPtr storeContextPtr);
    [SysInterop.DllImport(MdPFX.DLL_Crypt, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool CertSetCertificateContextProperty(Sys.IntPtr certificateContext, int propertyId, int flags, [SysInterop.In] ref MdPFX.CryptKeyProviderInformation data);
    [SysInterop.DllImport(MdPFX.DLL_Crypt, SetLastError = true, ExactSpelling = true)][return: SysInterop.MarshalAs(SysInterop.UnmanagedType.Bool)] private static extern bool PFXExportCertStoreEx(Sys.IntPtr certificateStoreHandle, ref MdPFX.CryptoApiBlob pfxBlob, Sys.IntPtr password, Sys.IntPtr reserved, int flags);
    private static void Check(bool nativeCallSucceeded) { if (!nativeCallSucceeded) { SysInterop.Marshal.ThrowExceptionForHR(SysInterop.Marshal.GetHRForLastWin32Error()); } }

    private static MdPFX.SystemTime ToSystemTime(Sys.DateTime dateTime)
    {
        long fileTime = dateTime.ToFileTime();
        MdPFX.SystemTime systemTime = default(MdPFX.SystemTime);
        MdPFX.Check(MdPFX.FileTimeToSystemTime(ref fileTime, out systemTime));
        return systemTime;
    }
    #endregion

    public static byte[] Create(string commonName, Sys.DateTime startTime, Sys.DateTime endTime, Sys.Security.SecureString password)
    {
        byte[] pfxData;
        if (commonName == null) { commonName = string.Empty; }
        MdPFX.SystemTime startSystemTime = MdPFX.ToSystemTime(startTime);
        MdPFX.SystemTime endSystemTime = MdPFX.ToSystemTime(endTime);
        string containerName = Sys.Guid.NewGuid().ToString();
        SysInterop.GCHandle dataHandle = default(SysInterop.GCHandle);
        Sys.IntPtr providerContext = Sys.IntPtr.Zero;
        Sys.IntPtr cryptKey = Sys.IntPtr.Zero;
        Sys.IntPtr certContext = Sys.IntPtr.Zero;
        Sys.IntPtr certStore = Sys.IntPtr.Zero;
        Sys.IntPtr storeCertContext = Sys.IntPtr.Zero;
        Sys.IntPtr passwordPtr = Sys.IntPtr.Zero;
        Sys.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
        try
        {
            MdPFX.Check(MdPFX.CryptAcquireContextW(out providerContext, containerName, null, 1, 8));
            MdPFX.Check(MdPFX.CryptGenKey(providerContext, 1, 1, out cryptKey));
            Sys.IntPtr errorStringPtr = Sys.IntPtr.Zero;
            int nameDataLength = 0;
            byte[] nameData = null;
            dataHandle = SysInterop.GCHandle.Alloc(commonName, SysInterop.GCHandleType.Pinned);
            if (!MdPFX.CertStrToNameW(0x00010001, dataHandle.AddrOfPinnedObject(), 3, Sys.IntPtr.Zero, null, ref nameDataLength, out errorStringPtr)) { throw new Sys.ArgumentException(SysInterop.Marshal.PtrToStringUni(errorStringPtr)); }
            nameData = new byte[nameDataLength];
            if (!MdPFX.CertStrToNameW(0x00010001, dataHandle.AddrOfPinnedObject(), 3, Sys.IntPtr.Zero, nameData, ref nameDataLength, out errorStringPtr)) { throw new Sys.ArgumentException(SysInterop.Marshal.PtrToStringUni(errorStringPtr)); }
            dataHandle.Free();
            dataHandle = SysInterop.GCHandle.Alloc(nameData, SysInterop.GCHandleType.Pinned);
            MdPFX.CryptoApiBlob nameBlob = new MdPFX.CryptoApiBlob(nameData.Length, dataHandle.AddrOfPinnedObject());
            MdPFX.CryptKeyProviderInformation kpi = new MdPFX.CryptKeyProviderInformation();
            kpi.ContainerName = containerName;
            kpi.ProviderType = 1;
            kpi.KeySpec = 1;
            certContext = MdPFX.CertCreateSelfSignCertificate(providerContext, ref nameBlob, 0, ref kpi, Sys.IntPtr.Zero, ref startSystemTime, ref endSystemTime, Sys.IntPtr.Zero);
            MdPFX.Check(certContext != Sys.IntPtr.Zero);
            dataHandle.Free();
            certStore = MdPFX.CertOpenStore("Memory", 0, Sys.IntPtr.Zero, 0x2000, Sys.IntPtr.Zero);
            MdPFX.Check(certStore != Sys.IntPtr.Zero);
            MdPFX.Check(MdPFX.CertAddCertificateContextToStore(certStore, certContext, 1, out storeCertContext));
            MdPFX.CertSetCertificateContextProperty(storeCertContext, 2, 0, ref kpi);
            if (password != null) { passwordPtr = SysInterop.Marshal.SecureStringToCoTaskMemUnicode(password); }
            MdPFX.CryptoApiBlob pfxBlob = new MdPFX.CryptoApiBlob();
            MdPFX.Check(MdPFX.PFXExportCertStoreEx(certStore, ref pfxBlob, passwordPtr, Sys.IntPtr.Zero, 7));
            pfxData = new byte[pfxBlob.DataLength];
            dataHandle = SysInterop.GCHandle.Alloc(pfxData, SysInterop.GCHandleType.Pinned);
            pfxBlob.Data = dataHandle.AddrOfPinnedObject();
            MdPFX.Check(MdPFX.PFXExportCertStoreEx(certStore, ref pfxBlob, passwordPtr, Sys.IntPtr.Zero, 7));
            dataHandle.Free();
        }
        finally
        {
            if (passwordPtr != Sys.IntPtr.Zero) { SysInterop.Marshal.ZeroFreeCoTaskMemUnicode(passwordPtr); }
            if (dataHandle.IsAllocated) { dataHandle.Free(); }
            if (certContext != Sys.IntPtr.Zero) { MdPFX.CertFreeCertificateContext(certContext); }
            if (storeCertContext != Sys.IntPtr.Zero) { MdPFX.CertFreeCertificateContext(storeCertContext); }
            if (certStore != Sys.IntPtr.Zero) { MdPFX.CertCloseStore(certStore, 0); }
            if (cryptKey != Sys.IntPtr.Zero) { MdPFX.CryptDestroyKey(cryptKey); }
            if (providerContext != Sys.IntPtr.Zero)
            {
                MdPFX.CryptReleaseContext(providerContext, 0);
                MdPFX.CryptAcquireContextW(out providerContext, containerName, null, 1, 0x10);
            }
        }
        return pfxData;
    }

    public static Sys.Security.SecureString CreateSecurePassword(string insecurePassword)
    {
        if (!string.IsNullOrEmpty(insecurePassword))
        {
            Sys.Security.SecureString password = new Sys.Security.SecureString();
            foreach (char ch in insecurePassword) { password.AppendChar(ch); }
            password.MakeReadOnly();
            return password;
        } else { return null; }
    }

    public static byte[] Create(string commonName, Sys.DateTime startTime, Sys.DateTime endTime, string insecurePassword)
    {
        byte[] pfxData;
        Sys.Security.SecureString password = null;
        try
        {
            password = MdPFX.CreateSecurePassword(insecurePassword);
            pfxData = MdPFX.Create(commonName, startTime, endTime, password);
        } finally { if (password != null) { password.Dispose(); } }
        return pfxData;
    }

    public static byte[] Create(string commonName, int YearsValid, Sys.Security.SecureString password)
    {
        if (!commonName.StartsWith("CN=", Sys.StringComparison.OrdinalIgnoreCase)) { commonName = "CN=" + commonName; }
        return MdPFX.Create(commonName, Sys.DateTime.Now, Sys.DateTime.Now.AddYears(YearsValid), password);
    }

    public static void Create(Sys.IO.Stream save, string commonName, int YearsValid, Sys.Security.SecureString password)
    {
        byte[] certificateData = MdPFX.Create(commonName, 5, password);
        using (Sys.IO.BinaryWriter binWriter = new Sys.IO.BinaryWriter(save))
        {
            binWriter.Write(certificateData);
            binWriter.Flush();
        }
    }

    public static byte[] Create(string commonName, Sys.DateTime startTime, Sys.DateTime endTime) { return MdPFX.Create(commonName, startTime, endTime, (Sys.Security.SecureString)null); }
    public static byte[] Create(string commonName, int YearsValid, string insecurePassword) { using (Sys.Security.SecureString password = MdPFX.CreateSecurePassword(insecurePassword)) { return MdPFX.Create(commonName, YearsValid, password); } }
    public static void Create(Sys.IO.Stream save, string commonName, int YearsValid, string insecurePassword) { using (Sys.Security.SecureString password = MdPFX.CreateSecurePassword(insecurePassword)) { MdPFX.Create(save, commonName, YearsValid, password); } }
    public static void Create(string savePath, string commonName, int YearsValid, Sys.Security.SecureString password) { using (Sys.IO.FileStream fStream = Sys.IO.File.Open(savePath, Sys.IO.FileMode.Create)) { MdPFX.Create(fStream, commonName, YearsValid, password); } }
    public static void Create(string savePath, string commonName, int YearsValid, string insecurePassword) { using (Sys.Security.SecureString password = MdPFX.CreateSecurePassword(insecurePassword)) { using (Sys.IO.FileStream fStream = Sys.IO.File.Open(savePath, Sys.IO.FileMode.Create)) { MdPFX.Create(fStream, commonName, YearsValid, password); } } }

    public static byte[] Create(SysCry509.X509Certificate2 certificate, string insecurePassword, SysCry509.X509Certificate2 signingCert, SysCry509.X509Certificate2Collection chain = null)
    {
        SysCry509.X509Certificate2Collection col = new SysCry509.X509Certificate2Collection(certificate);
        if (chain != null) { col.AddRange(chain); }
        if (signingCert != null)
        {
            SysCry509.X509Certificate2 sigCertNoPK = new SysCry509.X509Certificate2(signingCert.Export(SysCry509.X509ContentType.Cert));
            col.Add(sigCertNoPK);
        }
        return col.Export(SysCry509.X509ContentType.Pfx, insecurePassword);
    }

    public static byte[] Create(string commonName, string insecurePassword, int YearsValid, SysCry509.X509Certificate2 signingCert, SysCry509.X509Certificate2Collection chain = null)
    {
        SysCry509.X509Certificate2 certificate = new SysCry509.X509Certificate2();
        certificate.Import(MdPFX.Create(commonName, YearsValid, insecurePassword), insecurePassword, (SysCry509.X509KeyStorageFlags.PersistKeySet | SysCry509.X509KeyStorageFlags.Exportable));
        return MdPFX.Create(certificate, insecurePassword, signingCert, chain: chain);
    }

    public static SysCry509.X509Certificate2 Load(byte[] rawData, string insecurePassword)
    {
        try
        {
            SysCry509.X509Certificate2 rCert = new SysCry509.X509Certificate2();
            rCert.Import(rawData, insecurePassword, (SysCry509.X509KeyStorageFlags.PersistKeySet | SysCry509.X509KeyStorageFlags.Exportable));
            return rCert;
        } catch { return null; }
    }

    public static byte[] Create(string commonName, string insecurePassword, int YearsValid, bool Signed, SysCry509.X509Certificate2Collection chain = null)
    {
        if (Signed)
        {
            SysCry509.X509Store store = new SysCry509.X509Store(typeof(MdPFX).FullName, SysCry509.StoreLocation.LocalMachine);
            store.Open(SysCry509.OpenFlags.ReadWrite);
            const string rCertN = "A.Root.Cert.Name";
            SysCry509.X509Certificate2 rCert = null;
            if (store.Certificates.Count > 0) { foreach (SysCry509.X509Certificate2 c in store.Certificates) { if (c.SubjectName.Name == rCertN) { rCert = c; break; } } }
            if (rCert == null)
            {
                rCert = MdPFX.Load(MdPFX.Create(rCertN, 10, "A.Root.Cert.Pass"), "A.Root.Cert.Pass");
                store.Add(rCert);
            }
            store.Close();
            return MdPFX.Create(commonName, insecurePassword, YearsValid, rCert, chain: chain);
        } else { return MdPFX.Create(commonName, YearsValid, insecurePassword); }
    }
}

Then, if i run like this, it's not giving the certificate with chain to the "CA" created.

internal static class Program
{
    internal static void Main()
    {
        Sys.IO.File.WriteAllBytes("C:\\Users\\User\\Desktop\\cert.pfx", MdPFX.Create("My.Name", "Pass.123", 10, true, chain: null));
    }
}

UPDATED QUESTION: 2022-01-10

So one thing, @bartonjs recomended me to check this link: https://stackoverflow.com/a/48210587/6535399 I beleave i've seen this before, but still went on it and copied the code (as is) on the soution to a new blank project and ran (adding the "Export" to PFX with password).

File.WriteAllBytes("C:\\signed.pfx", cert.Export(X509ContentType.Pfx, "pwdpwdpwd"));

Then i imported the certificate, and still isn't exact: i'll post the generated certificate (image) and one that is "right" down below - the windows screen is in portuguese (since it's my OS language), but is the "Certificate Path" tab of each certiciate.

The certificate created from the sample code outed this:

Outed Certificate

But should be like this:

Proper form


Solution

  • PFX files do not contain certificate chains, they just contain certificates (which could happen to form a chain).

    cert.Export(X509ContentType.Pfx, "pwdpwdpwd")
    

    exports one certificate as a PFX. It's shorthand for

    X509Certificate2Collection coll = new X509Certificate2Collection();
    coll.Add(cert);
    coll.Export(X509ContentType.Pfx, "pwdpwdpwd");
    

    Given that, the way to export two certificates into one PFX should be apparent:

    X509Certificate2Collection coll = new X509Certificate2Collection();
    coll.Add(issuer);
    coll.Add(cert);
    coll.Export(X509ContentType.Pfx, "pwdpwdpwd");
    

    If you're exporting it as "a chain", then you probably do not want the issuer's private key to be present, so if you don't already have a public-only version of that cert, use new X509Certificate2(issuer.RawData) to make one.