# GopenPGP v3: a high-level OpenPGP library with profiles and builder-style handles

> GopenPGP is ProtonMail's Go library for OpenPGP, built on a fork of the Go crypto library, and its v3 release replaced the API with a builder pattern where you assemble a handle and then encrypt or decrypt. It offers RFC 9580 as a selectable profile, key generation and key rings, hidden recipients, and an explicit step to clear private key material, and it is MIT licensed.

**ProtonMail/gopenpgp** — A high-level OpenPGP library

- Repository: https://github.com/ProtonMail/gopenpgp
- Website: https://gopenpgp.org
- Stars: 1,251 · Forks: 141
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/protonmail-gopenpgp

## What a high-level OpenPGP library saves you

GopenPGP is a high-level OpenPGP library built on top of a fork of the golang crypto library, and the fork is ProtonMail's own go-crypto module.

The problem it solves is the one every Go developer meets when they need OpenPGP. The standard library's `crypto/openpgp` is deprecated, and even where an implementation exists, using it means understanding key packets, session key handling, armor encoding, compression preferences and the difference between a literal data packet and an encrypted one. That is a lot of protocol knowledge to acquire for the common case of encrypting a message to a recipient and decrypting it with a key.

GopenPGP is the layer that removes it. The whole API in the README's first example is a call to `crypto.PGP()` and nothing else, and the point of the library is that the next line does the right thing.

The intended user is a Go developer who needs OpenPGP as a feature of a product rather than as a research interest. The examples in the README are product-shaped: encrypt a message with a password, encrypt to a key, generate a key for a user, sign and verify, send to several recipients at once, send to one recipient without revealing who the other recipients are.

The project is MIT licensed, the primary language is Go, the homepage is gopenpgp.org, and the last push was on 2026-10-01. The recent releases are v3.5.2-proton and v2.11.1-proton, both from 2026-10-01, and v3.5.2 from 2026-09-30, which tells you the project maintains two major versions in parallel and that the v3 line is the active one.

The module requires Go 1.23.0 and depends on `github.com/ProtonMail/go-crypto` at v1.5.2, `github.com/ProtonMail/go-mime`, and `stretchr/testify` for tests, with `cloudflare/circl` and the `golang.org/x/crypto` packages pulled in indirectly.

## The builder API: assemble a handle, then act on it

The v3 API is a builder, and understanding its shape is the fastest way to read the whole library.

Every operation follows the same pattern. You start from a crypt object, call a method that selects what you want, chain the parameters, call `New()` to get a handle, and then perform the operation on the handle. Encrypting with a password is:

```go
import "github.com/ProtonMail/gopenpgp/v3/crypto"

password := []byte("hunter2")

pgp := crypto.PGP()
// Encrypt data with a password
encHandle, err := pgp.Encryption().Password(password).New()
pgpMessage, err := encHandle.Encrypt([]byte("my message"))
armored, err := pgpMessage.ArmorBytes()

// Decrypt data with a password
decHandle, err := pgp.Decryption().Password(password).New()
decrypted, err := decHandle.Decrypt(armored, crypto.Armor)
myMessage := decrypted.Bytes()
```

Two details in that snippet are worth calling out. The `Armor` argument to `Decrypt` tells the library the input is armored, and the same call accepts `crypto.Bytes` for a binary message, so the same code path handles both wire formats. And `ArmorBytes()` is a separate call, so armoring is something you ask for rather than something the library decides for you, which matters when you are moving bytes around inside your own service.

The `New()` call returning an error is a second detail that matters. The chain of builder calls before it is configuration, and the error comes from the point where the configuration becomes a usable object, so a malformed key or a missing parameter surfaces at a single place rather than at the moment of use.

The same shape covers keys. `crypto.NewKeyFromArmored(pubkey)` and `crypto.NewPrivateKeyFromArmored(privkey, passphrase)` load keys, and the README notes that keys should be put in backticks to avoid errors caused by spaces or tabs, which is a small but real trap when a key is embedded in source.

## Profiles decide the cryptography, and that is the part to think about

The most consequential feature in the library is the profile, because it decides which OpenPGP standard your messages conform to.

By default `crypto.PGP()` gives you the library's default behaviour. To target the latest OpenPGP standard, RFC 9580, you select a profile explicitly:

```go
import "github.com/ProtonMail/gopenpgp/v3/profile"

// Use the latest OpenPGP standard (RFC 9580).
pgp := crypto.PGPWithProfile(profile.RFC9580())
```

The README explains what that choice means in cryptographic terms. The RFC9580 profile uses Argon2 for protecting encrypted keys and messages encrypted using a passphrase, and uses AEAD for encryption, with AES-256 in OCB mode.

