GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English)
+Table of Contents
+-
+
- 1. Introduction + + +
- 2. GPGME Concepts + + +
- 3. GPGME Python bindings installation + + +
- 4. Fundamentals + + +
- 5. Working with keys
+
-
+
- 5.1. Key selection + + +
- 5.2. Get key +
+ - 6. Basic Functions + + +
- 7. Creating keys and subkeys + + +
- 8. Miscellaneous work-arounds + + +
- 9. Copyright and Licensing + + +
1 Introduction
+Version: | +0.1.0 | +
Author: | +Ben McGinnes <ben@gnupg.org> | +
Author GPG Key: | +DB4724E6FA4286C92B4E55C4321E4E2373590E5D | +
Language: | +Australian English, British English | +
xml:lang: | +en-AU, en-GB, en | +
+This document provides basic instruction in how to use the GPGME +Python bindings to programmatically leverage the GPGME library. +
+1.1 Python 2 versus Python 3
++Though the GPGME Python bindings themselves provide support for +both Python 2 and 3, the focus is unequivocally on Python 3 and +specifically from Python 3.4 and above. As a consequence all the +examples and instructions in this guide use Python 3 code. +
+ ++Much of it will work with Python 2, but much of it also deals with +Python 3 byte literals, particularly when reading and writing data. +Developers concentrating on Python 2.7, and possibly even 2.6, will +need to make the appropriate modifications to support the older +string and unicode types as opposed to bytes. +
+ ++There are multiple reasons for concentrating on Python 3; some of +which relate to the immediate integration of these bindings, some +of which relate to longer term plans for both GPGME and the python +bindings and some of which relate to the impending EOL period for +Python 2.7. Essentially, though, there is little value in tying +the bindings to a version of the language which is a dead end and +the advantages offered by Python 3 over Python 2 make handling the +data types with which GPGME deals considerably easier. +
+2 GPGME Concepts
+2.1 A C API
+
+Unlike many modern APIs with which programmers will be more
+familiar with these days, the GPGME API is a C API. The API is
+intended for use by C coders who would be able to access its
+features by including the gpgme.h
header file with their own C
+source code and then access its functions just as they would any
+other C headers.
+
+This is a very effective method of gaining complete access to the +API and in the most efficient manner possible. It does, however, +have the drawback that it cannot be directly used by other +languages without some means of providing an interface to those +languages. This is where the need for bindings in various +languages stems. +
+2.2 Python bindings
++The Python bindings for GPGME provide a higher level means of +accessing the complete feature set of GPGME itself. It also +provides a more pythonic means of calling these API functions. +
+ +
+The bindings are generated dynamically with SWIG and the copy of
+gpgme.h
generated when GPGME is compiled.
+
+This means that a version of the Python bindings is fundamentally
+tied to the exact same version of GPGME used to generate that copy
+of gpgme.h
.
+
2.3 Difference between the Python bindings and other GnuPG Python packages
++There have been numerous attempts to add GnuPG support to Python +over the years. Some of the most well known are listed here, along +with what differentiates them. +
+2.3.1 The python-gnupg package maintained by Vinay Sajip
+
+This is arguably the most popular means of integrating GPG with
+Python. The package utilises the subprocess
module to implement
+wrappers for the gpg
and gpg2
executables normally invoked on
+the command line (gpg.exe
and gpg2.exe
on Windows).
+
+The popularity of this package stemmed from its ease of use and +capability in providing the most commonly required features. +
+ +
+Unfortunately it has been beset by a number of security issues in
+the past; most of which stemmed from using unsafe methods of
+accessing the command line via the subprocess
calls. While some
+effort has been made over the last two to three years (as of 2018)
+to mitigate this, particularly by no longer providing shell access
+through those subprocess calls, the wrapper is still somewhat
+limited in the scope of its GnuPG features coverage.
+
+The python-gnupg package is available under the MIT license. +
+2.3.2 The gnupg package created and maintained by Isis Lovecruft
+
+In 2015 Isis Lovecruft from the Tor Project forked and then
+re-implemented the python-gnupg package as just gnupg. This new
+package also relied on subprocess to call the gpg
or gpg2
+binaries, but did so somewhat more securely.
+
+The naming and version numbering selected for this package, +however, resulted in conflicts with the original python-gnupg and +since its functions were called in a different manner to +python-gnupg, the release of this package also resulted in a great +deal of consternation when people installed what they thought was +an upgrade that subsequently broke the code relying on it. +
+ ++The gnupg package is available under the GNU General Public +License version 3.0 (or any later version). +
+2.3.3 The PyME package maintained by Martin Albrecht
+
+This package is the origin of these bindings, though they are
+somewhat different now. For details of when and how the PyME
+package was folded back into GPGME itself see the Short History
+document1 in this Python bindings docs
directory.2
+
+The PyME package was first released in 2002 and was also the first
+attempt to implement a low level binding to GPGME. In doing so it
+provided access to considerably more functionality than either the
+python-gnupg
or gnupg
packages.
+
+The PyME package is only available for Python 2.6 and 2.7. +
+ ++Porting the PyME package to Python 3.4 in 2015 is what resulted in +it being folded into the GPGME project and the current bindings +are the end result of that effort. +
+ ++The PyME package is available under the same dual licensing as +GPGME itself: the GNU General Public License version 2.0 (or any +later version) and the GNU Lesser General Public License version +2.1 (or any later version). +
+3 GPGME Python bindings installation
+3.1 No PyPI
++Most third-party Python packages and modules are available and +distributed through the Python Package Installer, known as PyPI. +
+ ++Due to the nature of what these bindings are and how they work, it +is infeasible to install the GPGME Python bindings in the same way. +
+ +
+This is because the bindings use SWIG to dynamically generate C
+bindings against gpgme.h
and gpgme.h
is generated from
+gpgme.h.in
at compile time when GPGME is built from source. Thus
+to include a package in PyPI which actually built correctly would
+require either statically built libraries for every architecture
+bundled with it or a full implementation of C for each
+architecture.
+
3.2 Requirements
++The GPGME Python bindings only have three requirements: +
+ +-
+
- A suitable version of Python 2 or Python 3. With Python 2 that +means Python 2.7 and with Python 3 that means Python 3.4 or +higher. +
- SWIG. +
- GPGME itself. Which also means that all of GPGME's dependencies +must be installed too. +
3.3 Installation
++Installing the Python bindings is effectively achieved by compiling +and installing GPGME itself. +
+ +
+Once SWIG is installed with Python and all the dependencies for
+GPGME are installed you only need to confirm that the version(s) of
+Python you want the bindings installed for are in your $PATH
.
+
+By default GPGME will attempt to install the bindings for the most
+recent or highest version number of Python 2 and Python 3 it
+detects in $PATH
. It specifically checks for the python
and
+python3
executables first and then checks for specific version
+numbers.
+
+For Python 2 it checks for these executables in this order:
+python
, python2
and python2.7
.
+
+For Python 3 it checks for these executables in this order:
+python3
, python3.6
, python3.5
and python3.4
.
+
4 Fundamentals
++Before we can get to the fun stuff, there are a few matters +regarding GPGME's design which hold true whether you're dealing with +the C code directly or these Python bindings. +
+4.1 No REST
++The first part of which is or will be fairly blatantly obvious upon +viewing the first example, but it's worth reiterating anyway. That +being that this API is not a REST API. Nor indeed could it +ever be one. +
+ ++Most, if not all, Python programmers (and not just Python +programmers) know how easy it is to work with a RESTful API. In +fact they've become so popular that many other APIs attempt to +emulate REST-like behaviour as much as they are able. Right down +to the use of JSON formatted output to facilitate the use of their +API without having to retrain developers. +
+ ++This API does not do that. It would not be able to do that and +also provide access to the entire C API on which it's built. It +does, however, provide a very pythonic interface on top of the +direct bindings and it's this pythonic layer with which this HOWTO +deals with. +
+4.2 Context
++One of the reasons which prevents this API from being RESTful is +that most operations require more than one instruction to the API +to perform the task. Sure, there are certain functions which can +be performed simultaneously, particularly if the result known or +strongly anticipated (e.g. selecting and encrypting to a key known +to be in the public keybox). +
+ ++There are many more, however, which cannot be manipulated so +readily: they must be performed in a specific sequence and the +result of one operation has a direct bearing on the outcome of +subsequent operations. Not merely by generating an error either. +
+ ++When dealing with this type of persistent state on the web, full of +both the RESTful and REST-like, it's most commonly referred to as a +session. In GPGME, however, it is called a context and every +operation type has one. +
+5 Working with keys
+5.1 Key selection
++Selecting keys to encrypt to or to sign with will be a common +occurrence when working with GPGMe and the means available for +doing so are quite simple. +
+ ++They do depend on utilising a Context; however once the data is +recorded in another variable, that Context does not need to be the +same one which subsequent operations are performed. +
+ ++The easiest way to select a specific key is by searching for that +key's key ID or fingerprint, preferably the full fingerprint +without any spaces in it. A long key ID will probably be okay, but +is not advised and short key IDs are already a problem with some +being generated to match specific patterns. It does not matter +whether the pattern is upper or lower case. +
+ ++So this is the best method: +
+ +import gpg + +k = gpg.Context().keylist(pattern="258E88DCBD3CD44D8E7AB43F6ECB6AF0DEADBEEF") +keys = list(k) ++
+This is passable and very likely to be common: +
+ +import gpg + +k = gpg.Context().keylist(pattern="0x6ECB6AF0DEADBEEF") +keys = list(k) ++
+And this is a really bad idea: +
+ +import gpg + +k = gpg.Context().keylist(pattern="0xDEADBEEF") +keys = list(k) ++
+Alternatively it may be that the intention is to create a list of +keys which all match a particular search string. For instance all +the addresses at a particular domain, like this: +
+ +import gpg + +ncsc = gpg.Context().keylist(pattern="ncsc.mil") +nsa = list(ncsc) ++
5.1.1 Counting keys
+
+Counting the number of keys in your public keybox (pubring.kbx
),
+the format which has superseded the old keyring format
+(pubring.gpg
and secring.gpg
), or the number of secret keys is
+a very simple task.
+
import gpg + +c = gpg.Context() +seckeys = c.keylist(pattern=None, secret=True) +pubkeys = c.keylist(pattern=None, secret=False) + +seclist = list(seckeys) +secnum = len(seclist) + +publist = list(pubkeys) +pubnum = len(publist) + +print(""" +Number of secret keys: {0} +Number of public keys: {1} +""".format(secnum, pubnum)) ++
5.2 Get key
+
+An alternative method of getting a single key via its fingerprint
+is available directly within a Context with Context().get_key
.
+This is the preferred method of selecting a key in order to modify
+it, sign or certify it and for obtaining relevant data about a
+single key as a part of other functions; when verifying a signature
+made by that key, for instance.
+
+By default this method will select public keys, but it can select +secret keys as well. +
+ ++This first example demonstrates selecting the current key of Werner +Koch, which is due to expire at the end of 2018: +
+ +import gpg + +fingerprint = "80615870F5BAD690333686D0F2AD85AC1E42B367" +key = gpg.Context().get_key(fingerprint) ++
+Whereas this example demonstrates selecting the author's current
+key with the secret
key word argument set to True
:
+
import gpg + +fingerprint = "DB4724E6FA4286C92B4E55C4321E4E2373590E5D" +key = gpg.Context().get_key(fingerprint, secret=True) ++
+It is, of course, quite possible to select expired, disabled and +revoked keys with this function, but only to effectively display +information about those keys. +
+ ++It is also possible to use both unicode or string literals and byte +literals with the fingerprint when getting a key in this way. +
+6 Basic Functions
++The most frequently called features of any cryptographic library +will be the most fundamental tasks for encryption software. In this +section we will look at how to programmatically encrypt data, +decrypt it, sign it and verify signatures. +
+6.1 Encryption
+
+Encrypting is very straight forward. In the first example below
+the message, text
, is encrypted to a single recipient's key. In
+the second example the message will be encrypted to multiple
+recipients.
+
6.1.1 Encrypting to one key
+
+Once the the Context is set the main issues with encrypting data
+is essentially reduced to key selection and the keyword arguments
+specified in the gpg.Context().encrypt()
method.
+
+Those keyword arguments are: recipients
, a list of keys
+encrypted to (covered in greater detail in the following section);
+sign
, whether or not to sign the plaintext data, see subsequent
+sections on signing and verifying signatures below (defaults to
+True
); sink
, to write results or partial results to a secure
+sink instead of returning it (defaults to None
); passphrase
,
+only used when utilising symmetric encryption (defaults to
+None
); always_trust
, used to override the trust model settings
+for recipient keys (defaults to False
); add_encrypt_to
,
+utilises any preconfigured encrypt-to
or default-key
settings
+in the user's gpg.conf
file (defaults to False
); prepare
,
+prepare for encryption (defaults to False
); expect_sign
,
+prepare for signing (defaults to False
); compress
, compresses
+the plaintext prior to encryption (defaults to True
).
+
import gpg + +a_key = "0x12345678DEADBEEF" +text = b"""Some text to test with. + +Since the text in this case must be bytes, it is most likely that +the input form will be a separate file which is opened with "rb" +as this is the simplest method of obtaining the correct data +format. +""" + +c = gpg.Context(armor=True) +rkey = list(c.keylist(pattern=a_key, secret=False)) +ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, sign=False) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) ++
+Though this is even more likely to be used like this; with the
+plaintext input read from a file, the recipient keys used for
+encryption regardless of key trust status and the encrypted output
+also encrypted to any preconfigured keys set in the gpg.conf
+file:
+
import gpg + +a_key = "0x12345678DEADBEEF" + +with open("secret_plans.txt", "rb") as afile: + text = afile.read() + +c = gpg.Context(armor=True) +rkey = list(c.keylist(pattern=a_key, secret=False)) +ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, + sign=True, always_trust=True, + add_encrypt_to=True) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) ++
+If the recipients
paramater is empty then the plaintext is
+encrypted symmetrically. If no passphrase
is supplied as a
+parameter or via a callback registered with the Context()
then
+an out-of-band prompt for the passphrase via pinentry will be
+invoked.
+
6.1.2 Encrypting to multiple keys
++Encrypting to multiple keys essentially just expands upon the key +selection process and the recipients from the previous examples. +
+ +
+The following example encrypts a message (text
) to everyone with
+an email address on the gnupg.org
domain,3 but does not encrypt
+to a default key or other key which is configured to normally
+encrypt to.
+
import gpg + +text = b"""Oh look, another test message. + +The same rules apply as with the previous example and more likely +than not, the message will actually be drawn from reading the +contents of a file or, maybe, from entering data at an input() +prompt. + +Since the text in this case must be bytes, it is most likely that +the input form will be a separate file which is opened with "rb" +as this is the simplest method of obtaining the correct data +format. +""" + +c = gpg.Context(armor=True) +rpattern = list(c.keylist(pattern="@gnupg.org", secret=False)) +logrus = [] + +for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + +ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, sign=False, + always_trust=True) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) ++
+All it would take to change the above example to sign the message
+and also encrypt the message to any configured default keys would
+be to change the c.encrypt
line to this:
+
ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + always_trust=True, + add_encrypt_to=True) ++
+The only keyword arguments requiring modification are those for
+which the default values are changing. The default value of
+sign
is True
, the default of always_trust
is False
, the
+default of add_encrypt_to
is False
.
+
+If always_trust
is not set to True
and any of the recipient
+keys are not trusted (e.g. not signed or locally signed) then the
+encryption will raise an error. It is possible to mitigate this
+somewhat with something more like this:
+
import gpg + +with open("secret_plans.txt.asc", "rb") as afile: + text = afile.read() + +c = gpg.Context(armor=True) +rpattern = list(c.keylist(pattern="@gnupg.org", secret=False)) +logrus = [] + +for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + +try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, add_encrypt_to=True) +except gpg.errors.InvalidRecipients as e: + for i in range(len(e.recipients)): + for n in range(len(logrus)): + if logrus[n].fpr == e.recipients[i].fpr: + logrus.remove(logrus[n]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, add_encrypt_to=True) + except: + pass + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) ++
+This will attempt to encrypt to all the keys searched for, then +remove invalid recipients if it fails and try again. +
+6.2 Decryption
++Decrypting something encrypted to a key in one's secret keyring is +fairly straight forward. +
+ +
+In this example code, however, preconfiguring either
+gpg.Context()
or gpg.core.Context()
as c
is unnecessary
+because there is no need to modify the Context prior to conducting
+the decryption and since the Context is only used once, setting it
+to c
simply adds lines for no gain.
+
import gpg + +ciphertext = input("Enter path and filename of encrypted file: ") +newfile = input("Enter path and filename of file to save decrypted data to: ") +with open(ciphertext, "rb") as cfile: + plaintext, result, verify_result = gpg.Context().decrypt(cfile) +with open(newfile, "wb") as nfile: + nfile.write(plaintext) ++
+The data available in plaintext in this example is the decrypted
+content as a byte object in plaintext[0]
, the recipient key IDs
+and algorithms in plaintext[1]
and the results of verifying any
+signatures of the data in plaintext[0]
.
+
6.3 Signing text and files
++The following sections demonstrate how to specify keys to sign with. +
+6.3.1 Signing key selection
++By default GPGME and the Python bindings will use the default key +configured for the user invoking the GPGME API. If there is no +default key specified and there is more than one secret key +available it may be necessary to specify the key or keys with +which to sign messages and files. +
+ +import gpg + +logrus = input("Enter the email address or string to match signing keys to: ") +hancock = gpg.Context().keylist(pattern=logrus, secret=True) +sig_src = list(hancock) ++
+The signing examples in the following sections include the
+explicitly designated signers
parameter in two of the five
+examples; once where the resulting signature would be ASCII
+armoured and once where it would not be armoured.
+
+While it would be possible to enter a key ID or fingerprint here +to match a specific key, it is not possible to enter two +fingerprints and match two keys since the patten expects a string, +bytes or None and not a list. A string with two fingerprints +won't match any single key. +
+6.3.2 Normal or default signing messages or files
++The normal or default signing process is essentially the same as +is most often invoked when also encrypting a message or file. So +when the encryption component is not utilised, the result is to +produce an encoded and signed output which may or may not be ASCII +armoured and which may or may not also be compressed. +
+ +
+By default compression will be used unless GnuPG detects that the
+plaintext is already compressed. ASCII armouring will be
+determined according to the value of gpg.Context().armor
.
+
+The compression algorithm is selected in much the same way as the +symmetric encryption algorithm or the hash digest algorithm is +when multiple keys are involved; from the preferences saved into +the key itself or by comparison with the preferences with all +other keys involved. +
+ +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context(armor=True, signers=sig_src) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) ++
+Though everything in this example is accurate, it is more likely +that reading the input data from another file and writing the +result to a new file will be performed more like the way it is done +in the next example. Even if the output format is ASCII armoured. +
+ +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + +with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) ++
6.3.3 Detached signing messages and files
++Detached signatures will often be needed in programmatic uses of +GPGME, either for signing files (e.g. tarballs of code releases) +or as a component of message signing (e.g. PGP/MIME encoded +email). +
+ +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context(armor=True) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) ++
+As with normal signatures, detached signatures are best handled as +byte literals, even when the output is ASCII armoured. +
+ +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context(signers=sig_src) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + +with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) ++
6.3.4 Clearsigning messages or text
++Though PGP/in-line messages are no longer encouraged in favour of +PGP/MIME, there is still sometimes value in utilising in-line +signatures. This is where clear-signed messages or text is of +value. +
+ +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) ++
+In spite of the appearance of a clear-signed message, the data +handled by GPGME in signing it must still be byte literals. +
+ +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + +with open("/path/to/statement.txt.asc", "wb") as afile: + afile.write(signed_data) ++
6.4 Signature verification
++Essentially there are two principal methods of verification of a +signature. The first of these is for use with the normal or +default signing method and for clear-signed messages. The second is +for use with files and data with detached signatures. +
+ ++The following example is intended for use with the default signing +method where the file was not ASCII armoured: +
+ +import gpg +import time + +filename = "statement.txt" +gpg_file = "statement.txt.gpg" + +c = gpg.Context() + +try: + data, result = c.verify(open(gpg_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +{0} +with key {1} +made at {2} +""".format(c.get_key(sign.fpr).uids[0].uid, + sign.fpr, time.ctime(sign.timestamp))) +else: + pass ++
+Whereas this next example, which is almost identical would work +with normal ASCII armoured files and with clear-signed files: +
+ +import gpg +import time + +filename = "statement.txt" +asc_file = "statement.txt.asc" + +c = gpg.Context() + +try: + data, result = c.verify(open(asc_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +{0} +with key {1} +made at {2} +""".format(c.get_key(sign.fpr).uids[0].uid, + sign.fpr, time.ctime(sign.timestamp))) +else: + pass ++
+In both of the previous examples it is also possible to compare the
+original data that was signed against the signed data in data
to
+see if it matches with something like this:
+
with open(filename, "rb") as afile: + text = afile.read() + +if text == data: + print("Good signature.") +else: + pass ++
+The following two examples, however, deal with detached signatures.
+With his method of verification the data that was signed does not
+get returned since it is already being explicitly referenced in the
+first argument of c.verify
. So data
is None
and only the
+information in result
is available.
+
import gpg +import time + +filename = "statement.txt" +sig_file = "statement.txt.sig" + +c = gpg.Context() + +try: + data, result = c.verify(open(filename), open(sig_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +{0} +with key {1} +made at {2} +""".format(c.get_key(sign.fpr).uids[0].uid, + sign.fpr, time.ctime(sign.timestamp))) +else: + pass ++
import gpg +import time + +filename = "statement.txt" +asc_file = "statement.txt.asc" + +c = gpg.Context() + +try: + data, result = c.verify(open(filename), open(asc_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is not None: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +{0} +with key {1} +made at {2} +""".format(c.get_key(sign.fpr).uids[0].uid, + sign.fpr, time.ctime(sign.timestamp))) +else: + pass ++
7 Creating keys and subkeys
++The one thing, aside from GnuPG itself, that GPGME depends on, of +course, is the keys themselves. So it is necessary to be able to +generate them and modify them by adding subkeys, revoking or +disabling them, sometimes deleting them and doing the same for user +IDs. +
+ +
+In the following examples a key will be created for the world's
+greatest secret agent, Danger Mouse. Since Danger Mouse is a secret
+agent he needs to be able to protect information to SECRET
level
+clearance, so his keys will be 3072-bit keys.
+
+The pre-configured gpg.conf
file which sets cipher, digest and
+other preferences contains the following configuration parameters:
+
expert +allow-freeform-uid +allow-secret-key-import +trust-model tofu+pgp +tofu-default-policy unknown +enable-large-rsa +enable-dsa2 +# cert-digest-algo SHA256 +cert-digest-algo SHA512 +default-preference-list TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 ZLIB BZIP2 ZIP Uncompressed +personal-cipher-preferences TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES +personal-digest-preferences SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 +personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed ++
7.1 Primary key
+
+Generating a primary key uses the create_key
method in a Context.
+It contains multiple arguments and keyword arguments, including:
+userid
, algorithm
, expires_in
, expires
, sign
, encrypt
,
+certify
, authenticate
, passphrase
and force
. The defaults
+for all of those except userid
, algorithm
, expires_in
,
+expires
and passphrase
is False
. The defaults for
+algorithm
and passphrase
is None
. The default for
+expires_in
is 0
. The default for expires
is True
. There
+is no default for userid
.
+
+If passphrase
is left as None
then the key will not be
+generated with a passphrase, if passphrase
is set to a string
+then that will be the passphrase and if passphrase
is set to
+True
then gpg-agent will launch pinentry to prompt for a
+passphrase. For the sake of convenience, these examples will keep
+passphrase
set to None
.
+
import gpg + +c = gpg.Context() + +c.home_dir = "~/.gnupg-dm" +userid = "Danger Mouse <dm@secret.example.net>" + +dmkey = c.create_key(userid, algorithm = "rsa3072", expires_in = 31536000, + sign = True, certify = True) ++
+One thing to note here is the use of setting the c.home_dir
+parameter. This enables generating the key or keys in a different
+location. In this case to keep the new key data created for this
+example in a separate location rather than adding it to existing
+and active key store data. As with the default directory,
+~/.gnupg
, any temporary or separate directory needs the
+permissions set to only permit access by the directory owner. On
+posix systems this means setting the directory permissions to 700.
+
+The successful generation of the key can be confirmed via the
+returned GenkeyResult
object, which includes the following data:
+
print(""" +Fingerprint: {0} +Primary Key: {1} + Public Key: {2} + Secret Key: {3} + Sub Key: {4} + User IDs: {5} +""".format(dmkey.fpr, dmkey.primary, dmkey.pubkey, dmkey.seckey, dmkey.sub, + dmkey.uid)) ++
+Alternatively the information can be confirmed using the command +line program: +
+ +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <dm@secret.example.net> + +bash-4.4$ ++
+As with generating keys manually, to preconfigure expanded
+preferences for the cipher, digest and compression algorithms, the
+gpg.conf
file must contain those details in the home directory in
+which the new key is being generated. I used a cut down version of
+my own gpg.conf
file in order to be able to generate this:
+
bash-4.4$ gpg --homedir ~/.gnupg-dm --edit-key 177B7C25DB99745EE2EE13ED026D2F19E99E63AA showpref quit +Secret key is available. + +sec rsa3072/026D2F19E99E63AA + created: 2018-03-15 expires: 2019-03-15 usage: SC + trust: ultimate validity: ultimate +[ultimate] (1). Danger Mouse <dm@secret.example.net> + +[ultimate] (1). Danger Mouse <dm@secret.example.net> + Cipher: TWOFISH, CAMELLIA256, AES256, CAMELLIA192, AES192, CAMELLIA128, AES, BLOWFISH, IDEA, CAST5, 3DES + Digest: SHA512, SHA384, SHA256, SHA224, RIPEMD160, SHA1 + Compression: ZLIB, BZIP2, ZIP, Uncompressed + Features: MDC, Keyserver no-modify + +bash-4.4$ ++
7.2 Subkeys
+
+Adding subkeys to a primary key is fairly similar to creating the
+primary key with the create_subkey
method. Most of the arguments
+are the same, but not quite all. Instead of the userid
argument
+there is now a key
argument for selecting which primary key to
+add the subkey to.
+
+In the following example an encryption subkey will be added to the +primary key. Since Danger Mouse is a security conscious secret +agent, this subkey will only be valid for about six months, half +the length of the primary key. +
+ +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +key = c.get_key(dmkey.fpr, secret = True) +dmsub = c.create_subkey(key, algorithm = "rsa3072", expires_in = 15768000, + encrypt = True) ++
+As with the primary key, the results here can be checked with: +
+ +print(""" +Fingerprint: {0} +Primary Key: {1} + Public Key: {2} + Secret Key: {3} + Sub Key: {4} + User IDs: {5} +""".format(dmsub.fpr, dmsub.primary, dmsub.pubkey, dmsub.seckey, dmsub.sub, + dmsub.uid)) ++
+As well as on the command line with: +
+ +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <dm@secret.example.net> +ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + +bash-4.4$ ++
7.3 User IDs
+
+By comparison to creating primary keys and subkeys, adding a new
+user ID to an existing key is much simpler. The method used to do
+this is key_add_uid
and the only arguments it takes are for the
+key
and the new uid
.
+
import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret = True) +uid = "Danger Mouse <danger.mouse@secret.example.net>" + +c.key_add_uid(key, uid) ++
+Unsurprisingly the result of this is: +
+ +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <danger.mouse@secret.example.net> +uid [ultimate] Danger Mouse <dm@secret.example.net> +ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + +bash-4.4$ ++
7.4 Key certification
+
+Since key certification is more frequently referred to as key
+signing, the method used to perform this function is key_sign
.
+
+The key_sign
method takes four arguments: key
, uids
,
+expires_in
and local
. The default value of uids
is None
+and which results in all user IDs being selected. The default
+values of expires_in
snd local
is False
; which result in the
+signature never expiring and being able to be exported.
+
+The key
is the key being signed rather than the key doing the
+signing. To change the key doing the signing refer to the signing
+key selection above for signing messages and files.
+
+If the uids
value is not None
then it must either be a string
+to match a single user ID or a list of strings to match multiple
+user IDs. In this case the matching of those strings must be
+precise and it is case sensitive.
+
+To sign Danger Mouse's key for just the initial user ID with a +signature which will last a little over a month, do this: +
+ +import gpg + +c = gpg.Context() +uid = "Danger Mouse <dm@secret.example.net>" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret = True) +c.key_sign(key, uids = uid, expires_in = 2764800) ++
8 Miscellaneous work-arounds
+8.1 Group lines
++There is not yet an easy way to access groups configured in the +gpg.conf file from within GPGME. As a consequence these central +groupings of keys cannot be shared amongst multiple programs, such +as MUAs readily. +
+ ++The following code, however, provides a work-around for obtaining +this information in Python. +
+ +import subprocess + +lines = subprocess.getoutput("gpgconf --list-options gpg").splitlines() + +for i in range(len(lines)): + if lines[i].startswith("group") is True: + line = lines[i] + else: + pass + +groups = line.split(":")[-1].replace('"', '').split(',') + +group_lines = groups +for i in range(len(group_lines)): + group_lines[i] = group_lines[i].split("=") + +group_lists = group_lines +for i in range(len(group_lists)): + group_lists[i][1] = group_lists[i][1].split() ++
+The result of that code is that group_lines
is a list of lists
+where group_lines[i][0]
is the name of the group and
+group_lines[i][1]
is the key IDs of the group as a string.
+
+The group_lists
result is very similar in that it is a list of
+lists. The first part, group_lists[i][0]
matches
+group_lines[i][0]
as the name of the group, but
+group_lists[i][1]
is the key IDs of the group as a string.
+
9 Copyright and Licensing
+9.2 License GPL compatible
++This file is free software; as a special exception the author gives +unlimited permission to copy and/or distribute it, with or without +modifications, as long as this notice is preserved. +
+ ++This file is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY, to the extent permitted by law; without even +the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. +
+Footnotes:
+
+Short_History.org
and/or Short_History.html
.
+
+The lang/python/docs/
directory in the GPGME source.
+
+You probably don't really want to do this. Searching the +keyservers for "gnupg.org" produces over 400 results, the majority of +which aren't actually at the gnupg.org domain, but just included a +comment regarding the project in their key somewhere. +