Search code examples
cominteropcryptographycom-interoppublic-key-encryption

Making a strongly named COM interop assembly with a PFX file


I am trying to create a strongly named COM interop assembly with a PFX file.
TlbImp.exe is perfectly happy with an SNK file created using "sn.exe -k" (which contains both public and private keys), but the problem is that I have a PFX file...
I can export the public key from PFX to SNK using sn.exe -p, but it only exports the public key, which TlbImp.exe does not like at all. Can I export both public and private keys?

I tried to install the PFX file using
sn.exe -i MyCompany.pfx xyz
and then import the type library using
TlbImp.exe /keycontainer:xyz ...",
but that gives me
TlbImp : error TI0000 : Invalid strong name parameters specified.

What can I do?
Thank you!

UPDATE: Oleg's answer and his utility below work perfectly. The PFX file (after it is re-exported) can be successfully used to extract the public key (sn.exe -p) to be used it with tlbimp.exe (tlbimp.exe  /publickey:xyz.pub). The PFX file can then be used to re-sign the interop dll (sn.exe -R)

Comodo really did drop the ball on this one. Below is the dump (certutil.exe -dump -v xyz.pfx) of the original and fixed PFX files:

 

Before:

CERT_KEY_PROV_INFO_PROP_ID(2):
Key Container = {36BDD7BD-F295-47B2-B9E7-C25BD5B4313E}
Unique container name: bf63afd9ba3fb912ccd3423c6486e5fc_25e0623f-f712-49e2-abda-f31f014b5dae
Provider = Microsoft Enhanced Cryptographic Provider v1.0
ProviderType = 1
Flags = 0
KeySpec = 1 -- AT_KEYEXCHANGE
...
Private Key:
PRIVATEKEYBLOB
Version: 2
aiKeyAlg: 0xa400
CALG_RSA_KEYX
Algorithm Class: 0xa000(5) ALG_CLASS_KEY_EXCHANGE
Algorithm Type: 0x400(2) ALG_TYPE_RSA
Algorithm Sub-id: 0x0(0) ALG_SID_RSA_ANY
 

After:

CERT_KEY_PROV_INFO_PROP_ID(2):
Key Container = {DBA6454E-F6D2-4F0B-AB1B-9E4F7C0E139C}
Unique container name: d2d09f87081c1af7c4225889f1af2250_25e0623f-f712-49e2-abda-f31f014b5dae
Provider = Microsoft Enhanced Cryptographic Provider v1.0
ProviderType = 1
Flags = 0
KeySpec = 2 -- AT_SIGNATURE
...
Private Key:
PRIVATEKEYBLOB
Version: 2
aiKeyAlg: 0x2400
CALG_RSA_SIGN
Algorithm Class: 0x2000(1) ALG_CLASS_SIGNATURE
Algorithm Type: 0x400(2) ALG_TYPE_RSA
Algorithm Sub-id: 0x0(0) ALG_SID_RSA_ANY


