diff options
author | Ben McGinnes <[email protected]> | 2018-10-03 21:09:40 +0000 |
---|---|---|
committer | Ben McGinnes <[email protected]> | 2018-10-03 21:09:40 +0000 |
commit | 962dfca9b86b53bb00f5b89e453b9eed95454934 (patch) | |
tree | 6fef9dfce101a7ed34ceed780e8eadf922a70595 /lang/python/docs/texinfo/gpgme-python-howto.texi | |
parent | docs: python (diff) | |
download | gpgme-962dfca9b86b53bb00f5b89e453b9eed95454934.tar.gz gpgme-962dfca9b86b53bb00f5b89e453b9eed95454934.zip |
dpcs: python howto
* More updates to the docs themselves and the versions to be available
with the next release.
* .texi and .rst copies of the HOWTO and the short history of (this
part) of the project.
Diffstat (limited to '')
-rw-r--r-- | lang/python/docs/texinfo/gpgme-python-howto.texi | 3156 |
1 files changed, 3156 insertions, 0 deletions
diff --git a/lang/python/docs/texinfo/gpgme-python-howto.texi b/lang/python/docs/texinfo/gpgme-python-howto.texi new file mode 100644 index 00000000..b474242d --- /dev/null +++ b/lang/python/docs/texinfo/gpgme-python-howto.texi @@ -0,0 +1,3156 @@ +\input texinfo @c -*- texinfo -*- +@c %**start of header +@setfilename gpgme-python-howto.info +@settitle GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English) +@documentencoding UTF-8 +@documentlanguage en +@c %**end of header + +@finalout +@titlepage +@title GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English) +@author Ben McGinnes +@end titlepage + +@contents + +@ifnottex +@node Top +@top GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English) +@end ifnottex + +@menu +* Introduction:: +* GPGME Concepts:: +* GPGME Python bindings installation:: +* Fundamentals:: +* Working with keys:: +* Basic Functions:: +* Creating keys and subkeys:: +* Advanced or Experimental Use Cases:: +* Miscellaneous extras and work-arounds:: +* Copyright and Licensing:: + +@detailmenu +--- The Detailed Node Listing --- + +Introduction + +* Python 2 versus Python 3:: +* Examples:: +* Unofficial Drafts:: +* What's New:: + +What's New + +* New in GPGME 1·12·0:: + +GPGME Concepts + +* A C API:: +* Python bindings:: +* Difference between the Python bindings and other GnuPG Python packages:: + +Difference between the Python bindings and other GnuPG Python packages + +* The python-gnupg package maintained by Vinay Sajip:: +* The gnupg package created and maintained by Isis Lovecruft:: +* The PyME package maintained by Martin Albrecht:: + +GPGME Python bindings installation + +* No PyPI:: +* Requirements:: +* Installation:: +* Known Issues:: + +Requirements + +* Recommended Additions:: + +Installation + +* Installing GPGME:: + +Known Issues + +* Breaking Builds:: +* Reinstalling Responsibly:: +* Multiple installations:: +* Won't Work With Windows:: +* CFFI is the Best™ and GPGME should use it instead of SWIG:: +* Virtualised Environments:: + +Fundamentals + +* No REST:: +* Context:: + +Working with keys + +* Key selection:: +* Get key:: +* Importing keys:: +* Exporting keys:: + +Key selection + +* Counting keys:: + +Importing keys + +* Working with ProtonMail:: +* Importing with HKP for Python:: +* Importing from ProtonMail with HKP for Python:: + +Exporting keys + +* Exporting public keys:: +* Exporting secret keys:: +* Sending public keys to the SKS Keyservers:: + +Basic Functions + +* Encryption:: +* Decryption:: +* Signing text and files:: +* Signature verification:: + +Encryption + +* Encrypting to one key:: +* Encrypting to multiple keys:: + +Signing text and files + +* Signing key selection:: +* Normal or default signing messages or files:: +* Detached signing messages and files:: +* Clearsigning messages or text:: + +Creating keys and subkeys + +* Primary key:: +* Subkeys:: +* User IDs:: +* Key certification:: + +User IDs + +* Adding User IDs:: +* Revokinging User IDs:: + +Advanced or Experimental Use Cases + +* C plus Python plus SWIG plus Cython:: + +Miscellaneous extras and work-arounds + +* Group lines:: +* Keyserver access for Python:: + +Keyserver access for Python + +* Key import format:: + +Copyright and Licensing + +* Copyright:: +* Draft Editions of this HOWTO:: +* License GPL compatible:: + +@end detailmenu +@end menu + +@node Introduction +@chapter Introduction + +@multitable {aaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item Version: +@tab 0.1.4 +@item GPGME Version: +@tab 1.12.0 +@item Author: +@tab @uref{https://gnupg.org/people/index.html#sec-1-5, Ben McGinnes} <ben@@gnupg.org> +@item Author GPG Key: +@tab DB4724E6FA4286C92B4E55C4321E4E2373590E5D +@item Language: +@tab Australian English, British English +@item xml:lang: +@tab en-AU, en-GB, en +@end multitable + +This document provides basic instruction in how to use the GPGME +Python bindings to programmatically leverage the GPGME library. + +@menu +* Python 2 versus Python 3:: +* Examples:: +* Unofficial Drafts:: +* What's New:: +@end menu + +@node Python 2 versus Python 3 +@section Python 2 versus Python 3 + +Though the GPGME Python bindings themselves provide support for both +Python 2 and 3, the focus is unequivocally on Python 3 and +specifically from Python 3.4 and above. As a consequence all the +examples and instructions in this guide use Python 3 code. + +Much of it will work with Python 2, but much of it also deals with +Python 3 byte literals, particularly when reading and writing data. +Developers concentrating on Python 2.7, and possibly even 2.6, will +need to make the appropriate modifications to support the older string +and unicode types as opposed to bytes. + +There are multiple reasons for concentrating on Python 3; some of +which relate to the immediate integration of these bindings, some of +which relate to longer term plans for both GPGME and the python +bindings and some of which relate to the impending EOL period for +Python 2.7. Essentially, though, there is little value in tying the +bindings to a version of the language which is a dead end and the +advantages offered by Python 3 over Python 2 make handling the data +types with which GPGME deals considerably easier. + +@node Examples +@section Examples + +All of the examples found in this document can be found as Python 3 +scripts in the @samp{lang/python/examples/howto} directory. + +@node Unofficial Drafts +@section Unofficial Drafts + +In addition to shipping with each release of GPGME, there is a section +on locations to read or download @ref{Draft Editions of this HOWTO, , draft editions} of this document from +at the end of it. These are unofficial versions produced in between +major releases. + +@node What's New +@section What's New + +The most obviously new point for those reading this guide is this +section on other new things, but that's hardly important. Not given +all the other things which spurred the need for adding this section +and its subsections. + +@menu +* New in GPGME 1·12·0:: +@end menu + +@node New in GPGME 1·12·0 +@subsection New in GPGME 1·12·0 + +There have been quite a number of additions to GPGME and the Python +bindings to it since the last release of GPGME with versions 1.11.0 +and 1.11.1 in April, 2018. + +The bullet points of new additiions are: + +@itemize +@item +an expanded section on @ref{Installation, , installing} and @ref{Known Issues, , troubleshooting} the Python +bindings. +@item +The release of Python 3.7.0; which appears to be working just fine +with our bindings, in spite of intermittent reports of problems for +many other Python projects with that new release. +@item +Python 3.7 has been moved to the head of the specified python +versions list in the build process. +@item +In order to fix some other issues, there are certain underlying +functions which are more exposed through the @ref{Context, , gpg.Context()}, but +ongoing documentation ought to clarify that or otherwise provide the +best means of using the bindings. Some additions to @samp{gpg.core} and +the @samp{Context()}, however, were intended (see below). +@item +Continuing work in identifying and confirming the cause of +oft-reported @ref{Won't Work With Windows, , problems installing the Python bindings on Windows}. +@item +GSOC: Google's Surreptitiously Ordered Conscription @dots{} erm @dots{} oh, +right; Google's Summer of Code. Though there were two hopeful +candidates this year; only one ended up involved with the GnuPG +Project directly, the other concentrated on an unrelated third party +project with closer ties to one of the GNU/Linux distributions than +to the GnuPG Project. Thus the Python bindings benefited from GSOC +participant Jacob Adams, who added the key@math{_import} function; building +on prior work by Tobias Mueller. +@item +Several new methods functions were added to the gpg.Context(), +including: @ref{Importing keys, , key@math{_import}}, @ref{Exporting keys, , key@math{_export}}, @ref{Exporting public keys, , key@math{_export}@math{_minimal}} and +@ref{Exporting secret keys, , key@math{_export}@math{_secret}}. +@item +Importing and exporting examples include versions integrated with +Marcel Fest's recently released @uref{https://github.com/Selfnet/hkp4py, HKP for Python} module. Some +@ref{Keyserver access for Python, , additional notes on this module} are included at the end of the HOWTO. +@item +Instructions for dealing with semi-walled garden implementations +like ProtonMail are also included. This is intended to make things +a little easier when communicating with users of ProtonMail's +services and should not be construed as an endorsement of said +service. The GnuPG Project neither favours, nor disfavours +ProtonMail and the majority of this deals with interacting with the +ProtonMail keyserver. +@item +Semi-formalised the location where @ref{Draft Editions of this HOWTO, , draft versions} of this HOWTO may +periodically be accessible. This is both for the reference of +others and testing the publishing of the document itself. Renamed +this file at around the same time. +@item +The Texinfo documentation build configuration has been replicated +from the parent project in order to make to maintain consistency +with that project (and actually ship with each release). +@item +a reStructuredText (@samp{.rst}) version is also generated for Python +developers more used to and comfortable with that format as it is +the standard Python documentation format and Python developers may +wish to use it with Sphinx. Please note that there has been no +testing of the reStructuredText version with Sphinx at all. The +reST file was generated by the simple expedient of using @uref{https://pandoc.org/, Pandoc}. +@item +Added a new section for @ref{Advanced or Experimental Use Cases, , advanced or experimental use}. +@item +Began the advanced use cases with @ref{C plus Python plus SWIG plus Cython, , a section} on using the module with +@uref{http://cython.org/, Cython}. +@item +Added a number of new scripts to the @samp{example/howto/} directory; +some of which may be in advance of their planned sections of the +HOWTO (and some are just there because it seemed like a good idea at +the time). +@item +Cleaned up a lot of things under the hood. +@end itemize + +@node GPGME Concepts +@chapter GPGME Concepts + +@menu +* A C API:: +* Python bindings:: +* Difference between the Python bindings and other GnuPG Python packages:: +@end menu + +@node A C API +@section A C API + +Unlike many modern APIs with which programmers will be more familiar +with these days, the GPGME API is a C API. The API is intended for +use by C coders who would be able to access its features by including +the @samp{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. + +@node Python bindings +@section Python bindings + +The Python bindings for GPGME provide a higher level means of +accessing the complete feature set of GPGME itself. It also provides +a more pythonic means of calling these API functions. + +The bindings are generated dynamically with SWIG and the copy of +@samp{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 +@samp{gpgme.h}. + +@node Difference between the Python bindings and other GnuPG Python packages +@section Difference between the Python bindings and other GnuPG Python packages + +There have been numerous attempts to add GnuPG support to Python over +the years. Some of the most well known are listed here, along with +what differentiates them. + +@menu +* The python-gnupg package maintained by Vinay Sajip:: +* The gnupg package created and maintained by Isis Lovecruft:: +* The PyME package maintained by Martin Albrecht:: +@end menu + +@node The python-gnupg package maintained by Vinay Sajip +@subsection The python-gnupg package maintained by Vinay Sajip + +This is arguably the most popular means of integrating GPG with +Python. The package utilises the @samp{subprocess} module to implement +wrappers for the @samp{gpg} and @samp{gpg2} executables normally invoked on the +command line (@samp{gpg.exe} and @samp{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 @samp{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. + +@node The gnupg package created and maintained by Isis Lovecruft +@subsection The gnupg package created and maintained by Isis Lovecruft + +In 2015 Isis Lovecruft from the Tor Project forked and then +re-implemented the python-gnupg package as just gnupg. This new +package also relied on subprocess to call the @samp{gpg} or @samp{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). + +@node The PyME package maintained by Martin Albrecht +@subsection The PyME package maintained by Martin Albrecht + +This package is the origin of these bindings, though they are somewhat +different now. For details of when and how the PyME package was +folded back into GPGME itself see the @emph{Short History} document@footnote{@samp{short-history.org} and/or @samp{short-history.html}.} +in the Python bindings @samp{docs} directory.@footnote{The @samp{lang/python/docs/} directory in the GPGME source.} + +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 +@samp{python-gnupg} or @samp{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). + +@node GPGME Python bindings installation +@chapter GPGME Python bindings installation + +@menu +* No PyPI:: +* Requirements:: +* Installation:: +* Known Issues:: +@end menu + +@node No PyPI +@section No PyPI + +Most third-party Python packages and modules are available and +distributed through the Python Package Installer, known as PyPI. + +Due to the nature of what these bindings are and how they work, it is +infeasible to install the GPGME Python bindings in the same way. + +This is because the bindings use SWIG to dynamically generate C +bindings against @samp{gpgme.h} and @samp{gpgme.h} is generated from +@samp{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. + +See the additional notes regarding @ref{CFFI is the Best™ and GPGME should use it instead of SWIG, , CFFI and SWIG} at the end of this +section for further details. + +@node Requirements +@section Requirements + +The GPGME Python bindings only have three requirements: + +@enumerate +@item +A suitable version of Python 2 or Python 3. With Python 2 that +means CPython 2.7 and with Python 3 that means CPython 3.4 or +higher. +@item +@uref{https://www.swig.org, SWIG}. +@item +GPGME itself. Which also means that all of GPGME's dependencies +must be installed too. +@end enumerate + +@menu +* Recommended Additions:: +@end menu + +@node Recommended Additions +@subsection Recommended Additions + +Though none of the following are absolute requirements, they are all +recommended for use with the Python bindings. In some cases these +recommendations refer to which version(s) of CPython to use the +bindings with, while others refer to third party modules which provide +a significant advantage in some way. + +@enumerate +@item +If possible, use Python 3 instead of 2. +@item +Favour a more recent version of Python since even 3.4 is due to +reach EOL soon. In production systems and services, Python 3.6 +should be robust enough to be relied on. +@item +If possible add the following Python modules which are not part of +the standard library: @uref{http://docs.python-requests.org/en/latest/index.html, Requests}, @uref{http://cython.org/, Cython} and @uref{https://github.com/Selfnet/hkp4py, hkp4py}. Chances are +quite high that at least the first one and maybe two of those will +already be installed. +@end enumerate + +Note that, as with Cython, some of the planned additions to the +@ref{Advanced or Experimental Use Cases, , Advanced} section, will bring with them additional requirements. Most +of these will be fairly well known and commonly installed ones, +however, which are in many cases likely to have already been installed +on many systems or be familiar to Python programmers. + +@node Installation +@section Installation + +Installing the Python bindings is effectively achieved by compiling +and installing GPGME itself. + +Once SWIG is installed with Python and all the dependencies for GPGME +are installed you only need to confirm that the version(s) of Python +you want the bindings installed for are in your @samp{$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 @samp{$PATH}. It specifically checks for the @samp{python} and @samp{python3} +executables first and then checks for specific version numbers. + +For Python 2 it checks for these executables in this order: @samp{python}, +@samp{python2} and @samp{python2.7}. + +For Python 3 it checks for these executables in this order: @samp{python3}, + @samp{python3.7}, @samp{python3.6}, @samp{python3.5} and @samp{python3.4}.@footnote{With no issues reported specific to Python 3.7, the release of +Python 3.7.1 at around the same time as GPGME 1.12.0 and the testing +with Python 3.7.1rc1, there is no reason to delay moving 3.7 ahead of +3.6 now. Production environments with more conservative requirements +will always enforce their own policies anyway and installation to each +supported minor release is quite possible too.} + +On systems where @samp{python} is actually @samp{python3} and not @samp{python2} it +may be possible that @samp{python2} may be overlooked, but there have been +no reports of that actually occurring as yet. + +In the three months or so since the release of Python 3.7.0 there has +been extensive testing and work with these bindings with no issues +specifically relating to the new version of Python or any of the new +features of either the language or the bindings. This has also been +the case with Python 3.7.1rc1. With that in mind and given the +release of Python 3.7.1 is scheduled for around the same time as GPGME +1.12.0, the order of preferred Python versions has been changed to +move Python 3.7 ahead of Python 3.6. + +@menu +* Installing GPGME:: +@end menu + +@node Installing GPGME +@subsection Installing GPGME + +See the GPGME @samp{README} file for details of how to install GPGME from +source. + +@node Known Issues +@section Known Issues + +There are a few known issues with the current build process and the +Python bindings. For the most part these are easily addressed should +they be encountered. + +@menu +* Breaking Builds:: +* Reinstalling Responsibly:: +* Multiple installations:: +* Won't Work With Windows:: +* CFFI is the Best™ and GPGME should use it instead of SWIG:: +* Virtualised Environments:: +@end menu + +@node Breaking Builds +@subsection Breaking Builds + +Occasionally when installing GPGME with the Python bindings included +it may be observed that the @samp{make} portion of that process induces a +large very number of warnings and, eventually errors which end that +part of the build process. Yet following that with @samp{make check} and +@samp{make install} appears to work seamlessly. + +The cause of this is related to the way SWIG needs to be called to +dynamically generate the C bindings for GPGME in the first place. So +the entire process will always produce @samp{lang/python/python2-gpg/} and +@samp{lang/python/python3-gpg/} directories. These should contain the +build output generated during compilation, including the complete +bindings and module installed into @samp{site-packages}. + +Occasionally the errors in the early part or some other conflict +(e.g. not installing as @strong{@emph{root}} or @strong{@emph{su}}) may result in nothing +being installed to the relevant @samp{site-packages} directory and the +build directory missing a lot of expected files. Even when this +occurs, the solution is actually quite simple and will always work. + +That solution is simply to run the following commands as either the +@strong{root} user or prepended with @samp{sudo -H}@footnote{Yes, even if you use virtualenv with everything you do in +Python. If you want to install this module as just your user account +then you will need to manually configure, compile and install the +@emph{entire} GnuPG stack as that user as well. This includes libraries +which are not often installed that way. It can be done and there are +circumstances under which it is worthwhile, but generally only on +POSIX systems which utilise single user mode (some even require it).} in the @samp{lang/python/} +directory: + +@example +/path/to/pythonX.Y setup.py build +/path/to/pythonX.Y setup.py build +/path/to/pythonX.Y setup.py install +@end example + +Yes, the build command does need to be run twice. Yes, you still need +to run the potentially failing or incomplete steps during the +@samp{configure}, @samp{make} and @samp{make install} steps with installing GPGME. +This is because those steps generate a lot of essential files needed, +both by and in order to create, the bindings (including both the +@samp{setup.py} and @samp{gpgme.h} files). + +@enumerate +@item +IMPORTANT Note + + +If specifying a selected number of languages to create bindings for, +try to leave Python last. Currently the majority of the other +language bindings are also preceding Python of either version when +listed alphabetically and so that just happens by default currently. + +If Python is set to precede one of the other languages then it is +possible that the errors described here may interrupt the build +process before generating bindings for those other languages. In +these cases it may be preferable to configure all preferred language +bindings separately with alternative @samp{configure} steps for GPGME using +the @samp{--enable-languages=$LANGUAGE} option. +@end enumerate + +@node Reinstalling Responsibly +@subsection Reinstalling Responsibly + +Regardless of whether you're installing for one version of Python or +several, there will come a point where reinstallation is required. +With most Python module installations, the installed files go into the +relevant site-packages directory and are then forgotten about. Then +the module is upgraded, the new files are copied over the old and +that's the end of the matter. + +While the same is true of these bindings, there have been intermittent +issues observed on some platforms which have benefited significantly +from removing all the previous installations of the bindings before +installing the updated versions. + +Removing the previous version(s) is simply a matter of changing to the +relevant @samp{site-packages} directory for the version of Python in +question and removing the @samp{gpg/} directory and any accompanying +egg-info files for that module. + +In most cases this will require root or administration privileges on +the system, but the same is true of installing the module in the first +place. + +@node Multiple installations +@subsection Multiple installations + +For a veriety of reasons it may be either necessary or just preferable +to install the bindings to alternative installed Python versions which +meet the requirements of these bindings. + +On POSIX systems this will generally be most simply achieved by +running the manual installation commands (build, build, install) as +described in the previous section for each Python installation the +bindings need to be installed to. + +As per the SWIG documentation: the compilers, libraries and runtime +used to build GPGME and the Python Bindings @strong{must} match those used to +compile Python itself, including the version number(s) (at least going +by major version numbers and probably minor numbers too). + +On most POSIX systems, including OS X, this will very likely be the +case in most, if not all, cases. + +@node Won't Work With Windows +@subsection Won't Work With Windows + +There are semi-regular reports of Windows users having considerable +difficulty in installing and using the Python bindings at all. Very +often, possibly even always, these reports come from Cygwin users +and/or MinGW users and/or Msys2 users. Though not all of them have +been confirmed, it appears that these reports have also come from +people who installed Python using the Windows installer files from the +@uref{https://python.org, Python website} (i.e. mostly MSI installers, sometimes self-extracting +@samp{.exe} files). + +The Windows versions of Python are not built using Cygwin, MinGW or +Msys2; they're built using Microsoft Visual Studio. Furthermore the +version used is @emph{considerably} more advanced than the version which +MinGW obtained a small number of files from many years ago in order to +be able to compile anything at all. Not only that, but there are +changes to the version of Visual Studio between some micro releases, +though that is is particularly the case with Python 2.7, since it has +been kept around far longer than it should have been. + +There are two theoretical solutions to this issue: + +@enumerate +@item +Compile and install the GnuPG stack, including GPGME and the +Python bibdings using the same version of Microsoft Visual Studio +used by the Python Foundation to compile the version of Python +installed. + +If there are multiple versions of Python then this will need to be +done with each different version of Visual Studio used. + +@item +Compile and install Python using the same tools used by choice, +such as MinGW or Msys2. +@end enumerate + +Do @strong{not} use the official Windows installer for Python unless +following the first method. + +In this type of situation it may even be for the best to accept that +there are less limitations on permissive software than free software +and simply opt to use a recent version of the Community Edition of +Microsoft Visual Studio to compile and build all of it, no matter +what. + +Investigations into the extent or the limitations of this issue are +ongoing. + +@node CFFI is the Best™ and GPGME should use it instead of SWIG +@subsection CFFI is the Best™ and GPGME should use it instead of SWIG + +There are many reasons for favouring @uref{https://cffi.readthedocs.io/en/latest/overview.html, CFFI} and proponents of it are +quite happy to repeat these things as if all it would take to switch +from SWIG to CFFI is repeating that list as if it were a new concept. + +The fact is that there are things which Python's CFFI implementation +cannot handle in the GPGME C code. Beyond that there are features of +SWIG which are simply not available with CFFI at all. SWIG generates +the bindings to Python using the @samp{gpgme.h} file, but that file is not +a single version shipped with each release, it too is generated when +GPGME is compiled. + +CFFI is currently unable to adapt to such a potentially mutable +codebase. If there were some means of applying SWIG's dynamic code +generation to produce the Python/CFFI API modes of accessing the GPGME +libraries (or the source source code directly), but such a thing does +not exist yet either and it currently appears that work is needed in +at least one of CFFI's dependencies before any of this can be +addressed. + +So if you're a massive fan of CFFI; that's great, but if you want this +project to switch to CFFI then rather than just insisting that it +should, I'd suggest you volunteer to bring CFFI up to the level this +project needs. + +If you're actually seriously considering doing so, then I'd suggest +taking the @samp{gpgme-tool.c} file in the GPGME @samp{src/} directory and +getting that to work with any of the CFFI API methods (not the ABI +methods, they'll work with pretty much anything). When you start +running into trouble with "ifdefs" then you'll know what sort of +things are lacking. That doesn't even take into account the amount of +work saved via SWIG's code generation techniques either. + +@node Virtualised Environments +@subsection Virtualised Environments + +It is fairly common practice amongst Python developers to, as much as +possible, use packages like virtualenv to keep various things that are +to be installed from interfering with each other. Given how much of +the GPGME bindings is often at odds with the usual pythonic way of +doing things, it stands to reason that this would be called into +question too. + +As it happens the answer as to whether or not the bindings can be used +with virtualenv, the answer is both yes and no. + +In general we recommend installing to the relevant path and matching +prefix of GPGME itself. Which means that when GPGME, and ideally the +rest of the GnuPG stack, is installed to a prefix like @samp{/usr/local} or +@samp{/opt/local} then the bindings would need to be installed to the main +Python installation and not a virtualised abstraction. Attempts to +separate the two in the past have been known to cause weird and +intermittent errors ranging from minor annoyances to complete failures +in the build process. + +As a consequence we only recommend building with and installing to the +main Python installations within the same prefix as GPGME is installed +to or which are found by GPGME's configuration stage immediately prior +to running the make commands. Which is exactly what the compiling and +installing process of GPGME does by default. + +Once that is done, however, it appears that a copy the compiled module +may be installed into a virtualenv of the same major and minor version +matching the build. Alternatively it is possible to utilise a +@samp{sites.pth} file in the @samp{site-packages/} directory of a viertualenv +installation, which links back to the system installations +corresponding directory in order to import anything installed system +wide. This may or may not be appropriate on a case by case basis. + +Though extensive testing of either of these options is not yet +complete, preliminary testing of them indicates that both are viable +as long as the main installation is complete. Which means that +certain other options normally restricted to virtual environments are +also available, including integration with pythonic test suites +(e.g. @uref{https://docs.pytest.org/en/latest/index.html, pytest}) and other large projects. + +That said, it is worth reiterating the warning regarding non-standard +installations. If one were to attempt to install the bindings only to +a virtual environment without somehow also including the full GnuPG +stack (or enough of it as to include GPGME) then it is highly likely +that errors would be encountered at some point and more than a little +likely that the build process itself would break. + +If a degree of separation from the main operating system is still +required in spite of these warnings, then consider other forms of +virtualisation. Either a virtual machine (e.g. @uref{https://www.virtualbox.org/, VirtualBox}), a +hardware emulation layer (e.g. @uref{https://www.qemu.org/, QEMU}) or an application container +(e.g. @uref{https://www.docker.com/why-docker, Docker}). + +Finally it should be noted that the limited tests conducted thus far +have been using the @samp{virtualenv} command in a new directory to create +the virtual python environment. As opposed to the standard @samp{python3 +-m venv} and it is possible that this will make a difference depending +on the system and version of Python in use. Another option is to run +the command @samp{python3 -m virtualenv /path/to/install/virtual/thingy} +instead. + +@node Fundamentals +@chapter Fundamentals + +Before we can get to the fun stuff, there are a few matters regarding +GPGME's design which hold true whether you're dealing with the C code +directly or these Python bindings. + +@menu +* No REST:: +* Context:: +@end menu + +@node No REST +@section No REST + +The first part of which is or will be fairly blatantly obvious upon +viewing the first example, but it's worth reiterating anyway. That +being that this API is @emph{@strong{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 that this HOWTO deals with. + +@node Context +@section Context + +One of the reasons which prevents this API from being RESTful is that +most operations require more than one instruction to the API to +perform the task. Sure, there are certain functions which can be +performed simultaneously, particularly if the result known or strongly +anticipated (e.g. selecting and encrypting to a key known to be in the +public keybox). + +There are many more, however, which cannot be manipulated so readily: +they must be performed in a specific sequence and the result of one +operation has a direct bearing on the outcome of subsequent +operations. Not merely by generating an error either. + +When dealing with this type of persistent state on the web, full of +both the RESTful and REST-like, it's most commonly referred to as a +session. In GPGME, however, it is called a context and every +operation type has one. + +@node Working with keys +@chapter Working with keys + +@menu +* Key selection:: +* Get key:: +* Importing keys:: +* Exporting keys:: +@end menu + +@node Key selection +@section Key selection + +Selecting keys to encrypt to or to sign with will be a common +occurrence when working with GPGMe and the means available for doing +so are quite simple. + +They do depend on utilising a Context; however once the data is +recorded in another variable, that Context does not need to be the +same one which subsequent operations are performed. + +The easiest way to select a specific key is by searching for that +key's key ID or fingerprint, preferably the full fingerprint without +any spaces in it. A long key ID will probably be okay, but is not +advised and short key IDs are already a problem with some being +generated to match specific patterns. It does not matter whether the +pattern is upper or lower case. + +So this is the best method: + +@example +import gpg + +k = gpg.Context().keylist(pattern="258E88DCBD3CD44D8E7AB43F6ECB6AF0DEADBEEF") +keys = list(k) +@end example + +This is passable and very likely to be common: + +@example +import gpg + +k = gpg.Context().keylist(pattern="0x6ECB6AF0DEADBEEF") +keys = list(k) +@end example + +And this is a really bad idea: + +@example +import gpg + +k = gpg.Context().keylist(pattern="0xDEADBEEF") +keys = list(k) +@end example + +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: + +@example +import gpg + +ncsc = gpg.Context().keylist(pattern="ncsc.mil") +nsa = list(ncsc) +@end example + +@menu +* Counting keys:: +@end menu + +@node Counting keys +@subsection Counting keys + +Counting the number of keys in your public keybox (@samp{pubring.kbx}), the +format which has superseded the old keyring format (@samp{pubring.gpg} and +@samp{secring.gpg}), or the number of secret keys is a very simple task. + +@example +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 example + +NOTE: The @ref{C plus Python plus SWIG plus Cython, , Cython} introduction in the @ref{Advanced or Experimental Use Cases, , Advanced and Experimental} +section uses this same key counting code with Cython to demonstrate +some areas where Cython can improve performance even with the +bindings. Users with large public keyrings or keyboxes, for instance, +should consider these options if they are comfortable with using +Cython. + +@node Get key +@section Get key + +An alternative method of getting a single key via its fingerprint is +available directly within a Context with @samp{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: + +@example +import gpg + +fingerprint = "80615870F5BAD690333686D0F2AD85AC1E42B367" +key = gpg.Context().get_key(fingerprint) +@end example + +Whereas this example demonstrates selecting the author's current key +with the @samp{secret} key word argument set to @samp{True}: + +@example +import gpg + +fingerprint = "DB4724E6FA4286C92B4E55C4321E4E2373590E5D" +key = gpg.Context().get_key(fingerprint, secret=True) +@end example + +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. + +@node Importing keys +@section Importing keys + +Importing keys is possible with the @samp{key_import()} method and takes +one argument which is a bytes literal object containing either the +binary or ASCII armoured key data for one or more keys. + +The following example retrieves one or more keys from the SKS +keyservers via the web using the requests module. Since requests +returns the content as a bytes literal object, we can then use that +directly to import the resulting data into our keybox. + +@example +import gpg +import os.path +import requests + +c = gpg.Context() +url = "https://sks-keyservers.net/pks/lookup" +pattern = input("Enter the pattern to search for key or user IDs: ") +payload = @{"op": "get", "search": pattern@} + +r = requests.get(url, verify=True, params=payload) +result = c.key_import(r.content) + +if result is not None and hasattr(result, "considered") is False: + print(result) +elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" + The total number of keys considered for import was: @{0@} + + Number of keys revoked: @{1@} + Number of new signatures: @{2@} + Number of new subkeys: @{3@} + Number of new user IDs: @{4@} + Number of new secret keys: @{5@} + Number of unchanged keys: @{6@} + + The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print("@{0@}\n".format(result.imports[i].fpr)) +else: + pass +@end example + +NOTE: When searching for a key ID of any length or a fingerprint +(without spaces), the SKS servers require the the leading @samp{0x} +indicative of hexadecimal be included. Also note that the old short +key IDs (e.g. @samp{0xDEADBEEF}) should no longer be used due to the +relative ease by which such key IDs can be reproduced, as demonstrated +by the Evil32 Project in 2014 (which was subsequently exploited in +2016). + +@menu +* Working with ProtonMail:: +* Importing with HKP for Python:: +* Importing from ProtonMail with HKP for Python:: +@end menu + +@node Working with ProtonMail +@subsection Working with ProtonMail + +Here is a variation on the example above which checks the constrained +ProtonMail keyserver for ProtonMail public keys. + +@example +import gpg +import requests +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. +""") + +c = gpg.Context(armor=True) +url = "https://api.protonmail.ch/pks/lookup" +ksearch = [] + +if len(sys.argv) >= 2: + keyterm = sys.argv[1] +else: + keyterm = input("Enter the key ID, UID or search string: ") + +if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) +elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True: + ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:])) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:])) + ksearch.append("@{0@}@@pm.me".format(keyterm[1:])) +elif keyterm.count("@@") == 0: + ksearch.append("@{0@}@@protonmail.com".format(keyterm)) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm)) + ksearch.append("@{0@}@@pm.me".format(keyterm)) +elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) +elif keyterm.count("@@") > 2: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) +else: + ksearch.append(keyterm) + +for k in ksearch: + payload = @{"op": "get", "search": k@} + try: + r = requests.get(url, verify=True, params=payload) + if r.ok is True: + result = c.key_import(r.content) + elif r.ok is False: + result = r.content + except Exception as e: + result = None + + if result is not None and hasattr(result, "considered") is False: + print("@{0@} for @{1@}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: @{0@} + +With UIDs wholely or partially matching the following string: + + @{1@} + + Number of keys revoked: @{2@} + Number of new signatures: @{3@} + Number of new subkeys: @{4@} + Number of new user IDs: @{5@} +Number of new secret keys: @{6@} + Number of unchanged keys: @{7@} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + print(e) +@end example + +Both the above example, @uref{../examples/howto/pmkey-import.py, pmkey-import.py}, and a version which prompts +for an alternative GnuPG home directory, @uref{../examples/howto/pmkey-import-alt.py, pmkey-import-alt.py}, are +available with the other examples and are executable scripts. + +Note that while the ProtonMail servers are based on the SKS servers, +their server is related more to their API and is not feature complete +by comparison to the servers in the SKS pool. One notable difference +being that the ProtonMail server does not permit non ProtonMail users +to update their own keys, which could be a vector for attacking +ProtonMail users who may not receive a key's revocation if it had been +compromised. + +@node Importing with HKP for Python +@subsection Importing with HKP for Python + +Performing the same tasks with the @uref{https://github.com/Selfnet/hkp4py, hkp4py module} (available via PyPI) +is not too much different, but does provide a number of options of +benefit to end users. Not least of which being the ability to perform +some checks on a key before importing it or not. For instance it may +be the policy of a site or project to only import keys which have not +been revoked. The hkp4py module permits such checks prior to the +importing of the keys found. + +@example +import gpg +import hkp4py +import sys + +c = gpg.Context() +server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") +results = [] + +if len(sys.argv) > 2: + pattern = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + pattern = sys.argv[1] +else: + pattern = input("Enter the pattern to search for keys or user IDs: ") + +try: + keys = server.search(pattern) + print("Found @{0@} key(s).".format(len(keys))) +except Exception as e: + keys = [] + for logrus in pattern.split(): + if logrus.startswith("0x") is True: + key = server.search(logrus) + else: + key = server.search("0x@{0@}".format(logrus)) + keys.append(key[0]) + print("Found @{0@} key(s).".format(len(keys))) + +for key in keys: + import_result = c.key_import(key.key_blob) + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print(result) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: @{0@} + + Number of keys revoked: @{1@} + Number of new signatures: @{2@} + Number of new subkeys: @{3@} + Number of new user IDs: @{4@} +Number of new secret keys: @{5@} + Number of unchanged keys: @{6@} + +The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + else: + pass +@end example + +Since the hkp4py module handles multiple keys just as effectively as +one (@samp{keys} is a list of responses per matching key), the example +above is able to do a little bit more with the returned data before +anything is actually imported. + +@node Importing from ProtonMail with HKP for Python +@subsection Importing from ProtonMail with HKP for Python + +Though this can provide certain benefits even when working with +ProtonMail, the scope is somewhat constrained there due to the +limitations of the ProtonMail keyserver. + +For instance, searching the SKS keyserver pool for the term "gnupg" +produces hundreds of results from any time the word appears in any +part of a user ID. Performing the same search on the ProtonMail +keyserver returns zero results, even though there are at least two +test accounts which include it as part of the username. + +The cause of this discrepancy is the deliberate configuration of that +server by ProtonMail to require an exact match of the full email +address of the ProtonMail user whose key is being requested. +Presumably this is intended to reduce breaches of privacy of their +users as an email address must already be known before a key for that +address can be obtained. + +@enumerate +@item +Import from ProtonMail via HKP for Python Example no. 1 + + +The following script is avalable with the rest of the examples under +the somewhat less than original name, @samp{pmkey-import-hkp.py}. + +@example +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. + +Usage: pmkey-import-hkp.py [search strings] +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://api.protonmail.ch") +keyterms = [] +ksearch = [] +allkeys = [] +results = [] +paradox = [] +homeless = None + +if len(sys.argv) > 2: + keyterms = sys.argv[1:] +elif len(sys.argv) == 2: + keyterm = sys.argv[1] + keyterms.append(keyterm) +else: + key_term = input("Enter the key ID, UID or search string: ") + keyterms = key_term.split() + +for keyterm in keyterms: + if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True: + ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:])) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:])) + ksearch.append("@{0@}@@pm.me".format(keyterm[1:])) + elif keyterm.count("@@") == 0: + ksearch.append("@{0@}@@protonmail.com".format(keyterm)) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm)) + ksearch.append("@{0@}@@pm.me".format(keyterm)) + elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) + elif keyterm.count("@@") > 2: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) + else: + ksearch.append(keyterm) + +for k in ksearch: + print("Checking for key for: @{0@}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print("@{0@} for @{1@}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: @{0@} + +With UIDs wholely or partially matching the following string: + + @{1@} + + Number of keys revoked: @{2@} + Number of new signatures: @{3@} + Number of new subkeys: @{4@} + Number of new user IDs: @{5@} +Number of new secret keys: @{6@} + Number of unchanged keys: @{7@} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass +@end example + +@item +Import from ProtonMail via HKP for Python Example no. 2 + + +Like its counterpart above, this script can also be found with the +rest of the examples, by the name pmkey-import-hkp-alt.py. + +With this script a modicum of effort has been made to treat anything +passed as a @samp{homedir} which either does not exist or which is not a +directory, as also being a pssible user ID to check for. It's not +guaranteed to pick up on all such cases, but it should cover most of +them. + +@example +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. Optionally enables specifying a different GnuPG home directory. + +Usage: pmkey-import-hkp.py [homedir] [search string] + or: pmkey-import-hkp.py [search string] +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://api.protonmail.ch") +keyterms = [] +ksearch = [] +allkeys = [] +results = [] +paradox = [] +homeless = None + +if len(sys.argv) > 3: + homedir = sys.argv[1] + keyterms = sys.argv[2:] +elif len(sys.argv) == 3: + homedir = sys.argv[1] + keyterm = sys.argv[2] + keyterms.append(keyterm) +elif len(sys.argv) == 2: + homedir = "" + keyterm = sys.argv[1] + keyterms.append(keyterm) +else: + keyterm = input("Enter the key ID, UID or search string: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + keyterms.append(keyterm) + +if len(homedir) == 0: + homedir = None + homeless = False + +if homedir is not None: + if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + if os.path.isdir(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.realpath(os.path.expanduser(homedir)) + else: + homeless = True + else: + homeless = True + elif os.path.exists(os.path.realpath(homedir)) is True: + if os.path.isdir(os.path.realpath(homedir)) is True: + c.home_dir = os.path.realpath(homedir) + else: + homeless = True + else: + homeless = True + +# First check to see if the homedir really is a homedir and if not, treat it as +# a search string. +if homeless is True: + keyterms.append(homedir) + c.home_dir = None +else: + pass + +for keyterm in keyterms: + if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True: + ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:])) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:])) + ksearch.append("@{0@}@@pm.me".format(keyterm[1:])) + elif keyterm.count("@@") == 0: + ksearch.append("@{0@}@@protonmail.com".format(keyterm)) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm)) + ksearch.append("@{0@}@@pm.me".format(keyterm)) + elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) + elif keyterm.count("@@") > 2: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) + else: + ksearch.append(keyterm) + +for k in ksearch: + print("Checking for key for: @{0@}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print("@{0@} for @{1@}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: @{0@} + +With UIDs wholely or partially matching the following string: + + @{1@} + + Number of keys revoked: @{2@} + Number of new signatures: @{3@} + Number of new subkeys: @{4@} + Number of new user IDs: @{5@} +Number of new secret keys: @{6@} + Number of unchanged keys: @{7@} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass +@end example +@end enumerate + +@node Exporting keys +@section Exporting keys + +Exporting keys remains a reasonably simple task, but has been +separated into three different functions for the OpenPGP cryptographic +engine. Two of those functions are for exporting public keys and the +third is for exporting secret keys. + +@menu +* Exporting public keys:: +* Exporting secret keys:: +* Sending public keys to the SKS Keyservers:: +@end menu + +@node Exporting public keys +@subsection Exporting public keys + +There are two methods of exporting public keys, both of which are very +similar to the other. The default method, @samp{key_export()}, will export +a public key or keys matching a specified pattern as normal. The +alternative, the @samp{key_export_minimal()} method, will do the same thing +except producing a minimised output with extra signatures and third +party signatures or certifications removed. + +@example +import gpg +import os.path +import sys + +print(""" +This script exports one or more public keys. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass +elif os.path.exists(homedir) is True: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export(pattern=logrus) +except: + result = c.key_export(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) +else: + pass +@end example + +It should be noted that the result will only return @samp{None} when a +search pattern has been entered, but has not matched any keys. When +the search pattern itself is set to @samp{None} this triggers the exporting +of the entire public keybox. + +@example +import gpg +import os.path +import sys + +print(""" +This script exports one or more public keys in minimised form. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass +elif os.path.exists(homedir) is True: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export_minimal(pattern=logrus) +except: + result = c.key_export_minimal(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) +else: + pass +@end example + +@node Exporting secret keys +@subsection Exporting secret keys + +Exporting secret keys is, functionally, very similar to exporting +public keys; save for the invocation of @samp{pinentry} via @samp{gpg-agent} in +order to securely enter the key's passphrase and authorise the export. + +The following example exports the secret key to a file which is then +set with the same permissions as the output files created by the +command line secret key export options. + +@example +import gpg +import os +import os.path +import sys + +print(""" +This script exports one or more secret keys. + +The gpg-agent and pinentry are invoked to authorise the export. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if os.path.exists(homedir) is False: + homedir = None +else: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export_secret(pattern=logrus) +except: + result = c.key_export_secret(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) + os.chmod(keyfile, 0o600) +else: + pass +@end example + +Alternatively the approach of the following script can be used. This +longer example saves the exported secret key(s) in files in the GnuPG +home directory, in addition to setting the file permissions as only +readable and writable by the user. It also exports the secret key(s) +twice in order to output both GPG binary (@samp{.gpg}) and ASCII armoured +(@samp{.asc}) files. + +@example +import gpg +import os +import os.path +import subprocess +import sys + +print(""" +This script exports one or more secret keys as both ASCII armored and binary +file formats, saved in files within the user's GPG home directory. + +The gpg-agent and pinentry are invoked to authorise the export. +""") + +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-dirs homedir" +else: + gpgconfcmd = "gpgconf --list-dirs homedir" + +a = gpg.Context(armor=True) +b = gpg.Context() +c = gpg.Context() + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if os.path.exists(homedir) is False: + homedir = None +else: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +if c.home_dir is not None: + if c.home_dir.endswith("/"): + gpgfile = "@{0@}@{1@}.gpg".format(c.home_dir, keyfile) + ascfile = "@{0@}@{1@}.asc".format(c.home_dir, keyfile) + else: + gpgfile = "@{0@}/@{1@}.gpg".format(c.home_dir, keyfile) + ascfile = "@{0@}/@{1@}.asc".format(c.home_dir, keyfile) +else: + if os.path.exists(os.environ["GNUPGHOME"]) is True: + hd = os.environ["GNUPGHOME"] + else: + try: + hd = subprocess.getoutput(gpgconfcmd) + except: + process = subprocess.Popen(gpgconfcmd.split(), + stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + hd = procom[0].strip() + else: + hd = procom[0].decode().strip() + gpgfile = "@{0@}/@{1@}.gpg".format(hd, keyfile) + ascfile = "@{0@}/@{1@}.asc".format(hd, keyfile) + +try: + a_result = a.key_export_secret(pattern=logrus) + b_result = b.key_export_secret(pattern=logrus) +except: + a_result = a.key_export_secret(pattern=None) + b_result = b.key_export_secret(pattern=None) + +if a_result is not None: + with open(ascfile, "wb") as f: + f.write(a_result) + os.chmod(ascfile, 0o600) +else: + pass + +if b_result is not None: + with open(gpgfile, "wb") as f: + f.write(b_result) + os.chmod(gpgfile, 0o600) +else: + pass +@end example + +@node Sending public keys to the SKS Keyservers +@subsection Sending public keys to the SKS Keyservers + +As with the previous section on importing keys, the @samp{hkp4py} module +adds another option with exporting keys in order to send them to the +public keyservers. + +The following example demonstrates how this may be done. + +@example +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script sends one or more public keys to the SKS keyservers and is +essentially a slight variation on the export-key.py script. +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") + +if len(sys.argv) > 2: + logrus = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + logrus = sys.argv[1] +else: + logrus = input("Enter the UID matching the key(s) to send: ") + +if len(logrus) > 0: + try: + export_result = c.key_export(pattern=logrus) + except Exception as e: + print(e) + export_result = None +else: + export_result = c.key_export(pattern=None) + +if export_result is not None: + try: + try: + send_result = server.add(export_result) + except: + send_result = server.add(export_result.decode()) + if send_result is not None: + print(send_result) + else: + pass + except Exception as e: + print(e) +else: + pass +@end example + +An expanded version of this script with additional functions for +specifying an alternative homedir location is in the examples +directory as @samp{send-key-to-keyserver.py}. + +The @samp{hkp4py} module appears to handle both string and byte literal text +data equally well, but the GPGME bindings deal primarily with byte +literal data only and so this script sends in that format first, then +tries the string literal form. + +@node Basic Functions +@chapter Basic Functions + +The most frequently called features of any cryptographic library will +be the most fundamental tasks for encryption software. In this +section we will look at how to programmatically encrypt data, decrypt +it, sign it and verify signatures. + +@menu +* Encryption:: +* Decryption:: +* Signing text and files:: +* Signature verification:: +@end menu + +@node Encryption +@section Encryption + +Encrypting is very straight forward. In the first example below the +message, @samp{text}, is encrypted to a single recipient's key. In the +second example the message will be encrypted to multiple recipients. + +@menu +* Encrypting to one key:: +* Encrypting to multiple keys:: +@end menu + +@node Encrypting to one key +@subsection Encrypting to one key + +Once the the Context is set the main issues with encrypting data is +essentially reduced to key selection and the keyword arguments +specified in the @samp{gpg.Context().encrypt()} method. + +Those keyword arguments are: @samp{recipients}, a list of keys encrypted to +(covered in greater detail in the following section); @samp{sign}, whether +or not to sign the plaintext data, see subsequent sections on signing +and verifying signatures below (defaults to @samp{True}); @samp{sink}, to write +results or partial results to a secure sink instead of returning it +(defaults to @samp{None}); @samp{passphrase}, only used when utilising symmetric +encryption (defaults to @samp{None}); @samp{always_trust}, used to override the +trust model settings for recipient keys (defaults to @samp{False}); +@samp{add_encrypt_to}, utilises any preconfigured @samp{encrypt-to} or +@samp{default-key} settings in the user's @samp{gpg.conf} file (defaults to +@samp{False}); @samp{prepare}, prepare for encryption (defaults to @samp{False}); +@samp{expect_sign}, prepare for signing (defaults to @samp{False}); @samp{compress}, +compresses the plaintext prior to encryption (defaults to @samp{True}). + +@example +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 example + +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 @samp{gpg.conf} file: + +@example +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 example + +If the @samp{recipients} paramater is empty then the plaintext is encrypted +symmetrically. If no @samp{passphrase} is supplied as a parameter or via a +callback registered with the @samp{Context()} then an out-of-band prompt +for the passphrase via pinentry will be invoked. + +@node Encrypting to multiple keys +@subsection Encrypting to multiple keys + +Encrypting to multiple keys essentially just expands upon the key +selection process and the recipients from the previous examples. + +The following example encrypts a message (@samp{text}) to everyone with an +email address on the @samp{gnupg.org} domain,@footnote{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.} but does @emph{not} encrypt +to a default key or other key which is configured to normally encrypt +to. + +@example +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 example + +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 @samp{c.encrypt} line to this: + +@example +ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + always_trust=True, + add_encrypt_to=True) +@end example + +The only keyword arguments requiring modification are those for which +the default values are changing. The default value of @samp{sign} is +@samp{True}, the default of @samp{always_trust} is @samp{False}, the default of +@samp{add_encrypt_to} is @samp{False}. + +If @samp{always_trust} is not set to @samp{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: + +@example +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) + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + except: + pass +@end example + +This will attempt to encrypt to all the keys searched for, then remove +invalid recipients if it fails and try again. + +@node Decryption +@section Decryption + +Decrypting something encrypted to a key in one's secret keyring is +fairly straight forward. + +In this example code, however, preconfiguring either @samp{gpg.Context()} +or @samp{gpg.core.Context()} as @samp{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 @samp{c} simply adds lines for no +gain. + +@example +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: + try: + plaintext, result, verify_result = gpg.Context().decrypt(cfile) + except gpg.errors.GPGMEError as e: + plaintext = None + print(e) + +if plaintext is not None: + with open(newfile, "wb") as nfile: + nfile.write(plaintext) + else: + pass +@end example + +The data available in @samp{plaintext} in this example is the decrypted +content as a byte object, the recipient key IDs and algorithms in +@samp{result} and the results of verifying any signatures of the data in +@samp{verify_result}. + +@node Signing text and files +@section Signing text and files + +The following sections demonstrate how to specify keys to sign with. + +@menu +* Signing key selection:: +* Normal or default signing messages or files:: +* Detached signing messages and files:: +* Clearsigning messages or text:: +@end menu + +@node Signing key selection +@subsection Signing key selection + +By default GPGME and the Python bindings will use the default key +configured for the user invoking the GPGME API. If there is no +default key specified and there is more than one secret key available +it may be necessary to specify the key or keys with which to sign +messages and files. + +@example +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 example + +The signing examples in the following sections include the explicitly +designated @samp{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. + +@node Normal or default signing messages or files +@subsection Normal or default signing messages or files + +The normal or default signing process is essentially the same as is +most often invoked when also encrypting a message or file. So when +the encryption component is not utilised, the result is to produce an +encoded and signed output which may or may not be ASCII armoured and +which may or may not also be compressed. + +By default compression will be used unless GnuPG detects that the +plaintext is already compressed. ASCII armouring will be determined +according to the value of @samp{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. + +@example +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 example + +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. + +@example +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 example + +@node Detached signing messages and files +@subsection Detached signing messages and files + +Detached signatures will often be needed in programmatic uses of +GPGME, either for signing files (e.g. tarballs of code releases) or as +a component of message signing (e.g. PGP/MIME encoded email). + +@example +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 example + +As with normal signatures, detached signatures are best handled as +byte literals, even when the output is ASCII armoured. + +@example +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 example + +@node Clearsigning messages or text +@subsection Clearsigning messages or text + +Though PGP/in-line messages are no longer encouraged in favour of +PGP/MIME, there is still sometimes value in utilising in-line +signatures. This is where clear-signed messages or text is of value. + +@example +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 example + +In spite of the appearance of a clear-signed message, the data handled +by GPGME in signing it must still be byte literals. + +@example +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 example + +@node Signature verification +@section Signature verification + +Essentially there are two principal methods of verification of a +signature. The first of these is for use with the normal or default +signing method and for clear-signed messages. The second is for use +with files and data with detached signatures. + +The following example is intended for use with the default signing +method where the file was not ASCII armoured: + +@example +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 example + +Whereas this next example, which is almost identical would work with +normal ASCII armoured files and with clear-signed files: + +@example +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 example + +In both of the previous examples it is also possible to compare the +original data that was signed against the signed data in @samp{data} to see +if it matches with something like this: + +@example +with open(filename, "rb") as afile: + text = afile.read() + +if text == data: + print("Good signature.") +else: + pass +@end example + +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 @samp{c.verify}. So @samp{data} is @samp{None} and only the information +in @samp{result} is available. + +@example +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 example + +@example +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 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 example + +@node Creating keys and subkeys +@chapter Creating keys and subkeys + +The one thing, aside from GnuPG itself, that GPGME depends on, of +course, is the keys themselves. So it is necessary to be able to +generate them and modify them by adding subkeys, revoking or disabling +them, sometimes deleting them and doing the same for user IDs. + +In the following examples a key will be created for the world's +greatest secret agent, Danger Mouse. Since Danger Mouse is a secret +agent he needs to be able to protect information to @samp{SECRET} level +clearance, so his keys will be 3072-bit keys. + +The pre-configured @samp{gpg.conf} file which sets cipher, digest and other +preferences contains the following configuration parameters: + +@example +expert +allow-freeform-uid +allow-secret-key-import +trust-model tofu+pgp +tofu-default-policy unknown +enable-large-rsa +enable-dsa2 +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 example + +@menu +* Primary key:: +* Subkeys:: +* User IDs:: +* Key certification:: +@end menu + +@node Primary key +@section Primary key + +Generating a primary key uses the @samp{create_key} method in a Context. +It contains multiple arguments and keyword arguments, including: +@samp{userid}, @samp{algorithm}, @samp{expires_in}, @samp{expires}, @samp{sign}, @samp{encrypt}, +@samp{certify}, @samp{authenticate}, @samp{passphrase} and @samp{force}. The defaults for +all of those except @samp{userid}, @samp{algorithm}, @samp{expires_in}, @samp{expires} and +@samp{passphrase} is @samp{False}. The defaults for @samp{algorithm} and +@samp{passphrase} is @samp{None}. The default for @samp{expires_in} is @samp{0}. The +default for @samp{expires} is @samp{True}. There is no default for @samp{userid}. + +If @samp{passphrase} is left as @samp{None} then the key will not be generated +with a passphrase, if @samp{passphrase} is set to a string then that will +be the passphrase and if @samp{passphrase} is set to @samp{True} then gpg-agent +will launch pinentry to prompt for a passphrase. For the sake of +convenience, these examples will keep @samp{passphrase} set to @samp{None}. + +@example +import gpg + +c = gpg.Context() + +c.home_dir = "~/.gnupg-dm" +userid = "Danger Mouse <dm@@secret.example.net>" + +dmkey = c.create_key(userid, algorithm="rsa3072", expires_in=31536000, + sign=True, certify=True) +@end example + +One thing to note here is the use of setting the @samp{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, @samp{~/.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 @samp{temp-homedir-config.py} script in the HOWTO examples directory +will create an alternative homedir with these configuration options +already set and the correct directory and file permissions. + +The successful generation of the key can be confirmed via the returned +@samp{GenkeyResult} object, which includes the following data: + +@example +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 example + +Alternatively the information can be confirmed using the command line +program: + +@example +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <dm@@secret.example.net> + +bash-4.4$ +@end example + +As with generating keys manually, to preconfigure expanded preferences +for the cipher, digest and compression algorithms, the @samp{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 @samp{gpg.conf} +file in order to be able to generate this: + +@example +bash-4.4$ gpg --homedir ~/.gnupg-dm --edit-key 177B7C25DB99745EE2EE13ED026D2F19E99E63AA showpref quit +Secret key is available. + +sec rsa3072/026D2F19E99E63AA + created: 2018-03-15 expires: 2019-03-15 usage: SC + trust: ultimate validity: ultimate +[ultimate] (1). Danger Mouse <dm@@secret.example.net> + +[ultimate] (1). Danger Mouse <dm@@secret.example.net> + Cipher: TWOFISH, CAMELLIA256, AES256, CAMELLIA192, AES192, CAMELLIA128, AES, BLOWFISH, IDEA, CAST5, 3DES + Digest: SHA512, SHA384, SHA256, SHA224, RIPEMD160, SHA1 + Compression: ZLIB, BZIP2, ZIP, Uncompressed + Features: MDC, Keyserver no-modify + +bash-4.4$ +@end example + +@node Subkeys +@section Subkeys + +Adding subkeys to a primary key is fairly similar to creating the +primary key with the @samp{create_subkey} method. Most of the arguments +are the same, but not quite all. Instead of the @samp{userid} argument +there is now a @samp{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. + +@example +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 example + +As with the primary key, the results here can be checked with: + +@example +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 example + +As well as on the command line with: + +@example +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <dm@@secret.example.net> +ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + +bash-4.4$ +@end example + +@node User IDs +@section User IDs + +@menu +* Adding User IDs:: +* Revokinging User IDs:: +@end menu + +@node Adding User IDs +@subsection Adding User IDs + +By comparison to creating primary keys and subkeys, adding a new user +ID to an existing key is much simpler. The method used to do this is +@samp{key_add_uid} and the only arguments it takes are for the @samp{key} and +the new @samp{uid}. + +@example +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret=True) +uid = "Danger Mouse <danger.mouse@@secret.example.net>" + +c.key_add_uid(key, uid) +@end example + +Unsurprisingly the result of this is: + +@example +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <danger.mouse@@secret.example.net> +uid [ultimate] Danger Mouse <dm@@secret.example.net> +ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + +bash-4.4$ +@end example + +@node Revokinging User IDs +@subsection Revokinging User IDs + +Revoking a user ID is a fairly similar process, except that it uses +the @samp{key_revoke_uid} method. + +@example +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret=True) +uid = "Danger Mouse <danger.mouse@@secret.example.net>" + +c.key_revoke_uid(key, uid) +@end example + +@node Key certification +@section Key certification + +Since key certification is more frequently referred to as key signing, +the method used to perform this function is @samp{key_sign}. + +The @samp{key_sign} method takes four arguments: @samp{key}, @samp{uids}, +@samp{expires_in} and @samp{local}. The default value of @samp{uids} is @samp{None} and +which results in all user IDs being selected. The default value of +both @samp{expires_in} and @samp{local} is @samp{False}; which results in the +signature never expiring and being able to be exported. + +The @samp{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 @samp{uids} value is not @samp{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: + +@example +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 example + +@node Advanced or Experimental Use Cases +@chapter Advanced or Experimental Use Cases + +@menu +* C plus Python plus SWIG plus Cython:: +@end menu + +@node C plus Python plus SWIG plus Cython +@section C plus Python plus SWIG plus Cython + +In spite of the apparent incongruence of using Python bindings to a C +interface only to generate more C from the Python; it is in fact quite +possible to use the GPGME bindings with @uref{http://docs.cython.org/en/latest/index.html, Cython}. Though in many cases +the benefits may not be obvious since the most computationally +intensive work never leaves the level of the C code with which GPGME +itself is interacting with. + +Nevertheless, there are some situations where the benefits are +demonstrable. One of the better and easier examples being the one of +the early examples in this HOWTO, the @ref{Counting keys, , key counting} code. Running that +example as an executable Python script, @samp{keycount.py} (available in +the @samp{examples/howto/} directory), will take a noticable amount of time +to run on most systems where the public keybox or keyring contains a +few thousand public keys. + +Earlier in the evening, prior to starting this section, I ran that +script on my laptop; as I tend to do periodically and timed it using +@samp{time} utility, with the following results: + +@example +bash-4.4$ time keycount.py + +Number of secret keys: 23 +Number of public keys: 12112 + + +real 11m52.945s +user 0m0.913s +sys 0m0.752s + +bash-4.4$ +@end example + +Sometime after that I imported another key and followed it with a +little test of Cython. This test was kept fairly basic, essentially +lifting the material from the @uref{http://docs.cython.org/en/latest/src/tutorial/cython_tutorial.html, Cython Basic Tutorial} to demonstrate +compiling Python code to C. The first step was to take the example +key counting code quoted previously, essentially from the importing of +the @samp{gpg} module to the end of the script: + +@example +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 example + +Save that into a file called @samp{keycount.pyx} and then create a +@samp{setup.py} file which contains this: + +@example +from distutils.core import setup +from Cython.Build import cythonize + +setup( + ext_modules = cythonize("keycount.pyx") +) +@end example + +Compile it: + +@example +bash-4.4$ python setup.py build_ext --inplace +bash-4.4$ +@end example + +Then run it in a similar manner to @samp{keycount.py}: + +@example +bash-4.4$ time python3.7 -c "import keycount" + +Number of secret keys: 23 +Number of public keys: 12113 + + +real 6m47.905s +user 0m0.785s +sys 0m0.331s + +bash-4.4$ +@end example + +Cython turned @samp{keycount.pyx} into an 81KB @samp{keycount.o} file in the +@samp{build/} directory, a 24KB @samp{keycount.cpython-37m-darwin.so} file to be +imported into Python 3.7 and a 113KB @samp{keycount.c} generated C source +code file of nearly three thousand lines. Quite a bit bigger than the +314 bytes of the @samp{keycount.pyx} file or the full 1,452 bytes of the +full executable @samp{keycount.py} example script. + +On the other hand it ran in nearly half the time; taking 6 minutes and +47.905 seconds to run. As opposed to the 11 minutes and 52.945 seconds +which the CPython script alone took. + +The @samp{keycount.pyx} and @samp{setup.py} files used to generate this example +have been added to the @samp{examples/howto/advanced/cython/} directory +The example versions include some additional options to annotate the +existing code and to detect Cython's use. The latter comes from the +@uref{http://docs.cython.org/en/latest/src/tutorial/pure.html#magic-attributes-within-the-pxd, Magic Attributes} section of the Cython documentation. + +@node Miscellaneous extras and work-arounds +@chapter Miscellaneous extras and work-arounds + +Most of the things in the following sections are here simply because +there was no better place to put them, even though some are only +peripherally related to the GPGME Python bindings. Some are also +workarounds for functions not integrated with GPGME as yet. This is +especially true of the first of these, dealing with @ref{Group lines, , group lines}. + +@menu +* Group lines:: +* Keyserver access for Python:: +@end menu + +@node Group lines +@section Group lines + +There is not yet an easy way to access groups configured in the +gpg.conf file from within GPGME. As a consequence these central +groupings of keys cannot be shared amongst multiple programs, such as +MUAs readily. + +The following code, however, provides a work-around for obtaining this +information in Python. + +@example +import subprocess +import sys + +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-options gpg" +else: + gpgconfcmd = "gpgconf --list-options gpg" + +try: + lines = subprocess.getoutput(gpgconfcmd).splitlines() +except: + process = subprocess.Popen(gpgconfcmd.split(), stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + lines = procom[0].splitlines() + else: + lines = procom[0].decode().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 = [] +group_lists = [] + +for i in range(len(groups)): + group_lines.append(groups[i].split("=")) + group_lists.append(groups[i].split("=")) + +for i in range(len(group_lists)): + group_lists[i][1] = group_lists[i][1].split() +@end example + +The result of that code is that @samp{group_lines} is a list of lists where +@samp{group_lines[i][0]} is the name of the group and @samp{group_lines[i][1]} +is the key IDs of the group as a string. + +The @samp{group_lists} result is very similar in that it is a list of +lists. The first part, @samp{group_lists[i][0]} matches +@samp{group_lines[i][0]} as the name of the group, but @samp{group_lists[i][1]} +is the key IDs of the group as a string. + +A demonstration of using the @samp{groups.py} module is also available in +the form of the executable @samp{mutt-groups.py} script. This second +script reads all the group entries in a user's @samp{gpg.conf} file and +converts them into crypt-hooks suitable for use with the Mutt and +Neomutt mail clients. + +@node Keyserver access for Python +@section Keyserver access for Python + +The @uref{https://github.com/Selfnet/hkp4py, hkp4py} module by Marcel Fest was originally a port of the old +@uref{https://github.com/dgladkov/python-hkp, python-hkp} module from Python 2 to Python 3 and updated to use the +@uref{http://docs.python-requests.org/en/latest/index.html, requests} module instead. It has since been modified to provide +support for Python 2.7 as well and is available via PyPI. + +Since it rewrites the @samp{hkp} protocol prefix as @samp{http} and @samp{hkps} as +@samp{https}, the module is able to be used even with servers which do not +support the full scope of keyserver functions.@footnote{Such as with ProtonMail servers. This also means that +restricted servers which only advertise either HTTP or HTTPS end +points and not HKP or HKPS end points must still be identified as as +HKP or HKPS within the Python Code. The @samp{hkp4py} module will rewrite +these appropriately when the connection is made to the server.} It also works quite +readily when incorporated into a @ref{C plus Python plus SWIG plus Cython, , Cython} generated and compiled version +of any code. + +@menu +* Key import format:: +@end menu + +@node Key import format +@subsection Key import format + +The hkp4py module returns key data via requests as string literals +(@samp{r.text}) instead of byte literals (@samp{r.content}). This means that +the retrurned key data must be encoded to UTF-8 when importing that +key material using a @samp{gpg.Context().key_import()} method. + +For this reason an alternative method has been added to the @samp{search} +function of @samp{hkp4py.KeyServer()} which returns the key in the correct +format as expected by @samp{key_import}. When importing using this module, +it is now possible to import with this: + +@example +for key in keys: + if key.revoked is False: + gpg.Context().key_import(key.key_blob) + else: + pass +@end example + +Without that recent addition it would have been necessary to encode +the contents of each @samp{hkp4py.KeyServer().search()[i].key} in +@samp{hkp4py.KeyServer().search()} before trying to import it. + +An example of this is included in the @ref{Importing keys, , Importing Keys} section of this +HOWTO and the corresponding executable version of that example is +available in the @samp{lang/python/examples/howto} directory as normal; the +executable version is the @samp{import-keys-hkp.py} file. + +@node Copyright and Licensing +@chapter Copyright and Licensing + +@menu +* Copyright:: +* Draft Editions of this HOWTO:: +* License GPL compatible:: +@end menu + +@node Copyright +@section Copyright + +Copyright © The GnuPG Project, 2018. + +Copyright (C) The GnuPG Project, 2018. + +@node Draft Editions of this HOWTO +@section Draft Editions of this HOWTO + +Draft editions of this HOWTO may be periodically available directly +from the author at any of the following URLs: + +@itemize +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.html, GPGME Python Bindings HOWTO draft (XHTML AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.html, GPGME Python Bindings HOWTO draft (XHTML AWS S3 no SSL)} +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.texi, GPGME Python Bindings HOWTO draft (Texinfo file AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.texi, GPGME Python Bindings HOWTO draft (Texinfo file AWS S3 no SSL)} +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.info, GPGME Python Bindings HOWTO draft (Info file AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.info, GPGME Python Bindings HOWTO draft (Info file AWS S3 no SSL)} +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.rst, GPGME Python Bindings HOWTO draft (reST file AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.rst, GPGME Python Bindings HOWTO draft (reST file AWS S3 no SSL)} +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.xml, GPGME Python Bindings HOWTO draft (Docbook 4.2 AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.xml, GPGME Python Bindings HOWTO draft (Docbook 4.2 AWS S3 no SSL)} +@end itemize + +All of these draft versions except for one have been generated from +this document via Emacs @uref{https://orgmode.org/, Org mode} and @uref{https://www.gnu.org/software/texinfo/, GNU Texinfo}. Though it is likely +that the specific @uref{https://files.au.adversary.org/crypto/gpgme-python-howto.org, file} @uref{http://files.au.adversary.org/crypto/gpgme-python-howto.org, version} used will be on the same server with +the generated output formats. + +The one exception is the reStructuredText version, which was converted +using the latest version of Pandoc from the Org mode source file using +the following command: + +@example +pandoc -f org -t rst+smart -o gpgme-python-howto.rst gpgme-python-howto.org +@end example + +In addition to these there is a significantly less frequently updated +version as a HTML @uref{https://files.au.adversary.org/crypto/gpgme-python-howto/webhelp/index.html, WebHelp site} (AWS S3 SSL); generated from DITA XML +source files, which can be found in @uref{https://dev.gnupg.org/source/gpgme/browse/ben%252Fhowto-dita/, an alternative branch} of the GPGME +git repository. + +These draft editions are not official documents and the version of +documentation in the master branch or which ships with released +versions is the only official documentation. Nevertheless, these +draft editions may occasionally be of use by providing more accessible +web versions which are updated between releases. They are provided on +the understanding that they may contain errors or may contain content +subject to change prior to an official release. + +@node License GPL compatible +@section License GPL compatible + +This file is free software; as a special exception the author gives +unlimited permission to copy and/or distribute it, with or without +modifications, as long as this notice is preserved. + +This file is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY, to the extent permitted by law; without even the +implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. + +@bye
\ No newline at end of file |