---
title: "Read keys from a GnuPG keyring in C#"
url: https://recursiveai.org/onecrypto/cookbook/read-gnupg-keyring/
description: "Read public and secret keys from a GnuPG keyring (pubring.kbx, private-keys-v1.d) in C#, without running gpg: list the certificates, unlock a secret key with its passphrase and sign with it, checked by gpg --list-keys and gpg --verify. Runnable code with OneCrypto 0.3.0-preview.2."
version: "0.3.0-preview.2"
---

# Read keys from a GnuPG keyring in C#

`GnuPGHome` reads the files GnuPG keeps in its home directory: the public keyring (`pubring.kbx`, or the older `pubring.gpg`) and the secret key files in `private-keys-v1.d`. No `gpg` process and no agent run; the files are parsed directly. The program below lists every certificate in the keyring, finds the secret key GnuPG holds for one of them, unlocks it with its passphrase, and signs a file with it.

```shell
dotnet new console -o GnuPGKeys && cd GnuPGKeys
dotnet add package RecursiveAI.OneCrypto.OpenPgp --version 0.3.0-preview.2
```

To try it without touching your own keyring, give GnuPG a throwaway home and a test key first. `GNUPGHOME` points gpg, and the program, at a new, empty home for this terminal session, so the test key is made there and nothing is imported into your own keyring; leave the line out and the program reads your own keyring, read-only.

```shell
export GNUPGHOME="$(mktemp -d)"
gpg --batch --pinentry-mode loopback --passphrase 'demo password' \
  --quick-gen-key 'Alice <alice@example.org>' default default 2y
```

OneCrypto is a preview, so `dotnet add package` needs `--version` or `--prerelease`. Put this in `Program.cs` and run `dotnet run`:

```csharp
using RecursiveAI.OneCrypto.Keys;
using RecursiveAI.OneCrypto.OpenPgp;
using RecursiveAI.OneCrypto.OpenPgp.GnuPG;

DateTimeOffset now = DateTimeOffset.UtcNow;

// Open the GnuPG home: $GNUPGHOME, else ~/.gnupg (%APPDATA%\gnupg on Windows). Read-only.
GnuPGHome home = GnuPGHome.Open(GnuPGHome.DefaultPath());
Console.WriteLine($"Keyring: {(home.HasKeybox ? "pubring.kbx" : "pubring.gpg")}");

// Public keys: every certificate in the keyring
List<PgpCertificate> certificates = home.ReadPublicKeys().Select(PgpCertificate.FromTransferable).ToList();
foreach (PgpCertificate cert in certificates)
{
    PgpKeyState state = cert.Primary.StateAt(now);
    Console.WriteLine($"pub {cert.Primary.Algorithm} {Convert.ToHexString(cert.Fingerprint)}");
    Console.WriteLine($"    valid: {state.IsValid}, expires {state.ExpiresAt:yyyy-MM-dd}");
    foreach (string uid in cert.UserIds)
        Console.WriteLine($"    {uid}");
    foreach (PgpKey sub in cert.Keys.Skip(1))
        Console.WriteLine($"sub {sub.Algorithm} {Convert.ToHexString(sub.Fingerprint)} {sub.StateAt(now).Usage}");
}

// Secret keys: GnuPG keeps them in private-keys-v1.d; find the file for Alice's signing key
PgpCertificate alice = certificates.First(c => c.UserIds.Any(u => u.Contains("<alice@example.org>")));
PgpKey signingKey = alice.FindSigningKey(now) ?? throw new InvalidOperationException("no signing key");
GnuPGKeyFile keyFile = home.FindKeyFile(signingKey.Packet) ?? throw new InvalidOperationException("no secret key");
Console.WriteLine($"secret key file: {keyFile.Algorithm} {keyFile.Curve}, protected: {keyFile.IsProtected}");

// Unlock it with the passphrase and make a detached OpenPGP signature
File.WriteAllText("notes.txt", "Signed with a key from the GnuPG home.\n");
using (GnuPGSecretKey secret = keyFile.Unlock("demo password")) // read it from your secret store
using (ISigningKey key = secret.ToSigningKey())
using (FileStream data = File.OpenRead("notes.txt"))
{
    byte[] signature = PgpPreparedSignature.PrepareDetached(signingKey, data, now).SignWith(key);
    File.WriteAllBytes("notes.txt.sig", signature);
    Console.WriteLine($"wrote notes.txt.sig ({signature.Length} bytes)");
}
```

