3

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

SammuelMiranda
  • 420
  • 4
  • 29
  • am i forgeting anything here? – SammuelMiranda Dec 17 '21 at 15:02
  • 2
    I would advice switching to BouncyCastle library for this kind of work instead of Win32 CryptApi. BouncyCastle is more popular library and there is much more examples in C# or Java https://stackoverflow.com/questions/3770233/is-it-possible-to-programmatically-generate-an-x509-certificate-using-only-c – Zuljin Dec 20 '21 at 15:58
  • i understand, but is too much of a library for just this - i'm writing it from scratch (almost, using just standard .Net Framework library, no packages) - that's the point, what's missing there? – SammuelMiranda Dec 20 '21 at 18:28
  • Instead of P/Invoking down to Win32 CertCreateSelfSignCertificate, why not just use the .NET CertificateRequest API, which is designed for this? https://stackoverflow.com/a/48210587/6535399 – bartonjs Jan 05 '22 at 17:42
  • i'll look into it - this implementation is on an older .Net version, i'll transfer if i can - i'll try it first and see - thanks – SammuelMiranda Jan 10 '22 at 11:24
  • so @bartonjs i run that code from the solution - still not getting the proper result (run as was, changed nothing on the code). Check my question from the update i'll make now if you can – SammuelMiranda Jan 10 '22 at 11:44

2 Answers2

0

I would say aim for these qualities in development certificates:

  • A root certificate authority file, eg myRoot.ca
  • A password protected PKCS12 file (containing a private key + certificate), whose root is the above CA, eg mySslCert p12.
  • The latter can also be a wildcard certificate, eg usable for multiple subdomains under *.mycompany.com, which is useful in terrms of simple administration.

CREATION

Personally I prefer to use OpenSSL to create certs, since this is the technology that secures the internet, and I am then sure that there is nothing technology specific about certs issued.

See my certificates repository and the makeCerts.sh file, for sone OpenSSL commands:

  • Create Root CA keypair
  • Create Root certificate
  • Create SSL keypair
  • Create SSL certificate signing request (which can be for a wildcard certificate)
  • Create SSL certificate
  • Create password protected PKCS12 file

If you want to use C# to create certs, then you need to follow the same 6 steps and produce the same files. Hopefully this makes your requirements clearer.

DEPLOYMENT

In real environments these days, you may end up deploying the Root CA file (mycompany.ca.pem in my example) and the PKCS12 file (mycompany.ssl.p12 in my example).

This is quite common in Private PKI setups within a private network, so it can be very useful to simulate on a Developer PC. My .NET Example API uses the certs issued, though in some cases I use tools such as cert-manager to automate the issuing.

Gary Archer
  • 22,534
  • 2
  • 12
  • 24
  • i'll read into it to check out more, and i see you gave a good and well explained answer, but not yet quite to what i want - you see, i'm not issuing certificates for sites, i will use this certificates internally, for each of my users, just need to be certain that i was the one emitting it - therefore have my self signed certificate (crated from that code) generate child certificates for "userX", Y and Z (or any user name) and deploy - i'd like to not relly on third party libraries for this, since the code, as is, is almost there ... just need to add that signing CA to it (but i get the idea) – SammuelMiranda Dec 28 '21 at 02:00
  • If you want to issue client certs to users, the process would be largely the same except for different certificate extended properties - [this repo](https://github.com/curityio/mutual-tls-api-example/blob/main/1-create-certs.sh) has an example. You'd need to find the C# code for each step. You would then need to deploy the root CA to each user, which may prove difficult to manage. – Gary Archer Dec 28 '21 at 21:43
0

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.

bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • indeed it works - i'll revise the original code to see where i can implement this (if it's possible at all on a older release of .Net Framework) - thanks – SammuelMiranda Jan 11 '22 at 11:49