---
title: "Sign a file with CMS (PKCS#7) in C#"
url: https://recursiveai.org/onecrypto/cookbook/sign-file-cms/
description: "Sign a file with CMS (PKCS#7, .p7s) in C# and verify the signature: a detached signature with an ECDSA certificate, a per-rule verification report, and the same file checked by openssl cms. Runnable code with OneCrypto 0.1.0-preview.1."
version: "0.1.0-preview.1"
---

# Sign a file with CMS (PKCS#7) in C#

`SignedData.Create` writes a CMS signature, the format behind `.p7s` files, S/MIME and signed PDFs, and `SignedData.Verify` checks one and reports each rule it applied. The program below signs a file with a certificate and key made by OpenSSL, writes a detached `.p7s`, and verifies it.

```shell
dotnet new console -o CmsSign && cd CmsSign
dotnet add package RecursiveAI.OneCrypto --version 0.1.0-preview.1
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 365 \
  -subj "/CN=Release Signing" -addext "keyUsage=critical,digitalSignature" \
  -keyout signer-key.pem -out signer.pem
echo "release 1.4.2" > release.txt
```

OneCrypto is a preview, so `dotnet add package` needs `--version` or `--prerelease`. Put this in `Program.cs` and run `dotnet run`:

```csharp
using System.Security.Cryptography;
using RecursiveAI.OneCrypto;

using LoadedItems signerFile = Crypto.Load("signer.pem");
using LoadedItems keyFile = Crypto.Load("signer-key.pem");
Certificate cert = signerFile.Certificates[0];

// Sign: a detached signature carries the certificate but not the file itself
byte[] content = File.ReadAllBytes("release.txt");
var signer = new CmsSignerSpec
{
    Certificate = cert,
    Key = keyFile.PrivateKeys[0].Signing,
};
var options = new SignedDataOptions { Detached = true };
SignedData signed = SignedData.Create(content, [signer], options);
File.WriteAllBytes("release.txt.p7s", signed.Encoded.ToArray());
Console.WriteLine($"Wrote release.txt.p7s ({signed.Encoded.Length} bytes)");

// Verify: parse the .p7s and check it against the file
SignedData p7s = SignedData.Parse(File.ReadAllBytes("release.txt.p7s"));
SignedDataReport report = p7s.Verify(File.ReadAllBytes("release.txt"));
SignerReport s = report.Signers[0];
foreach (CmsCheck check in s.Checks)
    Console.WriteLine($"  {check.Rule,-20} {check.Status}");

// The report proves the signature matches the certificate inside the .p7s.
// Whether you trust that certificate is your decision: here, compare it with
// the one you expect, by fingerprint.
byte[] expected = cert.ComputeFingerprint(HashAlgorithmName.SHA256);
bool pinned = s.Certificate is not null && CryptographicOperations.FixedTimeEquals(
    s.Certificate.ComputeFingerprint(HashAlgorithmName.SHA256), expected);
Console.WriteLine($"Signature valid: {report.IsValid}, expected signer: {pinned}");

File.AppendAllText("release.txt", "tampered\n");
bool stillValid = p7s.Verify(File.ReadAllBytes("release.txt")).IsValid;
Console.WriteLine($"After tampering: {stillValid}");
File.WriteAllText("release.txt", "release 1.4.2\n");
```

```text
Wrote release.txt.p7s (822 bytes)
  signer-certificate   Passed
  digest-algorithm     Passed
  attributes           Passed
  content-type         Passed
  message-digest       Passed
  signature            Passed
  algorithm-protection NotApplicable
  key-usage            Passed
  key-strength         Passed
  signing-certificate  Passed
Signature valid: True, expected signer: True
After tampering: False
```

Each check names a rule from RFC 5652 or a related RFC (`Reference` gives the clause, `Reason` the detail when it fails). Leave `Detached` off to embed the content in the message; `p7s.EncapsulatedContent` then returns it. RSA, Ed25519 and Ed448 keys sign the same way, and `SignStream` and `VerifyStream` handle files larger than memory.

## Check it with OpenSSL

OpenSSL verifies the same `.p7s`. Here it uses the signer’s own certificate as the trusted one:

```shell
openssl cms -verify -binary -inform DER -in release.txt.p7s -content release.txt \
  -CAfile signer.pem -purpose any -out /dev/null 2>&1
```

```text
CMS Verification successful
```

## Limits

- **Not audited.** This library has not been independently audited. It is a preview: 0.1.0-preview.1, and the API can change.
- **Trust is not checked by default.** `Verify` takes no clock and builds no certificate chain: a valid report says the signature is sound for the certificate in the message, nothing about who issued it. Check the signer yourself, as above. OneCrypto has an RFC 5280 path validator (`SignedDataVerifyOptions.Chain`), but it is a preview: the project says it is “not yet recommended for trust decisions”, so don’t rely on it to decide whom to trust yet ([Path validation](https://recursiveai.org/onecrypto/docs/validation/)).
- **Speed.** Against BouncyCastle 2.7.0, CMS signing takes 0.18 of its time and verifying 0.33 on x64 (geometric means; 0.34 and 0.18 on arm64), from the project’s benchmark table. ECDSA P-256 verification is the slowest row, 1.66× for a 1 KiB message on x64, because it calls the platform’s ECDSA ([Conformance](https://recursiveai.org/onecrypto/docs/conformance/)).

Every rule, limit and the OpenSSL test matrix: [Cryptographic Message Syntax](https://recursiveai.org/onecrypto/docs/cms/). To encrypt rather than sign: [Encrypt and decrypt with CMS](https://recursiveai.org/onecrypto/cookbook/encrypt-file-cms/).