Solution

  • Try to do the following

    sn.exe -p MyCompany.pfx MyCompany.pub
    tlbimp.exe My.dll /delaysign /publickey:MyCompany.pub /out:Interop.My.dll
    sn.exe -R Interop.My.dll MyCompany.pfx
    

    If you create primary interop assembly from tlb file of a COM DLL you will tlbimp.exe in the form

    tlbimp.exe My.tlb /primary /delaysign /publickey:MyCompany.pub /out:Interop.My.dll
    

    You can additionally use code signing of the interop assembly:

    signtool.exe sign /f MyCompany.pfx /p password /t http://timestamp.verisign.com/scripts/timstamp.dll /v Interop.My.dll
    

    UPDATED: I found the reason of your problem which you has. First of all I should explain (or remind) about one feature of the key container.

    The private key exist in the key container. The key container can be on the local hard disk in some file from the folder %APPDATA%\Microsoft\Crypto\RSA\YourUserSid. In the same way the key container can be the part of PFX file. In both cases one key container can save up to two private keys: one for digital signature (AT_SIGNATURE) and another for key exchange (AT_KEYEXCHANGE). You can look through parameters of CryptGenKey function for more details. Both keys can be absolutely different. For example you can hold in the digital signature of one key container private key of the size 4096 and in the key exchange part of the same container another private key of the size 2048.

    The certificate has CERT_KEY_PROV_INFO_PROP_ID as one from its certificate extended property. It save the CRYPT_KEY_PROV_INFO structure which dwKeySpec (with the value AT_SIGNATURE, AT_KEYEXCHANGE or some other like AT_KEYEXCHANGE | AT_SIGNATURE) field are used together with other fields pwszContainerName (le-fe9728d2-af26-4f15-9be0-48c5af6f21dc for example), pwszProvName ("Microsoft Enhanced Cryptographic Provider v1.0" for example), dwProvType (PROV_RSA_FULL for example). So one has full reference to the key from the certificate.

    The problem is that the certificate which you have has the key saved in the key exchange part of the container and not in the digital signature part. It make no problem in case of usage of SignTool.exe tool, but in case of the usage tlbimp.exe you get the error

    TlbImp : error TI1000 : The type library importer encountered an unexpected exception: System.Security.SecurityException - Invalid assembly public key. (Exception from HRESULT: 0x8013141E)

    I suppose that it's common problem of Comodo or probably Comodo certificates which one get from through Tucows.

    CN = UTN-USERFirst-Object
    OU = http://www.usertrust.com
    O = The USERTRUST Network
    L = Salt Lake City
    S = UT
    C = US
    

    To reproduce the problem which I describe you can do the following steps:

    1) Create key pair, save it in the key exchange of the new key container and create self-signed certificate which you the key pair. We can use for example the following parameter of MakeCert.exe to do this:

    MakeCert.exe -pe -ss MY -a sha1 -cy authority -len 2048 -e 12/31/2020 -r -n "CN=My Company Root Authority,O=My Company,C=DE" -eku 1.3.6.1.5.5.7.3.3 -sky exchange -len 2048 -sk myKeyContainer

    In the above example we save the new key in the key container with explicit specified name 'myKeyContainer' to make easier to find and examine it in the future. In practice one don't specify the parameter and the container name will be generated automatically based on new GUID.

    2) Use "Certificates" Snap-In to export from MMC.EXE the new created certificate as PFX file. You have to choose a password during the export. Alternatively you can use CertUtil.exe to do the same.

    CertUtil.exe -privatekey -user -exportpfx -p yourPassword "My Company Root Authority" MyCompany.pfx

    3) Export public part of the key in the SNK format. I prefer to use .PUB extension instead of .SNK to distinguish it more clear from the .SNK file holding the key pair. One can use sn.exe with -p parameter to do this:

    sn.exe -p MyCompany.pfx MyCompany.pub

    4) Now one can use tlbimp.exe to reproduce the error.

    tlbimp.exe My.tlb /machine:X86 /primary /delaysign /publickey:MyCompany.pub /out:Interop.My.dll

    If you would not use the switch -sky exchange at the first step the usage of tlbimp.exe will be successfully and you could use in the next step

    sn.exe -R Interop.My.dll MyCompany.pfx

    to sign the Interop.My.dll. After that you can use code signing to dll additionally

    signtool.exe sign /f MyCompany.pfx /p yourPassword /t http://timestamp.verisign.com/scripts/timstamp.dll /v Interop.My.dll

    Sorry for the long text, but I want be sure that people who will have the same problem will be able to understand it.

    To solve the problem you need use any tool which can export and import private key. You should export the key from the key exchange part of the container and import it in the same container in the digital signature part. If you would have problem to find the corresponding tool I can try help. I don't know existing tools very good, but I know many ways how to write program which do this. In case of .NET I would use ExportCspBlob to export the key and use ImportCspBlob to import it back. One should change only one bit in the key blob to use AT_SIGNATURE instead of AT_KEYEXCHANGE. If you open the key container you should use KeyNumber.Exchange once as CspParameters.KeyNumber parameter and KeyNumber.Signature in the next step.

    UPDATED 2: I found here description which corresponds my assumptions. One workaround which are suggested is to set REG_DWORD value with the name KeySpec and the value 2 (AT_SIGNATURE) in the key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\StrongName. I tested this, but it's not help for the usage of tlbimp.exe.

    So I just wrote the utility which do all the work. You should import your PFX file and then start my utility ChangeKeyProvInfo.exe with the certificate Issue name. For example

    ChangeKeyProvInfo.exe "Dmitry Streblechenko"
    

    After that I add the digital signature part in the key container with the same private key as has the key exchange part. Then I change the property of the certificate to use the digital signature part of the key container.

    You can download the utility here and here the source code.

    To make easier to find the code I post it also below. You should see the code only as the quick and dirty solution which I wrote today, so the code is not perfect.

    #define STRICT
    
    #include <windows.h>
    #include <wincrypt.h>
    #include <tchar.h>
    
    #pragma comment (lib, "Crypt32.lib")
    
    int _tmain(int argc, LPCTSTR *argv)
    {
        HCERTSTORE hStore = NULL;
        PCCERT_CONTEXT pCertContext = NULL;
        LPCTSTR pszCertSubjectName = NULL;
        BOOL bSuccess, bResult = FALSE;
        DWORD cbData = 0, dwKeySpec = 0;
        HCRYPTKEY hKey = 0, hKeyNew = 0;
        BOOL fCallerFreeProvOrNCryptKey;
        PBYTE pPrivateKeyBlob = NULL;
        PUBLICKEYSTRUC *pPublicKey = NULL;
        HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hCryptProvOrNCryptKey = 0;
        PCRYPT_KEY_PROV_INFO pKeyProvInfo = NULL;
    
        if (argc != 2) {
            _tprintf(TEXT("USAGE:\r\n\tChangeKeyProvInfo.exe CertSubjectName\n"));
            return 1;
        }
        pszCertSubjectName = argv[1];
    
        __try {
            hStore = CertOpenStore (CERT_STORE_PROV_SYSTEM, 
                X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                (HCRYPTPROV_LEGACY)0,
                CERT_STORE_ENUM_ARCHIVED_FLAG | CERT_STORE_OPEN_EXISTING_FLAG | CERT_SYSTEM_STORE_CURRENT_USER,
                TEXT("MY"));
            if (hStore == NULL) {
                _tprintf(TEXT("Error in CertOpenStore(): %d"), GetLastError());
                __leave;
            }
    
            pCertContext = CertFindCertificateInStore (hStore,
                X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                0,
                CERT_FIND_SUBJECT_STR,
                pszCertSubjectName,
                NULL);
            if (pCertContext == NULL) {
                _tprintf(TEXT("Error in CertFindCertificateInStore(): %d"), GetLastError());
                __leave;
            }
    
            bSuccess = CryptAcquireCertificatePrivateKey(pCertContext,
                CRYPT_ACQUIRE_COMPARE_KEY_FLAG | CRYPT_ACQUIRE_NO_HEALING,
                NULL,
                &hCryptProvOrNCryptKey,
                &dwKeySpec,
                &fCallerFreeProvOrNCryptKey);
            if (!bSuccess) {
                _tprintf(TEXT("Error in CryptAcquireCertificatePrivateKey(): %d"), GetLastError());
                __leave;
            }
    
            bSuccess = CryptGetUserKey (hCryptProvOrNCryptKey, dwKeySpec, &hKey);
            if (!bSuccess) {
                _tprintf(TEXT("Error in CryptGetUserKey(): %d"), GetLastError());
                __leave;
            }
    
            // get privale key as clear text data in form PRIVATEKEYBLOB
            cbData = 0;
            bSuccess = CryptExportKey (hKey, 0, PRIVATEKEYBLOB, 0, NULL, &cbData);
            if (!bSuccess) {
                _tprintf(TEXT("Error in CryptExportKey(): %d"), GetLastError());
                __leave;
            }
    
            pPrivateKeyBlob = (PBYTE)LocalAlloc(LPTR, cbData);
            if (pPrivateKeyBlob == NULL) {
                _tprintf(TEXT("Error in LocalAlloc(): %d"), GetLastError());
                __leave;
            }
    
            bSuccess = CryptExportKey (hKey, 0, PRIVATEKEYBLOB, 0, pPrivateKeyBlob, &cbData);
            if (!bSuccess) {
                _tprintf(TEXT("Error in CryptExportKey(): %d"), GetLastError());
                __leave;
            }
    
            // the PRIVATEKEYBLOB started with the PUBLICKEYSTRUC which contains the KeyAlg
            // CALG_RSA_KEYX are used for AT_KEYEXCHANGE
            // CALG_RSA_SIGN are used for AT_SIGNATURE
            pPublicKey = (PUBLICKEYSTRUC *)pPrivateKeyBlob;
            if (pPublicKey->aiKeyAlg == CALG_RSA_SIGN) {
                _tprintf(TEXT("Currently AT_SIGNATURE are used - nothing to do.\n"));
                __leave;
            } else if (pPublicKey->aiKeyAlg != CALG_RSA_KEYX) {
                _tprintf(TEXT("ERROR: Unknown algorithm 0x%X are used\n"), pPublicKey->aiKeyAlg);
                __leave;
            }
    
            // !!!!!! the next line in the most important !!!!!!
            pPublicKey->aiKeyAlg = CALG_RSA_SIGN; 
            bSuccess = CryptImportKey (hCryptProvOrNCryptKey, pPrivateKeyBlob, cbData, (HCRYPTKEY)NULL, CRYPT_EXPORTABLE, &hKeyNew);
            if (!bSuccess) {
                _tprintf(TEXT("Error in CertGetCertificateContextProperty(): %d"), GetLastError());
                __leave;
            }
            _tprintf(TEXT("The key container has successfully imported the same key for the AT_SIGNATURE usage.\n"));
    
            bSuccess = CertGetCertificateContextProperty (pCertContext, CERT_KEY_PROV_INFO_PROP_ID, 0, &cbData);
            if (!bSuccess) {
                _tprintf(TEXT("Error in CertGetCertificateContextProperty(): %d"), GetLastError());
                __leave;
            }
            pKeyProvInfo = (PCRYPT_KEY_PROV_INFO)LocalAlloc (LPTR, cbData);
            if (pKeyProvInfo == NULL) {
                _tprintf(TEXT("Error in LocalAlloc(): %d"), GetLastError());
                __leave;
            }
    
            bSuccess = CertGetCertificateContextProperty (pCertContext, CERT_KEY_PROV_INFO_PROP_ID, pKeyProvInfo, &cbData);
            if (!bSuccess) {
                _tprintf(TEXT("Error in CertGetCertificateContextProperty(): %d"), GetLastError());
                __leave;
            }
            // !!!!!! the next line in the most important !!!!!!
            pKeyProvInfo->dwKeySpec = AT_SIGNATURE;
            bSuccess = CertSetCertificateContextProperty (pCertContext, CERT_KEY_PROV_INFO_PROP_ID, CERT_SET_PROPERTY_IGNORE_PERSIST_ERROR_FLAG, pKeyProvInfo);
            if (!bSuccess) {
                _tprintf(TEXT("Error in CertSetCertificateContextProperty(): %d"), GetLastError());
                __leave;
            }
            _tprintf(TEXT("The certificale property was successfully changed to use the key with AT_SIGNATURE.\n"));
            bResult = TRUE;
        }
        __finally {
            if (pPrivateKeyBlob)
                pPrivateKeyBlob = (PBYTE) LocalFree (pPrivateKeyBlob);
    
            if (pKeyProvInfo)
                pKeyProvInfo = (PCRYPT_KEY_PROV_INFO) LocalFree (pKeyProvInfo);
    
            if (pCertContext)
                bSuccess = CertFreeCertificateContext (pCertContext);
    
            if (hKeyNew)
                bSuccess = CryptDestroyKey (hKeyNew);
    
            if (hCryptProvOrNCryptKey)
                bSuccess = CryptReleaseContext (hCryptProvOrNCryptKey, 0);
    
            if (hStore)
                bSuccess = CertCloseStore (hStore, CERT_CLOSE_STORE_CHECK_FLAG);
        }
    
        return bResult? 0: 1;
    }
    

    UPDATED 2: Here you can download full protocol of an experiment which everybody can repeat to reproduce the problem how I understand if. The first certificate saved in the My.pfx can be analysed with respect of certutil.exe -dump -v My.pfx. The full output you can see here. The most important part of the output is:

    ...
      CERT_KEY_PROV_INFO_PROP_ID(2):
        ...
        KeySpec = 1 -- AT_KEYEXCHANGE
    
    ...
    Private Key:
      PRIVATEKEYBLOB
      Version: 2
      aiKeyAlg: 0xa400
        CALG_RSA_KEYX
        Algorithm Class: 0xa000(5) ALG_CLASS_KEY_EXCHANGE
    ...
    Encryption test passed
    

    The modified certificates will be saved in My1.pfx. The output of the certutil.exe -dump -v My.pfx shows that the problem is fixed:

    ...
      CERT_KEY_PROV_INFO_PROP_ID(2):
        ...
        KeySpec = 2 -- AT_SIGNATURE
    
    ...
    Private Key:
      PRIVATEKEYBLOB
      Version: 2
      aiKeyAlg: 0x2400
        CALG_RSA_SIGN
        Algorithm Class: 0x2000(1) ALG_CLASS_SIGNATURE
    ...
    Signature test passed
    

    UPDATE 3: After 3 years, the problem with Comodo signatures is still present. Below is their response. Next time my certificate needs to be renewed, I will go with somebody else.


    Our Code Signing certificates are intended to be used for Microsoft Authenticode signatures (and other object-signing related items; e.g. jar signing) and are not quite intended for Microsoft Strong Name signing for assemblies. If Microsoft's application won't use a keypair with the keySpec set to their suggested default (1, AT_KEYEXCHANGE; which allows both signing and encryption) and only works with AT_SIGNATURE (only allows signing), then it would be to be a processing problem with Microsoft's utility and you would need to open a case with Microsoft to correct the issue. We have hundreds of thousands of customers that are able to apply Microsoft Authenticode & other object related signatures without issue with this keySpec set as-is. We are unwilling to modify a process that is working flawlessly for virtually everyone aside from a handful of individuals who wish to go the extreme and use a publicly trusted CA to strongName sign their assemblies. Most developers that we encounter do not use a CA signed certificate for Strong Name signing assemblies, but rather do so with a self-signed certificate due to maximum term limits that public CAs impose on all certs (no more than 3-5 years max.)

    Based on the stackoverflow post, you have created a utility that corrects & does provide you with a workaround to your specific issue so unfortunately there would be nothing further that we can advise upon in this matter.

    We apologize for the inconvenience.

    Kind Regards,

    Technical Support