I am testing the iText 7.1.2.0 library to sign pdf files, using a digital certificate or smart card (X509Certificate2) in a C # project. But I'm getting this error when I try to create the IExternalSignature.
According to the documentation found (here, here and here), the way to achieve this process is using the BouncyCastle library that allows extracting the primary keys from the digital certificate, however, it is giving me an error and I can not find another way to do it.
In the documentation (here) they are created from .pfx files, but for this case, I need to take the primary keys directly from the certificate in the card reader. In previous versions of iText, it's allowed the creation with the following command:
IExternalSignature externalSignature = new X509Certificate2Signature(Certificate, "SHA-1");
But in version 7 it is no longer available and in the documentation I do not see how to do it.
Someone has used iText 7 and has been able to sign using an X509Certificate2 that knows the correct way to create the IExternalSignature?
This is the code that I am using:
public void SignPDF(string source, string target, X509Certificate2 certificate, string reason, string location, bool addVisibleSign, bool addTimeStamp, string strTSA, int qtySigns, int pageNumber)
{
try
{
Org.BouncyCastle.X509.X509Certificate vert = Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(certificate);
X509CertificateParser objCP = new X509CertificateParser();
Org.BouncyCastle.X509.X509Certificate[] objChain = new Org.BouncyCastle.X509.X509Certificate[] { objCP.ReadCertificate(certificate.RawData) };
IList<ICrlClient> crlList = new List<ICrlClient>();
crlList.Add(new CrlClientOnline(objChain));
PdfReader objReader = new PdfReader(source);
PdfSigner objStamper = new PdfSigner(objReader, new FileStream(target, FileMode.Create), false);
ITSAClient tsaClient = null;
IOcspClient ocspClient = null;
if (addTimeStamp)
{
OCSPVerifier ocspVerifier = new OCSPVerifier(null, null);
ocspClient = new OcspClientBouncyCastle(ocspVerifier);
tsaClient = new TSAClientBouncyCastle(strTSA);
}
PdfSignatureAppearance signatureAppearance = objStamper.GetSignatureAppearance();
signatureAppearance.SetReason(reason);
signatureAppearance.SetLocation(location);
signatureAppearance.SetPageNumber(pageNumber);
signatureAppearance.SetRenderingMode(PdfSignatureAppearance.RenderingMode.NAME_AND_DESCRIPTION);
if (addVisibleSign && qtySigns == 1)
signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(36, 20, 144, 53)).SetPageNumber(pageNumber);
else if (addVisibleSign && qtySigns == 2)
signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(160, 20, 268, 53)).SetPageNumber(pageNumber);
else if (addVisibleSign && qtySigns == 3)
signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(284, 20, 392, 53)).SetPageNumber(pageNumber);
else if (addVisibleSign && qtySigns == 4)
signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(408, 20, 516, 53)).SetPageNumber(pageNumber);
var pk = Org.BouncyCastle.Security.DotNetUtilities.GetKeyPair(certificate.PrivateKey).Private;
IExternalSignature externalSignature = new PrivateKeySignature(pk, "SHA-1");
objStamper.SignDetached(externalSignature, objChain, crlList, ocspClient, tsaClient, 0, PdfSigner.CryptoStandard.CMS);
if (objReader != null)
{
objReader.Close();
}
}
catch (Exception ex)
{
result.error = true;
result.errorMessage += "Error: " + ex.Message;
}
}
Thanks!
I do not believe that class was ported to iText 7- it is just a wrapper class.
You can see how to create a custom IExternalSignatureContainer in this example
Note that the source for the iText 5 X509Certificate2Signature can be found here
So something like this:
public class X509Certificate2Signature: IExternalSignature {
private String hashAlgorithm;
private String encryptionAlgorithm;
private X509Certificate2 certificate;
public X509Certificate2Signature(X509Certificate2 certificate, String hashAlgorithm) {
if (!certificate.HasPrivateKey)
throw new ArgumentException("No private key.");
this.certificate = certificate;
this.hashAlgorithm = DigestAlgorithms.GetDigest(DigestAlgorithms.GetAllowedDigest(hashAlgorithm));
if (certificate.PrivateKey is RSACryptoServiceProvider)
encryptionAlgorithm = "RSA";
else if (certificate.PrivateKey is DSACryptoServiceProvider)
encryptionAlgorithm = "DSA";
else
throw new ArgumentException("Unknown encryption algorithm " + certificate.PrivateKey);
}
public virtual byte[] Sign(byte[] message) {
if (certificate.PrivateKey is RSACryptoServiceProvider) {
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider) certificate.PrivateKey;
return rsa.SignData(message, hashAlgorithm);
}
else {
DSACryptoServiceProvider dsa = (DSACryptoServiceProvider) certificate.PrivateKey;
return dsa.SignData(message);
}
}
public virtual String GetHashAlgorithm() {
return hashAlgorithm;
}
public virtual String GetEncryptionAlgorithm() {
return encryptionAlgorithm;
}
}
Will replicate the class' function in iText 7. How to use the class is shown here in my first link, though you most likely will be using the signDetached() method instead of the signDeffered() method.
The certificate.PrivateKey in my experience is not reliable when you're dealing with non-RSA key pairs. For example a certificate that has an ECDsa key-pair will throw a NotSupportedException("The certificate key algorithm is not supported") on calling .PrivateKey, don't know why (actually here's why, also probably useful). So I implemented a (little janky) workaround to retrieve the private key, and create an IExternalSignature out of it. Since the code has no knowledge of what type of key a certificate has, and I couldn't be bothered to process the other properties to find out, I wrote a loop that tries for a generic, RSA, DSA and ECDsa key extraction. The generic works for RSA too, but since X509Certificate2 has a GetRSAPrivateKey() method, I made a switch case for it just in case.
#nullable enable
private (IExternalSignature? signature, IDisposable? key) ExtractPrivateKey(X509Certificate2 certificate)
{
IExternalSignature? signature = null;
bool foundPk = false;
int attempt = 0;
IDisposable? key = null;
while (foundPk is false)
{
try
{
switch (attempt)
{
case 0:
AsymmetricAlgorithm? pk = certificate.PrivateKey; //throws exception when certificate has an ECDsa key
signature = new PrivateKeySignature(DotNetUtilities.GetKeyPair(pk).Private, _digestAlgorithm);
key = pk;
break;
case 1:
RSA rsa = certificate.GetRSAPrivateKey() ?? throw new NotSupportedException("RSA private key is null on the certificate.");
signature = new PrivateKeySignature(DotNetUtilities.GetRsaKeyPair(rsa).Private, _digestAlgorithm);
key = rsa;
break;
case 2:
DSA dsa = certificate.GetDSAPrivateKey() ?? throw new NotSupportedException("Dsa private key is null on the certificate.");
signature = new PrivateKeySignature(DotNetUtilities.GetDsaKeyPair(dsa).Private, _digestAlgorithm);
key = dsa;
break;
case 3:
ECDsa ecdsa = certificate.GetECDsaPrivateKey() ?? throw new NotSupportedException("ECDsa private key is null on the certificate.");
signature = new EcdsaSignature(ecdsa, _digestAlgorithm);
key = ecdsa;
break;
}
foundPk = true;
}
catch (Exception e)
{
_logger.LogDebug(e, $"Private key extraction ran into an exception. Attempt (zero-based): {attempt}");
}
attempt++;
}
if (signature == null)
{
throw new NotSupportedException(
$"The private key of the certificate could not be retrieved for signing. {JsonConvert.SerializeObject(certificate.SubjectName)}");
}
return (signature, key);
}
As you can see iText 7 has a handy utilities class, that contains a lot of util functions ported from the Java version, but for some reason only a lot, and not all of them. It doesn't support ECDsa key pairs, so I had to implement my own version of that. For completeness that class is included below.
#nullable enable
using System;
using System.Security.Cryptography;
using iText.Signatures;
public class EcdsaSignature : IExternalSignature
{
private readonly string _encryptionAlgorithm;
private readonly string _hashAlgorithm;
private readonly ECDsa _pk;
private const string EcdsaEncryptionAlgorithm = "ECDSA";
public EcdsaSignature(ECDsa? pk, string hashAlgorithm)
{
_pk = pk ?? throw new ArgumentNullException(nameof(pk), "ECDSA private key cannot be null.");
_hashAlgorithm = DigestAlgorithms.GetDigest(DigestAlgorithms.GetAllowedDigest(hashAlgorithm));
_encryptionAlgorithm = EcdsaEncryptionAlgorithm;
}
/// <summary>
/// <inheritDoc />
/// </summary>
public virtual string GetEncryptionAlgorithm()
{
return _encryptionAlgorithm;
}
/// <summary>
/// <inheritDoc />
/// </summary>
public virtual string GetHashAlgorithm()
{
return _hashAlgorithm;
}
/// <summary>
/// <inheritDoc />
/// </summary>
/// <remarks>
/// <see href="https://stackoverflow.com/a/67255440/6389395"> RFC 5480 format.</see>
/// </remarks>
public virtual byte[] Sign(byte[] message)
{
return _pk.SignData(message, new HashAlgorithmName(_hashAlgorithm), DSASignatureFormat.Rfc3279DerSequence);
}
}
Also note that the ExtractPrivateKey() method returns the key as an IDisposable as well, this is so it can be disposed of after the signature is done. I could have made a public property in EcdsaSignature, but iText's own IExternalSignatures don't expose the PK either, so neither should I.
Related
I am testing the iText 7.1.2.0 library to sign pdf files, using a digital certificate or smart card (X509Certificate2) in a C # project. But I'm getting this error when I try to create the IExternalSignature.
According to the documentation found (here, here and here), the way to achieve this process is using the BouncyCastle library that allows extracting the primary keys from the digital certificate, however, it is giving me an error and I can not find another way to do it.
In the documentation (here) they are created from .pfx files, but for this case, I need to take the primary keys directly from the certificate in the card reader. In previous versions of iText, it's allowed the creation with the following command:
IExternalSignature externalSignature = new X509Certificate2Signature(Certificate, "SHA-1");
But in version 7 it is no longer available and in the documentation I do not see how to do it.
Someone has used iText 7 and has been able to sign using an X509Certificate2 that knows the correct way to create the IExternalSignature?
This is the code that I am using:
public void SignPDF(string source, string target, X509Certificate2 certificate, string reason, string location, bool addVisibleSign, bool addTimeStamp, string strTSA, int qtySigns, int pageNumber)
{
try
{
Org.BouncyCastle.X509.X509Certificate vert = Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(certificate);
X509CertificateParser objCP = new X509CertificateParser();
Org.BouncyCastle.X509.X509Certificate[] objChain = new Org.BouncyCastle.X509.X509Certificate[] { objCP.ReadCertificate(certificate.RawData) };
IList<ICrlClient> crlList = new List<ICrlClient>();
crlList.Add(new CrlClientOnline(objChain));
PdfReader objReader = new PdfReader(source);
PdfSigner objStamper = new PdfSigner(objReader, new FileStream(target, FileMode.Create), false);
ITSAClient tsaClient = null;
IOcspClient ocspClient = null;
if (addTimeStamp)
{
OCSPVerifier ocspVerifier = new OCSPVerifier(null, null);
ocspClient = new OcspClientBouncyCastle(ocspVerifier);
tsaClient = new TSAClientBouncyCastle(strTSA);
}
PdfSignatureAppearance signatureAppearance = objStamper.GetSignatureAppearance();
signatureAppearance.SetReason(reason);
signatureAppearance.SetLocation(location);
signatureAppearance.SetPageNumber(pageNumber);
signatureAppearance.SetRenderingMode(PdfSignatureAppearance.RenderingMode.NAME_AND_DESCRIPTION);
if (addVisibleSign && qtySigns == 1)
signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(36, 20, 144, 53)).SetPageNumber(pageNumber);
else if (addVisibleSign && qtySigns == 2)
signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(160, 20, 268, 53)).SetPageNumber(pageNumber);
else if (addVisibleSign && qtySigns == 3)
signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(284, 20, 392, 53)).SetPageNumber(pageNumber);
else if (addVisibleSign && qtySigns == 4)
signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(408, 20, 516, 53)).SetPageNumber(pageNumber);
var pk = Org.BouncyCastle.Security.DotNetUtilities.GetKeyPair(certificate.PrivateKey).Private;
IExternalSignature externalSignature = new PrivateKeySignature(pk, "SHA-1");
objStamper.SignDetached(externalSignature, objChain, crlList, ocspClient, tsaClient, 0, PdfSigner.CryptoStandard.CMS);
if (objReader != null)
{
objReader.Close();
}
}
catch (Exception ex)
{
result.error = true;
result.errorMessage += "Error: " + ex.Message;
}
}
Thanks!
I do not believe that class was ported to iText 7- it is just a wrapper class.
You can see how to create a custom IExternalSignatureContainer in this example
Note that the source for the iText 5 X509Certificate2Signature can be found here
So something like this:
public class X509Certificate2Signature: IExternalSignature {
private String hashAlgorithm;
private String encryptionAlgorithm;
private X509Certificate2 certificate;
public X509Certificate2Signature(X509Certificate2 certificate, String hashAlgorithm) {
if (!certificate.HasPrivateKey)
throw new ArgumentException("No private key.");
this.certificate = certificate;
this.hashAlgorithm = DigestAlgorithms.GetDigest(DigestAlgorithms.GetAllowedDigest(hashAlgorithm));
if (certificate.PrivateKey is RSACryptoServiceProvider)
encryptionAlgorithm = "RSA";
else if (certificate.PrivateKey is DSACryptoServiceProvider)
encryptionAlgorithm = "DSA";
else
throw new ArgumentException("Unknown encryption algorithm " + certificate.PrivateKey);
}
public virtual byte[] Sign(byte[] message) {
if (certificate.PrivateKey is RSACryptoServiceProvider) {
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider) certificate.PrivateKey;
return rsa.SignData(message, hashAlgorithm);
}
else {
DSACryptoServiceProvider dsa = (DSACryptoServiceProvider) certificate.PrivateKey;
return dsa.SignData(message);
}
}
public virtual String GetHashAlgorithm() {
return hashAlgorithm;
}
public virtual String GetEncryptionAlgorithm() {
return encryptionAlgorithm;
}
}
Will replicate the class' function in iText 7. How to use the class is shown here in my first link, though you most likely will be using the signDetached() method instead of the signDeffered() method.
The certificate.PrivateKey in my experience is not reliable when you're dealing with non-RSA key pairs. For example a certificate that has an ECDsa key-pair will throw a NotSupportedException("The certificate key algorithm is not supported") on calling .PrivateKey, don't know why (actually here's why, also probably useful). So I implemented a (little janky) workaround to retrieve the private key, and create an IExternalSignature out of it. Since the code has no knowledge of what type of key a certificate has, and I couldn't be bothered to process the other properties to find out, I wrote a loop that tries for a generic, RSA, DSA and ECDsa key extraction. The generic works for RSA too, but since X509Certificate2 has a GetRSAPrivateKey() method, I made a switch case for it just in case.
#nullable enable
private (IExternalSignature? signature, IDisposable? key) ExtractPrivateKey(X509Certificate2 certificate)
{
IExternalSignature? signature = null;
bool foundPk = false;
int attempt = 0;
IDisposable? key = null;
while (foundPk is false)
{
try
{
switch (attempt)
{
case 0:
AsymmetricAlgorithm? pk = certificate.PrivateKey; //throws exception when certificate has an ECDsa key
signature = new PrivateKeySignature(DotNetUtilities.GetKeyPair(pk).Private, _digestAlgorithm);
key = pk;
break;
case 1:
RSA rsa = certificate.GetRSAPrivateKey() ?? throw new NotSupportedException("RSA private key is null on the certificate.");
signature = new PrivateKeySignature(DotNetUtilities.GetRsaKeyPair(rsa).Private, _digestAlgorithm);
key = rsa;
break;
case 2:
DSA dsa = certificate.GetDSAPrivateKey() ?? throw new NotSupportedException("Dsa private key is null on the certificate.");
signature = new PrivateKeySignature(DotNetUtilities.GetDsaKeyPair(dsa).Private, _digestAlgorithm);
key = dsa;
break;
case 3:
ECDsa ecdsa = certificate.GetECDsaPrivateKey() ?? throw new NotSupportedException("ECDsa private key is null on the certificate.");
signature = new EcdsaSignature(ecdsa, _digestAlgorithm);
key = ecdsa;
break;
}
foundPk = true;
}
catch (Exception e)
{
_logger.LogDebug(e, $"Private key extraction ran into an exception. Attempt (zero-based): {attempt}");
}
attempt++;
}
if (signature == null)
{
throw new NotSupportedException(
$"The private key of the certificate could not be retrieved for signing. {JsonConvert.SerializeObject(certificate.SubjectName)}");
}
return (signature, key);
}
As you can see iText 7 has a handy utilities class, that contains a lot of util functions ported from the Java version, but for some reason only a lot, and not all of them. It doesn't support ECDsa key pairs, so I had to implement my own version of that. For completeness that class is included below.
#nullable enable
using System;
using System.Security.Cryptography;
using iText.Signatures;
public class EcdsaSignature : IExternalSignature
{
private readonly string _encryptionAlgorithm;
private readonly string _hashAlgorithm;
private readonly ECDsa _pk;
private const string EcdsaEncryptionAlgorithm = "ECDSA";
public EcdsaSignature(ECDsa? pk, string hashAlgorithm)
{
_pk = pk ?? throw new ArgumentNullException(nameof(pk), "ECDSA private key cannot be null.");
_hashAlgorithm = DigestAlgorithms.GetDigest(DigestAlgorithms.GetAllowedDigest(hashAlgorithm));
_encryptionAlgorithm = EcdsaEncryptionAlgorithm;
}
/// <summary>
/// <inheritDoc />
/// </summary>
public virtual string GetEncryptionAlgorithm()
{
return _encryptionAlgorithm;
}
/// <summary>
/// <inheritDoc />
/// </summary>
public virtual string GetHashAlgorithm()
{
return _hashAlgorithm;
}
/// <summary>
/// <inheritDoc />
/// </summary>
/// <remarks>
/// <see href="https://stackoverflow.com/a/67255440/6389395"> RFC 5480 format.</see>
/// </remarks>
public virtual byte[] Sign(byte[] message)
{
return _pk.SignData(message, new HashAlgorithmName(_hashAlgorithm), DSASignatureFormat.Rfc3279DerSequence);
}
}
Also note that the ExtractPrivateKey() method returns the key as an IDisposable as well, this is so it can be disposed of after the signature is done. I could have made a public property in EcdsaSignature, but iText's own IExternalSignatures don't expose the PK either, so neither should I.
I have a problem when I try to decrypt a string generated using RSA in C#. It Encrypts the strings well, but it throws an error when I try to decrypt the string using the private key:
Error occurred while decoding OAEP padding.
I tried changing the fOAEP parameter between true and false, changing the RSACryptoServiceProviders with 2048 as parameter and still doesn't work.
Project is a WPF App which generates 2 files with the keys, keys are loaded by the core.cs file.
Then keys are being loaded.
The example I taken only uses the public key to encrypt the string and only the private key to decrypt the string.
Core.cs File
// Keys are generated
public void GeneratePublicKey(string publicKeyFile) {
using (var rsa = new RSACryptoServiceProvider(2048)) {
rsa.PersistKeyInCsp = false;
if (File.Exists(publicKeyFile))
File.Delete(publicKeyFile);
//and the public key ...
var pubKey = rsa.ExportParameters(false);
//converting the public key into a string representation
string pubKeyString; {
//we need some buffer
var sw = new System.IO.StringWriter();
//we need a serializer
var xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters));
//serialize the key into the stream
xs.Serialize(sw, pubKey);
//get the string from the stream
pubKeyString = sw.ToString();
}
File.WriteAllText(publicKeyFile, pubKeyString);
}
}
public void GeneratePrivateKey(string privateKeyFile) {
using (var rsa = new RSACryptoServiceProvider(2048)) {
rsa.PersistKeyInCsp = false;
if (File.Exists(privateKeyFile))
File.Delete(privateKeyFile);
//how to get the private key
var privKey = rsa.ExportParameters(true);
//converting the public key into a string representation
string privKeyString;
{
//we need some buffer
var sw = new System.IO.StringWriter();
//we need a serializer
var xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters));
//serialize the key into the stream
xs.Serialize(sw, privKey);
//get the string from the stream
privKeyString = sw.ToString();
}
File.WriteAllText(privateKeyFile, privKeyString);
}
}
//Si las llaves ya existen entonces NO Generar, solo leer la llave indicada (leer el texto de la ruta)
public RSAParameters ReadPublicKey(string publicKeyFile) {
//Leer
string pubKeyString = File.ReadAllText(publicKeyFile);
//Reconvertir
var sr = new System.IO.StringReader(pubKeyString);
//we need a deserializer
var xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters));
//get the object back from the stream
return (RSAParameters)xs.Deserialize(sr);
}
public RSAParameters ReadPrivateKey(string privateKeyFile) {
//Leer
string privKeyString = File.ReadAllText(privateKeyFile);
//Reconvertir
var sr = new System.IO.StringReader(privKeyString);
//we need a deserializer
var xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters));
//get the object back from the stream
return (RSAParameters)xs.Deserialize(sr);
}
//Con la llave publica se encripta el texto
public string Encrypt(string publicKeyFile, string textToEncrypt) {
var csp = new RSACryptoServiceProvider();
csp.ImportParameters(ReadPublicKey(publicKeyFile));
//for encryption, always handle bytes...
var bytesPlainTextData = System.Text.Encoding.Unicode.GetBytes(textToEncrypt);
//apply pkcs#1.5 padding and encrypt our data
var bytesCypherText = csp.Encrypt(bytesPlainTextData, true);
//we might want a string representation of our cypher text... base64 will do
Debug.WriteLine("Texto Encriptado: "+Convert.ToBase64String(bytesCypherText));
return Convert.ToBase64String(bytesCypherText);
}
/// <summary>
/// Con la llave Privada se Desencripta
/// </summary>
/// <param name="privateKeyFile"></param>
/// <param name="textToDecrypt"></param>
/// <returns></returns>
public string Decrypt(string privateKeyFile, string textToDecrypt) {
//first, get our bytes back from the base64 string ...
var bytesCypherText = Convert.FromBase64String(textToDecrypt);
//we want to decrypt, therefore we need a csp and load our private key
var csp = new RSACryptoServiceProvider();
csp.ImportParameters(ReadPrivateKey(privateKeyFile));
//decrypt and strip pkcs#1.5 padding
var bytesPlainTextData = csp.Decrypt(bytesCypherText, true);
Debug.WriteLine("Desencriptado: "+
System.Text.Encoding.Unicode.GetString(bytesPlainTextData));
//get our original plainText back...
return System.Text.Encoding.Unicode.GetString(bytesPlainTextData);
}
MainWindow.cs
public partial class MainWindow : Window {
readonly RsaEnc _rs = new RsaEnc();
private string _publicKeyFile = "./public.cert";
private string _privateKeyFile = "./private.key";
public MainWindow() {
InitializeComponent();
}
private void GenerateBtn_Click(object sender, RoutedEventArgs e) {
_rs.GeneratePublicKey(_publicKeyFile);
_rs.GeneratePrivateKey(_privateKeyFile);
}
private void OpenPublic_Click(object sender, RoutedEventArgs e) {
OpenFileDialog fileDialog = new OpenFileDialog {
Multiselect = false, Filter = "Public |*.cert", DefaultExt = ".cert"
};
bool? dialogOk = fileDialog.ShowDialog();
if (dialogOk == true) {
string sFilenames = "";
foreach (string sFileName in fileDialog.FileNames) {
sFilenames += ";" + sFileName;
}
sFilenames = sFilenames.Substring(1);
_publicKeyFile = sFilenames;//Esto solo da la RUTA
Debug.WriteLine("public Cert: " + _publicKeyFile);
}
}
private void OpenPrivate_Click(object sender, RoutedEventArgs e) {
OpenFileDialog fileDialog = new OpenFileDialog
{
Multiselect = false, Filter = "Certificates |*.key", DefaultExt = ".key"
};
bool? dialogOk = fileDialog.ShowDialog();
if (dialogOk == true) {
string sFilenames = "";
foreach (string sFileName in fileDialog.FileNames) {
sFilenames += ";" + sFileName;
}
sFilenames = sFilenames.Substring(1);
_privateKeyFile = sFilenames; //Esto solo da la RUTA
Debug.WriteLine("private Key: " + _privateKeyFile);
}
}
private void EncryptBtn_Click(object sender, RoutedEventArgs e) {
_rs.Encrypt(_publicKeyFile, BoxToEncrypt.Text);
}
private void DecryptBtn_Click(object sender, RoutedEventArgs e) {
_rs.Decrypt(_privateKeyFile, BoxToDecrypt.Text);
}
}
First thing, WPF and Decryption are two different things. So, we have to concentrate only on the RSA encrypt/decrypt part.
In your code, you are trying to generate public/private keys using different methods. One single rsaprovider should export both parameters. I have provided a sample code for using RSA encryption/decryption. Please check below.
internal sealed class RSA
{
public static (string public_key, string private_key) getKeyPair()
{
try
{
var rsa_provider = new RSACryptoServiceProvider(1024);
return (rsa_provider.ToXmlString(false), rsa_provider.ToXmlString(true));
}
catch (Exception ex)
{
throw ex;
}
}
public static byte[] shroud_divulge(byte[] input_byte, string _key,bool is_shroud)
{
try
{
var rsa_provider = new RSACryptoServiceProvider();
rsa_provider.FromXmlString(_key);
var padding = RSAEncryptionPadding.OaepSHA256;
switch(is_shroud)
{
case true:
return rsa_provider.Encrypt(input_byte, padding);
case false:
return rsa_provider.Decrypt(input_byte, padding);
}
return null;
}
catch (Exception)
{
throw;
}
}
}
It has two simple methods and returns output using valuetuple(So add Valutuple nuget package).
The file saving and processing are done outside of the RSA logic. Shroud- Encryption, Divulge-Decryption. (Please excuse the naming conventions used, its as per our company standards).
We usually ship our publickey along with our WPF application.
For encryption (Shroud), we pass in the private key..
For decryption (Divulge) , we pass in the publich key..
RSA keys are always generated in pairs, a public and a private. These keys are created in the constructor of RSACryptoServiceProvider. Your code calls this constructor in both GeneratePublicKey and GeneratePrivateKey, therefore there will be two unrelated key-pairs generated.
You should be able to check this. When exporting parameters the public component is always included. If they are not identical that that would serve as confirmation.
The fix would be to generate the public and private key at the same time.
Is there a simple way to check if a given AsymmetricAlgorithm is a private or a public key? Consider the following example:
private void SavePrivateKey(AsymmetricAlgorithm asymmetricAlgorithm)
{
// if (asymmetricAlgorithm.IsPrivateKey == false)
// throw new ArgumentException();
}
private void SavePrivateKeys()
{
var certificate = CreateCertificate();
var privateKey = RSACertificateExtensions.GetRSAPrivateKey(certificate);
var publicKey = RSACertificateExtensions.GetRSAPublicKey(certificate);
SavePrivateKey(privateKey);
SavePrivateKey(publicKey); // this should throw an exception
}
private X509Certificate2 CreateCertificate()
{
CngKeyCreationParameters keyParams = new CngKeyCreationParameters();
keyParams.KeyUsage = CngKeyUsages.Signing;
keyParams.Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider;
keyParams.ExportPolicy = CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport;
keyParams.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(2048), CngPropertyOptions.None));
var cngKey = CngKey.Create(CngAlgorithm.Rsa, Guid.NewGuid().ToString(), keyParams);
var rsaKey = new RSACng(cngKey);
var req = new CertificateRequest("cn=mycert", rsaKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
return cert;
}
Here, both private and public keys have the same type - RSACng. In theory I can try to export key parameters and see if the public key fails to export the private key params. But then I would not know if the export failed because it is a public key, or because it is missing export policies or something else went wrong. Also the underlying key type may be different, it could be RSACng, RSACryptoServiceProvider, DSA etc.
The ToXmlString() method takes in the includePrivateParameters parameter. If that parameter is set, and the AsymmetricAlgorithm object does not include information about the private key, ToXmlString() will throw a CryptographicException exception:
private void SavePrivateKey(AsymmetricAlgorithm aa)
{
System.Console.Write("This is ");
try
{
aa.ToXmlString(true);
}
catch(CryptographicException ce)
{
System.Console.Write("not ");
}
System.Console.WriteLine("a private key");
}
I'm writing a digital signing program using C# and I use RSACryptoServiceProvider class that generates public and private keys and signatures depending on the file. If in the program, I check the signature with the public key, signature and file, it works correctly but If I save my keys to any format in the file, In other words, I will change their format and return to the first state It doesn't work. because I can not turn it into RSAParameters correctly. please guide me?
Simple example test to show the change:
var publicParams = rsaWrite.ExportParameters(false); // Generate the public key.
var testpublicParams = publicParams;
string st = Encoding.ASCII.GetString(publicParams.Modulus);
testpublicParams.Modulus = Encoding.ASCII.GetBytes(st);
if(publicParams.Modulus != testpublicParams.Modulus) {
Console.WriteLine("The key has been changed.");
}
You can get PublicKey as string format and save it on the other text file.
public static string PublicKey(string certSubject)
{
var my = new X509Store(StoreName.My, StoreLocation.LocalMachine);
my.Open(OpenFlags.ReadOnly);
RSACryptoServiceProvider csp = null;
byte[] publicKeyByte = null;
foreach (var cert in my.Certificates)
{
if (cert.Subject.Contains(certSubject))
{
csp = (RSACryptoServiceProvider)cert.PublicKey.Key;
publicKeyByte = cert.PublicKey.EncodedKeyValue.RawData;
}
}
if (csp == null)
{
throw new Exception("No valid cert was found");
}
var publicKey = new StringBuilder();
publicKey.AppendLine("-----BEGIN PUBLIC KEY-----");
publicKey.AppendLine(Convert.ToBase64String(publicKeyByte, Base64FormattingOptions.InsertLineBreaks));
publicKey.AppendLine("-----END PUBLIC KEY-----");
return publicKey.ToString();
}
This code has two problems:
The use of Encoding.ASCII.GetBytes is wrong because it might have a non-ASCII characters so we use the Convert.ToBase64String.
publicParams.Modulus is C# byte array so != probably not the right answer so we use SequenceEqual.
And the key will not change.
var rsaWrite = new RSACryptoServiceProvider();
var publicParams = rsaWrite.ExportParameters(false); // Generate the public key.
var testpublicParams = publicParams;
string st = Convert.ToBase64String(publicParams.Modulus);
testpublicParams.Modulus = Convert.FromBase64String(st);
if (!publicParams.Modulus.SequenceEqual(testpublicParams.Modulus))
{
Console.WriteLine("The key has been changed.");
}
else
{
Console.WriteLine("The key has not been changed. :D");
}
There is an issue came into my mind.
Is there any common way to make an app running only when there is an installed certificate in the system. And I want such a certificate to be issued and verified by my self-signed certificate?
I can get a certificate by it's name from the storage, but how do I make sure such a certificate is signed by my self-signed certificate and nobody have issued a certificate with the same name and replaced the one in the local storage?
Or in other words, how do I make sure the certificate which signed the certificate at local storage is not a forged one?
I'm sorry if its not correct and|or clear question, but I'll be happy to have help regarding it.
Very good question indeed.
There is always a possibility of the end-user creating a valid certificate chain with your subject name and as the issuer, another for the issuer ceritificate, all up to the root.
What they canot do is to sign those certifcates with the issuer certificate's private key.
Therefore, the code below loads the application certificate from the personal certificate store of the current user, then, loads the issuer certificate of the issuer from the resources and verifies the signature on the application certificate installed on the client machine using the public key of the issuer certificate.
In my source code, the issuer certificate is added to the resources with the key IssuerCertificate
I am actually fond of coming out with a solution like this.
In the code I mention an encoding ASN.1. Check it here if you need
static void Main(string[] args)
{
string expectedSubjectName = "My Application";
X509Certificate2 issuerCertificate = new X509Certificate2(Resource1.IssuerCertificate);
string expectedIssuerName = issuerCertificate.Subject;
bool result = VerifyCertificateIssuer(expectedSubjectName, expectedIssuerName, issuerCertificate);
}
private static void ThrowCertificateNotFoundException(string expectedSubjectName, string expectedIssuerName, bool isThumbprintMismatch)
{
if (isThumbprintMismatch)
{
// Notification for possible certificate forgery
}
throw new SecurityException("A certificate with subject name " + expectedSubjectName + " issued by " + expectedIssuerName + " is required to run this application");
}
private static X509Certificate2 GetCertificate(string expectedSubjectName, string expectedIssuerName)
{
X509Store personalCertificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
personalCertificateStore.Open(OpenFlags.ReadOnly);
X509CertificateCollection certificatesBySubjectName = personalCertificateStore.Certificates.Find(X509FindType.FindBySubjectName, expectedSubjectName, true);
if (certificatesBySubjectName.Count == 0)
{
ThrowCertificateNotFoundException(expectedSubjectName, expectedIssuerName, false);
}
X509Certificate2 matchingCertificate = null;
foreach (X509Certificate2 certificateBySubjectName in certificatesBySubjectName)
{
if (certificateBySubjectName.Issuer == expectedIssuerName)
{
matchingCertificate = certificateBySubjectName;
break;
}
}
if (matchingCertificate == null)
{
ThrowCertificateNotFoundException(expectedSubjectName, expectedIssuerName, false);
}
return matchingCertificate;
}
private static bool VerifyCertificateIssuer(string expectedSubjectName, string expectedIssuerName, X509Certificate2 issuerCertificate)
{
X509Certificate2 matchingCertificate = GetCertificate(expectedSubjectName, expectedIssuerName);
X509Chain chain = new X509Chain();
chain.Build(matchingCertificate);
// bool x = Encoding.ASCII.GetString(chain.ChainElements[1].Certificate.RawData) == Encoding.ASCII.GetString(issuerCertificate.RawData);
byte[] certificateData = matchingCertificate.RawData;
MemoryStream asn1Stream = new MemoryStream(certificateData);
BinaryReader asn1StreamReader = new BinaryReader(asn1Stream);
// The der encoded certificate structure is like this:
// Root Sequence
// Sequence (Certificate Content)
// Sequence (Signature Algorithm (like SHA256withRSAEncryption)
// Sequence (Signature)
// We need to decode the ASN.1 content to get
// Sequence 0 (which is the actual certificate content that is signed by the issuer, including the sequence definition and tag number and length)
// Sequence 2 (which is the signature. X509Certificate2 class does not give us that information. The year is 2015)
// Read the root sequence (ignore)
byte leadingOctet = asn1StreamReader.ReadByte();
ReadTagNumber(leadingOctet, asn1StreamReader);
ReadDataLength(asn1StreamReader);
// Save the current position because we will need it for including the sequence header with the certificate content
int sequence0StartPosition = (int)asn1Stream.Position;
leadingOctet = asn1StreamReader.ReadByte();
ReadTagNumber(leadingOctet, asn1StreamReader);
int sequence0ContentLength = ReadDataLength(asn1StreamReader);
int sequence0HeaderLength = (int)asn1Stream.Position - sequence0StartPosition;
sequence0ContentLength += sequence0HeaderLength;
byte[] sequence0Content = new byte[sequence0ContentLength];
asn1Stream.Position -= 4;
asn1StreamReader.Read(sequence0Content, 0, sequence0ContentLength);
// Skip sequence 1 (signature algorithm) since we don't need it and assume that we know it because we own the issuer certificate
// This sequence, containing the algorithm used during the signing process IS HIDDEN FROM US BY DEFAULT. The year is 2015.
// What should have been done for real is, to get the algorithm ID (hash algorithm and asymmetric algorithm) and to use those
// algorithms during the verification process
leadingOctet = asn1StreamReader.ReadByte();
ReadTagNumber(leadingOctet, asn1StreamReader);
int sequence1ContentLength = ReadDataLength(asn1StreamReader);
byte[] sequence1Content = new byte[sequence1ContentLength];
asn1StreamReader.Read(sequence1Content, 0, sequence1ContentLength);
// Read sequence 2 (signature)
// The actual signature of the certificate IS HIDDEN FROM US BY DEFAULT. The year is 2015.
leadingOctet = asn1StreamReader.ReadByte();
ReadTagNumber(leadingOctet, asn1StreamReader);
int sequence2ContentLength = ReadDataLength(asn1StreamReader);
byte unusedBits = asn1StreamReader.ReadByte();
sequence2ContentLength -= 1;
byte[] sequence2Content = new byte[sequence2ContentLength];
asn1StreamReader.Read(sequence2Content, 0, sequence2ContentLength);
// At last, we have the data that is signed and the signature.
bool verificationResult = ((RSACryptoServiceProvider)issuerCertificate.PublicKey.Key)
.VerifyData
(
sequence0Content,
CryptoConfig.MapNameToOID("SHA256"),
sequence2Content
);
return verificationResult;
}
private static byte[] ReadTagNumber(byte leadingOctet, BinaryReader inputStreamReader)
{
List<byte> byts = new List<byte>();
byte temporaryByte;
if ((leadingOctet & 0x1F) == 0x1F) // More than 1 byte is used to specify the tag number
{
while (((temporaryByte = inputStreamReader.ReadByte()) & 0x80) > 0)
{
byts.Add((byte)(temporaryByte & 0x7F));
}
byts.Add(temporaryByte);
}
else
{
byts.Add((byte)(leadingOctet & 0x1F));
}
return byts.ToArray();
}
private static int ReadDataLength(BinaryReader inputStreamReader)
{
byte leadingOctet = inputStreamReader.ReadByte();
if ((leadingOctet & 0x80) > 0)
{
int subsequentialOctetsCount = leadingOctet & 0x7F;
int length = 0;
for (int i = 0; i < subsequentialOctetsCount; i++)
{
length <<= 8;
length += inputStreamReader.ReadByte();
}
return length;
}
else
{
return leadingOctet;
}
}
private static byte[] GetTagNumber(byte leadingOctet, BinaryReader inputStreamReader, ref int readBytes)
{
List<byte> byts = new List<byte>();
byte temporaryByte;
if ((leadingOctet & 0x1F) == 0x1F) // More than 1 byte is used to specify the tag number
{
while (((temporaryByte = inputStreamReader.ReadByte()) & 0x80) > 0)
{
readBytes++;
byts.Add((byte)(temporaryByte & 0x7F));
}
byts.Add(temporaryByte);
}
else
{
byts.Add((byte)(leadingOctet & 0x1F));
}
return byts.ToArray();
}