Precomputed hashes, RSA signatures and CMS signed attributes
Illustrative code, not executed.
Original English publication date: 2022-09-01T12:41:00.005+03:00. Original English article
The archived article adapts a Java/Bouncy Castle example to a .NET certificate-backed signer. There are two different hashes to keep separate: the hash of the content and, when CMS signed attributes are present, the hash of their DER encoding. Calling SignHash on a content digest produces a raw RSA signature; it does not by itself produce a CMS SignedData object. [1][2]
A raw signature over a known SHA-256 digest
The following helper accepts exactly 32 digest bytes and an explicitly supplied certificate. The padding choice must match the receiving protocol; PKCS#1 v1.5 here matches the archived sha256WithRSAEncryption intent. No certificate store is searched, and the returned RSA handle is disposed. Neither this helper nor any other key operation in the article was executed. [1][5]
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
public static class PrecomputedRsaExample
{
public static byte[] SignSha256Digest(byte[] digest, X509Certificate2 certificate)
{
if (digest == null || digest.Length != 32)
throw new ArgumentException("A SHA-256 digest is exactly 32 bytes.", nameof(digest));
if (certificate == null) throw new ArgumentNullException(nameof(certificate));
using RSA rsa = certificate.GetRSAPrivateKey()
?? throw new CryptographicException("An accessible RSA private key is required.");
return rsa.SignHash(digest, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}Repair the streaming boundary
ISigner.BlockUpdate receives an offset and a count and may be called repeatedly. The archived Take(length) ignores the offset; signing each block overwrites the previous result. A signature factory must consume every byte once, in order, and sign only at finalization. The standalone accumulator below illustrates that boundary without pretending to be a version-complete Bouncy Castle ISigner implementation. Its caller owns the RSA object. [3]
// Add to a file with using System and System.Security.Cryptography.
public sealed class Sha256SigningAccumulator : IDisposable
{
private readonly IncrementalHash hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
private bool finished;
public void Append(byte[] input, int offset, int count)
{
if (finished) throw new InvalidOperationException("Already finalized.");
if (input == null) throw new ArgumentNullException(nameof(input));
if (offset < 0 || count < 0 || offset > input.Length - count)
throw new ArgumentOutOfRangeException(nameof(offset));
hash.AppendData(input, offset, count);
}
public byte[] Finish(RSA rsa)
{
if (finished) throw new InvalidOperationException("Already finalized.");
if (rsa == null) throw new ArgumentNullException(nameof(rsa));
finished = true;
byte[] digest = hash.GetHashAndReset();
try { return rsa.SignHash(digest, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }
finally { CryptographicOperations.ZeroMemory(digest); }
}
public void Dispose() { finished = true; hash.Dispose(); }
}CMS is a separate construction
For detached CMS with signed attributes, bind messageDigest to the actual external content, include the matching contentType, DER-encode the signed-attribute SET and sign those encoded attribute bytes. Do not put an empty byte array into encapsulated content while supplying a digest of a different document. The original Generate(..., empty, true) therefore cannot represent that document correctly. A digest-only workflow must use a deliberate detached-content/precomputed-digest integration supported by the chosen library version; the accumulator must then receive the signed-attribute encoding, not the content digest. [2][4]
Use the library’s ordinary content-based CMS path when the content is available. A production adapter must implement its complete version-specific lifecycle, including initialization, reset, single-byte updates and any span methods, and must never silently ignore unsupported operations. Test empty input, nonzero offsets, many chunks, altered content and independent CMS verification before use. These are proposed checks, not reported test results. [3]
Including certificates is not a trust decision. The recipient must validate the signer for the intended purpose under its trust and revocation policy. Do not ignore X509Chain.Build failure, disable revocation as a production shortcut, or treat a verified RSA signature as proof that the certificate is trusted. [2]
Primary references
- Microsoft RSA.SignHash
- RFC 5652 sections 5.2–5.6 and 11
- Bouncy Castle ISigner source
- Bouncy Castle CmsProcessableByteArray source
- Microsoft GetRSAPrivateKey
Original links in historical context
Original links are retained for attribution. They do not endorse the old code; current availability was not verified.