Those are not cosmetic differences. Argon2 is a memory-hard key derivation function, so a passphrase protected under that profile is materially harder to attack offline than one under the older scheme. OCB is an authenticated encryption mode, so the ciphertext carries its own integrity guarantee. The cost is compatibility, and that is the trade you have to make deliberately: a message produced under the RFC 9580 profile is not readable by an implementation that only speaks RFC 4880.

The preset form is the same call with a different profile:

```go
import "github.com/ProtonMail/gopenpgp/v3/profile"

// RFC4880 profile
pgp4880 := crypto.PGPWithProfile(profile.RFC4880()) 
// RFC9580 profile
pgpCryptoRefresh := crypto.PGPWithProfile(profile.RFC9580())
```

So the practical question for an adopter is not which profile is better but which profile the other end of your pipeline can read. A system talking to a mix of modern and legacy clients will need both, and the profile mechanism is what makes that a configuration choice rather than a fork.

## Key generation, key rings and hiding a recipient

Key management is part of the library rather than something you bolt on, and three pieces of it are worth understanding.

Key generation is a builder in its own right, and it produces a private key you can then convert:

```go
pgp := crypto.PGP()
aliceKeyPriv, err := pgp.KeyGeneration().
  AddUserId("alice", "alice@alice.com").
  New().
  GenerateKey()
aliceKeyPub, err := aliceKeyPriv.ToPublic()
```

Key rings are how you address several recipients at once. You build a ring and add keys to it:

```go
recipients, err := crypto.NewKeyRing(bobKeyPub)
err = recipients.AddKey(carolKeyPub)
```

and then pass the ring to the encryption builder with `Recipients(recipients)`.

The third piece is the most interesting, and it is the one with a security consequence. The builder has both `Recipient` and `HiddenRecipient`, and the README spells out the difference. The key fingerprint of bob's key is visible in the key packet and is included in the signature's intended recipient list, while the key fingerprint of carol's key is not visible in the key packet, which the README calls an anonymous key packet, and is not included in the signature's intended recipient list.

That is a meaningful property for a system where the set of recipients is sensitive. Without it, an observer of the message learns who it was encrypted to. With it, they learn that someone was, but not who. The trade is that the recipient check on the decryption side has to be explicit, because the information needed to perform it is no longer in the message.

## Signatures are checked explicitly, and private material is cleared by hand

Two behaviours in the API are worth designing around rather than discovering later.

The first is that signature verification is a separate step with its own error. You give the decryption handle a verification key along with the decryption key, and then ask the result for its signature error:

```go
decrypted, err := decHandle.Decrypt(armored, crypto.Armor)
if sigErr := decrypted.SignatureError(); sigErr != nil {
  // Signature verification failed with sigErr
}
myMessage := decrypted.Bytes()
```

That is a deliberate design choice. The library does not treat a failed signature as an error on the decryption itself, so a caller that forgets to check `SignatureError()` will happily hand out plaintext from an unauthenticated message. The verification key also has to be the one you expect, which is the second thing to check when you adopt it.

The second behaviour is the manual cleanup. Both the encryption and decryption handles expose `ClearPrivateParams()`, and the README calls it after the operation in every example that touched a private key or a passphrase.

The library does not do this for you, and that is the point. A handle that holds a decrypted private key or a passphrase in memory is a thing with a lifetime, and the lifetime is a decision the application has to make. If your service creates a handle per request, calling `ClearPrivateParams()` when you are done is what keeps the passphrase out of a heap that a crash dump would capture.

The same pattern applies to the anonymous recipient case, where the README notes that decryption checks if bob's key fingerprint is in the intended recipient list of alice's signature in the message, and that there is an option to disable the intended recipient check, in which case the decryption function tries all supplied keys. That is a performance and a privacy trade, and it is another decision the application makes rather than the library.

## V3, V2 and the Proton fork

Two structural facts about the project matter to anyone planning an adoption, and both are in the README.

The first is that GopenPGP V3 introduces a new API with significant enhancements, and it is not backward compatible with GopenPGP V2. The project recommends upgrading to V3 for the latest features and improvements, and continues to support V2, with support including ongoing bug fixes and minor feature updates to ensure stability and functionality for existing users. The V2 line lives on the v2 branch of the same repository, and the release list shows that it is still being cut, with v2.11.1-proton published on the same day as v3.5.2-proton.

So the migration question is concrete. A team on V2 has a decision to make, and the answer depends on how much of the V2 API surface they use. The parallel release stream means there is no forced deadline, but it also means two lines to track.

The second fact is the dependency. GopenPGP is built on a fork of the golang crypto library, and that fork is `github.com/ProtonMail/go-crypto`, pinned in `go.mod` at v1.5.2. The fork is a maintained piece of the design rather than a temporary patch, since the module carries its own version line and the project pins it explicitly.

