diff --git a/.gitignore b/.gitignore index de173b8f..e5bf69dc 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,3 @@ nosetests.xml default.profraw .DS_Store ._.DS_Store -default.profraw \ No newline at end of file diff --git a/TODO b/TODO index 5f03fb61..a915ed7c 100644 --- a/TODO +++ b/TODO @@ -14,23 +14,46 @@ Hey Emacs, this is -*- org -*- mode! tracked through the [[https://dev.gnupg.org/][dev.gnupg.org]] site. -* TODO Document all the new stuff. +* Documentation :PROPERTIES: - :CUSTOM_ID: more-docs-is-better + :CUSTOM_ID: documentation :END: -** STARTED Fix this TODO list. +** Document all the new stuff. :PROPERTIES: - :CUSTOM_ID: fix-todo + :CUSTOM_ID: more-docs-is-better :END: - - State "STARTED" from "TODO" [2018-03-09 Fri 08:31] - Clean up the current TODO list. Include properties as relevant (so - if someone does make a PDF or HTML version the TOC will work). - Also check ans see if some of these ancient things can be removed - (e.g. do we really need to fix things that were broken in GPG - 1.3.x? I'm thinking not so much). +*** TODO Fix this TODO list. + :PROPERTIES: + :CUSTOM_ID: fix-todo + :END: + Clean up the current TODO list. Include properties as relevant (so + if someone does make a PDF or HTML version the TOC will work). + + Also check ans see if some of these ancient things can be removed + (e.g. do we really need to fix things that were broken in GPG + 1.3.x? I'm thinking not so much). + +**** DONE fix TODO items + CLOSED: [2018-03-04 Sun 08:55] + :PROPERTIES: + :CUSTOM_ID: fix-todo-items + :END: + + Adjust todo items so each can now be referenced by custom-id and + checked off as necessary. + +** TODO Document validity and trust issues. + :PROPERTIES: + :CUSTOM_ID: valid-trust-issues + :END: + +** In gpgme.texi: Register callbacks under the right letter in the index. + :PROPERTIES: + :CUSTOM_ID: gpgme-texi + :END: * Fix the remaining UI Server problems: @@ -63,10 +86,12 @@ Hey Emacs, this is -*- org -*- mode! :END: Right now we block reading the next line with assuan. + * Before release: :PROPERTIES: :CUSTOM_ID: pre-release :END: + ** CANCELLED Some gpg tests fail with gpg 1.3.4-cvs (gpg/t-keylist-sig) CLOSED: [2018-03-09 Fri 08:16] :PROPERTIES: @@ -75,100 +100,123 @@ Hey Emacs, this is -*- org -*- mode! - State "CANCELLED" from "TODO" [2018-03-09 Fri 08:16] \\ WON'T FIX — too old or no longer applies. The test is currently disabled there and in gpg/t-import. + ** When gpg supports it, write binary subpackets directly, :PROPERTIES: :CUSTOM_ID: binary-subpackets :END: and parse SUBPACKET status lines. + * ABI's to break: :PROPERTIES: :CUSTOM_ID: abi-breakage-apparently-on-purpose :END: + ** Old opassuan interface. :PROPERTIES: :CUSTOM_ID: old-opassuan :END: + ** Implementation: Remove support for old style error codes in :PROPERTIES: :CUSTOM_ID: remove-old-error-codes :END: conversion.c::_gpgme_map_gnupg_error. + ** gpgme_edit_cb_t: Add "processed" return argument :PROPERTIES: :CUSTOM_ID: add-processed-return :END: (see edit.c::command_handler). + ** I/O and User Data could be made extensible. But this can be done :PROPERTIES: :CUSTOM_ID: add-io-user-data :END: without breaking the ABI hopefully. + ** All enums should be replaced by ints and simple macros for :PROPERTIES: :CUSTOM_ID: enums-should-be-ints :END: maximum compatibility. + ** Compatibility interfaces that can be removed in future versions: :PROPERTIES: :CUSTOM_ID: compat-interfaces-to-go :END: + *** gpgme_data_new_from_filepart :PROPERTIES: :CUSTOM_ID: gpgme-data-new-from-filepart :END: + *** gpgme_data_new_from_file :PROPERTIES: :CUSTOM_ID: gpgme-data-new-from-file :END: + *** gpgme_data_new_with_read_cb :PROPERTIES: :CUSTOM_ID: gpgme-data-new-with-read-cb :END: + *** gpgme_data_rewind :PROPERTIES: :CUSTOM_ID: gpgme-data-rewind :END: + *** gpgme_op_import_ext :PROPERTIES: :CUSTOM_ID: gpgme-op-import-ext :END: + *** gpgme_get_sig_key :PROPERTIES: :CUSTOM_ID: gpgme-get-sig-key :END: + *** gpgme_get_sig_ulong_attr :PROPERTIES: :CUSTOM_ID: gpgme-get-sig-ulong-attr :END: + *** gpgme_get_sig_string_attr :PROPERTIES: :CUSTOM_ID: gpgme-get-sig-string-attr :END: + *** GPGME_SIG_STAT_* :PROPERTIES: :CUSTOM_ID: gpgme-sig-stat :END: + *** gpgme_get_sig_status :PROPERTIES: :CUSTOM_ID: gpgme-get-sig-status :END: + *** gpgme_trust_item_release :PROPERTIES: :CUSTOM_ID: gpgme-trust-item-release :END: + *** gpgme_trust_item_get_string_attr :PROPERTIES: :CUSTOM_ID: gpgme-trust-item-get-string-attr :END: + *** gpgme_trust_item_get_ulong_attr :PROPERTIES: :CUSTOM_ID: gpgme-trust-item-get-ulong-attr :END: + *** gpgme_attr_t :PROPERTIES: :CUSTOM_ID: gpgme-attr-t :END: + *** All Gpgme* typedefs. :PROPERTIES: :CUSTOM_ID: all-gpgme-typedefs @@ -179,20 +227,24 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: threads :END: + ** When GNU Pth supports sendmsg/recvmsg, wrap them properly. :PROPERTIES: :CUSTOM_ID: wrap-oth :END: + ** Without timegm (3) support our ISO time parser is not thread safe. :PROPERTIES: :CUSTOM_ID: time-threads :END: There is a configure time warning, though. + * New features: :PROPERTIES: :CUSTOM_ID: new-features :END: + ** Flow control for data objects. :PROPERTIES: :CUSTOM_ID: flow-control-is-not-a-euphemism-for-an-s-bend @@ -205,11 +257,13 @@ Hey Emacs, this is -*- org -*- mode! respective event loop. or (B) a way for gpgme data objects to be associated with a waitable object, that can be registered with the user event loop. Neither is particularly simple. + ** Extended notation support. When gpg supports arbitrary binary :PROPERTIES: :CUSTOM_ID: extended-notation :END: notation data, provide a user interface for that. + ** notification system :PROPERTIES: :CUSTOM_ID: notification-system @@ -236,25 +290,30 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: stat-data :END: + ** Implement support for photo ids. :PROPERTIES: :CUSTOM_ID: photo-id :END: + ** Allow selection of subkeys :PROPERTIES: :CUSTOM_ID: subkey-selection :END: + ** Allow to return time stamps in ISO format :PROPERTIES: :CUSTOM_ID: iso-format-datetime :END: - This allows us to handle years later than 2037 properly. With the - time_t interface they are all mapped to 2037-12-31 + This allows us to handle years later than 2037 properly. With the + time_t interface they are all mapped to 2037-12-31 + ** New features requested by our dear users, but rejected or left for :PROPERTIES: :CUSTOM_ID: feature-requests :END: later consideration: + *** Allow to export secret keys. :PROPERTIES: :CUSTOM_ID: export-secret-keys @@ -262,6 +321,7 @@ Hey Emacs, this is -*- org -*- mode! Rejected because this is conceptually flawed. Secret keys on a smart card can not be exported, for example. May eventually e supproted with a keywrapping system. + *** Selecting the key ring, setting the version or comment in output. :PROPERTIES: :CUSTOM_ID: select-keyring-version @@ -269,46 +329,23 @@ Hey Emacs, this is -*- org -*- mode! Rejected because the naive implementation is engine specific, the configuration is part of the engine's configuration or readily worked around in a different way + *** Selecting the symmetric cipher. :PROPERTIES: :CUSTOM_ID: symmetric-cipher-selection :END: + *** Exchanging keys with key servers. :PROPERTIES: :CUSTOM_ID: key-server-exchange :END: -* Documentation - :PROPERTIES: - :CUSTOM_ID: documentation - :END: -** TODO Document validity and trust issues. - :PROPERTIES: - :CUSTOM_ID: valid-trust-issues - :END: -** In gpgme.texi: Register callbacks under the right letter in the index. - :PROPERTIES: - :CUSTOM_ID: gpgme-texi - :END: -** TODO Update TODO file - :PROPERTIES: - :CUSTOM_ID: todo-update - :END: - -*** DONE fix TODO items - CLOSED: [2018-03-04 Sun 08:55] - :PROPERTIES: - :CUSTOM_ID: fix-todo-items - :END: - - Adjust todo items so each can now be referenced by custom-id and - checked off as necessary. - * Engines :PROPERTIES: :CUSTOM_ID: engines :END: + ** Do not create/destroy engines, but create engine and then reset it. :PROPERTIES: :CUSTOM_ID: reset-engine-is-not-quite-just-ignition @@ -321,26 +358,31 @@ Hey Emacs, this is -*- org -*- mode! Note that we need support in gpgsm to set include-certs to default as RESET does not reset it, also for no_encrypt_to and probably other options. + ** Optimize the case where a data object has an underlying fd we can pass :PROPERTIES: :CUSTOM_ID: optimus-data-cousin-of-optimus-prime :END: directly to the engine. This will be automatic with socket I/O and descriptor passing. + ** Move code common to all engines up from gpg to engine. :PROPERTIES: :CUSTOM_ID: move-code-common-to-engines-out-of-gpg :END: + ** engine operations can return General Error on unknown protocol :PROPERTIES: :CUSTOM_ID: general-error-looking-to-be-court-martialled :END: (it's an internal error, as select_protocol checks already). + ** When server mode is implemented properly, more care has to be taken to :PROPERTIES: :CUSTOM_ID: server-mode :END: release all resources on error (for example to free assuan_cmd). + ** op_import_keys and op_export_keys have a limit in the number of keys. :PROPERTIES: :CUSTOM_ID: import-export-problems @@ -354,6 +396,7 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: gpg-breakage :END: + ** CANCELLED gpg 1.4.2 lacks error reporting if sign/encrypt with revoked key. CLOSED: [2018-03-09 Fri 08:19] :PROPERTIES: @@ -361,6 +404,7 @@ Hey Emacs, this is -*- org -*- mode! :END: - State "CANCELLED" from "TODO" [2018-03-09 Fri 08:19] \\ WON'T FIX. + ** CANCELLED gpg 1.4.2 does crappy error reporting (namely none at all) when CLOSED: [2018-03-09 Fri 08:20] :PROPERTIES: @@ -374,6 +418,7 @@ Hey Emacs, this is -*- org -*- mode! gpg: signing failed: general error [GNUPG:] BEGIN_ENCRYPTION 2 10 gpg: test: sign+encrypt failed: general error + ** DONE Without agent and with wrong passphrase, gpg 1.4.2 enters into an CLOSED: [2018-03-09 Fri 08:20] :PROPERTIES: @@ -382,6 +427,7 @@ Hey Emacs, this is -*- org -*- mode! - State "DONE" from "TODO" [2018-03-09 Fri 08:20] \\ Must have been fixed in a subsequent release. infinite loop. + ** CANCELLED Use correct argv[0] CLOSED: [2018-03-09 Fri 08:24] :PROPERTIES: @@ -402,71 +448,86 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: operations-are-not-surgical :END: + ** Include cert values -2, -1, 0 and 1 should be defined as macros. :PROPERTIES: :CUSTOM_ID: certified-macros :END: + ** If an operation failed, make sure that the result functions don't return :PROPERTIES: :CUSTOM_ID: operation-failure :END: corrupt partial information. !!! NOTE: The EOF status handler is not called in this case !!! + ** Verify must not fail on NODATA premature if auto-key-retrieval failed. :PROPERTIES: :CUSTOM_ID: autobot-key-retrieval :END: It should not fail silently if it knows there is an error. !!! + ** All operations: Better error reporting. !! :PROPERTIES: :CUSTOM_ID: better-reporting-not-like-fox-news :END: + ** Export status handler need much more work. !!! :PROPERTIES: :CUSTOM_ID: export-status-handler :END: + ** Import should return a useful error when one happened. :PROPERTIES: :CUSTOM_ID: import-useful-stuff-even-wrong-stuff :END: + *** Import does not take notice of NODATA status report. :PROPERTIES: :CUSTOM_ID: import-no-data :END: + *** When GPGSM does issue IMPORT_OK status reports, make sure to check for :PROPERTIES: :CUSTOM_ID: gpgsm-import-ok :END: them in tests/gpgs m/t-import.c. + ** Verify can include info about version/algo/class, but currently :PROPERTIES: :CUSTOM_ID: verify-class :END: this is only available for gpg, not gpgsm. + ** Return ENC_TO output in verify result. Again, this is not available :PROPERTIES: :CUSTOM_ID: return-to-enc :END: for gpgsm. + ** Genkey should return something more useful than General_Error. :PROPERTIES: :CUSTOM_ID: general-key-assumed-command-from-general-error :END: + ** If possible, use --file-setsize to set the file size for proper progress :PROPERTIES: :CUSTOM_ID: file-setsize :END: callback handling. Write data interface for file size. + ** Optimize the file descriptor list, so the number of open fds is :PROPERTIES: :CUSTOM_ID: optimus-descriptus-younger-brother-of-optimus-prime :END: always known easily. + ** Encryption: It should be verified that the behaviour for partially untrusted :PROPERTIES: :CUSTOM_ID: only-mostly-dead-means-partially-alive :END: recipients is correct. + ** When GPG issues INV_something for invalid signers, catch them. :PROPERTIES: :CUSTOM_ID: invalid-sig @@ -477,15 +538,18 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: error-value :END: + ** Map ASSUAN/GpgSM ERR error values in a better way than is done now. !! :PROPERTIES: :CUSTOM_ID: map-ass-error :END: + ** Some error values should identify the source more correctly (mostly error :PROPERTIES: :CUSTOM_ID: source-errors :END: values derived from status messages). + ** In rungpg.c we need to check the version of the engine :PROPERTIES: :CUSTOM_ID: rungpg-c-engine-ver @@ -498,6 +562,7 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: tests :END: + ** TODO Write a fake gpg-agent so that we can supply known passphrases to :PROPERTIES: :CUSTOM_ID: test-fake-gpg-agent @@ -505,23 +570,28 @@ Hey Emacs, this is -*- org -*- mode! gpgsm and setup the configuration files to use the agent. Without this we are testing a currently running gpg-agent which is not a clever idea. ! + ** t-data :PROPERTIES: :CUSTOM_ID: test-data :END: + *** Test gpgme_data_release_and_get_mem. :PROPERTIES: :CUSTOM_ID: test-gpgme-data-release-mem :END: + *** Test gpgme_data_seek for invalid types. :PROPERTIES: :CUSTOM_ID: test-gpgme-data-seek :END: + ** t-keylist :PROPERTIES: :CUSTOM_ID: test-keylist :END: Write a test for ext_keylist. + ** Test reading key signatures. :PROPERTIES: :CUSTOM_ID: test-key-sig @@ -532,6 +602,7 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: debug :END: + ** Tracepoints should be added at: Every public interface enter/leave, :PROPERTIES: :CUSTOM_ID: tracepoint-pub-int @@ -547,6 +618,7 @@ Hey Emacs, this is -*- org -*- mode! decrypt-verify.c delete.c edit.c encrypt.c encrypt-sign.c export.c genkey.c import.c key.c keylist.c passphrase.c progress.c signers.c sig-notation.c trust-item.c trustlist.c verify.c + ** TODO Handle malloc and vasprintf errors. But decide first if they should be :PROPERTIES: :CUSTOM_ID: malloc-vasprintf @@ -559,10 +631,12 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: build-suite :END: + ** TODO Make sure everything is cleaned correctly (esp. test area). :PROPERTIES: :CUSTOM_ID: clean-tests :END: + ** TODO Enable AC_CONFIG_MACRO_DIR and bump up autoconf version requirement. :PROPERTIES: :CUSTOM_ID: autoconf-macros @@ -575,6 +649,7 @@ Hey Emacs, this is -*- org -*- mode! :PROPERTIES: :CUSTOM_ID: error-checking :END: + ** TODO engine-gpgsm, with-validation :PROPERTIES: :CUSTOM_ID: gpgsm-validation @@ -615,7 +690,11 @@ Hey Emacs, this is -*- org -*- mode! Write a guide/best practices for maintainers of GPGME packages with third party package management systems. -Copyright 2004, 2005, 2018 g10 Code GmbH + +* Copyright 2004, 2005, 2018 g10 Code GmbH + :PROPERTIES: + :CUSTOM_ID: copyright-and-license + :END: This file is free software; as a special exception the author gives unlimited permission to copy and/or distribute it, with or without diff --git a/lang/python/docs/GPGMEpythonHOWTOen.org b/lang/python/docs/GPGMEpythonHOWTOen.org new file mode 100644 index 00000000..655209ac --- /dev/null +++ b/lang/python/docs/GPGMEpythonHOWTOen.org @@ -0,0 +1,1370 @@ +#+TITLE: GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English) +#+LATEX_COMPILER: xelatex +#+LATEX_CLASS: article +#+LATEX_CLASS_OPTIONS: [12pt] +#+LATEX_HEADER: \usepackage{xltxtra} +#+LATEX_HEADER: \usepackage[margin=1in]{geometry} +#+LATEX_HEADER: \setmainfont[Ligatures={Common}]{Times New Roman} +#+LATEX_HEADER: \author{Ben McGinnes } + + +* Introduction + :PROPERTIES: + :CUSTOM_ID: intro + :END: + + | Version: | 0.1.0-draft | + | Author: | Ben McGinnes | + | Author GPG Key: | DB4724E6FA4286C92B4E55C4321E4E2373590E5D | + | Language: | Australian English, British English | + | xml:lang: | en-AU, en-GB, en | + + This document provides basic instruction in how to use the GPGME + Python bindings to programmatically leverage the GPGME library. + + +** Python 2 versus Python 3 + :PROPERTIES: + :CUSTOM_ID: py2-vs-py3 + :END: + + Though the GPGME Python bindings themselves provide support for + both Python 2 and 3, the focus is unequivocally on Python 3 and + specifically from Python 3.4 and above. As a consequence all the + examples and instructions in this guide use Python 3 code. + + Much of it will work with Python 2, but much of it also deals with + Python 3 byte literals, particularly when reading and writing data. + Developers concentrating on Python 2.7, and possibly even 2.6, will + need to make the appropriate modifications to support the older + string and unicode types as opposed to bytes. + + There are multiple reasons for concentrating on Python 3; some of + which relate to the immediate integration of these bindings, some + of which relate to longer term plans for both GPGME and the python + bindings and some of which relate to the impending EOL period for + Python 2.7. Essentially, though, there is little value in tying + the bindings to a version of the language which is a dead end and + the advantages offered by Python 3 over Python 2 make handling the + data types with which GPGME deals considerably easier. + + +** Examples + :PROPERTIES: + :CUSTOM_ID: howto-python3-examples + :END: + + All of the examples found in this document can be found as Python 3 + scripts in the =lang/python/examples/howto= directory. + + +* GPGME Concepts + :PROPERTIES: + :CUSTOM_ID: gpgme-concepts + :END: + + +** A C API + :PROPERTIES: + :CUSTOM_ID: gpgme-c-api + :END: + + Unlike many modern APIs with which programmers will be more + familiar with these days, the GPGME API is a C API. The API is + intended for use by C coders who would be able to access its + features by including the =gpgme.h= header file with their own C + source code and then access its functions just as they would any + other C headers. + + This is a very effective method of gaining complete access to the + API and in the most efficient manner possible. It does, however, + have the drawback that it cannot be directly used by other + languages without some means of providing an interface to those + languages. This is where the need for bindings in various + languages stems. + + +** Python bindings + :PROPERTIES: + :CUSTOM_ID: gpgme-python-bindings + :END: + + The Python bindings for GPGME provide a higher level means of + accessing the complete feature set of GPGME itself. It also + provides a more pythonic means of calling these API functions. + + The bindings are generated dynamically with SWIG and the copy of + =gpgme.h= generated when GPGME is compiled. + + This means that a version of the Python bindings is fundamentally + tied to the exact same version of GPGME used to generate that copy + of =gpgme.h=. + + +** Difference between the Python bindings and other GnuPG Python packages + :PROPERTIES: + :CUSTOM_ID: gpgme-python-bindings-diffs + :END: + + There have been numerous attempts to add GnuPG support to Python + over the years. Some of the most well known are listed here, along + with what differentiates them. + + +*** The python-gnupg package maintained by Vinay Sajip + :PROPERTIES: + :CUSTOM_ID: diffs-python-gnupg + :END: + + This is arguably the most popular means of integrating GPG with + Python. The package utilises the =subprocess= module to implement + wrappers for the =gpg= and =gpg2= executables normally invoked on + the command line (=gpg.exe= and =gpg2.exe= on Windows). + + The popularity of this package stemmed from its ease of use and + capability in providing the most commonly required features. + + Unfortunately it has been beset by a number of security issues in + the past; most of which stemmed from using unsafe methods of + accessing the command line via the =subprocess= calls. While some + effort has been made over the last two to three years (as of 2018) + to mitigate this, particularly by no longer providing shell access + through those subprocess calls, the wrapper is still somewhat + limited in the scope of its GnuPG features coverage. + + The python-gnupg package is available under the MIT license. + + +*** The gnupg package created and maintained by Isis Lovecruft + :PROPERTIES: + :CUSTOM_ID: diffs-isis-gnupg + :END: + + In 2015 Isis Lovecruft from the Tor Project forked and then + re-implemented the python-gnupg package as just gnupg. This new + package also relied on subprocess to call the =gpg= or =gpg2= + binaries, but did so somewhat more securely. + + The naming and version numbering selected for this package, + however, resulted in conflicts with the original python-gnupg and + since its functions were called in a different manner to + python-gnupg, the release of this package also resulted in a great + deal of consternation when people installed what they thought was + an upgrade that subsequently broke the code relying on it. + + The gnupg package is available under the GNU General Public + License version 3.0 (or any later version). + + +*** The PyME package maintained by Martin Albrecht + :PROPERTIES: + :CUSTOM_ID: diffs-pyme + :END: + + This package is the origin of these bindings, though they are + somewhat different now. For details of when and how the PyME + package was folded back into GPGME itself see the /Short History/ + document[fn:1] in this Python bindings =docs= directory.[fn:2] + + The PyME package was first released in 2002 and was also the first + attempt to implement a low level binding to GPGME. In doing so it + provided access to considerably more functionality than either the + =python-gnupg= or =gnupg= packages. + + The PyME package is only available for Python 2.6 and 2.7. + + Porting the PyME package to Python 3.4 in 2015 is what resulted in + it being folded into the GPGME project and the current bindings + are the end result of that effort. + + The PyME package is available under the same dual licensing as + GPGME itself: the GNU General Public License version 2.0 (or any + later version) and the GNU Lesser General Public License version + 2.1 (or any later version). + + +* GPGME Python bindings installation + :PROPERTIES: + :CUSTOM_ID: gpgme-python-install + :END: + + +** No PyPI + :PROPERTIES: + :CUSTOM_ID: do-not-use-pypi + :END: + + Most third-party Python packages and modules are available and + distributed through the Python Package Installer, known as PyPI. + + Due to the nature of what these bindings are and how they work, it + is infeasible to install the GPGME Python bindings in the same way. + + This is because the bindings use SWIG to dynamically generate C + bindings against =gpgme.h= and =gpgme.h= is generated from + =gpgme.h.in= at compile time when GPGME is built from source. Thus + to include a package in PyPI which actually built correctly would + require either statically built libraries for every architecture + bundled with it or a full implementation of C for each + architecture. + + +** Requirements + :PROPERTIES: + :CUSTOM_ID: gpgme-python-requirements + :END: + + The GPGME Python bindings only have three requirements: + + 1. A suitable version of Python 2 or Python 3. With Python 2 that + means Python 2.7 and with Python 3 that means Python 3.4 or + higher. + 2. SWIG. + 3. GPGME itself. Which also means that all of GPGME's dependencies + must be installed too. + + +** Installation + :PROPERTIES: + :CUSTOM_ID: installation + :END: + + Installing the Python bindings is effectively achieved by compiling + and installing GPGME itself. + + Once SWIG is installed with Python and all the dependencies for + GPGME are installed you only need to confirm that the version(s) of + Python you want the bindings installed for are in your =$PATH=. + + By default GPGME will attempt to install the bindings for the most + recent or highest version number of Python 2 and Python 3 it + detects in =$PATH=. It specifically checks for the =python= and + =python3= executables first and then checks for specific version + numbers. + + For Python 2 it checks for these executables in this order: + =python=, =python2= and =python2.7=. + + For Python 3 it checks for these executables in this order: + =python3=, =python3.6=, =python3.5= and =python3.4=. + + +*** Installing GPGME + :PROPERTIES: + :CUSTOM_ID: install-gpgme + :END: + + See the GPGME =README= file for details of how to install GPGME from + source. + + +* Fundamentals + :PROPERTIES: + :CUSTOM_ID: howto-fund-a-mental + :END: + + Before we can get to the fun stuff, there are a few matters + regarding GPGME's design which hold true whether you're dealing with + the C code directly or these Python bindings. + + +** No REST + :PROPERTIES: + :CUSTOM_ID: no-rest-for-the-wicked + :END: + + The first part of which is or will be fairly blatantly obvious upon + viewing the first example, but it's worth reiterating anyway. That + being that this API is /*not*/ a REST API. Nor indeed could it + ever be one. + + Most, if not all, Python programmers (and not just Python + programmers) know how easy it is to work with a RESTful API. In + fact they've become so popular that many other APIs attempt to + emulate REST-like behaviour as much as they are able. Right down + to the use of JSON formatted output to facilitate the use of their + API without having to retrain developers. + + This API does not do that. It would not be able to do that and + also provide access to the entire C API on which it's built. It + does, however, provide a very pythonic interface on top of the + direct bindings and it's this pythonic layer with which this HOWTO + deals with. + + +** Context + :PROPERTIES: + :CUSTOM_ID: howto-get-context + :END: + + One of the reasons which prevents this API from being RESTful is + that most operations require more than one instruction to the API + to perform the task. Sure, there are certain functions which can + be performed simultaneously, particularly if the result known or + strongly anticipated (e.g. selecting and encrypting to a key known + to be in the public keybox). + + There are many more, however, which cannot be manipulated so + readily: they must be performed in a specific sequence and the + result of one operation has a direct bearing on the outcome of + subsequent operations. Not merely by generating an error either. + + When dealing with this type of persistent state on the web, full of + both the RESTful and REST-like, it's most commonly referred to as a + session. In GPGME, however, it is called a context and every + operation type has one. + + +* Working with keys + :PROPERTIES: + :CUSTOM_ID: howto-keys + :END: + + +** Key selection + :PROPERTIES: + :CUSTOM_ID: howto-keys-selection + :END: + + Selecting keys to encrypt to or to sign with will be a common + occurrence when working with GPGMe and the means available for + doing so are quite simple. + + They do depend on utilising a Context; however once the data is + recorded in another variable, that Context does not need to be the + same one which subsequent operations are performed. + + The easiest way to select a specific key is by searching for that + key's key ID or fingerprint, preferably the full fingerprint + without any spaces in it. A long key ID will probably be okay, but + is not advised and short key IDs are already a problem with some + being generated to match specific patterns. It does not matter + whether the pattern is upper or lower case. + + So this is the best method: + + #+begin_src python + import gpg + + k = gpg.Context().keylist(pattern="258E88DCBD3CD44D8E7AB43F6ECB6AF0DEADBEEF") + keys = list(k) + #+end_src + + This is passable and very likely to be common: + + #+begin_src python + import gpg + + k = gpg.Context().keylist(pattern="0x6ECB6AF0DEADBEEF") + keys = list(k) + #+end_src + + And this is a really bad idea: + + #+begin_src python + import gpg + + k = gpg.Context().keylist(pattern="0xDEADBEEF") + keys = list(k) + #+end_src + + Alternatively it may be that the intention is to create a list of + keys which all match a particular search string. For instance all + the addresses at a particular domain, like this: + + #+begin_src python + import gpg + + ncsc = gpg.Context().keylist(pattern="ncsc.mil") + nsa = list(ncsc) + #+end_src + + +*** Counting keys + :PROPERTIES: + :CUSTOM_ID: howto-keys-counting + :END: + + Counting the number of keys in your public keybox (=pubring.kbx=), + the format which has superseded the old keyring format + (=pubring.gpg= and =secring.gpg=), or the number of secret keys is + a very simple task. + + #+begin_src python + import gpg + + c = gpg.Context() + seckeys = c.keylist(pattern=None, secret=True) + pubkeys = c.keylist(pattern=None, secret=False) + + seclist = list(seckeys) + secnum = len(seclist) + + publist = list(pubkeys) + pubnum = len(publist) + + print(""" + Number of secret keys: {0} + Number of public keys: {1} + """.format(secnum, pubnum)) + #+end_src + + +** Get key + :PROPERTIES: + :CUSTOM_ID: howto-get-key + :END: + + An alternative method of getting a single key via its fingerprint + is available directly within a Context with =Context().get_key=. + This is the preferred method of selecting a key in order to modify + it, sign or certify it and for obtaining relevant data about a + single key as a part of other functions; when verifying a signature + made by that key, for instance. + + By default this method will select public keys, but it can select + secret keys as well. + + This first example demonstrates selecting the current key of Werner + Koch, which is due to expire at the end of 2018: + + #+begin_src python + import gpg + + fingerprint = "80615870F5BAD690333686D0F2AD85AC1E42B367" + key = gpg.Context().get_key(fingerprint) + #+end_src + + Whereas this example demonstrates selecting the author's current + key with the =secret= key word argument set to =True=: + + #+begin_src python + import gpg + + fingerprint = "DB4724E6FA4286C92B4E55C4321E4E2373590E5D" + key = gpg.Context().get_key(fingerprint, secret=True) + #+end_src + + It is, of course, quite possible to select expired, disabled and + revoked keys with this function, but only to effectively display + information about those keys. + + It is also possible to use both unicode or string literals and byte + literals with the fingerprint when getting a key in this way. + + +* Basic Functions + :PROPERTIES: + :CUSTOM_ID: howto-the-basics + :END: + + The most frequently called features of any cryptographic library + will be the most fundamental tasks for encryption software. In this + section we will look at how to programmatically encrypt data, + decrypt it, sign it and verify signatures. + + +** Encryption + :PROPERTIES: + :CUSTOM_ID: howto-basic-encryption + :END: + + Encrypting is very straight forward. In the first example below + the message, =text=, is encrypted to a single recipient's key. In + the second example the message will be encrypted to multiple + recipients. + + +*** Encrypting to one key + :PROPERTIES: + :CUSTOM_ID: howto-basic-encryption-single + :END: + + Once the the Context is set the main issues with encrypting data + is essentially reduced to key selection and the keyword arguments + specified in the =gpg.Context().encrypt()= method. + + Those keyword arguments are: =recipients=, a list of keys + encrypted to (covered in greater detail in the following section); + =sign=, whether or not to sign the plaintext data, see subsequent + sections on signing and verifying signatures below (defaults to + =True=); =sink=, to write results or partial results to a secure + sink instead of returning it (defaults to =None=); =passphrase=, + only used when utilising symmetric encryption (defaults to + =None=); =always_trust=, used to override the trust model settings + for recipient keys (defaults to =False=); =add_encrypt_to=, + utilises any preconfigured =encrypt-to= or =default-key= settings + in the user's =gpg.conf= file (defaults to =False=); =prepare=, + prepare for encryption (defaults to =False=); =expect_sign=, + prepare for signing (defaults to =False=); =compress=, compresses + the plaintext prior to encryption (defaults to =True=). + + #+begin_src python + import gpg + + a_key = "0x12345678DEADBEEF" + text = b"""Some text to test with. + + Since the text in this case must be bytes, it is most likely that + the input form will be a separate file which is opened with "rb" + as this is the simplest method of obtaining the correct data + format. + """ + + c = gpg.Context(armor=True) + rkey = list(c.keylist(pattern=a_key, secret=False)) + ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, sign=False) + + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + #+end_src + + Though this is even more likely to be used like this; with the + plaintext input read from a file, the recipient keys used for + encryption regardless of key trust status and the encrypted output + also encrypted to any preconfigured keys set in the =gpg.conf= + file: + + #+begin_src python + import gpg + + a_key = "0x12345678DEADBEEF" + + with open("secret_plans.txt", "rb") as afile: + text = afile.read() + + c = gpg.Context(armor=True) + rkey = list(c.keylist(pattern=a_key, secret=False)) + ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, + sign=True, always_trust=True, + add_encrypt_to=True) + + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + #+end_src + + If the =recipients= paramater is empty then the plaintext is + encrypted symmetrically. If no =passphrase= is supplied as a + parameter or via a callback registered with the =Context()= then + an out-of-band prompt for the passphrase via pinentry will be + invoked. + + +*** Encrypting to multiple keys + :PROPERTIES: + :CUSTOM_ID: howto-basic-encryption-multiple + :END: + + Encrypting to multiple keys essentially just expands upon the key + selection process and the recipients from the previous examples. + + The following example encrypts a message (=text=) to everyone with + an email address on the =gnupg.org= domain,[fn:3] but does /not/ encrypt + to a default key or other key which is configured to normally + encrypt to. + + #+begin_src python + import gpg + + text = b"""Oh look, another test message. + + The same rules apply as with the previous example and more likely + than not, the message will actually be drawn from reading the + contents of a file or, maybe, from entering data at an input() + prompt. + + Since the text in this case must be bytes, it is most likely that + the input form will be a separate file which is opened with "rb" + as this is the simplest method of obtaining the correct data + format. + """ + + c = gpg.Context(armor=True) + rpattern = list(c.keylist(pattern="@gnupg.org", secret=False)) + logrus = [] + + for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, sign=False, + always_trust=True) + + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + #+end_src + + All it would take to change the above example to sign the message + and also encrypt the message to any configured default keys would + be to change the =c.encrypt= line to this: + + #+begin_src python + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + always_trust=True, + add_encrypt_to=True) + #+end_src + + The only keyword arguments requiring modification are those for + which the default values are changing. The default value of + =sign= is =True=, the default of =always_trust= is =False=, the + default of =add_encrypt_to= is =False=. + + If =always_trust= is not set to =True= and any of the recipient + keys are not trusted (e.g. not signed or locally signed) then the + encryption will raise an error. It is possible to mitigate this + somewhat with something more like this: + + #+begin_src python + import gpg + + with open("secret_plans.txt.asc", "rb") as afile: + text = afile.read() + + c = gpg.Context(armor=True) + rpattern = list(c.keylist(pattern="@gnupg.org", secret=False)) + logrus = [] + + for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, add_encrypt_to=True) + except gpg.errors.InvalidRecipients as e: + for i in range(len(e.recipients)): + for n in range(len(logrus)): + if logrus[n].fpr == e.recipients[i].fpr: + logrus.remove(logrus[n]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, add_encrypt_to=True) + except: + pass + + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + #+end_src + + This will attempt to encrypt to all the keys searched for, then + remove invalid recipients if it fails and try again. + + +** Decryption + :PROPERTIES: + :CUSTOM_ID: howto-basic-decryption + :END: + + Decrypting something encrypted to a key in one's secret keyring is + fairly straight forward. + + In this example code, however, preconfiguring either + =gpg.Context()= or =gpg.core.Context()= as =c= is unnecessary + because there is no need to modify the Context prior to conducting + the decryption and since the Context is only used once, setting it + to =c= simply adds lines for no gain. + + #+begin_src python + import gpg + + ciphertext = input("Enter path and filename of encrypted file: ") + newfile = input("Enter path and filename of file to save decrypted data to: ") + with open(ciphertext, "rb") as cfile: + plaintext, result, verify_result = gpg.Context().decrypt(cfile) + with open(newfile, "wb") as nfile: + nfile.write(plaintext) + #+end_src + + The data available in plaintext in this example is the decrypted + content as a byte object in =plaintext[0]=, the recipient key IDs + and algorithms in =plaintext[1]= and the results of verifying any + signatures of the data in =plaintext[0]=. + + +** Signing text and files + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing + :END: + + The following sections demonstrate how to specify keys to sign with. + + +*** Signing key selection + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing-signers + :END: + + By default GPGME and the Python bindings will use the default key + configured for the user invoking the GPGME API. If there is no + default key specified and there is more than one secret key + available it may be necessary to specify the key or keys with + which to sign messages and files. + + #+begin_src python + import gpg + + logrus = input("Enter the email address or string to match signing keys to: ") + hancock = gpg.Context().keylist(pattern=logrus, secret=True) + sig_src = list(hancock) + #+end_src + + The signing examples in the following sections include the + explicitly designated =signers= parameter in two of the five + examples; once where the resulting signature would be ASCII + armoured and once where it would not be armoured. + + While it would be possible to enter a key ID or fingerprint here + to match a specific key, it is not possible to enter two + fingerprints and match two keys since the patten expects a string, + bytes or None and not a list. A string with two fingerprints + won't match any single key. + + +*** Normal or default signing messages or files + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing-normal + :END: + + The normal or default signing process is essentially the same as + is most often invoked when also encrypting a message or file. So + when the encryption component is not utilised, the result is to + produce an encoded and signed output which may or may not be ASCII + armoured and which may or may not also be compressed. + + By default compression will be used unless GnuPG detects that the + plaintext is already compressed. ASCII armouring will be + determined according to the value of =gpg.Context().armor=. + + The compression algorithm is selected in much the same way as the + symmetric encryption algorithm or the hash digest algorithm is + when multiple keys are involved; from the preferences saved into + the key itself or by comparison with the preferences with all + other keys involved. + + #+begin_src python + import gpg + + text0 = """Declaration of ... something. + + """ + text = text0.encode() + + c = gpg.Context(armor=True, signers=sig_src) + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + + with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) + #+end_src + + Though everything in this example is accurate, it is more likely + that reading the input data from another file and writing the + result to a new file will be performed more like the way it is done + in the next example. Even if the output format is ASCII armoured. + + #+begin_src python + import gpg + + with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + + c = gpg.Context() + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + + with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) + #+end_src + + +*** Detached signing messages and files + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing-detached + :END: + + Detached signatures will often be needed in programmatic uses of + GPGME, either for signing files (e.g. tarballs of code releases) + or as a component of message signing (e.g. PGP/MIME encoded + email). + + #+begin_src python + import gpg + + text0 = """Declaration of ... something. + + """ + text = text0.encode() + + c = gpg.Context(armor=True) + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + + with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) + #+end_src + + As with normal signatures, detached signatures are best handled as + byte literals, even when the output is ASCII armoured. + + #+begin_src python + import gpg + + with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + + c = gpg.Context(signers=sig_src) + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + + with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) + #+end_src + + +*** Clearsigning messages or text + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing-clear + :END: + + Though PGP/in-line messages are no longer encouraged in favour of + PGP/MIME, there is still sometimes value in utilising in-line + signatures. This is where clear-signed messages or text is of + value. + + #+begin_src python + import gpg + + text0 = """Declaration of ... something. + + """ + text = text0.encode() + + c = gpg.Context() + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + + with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) + #+end_src + + In spite of the appearance of a clear-signed message, the data + handled by GPGME in signing it must still be byte literals. + + #+begin_src python + import gpg + + with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + + c = gpg.Context() + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + + with open("/path/to/statement.txt.asc", "wb") as afile: + afile.write(signed_data) + #+end_src + + +** Signature verification + :PROPERTIES: + :CUSTOM_ID: howto-basic-verification + :END: + + Essentially there are two principal methods of verification of a + signature. The first of these is for use with the normal or + default signing method and for clear-signed messages. The second is + for use with files and data with detached signatures. + + The following example is intended for use with the default signing + method where the file was not ASCII armoured: + + #+begin_src python + import gpg + import time + + filename = "statement.txt" + gpg_file = "statement.txt.gpg" + + c = gpg.Context() + + try: + data, result = c.verify(open(gpg_file)) + verified = True + except gpg.errors.BadSignatures as e: + verified = False + print(e) + + if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: + {0} + with key {1} + made at {2} + """.format(c.get_key(sign.fpr).uids[0].uid, + sign.fpr, time.ctime(sign.timestamp))) + else: + pass + #+end_src + + Whereas this next example, which is almost identical would work + with normal ASCII armoured files and with clear-signed files: + + #+begin_src python + import gpg + import time + + filename = "statement.txt" + asc_file = "statement.txt.asc" + + c = gpg.Context() + + try: + data, result = c.verify(open(asc_file)) + verified = True + except gpg.errors.BadSignatures as e: + verified = False + print(e) + + if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: + {0} + with key {1} + made at {2} + """.format(c.get_key(sign.fpr).uids[0].uid, + sign.fpr, time.ctime(sign.timestamp))) + else: + pass + #+end_src + + In both of the previous examples it is also possible to compare the + original data that was signed against the signed data in =data= to + see if it matches with something like this: + + #+begin_src python + with open(filename, "rb") as afile: + text = afile.read() + + if text == data: + print("Good signature.") + else: + pass + #+end_src + + The following two examples, however, deal with detached signatures. + With his method of verification the data that was signed does not + get returned since it is already being explicitly referenced in the + first argument of =c.verify=. So =data= is =None= and only the + information in =result= is available. + + #+begin_src python + import gpg + import time + + filename = "statement.txt" + sig_file = "statement.txt.sig" + + c = gpg.Context() + + try: + data, result = c.verify(open(filename), open(sig_file)) + verified = True + except gpg.errors.BadSignatures as e: + verified = False + print(e) + + if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: + {0} + with key {1} + made at {2} + """.format(c.get_key(sign.fpr).uids[0].uid, + sign.fpr, time.ctime(sign.timestamp))) + else: + pass + #+end_src + + #+begin_src python + import gpg + import time + + filename = "statement.txt" + asc_file = "statement.txt.asc" + + c = gpg.Context() + + try: + data, result = c.verify(open(filename), open(asc_file)) + verified = True + except gpg.errors.BadSignatures as e: + verified = False + print(e) + + if verified is not None: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: + {0} + with key {1} + made at {2} + """.format(c.get_key(sign.fpr).uids[0].uid, + sign.fpr, time.ctime(sign.timestamp))) + else: + pass + #+end_src + + +* Creating keys and subkeys + :PROPERTIES: + :CUSTOM_ID: key-generation + :END: + + The one thing, aside from GnuPG itself, that GPGME depends on, of + course, is the keys themselves. So it is necessary to be able to + generate them and modify them by adding subkeys, revoking or + disabling them, sometimes deleting them and doing the same for user + IDs. + + In the following examples a key will be created for the world's + greatest secret agent, Danger Mouse. Since Danger Mouse is a secret + agent he needs to be able to protect information to =SECRET= level + clearance, so his keys will be 3072-bit keys. + + The pre-configured =gpg.conf= file which sets cipher, digest and + other preferences contains the following configuration parameters: + + #+begin_src conf + expert + allow-freeform-uid + allow-secret-key-import + trust-model tofu+pgp + tofu-default-policy unknown + enable-large-rsa + enable-dsa2 + # cert-digest-algo SHA256 + cert-digest-algo SHA512 + default-preference-list TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 ZLIB BZIP2 ZIP Uncompressed + personal-cipher-preferences TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES + personal-digest-preferences SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 + personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed + #+end_src + + +** Primary key + :PROPERTIES: + :CUSTOM_ID: keygen-primary + :END: + + Generating a primary key uses the =create_key= method in a Context. + It contains multiple arguments and keyword arguments, including: + =userid=, =algorithm=, =expires_in=, =expires=, =sign=, =encrypt=, + =certify=, =authenticate=, =passphrase= and =force=. The defaults + for all of those except =userid=, =algorithm=, =expires_in=, + =expires= and =passphrase= is =False=. The defaults for + =algorithm= and =passphrase= is =None=. The default for + =expires_in= is =0=. The default for =expires= is =True=. There + is no default for =userid=. + + If =passphrase= is left as =None= then the key will not be + generated with a passphrase, if =passphrase= is set to a string + then that will be the passphrase and if =passphrase= is set to + =True= then gpg-agent will launch pinentry to prompt for a + passphrase. For the sake of convenience, these examples will keep + =passphrase= set to =None=. + + #+begin_src python + import gpg + + c = gpg.Context() + + c.home_dir = "~/.gnupg-dm" + userid = "Danger Mouse " + + dmkey = c.create_key(userid, algorithm = "rsa3072", expires_in = 31536000, + sign = True, certify = True) + #+end_src + + One thing to note here is the use of setting the =c.home_dir= + parameter. This enables generating the key or keys in a different + location. In this case to keep the new key data created for this + example in a separate location rather than adding it to existing + and active key store data. As with the default directory, + =~/.gnupg=, any temporary or separate directory needs the + permissions set to only permit access by the directory owner. On + posix systems this means setting the directory permissions to 700. + + The successful generation of the key can be confirmed via the + returned =GenkeyResult= object, which includes the following data: + + #+begin_src python + print(""" + Fingerprint: {0} + Primary Key: {1} + Public Key: {2} + Secret Key: {3} + Sub Key: {4} + User IDs: {5} + """.format(dmkey.fpr, dmkey.primary, dmkey.pubkey, dmkey.seckey, dmkey.sub, + dmkey.uid)) + #+end_src + + Alternatively the information can be confirmed using the command + line program: + + #+begin_src shell + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse + + bash-4.4$ + #+end_src + + As with generating keys manually, to preconfigure expanded + preferences for the cipher, digest and compression algorithms, the + =gpg.conf= file must contain those details in the home directory in + which the new key is being generated. I used a cut down version of + my own =gpg.conf= file in order to be able to generate this: + + #+begin_src shell + bash-4.4$ gpg --homedir ~/.gnupg-dm --edit-key 177B7C25DB99745EE2EE13ED026D2F19E99E63AA showpref quit + Secret key is available. + + sec rsa3072/026D2F19E99E63AA + created: 2018-03-15 expires: 2019-03-15 usage: SC + trust: ultimate validity: ultimate + [ultimate] (1). Danger Mouse + + [ultimate] (1). Danger Mouse + Cipher: TWOFISH, CAMELLIA256, AES256, CAMELLIA192, AES192, CAMELLIA128, AES, BLOWFISH, IDEA, CAST5, 3DES + Digest: SHA512, SHA384, SHA256, SHA224, RIPEMD160, SHA1 + Compression: ZLIB, BZIP2, ZIP, Uncompressed + Features: MDC, Keyserver no-modify + + bash-4.4$ + #+end_src + + +** Subkeys + :PROPERTIES: + :CUSTOM_ID: keygen-subkeys + :END: + + Adding subkeys to a primary key is fairly similar to creating the + primary key with the =create_subkey= method. Most of the arguments + are the same, but not quite all. Instead of the =userid= argument + there is now a =key= argument for selecting which primary key to + add the subkey to. + + In the following example an encryption subkey will be added to the + primary key. Since Danger Mouse is a security conscious secret + agent, this subkey will only be valid for about six months, half + the length of the primary key. + + #+begin_src python + import gpg + + c = gpg.Context() + c.home_dir = "~/.gnupg-dm" + + key = c.get_key(dmkey.fpr, secret = True) + dmsub = c.create_subkey(key, algorithm = "rsa3072", expires_in = 15768000, + encrypt = True) + #+end_src + + As with the primary key, the results here can be checked with: + + #+begin_src python + print(""" + Fingerprint: {0} + Primary Key: {1} + Public Key: {2} + Secret Key: {3} + Sub Key: {4} + User IDs: {5} + """.format(dmsub.fpr, dmsub.primary, dmsub.pubkey, dmsub.seckey, dmsub.sub, + dmsub.uid)) + #+end_src + + As well as on the command line with: + + #+begin_src shell + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse + ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + + bash-4.4$ + #+end_src + + +** User IDs + :PROPERTIES: + :CUSTOM_ID: keygen-uids + :END: + + By comparison to creating primary keys and subkeys, adding a new + user ID to an existing key is much simpler. The method used to do + this is =key_add_uid= and the only arguments it takes are for the + =key= and the new =uid=. + + #+begin_src python + import gpg + + c = gpg.Context() + c.home_dir = "~/.gnupg-dm" + + dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" + key = c.get_key(dmfpr, secret = True) + uid = "Danger Mouse " + + c.key_add_uid(key, uid) + #+end_src + + Unsurprisingly the result of this is: + + #+begin_src shell + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse + uid [ultimate] Danger Mouse + ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + + bash-4.4$ + #+end_src + + +** Key certification + :PROPERTIES: + :CUSTOM_ID: key-sign + :END: + + Since key certification is more frequently referred to as key + signing, the method used to perform this function is =key_sign=. + + The =key_sign= method takes four arguments: =key=, =uids=, + =expires_in= and =local=. The default value of =uids= is =None= + and which results in all user IDs being selected. The default + values of =expires_in= snd =local= is =False=; which result in the + signature never expiring and being able to be exported. + + The =key= is the key being signed rather than the key doing the + signing. To change the key doing the signing refer to the signing + key selection above for signing messages and files. + + If the =uids= value is not =None= then it must either be a string + to match a single user ID or a list of strings to match multiple + user IDs. In this case the matching of those strings must be + precise and it is case sensitive. + + To sign Danger Mouse's key for just the initial user ID with a + signature which will last a little over a month, do this: + + #+begin_src python + import gpg + + c = gpg.Context() + uid = "Danger Mouse " + + dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" + key = c.get_key(dmfpr, secret = True) + c.key_sign(key, uids = uid, expires_in = 2764800) + #+end_src + + +* Miscellaneous work-arounds + :PROPERTIES: + :CUSTOM_ID: cheats-and-hacks + :END: + + +** Group lines + :PROPERTIES: + :CUSTOM_ID: group-lines + :END: + + There is not yet an easy way to access groups configured in the + gpg.conf file from within GPGME. As a consequence these central + groupings of keys cannot be shared amongst multiple programs, such + as MUAs readily. + + The following code, however, provides a work-around for obtaining + this information in Python. + + #+begin_src python + import subprocess + + lines = subprocess.getoutput("gpgconf --list-options gpg").splitlines() + + for i in range(len(lines)): + if lines[i].startswith("group") is True: + line = lines[i] + else: + pass + + groups = line.split(":")[-1].replace('"', '').split(',') + + group_lines = groups + for i in range(len(group_lines)): + group_lines[i] = group_lines[i].split("=") + + group_lists = group_lines + for i in range(len(group_lists)): + group_lists[i][1] = group_lists[i][1].split() + #+end_src + + The result of that code is that =group_lines= is a list of lists + where =group_lines[i][0]= is the name of the group and + =group_lines[i][1]= is the key IDs of the group as a string. + + The =group_lists= result is very similar in that it is a list of + lists. The first part, =group_lists[i][0]= matches + =group_lines[i][0]= as the name of the group, but + =group_lists[i][1]= is the key IDs of the group as a string. + + +* Copyright and Licensing + :PROPERTIES: + :CUSTOM_ID: copyright-and-license + :END: + + +** Copyright (C) The GnuPG Project, 2018 + :PROPERTIES: + :CUSTOM_ID: copyright + :END: + + Copyright © The GnuPG Project, 2018. + + +** License GPL compatible + :PROPERTIES: + :CUSTOM_ID: license + :END: + + This file is free software; as a special exception the author gives + unlimited permission to copy and/or distribute it, with or without + modifications, as long as this notice is preserved. + + This file is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY, to the extent permitted by law; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. + + +* Footnotes + +[fn:1] =Short_History.org= and/or =Short_History.html=. + +[fn:2] The =lang/python/docs/= directory in the GPGME source. + +[fn:3] You probably don't really want to do this. Searching the +keyservers for "gnupg.org" produces over 400 results, the majority of +which aren't actually at the gnupg.org domain, but just included a +comment regarding the project in their key somewhere. diff --git a/lang/python/docs/TODO.org b/lang/python/docs/TODO.org index 9f039d81..add8f4ff 100644 --- a/lang/python/docs/TODO.org +++ b/lang/python/docs/TODO.org @@ -28,13 +28,74 @@ to produce reST versions via Pandoc and DITA XML can be reached through converting to either Markdown or XHTML first. -** TODO Documentation HOWTO + +** STARTED Documentation HOWTO :PROPERTIES: :CUSTOM_ID: todo-docs-howto :END: + - State "STARTED" from "TODO" [2018-03-08 Thu 13:59] \\ + Started yesterday. Write a HOWTO style guide for the current Python bindings. +*** DONE Start python bindings HOWTO + CLOSED: [2018-03-07 Wed 18:14] + :PROPERTIES: + :CUSTOM_ID: howto-start + :END: + + +*** STARTED Include certain specific instructions in the HOWTO + :PROPERTIES: + :CUSTOM_ID: howto-requests + :END: + + Note: moved the S/MIME bits out to their own section of the TODO + list and may be served better by separate HOWTO documentation + anyway. + + - State "STARTED" from "TODO" [2018-03-09 Fri 15:27] + Some functions can be worked out from the handful of examples + available, but many more can't and I've already begun receiving + requests for certain functions to be explained. + + +**** DONE Standard scenarios + CLOSED: [2018-03-19 Mon 12:34] + :PROPERTIES: + :CUSTOM_ID: howto-the-basics + :END: + + - State "DONE" from "STARTED" [2018-03-19 Mon 12:34] \\ + All four of those are done. + - State "STARTED" from "TODO" [2018-03-09 Fri 15:26] \\ + Began with the example code, now to add the text. + What everyone expects: encryption, decryption, signing and verifying. + + +**** STARTED Key control + :PROPERTIES: + :CUSTOM_ID: howto-key-control + :END: + + - State "STARTED" from "TODO" [2018-03-19 Mon 12:35] \\ + Generating keys and subkeys are done, but revocation is still to be done. + Generating keys, adding subkeys, revoking subkeys (and keeping + the cert key), adding and revoking UIDs, signing/certifying keys. + + +**** DONE More key control + CLOSED: [2018-03-19 Mon 12:36] + :PROPERTIES: + :CUSTOM_ID: howto-key-selection + :END: + + - State "DONE" from "TODO" [2018-03-19 Mon 12:36] \\ + Key selection, searching, matching and counting is done. + Selecting keys to encrypt to or manipulate in other ways (e.g. as + with key control or the basics). + + ** TODO Documentation SWIG :PROPERTIES: :CUSTOM_ID: todo-docs-swig @@ -47,6 +108,7 @@ something to be used in conjunction with the existing GPGME documentation which makes it easier for Python developers to use. + ** TODO GUI examples :PROPERTIES: :CUSTOM_ID: todo-gui-examples @@ -56,6 +118,7 @@ to either match or be similar to the old GTK2 examples available with PyME. + ** TODO Replace SWIG :PROPERTIES: :CUSTOM_ID: todo-replace-swig @@ -71,6 +134,7 @@ bindings using a more suitable means of interfacing with the GPGME C source code. + *** TODO Replacement for SWIG :PROPERTIES: :CUSTOM_ID: todo-replace-swig-replacement @@ -80,6 +144,7 @@ the most viable candidate, but some additional testing and checks are yet to be completed. + ** TODO API for an API :PROPERTIES: :CUSTOM_ID: todo-api-squared @@ -94,6 +159,16 @@ available or for which it is too difficult to create proper bindings. + +** TODO S/MIME + :PROPERTIES: + :CUSTOM_ID: s-mime + :END: + + Eventually add some of this, but the OpenPGP details are far more + important at the moment. + + * Project Task Details :PROPERTIES: :CUSTOM_ID: detailed-tasks diff --git a/lang/python/examples/howto/README.org b/lang/python/examples/howto/README.org new file mode 100644 index 00000000..b74ae7e2 --- /dev/null +++ b/lang/python/examples/howto/README.org @@ -0,0 +1,58 @@ +#+TITLE: GPGME Python Bindings HOWTO Examples +#+LATEX_COMPILER: xelatex +#+LATEX_CLASS: article +#+LATEX_CLASS_OPTIONS: [12pt] +#+LATEX_HEADER: \usepackage{xltxtra} +#+LATEX_HEADER: \usepackage[margin=1in]{geometry} +#+LATEX_HEADER: \setmainfont[Ligatures={Common}]{Times New Roman} +#+LATEX_HEADER: \author{Ben McGinnes } + + +* Examples + :PROPERTIES: + :CUSTOM_ID: gpgme-python3-examples + :END: + + The contents of this directory are the examples included in the /GNU + Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO/ file. Each + script is explicitly for Python 3 and specifically for Python 3.4 or + later. + + Some of these scripts may work with Python 2.7, but there are no + guarantees. They will include the relevant imports from the + =__future__= module to facilitate that if possible. + + +* Copyright and Licensing + :PROPERTIES: + :CUSTOM_ID: copyright-and-license + :END: + + Unless otherwise stated, all the examples in this directory are + released under the same terms as GPGME itself; that is they are dual + licensed under the terms of both 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). + + +** Copyright (C) The GnuPG Project, 2018 + :PROPERTIES: + :CUSTOM_ID: copyright + :END: + + Copyright © The GnuPG Project, 2018. + + +** License GPL compatible + :PROPERTIES: + :CUSTOM_ID: license + :END: + + This file is free software; as a special exception the author gives + unlimited permission to copy and/or distribute it, with or without + modifications, as long as this notice is preserved. + + This file is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY, to the extent permitted by law; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. diff --git a/lang/python/examples/howto/clear-sign-file.py b/lang/python/examples/howto/clear-sign-file.py new file mode 100755 index 00000000..597bbc59 --- /dev/null +++ b/lang/python/examples/howto/clear-sign-file.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import gpg +import sys + +""" +Clear-signs a file with a specified key. If entering both the key and the +filename on the command line, the key must be entered first. +""" + +if len(sys.argv) > 3: + logrus = sys.argv[1] + filename = " ".join(sys.argv[2:]) +elif len(sys.argv) == 3: + logrus = sys.argv[1] + filename = sys.argv[2] +elif len(sys.argv) == 2: + logrus = sys.argv[1] + filename = input("Enter the path and filename to sign: ") +else: + logrus = input("Enter the fingerprint or key ID to sign with: ") + filename = input("Enter the path and filename to sign: ") + +with open(filename, "rb") as f: + text = f.read() + +key = list(gpg.Context().keylist(pattern=logrus)) + +with gpg.Context(armor=True, signers=key) as c: + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + with open("{0}.asc".format(filename), "wb") as f: + f.write(signed_data) diff --git a/lang/python/examples/howto/decrypt-file.py b/lang/python/examples/howto/decrypt-file.py new file mode 100755 index 00000000..60a050bd --- /dev/null +++ b/lang/python/examples/howto/decrypt-file.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import gpg +import sys + +if len(sys.argv) == 3: + ciphertext = sys.argv[1] + newfile = sys.argv[2] +elif len(sys.argv) == 2: + ciphertext = sys.argv[1] + newfile = input("Enter path and filename of file to save decrypted data to: ") +else: + ciphertext = input("Enter path and filename of encrypted file: ") + newfile = input("Enter path and filename of file to save decrypted data to: ") + +with open(ciphertext, "rb") as cfile: + plaintext, result, verify_result = gpg.Context().decrypt(cfile) + +with open(newfile, "wb") as nfile: + nfile.write(plaintext) diff --git a/lang/python/examples/howto/detach-sign-file.py b/lang/python/examples/howto/detach-sign-file.py new file mode 100755 index 00000000..99fbe65e --- /dev/null +++ b/lang/python/examples/howto/detach-sign-file.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import gpg +import sys + +""" +Signs a file with a specified key. If entering both the key and the filename +on the command line, the key must be entered first. + +Will produce both an ASCII armoured and GPG binary format copy of the detached +signature file. +""" + +if len(sys.argv) > 3: + logrus = sys.argv[1] + filename = " ".join(sys.argv[2:]) +elif len(sys.argv) == 3: + logrus = sys.argv[1] + filename = sys.argv[2] +elif len(sys.argv) == 2: + logrus = sys.argv[1] + filename = input("Enter the path and filename to sign: ") +else: + logrus = input("Enter the fingerprint or key ID to sign with: ") + filename = input("Enter the path and filename to sign: ") + +with open(filename, "rb") as f: + text = f.read() + +key = list(gpg.Context().keylist(pattern=logrus)) + +with gpg.Context(armor=True, signers=key) as ca: + signed_data, result = ca.sign(text, mode=gpg.constants.sig.mode.DETACH) + with open("{0}.asc".format(filename), "wb") as fa: + fa.write(signed_data) + +with gpg.Context(signers=key) as cb: + signed_data, result = cb.sign(text, mode=gpg.constants.sig.mode.DETACH) + with open("{0}.sig".format(filename), "wb") as fb: + fb.write(signed_data) diff --git a/lang/python/examples/howto/encrypt-file.py b/lang/python/examples/howto/encrypt-file.py new file mode 100755 index 00000000..ad4e1cef --- /dev/null +++ b/lang/python/examples/howto/encrypt-file.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import gpg +import sys + +""" +Encrypts a file to a specified key. If entering both the key and the filename +on the command line, the key must be entered first. + +Will produce both an ASCII armoured and GPG binary format copy of the encrypted +file. +""" + +if len(sys.argv) > 3: + a_key = sys.argv[1] + filename = " ".join(sys.argv[2:]) +elif len(sys.argv) == 3: + a_key = sys.argv[1] + filename = sys.argv[2] +elif len(sys.argv) == 2: + a_key = sys.argv[1] + filename = input("Enter the path and filename to encrypt: ") +else: + a_key = input("Enter the fingerprint or key ID to encrypt to: ") + filename = input("Enter the path and filename to encrypt: ") + +rkey = list(gpg.Context().keylist(pattern=a_key, secret=False)) +with open(filename, "rb") as f: + text = f.read() + +with gpg.Context(armor=True) as ca: + try: + ciphertext, result, sign_result = ca.encrypt(text, recipients=rkey, + sign=False) + with open("{0}.asc".format(filename), "wb") as fa: + fa.write(ciphertext) + except gpg.errors.InvalidRecipients as e: + print(e) + +with gpg.Context() as cg: + try: + ciphertext, result, sign_result = cg.encrypt(text, recipients=rkey, + sign=False) + with open("{0}.gpg".format(filename), "wb") as fg: + fg.write(ciphertext) + except gpg.errors.InvalidRecipients as e: + print(e) diff --git a/lang/python/examples/howto/encrypt-sign-file.py b/lang/python/examples/howto/encrypt-sign-file.py new file mode 100755 index 00000000..41aaac86 --- /dev/null +++ b/lang/python/examples/howto/encrypt-sign-file.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import gpg +import sys + +""" +Signs and encrypts a file to a specified key. If entering both the key and the +filename on the command line, the key must be entered first. + +Signs with and also encrypts to the default key of the user invoking the +script. Will treat all recipients as trusted to permit encryption. + +Will produce both an ASCII armoured and GPG binary format copy of the signed +and encrypted file. +""" + +if len(sys.argv) > 3: + a_key = sys.argv[1] + filename = " ".join(sys.argv[2:]) +elif len(sys.argv) == 3: + a_key = sys.argv[1] + filename = sys.argv[2] +elif len(sys.argv) == 2: + a_key = sys.argv[1] + filename = input("Enter the path and filename to encrypt: ") +else: + a_key = input("Enter the fingerprint or key ID to encrypt to: ") + filename = input("Enter the path and filename to encrypt: ") + +rkey = list(gpg.Context().keylist(pattern=a_key, secret=False)) +with open(filename, "rb") as f: + text = f.read() + +with gpg.Context(armor=True) as ca: + ciphertext, result, sign_result = ca.encrypt(text, recipients=rkey, + always_trust=True, + add_encrypt_to=True) + with open("{0}.asc".format(filename), "wb") as fa: + fa.write(ciphertext) + +with gpg.Context() as cg: + ciphertext, result, sign_result = cg.encrypt(text, recipients=rkey, + always_trust=True, + add_encrypt_to=True) + with open("{0}.gpg".format(filename), "wb") as fg: + fg.write(ciphertext) diff --git a/lang/python/examples/howto/groups.py b/lang/python/examples/howto/groups.py new file mode 100644 index 00000000..5e7fdf60 --- /dev/null +++ b/lang/python/examples/howto/groups.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import subprocess + +""" +Intended for use with other scripts. + +Usage: from groups import group_lists +""" + +lines = subprocess.getoutput("gpgconf --list-options gpg").splitlines() + +for i in range(len(lines)): + if lines[i].startswith("group") is True: + line = lines[i] + else: + pass + +groups = line.split(":")[-1].replace('"', '').split(',') + +group_lines = groups +for i in range(len(group_lines)): + group_lines[i] = group_lines[i].split("=") + +group_lists = group_lines +for i in range(len(group_lists)): + group_lists[i][1] = group_lists[i][1].split() diff --git a/lang/python/examples/howto/keycount.py b/lang/python/examples/howto/keycount.py new file mode 100755 index 00000000..8e25454c --- /dev/null +++ b/lang/python/examples/howto/keycount.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +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)) diff --git a/lang/python/examples/howto/sign-file.py b/lang/python/examples/howto/sign-file.py new file mode 100755 index 00000000..01006df0 --- /dev/null +++ b/lang/python/examples/howto/sign-file.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import gpg +import sys + +""" +Signs a file with a specified key. If entering both the key and the filename +on the command line, the key must be entered first. + +Will produce both an ASCII armoured and GPG binary format copy of the signed +file. +""" + +if len(sys.argv) > 3: + logrus = sys.argv[1] + filename = " ".join(sys.argv[2:]) +elif len(sys.argv) == 3: + logrus = sys.argv[1] + filename = sys.argv[2] +elif len(sys.argv) == 2: + logrus = sys.argv[1] + filename = input("Enter the path and filename to sign: ") +else: + logrus = input("Enter the fingerprint or key ID to sign with: ") + filename = input("Enter the path and filename to sign: ") + +with open(filename, "rb") as f: + text = f.read() + +key = list(gpg.Context().keylist(pattern=logrus)) + +with gpg.Context(armor=True, signers=key) as ca: + signed_data, result = ca.sign(text, mode=gpg.constants.sig.mode.NORMAL) + with open("{0}.asc".format(filename), "wb") as fa: + fa.write(signed_data) + +with gpg.Context(signers=key) as cg: + signed_data, result = cg.sign(text, mode=gpg.constants.sig.mode.NORMAL) + with open("{0}.gpg".format(filename), "wb") as fg: + fg.write(signed_data) diff --git a/lang/python/examples/howto/verify-signatures.py b/lang/python/examples/howto/verify-signatures.py new file mode 100755 index 00000000..8aafc3ba --- /dev/null +++ b/lang/python/examples/howto/verify-signatures.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import gpg +import sys +import time + +""" +Verifies a signed file which has been signed with a detached signature. +""" + +if len(sys.argv) > 2: + filename = sys.argv[1] + sig_file = sys.argv[2] +elif len(sys.argv) == 2: + filename = sys.argv[1] + sig_file = input("Enter the path and filename of the detached signature: ") +else: + filename = input("Enter the path and filename to verify: ") + sig_file = input("Enter the path and filename of the detached signature: ") + +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 diff --git a/lang/python/examples/howto/verify-signed-file.py b/lang/python/examples/howto/verify-signed-file.py new file mode 100755 index 00000000..9f8702f5 --- /dev/null +++ b/lang/python/examples/howto/verify-signed-file.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public Licensefor more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# . + +import gpg +import sys +import time + +""" +Verifies a signed file which has been signed with either NORMAL or CLEAR modes. +""" + +if len(sys.argv) > 2: + filename = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + filename = sys.argv[1] +else: + filename = input("Enter the path and filename to sign: ") + +c = gpg.Context() + +try: + data, result = c.verify(open(filename)) + 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 diff --git a/lang/python/examples/encrypt-to-all.py b/lang/python/examples/low_level-encrypt_to_all.py similarity index 100% rename from lang/python/examples/encrypt-to-all.py rename to lang/python/examples/low_level-encrypt_to_all.py diff --git a/src/gpgme-tool.c b/src/gpgme-tool.c index 3e2dc785..e7a7a6f3 100644 --- a/src/gpgme-tool.c +++ b/src/gpgme-tool.c @@ -3101,7 +3101,7 @@ cmd_hash_algo_name (assuan_context_t ctx, char *line) static const char hlp_identify[] = - "IDENTIY\n" + "IDENTIFY\n" "\n" "Identify the type of data set with the INPUT command."; static gpg_error_t