Recursive AI

Generate a PGP key in C#

All onecrypto recipes

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.

Terminal
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):

C#
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");
}
Text
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:

Oraclev6 Ed25519/X25519v4 native Ed25519/X25519v4 EdDSA legacy / Cv25519
GnuPG 2.4.4, 2.4.8not readimported, signature not checkableimport, verify, encrypt all work
GnuPG 2.5.24cannot use itcannot use itall work
RNP 0.17.0not readnot readall work
sqop 0.38.0, gosop 1.1.0, OpenPGP.js 6.3.2all workall workall work

Generate version 6 only when every party runs Sequoia, GopenPGP, OpenPGP.js or another RFC 9580 implementation. The profiles:

ProfileKeys (primary / subkey)
Rfc4880Curve25519 (default of Pgp.GenerateKey)v4 EdDSA (legacy) / ECDH Curve25519
Rfc9580 (default of PgpKeyGenerator)v6 Ed25519 / X25519
Rfc9580Ed448v6 Ed448 / X448
Rfc9580Rsa, Rfc4880Rsav6, v4 RSA-3072
Rfc9580V4v4 Ed25519 / X25519 (native)
NistP256, NistP384, NistP521v4 ECDSA / ECDH
Rfc9980Pqc, Rfc9980PqcHighv6 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.

Terminal
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
Text
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:

Terminal
dotnet run -- roundtrip
gpg --batch --pinentry-mode loopback --passphrase 'demo password' --decrypt to-gpg.asc
gpg --verify to-gpg.sig reply.txt
Text
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:

Terminal
gpg --show-keys alice-v6.pub.asc
Text
gpg: 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.

Terminal
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
Text
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 Rfc4880Curve25519 encryption 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.