The tree shows the shape of the library. There is `crypto/` for the main API, `profile/` for the profile definitions, `armor/` for the armor encoding, `mime/` for the multipart handling, `constants/` for the shared constants, `internal/` for the unexported machinery, and `mobile/` for the Go Mobile bindings, which the README has its own section for. The `build.sh` script and the `.golangci.yml` configuration sit at the root, and `crypto/crypto_example_test.go` is the file of runnable examples the README points at, which is worth knowing about because it is a set of examples that are compiled and run rather than pasted into a document.

## Where gopenpgp is the wrong tool

There are three cases where this library is the wrong choice, and the first is about the layer you are working at.

The first is when you need the low-level packet API. gopenpgp is explicitly a high-level library, and the `crypto/openpgp` style work of assembling packets yourself, choosing compression algorithms, inspecting the structure of a message, or supporting a non-standard packet layout is out of scope. If you need to see the bytes, the upstream `go-crypto` module is where that work lives, and gopenpgp is a consumer of it.

The second is when interoperability has to be broad and you cannot control the peers. The library produces standard OpenPGP messages, but the profile question is the whole ball game. An RFC 9580 message is better protected and unreadable to old clients, and an RFC 4880 message is readable by everyone including implementations with weaker defaults. Neither is wrong. The point is that the choice is not a detail you can defer, and if your peers are unknown the safest default is the older profile.

The third is a language other than Go. There is no reason to reach for a Go library from another runtime, and the Go Mobile bindings, which the README has a section for, are for calling Go from a mobile app rather than for making OpenPGP portable across languages.

There is one more consideration that is about the project rather than the API. gopenpgp is maintained by ProtonMail for ProtonMail's own needs, and the fork of the crypto library sits underneath it. That is a strength in the sense that the code is exercised by a mail client handling real traffic, and it is a consideration in the sense that changes are driven by what a mail provider needs. The two major versions in parallel, the `-proton` suffixed release tags and the v2 branch are all signs of a project that expects to keep serving users of an older API for a long time, which is reassuring for stability and worth checking against your own upgrade plan. The material a reader would want next, and which the README does not provide, is a statement of which upstream go-crypto features are exposed and which are deliberately hidden, and that question is best answered by reading the `crypto/` package before committing.

## Conclusion

Adopt GopenPGP if you are writing a Go service that encrypts, decrypts or signs data with OpenPGP and you want the common cases to be a few lines rather than an assembly of packets, since the v3 builder API and the profile mechanism cover both the legacy and the current standard. Do not adopt it if you need the low-level packet API that the Go standard library or the upstream go-crypto module already provides, since gopenpgp deliberately sits above that layer, and expect to read the v2 branch if you maintain code on the older API. Verify three things first: that the profile you select matches what your counterpart expects, since RFC 9580 and RFC 4880 produce messages that older implementations cannot read, that the recipient key you use for verification is the one you expect, since the signature check is explicit rather than implicit, and that `ClearPrivateParams()` is called on every handle that held a passphrase, because the library will not do it for you.

## FAQ

### How do I install GopenPGP v3 in a Go project?

With Go Modules, run `go get github.com/ProtonMail/gopenpgp/v3` in your project folder, then import `github.com/ProtonMail/gopenpgp/v3/crypto` and start from `crypto.PGP()`. The API documentation is on pkg.go.dev, and runnable examples are in crypto/crypto_example_test.go.

### What is the difference between the RFC4880 and RFC9580 profiles in GopenPGP?

The RFC9580 profile uses Argon2 for protecting encrypted keys and passphrase-encrypted messages and uses AEAD for encryption with AES-256 in OCB mode, while the RFC4880 profile is the older standard. You select one with `crypto.PGPWithProfile(profile.RFC9580())` or `profile.RFC4880()`, and the choice determines what older implementations can read.

### Is GopenPGP v3 backward compatible with v2?

No. The README states that v3 introduces a new API with significant enhancements and is not backward compatible with v2. The project still supports v2 with ongoing bug fixes and minor feature updates, and v2 is available on the v2 branch of the repository.

### How does GopenPGP handle signature verification?

Verification is explicit. You give the decryption handle a VerificationKey, and then check the result with `decrypted.SignatureError()`, which returns an error if verification failed. A caller that skips that check will accept plaintext from an unauthenticated message.

### What is ClearPrivateParams in GopenPGP?

It is a method on the encryption and decryption handles that clears the private key material and passphrase held by the handle. The README calls it in every example that used a private key, because the library leaves that lifetime to the application rather than clearing it automatically.

## Sources

- [License: MIT](https://github.com/ProtonMail/gopenpgp/blob/main/LICENSE)
- [Project website](https://gopenpgp.org)
- [ProtonMail/gopenpgp on GitHub](https://github.com/ProtonMail/gopenpgp)
- [README](https://github.com/ProtonMail/gopenpgp/blob/main/README.md)
- [Releases](https://github.com/ProtonMail/gopenpgp/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/protonmail-gopenpgp
