Using CryptUI Library to Sign Byte Array in C#
Asked Answered
E

1

3

I was able to succesfully use the information on this page to digitally sign a file using an x509 certificate (.pfx file) using the following code:

    const Int32 CRYPTUI_WIZ_NO_UI = 1;
    const Int32 CRYPTUI_WIZ_DIGITAL_SIGN_SUBJECT_FILE = 1;
    const Int32 CRYPTUI_WIZ_DIGITAL_SIGN_CERT = 1;
    
    struct CRYPTUI_WIZ_DIGITAL_SIGN_INFO
    {
        public Int32 dwSize;
        public Int32 dwSubjectChoice;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string pwszFileName;
        public Int32 dwSigningCertChoice;
        public IntPtr pSigningCertContext;
        public string pwszTimestampURL;
        public Int32 dwAdditionalCertChoice;
        public IntPtr pSignExtInfo;
    }

    [DllImport("Cryptui.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    static extern bool CryptUIWizDigitalSign(Int32 dwFlags, IntPtr hwndParent, string pwszWizardTitle, ref CRYPTUI_WIZ_DIGITAL_SIGN_INFO pDigitalSignInfo, ref IntPtr ppSignContext);

    public bool SignExecutable(string certificatePath, string applicationPath, string certificatePassword)
    {
        var cert = new X509Certificate2(certificatePath, certificatePassword);
        var pSigningCertContext = cert.Handle;

        var digitalSignInfo = default(CRYPTUI_WIZ_DIGITAL_SIGN_INFO);
        digitalSignInfo = new CRYPTUI_WIZ_DIGITAL_SIGN_INFO()
        {
            dwSize = Marshal.SizeOf(digitalSignInfo),
            dwSubjectChoice = CRYPTUI_WIZ_DIGITAL_SIGN_SUBJECT_FILE,
            pwszFileName = applicationPath,
            dwSigningCertChoice = CRYPTUI_WIZ_DIGITAL_SIGN_CERT,
            pSigningCertContext = pSigningCertContext,
            pwszTimestampURL = null,
            dwAdditionalCertChoice = 0,
            pSignExtInfo = IntPtr.Zero
        };

        var pSignContext = default(IntPtr);
        return CryptUIWizDigitalSign(CRYPTUI_WIZ_NO_UI, IntPtr.Zero, null, ref digitalSignInfo, ref pSignContext));
    }

However, this process requires that I write my file to disk first. What I need to be able to do is digitally sign the content of the file in memory (since it is dynamically generated) before writing it to disk. The CryptUI documentation supposedly supports this, so I altered the code as follows:

    const Int32 CRYPTUI_WIZ_NO_UI = 1;
    const Int32 CRYPTUI_WIZ_DIGITAL_SIGN_SUBJECT_BLOB = 2;
    const Int32 CRYPTUI_WIZ_DIGITAL_SIGN_CERT = 1;

    public struct CRYPTUI_WIZ_DIGITAL_SIGN_BLOB_INFO
    {
        public Int32 dwSize;
        public IntPtr pGuidSubject;
        public Int32 cbBlob;
        public IntPtr pbBlob;
        public string pwszDisplayName;
    }

    struct CRYPTUI_WIZ_DIGITAL_SIGN_INFO
    {
        public Int32 dwSize;
        public Int32 dwSubjectChoice;
        public IntPtr pSignBlobInfo;
        public Int32 dwSigningCertChoice;
        public IntPtr pSigningCertContext;
        public string pwszTimestampURL;
        public Int32 dwAdditionalCertChoice;
        public IntPtr pSignExtInfo;
    }

    struct CRYPTUI_WIZ_DIGITAL_SIGN_CONTEXT
    {
        public Int32 dwSize;
        public Int32 cbBlob;
        public IntPtr pbBlob;
    };

    [DllImport("Cryptui.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    static extern bool CryptUIWizDigitalSign(Int32 dwFlags, IntPtr hwndParent, string pwszWizardTitle, ref CRYPTUI_WIZ_DIGITAL_SIGN_INFO pDigitalSignInfo, ref CRYPTUI_WIZ_DIGITAL_SIGN_CONTEXT ppSignContext);

    public byte[] SignExecutableBLOB(string certificatePath, byte[] applicationContent, string certificatePassword)
    {
        var cert = new X509Certificate2(certificatePath, certificatePassword);
        var pSigningCertContext = cert.Handle;

        int size = Marshal.SizeOf(applicationContent[0]) * applicationContent.Length;
        var blobInfo = default(CRYPTUI_WIZ_DIGITAL_SIGN_BLOB_INFO);
        blobInfo = new CRYPTUI_WIZ_DIGITAL_SIGN_BLOB_INFO()
        {
            dwSize = Marshal.SizeOf(blobInfo),
            pGuidSubject = IntPtr.Zero,
            cbBlob = size,
            pbBlob = Marshal.AllocHGlobal(size),
            pwszDisplayName = null
        };
        Marshal.Copy(applicationContent, 0, blobInfo.pbBlob, size);

        IntPtr pBlobInfo = Marshal.AllocHGlobal(Marshal.SizeOf(blobInfo));
        Marshal.StructureToPtr(blobInfo, pBlobInfo, false);

        var digitalSignInfo = default(CRYPTUI_WIZ_DIGITAL_SIGN_INFO);
        digitalSignInfo = new CRYPTUI_WIZ_DIGITAL_SIGN_INFO()
        {
            dwSize = Marshal.SizeOf(digitalSignInfo),
            dwSubjectChoice = CRYPTUI_WIZ_DIGITAL_SIGN_SUBJECT_BLOB,
            pSignBlobInfo = pBlobInfo,
            dwSigningCertChoice = CRYPTUI_WIZ_DIGITAL_SIGN_CERT,
            pSigningCertContext = pSigningCertContext,
            pwszTimestampURL = null,
            dwAdditionalCertChoice = 0,
            pSignExtInfo = IntPtr.Zero
        };

        try
        {
            var pSignContext = default(CRYPTUI_WIZ_DIGITAL_SIGN_CONTEXT);
            if (!CryptUIWizDigitalSign(CRYPTUI_WIZ_NO_UI, IntPtr.Zero, null, ref digitalSignInfo, ref pSignContext))
                throw new Win32Exception($"An error occurred attempting to digitally sign the content");
            
            var signedApplicationContent = new byte[pSignContext.cbBlob];
            Marshal.Copy(pSignContext.pbBlob, signedApplicationContent, 0, pSignContext.cbBlob);
            return signedApplicationContent;
        }
        finally
        {
            Marshal.FreeHGlobal(pBlobInfo);
            Marshal.FreeHGlobal(blobInfo.pbBlob);
        }
    }

So far, all attempts to run this code result in an error 0x80070057 (Parameter is incorrect). I have tried many small alterations but I just can't seem to find the proper way to get this call to succeed. I am pretty sure it has something to do with improperly marshaling data between managed and unmanaged code, but I have not been able to figure it out. Any help would be appreciated.

Elegize answered 31/8, 2023 at 15:25 Comment(0)
C
7

I stumbled upon this unanswered question a few days ago because I was fighting the same thing. My company (Gibson Research Corporation) sells a commercial software product (SpinRite) which is a hybrid DOS/Windows executable. Each licensed executable is customized for its purchaser, so we need our web server to be able to sign these downloads on the fly. Like the person who posted this question, it would be messy (and wrong) to write the file to the file system only so that it could then be signed, then sent to its user. The right way to do this is to hold the file in RAM, have it signed there, in place, stream that signed file data to its owner, then release the RAM allocation.

The thing that's so aggravating is the astonishing lack of documentation about the use of Windows' crypto API. It's even wrong, since Microsoft's documentation for the critical “pGuidSubject” member of the “CRYPTUI_WIZ_DIGITAL_SIGN_BLOB_INFO” structure describes it as “A pointer to a GUID that contains the GUID that identifies the Session Initiation Protocol (SIP) functions to load.” The problem is, “SIP” in this context does not stand for “Session Initiation Protocol”... it stands for “subject interface package.”

I've never been a member here before today. But I have obtained so much help from those here in the past, that I decided it was time to give back. Here's what's going on:

As we saw in the poster's original question, above, he was able to get signing to work when he gave Windows a filename. The reason it worked is that this allows Windows to examine the file to determine, for itself, how to sign that specific type of file. But if, instead, we want to have Windows sign an amorphous “blob” in RAM, we need to tell Windows what sort of “blob” it is that we want it to sign... and that's where the “pGuidSubject” structure member comes in. :) In that CRYPTUI_WIZ_DIGITAL_SIGN_BLOB_INFO structure, we need to provide a GUID that tells Windows that the blob we're giving it to sign is a Windows executable file.

So, where to we find the GUID for that? Normally such a GUID would be pre-defined in a header file somewhere. But not in this case. In order to make this even more “open-ended”, Microsoft defined a function which can be provided a sample file and it will return the SIP “subject interface package” GUID for that file type. The function is:

CryptSIPRetrieveSubjectGuid

If you give it a sample Windows executable, a null handle and a pointer to an empty GUID to fill-in, it will return:

{C689AAB8-8E78-11D0-8C47-00C04FC295EE}

THAT is the missing magic incantation that's needed to make signing blobs work. Just point the “pGuidSubject” structure memory to that GUID. If you want to have Windows sign other sorts of blobs, just put that in a file, point “CryptSIPRetrieveSubjectGuid” to it, and it will return a GUID that can be used for “in-memory” signing. :)

I hope this will be useful to others who also have this need. It's astonishing to me that we need to fill-in for Microsoft's amazing lack of documentation (or even example code) but that's where we are today. At least this community can make up for its lack.

Cholla answered 15/11, 2023 at 17:29 Comment(2)
Thanks Steve...I've owned a few copies of SpinRite back in the day.Monogamy
Awesome! Thank you so much for finding and posting the fix for this issue! You're awesome!Elegize

© 2022 - 2024 — McMap. All rights reserved.