This article is written after experiencing how difficult it is today to create self-signed certificates that works with .NET and specificially WCF. If you use the default method in Windows, using the cmdlet New-SelfSignedCertificate
in Powershell, chances are high that you will stumble upon this error:
System.Security.Cryptography.CryptographicException: Invalid provider type specified.
A few years back, creating certificates was way easier using
makecert.exe
Then Windows required the certificates to have length of minimum 1024. After some years passed,
makecert.exe was deprecated. Microsoft now offers .NET developers to create a self signed certificate the cmdlet New-SelfSignedCertificate to use in development and test environments. Actually it turns out that you still must adjust the private part of certificate to use the RSACryptoServiceProvider by using OpenSSL to achieve this and then import to the certificate, sadly Microsoft gives developers tools for self-signed certificate generation that needs to use third-part libraries such as SSLR to work properly with .NET, at least this is the case with WCF..
Here is the Powershell script I ended up with:
Write-Host "Generating a self signed certificate for client in MSMQ WCF demo"
$clientCertFile = "C:\temp\MSMQWcfDemoClient.pfx"
$clientCertFileRsaFormatted = "C:\temp\MSMQWcfDemoClient.RSAConverted.pfx"
$cert = New-SelfSignedCertificate -Subject "CN=MSMQWcfDemoClient" -certstorelocation cert:\localmachine\my `
-NotAfter (Get-Date).AddYears(3) -KeyLength 2048 -KeySpec KeyExchange
$pwd = ConvertTo-SecureString -String ‘bongo’ -Force -AsPlainText
$path = 'cert:\localMachine\my\' + $cert.thumbprint
Export-PfxCertificate -cert $path -FilePath $clientCertFile -Password $pwd
$clientCertPasswordSec = ConvertTo-SecureString "bongo" -AsPlainText -Force
Write-Host "Generating a self signed certificate for server in MSMQ WCF demo"
$serverCertFile = "C:\temp\MSMQWcfDemoServer.pfx"
$serverCertFileRsaFormatted = "C:\temp\MSMQWcfDemoServer.RSAConverted.pfx"
$certServer = New-SelfSignedCertificate -Subject "CN=MSMQWcfDemoserver" -certstorelocation cert:\localmachine\my `
-NotAfter (Get-Date).AddYears(3) -KeyExportPolicy Exportable -KeyLength 2048 -KeySpec KeyExchange
$pwdServer = ConvertTo-SecureString -String ‘kongo’ -Force -AsPlainText
$pathServer = 'cert:\localMachine\my\' + $certServer.thumbprint
Export-PfxCertificate -cert $pathServer -FilePath $serverCertFile -Password $pwdServer
$serverCertPasswordSec = ConvertTo-SecureString "kongo" -AsPlainText -Force
$command = @'
cmd.exe /c c:\temp\rsaconvert.bat
'@
Write-Host "Starting bat file to convert from CNG to RSA format.."
Invoke-Expression -Command:$command
Write-Host "Importing RSA formatted certificates.."
Import-PfxCertificate -FilePath $clientCertFileRsaFormatted -CertStoreLocation Cert:\LocalMachine\My -Password $clientCertPasswordSec
Import-PfxCertificate -FilePath $clientCertFileRsaFormatted -CertStoreLocation Cert:\LocalMachine\root -Password $clientCertPasswordSec
Import-PfxCertificate -FilePath $serverCertFileRsaFormatted -CertStoreLocation Cert:\LocalMachine\My -Password $serverCertPasswordSec
Import-PfxCertificate -FilePath $serverCertFileRsaFormatted -CertStoreLocation Cert:\LocalMachine\root -Password $serverCertPasswordSec
The powershell script uses a bat file that calls Openssl to convert the pfx certificate to use the RSACryptoServiceProvider. This is how the bat file looks like:
echo Generating RSA format certificates for server using OpenSSL..
c:\openssl\openssl.exe pkcs12 -in "C:\temp\MSMQWcfDemoserver.pfx" -nokeys -out "C:\temp\MSMQWcfDemoserver.cer" -passin "pass:kongo"
c:\openssl\OpenSSL.exe pkcs12 -in "C:\temp\MSMQWcfDemoserver.pfx" -nocerts -out "C:\temp\MSMQWcfDemoserver.pem" -passin "pass:kongo" -passout "pass:kongo"
c:\openssl\OpenSSL.exe rsa -inform PEM -in "C:\temp\MSMQWcfDemoserver.pem" -out "C:\temp\MSMQWcfDemoserver.rsa" -passin "pass:kongo" -passout "pass:kongo"
c:\openssl\openssl.exe pkcs12 -export -in "C:\temp\MSMQWcfDemoserver.cer" -inkey "C:\temp\MSMQWcfDemoserver.rsa" -out "C:\temp\MSMQWcfDemoserver.RSAConverted.pfx" -passin "pass:kongo" -passout "pass:kongo"
echo Generating RSA format certificates for client using OpenSSL..
c:\openssl\openssl.exe pkcs12 -in "C:\temp\MSMQWcfDemoclient.pfx" -nokeys -out "C:\temp\MSMQWcfDemoclient.cer" -passin "pass:bongo"
c:\openssl\OpenSSL.exe pkcs12 -in "C:\temp\MSMQWcfDemoclient.pfx" -nocerts -out "C:\temp\MSMQWcfDemoclient.pem" -passin "pass:bongo" -passout "pass:bongo"
c:\openssl\OpenSSL.exe rsa -inform PEM -in "C:\temp\MSMQWcfDemoclient.pem" -out "C:\temp\MSMQWcfDemoclient.rsa" -passin "pass:bongo" -passout "pass:bongo"
c:\openssl\openssl.exe pkcs12 -export -in "C:\temp\MSMQWcfDemoclient.cer" -inkey "C:\temp\MSMQWcfDemoclient.rsa" -out "C:\temp\MSMQWcfDemoclient.RSAConverted.pfx" -passin "pass:bongo" -passout "pass:bongo"
The bat file uses OpenSSL to extract (dismantle) the public part of the certificate into a .cer file (this part could have been done with MMC). The next step is to generate a from our existing pfx file to export to a pem file and then a rsa file with OpenSSL. We then meld the .cer file and .rsa file into a converted .pfx file.
This file is with the Powershell script automatically imported into the certificate store Personal (My) of certificate location Local Computer. The Powershell script also Import-PfxCertificate to Trusted Root Certification Authorities.
Anyways, my goal was to give you a demo about .NET, WCF and NetMsmqBinding using message security, but I first had to get over this hurdle to be able to have some protection in WCF with certificate and had no idea that Microsoft had given developers so cumbersome tools to generate a self signed certificate to actually work with WCF.
Now my MSMQ message queue is filled with encrypted and protection MSMQ messages (containing WCF message to be consumed)! :)
Note: the MSMQ queue was inspected using this Powershell script:
#
# InspectMessageQueueWcfContent.ps1
#
#
# InspectMessageQueue.ps1
#
[System.Reflection.Assembly]::LoadWithPartialName("System.Messaging") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.Xml") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.ServiceModel") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.IO") | Out-Null
$queuePath = ".\private$\demoqueue3"
Write-Host "Powershell MSMQ queue WCF inspector v0.1. Inspecting queue contents of the queue: $queuePath"
Write-Host ""
Run-MainDemoIterateMsmq $queuePath
Function Get-XmlFromWcfMessage([System.Messaging.Message] $msg) {
$doc = New-Object System.Xml.XmlDocument;
$messageLength = [int] $msg.BodyStream.Length
$buffer = New-Object byte[] $messageLength
$msg.BodyStream.Read($buffer, 0, $messageLength)
$envelopeStart = Find-SoapEnvelopeStart($buffer)
$envelopeStart = $envelopeStart - 0
$envelopeLength = $($buffer.Length - $envelopeStart)
#Write-Host $envelopeStart
$stream = New-Object System.IO.MemoryStream($buffer, $envelopeStart, $envelopeLength)
$elm = New-Object System.ServiceModel.Channels.BinaryMessageEncodingBindingElement
$elm.ReaderQuotas.MaxStringContentLength = 10000000
$elm.ReaderQuotas.MaxBytesPerRead = 10000000
$msg1 = $elm.CreateMessageEncoderFactory().Encoder.ReadMessage($stream, 10000000);
$doc.Load($msg1.GetReaderAtBodyContents());
$msg.BodyStream.Position = 0;
return $doc;
}
Function Find-SoapEnvelopeStart([byte[]] $stream)
{
$i = 0;
$j = 0;
$prevByte = $stream[$i];
$curByte = [byte]$j;
for ($i = 0; $i -lt $stream.Length; $i++)
{
$curByte = $stream[$i];
if ($curByte -eq [byte] 0x02 -and $prevByte -eq [byte] 0x56) {
break;
}
$prevByte = $curByte;
}
return $i - 1;
}
Function Run-MainDemoIterateMsmq([string] $queuePath) {
$queue = New-Object System.Messaging.MessageQueue $queuePath
foreach ($message in $queue.GetAllMessages()){
$xmlDoc = Get-XmlFromWcfMessage $message
Write-Host $xmlDoc.OuterXml
}
}