aboutsummaryrefslogtreecommitdiffstats
path: root/lang/python/docs/GPGMEpythonHOWTOen.org
diff options
context:
space:
mode:
Diffstat (limited to 'lang/python/docs/GPGMEpythonHOWTOen.org')
-rw-r--r--lang/python/docs/GPGMEpythonHOWTOen.org1370
1 files changed, 1370 insertions, 0 deletions
diff --git a/lang/python/docs/GPGMEpythonHOWTOen.org b/lang/python/docs/GPGMEpythonHOWTOen.org
new file mode 100644
index 00000000..655209ac
--- /dev/null
+++ b/lang/python/docs/GPGMEpythonHOWTOen.org
@@ -0,0 +1,1370 @@
+#+TITLE: GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English)
+#+LATEX_COMPILER: xelatex
+#+LATEX_CLASS: article
+#+LATEX_CLASS_OPTIONS: [12pt]
+#+LATEX_HEADER: \usepackage{xltxtra}
+#+LATEX_HEADER: \usepackage[margin=1in]{geometry}
+#+LATEX_HEADER: \setmainfont[Ligatures={Common}]{Times New Roman}
+#+LATEX_HEADER: \author{Ben McGinnes <[email protected]>}
+
+
+* Introduction
+ :PROPERTIES:
+ :CUSTOM_ID: intro
+ :END:
+
+ | Version: | 0.1.0-draft |
+ | Author: | Ben McGinnes <[email protected]> |
+ | 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.
+
+
+** Python 2 versus Python 3
+ :PROPERTIES:
+ :CUSTOM_ID: py2-vs-py3
+ :END:
+
+ 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.
+
+
+** Examples
+ :PROPERTIES:
+ :CUSTOM_ID: howto-python3-examples
+ :END:
+
+ All of the examples found in this document can be found as Python 3
+ scripts in the =lang/python/examples/howto= directory.
+
+
+* GPGME Concepts
+ :PROPERTIES:
+ :CUSTOM_ID: gpgme-concepts
+ :END:
+
+
+** A C API
+ :PROPERTIES:
+ :CUSTOM_ID: gpgme-c-api
+ :END:
+
+ 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.
+
+
+** Python bindings
+ :PROPERTIES:
+ :CUSTOM_ID: gpgme-python-bindings
+ :END:
+
+ 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=.
+
+
+** Difference between the Python bindings and other GnuPG Python packages
+ :PROPERTIES:
+ :CUSTOM_ID: gpgme-python-bindings-diffs
+ :END:
+
+ 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.
+
+
+*** The python-gnupg package maintained by Vinay Sajip
+ :PROPERTIES:
+ :CUSTOM_ID: diffs-python-gnupg
+ :END:
+
+ 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.
+
+
+*** The gnupg package created and maintained by Isis Lovecruft
+ :PROPERTIES:
+ :CUSTOM_ID: diffs-isis-gnupg
+ :END:
+
+ 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).
+
+
+*** The PyME package maintained by Martin Albrecht
+ :PROPERTIES:
+ :CUSTOM_ID: diffs-pyme
+ :END:
+
+ 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/
+ document[fn:1] in this Python bindings =docs= directory.[fn: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).
+
+
+* GPGME Python bindings installation
+ :PROPERTIES:
+ :CUSTOM_ID: gpgme-python-install
+ :END:
+
+
+** No PyPI
+ :PROPERTIES:
+ :CUSTOM_ID: do-not-use-pypi
+ :END:
+
+ 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.
+
+
+** Requirements
+ :PROPERTIES:
+ :CUSTOM_ID: gpgme-python-requirements
+ :END:
+
+ The GPGME Python bindings only have three requirements:
+
+ 1. 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.
+ 2. SWIG.
+ 3. GPGME itself. Which also means that all of GPGME's dependencies
+ must be installed too.
+
+
+** Installation
+ :PROPERTIES:
+ :CUSTOM_ID: installation
+ :END:
+
+ 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=.
+
+
+*** Installing GPGME
+ :PROPERTIES:
+ :CUSTOM_ID: install-gpgme
+ :END:
+
+ See the GPGME =README= file for details of how to install GPGME from
+ source.
+
+
+* Fundamentals
+ :PROPERTIES:
+ :CUSTOM_ID: howto-fund-a-mental
+ :END:
+
+ 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.
+
+
+** No REST
+ :PROPERTIES:
+ :CUSTOM_ID: no-rest-for-the-wicked
+ :END:
+
+ 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.
+
+
+** Context
+ :PROPERTIES:
+ :CUSTOM_ID: howto-get-context
+ :END:
+
+ 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.
+
+
+* Working with keys
+ :PROPERTIES:
+ :CUSTOM_ID: howto-keys
+ :END:
+
+
+** Key selection
+ :PROPERTIES:
+ :CUSTOM_ID: howto-keys-selection
+ :END:
+
+ 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:
+
+ #+begin_src python
+ import gpg
+
+ k = gpg.Context().keylist(pattern="258E88DCBD3CD44D8E7AB43F6ECB6AF0DEADBEEF")
+ keys = list(k)
+ #+end_src
+
+ This is passable and very likely to be common:
+
+ #+begin_src python
+ import gpg
+
+ k = gpg.Context().keylist(pattern="0x6ECB6AF0DEADBEEF")
+ keys = list(k)
+ #+end_src
+
+ And this is a really bad idea:
+
+ #+begin_src python
+ import gpg
+
+ k = gpg.Context().keylist(pattern="0xDEADBEEF")
+ keys = list(k)
+ #+end_src
+
+ 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:
+
+ #+begin_src python
+ import gpg
+
+ ncsc = gpg.Context().keylist(pattern="ncsc.mil")
+ nsa = list(ncsc)
+ #+end_src
+
+
+*** Counting keys
+ :PROPERTIES:
+ :CUSTOM_ID: howto-keys-counting
+ :END:
+
+ 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.
+
+ #+begin_src python
+ 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))
+ #+end_src
+
+
+** Get key
+ :PROPERTIES:
+ :CUSTOM_ID: howto-get-key
+ :END:
+
+ 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:
+
+ #+begin_src python
+ import gpg
+
+ fingerprint = "80615870F5BAD690333686D0F2AD85AC1E42B367"
+ key = gpg.Context().get_key(fingerprint)
+ #+end_src
+
+ Whereas this example demonstrates selecting the author's current
+ key with the =secret= key word argument set to =True=:
+
+ #+begin_src python
+ import gpg
+
+ fingerprint = "DB4724E6FA4286C92B4E55C4321E4E2373590E5D"
+ key = gpg.Context().get_key(fingerprint, secret=True)
+ #+end_src
+
+ 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.
+
+
+* Basic Functions
+ :PROPERTIES:
+ :CUSTOM_ID: howto-the-basics
+ :END:
+
+ 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.
+
+
+** Encryption
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-encryption
+ :END:
+
+ 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.
+
+
+*** Encrypting to one key
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-encryption-single
+ :END:
+
+ 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=).
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+ 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:
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+ 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.
+
+
+*** Encrypting to multiple keys
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-encryption-multiple
+ :END:
+
+ 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,[fn:3] but does /not/ encrypt
+ to a default key or other key which is configured to normally
+ encrypt to.
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+ 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:
+
+ #+begin_src python
+ ciphertext, result, sign_result = c.encrypt(text, recipients=logrus,
+ always_trust=True,
+ add_encrypt_to=True)
+ #+end_src
+
+ 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:
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+ This will attempt to encrypt to all the keys searched for, then
+ remove invalid recipients if it fails and try again.
+
+
+** Decryption
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-decryption
+ :END:
+
+ 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.
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+ 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]=.
+
+
+** Signing text and files
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-signing
+ :END:
+
+ The following sections demonstrate how to specify keys to sign with.
+
+
+*** Signing key selection
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-signing-signers
+ :END:
+
+ 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.
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+ 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.
+
+
+*** Normal or default signing messages or files
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-signing-normal
+ :END:
+
+ 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.
+
+ #+begin_src python
+ 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())
+ #+end_src
+
+ 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.
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+
+*** Detached signing messages and files
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-signing-detached
+ :END:
+
+ 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).
+
+ #+begin_src python
+ 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())
+ #+end_src
+
+ As with normal signatures, detached signatures are best handled as
+ byte literals, even when the output is ASCII armoured.
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+
+*** Clearsigning messages or text
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-signing-clear
+ :END:
+
+ 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.
+
+ #+begin_src python
+ 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())
+ #+end_src
+
+ In spite of the appearance of a clear-signed message, the data
+ handled by GPGME in signing it must still be byte literals.
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+
+** Signature verification
+ :PROPERTIES:
+ :CUSTOM_ID: howto-basic-verification
+ :END:
+
+ 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:
+
+ #+begin_src python
+ 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
+ #+end_src
+
+ Whereas this next example, which is almost identical would work
+ with normal ASCII armoured files and with clear-signed files:
+
+ #+begin_src python
+ 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
+ #+end_src
+
+ 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:
+
+ #+begin_src python
+ with open(filename, "rb") as afile:
+ text = afile.read()
+
+ if text == data:
+ print("Good signature.")
+ else:
+ pass
+ #+end_src
+
+ 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.
+
+ #+begin_src python
+ 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
+ #+end_src
+
+ #+begin_src python
+ 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
+ #+end_src
+
+
+* Creating keys and subkeys
+ :PROPERTIES:
+ :CUSTOM_ID: key-generation
+ :END:
+
+ 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:
+
+ #+begin_src conf
+ 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
+ #+end_src
+
+
+** Primary key
+ :PROPERTIES:
+ :CUSTOM_ID: keygen-primary
+ :END:
+
+ 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=.
+
+ #+begin_src python
+ import gpg
+
+ c = gpg.Context()
+
+ c.home_dir = "~/.gnupg-dm"
+ userid = "Danger Mouse <[email protected]>"
+
+ dmkey = c.create_key(userid, algorithm = "rsa3072", expires_in = 31536000,
+ sign = True, certify = True)
+ #+end_src
+
+ 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:
+
+ #+begin_src python
+ 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))
+ #+end_src
+
+ Alternatively the information can be confirmed using the command
+ line program:
+
+ #+begin_src shell
+ 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 <[email protected]>
+
+ bash-4.4$
+ #+end_src
+
+ 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:
+
+ #+begin_src shell
+ 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 <[email protected]>
+
+ [ultimate] (1). Danger Mouse <[email protected]>
+ 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$
+ #+end_src
+
+
+** Subkeys
+ :PROPERTIES:
+ :CUSTOM_ID: keygen-subkeys
+ :END:
+
+ 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.
+
+ #+begin_src python
+ 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)
+ #+end_src
+
+ As with the primary key, the results here can be checked with:
+
+ #+begin_src python
+ 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))
+ #+end_src
+
+ As well as on the command line with:
+
+ #+begin_src shell
+ 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 <[email protected]>
+ ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13]
+
+ bash-4.4$
+ #+end_src
+
+
+** User IDs
+ :PROPERTIES:
+ :CUSTOM_ID: keygen-uids
+ :END:
+
+ 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=.
+
+ #+begin_src python
+ import gpg
+
+ c = gpg.Context()
+ c.home_dir = "~/.gnupg-dm"
+
+ dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA"
+ key = c.get_key(dmfpr, secret = True)
+ uid = "Danger Mouse <[email protected]>"
+
+ c.key_add_uid(key, uid)
+ #+end_src
+
+ Unsurprisingly the result of this is:
+
+ #+begin_src shell
+ 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 <[email protected]>
+ uid [ultimate] Danger Mouse <[email protected]>
+ ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13]
+
+ bash-4.4$
+ #+end_src
+
+
+** Key certification
+ :PROPERTIES:
+ :CUSTOM_ID: key-sign
+ :END:
+
+ 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:
+
+ #+begin_src python
+ import gpg
+
+ c = gpg.Context()
+ uid = "Danger Mouse <[email protected]>"
+
+ dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA"
+ key = c.get_key(dmfpr, secret = True)
+ c.key_sign(key, uids = uid, expires_in = 2764800)
+ #+end_src
+
+
+* Miscellaneous work-arounds
+ :PROPERTIES:
+ :CUSTOM_ID: cheats-and-hacks
+ :END:
+
+
+** Group lines
+ :PROPERTIES:
+ :CUSTOM_ID: group-lines
+ :END:
+
+ 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.
+
+ #+begin_src 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()
+ #+end_src
+
+ 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.
+
+
+* Copyright and Licensing
+ :PROPERTIES:
+ :CUSTOM_ID: copyright-and-license
+ :END:
+
+
+** Copyright (C) The GnuPG Project, 2018
+ :PROPERTIES:
+ :CUSTOM_ID: copyright
+ :END:
+
+ Copyright © The GnuPG Project, 2018.
+
+
+** License GPL compatible
+ :PROPERTIES:
+ :CUSTOM_ID: license
+ :END:
+
+ 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
+
+[fn:1] =Short_History.org= and/or =Short_History.html=.
+
+[fn:2] The =lang/python/docs/= directory in the GPGME source.
+
+[fn:3] 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.