When I began working on PGPy back in April, the decision to do so was not made lightly. Another software engineer here was in need of a Python OpenPGP library that was able to fulfill some requirements which the one he was using could not do. In order to help take some work off his plate, I volunteered to evaluate as many existing libraries as I could find in hopes that a good fit existed somewhere. Most of the existing options fell into one of two categories:

  • A direct Popen wrapper around the gpg command-line binary
  • A wrapper around GPGME, which is a gpg command-line binary wrapping library written in C

There were also a handful of utility libraries that could read and dump OpenPGP packet information but could not actually take any actions with them, and a couple of what appeared to be early starts on real OpenPGP implementations suffering largely from a lack of documentation and missing some parts of the OpenPGP specification that we either needed immediately, or an iteration or two down the line.

Among our requirements, the most paramount was to avoid a wrapper around a separate binary. The primary reason for this was because we wanted to be able to keep all key management tasks within a single memory address space, and to avoid the problems relating to securely sending passphrases to other processes. We also wanted to avoid having to store keys in on-disk keyring files, to be able to protect them further with other means of access control, while also adding the benefit of being able to reduce some of the disk I/O requirements for the system.

During my searches, I found what seemed to me to be a fairly decent desire for a robust OpenPGP implementation for Python that was capable of platform agnosticism, was well documented, and most importantly, easy to use. I recognized that there was a greater need outside this office that, for at least some other people, was going unfulfilled. So, I cracked open a copy of RFC 4880, and got to work.

Other than prioritizing fulfilling my coworkers’ most immediate needs first, my primary goal while designing and implementing PGPy’s API has been to make it as easy to use, correctly, as possible. Particularly, it should be simple and natural to do the “right” thing from a security perspective, easy to remember without having to constantly reference the documentation, and difficult to do things egregiously insecure. I have spent a lot of time writing and rewriting documentation and unit tests to help ensure that the previous three goals are met.

The end result is a package that can accomplish a lot in very few lines of code. Consider the following example:

import pgpy

my_privkey = pgpy.PGPKey.from_file("path/to/my/privatekey.asc")

with my_privkey.unlock("sooper_s3kret"), open("path/to/document", 'r') as document:
    doc_sig = my_privkey.sign(document.read())

with open("path/to/document.sig", 'w') as dsf:
    dsf.write(str(doc_sig))

In just 5 lines of code, we have loaded a private key, unlocked it using a passphrase, signed a document, and then saved the new signature to the disk. The document can now be verified with that signature using any compliant OpenPGP implementation, such as GnuPG.

PGPy 0.3.0 is not yet a complete implementation of the OpenPGP specification. Most notably, it cannot yet be used to generate keys. It also does not currently implement legacy (v3 key/signature formats) support at all. It does, however, support signing and signature verification using RSA and DSA, asymmetric encryption and decryption using RSA, and symmetric encryption using passphrases with a variety of algorithms.

If your appetite has been whetted, a wealth of additional information about PGPy can be found in the documentation.The latest version can always be installed from PyPI using pip, and I am also working on getting packages for several Linux distributions into their repositories. The codebase itself lives on our GitHub, and any and all feedback and bug reports are welcome, encouraged, and appreciated.

Get a monthly digest of our blog posts