---
title: "Generate a PGP key in C#"
url: https://recursiveai.org/onecrypto/cookbook/generate-pgp-key/
description: "Generate a PGP (OpenPGP) key pair in C# with one call: a password-protected version 4 Ed25519/Cv25519 key that GnuPG 2.4 imports, encrypts to and verifies, exported as .asc, or a version 6 RFC 9580 key on request, checked with Sequoia. Runnable code with OneCrypto 0.3.0-preview.2."
version: "0.3.0-preview.2"
---

# Generate a PGP key in C#

`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.

```shell
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):

```csharp
using System.Text;
using RecursiveAI.OneCrypto;
using RecursiveAI.OneCrypto.OpenPgp;

const string userId = "Alice <alice@example.org>";
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 <alice@example.org>
  primary v4 EdDsaLegacy Certify, Sign
          1F6629D16B9534AF455E2828BFF62AE404F70B8A
  subkey  v4 Ecdh        EncryptCommunications, EncryptStorage
          8E1BB9726734411B71D537FD0131912B4EA61126
alice-v6: Alice <alice@example.org>
  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.

```shell
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 alice@example.org \
  --encrypt < hello.txt > from-gpg.asc
gpg --batch --pinentry-mode loopback --passphrase 'demo password' --armor \
  --detach-sign -u alice@example.org < 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 <alice@example.org>
sub   cv25519 2026-11-02 [E]

gpg: key BFF62AE404F70B8A: public key "Alice <alice@example.org>" 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:

```shell
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 <alice@example.org>"
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 <alice@example.org>" [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:

```shell
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.

```shell
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](https://recursiveai.org/onecrypto/docs/openpgp/#key-management). Using the key: [Encrypt and decrypt a file with PGP](https://recursiveai.org/onecrypto/cookbook/pgp-encrypt-decrypt-file/), [Sign and verify with PGP](https://recursiveai.org/onecrypto/cookbook/pgp-sign-verify/).