```text
Keyring: pubring.kbx
pub EdDsaLegacy 7C81D00461E84F944110F4727CA4131E1159CAA8
    valid: True, expires 2028-11-01
    Alice <alice@example.org>
sub Ecdh E85D6889C53DF47A5B8F115B4ED6739D89154EF2 EncryptCommunications, EncryptStorage
secret key file: ecc Ed25519, protected: True
wrote notes.txt.sig (119 bytes)
```

Each certificate is a `PgpCertificate`, the same type the rest of the OpenPGP API takes, so a key read here can go straight into `Recipients` or `Certificates` ([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/)). `valid` is the key’s own state: its self-signatures hold and it is not expired or revoked. It is not GnuPG’s `[ultimate]` or `[unknown]`, which come from the trust database; `home.ReadTrustDb()` reads the owner trust, and `ClassicTrust` computes validity the way GnuPG does ([OpenPGP trust models](https://recursiveai.org/onecrypto/docs/openpgp-trust/)).

The secret key file is found by the key’s public value and unlocked with the passphrase; a wrong one throws `GnuPGWrongPassphraseException`. `ToSigningKey()` gives a key for any OneCrypto signing call, and `PgpPreparedSignature` turns it into an OpenPGP signature. For decryption, `ToAgreementKey()` (Curve25519, X448, NIST curves) and `ToDecryptionKey()` (RSA) do the same.

## Check it with GnuPG

GnuPG 2.4.8 made the keyring; its listing has the same fingerprint the program read:

```shell
gpg --list-keys
```

```text
gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: next trustdb check due at 2028-11-01
/tmp/tmp.IN9CA15h3E/pubring.kbx
-------------------------------
pub   ed25519 2026-11-02 [SC] [expires: 2028-11-01]
      7C81D00461E84F944110F4727CA4131E1159CAA8
uid           [ultimate] Alice <alice@example.org>
sub   cv25519 2026-11-02 [E]
```

And the other way: GnuPG verifies the signature the program made with the key from its own home:

```shell
gpg --verify notes.txt.sig notes.txt
```

```text
gpg: Signature made Mon 02 Nov 2026 06:53:37 AM UTC
gpg:                using EDDSA key 7C81D00461E84F944110F4727CA4131E1159CAA8
gpg: Good signature from "Alice <alice@example.org>" [ultimate]
```

`[ultimate]` here is GnuPG’s trust in a key it generated itself, not something the signature proves.

## 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.
- **Read-only for the GnuPG home.** OneCrypto reads GnuPG’s files and is not a key store: to add keys, use `gpg`. The shared certificate directory (`pgp.cert.d`, the store Sequoia uses) is read and written by `CertDirectory` in `RecursiveAI.OneCrypto.OpenPgp.CertD`.
- **Secret keys that won’t unlock:** DSA, Elgamal and Kyber secret keys, smartcard stubs (shadowed keys) and `openpgp-native` protection throw `NotSupportedException`. Keys unlock for RSA, NIST and Brainpool curves, Ed25519, Cv25519, Ed448 and X448.
- An unprotected key file keeps its secret in the parsed file for the life of the `GnuPGKeyFile`, and a garbage-collected runtime can’t promise to erase it.

File formats, the keybox layout and what is checked against GnuPG 2.2 to 2.5: [GnuPG home and the certificate directory](https://recursiveai.org/onecrypto/docs/gnupg-home/).
