I want to use a Hardware Security Module (HSM) to sign a string. Specifically, I am using the ePass3003Auto
to do so. I have stored the certificate in the HSM, which I have it's private key. Using the Pkcs11Interop
package and the CKM.CKM_SHA256_RSA_PKCS
mechanism, I get the result X
. To verify the accuracy of the signing process, I also use the RSACryptoServiceProvider
to sign the same data using HashAlgorithmName.SHA256
and RSASignaturePadding.Pkcs1
arguments, which produces the result Y
.
What have I done so far?
RSACryptoServiceProvider
is identical to the one stored in the HSM. To further ensure this, I tested both methods using SHA1
and got the same signatures, indicating that the keys are indeed the same.byte
arrays passed to the signing methods are also the same.x86
and x64
configurations, but as the HSM only has one library and there is no separate library for different architectures, there was no difference in the results.Y
. Therefore, it appears that the RSACryptoServiceProvider
is generating the correct signature, and my approach using Pkcs11Interop
is incorrect.Here is a simplified version of my code:
using Net.Pkcs11Interop.Common;
using Net.Pkcs11Interop.HighLevelAPI;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace TestSigningProblem
{
internal class Program
{
internal static string SignUsingHSM(string data, string tokenId, string pin)
{
const string modulePath = @"C:\Windows\System32\ShuttleCsp11_3003.dll";
var factories = new Pkcs11InteropFactories();
using (var pkcs11Library = factories.Pkcs11LibraryFactory.LoadPkcs11Library(factories, modulePath, AppType.MultiThreaded))
{
// Find first slot with token present
var slot = pkcs11Library.GetSlotList(SlotsType.WithOrWithoutTokenPresent).First();
// Open RW session
using (var session = slot.OpenSession(SessionType.ReadWrite))
{
// Login as normal user
session.Login(CKU.CKU_USER, pin);
// Specify signing mechanism
var mechanism = session.Factories.MechanismFactory.Create(CKM.CKM_SHA256_RSA_PKCS);
var sourceData = Encoding.UTF8.GetBytes(data);
var pkcs11UriBuilder = new Pkcs11UriBuilder
{
ModulePath = modulePath,
PinValue = pin,
Type = CKO.CKO_PRIVATE_KEY
};
var searchTemplate = Pkcs11UriUtils.GetObjectAttributes(new Pkcs11Uri(pkcs11UriBuilder.ToString()), session.Factories.ObjectAttributeFactory);
var allObjects = session.FindAllObjects(searchTemplate);
var foundObject = allObjects.FirstOrDefault(x => session.GetAttributeValue(x, new List<CKA>() { CKA.CKA_ID }).First().GetValueAsString().StartsWith(tokenId, StringComparison.OrdinalIgnoreCase));
if (foundObject == null)
{
throw new Exception("Certificate not found");
}
// Sign data
var signature = session.Sign(mechanism, foundObject, sourceData);
session.Logout();
return ConvertUtils.BytesToBase64String(signature);
}
}
}
internal static string SignUsingPrivateKey(string data, string privateKey)
{
var pem = $"-----BEGIN PRIVATE KEY-----\n{privateKey}\n-----END PRIVATE KEY-----"; // Add header and footer
var pemReader = new PemReader(new StringReader(pem));
var rsaParams = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)pemReader.ReadObject());
var cryptoServiceProvider = new RSACryptoServiceProvider();
cryptoServiceProvider.ImportParameters(rsaParams);
var dataBytes = Encoding.UTF8.GetBytes(data);
return Convert.ToBase64String(cryptoServiceProvider.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
}
static void Main(string[] args)
{
var data = "T";
var privateKey = File.ReadAllText(@"private.key");
Console.WriteLine(SignUsingHSM(data, "********************************", "********"));
Console.WriteLine(SignUsingPrivateKey(data, privateKey));
Console.ReadKey();
}
}
}
For others who may have similar problems, I post my working code based on the Pkcs11Interop.X509Store
library which was suggested by jariq in comments. This package is dependent on Pkcs11Interop
library which allows high level operations.
public string Sign(string data, string serialNumber)
{
var libraryPath = @"C:\Windows\System32\ShuttleCsp11_3003.dll";
using (var store = new Pkcs11X509Store(libraryPath, new UiPinProvider()))
{
var certificate = FindCertificateBySerialNumber(store, serialNumber) ?? throw new Exception("Certificate not found");
var rsa = certificate.GetRSAPrivateKey();
using (var sha256 = SHA256.Create())
{
var binaryData = Encoding.UTF8.GetBytes(data);
var hash = sha256.ComputeHash(binaryData);
var formatter = new RSAPKCS1SignatureFormatter(rsa);
formatter.SetHashAlgorithm("SHA256");
byte[] signature = formatter.CreateSignature(hash);
return Convert.ToBase64String(signature);
}
}
}
public Pkcs11X509Certificate FindCertificateBySerialNumber(Pkcs11X509Store store, string serialNumber)
{
foreach (var slot in store.Slots)
{
if (slot.Token == null)
continue;
if (!slot.Token.Info.Initialized)
continue;
foreach (var certificate in slot.Token.Certificates)
{
if (certificate.Info?.ParsedCertificate?.SerialNumber?.Equals(serialNumber, StringComparison.OrdinalIgnoreCase) == true)
{
return certificate;
}
}
}
return null;
}