2018-03-07 09:05:21 +00:00
|
|
|
#+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 <ben@gnupg.org>}
|
|
|
|
|
|
|
|
|
|
|
|
* Introduction
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: intro
|
|
|
|
:END:
|
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
| Version: | 0.1.0-draft |
|
2018-03-12 19:09:53 +00:00
|
|
|
| Author: | Ben McGinnes <ben@gnupg.org> |
|
|
|
|
| Author GPG Key: | DB4724E6FA4286C92B4E55C4321E4E2373590E5D |
|
2018-03-13 15:40:41 +00:00
|
|
|
| Language: | Australian English, British English |
|
|
|
|
| xml:lang: | en-AU, en-GB, en |
|
2018-03-07 09:05:21 +00:00
|
|
|
|
2018-03-09 04:22:24 +00:00
|
|
|
This document provides basic instruction in how to use the GPGME
|
|
|
|
Python bindings to programmatically leverage the GPGME library.
|
2018-03-07 09:05:21 +00:00
|
|
|
|
2018-03-13 14:41:21 +00:00
|
|
|
** 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
|
2018-03-15 03:59:36 +00:00
|
|
|
need to make the appropriate modifications to support the older
|
|
|
|
string and unicode types as opposed to bytes.
|
2018-03-13 14:41:21 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2018-03-07 09:05:21 +00:00
|
|
|
|
|
|
|
* 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
|
2018-03-15 03:59:36 +00:00
|
|
|
features by including the =gpgme.h= header file with their own C
|
2018-03-07 09:05:21 +00:00
|
|
|
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
|
2018-03-07 09:12:26 +00:00
|
|
|
=gpgme.h= generated when GPGME is compiled.
|
2018-03-07 09:05:21 +00:00
|
|
|
|
|
|
|
This means that a version of the Python bindings is fundamentally
|
2018-03-13 14:41:21 +00:00
|
|
|
tied to the exact same version of GPGME used to generate that copy
|
2018-03-07 09:12:26 +00:00
|
|
|
of =gpgme.h=.
|
2018-03-07 09:05:21 +00:00
|
|
|
|
|
|
|
** 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,
|
|
|
|
most of which stemmed from using unsafe methods of accessing the
|
|
|
|
command line via the =subprocess= calls.
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
However the naming and version numbering selected for this package
|
|
|
|
resulted in conflicts with the original python-gnupg and since its
|
|
|
|
functions were called in a different manner, 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 later).
|
|
|
|
|
|
|
|
*** 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
|
2018-03-12 13:33:11 +00:00
|
|
|
package was folded back into GPGME itself see the /Short History/
|
|
|
|
document[fn:1] in this Python bindings =docs= directory.[fn:2]
|
2018-03-07 09:05:21 +00:00
|
|
|
|
|
|
|
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
|
2018-03-16 16:46:02 +00:00
|
|
|
later version) and the GNU Lesser General Public License version
|
|
|
|
2.1 (or any later version).
|
2018-03-07 09:05:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
* 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.
|
|
|
|
|
2018-03-15 16:52:58 +00:00
|
|
|
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.
|
|
|
|
|
2018-03-07 09:05:21 +00:00
|
|
|
** 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
|
2018-03-15 03:59:36 +00:00
|
|
|
=python3= executables first and then checks for specific version
|
2018-03-07 09:05:21 +00:00
|
|
|
numbers.
|
|
|
|
|
|
|
|
For Python 2 it checks for these executables in this order:
|
2018-03-07 09:12:26 +00:00
|
|
|
=python=, =python2= and =python2.7=.
|
2018-03-07 09:05:21 +00:00
|
|
|
|
|
|
|
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:
|
|
|
|
|
2018-03-12 13:33:11 +00:00
|
|
|
See the GPGME =README= file for details of how to install GPGME from
|
2018-03-07 09:05:21 +00:00
|
|
|
source.
|
|
|
|
|
|
|
|
|
2018-03-08 04:23:05 +00:00
|
|
|
* 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
|
2018-03-16 16:46:02 +00:00
|
|
|
strongly anticipated (e.g. selecting and encrypting to a key known
|
2018-03-08 04:23:05 +00:00
|
|
|
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.
|
|
|
|
|
2018-03-15 03:59:36 +00:00
|
|
|
When dealing with this type of persistent state on the web, full of
|
2018-03-08 04:23:05 +00:00
|
|
|
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.
|
|
|
|
|
|
|
|
|
2018-03-13 07:32:30 +00:00
|
|
|
* Working with keys
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-keys
|
|
|
|
:END:
|
|
|
|
|
2018-03-13 15:21:44 +00:00
|
|
|
** 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
|
|
|
|
|
|
|
|
|
2018-03-15 01:14:29 +00:00
|
|
|
*** Counting keys
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-keys-counting
|
|
|
|
:END:
|
|
|
|
|
|
|
|
Counting the number of keys in your public keybox (=pubring.kbx=),
|
2018-03-15 03:59:36 +00:00
|
|
|
the format which has superseded the old keyring format
|
2018-03-15 01:14:29 +00:00
|
|
|
(=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
|
2018-03-13 07:32:30 +00:00
|
|
|
:PROPERTIES:
|
2018-03-15 01:14:29 +00:00
|
|
|
:CUSTOM_ID: howto-get-key
|
2018-03-13 07:32:30 +00:00
|
|
|
:END:
|
|
|
|
|
2018-03-15 01:14:29 +00:00
|
|
|
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:
|
2018-03-13 07:32:30 +00:00
|
|
|
|
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
|
2018-03-15 01:14:29 +00:00
|
|
|
fingerprint = "80615870F5BAD690333686D0F2AD85AC1E42B367"
|
|
|
|
key = gpg.Context().get_key(fingerprint)
|
|
|
|
#+end_src
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-15 01:14:29 +00:00
|
|
|
Whereas this example demonstrates selecting the author's current
|
|
|
|
key with the =secret= key word argument set to =True=:
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-15 01:14:29 +00:00
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-15 01:14:29 +00:00
|
|
|
fingerprint = "DB4724E6FA4286C92B4E55C4321E4E2373590E5D"
|
|
|
|
key = gpg.Context().get_key(fingerprint, secret=True)
|
2018-03-13 07:32:30 +00:00
|
|
|
#+end_src
|
|
|
|
|
2018-03-15 01:14:29 +00:00
|
|
|
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.
|
|
|
|
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-08 17:42:41 +00:00
|
|
|
* Basic Functions
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-the-basics
|
|
|
|
:END:
|
|
|
|
|
2018-03-09 05:49:05 +00:00
|
|
|
The most frequently called features of any cryptographic library
|
2018-03-15 03:59:36 +00:00
|
|
|
will be the most fundamental tasks for encryption software. In this
|
2018-03-09 05:49:05 +00:00
|
|
|
section we will look at how to programmatically encrypt data,
|
|
|
|
decrypt it, sign it and verify signatures.
|
|
|
|
|
2018-03-08 17:42:41 +00:00
|
|
|
** Encryption
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-basic-encryption
|
|
|
|
:END:
|
|
|
|
|
2018-03-09 05:49:05 +00:00
|
|
|
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:
|
|
|
|
|
|
|
|
The text is then encapsulated in a GPGME Data object as =plain= and
|
|
|
|
the =cipher= object is created with another Data object. Then we
|
|
|
|
create the Context as =c= and set it to use the ASCII armoured
|
|
|
|
OpenPGP format. In later examples there will be alternative
|
|
|
|
methods of setting the OpenPGP output to be ASCII armoured.
|
|
|
|
|
|
|
|
Next we prepare a keylist object in our Context and follow it with
|
|
|
|
specifying the recipients as =r=. Note that the configuration in
|
|
|
|
one's =gpg.conf= file is honoured, so if you have the options set
|
|
|
|
to encrypt to one key or to a default key, that will be included
|
|
|
|
with this operation.
|
|
|
|
|
|
|
|
This is followed by a quick check to be sure that the recipient is
|
|
|
|
actually selected and that the key is available. Assuming it is,
|
|
|
|
the encryption can proceed, but if not a message will print stating
|
|
|
|
the key was not found.
|
|
|
|
|
|
|
|
The encryption operation is invoked within the Context with the
|
2018-03-15 03:59:36 +00:00
|
|
|
=c.op_encrypt= function, loading the recipients (=r=), the message
|
2018-03-09 05:49:05 +00:00
|
|
|
(=plain=) and the =cipher=. The =cipher.seek= uses =os.SEEK_SET=
|
|
|
|
to set the data to the correct byte format for GPGME to use it.
|
|
|
|
|
|
|
|
At this point we no longer need the plaintext material, so we
|
|
|
|
delete both the =text= and the =plain= objects. Then we write the
|
|
|
|
encrypted data out to a file, =secret_plans.txt.asc=.
|
2018-03-08 17:42:41 +00:00
|
|
|
|
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
import os
|
|
|
|
|
|
|
|
rkey = "0x12345678DEADBEEF"
|
|
|
|
text = """
|
|
|
|
Some plain text to test with. Obtained from any input source Python can read.
|
|
|
|
|
|
|
|
It makes no difference whether it is string or bytes, but the bindings always
|
|
|
|
produce byte output data. Which is useful to know when writing out either the
|
|
|
|
encrypted or decrypted results.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
plain = gpg.core.Data(text)
|
|
|
|
cipher = gpg.core.Data()
|
|
|
|
c = gpg.core.Context()
|
|
|
|
c.set_armor(1)
|
|
|
|
|
|
|
|
c.op_keylist_start(rkey, 0)
|
|
|
|
r = c.op_keylist_next()
|
|
|
|
|
|
|
|
if r == None:
|
|
|
|
print("""The key for user "{0}" was not found""".format(rkey))
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
c.op_encrypt([r], 1, plain, cipher)
|
|
|
|
cipher.seek(0, os.SEEK_SET)
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("secret_plans.txt.asc", "wb") as afile:
|
|
|
|
afile.write(cipher.read())
|
2018-03-08 17:42:41 +00:00
|
|
|
except gpg.errors.GPGMEError as ex:
|
|
|
|
print(ex.getstring())
|
|
|
|
#+end_src
|
|
|
|
|
2018-03-18 21:49:17 +00:00
|
|
|
*** Encrypting to one key using the second method
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-basic-encryption-monogamous
|
|
|
|
:END:
|
|
|
|
|
|
|
|
This example re-creates the first encryption example except it
|
|
|
|
uses the same =encrypt= method used in the subsequent examples
|
|
|
|
instead of the =op_encrypt= method. This means that, unlike the
|
|
|
|
=op_encrypt= method, it /must/ use byte literal input data.
|
|
|
|
|
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
|
|
|
|
rkey = "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)
|
|
|
|
rpattern = list(c.keylist(pattern=rkey, secret=False))
|
|
|
|
logrus = []
|
|
|
|
|
|
|
|
for i in range(len(rpattern)):
|
|
|
|
if rpattern[i].can_encrypt == 1:
|
|
|
|
logrus.append(rpattern[i])
|
|
|
|
|
|
|
|
cipher = c.encrypt(text, recipients=logrus, sign=False, always_trust=True)
|
|
|
|
|
|
|
|
with open("secret_plans.txt.asc", "wb") as afile:
|
|
|
|
afile.write(cipher[0])
|
|
|
|
#+end_src
|
|
|
|
|
|
|
|
With one or two exceptions, this method will probably prove to be
|
|
|
|
easier to implement than the first method and thus it is the
|
|
|
|
recommended encryption method. Though it is even more likely to
|
|
|
|
be used like this:
|
|
|
|
|
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
|
|
|
|
rkey = "0x12345678DEADBEEF"
|
|
|
|
|
|
|
|
afile = open("secret_plans.txt", "rb")
|
|
|
|
text = afile.read()
|
|
|
|
afile.close()
|
|
|
|
|
|
|
|
c = gpg.Context(armor=True)
|
|
|
|
rpattern = list(c.keylist(pattern=rkey, secret=False))
|
|
|
|
logrus = []
|
|
|
|
|
|
|
|
for i in range(len(rpattern)):
|
|
|
|
if rpattern[i].can_encrypt == 1:
|
|
|
|
logrus.append(rpattern[i])
|
|
|
|
|
|
|
|
cipher = c.encrypt(text, recipients=logrus, sign=False, always_trust=True)
|
|
|
|
|
|
|
|
with open("secret_plans.txt.asc", "wb") as afile:
|
|
|
|
afile.write(cipher[0])
|
|
|
|
#+end_src
|
|
|
|
|
2018-03-09 05:49:05 +00:00
|
|
|
*** Encrypting to multiple keys
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-basic-encryption-multiple
|
|
|
|
:END:
|
|
|
|
|
2018-03-12 17:55:44 +00:00
|
|
|
Encrypting to multiple keys, in addition to a default key or a key
|
|
|
|
configured to always encrypt to, is a little different and uses a
|
2018-03-15 05:13:34 +00:00
|
|
|
slightly different call to the =op_encrypt= call demonstrated in the
|
2018-03-12 17:55:44 +00:00
|
|
|
previous section.
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2018-03-12 19:09:53 +00:00
|
|
|
text = b"""Oh look, another test message.
|
2018-03-12 17:55:44 +00:00
|
|
|
|
|
|
|
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))
|
2018-03-13 14:41:21 +00:00
|
|
|
logrus = []
|
2018-03-12 17:55:44 +00:00
|
|
|
|
|
|
|
for i in range(len(rpattern)):
|
|
|
|
if rpattern[i].can_encrypt == 1:
|
2018-03-13 14:41:21 +00:00
|
|
|
logrus.append(rpattern[i])
|
2018-03-12 17:55:44 +00:00
|
|
|
|
2018-03-13 14:41:21 +00:00
|
|
|
cipher = c.encrypt(text, recipients=logrus, sign=False, always_trust=True)
|
2018-03-12 17:55:44 +00:00
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("secret_plans.txt.asc", "wb") as afile:
|
|
|
|
afile.write(cipher[0])
|
2018-03-12 17:55:44 +00:00
|
|
|
#+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
|
2018-03-13 14:41:21 +00:00
|
|
|
cipher = c.encrypt(text, recipients=logrus, always_trust=True,
|
2018-03-12 17:55:44 +00:00
|
|
|
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
|
2018-03-12 20:42:04 +00:00
|
|
|
import gpg
|
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("secret_plans.txt.asc", "rb") as afile:
|
|
|
|
text = afile.read()
|
2018-03-12 20:42:04 +00:00
|
|
|
|
|
|
|
c = gpg.Context(armor=True)
|
|
|
|
rpattern = list(c.keylist(pattern="@gnupg.org", secret=False))
|
2018-03-13 14:41:21 +00:00
|
|
|
logrus = []
|
2018-03-12 20:42:04 +00:00
|
|
|
|
|
|
|
for i in range(len(rpattern)):
|
|
|
|
if rpattern[i].can_encrypt == 1:
|
2018-03-13 14:41:21 +00:00
|
|
|
logrus.append(rpattern[i])
|
2018-03-12 20:42:04 +00:00
|
|
|
|
2018-03-12 17:55:44 +00:00
|
|
|
try:
|
2018-03-13 14:41:21 +00:00
|
|
|
cipher = c.encrypt(text, recipients=logrus, add_encrypt_to=True)
|
2018-03-12 17:55:44 +00:00
|
|
|
except gpg.errors.InvalidRecipients as e:
|
|
|
|
for i in range(len(e.recipients)):
|
2018-03-13 14:41:21 +00:00
|
|
|
for n in range(len(logrus)):
|
|
|
|
if logrus[n].fpr == e.recipients[i].fpr:
|
|
|
|
logrus.remove(logrus[n])
|
2018-03-12 18:42:50 +00:00
|
|
|
else:
|
|
|
|
pass
|
2018-03-12 17:55:44 +00:00
|
|
|
try:
|
2018-03-13 14:41:21 +00:00
|
|
|
cipher = c.encrypt(text, recipients=logrus, add_encrypt_to=True)
|
2018-03-12 17:55:44 +00:00
|
|
|
except:
|
|
|
|
pass
|
2018-03-12 20:42:04 +00:00
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("secret_plans.txt.asc", "wb") as afile:
|
|
|
|
afile.write(cipher[0])
|
2018-03-12 17:55:44 +00:00
|
|
|
#+end_src
|
|
|
|
|
|
|
|
This will attempt to encrypt to all the keys searched for, then
|
|
|
|
remove invalid recipients if it fails and try again.
|
|
|
|
|
2018-03-09 05:49:05 +00:00
|
|
|
|
2018-03-08 18:25:49 +00:00
|
|
|
** Decryption
|
|
|
|
:PROPERTIES:
|
2018-03-15 14:48:56 +00:00
|
|
|
:CUSTOM_ID: howto-basic-decryption
|
2018-03-08 18:25:49 +00:00
|
|
|
:END:
|
|
|
|
|
2018-03-13 00:50:38 +00:00
|
|
|
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.
|
2018-03-08 18:25:49 +00:00
|
|
|
|
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
|
2018-03-18 21:43:36 +00:00
|
|
|
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)
|
2018-03-08 18:25:49 +00:00
|
|
|
#+end_src
|
|
|
|
|
2018-03-13 00:50:38 +00:00
|
|
|
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]=.
|
|
|
|
|
2018-03-09 05:49:05 +00:00
|
|
|
|
2018-03-13 07:32:30 +00:00
|
|
|
** Signing text and files
|
2018-03-08 20:53:57 +00:00
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-basic-signing
|
|
|
|
:END:
|
|
|
|
|
2018-03-13 14:41:21 +00:00
|
|
|
The following sections demonstrate how to specify
|
2018-03-13 07:32:30 +00:00
|
|
|
|
|
|
|
*** 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.
|
|
|
|
|
2018-03-13 08:20:44 +00:00
|
|
|
#+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.
|
|
|
|
|
2018-03-13 14:41:21 +00:00
|
|
|
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.
|
|
|
|
|
2018-03-13 07:32:30 +00:00
|
|
|
*** Normal or default signing messages or files
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-basic-signing-normal
|
|
|
|
:END:
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-13 15:40:41 +00:00
|
|
|
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=.
|
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
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.
|
|
|
|
|
2018-03-08 20:53:57 +00:00
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
text0 = """Declaration of ... something.
|
2018-03-08 20:53:57 +00:00
|
|
|
|
|
|
|
"""
|
2018-03-16 16:46:02 +00:00
|
|
|
text = text0.encode()
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-13 08:20:44 +00:00
|
|
|
c = gpg.Context(armor=True, signers=sig_src)
|
2018-03-13 04:03:11 +00:00
|
|
|
signed = c.sign(text, mode=0)
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt.asc", "w") as afile:
|
|
|
|
for line in signed[0]:
|
|
|
|
afile.write("{0}\n".format(line.decode()))
|
2018-03-08 20:53:57 +00:00
|
|
|
#+end_src
|
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
Though everything in this example is accurate, it is more likely
|
|
|
|
that reading the input data from another file and writing the
|
2018-03-15 03:59:36 +00:00
|
|
|
result to a new file will be performed more like the way it is done
|
2018-03-14 16:51:51 +00:00
|
|
|
in the next example. Even if the output format is ASCII armoured.
|
|
|
|
|
2018-03-08 20:53:57 +00:00
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt", "rb") as tfile:
|
|
|
|
text = tfile.read()
|
2018-03-08 20:53:57 +00:00
|
|
|
|
|
|
|
c = gpg.Context()
|
2018-03-13 07:32:30 +00:00
|
|
|
signed = c.sign(text, mode=0)
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt.sig", "wb") as afile:
|
|
|
|
afile.write(signed[0])
|
2018-03-08 20:53:57 +00:00
|
|
|
#+end_src
|
|
|
|
|
2018-03-13 07:32:30 +00:00
|
|
|
*** Detached signing messages and files
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-basic-signing-detached
|
|
|
|
:END:
|
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
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).
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
text0 = """Declaration of ... something.
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
"""
|
2018-03-16 16:46:02 +00:00
|
|
|
text = text0.encode()
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
c = gpg.Context(armor=True)
|
|
|
|
signed = c.sign(text, mode=1)
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt.asc", "w") as afile:
|
|
|
|
for line in signed[0].splitlines():
|
|
|
|
afile.write("{0}\n".format(line.decode()))
|
2018-03-14 16:51:51 +00:00
|
|
|
#+end_src
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
As with normal signatures, detached signatures are best handled as
|
|
|
|
byte literals, even when the output is ASCII armoured.
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt", "rb") as tfile:
|
|
|
|
text = tfile.read()
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-14 16:51:51 +00:00
|
|
|
c = gpg.Context(signers=sig_src)
|
|
|
|
signed = c.sign(text, mode=1)
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt.sig", "wb") as afile:
|
|
|
|
afile.write(signed[0])
|
2018-03-14 16:51:51 +00:00
|
|
|
#+end_src
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-13 07:32:30 +00:00
|
|
|
*** Clearsigning messages or text
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-basic-signing-clear
|
|
|
|
:END:
|
|
|
|
|
2018-03-14 17:07:57 +00:00
|
|
|
Though PGP/in-line messages are no longer encouraged in favour of
|
|
|
|
PGP/MIME, there is still sometimes value in utilising in-line
|
2018-03-14 20:20:31 +00:00
|
|
|
signatures. This is where clear-signed messages or text is of
|
2018-03-14 17:07:57 +00:00
|
|
|
value.
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-14 17:07:57 +00:00
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-14 17:07:57 +00:00
|
|
|
text0 = """Declaration of ... something.
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-14 17:07:57 +00:00
|
|
|
"""
|
2018-03-16 16:46:02 +00:00
|
|
|
text = text0.encode()
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-14 17:07:57 +00:00
|
|
|
c = gpg.Context()
|
|
|
|
signed = c.sign(text, mode=2)
|
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt.asc", "w") as afile:
|
|
|
|
for line in signed[0].splitlines():
|
|
|
|
afile.write("{0}\n".format(line.decode()))
|
2018-03-14 17:07:57 +00:00
|
|
|
#+end_src
|
|
|
|
|
2018-03-14 20:20:31 +00:00
|
|
|
In spite of the appearance of a clear-signed message, the data
|
2018-03-14 17:07:57 +00:00
|
|
|
handled by GPGME in signing it must still be byte literals.
|
|
|
|
|
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt", "rb") as tfile:
|
|
|
|
text = tfile.read()
|
2018-03-14 17:07:57 +00:00
|
|
|
|
|
|
|
c = gpg.Context()
|
|
|
|
signed = c.sign(text, mode=2)
|
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
with open("/path/to/statement.txt.asc", "wb") as afile:
|
|
|
|
afile.write(signed[0])
|
2018-03-14 17:07:57 +00:00
|
|
|
#+end_src
|
2018-03-13 07:32:30 +00:00
|
|
|
|
2018-03-08 20:53:57 +00:00
|
|
|
|
|
|
|
** Signature verification
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: howto-basic-verification
|
|
|
|
:END:
|
|
|
|
|
2018-03-14 20:20:31 +00:00
|
|
|
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:
|
2018-03-09 04:22:24 +00:00
|
|
|
|
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
import time
|
|
|
|
|
2018-03-14 20:20:31 +00:00
|
|
|
filename = "statement.txt"
|
|
|
|
gpg_file = "statement.txt.gpg"
|
|
|
|
|
2018-03-09 04:22:24 +00:00
|
|
|
c = gpg.Context()
|
|
|
|
|
2018-03-14 20:20:31 +00:00
|
|
|
try:
|
|
|
|
verified = c.verify(open(gpg_file))
|
|
|
|
except gpg.errors.BadSignatures as e:
|
|
|
|
verified = None
|
|
|
|
print(e)
|
|
|
|
|
|
|
|
if verified is not None:
|
|
|
|
for i in range(len(verified[1].signatures)):
|
|
|
|
sign = verified[1].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(e)
|
|
|
|
#+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:
|
|
|
|
verified = c.verify(open(asc_file))
|
|
|
|
except gpg.errors.BadSignatures as e:
|
|
|
|
verified = None
|
|
|
|
print(e)
|
|
|
|
|
|
|
|
if verified is not None:
|
|
|
|
for i in range(len(verified[1].signatures)):
|
|
|
|
sign = verified[1].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
|
|
|
|
|
2018-03-15 00:18:02 +00:00
|
|
|
In both of the previous examples it is also possible to compare the
|
|
|
|
original data that was signed against the signed data in
|
|
|
|
=verified[0]= to see if it matches with something like this:
|
|
|
|
|
|
|
|
#+begin_src python
|
|
|
|
afile = open(filename, "rb")
|
|
|
|
text = afile.read()
|
|
|
|
afile.close()
|
|
|
|
|
|
|
|
if text == verified[0]:
|
|
|
|
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 =verified[0]= is None and only
|
|
|
|
the data in =verified[1]= is available.
|
|
|
|
|
2018-03-14 20:20:31 +00:00
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
import time
|
|
|
|
|
|
|
|
filename = "statement.txt"
|
|
|
|
sig_file = "statement.txt.sig"
|
|
|
|
|
|
|
|
c = gpg.Context()
|
|
|
|
|
|
|
|
try:
|
|
|
|
verified = c.verify(open(filename), open(sig_file))
|
|
|
|
except gpg.errors.BadSignatures as e:
|
|
|
|
verified = None
|
|
|
|
print(e)
|
|
|
|
|
|
|
|
if verified is not None:
|
|
|
|
for i in range(len(verified[1].signatures)):
|
|
|
|
sign = verified[1].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:
|
|
|
|
verified = c.verify(open(filename), open(asc_file))
|
|
|
|
except gpg.errors.BadSignatures as e:
|
|
|
|
verified = None
|
|
|
|
print(e)
|
|
|
|
|
|
|
|
if verified is not None:
|
|
|
|
for i in range(len(verified[1].signatures)):
|
|
|
|
sign = verified[1].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
|
2018-03-09 04:22:24 +00:00
|
|
|
#+end_src
|
2018-03-08 20:53:57 +00:00
|
|
|
|
2018-03-08 17:42:41 +00:00
|
|
|
|
2018-03-15 01:27:45 +00:00
|
|
|
* 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.
|
|
|
|
|
2018-03-15 03:01:30 +00:00
|
|
|
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.
|
|
|
|
|
2018-03-15 03:43:44 +00:00
|
|
|
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
|
|
|
|
# no-auto-check-trustdb
|
|
|
|
enable-large-rsa
|
|
|
|
enable-dsa2
|
|
|
|
# no-emit-version
|
|
|
|
# no-comments
|
|
|
|
# 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
|
|
|
|
|
2018-03-15 01:27:45 +00:00
|
|
|
|
|
|
|
** Primary key
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: keygen-primary
|
|
|
|
:END:
|
|
|
|
|
2018-03-15 03:01:30 +00:00
|
|
|
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()
|
|
|
|
|
2018-03-16 16:46:02 +00:00
|
|
|
c.home_dir = "~/.gnupg-dm"
|
2018-03-15 03:01:30 +00:00
|
|
|
userid = "Danger Mouse <dm@secret.example.net>"
|
|
|
|
|
|
|
|
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
|
2018-03-16 16:46:02 +00:00
|
|
|
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.
|
2018-03-15 03:01:30 +00:00
|
|
|
|
|
|
|
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
|
2018-03-16 16:46:02 +00:00
|
|
|
bash-4.4$ gpg --homedir ~/.gnupg-dm -K
|
|
|
|
~/.gnupg-dm/pubring.kbx
|
2018-03-15 03:01:30 +00:00
|
|
|
----------------------
|
|
|
|
sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15]
|
|
|
|
177B7C25DB99745EE2EE13ED026D2F19E99E63AA
|
|
|
|
uid [ultimate] Danger Mouse <dm@secret.example.net>
|
|
|
|
|
|
|
|
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
|
2018-03-16 16:46:02 +00:00
|
|
|
bash-4.4$ gpg --homedir ~/.gnupg-dm --edit-key 177B7C25DB99745EE2EE13ED026D2F19E99E63AA showpref quit
|
2018-03-15 03:01:30 +00:00
|
|
|
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$
|
|
|
|
#+end_src
|
|
|
|
|
2018-03-15 01:27:45 +00:00
|
|
|
|
|
|
|
** Subkeys
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: keygen-subkeys
|
|
|
|
:END:
|
|
|
|
|
2018-03-15 03:43:44 +00:00
|
|
|
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()
|
2018-03-16 16:46:02 +00:00
|
|
|
c.home_dir = "~/.gnupg-dm"
|
2018-03-15 03:43:44 +00:00
|
|
|
|
|
|
|
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
|
2018-03-16 16:46:02 +00:00
|
|
|
bash-4.4$ gpg --homedir ~/.gnupg-dm -K
|
|
|
|
~/.gnupg-dm/pubring.kbx
|
2018-03-15 03:43:44 +00:00
|
|
|
----------------------
|
|
|
|
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$
|
|
|
|
#+end_src
|
|
|
|
|
2018-03-15 01:27:45 +00:00
|
|
|
|
|
|
|
** User IDs
|
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: keygen-uids
|
|
|
|
:END:
|
|
|
|
|
2018-03-15 04:16:23 +00:00
|
|
|
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=.
|
2018-03-15 01:27:45 +00:00
|
|
|
|
2018-03-15 04:16:23 +00:00
|
|
|
#+begin_src python
|
|
|
|
import gpg
|
|
|
|
|
|
|
|
c = gpg.Context()
|
2018-03-16 16:46:02 +00:00
|
|
|
c.home_dir = "~/.gnupg-dm"
|
2018-03-15 04:16:23 +00:00
|
|
|
|
|
|
|
dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA"
|
|
|
|
key = c.get_key(dmfpr, secret = True)
|
|
|
|
uid = "Danger Mouse <danger.mouse@secret.example.net>"
|
|
|
|
|
|
|
|
c.key_add_uid(key, uid)
|
|
|
|
#+end_src
|
|
|
|
|
|
|
|
Unsurprisingly the result of this is:
|
|
|
|
|
|
|
|
#+begin_src shell
|
2018-03-16 16:46:02 +00:00
|
|
|
bash-4.4$ gpg --homedir ~/.gnupg-dm -K
|
|
|
|
~/.gnupg-dm/pubring.kbx
|
2018-03-15 04:16:23 +00:00
|
|
|
----------------------
|
|
|
|
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$
|
|
|
|
#+end_src
|
2018-03-15 01:27:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
** Key certification
|
|
|
|
:PROPERTIES:
|
2018-03-15 04:16:23 +00:00
|
|
|
:CUSTOM_ID: key-sign
|
2018-03-15 01:27:45 +00:00
|
|
|
:END:
|
|
|
|
|
2018-03-15 04:51:01 +00:00
|
|
|
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 <dm@secret.example.net>"
|
|
|
|
|
|
|
|
dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA"
|
|
|
|
key = c.get_key(dmfpr, secret = True)
|
|
|
|
c.key_sign(key, uids = uid, expires_in = 2764800)
|
|
|
|
#+end_src
|
|
|
|
|
2018-03-15 01:27:45 +00:00
|
|
|
|
2018-03-12 21:26:22 +00:00
|
|
|
* 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.
|
|
|
|
|
|
|
|
|
2018-03-07 09:05:21 +00:00
|
|
|
* 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.
|
|
|
|
|
2018-03-07 10:27:54 +00:00
|
|
|
** License GPL compatible
|
2018-03-07 09:05:21 +00:00
|
|
|
:PROPERTIES:
|
|
|
|
:CUSTOM_ID: license
|
|
|
|
:END:
|
|
|
|
|
2018-03-07 10:27:54 +00:00
|
|
|
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.
|
2018-03-12 13:33:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
* Footnotes
|
|
|
|
|
2018-03-12 19:09:53 +00:00
|
|
|
[fn:1] =Short_History.org= and/or =Short_History.html=.
|
2018-03-12 13:33:11 +00:00
|
|
|
|
|
|
|
[fn:2] The =lang/python/docs/= directory in the GPGME source.
|
2018-03-12 17:55:44 +00:00
|
|
|
|
|
|
|
[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.
|