PgpMessageWriter.SignDetached writes a detached signature (a .sig or .asc next to the file) and SignCleartext writes the readable -----BEGIN PGP SIGNED MESSAGE----- form; PgpMessageReader.VerifyDetached and VerifyCleartext check them against a certificate (public key). The program below makes a test key on its first run, signs a small file both ways, verifies both, and shows a changed file failing.
dotnet new console -o PgpSign && cd PgpSign dotnet add package RecursiveAI.OneCrypto.OpenPgp --version 0.3.0-preview.2
OneCrypto is a preview, so dotnet add package needs --version or --prerelease. Put this in Program.cs and run dotnet run:
using System.Text; using RecursiveAI.OneCrypto.OpenPgp; DateTimeOffset now = DateTimeOffset.UtcNow; // The key's password: read it from your secret store byte[] password = Encoding.UTF8.GetBytes("demo password"); // A test key, made on the first run: version 4 Curve25519, a key GnuPG 2.4 can read if (!File.Exists("alice.key.asc")) { PgpCertificate created = PgpKeyGenerator.Generate(new PgpKeyGenerationOptions { Now = now, UserIds = ["Alice <[email protected]>"], Profile = PgpKeyProfile.Rfc4880Curve25519, Password = password, }); // The secret key stays private; the certificate (public key) is what you share File.WriteAllBytes("alice.key.asc", created.Export(armor: true)); File.WriteAllBytes("alice.pub.asc", created.ExportPublic(armor: true)); } PgpCertificate certificate = PgpCertificate.Parse(File.ReadAllBytes("alice.pub.asc")); PgpCertificate secretKey = PgpCertificate.Parse(File.ReadAllBytes("alice.key.asc")); File.WriteAllText("notes.txt", "Release 2.1 is approved for Friday.\n"); // Sign: unlock the signing key, then write a detached and a cleartext signature PgpKey signingKey = secretKey.FindSigningKey(now) ?? throw new InvalidOperationException("no signing key"); using (PgpSecretKey signer = PgpSecretKey.Unlock(signingKey, [password])) { var options = new PgpWriteOptions { Now = now, Signers = [signer], Armor = true }; using (FileStream data = File.OpenRead("notes.txt")) using (FileStream sig = File.Create("notes.txt.sig")) PgpMessageWriter.SignDetached(data, sig, options); using (FileStream text = File.OpenRead("notes.txt")) using (FileStream signedText = File.Create("notes.txt.asc")) PgpMessageWriter.SignCleartext(text, signedText, options); } Console.WriteLine("Signed: notes.txt.sig (detached), notes.txt.asc (cleartext)"); // Verify with the certificate alone: our two files, or the files named on the command line var verify = new PgpReadOptions { Now = now, Certificates = [certificate] }; foreach (string path in args.Length > 0 ? args : ["notes.txt.sig", "notes.txt.asc"]) { PgpReadResult result; if (File.ReadAllText(path).StartsWith("-----BEGIN PGP SIGNED MESSAGE-----")) { using FileStream input = File.OpenRead(path); result = PgpMessageReader.VerifyCleartext(input, Stream.Null, verify); } else { using FileStream data = File.OpenRead("notes.txt"); using FileStream sig = File.OpenRead(path); result = PgpMessageReader.VerifyDetached(data, sig, verify); } foreach (PgpSignatureResult s in result.Signatures) Console.WriteLine($"{path}: {s.Status}, key {Convert.ToHexString(s.Signer?.Fingerprint ?? [])}, {s.CreatedAt:u}"); } // A changed file no longer matches the detached signature using (var changed = new MemoryStream(Encoding.UTF8.GetBytes("Release 2.1 is approved for Monday.\n"))) using (FileStream sig = File.OpenRead("notes.txt.sig")) Console.WriteLine($"Changed file: {PgpMessageReader.VerifyDetached(changed, sig, verify).Signatures[0].Status}");
Signed: notes.txt.sig (detached), notes.txt.asc (cleartext) notes.txt.sig: Valid, key 45D7DAFC79B0B842B5CC8535B038DBD58D86E274, 2026-11-02 06:53:35Z notes.txt.asc: Valid, key 45D7DAFC79B0B842B5CC8535B038DBD58D86E274, 2026-11-02 06:53:35Z Changed file: Invalid
A detached signature leaves the file untouched; a cleartext signature wraps the text so it stays readable, which suits emails and release notes. notes.txt.asc looks like this:
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 Release 2.1 is approved for Friday. -----BEGIN PGP SIGNATURE----- wnUEARYKAB0FAmroGCsWIQQ6eH3bqs4+LATms5QTP7ulwAqjEgAKCRATP7ulwAqj Ej9uAP0U0dVXSGoV9ZK78bS3WF55m0yuTfJbja7Xj1G06PMsCAEAxwgx53wc9V0P M+Mlu6vC3Xt3BQd5/cIkCkeebiz79AE= =0thh -----END PGP SIGNATURE-----
Valid means three things: the signature matches the data, the key was valid when it signed (bound, not expired, not revoked), and the policy accepts its hash and key size. Otherwise the status says which failed: Invalid, UnknownKey (no certificate you passed has the key), KeyNotValid, Rejected or Unsupported, with Reasons. To sign and encrypt in one message, set both Signers and Recipients on PgpMessageWriter.Write.
Check it with GnuPG
GnuPG 2.4.8, with only the certificate imported, verifies both signatures. GNUPGHOME points gpg at a new, empty home for this terminal session, so nothing here touches your own keyring; the later gpg commands on this page use the same home.
export GNUPGHOME="$(mktemp -d)" gpg --batch --import alice.pub.asc gpg --verify notes.txt.sig notes.txt gpg --verify - < notes.txt.asc
gpg: keybox '/tmp/tmp.lDgjuE16rT/pubring.kbx' created gpg: /tmp/tmp.lDgjuE16rT/trustdb.gpg: trustdb created gpg: key B038DBD58D86E274: public key "Alice <[email protected]>" imported gpg: Total number processed: 1 gpg: imported: 1 gpg: Signature made Mon 02 Nov 2026 06:53:35 AM UTC gpg: using EDDSA key 45D7DAFC79B0B842B5CC8535B038DBD58D86E274 gpg: Good signature from "Alice <[email protected]>" [unknown] gpg: WARNING: This key is not certified with a trusted signature! gpg: There is no indication that the signature belongs to the owner. Primary key fingerprint: 45D7 DAFC 79B0 B842 B5CC 8535 B038 DBD5 8D86 E274 gpg: Signature made Mon 02 Nov 2026 06:53:35 AM UTC gpg: using EDDSA key 45D7DAFC79B0B842B5CC8535B038DBD58D86E274 gpg: Good signature from "Alice <[email protected]>" [unknown] gpg: WARNING: This key is not certified with a trusted signature! gpg: There is no indication that the signature belongs to the owner. Primary key fingerprint: 45D7 DAFC 79B0 B842 B5CC 8535 B038 DBD5 8D86 E274
GnuPG’s warning is the honest one, and it applies to the program as well: the signature is good for this certificate, and nothing yet says the certificate is Alice’s. And the other way: with the secret key imported, GnuPG signs the file both ways and the program verifies its signatures.
gpg --batch --pinentry-mode loopback --passphrase 'demo password' --import alice.key.asc gpg --batch --pinentry-mode loopback --passphrase 'demo password' --armor \ --output notes.txt.gpg.sig --detach-sign notes.txt gpg --batch --pinentry-mode loopback --passphrase 'demo password' \ --output notes.gpg.asc --clearsign notes.txt dotnet run -- notes.txt.gpg.sig notes.gpg.asc
gpg: key B038DBD58D86E274: "Alice <[email protected]>" not changed gpg: key B038DBD58D86E274: secret key imported gpg: Total number processed: 1 gpg: unchanged: 1 gpg: secret keys read: 1 gpg: secret keys imported: 1 Signed: notes.txt.sig (detached), notes.txt.asc (cleartext) notes.txt.gpg.sig: Valid, key 45D7DAFC79B0B842B5CC8535B038DBD58D86E274, 2026-11-02 06:53:35Z notes.gpg.asc: Valid, key 45D7DAFC79B0B842B5CC8535B038DBD58D86E274, 2026-11-02 06:53:36Z Changed file: Invalid
Sequoia’s sqop 0.38.0 agrees in both directions:
printf 'demo password' > password.txt sqop verify notes.txt.sig alice.pub.asc < notes.txt sqop sign --with-key-password=password.txt alice.key.asc < notes.txt > notes.txt.sqop.sig dotnet run -- notes.txt.sqop.sig
2026-11-02T06:53:38Z 45D7DAFC79B0B842B5CC8535B038DBD58D86E274 45D7DAFC79B0B842B5CC8535B038DBD58D86E274 mode:binary {"signers":["alice.pub.asc"]} Signed: notes.txt.sig (detached), notes.txt.asc (cleartext) notes.txt.sqop.sig: Valid, key 45D7DAFC79B0B842B5CC8535B038DBD58D86E274, 2026-11-02 06:53:38Z Changed file: Invalid
The key is version 4 Curve25519 because GnuPG 2.4 reads neither version 6 keys nor the native Ed25519 algorithm; Sequoia reads all of them. The table is in Encrypt and decrypt a file with PGP.
Limits
- Not audited. OneCrypto (cryptography, not cryptocurrency) has not been independently audited. It is a preview: 0.3.0-preview.2, and the API can change.
- A valid signature is not an identity. It proves the holder of the key behind the certificate you passed signed the data. Whether that key is Alice’s is a separate check: compare the fingerprint with her over another channel, or compute it from certifications with the trust models (OpenPGP trust models).
- Old hashes. Under the default
CryptoPolicy.Standard, SHA-1 data signatures made before 2031 verify with a flag inPolicy(GnuPG accepts them too); passCryptoPolicy.StrictinPolicyto reject them. SHA-224 and RIPEMD-160 signatures verify but are never written. - Not yet: SLH-DSA signatures. The post-quantum ML-DSA composites (RFC 9980) sign and verify where the platform has ML-DSA.
Every signature version, algorithm and rule: OpenPGP. To encrypt instead: Encrypt and decrypt a file with PGP.