Update 11.01.2022: fixed number conversion in "__composePRIVATEKEYBLOB" function
Hello everyone!
Some time ago I wrote a script that converts PEM file to CryptoAPI compatible format: How to convert PEM file to a CryptoAPI compatible format. The script involves some non-PowerShell commands (certutil) which associates private key with a certificate instance. I received several feedback comments about avoiding certutil in favor of native PowerShell/.NET managed code. In this post I want to show some code that eliminates certutil from the script.
Just to recall what we generally do when converting PEM to X509Certificate2/PFX:
In the first version of such converter I successfully done first three steps. I didn’t know how to do the last step natively by using PowerShell/.NET and used certutil –mergePFX
command to associate PRIVATEKYEBLOB with public certificate.
Recently I figured that X509Certificate2.PrivateKey property has setter accessor. and the page provides lots of hints. As per documentation, the property accepts either an RSACryptoServiceProvider or a DSACryptoServiceProvider objects.
RSACryptoServiceProvider class contains a ImportCspBlob(Byte[]) method which does the trick. It accepts binary PRIVATEKEYBLOB as a parameter! However, the key must be stored in some crypto provider and must have a container name within provider. So, if we look at constructors, we can find a suitable one: RSACryptoServiceProvider(CspParameters). So, we need to prepare crypto provider information and use this info during key import.
What information we need to provide in the CspParameters object? At a minimum, we must specify:
Microsoft Enhanced RSA and AES Cryptographic Provider
as it supports a wide range of keys and key sizes.$cspParams = New-Object Security.Cryptography.CspParameters -Property @{ ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider" KeyContainerName = "pspki-" + [Guid]::NewGuid().ToString() KeyNumber = $KeySpec # 1 - AT_EXCHANGE, 2 - AT_SIGNATURE } # if we want to install the certificate to local machine store specify appropriate flag # there is no need to specify additional flags when installing to current user store. if ($Install -and $StoreLocation -eq "LocalMachine") { $cspParams.Flags += [Security.Cryptography.CspProviderFlags]::UseMachineKeyStore } # construct RSACryptoServiceProvider from CSP information $rsa = New-Object Security.Cryptography.RSACryptoServiceProvider $cspParams # load PRIVATEKEYBLOB into CSP $rsa.ImportCspBlob($PrivateKey) # attach private key to certificate $Cert.PrivateKey = $rsa
By default, the key is exportable, so you can do with it whatever you want. Export policy and other key settings are configured in the CspParameters.Flags property.
Eventually, I reworked the script to provide more flexibility. This includes the following features:
New function requires only one parameter: path to a PEM file. The rest is optional and depends on your needs.
The function returns an instance of X509Certificate2 class for reference only. Private key object is disposed and cannot be used in this state.
And the full function code:
Add-Type @" namespace System.Security.Cryptography.X509Certificates { public enum X509KeySpecFlags { None = 0, AT_KEYEXCHANGE = 1, AT_SIGNATURE = 2 } } "@ function Convert-PemToPfx { [OutputType('[System.Security.Cryptography.X509Certificates.X509Certificate2]')] [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$InputPath, [string]$KeyPath, [string]$OutputPath, [Security.Cryptography.X509Certificates.X509KeySpecFlags]$KeySpec = "AT_KEYEXCHANGE", [Security.SecureString]$Password, [string]$ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider", [Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation = "CurrentUser", [switch]$Install ) if ($PSBoundParameters.Verbose) {$VerbosePreference = "continue"} if ($PSBoundParameters.Debug) { $Host.PrivateData.DebugForegroundColor = "Cyan" $DebugPreference = "continue" } #region helper functions function __normalizeAsnInteger ($array) { $padding = $array.Length % 8 if ($padding) { $array = $array[$padding..($array.Length - 1)] } [array]::Reverse($array) [Byte[]]$array } function __extractCert([string]$Text) { if ($Text -match "(?msx).*-{5}BEGIN\sCERTIFICATE-{5}(.+)-{5}END\sCERTIFICATE-{5}") { $keyFlags = [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable if ($Install) { if ($StoreLocation -eq "CurrentUser") { $keyFlags = $keyFlags -bor [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet } else { $keyFlags = $keyFlags -bor [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet } } $RawData = [Convert]::FromBase64String($matches[1]) try { New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $RawData, "", $keyFlags } catch {throw "The data is not valid security certificate."} Write-Debug "X.509 certificate is correct." } else {throw "Missing certificate file."} } # returns [byte[]] function __composePRIVATEKEYBLOB($modulus, $PublicExponent, $PrivateExponent, $Prime1, $Prime2, $Exponent1, $Exponent2, $Coefficient) { Write-Debug "Calculating key length." $bitLen = "{0:X4}" -f $($modulus.Length * 8) Write-Debug "Key length is $($modulus.Length * 8) bits." [byte[]]$bitLen1 = Invoke-Expression 0x$($bitLen.Substring(0,2)) [byte[]]$bitLen2 = Invoke-Expression 0x$($bitLen.Substring(2,2)) [Byte[]]$PrivateKey = 0x07,0x02,0x00,0x00,0x00,0x24,0x00,0x00,0x52,0x53,0x41,0x32,0x00 [Byte[]]$PrivateKey = $PrivateKey + $bitLen1 + $bitLen2 + $PublicExponent + ,0x00 + ` $modulus + $Prime1 + $Prime2 + $Exponent1 + $Exponent2 + $Coefficient + $PrivateExponent $PrivateKey } # returns RSACryptoServiceProvider for dispose purposes function __attachPrivateKey($Cert, [Byte[]]$PrivateKey) { $cspParams = New-Object Security.Cryptography.CspParameters -Property @{ ProviderName = $ProviderName KeyContainerName = "pspki-" + [Guid]::NewGuid().ToString() KeyNumber = [int]$KeySpec } if ($Install -and $StoreLocation -eq "LocalMachine") { $cspParams.Flags = $cspParams.Flags -bor [Security.Cryptography.CspProviderFlags]::UseMachineKeyStore } $rsa = New-Object Security.Cryptography.RSACryptoServiceProvider $cspParams $rsa.ImportCspBlob($PrivateKey) $Cert.PrivateKey = $rsa $rsa } # returns Asn1Reader function __decodePkcs1($base64) { Write-Debug "Processing PKCS#1 RSA KEY module." $asn = New-Object SysadminsLV.Asn1Parser.Asn1Reader @(,[Convert]::FromBase64String($base64)) if ($asn.Tag -ne 48) {throw "The data is invalid."} $asn } # returns Asn1Reader function __decodePkcs8($base64) { Write-Debug "Processing PKCS#8 Private Key module." $asn = New-Object SysadminsLV.Asn1Parser.Asn1Reader @(,[Convert]::FromBase64String($base64)) if ($asn.Tag -ne 48) {throw "The data is invalid."} # version if (!$asn.MoveNext()) {throw "The data is invalid."} # algorithm identifier if (!$asn.MoveNext()) {throw "The data is invalid."} # octet string if (!$asn.MoveNextCurrentLevel()) {throw "The data is invalid."} if ($asn.Tag -ne 4) {throw "The data is invalid."} if (!$asn.MoveNext()) {throw "The data is invalid."} $asn } #endregion $ErrorActionPreference = "Stop" $File = Get-Item $InputPath -Force -ErrorAction Stop if ($KeyPath) {$Key = Get-Item $KeyPath -Force -ErrorAction Stop} # parse content $Text = Get-Content -Path $InputPath -Raw -ErrorAction Stop Write-Debug "Extracting certificate information..." $Cert = __extractCert $Text if ($Key) {$Text = Get-Content -Path $KeyPath -Raw -ErrorAction Stop} $asn = if ($Text -match "(?msx).*-{5}BEGIN\sPRIVATE\sKEY-{5}(.+)-{5}END\sPRIVATE\sKEY-{5}") { __decodePkcs8 $matches[1] } elseif ($Text -match "(?msx).*-{5}BEGIN\sRSA\sPRIVATE\sKEY-{5}(.+)-{5}END\sRSA\sPRIVATE\sKEY-{5}") { __decodePkcs1 $matches[1] } else {throw "The data is invalid."} # private key version if (!$asn.MoveNext()) {throw "The data is invalid."} # modulus n if (!$asn.MoveNext()) {throw "The data is invalid."} $modulus = __normalizeAsnInteger $asn.GetPayload() Write-Debug "Modulus length: $($modulus.Length)" # public exponent e if (!$asn.MoveNext()) {throw "The data is invalid."} # public exponent must be 4 bytes exactly. $PublicExponent = if ($asn.GetPayload().Length -eq 3) { ,0 + $asn.GetPayload() } else { $asn.GetPayload() } Write-Debug "PublicExponent length: $($PublicExponent.Length)" # private exponent d if (!$asn.MoveNext()) {throw "The data is invalid."} $PrivateExponent = __normalizeAsnInteger $asn.GetPayload() Write-Debug "PrivateExponent length: $($PrivateExponent.Length)" # prime1 p if (!$asn.MoveNext()) {throw "The data is invalid."} $Prime1 = __normalizeAsnInteger $asn.GetPayload() Write-Debug "Prime1 length: $($Prime1.Length)" # prime2 q if (!$asn.MoveNext()) {throw "The data is invalid."} $Prime2 = __normalizeAsnInteger $asn.GetPayload() Write-Debug "Prime2 length: $($Prime2.Length)" # exponent1 d mod (p-1) if (!$asn.MoveNext()) {throw "The data is invalid."} $Exponent1 = __normalizeAsnInteger $asn.GetPayload() Write-Debug "Exponent1 length: $($Exponent1.Length)" # exponent2 d mod (q-1) if (!$asn.MoveNext()) {throw "The data is invalid."} $Exponent2 = __normalizeAsnInteger $asn.GetPayload() Write-Debug "Exponent2 length: $($Exponent2.Length)" # coefficient (inverse of q) mod p if (!$asn.MoveNext()) {throw "The data is invalid."} $Coefficient = __normalizeAsnInteger $asn.GetPayload() Write-Debug "Coefficient length: $($Coefficient.Length)" # creating Private Key BLOB structure $PrivateKey = __composePRIVATEKEYBLOB $modulus $PublicExponent $PrivateExponent $Prime1 $Prime2 $Exponent1 $Exponent2 $Coefficient #region key attach and export routine $rsaKey = __attachPrivateKey $Cert $PrivateKey if (![string]::IsNullOrEmpty($OutputPath)) { if (!$Password) { $Password = Read-Host -Prompt "Enter PFX password" -AsSecureString } $pfxBytes = $Cert.Export("pfx", $Password) Set-Content -Path $OutputPath -Value $pfxBytes -Encoding Byte } #endregion if ($Install) { $store = New-Object Security.Cryptography.X509Certificates.X509Store "my", $StoreLocation $store.Open("ReadWrite") $store.Add($Cert) $store.Close() } $rsaKey.Dispose() $Cert }
The code relies on my new ASN.1 binary parser which is available at GitHub: Asn1DerParser.NET. Click on file and press “Raw” button to download the DLL.
HTH
I've downloaded the additional dll and unblocked the file, its in the same folder as the function in a ps1 file, however I keep get the following error:
Unable to find type [Security.Cryptography.X509Certificates.X509KeySpecFlags]
I've tried running PowerShell as admin but I don't think that is the issue, it can't seem to find the type on my system, I'm running Windows 10 if that makes a difference, as I've tried on a Server 2012 R2 box with the same results, any ideas?
I updated the post. It was missing one custom enum type. Now it should work.
I had the same problem as "Aftab" where I would receive the error:
Convert-PemToPfx : Unable to find type [Security.Cryptography.X509Certificates.X509KeySpecFlags]
I did a little digging into this and this error is because the type is defined in the PKI.Core assembly rather than .NET framework.
I fixed the error by first loading the assembly PKI.Core into the AppDomain.
After this error I got a simmilar one for the type SysadminsLV.Asn1Parser.Asn1Reader
New-Object : Cannot find type [SysadminsLV.Asn1Parser.Asn1Reader]: verify that the assembly containing this type is loaded.
Again this was fixed by first loading the assembly SysadminsLV.Asn1Parser into the AppDomain.
Perhaps the article could be updated to include these or is there a better way of fixing this problem.
Is it possible / planned to add russian cryptographic algorithms (GOST 2012 / 2001)?
There are no such plans. Currently, I'm planning to add ellyptic curve cryptography only.
Thank you for researching and providing a solution for us CryptoAPI n00bs.
@Carl Reid, the script just needs to have the Add-Type and [System.Reflectin.Assymbly]::LoadFile() call to load the extension class at the top of the script. Or in my case, I just replaced the Enum type variable with the integer I wanted, as I only needed the ability to create a PFX/Pkcs12 compliant file.
Hello Vadmins
Thanks very much for posting, I am trying to use the script but I get an error. Before getting to the error I am trying to do something slightly different but close to what your script does
I have a PEM file for standard certificate e.g. -----BEGIN CERTIFICATE-----blah blah base64 blah blah----END CERTIFICATE----- this PEM has No private key information
I want to import the PEM into the CurrentUser\My store (or LocalMachine does not matter for my perposes) but importantly I want to use the "Microsoft Enhanced RSA and AES Cryptographic Provider" if I import the certificate using the MMC or basic PowerShell it appears to end up in the CAPI store (as it were) and therefore I cannot use the certificates public key to check a SHA256 signature as this cryptographic primiative is not available to the CAPI store (as I understand it) but only to CAPI2
I have gotten around this by using openssl on windows like so
openssl pkcs12 -export -in MyCert.crt -out MyCert.pfx -nodes -nokeys -CSP “Microsoft Enhanced RSA and AES Cryptographic Provider” -passout pass:secret
however I then need to import the PFX to my store, then once in the store I have to get back out again into a certifcate object so I can use the following method of the public key
$Certificate.PublicKey.Key.VerifyData([byte[]][char[]]$DataString, 'SHA256', $SignatureBytes)
basically checking the DataString is signed OK using (SHA256), this works OK but is a lot of work
I found if I just get the base64 certificate and convert it to a certificate object I do not have the PublicKey.Key.VerifyData method, so basically need to install into the store first then get back again, then I have the method, but for SHA265 need to get from the CAPI2 store so needs to be added to the CAPI2 store in the first place.
can you script take a PEM without a private key and add it to the CAPI2 store please?
when I run the script I first load your PSPKI module then I add the following like to the script [System.Reflection.Assembly]::LoadFile("C:\unix\SysadminsLV.Asn1Parser.dll")
then I run the script and using the single parameter -inputPath e.g.
Convert-PemToPFX -InputPath C:\temp\MyCert.cer
I get the following error
The data is invalid.
At C:\Unix\Convert.ps1:146 char:9
+ else { throw "The data is invalid." }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (The data is invalid.:String) [], RuntimeException
+ FullyQualifiedErrorId : The data is invalid.
Looking at the PowerShell code in your script I believe it is throwing this error was my PEM does not contain a private key
the reason it does not contain a private key is because what I am really trying to do is just import a standard PEM to the CAPI2 store (Microsoft Enhanced RSA and AES Cryptographic Provider)
I would be very grateful for your advice, thanks in advance
Ernest
Few notes:
> [System.Reflection.Assembly]::LoadFile("C:\unix\SysadminsLV.Asn1Parser.dll")
it is no longer supported in PowerShell. Instead, you have to use Add-Type cmdlet.
> CAPI2 store (Microsoft Enhanced RSA and AES Cryptographic Provider)
mentioned provider is still CAPI1 provider. CAPI2 providers are key storage providers. Your question is not related to this script, and the script won't help you.
What you can try is to try manually load public key to CngKey .NET class to load the key to CNG provider and then use this object to validate SHA2 signatures. Or call directly NCrypt functions via p/invoke.
Hello Vadims
Thanks very much for taking the time to reply, I get the basic concept of what you are saying and will have a go at working it out.
In the mean time if you can have a think about how much you would charge to create a PowerShell function to take an input of a certificate [System.Security.Cryptography.X509Certificates.X509Certificate2] (again I do not have the private key material) and allow the public key to be used to verify a SHA256 signature.
Then if I cannot figure I have a plan B e.g. ask my project manager to get the money to pay you via our supplier (as we did last time for the tuition) for the function
Thanks Vadmins
You can do it this way:
$key = [Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($cert)This line will return public key loaded into CNG provider and you can use this object to verify the signature. More details: https://msdn.microsoft.com/en-us/library/system.security.cryptography.rsa(v=vs.110).aspx
Hi Vadims
Thanks very much, I really appreciate you taking the time,
looks at the overloads on the VerifyData method require a parameter for Padding, I presume I need to pass it something like [System.Security.Cryptography.RSASignaturePadding]::Pkcs1
There is only a few options that I can see for padding (if I am thinking about it in the right way with my pea sized brain), I will give the above padding a try first
I feel like I owe you something for all your kind help we can't be all as bright as you Vadims :) , weather in the UK is awful :( but I am off to join Ana in sunny Portugal in two weeks :) so you are always welcome at our house there (good wine, food, weather).
Cheers
Ernest
I attempted to set this up. I found a couple of problems.
First the Add-Type line needs to be the first line in the script and not inside the function.
Second, no matter what I set for the $Keyspec flag, It is always set to AT_SIGNATURE.
I debugged it for a while and can see the value being set to one on the property to 1 for AT_EXCHANGE but for whatever reason, the resulting certificate has it set to 2 AT_SIGNATURE.
> First the Add-Type line needs to be the first line in the script and not inside the function.
yes, you are correct. I updated the code.
After some more digging I found that if I add install and , I always get the following error
Convert-PemToPfx : Method invocation failed because [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags] does not contain a method named 'op_Addition'.
At C:\tmp\converttopfx.ps1:182 char:1
+ Convert-PemToPfx -InputPath cert.pem -KeyPath cert.key -OutputPath cert.pfx -ins ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Addition:String) [Convert-PemToPfx], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound,Convert-PemToPfx
Running Windows 2012 and Powershell 5
This code perfectly runs on PowerShell v5, though is incompatible with versions prior to PowerShell 4.0. I updated the code with compatible syntax.
I would say that the code runs fine but the implementation is slightly off.
If I run with the -KeySpec AT_EXCHANGE, It always creates the certficate with a KeySpec of -AT_SIGNATURE
I confirm this by running the following command to verify the setup
certutil –v –store "my" "<certificate name in the certificate store>"
I always get:
ProviderType = 18
Flags = 20 (32)
CRYPT_MACHINE_KEYSET -- 20 (32)
KeySpec = 2 -- AT_SIGNATURE
I need to do more tests. I can't tell now what is wrong with KeySpec value.
Hi,
Do you have C# example for converting PEM to PFX?
please help
thanks
No, I don't have C# equivalent of this script.
For those that have looked to use this code in say PowerShell Core, do know that the line where the private key is being set (within the __attachPrivateKey private function) was not backported in the X509Certificate2 API. Instead, there is a helper extension method, CopyWithPrivateKey() that should be used. The following code works on a PowerShell Core 7.0.2 system:
# This is here due to .NetCore changes to the X509Certificate2 API. The PrivateKey property cannot be set, and getter is only available for backwards compat. (https://github.com/dotnet/runtime/issues/27346)
if ($PSVersionTable.PSEdition -eq "Core")
{
[void][Reflection.Assembly]::Load("System.Security.Cryptography.X509Certificates")
$Cert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey($Cert.RawData, $rsa)
}
else
{
$Cert.PrivateKey = $rsa
}
@Chris Lynch
Thanks for update! I'm aware about this limitation in .NET Core, the article was written before first .NET Core release and wasn't updated to reflect this change.
Thanks for update! I'm aware about this limitation in .NET Core, the article was written before first .NET Core release and wasn't updated to reflect this change.
You're welcome. This was really to help others that have been using your wonderful code here to handle PEM certs and creating PFX files, and struggling with .Net Core and/or PowerShell Core.
One more thing for your readers:
The __attachPrivateKey private function should not be useing the same $Cert parameter. That winds up stepping over the $Cert variable within the script, and somehow loses the private key. So, I would change the paramerter $Cert to $_Cert, and ensure the line $Cert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey($Cert.RawData, $rsa) is actually $Cert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey($_Cert.RawData, $rsa).
I have been pulling out what little hair I have left over the past day trying to figure out why using the CopyWithPrivateKey does indeed create a new cert with the private key, but then the PFX created does not.
Actually, let me just provide a Gist here.
Yes, I understand the problem, but not sure what is wrong with these variables. Because when you specify a variable at function scope, it won't propagate from outer scopes. Though for safety I've updated PSPKI to handle this: https://github.com/PKISolutions/PSPKI/commit/8a7919233e21f2355677993ba04f443729ca4640
Big thanks to Chris Lynch for his updates to this module! We still use his code he shared in his Gist because I believe it has more changes than what's been implemented. I'd suggest you guys work together and think about Chris' suggestions.
Also, there's still a typo in the code. About row 85 or so, you'll find "$scriptCert.PrivateKey = $rsa" for us Powershell <7.0 folks but that line is missing a colon specifying the scope for $Cert. The line should read "$script:Cert.PrivateKey = $rsa"
Thanks for everyone's efforts.
Hey Scott, good catch! I've fixed missing colon: https://github.com/PKISolutions/PSPKI/commit/2c2e2be929cedab0c2032bf1551d886a03bc940c
> I believe it has more changes than what's been implemented.
No, it does not have extra changes.
Finally I got the RSA private key import working in PowerShell Core. My application is using a derived Convert-PemToPfx to do the trick (see https://github.com/PKISolutions/PSPKI/issues/64 for reasoning).
My changes are in __attachRSAPrivateKey() and there I'm using CngKey and RSACng for the import. After failing a lot on the import on PS core, this version of mine finally does the trick.
Feel free to utilize the approach in PSPKI, I certainly borrowed tons of your code in my app.
Apologies. The Github link was missing in my previous comment.
https://github.com/HQJaTu/RDP-cert-tools/blob/main/update-RDP-cert.ps1
Hi Vadims,
This is very helpful. The script is ruuning fine however I don't know if it is an issue or you did it intentionally.
The private key should not be exportable.
Open Certificate Export Wizard(certmgr.msc, select this certificate -> All Task-> Export )
"Yes, export the private key" this option should be disabled.
Can you tell which property need to set to make this option disabled in certifiacte export wizard.
Thanks,
Yatish
Hi,
Really appreciate sharing this info. I am using this example to convert a private key but in this case, the length of the key is 3072 and it is ending an error when executing the following code:
[byte[]]$bitLen2 = Invoke-Expression 0x$([int]$bitLen.Substring(2,2))
Cannot convert value "0C" to type "System.Int32". Error: "Input string was not in a correct format."
At C:\Users\kirashiv\Documents\CertRequest\codesign_2022\CertModule.ps1:231 char:47
+ ... byte[]]$bitLen1 = Invoke-Expression 0x$([int]$bitLen.Substring(0,2))
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastFromStringToInteger
0x : The term '0x' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is
correct and try again.
At line:1 char:1
+ 0x
+ ~~
+ CategoryInfo : ObjectNotFound: (0x:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
Can you please advise what can be done to add support for increased key length?
Regards,
-Shiva
Hello Shiva, please check updated version. There was a bug in the script and I believe it is now fixed.
Post your comment:
Comments: