Pgp.GenerateKey makes an OpenPGP key in one call: a version 4 key with an Ed25519 primary key that certifies and signs and a Cv25519 subkey for encryption, the form GnuPG, RNP, Sequoia, OpenPGP.js and GopenPGP all read. Pass PgpKeyProfile.Rfc9580 for a version 6 key (RFC 9580). The program below makes one of each, protects both with a password, and writes each as a public certificate and a secret key in armored .asc files.
dotnet new console -o PgpKeyGen && cd PgpKeyGen 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 (dotnet run -- roundtrip is for the GnuPG check further down):
using System.Text; using RecursiveAI.OneCrypto; using RecursiveAI.OneCrypto.OpenPgp; const string userId = "Alice <[email protected]>"; string password = "demo password"; // read it from your secret store DateTimeOffset now = DateTimeOffset.UtcNow; if (args is ["roundtrip"]) { RoundTrip(); return; } // The default: version 4, Ed25519 to sign, Cv25519 to encrypt. One call. PgpCertificate key = Pgp.GenerateKey(userId, password); Save("alice", key); // Version 6 (RFC 9580), Ed25519 and X25519: ask for it by profile PgpCertificate v6 = Pgp.GenerateKey(userId, PgpKeyProfile.Rfc9580, password); Save("alice-v6", v6); void Save(string name, PgpCertificate cert) { // The certificate (public key) to publish, and the secret key to keep private File.WriteAllBytes($"{name}.pub.asc", cert.ExportPublic(armor: true)); File.WriteAllBytes($"{name}.key.asc", cert.Export(armor: true)); Console.WriteLine($"{name}: {string.Join(", ", cert.UserIds)}"); foreach (PgpKey k in cert.Keys) { string role = k.IsPrimary ? "primary" : "subkey "; PgpKeyState state = k.StateAt(now); Console.WriteLine( $" {role} v{k.Packet.Version} {k.Algorithm,-11} {state.Usage}"); Console.WriteLine($" {Convert.ToHexString(k.Fingerprint)}"); } } // Use the version 4 key on GnuPG's files, and write files for GnuPG to open void RoundTrip() { PgpCertificate secret = PgpCertificate.Parse(File.ReadAllBytes("alice.key.asc")); PgpCertificate pub = PgpCertificate.Parse(File.ReadAllBytes("alice.pub.asc")); byte[] pw = Encoding.UTF8.GetBytes(password); var read = new PgpReadOptions { Now = now, SecretKeys = [secret], KeyPasswords = [pw], Certificates = [pub], }; using (FileStream input = File.OpenRead("from-gpg.asc")) { var plain = new MemoryStream(); PgpMessageReader.Read(input, plain, read); Console.Write($"from-gpg.asc: {Encoding.UTF8.GetString(plain.ToArray())}"); } using (FileStream data = File.OpenRead("hello.txt")) using (FileStream sig = File.OpenRead("from-gpg.sig")) { PgpReadResult checkedSig = PgpMessageReader.VerifyDetached(data, sig, read); Console.WriteLine($"from-gpg.sig: {checkedSig.Signatures[0].Status}"); } File.WriteAllText("reply.txt", "Hello from OneCrypto.\n"); var encrypt = new PgpWriteOptions { Now = now, Recipients = [pub], Armor = true }; using (FileStream input = File.OpenRead("reply.txt")) using (FileStream output = File.Create("to-gpg.asc")) PgpMessageWriter.Write(input, output, encrypt); PgpKey signingKey = secret.FindSigningKey(now) ?? throw new InvalidOperationException("no signing key"); using PgpSecretKey signer = PgpSecretKey.Unlock(signingKey, [pw]); var sign = new PgpWriteOptions { Now = now, Signers = [signer], Armor = true }; using (FileStream input = File.OpenRead("reply.txt")) using (FileStream output = File.Create("to-gpg.sig")) PgpMessageWriter.SignDetached(input, output, sign); Console.WriteLine("Wrote to-gpg.asc and to-gpg.sig"); }
alice: Alice <[email protected]> primary v4 EdDsaLegacy Certify, Sign 1F6629D16B9534AF455E2828BFF62AE404F70B8A subkey v4 Ecdh EncryptCommunications, EncryptStorage 8E1BB9726734411B71D537FD0131912B4EA61126 alice-v6: Alice <[email protected]> primary v6 Ed25519 Certify, Sign AE2D2F6F57A31E70D4E81910316A37697C50884056917355C5A6335AE9398259 subkey v6 X25519 EncryptCommunications, EncryptStorage 5086CB230AB3A54C47AE14AE4BD9D016AFCD243E2B77DCA186AB97298F6DE54F
A version 4 fingerprint is 20 bytes, a version 6 one 32 (SHA-256). ExportPublic gives the certificate you hand out or publish; Export includes the secret keys, encrypted with the password: an iterated SHA-256 key derivation and AES-256 for version 4 (the Cv25519 subkey gets the protection GnuPG itself writes), Argon2id and AES-256 with OCB for version 6. Leave the password null only for keys that never leave memory. For an expiry date, more User IDs or a signing-only key, use PgpKeyGenerator.Generate with PgpKeyGenerationOptions (ValidFor, UserIds, SigningOnly).
Which version to generate
The default changed in 0.3.0-preview.2. Pgp.GenerateKey(userId, password) used to make a version 6 key; it now makes version 4, because, in the project’s words, “GnuPG 2.4 and 2.5 and RNP cannot use version 6 keys, while every implementation tested reads the version 4 form.” The project’s interop check, per its OpenPGP docs:
| Oracle | v6 Ed25519/X25519 | v4 native Ed25519/X25519 | v4 EdDSA legacy / Cv25519 |
|---|---|---|---|
| GnuPG 2.4.4, 2.4.8 | not read | imported, signature not checkable | import, verify, encrypt all work |
| GnuPG 2.5.24 | cannot use it | cannot use it | all work |
| RNP 0.17.0 | not read | not read | all work |
| sqop 0.38.0, gosop 1.1.0, OpenPGP.js 6.3.2 | all work | all work | all work |
Generate version 6 only when every party runs Sequoia, GopenPGP, OpenPGP.js or another RFC 9580 implementation. The profiles:
| Profile | Keys (primary / subkey) |
|---|---|
Rfc4880Curve25519 (default of Pgp.GenerateKey) | v4 EdDSA (legacy) / ECDH Curve25519 |
Rfc9580 (default of PgpKeyGenerator) | v6 Ed25519 / X25519 |
Rfc9580Ed448 | v6 Ed448 / X448 |
Rfc9580Rsa, Rfc4880Rsa | v6, v4 RSA-3072 |
Rfc9580V4 | v4 Ed25519 / X25519 (native) |
NistP256, NistP384, NistP521 | v4 ECDSA / ECDH |
Rfc9980Pqc, Rfc9980PqcHigh | v6 ML-DSA + Ed25519 or Ed448 / ML-KEM + X25519 or X448 |
Check the default key with GnuPG
GnuPG 2.4.8 reads the version 4 certificate and imports the secret key with its password. Then it encrypts a message to Alice and signs one with her key. 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 --show-keys alice.pub.asc gpg --batch --pinentry-mode loopback --passphrase 'demo password' \ --import alice.key.asc echo 'Hello from GnuPG.' > hello.txt gpg --batch --trust-model always --armor -r [email protected] \ --encrypt < hello.txt > from-gpg.asc gpg --batch --pinentry-mode loopback --passphrase 'demo password' --armor \ --detach-sign -u [email protected] < hello.txt > from-gpg.sig
gpg: keybox '/tmp/tmp.XwwCAUGITK/pubring.kbx' created gpg: /tmp/tmp.XwwCAUGITK/trustdb.gpg: trustdb created pub ed25519 2026-11-02 [SC] 1F6629D16B9534AF455E2828BFF62AE404F70B8A uid Alice <[email protected]> sub cv25519 2026-11-02 [E] gpg: key BFF62AE404F70B8A: public key "Alice <[email protected]>" imported gpg: key BFF62AE404F70B8A: secret key imported gpg: Total number processed: 1 gpg: imported: 1 gpg: secret keys read: 1 gpg: secret keys imported: 1
The program opens GnuPG’s message, checks its signature and writes its own; GnuPG decrypts and verifies those:
dotnet run -- roundtrip gpg --batch --pinentry-mode loopback --passphrase 'demo password' --decrypt to-gpg.asc gpg --verify to-gpg.sig reply.txt
from-gpg.asc: Hello from GnuPG. from-gpg.sig: Valid Wrote to-gpg.asc and to-gpg.sig gpg: encrypted with cv25519 key, ID 0131912B4EA61126, created 2026-11-02 "Alice <[email protected]>" Hello from OneCrypto. gpg: Signature made Mon 02 Nov 2026 06:53:37 AM UTC gpg: using EDDSA key 1F6629D16B9534AF455E2828BFF62AE404F70B8A 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: 1F66 29D1 6B95 34AF 455E 2828 BFF6 2AE4 04F7 0B8A
GnuPG’s warning means nothing in its keyring vouches for the key yet; the signature itself is good.
Check the version 6 key with Sequoia
GnuPG 2.4.8 doesn’t read the version 6 certificate:
gpg --show-keys alice-v6.pub.ascgpg: packet(6) with unknown version 6 gpg: read_block: read error: Invalid packet gpg: import from 'alice-v6.pub.asc' failed: Invalid keyring
Sequoia’s sqop 0.37.3 uses it: it encrypts to the certificate and decrypts with the password-protected secret key, and verifies a signature it made with that key.
printf 'demo password' > password.txt echo 'Hello, version 6.' | sqop encrypt alice-v6.pub.asc \ | sqop decrypt --with-key-password=password.txt alice-v6.key.asc echo 'Hello, version 6.' > hello6.txt sqop sign --with-key-password=password.txt alice-v6.key.asc < hello6.txt > hello6.sig sqop verify hello6.sig alice-v6.pub.asc < hello6.txt
Hello, version 6. 2026-11-02T06:53:38Z AE2D2F6F57A31E70D4E81910316A37697C50884056917355C5A6335AE9398259 AE2D2F6F57A31E70D4E81910316A37697C50884056917355C5A6335AE9398259 mode:binary {"signers":["alice-v6.pub.asc"]}
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.
- Version 6 keys don’t work with GnuPG or RNP, and GnuPG 2.4 can’t check signatures from the native Ed25519 algorithms of
Rfc9580V4. Version 5 (LibrePGP) keys are read but never generated. - Keep a password on keys for GnuPG 2.5. The project found that GnuPG 2.5.24 does not import an unprotected ECDH subkey (it lists it as
ssb#); protected keys work, and 2.4.8 takes both. - The
Rfc4880Curve25519encryption subkey is protected with AES-128 and a SHA-1 key derivation, the form GnuPG itself writes, because GnuPG 2.4.8 rejects that subkey under any other protection. - The post-quantum profiles need ML-DSA and ML-KEM on the platform (OpenSSL 3.5 on Linux); elsewhere they throw
AlgorithmNotSupportedException.
Profiles, preferences, editing, revocation and certification: OpenPGP, Key management. Using the key: Encrypt and decrypt a file with PGP, Sign and verify with PGP.