---
title: "Sign with an HSM (PKCS#11) in C#"
url: https://recursiveai.org/onecrypto/cookbook/sign-with-hsm-pkcs11/
description: "Sign with a private key that never leaves an HSM, smart card or token, through PKCS #11 in C#: load the vendor’s module, log in, find the key by label, sign, and check the signature with OpenSSL. Run on SoftHSM2, a software token. Runnable code with OneCrypto.Keys 0.4.0-preview.1."
version: "0.4.0-preview.1"
---

# Sign with an HSM (PKCS#11) in C#

An HSM, smart card or USB token keeps its private keys inside: you send it data and get a signature back. PKCS #11 is the standard interface every vendor ships as a native library (a “module”). `RecursiveAI.OneCrypto.Keys` loads that module, logs in and turns a key on the token into an ordinary OneCrypto signing key, so CMS, X.509, OpenPGP and PDF signing can use it like a key in memory.

```shell
dotnet new console -o HsmSign && cd HsmSign
dotnet add package RecursiveAI.OneCrypto.Keys --version 0.4.0-preview.1
```

OneCrypto is a preview, so `dotnet add package` needs `--version` or `--prerelease`. This page was run on [SoftHSM2](https://github.com/softhsm/SoftHSMv2) 2.6.1, a software token that keeps its keys in files, so it can be tried without hardware (`sudo apt install softhsm2 opensc` on Debian and Ubuntu, `brew install softhsm opensc` on macOS). The commands below make a throwaway token in a temporary directory, with an ECDSA P-256 key generated inside it by OpenSC’s `pkcs11-tool`. With a real HSM, skip this step: its vendor’s tools create the key, and `PKCS11_MODULE` is the vendor’s library.

```shell
export SOFTHSM2_CONF="$(mktemp -d)/softhsm2.conf"
mkdir "${SOFTHSM2_CONF%/*}/tokens"
echo "directories.tokendir = ${SOFTHSM2_CONF%/*}/tokens" > "$SOFTHSM2_CONF"
softhsm2-util --init-token --free --label "demo token" \
  --so-pin 12345678 --pin 1234 > /dev/null

export PKCS11_MODULE=/usr/lib/softhsm/libsofthsm2.so
pkcs11-tool --module "$PKCS11_MODULE" --token-label "demo token" --login --pin 1234 \
  --keypairgen --key-type EC:prime256v1 --label "signing key" --id 01
```

```text
Key pair generated:
Private Key Object; EC
  label:      signing key
  ID:         01
  Usage:      decrypt, sign, signRecover, unwrap, derive
  Access:     sensitive, always sensitive, never extractable, local
  uri:        pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=8bbc117ebbb4411e;token=demo%20token;id=%01;object=signing%20key;type=private
Public Key Object; EC  EC_POINT 256 bits
  EC_POINT:   044104498a181ccae5a22ec16fa1f5a1eb5187442b7a1e4ec3475067f06a3f69c7849167635b3674d637996e19740c23f932e2953252268aa87aba378bc9e9c47425d3
  EC_PARAMS:  06082a8648ce3d030107 ("prime256v1" OID:"1.2.840.10045.3.1.7")
  label:      signing key
  ID:         01
  Usage:      encrypt, verify, verifyRecover, wrap, derive
  Access:     local
  uri:        pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=8bbc117ebbb4411e;token=demo%20token;id=%01;object=signing%20key;type=public
```

Put this in `Program.cs`:

```csharp
using System.Text;
using RecursiveAI.OneCrypto.Keys;
using RecursiveAI.OneCrypto.Keys.Pkcs11;

// The vendor's PKCS #11 library; SoftHSM2's on Debian and Ubuntu by default
string modulePath = Environment.GetEnvironmentVariable("PKCS11_MODULE")
    ?? "/usr/lib/softhsm/libsofthsm2.so";
string pin = "1234"; // read it from your secret store, or ask the user

using Pkcs11Module module = Pkcs11Module.Load(modulePath);

// Find the token by its label, not by slot number: slot numbers can change
Pkcs11Slot slot = module.GetSlots().First(s => s.GetTokenInfo().Label == "demo token");
Console.WriteLine($"Token: {slot.GetTokenInfo().Label} ({slot.GetTokenInfo().Model})");
using Pkcs11Session session = slot.OpenSession();
session.Login(pin);

// A handle to the key; the private key stays in the token
using Pkcs11Key key = session.OpenKey(label: "signing key");
var signer = (ISigningKey)key;
Console.WriteLine($"Key: {key.Label}, {key.GetType().Name}");

byte[] document = Encoding.UTF8.GetBytes("Invoice 2026-118: 4,200.00 EUR\n");
File.WriteAllBytes("invoice.txt", document);

// The token signs. OneCrypto hashes the data first and checks the token's
// signature against the public key before it returns it.
SignatureSpec spec = signer.DefaultSpec;
byte[] signature = signer.Sign(document, spec);
File.WriteAllBytes("invoice.sig", signature);
Console.WriteLine($"Signature: {signature.Length} bytes, {spec}");

// Anyone with the public key can check it
Console.WriteLine($"Check: {spec.Verify(key.PublicKey, document, signature).Status}");
byte[] changed = Encoding.UTF8.GetBytes("Invoice 2026-118: 9,200.00 EUR\n");
Console.WriteLine($"Changed: {spec.Verify(key.PublicKey, changed, signature).Status}");
```

```text
Token: demo token (SoftHSM v2)
Key: signing key, Pkcs11EcKey
Signature: 71 bytes, ECDSA SHA256 (Der)
Check: Valid
Changed: Invalid
```

`DefaultSpec` for a P-256 key is ECDSA with SHA-256, DER-encoded as OpenSSL expects. For an RSA key, `OpenKey` returns an `IRsaKey`, and `SignatureSpec.RsaPss(HashAlgorithmName.SHA256)` or `SignatureSpec.RsaPkcs1(HashAlgorithmName.SHA256)` picks the padding. `OpenKey` also takes the key’s ID (`id:`), and requires exactly one private key to match.

## Check it with OpenSSL

Read the public key out of the token with `pkcs11-tool`, not from the program, and let OpenSSL verify the signature with it:

```shell
pkcs11-tool --module "$PKCS11_MODULE" --token-label "demo token" \
  --read-object --type pubkey --label "signing key" -o public.der
openssl dgst -sha256 -verify public.der -keyform DER -signature invoice.sig invoice.txt
```

```text
Verified OK
```

When you’re done, delete the token: `rm -r "${SOFTHSM2_CONF%/*}"`.

## Signing for CMS, PDF and OpenPGP

The token key is an `ISigningKey`, so it goes wherever OneCrypto takes one: `CmsSignerSpec.Key` for a CMS signature ([Sign a file with CMS](https://recursiveai.org/onecrypto/cookbook/sign-file-cms/)), `X509Signer.FromKey` to issue certificates, the PDF signer ([Sign a PDF with PAdES and long-term validation](https://recursiveai.org/onecrypto/cookbook/pades-ltv-pdf-signature/)), and `PgpSecretKey.FromExternalKey(publicKey, key)` for OpenPGP. If the token is on another machine than the document, sign in two steps: prepare the signature in one process (`PreparedSignedData`, `PgpPreparedSignature`), sign its bytes where the token is, and complete it in a third; see [Keys and two-phase signing](https://recursiveai.org/onecrypto/docs/keys/).

## Limits

- **Not audited.** OneCrypto (cryptography, not cryptocurrency) has not been independently audited. It is a preview: 0.4.0-preview.1, and the API can change.
- **The key-backend glue** (PKCS #11, gpg-agent and ssh-agent) “had an internal review … it has not been independently audited” ([Key-backend review](https://recursiveai.org/onecrypto/docs/key-backend-review/)).
- **No hardware token tested.** The project tests against SoftHSM2 2.6.1 and 2.7.0; “there is no hardware token in the test set”. This page ran on SoftHSM2 2.6.1, which keeps keys in files and is no substitute for an HSM.
- **Linux and macOS, 64-bit.** PKCS #11 on Windows is refused with a `Pkcs11ModuleException`.
- A key marked CKA_ALWAYS_AUTHENTICATE (it wants the PIN again before every signature, as some smart cards do) is refused. A wrong PIN is a `Pkcs11PinException`, and the library never retries it.

Mechanisms, PIN handling, errors and what was tested: [Keys, two-phase signing and key backends](https://recursiveai.org/onecrypto/docs/keys/#pkcs-11-tokens-recursiveaionecryptokeyspkcs11-package-keys).
