Recursive AI

Read keys from a GnuPG keyring in C#

All onecrypto recipes

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.

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

Terminal
export GNUPGHOME="$(mktemp -d)"
gpg --batch --pinentry-mode loopback --passphrase 'demo password' \
  --quick-gen-key 'Alice <[email protected]>' default default 2y

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

C#
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("<[email protected]>")));
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 <[email protected]>
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, Sign and verify with PGP). 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).

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:

Terminal
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 <[email protected]>
sub   cv25519 2026-11-02 [E]

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

Terminal
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 <[email protected]>" [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.