Programmatically sign a PKCS#10 request with an officer certificate

Mar 5, 2016 at 5:40 PM
Edited Mar 5, 2016 at 5:48 PM
Hello !

For some certificates templates in my company it is required to sign the requests by PKI officers certificates (on a smart card) before issuance of the certificate (this allows some double-factor authent/multi-party control for sensitive templates).

I would like to simplify the process and do most of the work programmatically but after some tries I did not succeed in creating the signed PKCS#7 request.

Since your own PKCS#7 classes are only for decoding, here is the code I came up with with the built-in classes:
$cert = # Get X509Certificate2 object used for signing (private key is on a smart card)

$pkcs10 = Get-CertificateRequest -Path "path_to_pkcs10.req" # object is OK
$contentInfo = New-Object System.Security.Cryptography.Pkcs.ContentInfo (,$pkcs10.RawData)
$cmsSigner = New-Object System.Security.Cryptography.Pkcs.CmsSigner $cert
$signedCms = New-Object System.Security.Cryptography.Pkcs.SignedCms $contentInfo
$signedCms.ComputeSignature($cmsSigner)
$pkcs7Bytes = $signedCms.Encode()

[IO.File]::WriteAllBytes("path_to_pkcs7.req", $pkcs7Bytes)
$pkcs7 = Get-CertificateRequest -RawRequest $pkcs7Bytes
When using certutil -dump on the generated file, it looks OK. But the last line generates the following error:
New-Object : Exception when calling ".ctor" with "1" argument(s): "Invalid data"
At character C:\Program Files\WindowsPowerShell\Modules\PSPKI\Client\Get-CertificateRequest.ps1:22 : 14
+ ...  "RawData" {New-Object Security.Cryptography.X509CertificateRequests. ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation : (:) [New-Object], MethodInvocationException
    + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand

    at System.Management.Automation.DotNetAdapter.AuxiliaryConstructorInvoke(MethodInformation methodInformation, Object[] arguments, Object[] 
originalArguments)
   at System.Management.Automation.DotNetAdapter.ConstructorInvokeDotNet(Type type, ConstructorInfo[] constructors, Object[] arguments)
   at Microsoft.PowerShell.Commands.NewObjectCommand.CallConstructor(Type type, ConstructorInfo[] constructors, Object[] args)
And trying to submit the generated file to the CA fails with this error:
Error Parsing Request  ASN1 unexpected end of data. 0x80093102 (ASN: 258 CRYPT_E_ASN1_EOD)
Should I maybe specify some particular OID when creating the ContentInfo object? Or maybe I need to encode the PKCS#10 in some way before simply signing it?

Thanks in advance for any help you can provide me.
Jordan
Coordinator
Mar 8, 2016 at 8:54 AM
I would look into ASN.1 contents. There might be issues with ASN encoding in the file.
Mar 14, 2016 at 1:22 PM
After further testing, it would seem that signing a PKCS#10 request is not possible that way...
When directly using certreq -sign, I have the same error as above with a PKCS#10 request whereas all works fine with a CMC request (and several forums seem to confirm this behavior).
I don't mind invoking certreq -sign directly from my script (is is fully automatable) but unfortunately I cannot ensure all my requests are in CMC format (most of them are not currently and our users would be completely lost if we suddenly ask them to generate CMC requests only...).

Is there any way to use the "This number of authorized signatures" certificate template option with both CMC and PKCS#10 requests?
Or maybe it is possible to convert PKCS#10 to CMC (without knowledge of the private key)?

I will directly ask Microsoft but I have a feeling you will have a better answer than they will :)
Thank you.
Coordinator
Mar 14, 2016 at 7:39 PM
PKCS#10 do not support multiple signatures. You have to convert (embed) PKCS#10 request to PKCS#7 and use external signature to sign PKCS#7 message. You may need to check out my blog post: https://www.sysadmins.lv/blog-en/introducing-to-certificate-enrollment-apis-part-5-enroll-on-behalf-of.aspx
Mar 15, 2016 at 12:33 PM
OK I understand better. I tried to adapt your code from your blog post but am stuck at the initialize method of the ISignerCertificate COM object.
My signing certificate has its private key on a smart card and uses a 3rd party CSP. Even if I change the value of the second argument of the initialize method (I tried all values up till 4) and set the silent (=false) and even PIN properties before invoking Initialize, I get the following error message:
CertEnroll::CSignerCertificate::Initialize: Key does not exist. 0x8009000d (-2146893811 NTE_NO_KEY)
Using other tools to sign data (the SignedCms .NET class for instance) it works without any problem (prompt me for PIN then sign).
Do you know how I can construct my ISignerCertificate object properly?
Or adapt my .NET code from above so that it replicates the behaviour of IX509CertificateRequestPkcs7.InitializeFromInnerRequest (when giving a PKCS#10)?

Thank you again,
May 18, 2016 at 12:54 PM
Hello Camelot!

Just in case you're ever interested in this, note that the error mentioned above with the CSignerCertificate seems to be caused by a bug with my 3rd-party smart card CSP (which might never be fixed unfortunately...)
With any other software signing certificate I am able to embed PKCS#10 in PKCS#7 files and sign them.

If you have time some day and want to take a look (I'd understand if you don't of course!), my problem would probably be solved if we could translate IX509CertificateRequestPkcs7::InitializeFromInnerRequest into pure .NET code since I have no problem using the same smart card certificate when using the System.Security.Cryptography.Pkcs.CmsSigner class.
This is what I tried unsuccessfully in my first post but unfortunately I don't know enough the internal structure of PKCS files to get it working.

It would be a good addition to the .NET library in my opinion :D
Best regards,
Jordan
Coordinator
Jun 4, 2016 at 1:23 PM
Can you elaborate this point:
If you have time some day and want to take a look (I'd understand if you don't of course!), my problem would probably be solved if we could translate IX509CertificateRequestPkcs7::InitializeFromInnerRequest into pure .NET code
do you mean to convert existing PKCS10 request to PKCS7?
Jun 6, 2016 at 2:04 PM
Yes, in order to sign it with an officer certificate.
Since it is not possible to directly sign a PKCS#10 and the CertEnroll API is not usable with my 3rd party CSP (well it may be resolved sooner rather than later finally because the vendor has provided a fix already, I'm now waiting for an official release), mimicking it in .NET would do the trick.

I haven't tested with something else than a PKCS#10 but in that case, InitializeFromInnerRequest works well and create a PKCS#7 object ready to be signed. Unfortunately, my .NET attempt in the very first post is not the right way to do it.