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.
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:
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");
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:
openssl cms -verify -binary -inform DER -in release.txt.p7s -content release.txt \ -CAfile signer.pem -purpose any -out /dev/null 2>&1
CMS Verification successfulLimits
- 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.
Verifytakes 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). - 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).
Every rule, limit and the OpenSSL test matrix: Cryptographic Message Syntax. To encrypt rather than sign: Encrypt and decrypt with CMS.