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.
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.
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:
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)"); }
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:
gpg --list-keysgpg: 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:
gpg --verify notes.txt.sig notes.txtgpg: 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 byCertDirectoryinRecursiveAI.OneCrypto.OpenPgp.CertD. - Secret keys that won’t unlock: DSA, Elgamal and Kyber secret keys, smartcard stubs (shadowed keys) and
openpgp-nativeprotection throwNotSupportedException. 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.