Repository: vsajip/python-gnupg Branch: master Commit: f6f172164ebb Files: 29 Total size: 314.2 KB Directory structure: gitextract_4ty4owtm/ ├── .coveragerc ├── .flake8 ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── python-package.yml ├── .gitignore ├── .hgignore ├── .hgtags ├── .readthedocs.yaml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs/ │ ├── Makefile │ ├── _static/ │ │ └── sidebar.js │ ├── _templates/ │ │ └── page.html │ ├── conf.py │ ├── index.rst │ ├── requirements.txt │ └── spelling_wordlist.txt ├── gnupg.py ├── messages.json ├── package.json ├── pyproject.toml ├── release ├── setup.cfg ├── test_gnupg.py ├── test_pubring.gpg ├── test_secring.gpg └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] branch = True omit = /opt/python/* [report] exclude_lines = pragma: no cover raise NotImplementedError ================================================ FILE: .flake8 ================================================ [flake8] max-line-length=120 ignore = E731 W504 exclude = build .tox per-file-ignores = docs/conf.py:E265,E401,E402 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve this library. title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Environment** - OS, including version - Version of this library - Version of GnuPG **Additional information** Add any other information about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/python-package.yml ================================================ name: Tests on: push: branches: [ master ] paths-ignore: - 'LICENSE.*' - 'README.*' - '.github/ISSUE-TEMPLATE/**' - 'docs/**' - '.hgignore' - '.gitignore' pull_request: branches: [ master ] paths-ignore: - 'LICENSE.*' - 'README.*' - '.github/ISSUE-TEMPLATE/**' - 'docs/**' - '.hgignore' - '.gitignore' schedule: # at 03:07 on day-of-month 7 - cron: '7 3 7 * *' env: FORCE_COLOR: 1 PIP_DISABLE_PIP_VERSION_CHECK: 1 jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.13t', '3.14', '3.14t', 'pypy-3.9'] steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Windows-only dependencies run: | $env:PATH = "C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\ProgramData\chocolatey\bin" [Environment]::SetEnvironmentVariable("Path", $env:PATH, "Machine") choco install gnupg --version "2.4.8" echo "C:\Program Files (x86)\GnuPG\bin" >> $env:GITHUB_PATH if: ${{ matrix.os == 'windows-latest' }} - name: Test with unittest run: | gpg --version python test_gnupg.py -v env: NO_EXTERNAL_TESTS: 1 - name: Report failure info if: ${{ failure() }} run: | cat test_gnupg.log - name: Test with coverage run: | pip install coverage coverage run --branch test_gnupg.py coverage xml env: NO_EXTERNAL_TESTS: 1 - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: flags: unittests files: coverage.xml ================================================ FILE: .gitignore ================================================ build dist keys .tox .egg-info DETAILS MANIFEST random_binary_data __pycache__ *.pyc *.log .dict-validwords docs/themes/ docs/_build/ .idea/ .idea_modules/ *.iml *.iws *.ipr .vscode/ .settings/ .venv/ venv/ ================================================ FILE: .hgignore ================================================ (build|dist|keys|\.(tox|egg-info))/ (DETAILS|MANIFEST|random_binary_data|watcher.conf|hover.json)$ \.(pyc|log|dict-validwords|yapf)$ local.*\.(sh|cmd) docs/themes/ ================================================ FILE: .hgtags ================================================ afbe4e74cfc7fb79dde344499e8934c88874ef85 0.3.6 1979c07150aa6c7cb16b7f70db71f1b973153b93 0.3.7 7a54f558eb05e2b0ff25b51f430255d92312705c 0.3.8 1ab8db449e5b2b28b2d1f4c96677dd31050d296e 0.3.9 d18b8320539fc43cf406e829080d9fb72388f7ce 0.4.0 1fe9f4c3d9b3a7672a43beb8ee08ac3e8234710d 0.4.1 20a9a5727c11ea07188f99377c67d1bd937f4d7e 0.4.2 e0f2692d6539aca706b63dba22d900d2c70d59f8 0.4.3 c2dc6e154027ab8fc13eba8a440ea43568c37d8b 0.4.4 5cedc567072cead1415b24b7aa1ee74901c3f66f 0.4.4.1 79af87708a2338d1ec58b5ff32747a6dc32e5147 0.4.5 5eae1f2c1034f1eb3e5b29dece3a7006cd687733 0.4.6 2eae4d96f406b9f09b274b6a2457b0a87a1a894e 0.4.7 0a57c41eb34b01878fa6ac00a6f1afb2e1c4f6be 0.4.8 9e58092577ebf033bd8b95dc50905e518ff4a7ea 0.4.9 129c8fa7451a75c71f5e2fd54686896b266fefc8 0.5.0 1f1265fd99f1dda764dfa5ba3b4f34093203eaf5 0.5.1 f7d1effbb6e19cc233a139510c04f0f3aad83dd7 0.5.2 b0327e6c3ce295d2f5780af98e7aec0cf1862091 0.5.3 3efac76918850def676c20aa4bddb6e132adf1b6 0.5.4 1b77f5b12ad72096eb71fd94b2f767ffc565d3eb 0.5.5 2825bcb5914434854a11657563d829cfd9d06b67 0.5.6 ================================================ FILE: .readthedocs.yaml ================================================ # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # We recommend specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: docs/requirements.txt ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2008-2022 by Vinay Sajip. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name(s) of the copyright holder(s) may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE.txt include README.rst include test_gnupg.py include messages.json include test_*ring.gpg ================================================ FILE: README.rst ================================================ |badge1| |badge2| |badge3| .. |badge1| image:: https://img.shields.io/github/actions/workflow/status/vsajip/python-gnupg/python-package.yml :alt: GitHub test status .. |badge2| image:: https://img.shields.io/codecov/c/github/vsajip/python-gnupg :target: https://app.codecov.io/gh/vsajip/python-gnupg :alt: GitHub coverage status .. |badge3| image:: https://img.shields.io/pypi/v/python-gnupg :target: https://pypi.org/project/python-gnupg/ :alt: PyPI package What is it? =========== The GNU Privacy Guard (gpg, or gpg.exe on Windows) is a command-line program which provides support for programmatic access via spawning a separate process to run it and then communicating with that process from your program. This project, ``python-gnupg``, implements a Python library which takes care of the internal details and allows its users to generate and manage keys, encrypt and decrypt data, and sign and verify messages. Installation ============ Installing from PyPI -------------------- You can install this package from the Python Package Index (pyPI) by running:: pip install python-gnupg .. important:: There is at least one fork of this project, which was apparently created because an earlier version of this software used the ``subprocess`` module with ``shell=True``, making it vulnerable to shell injection. **This is no longer the case**. Forks may not be drop-in compatible with this software, so take care to use the correct version, as indicated in the ``pip install`` command above. Installing from a source distribution archive --------------------------------------------- To install this package from a source distribution archive, do the following: 1. Extract all the files in the distribution archive to some directory on your system. 2. In that directory, run ``pip install .``, referencing a suitable ``pip`` (e.g. one from a specific venv which you want to install to). 3. Optionally, run ``python test_gnupg.py`` to ensure that the package is working as expected. Credits ======= * The developers of the GNU Privacy Guard. * The original version of this module was developed by Andrew Kuchling. * It was improved by Richard Jones. * It was further improved by Steve Traugott. The present incarnation, based on the earlier versions, uses the ``subprocess`` module and so works on Windows as well as Unix/Linux platforms. It's not, however, 100% backwards-compatible with earlier incarnations. Change log ========== .. note:: GCnn refers to an issue nn on Google Code. 0.5.7 (future) -------------- Released: Not yet 0.5.6 ----- Released: 2025-12-31 * Fix #261: Ensure capability, fingerprint and keygrip are added to subkey_info. * Set username in the result when Verify uses a signing key that has expired or been revoked. Thanks to Steven Galgano for the patch. 0.5.5 ----- Released: 2025-08-04 * Fix #249: Handle fetching GPG version when not the first item in the configuration. * Fix #250: Capture uid info in a uid_map attribute of ScanKeys/ListKeys. * Fix #255: Improve handling of exceptions raised in background threads. 0.5.4 ----- Released: 2025-01-07 * Fix #242: Handle exceptions in ``on_data`` callable. 0.5.3 ----- Released: 2024-09-20 * Fix #117: Add WKD (Web Key Directory) support for auto-locating keys. Thanks to Myzel394 for the patch. * Fix #237: Ensure local variable is initialized even when an exception occurs. * Fix #239: Remove logging of decryption result. 0.5.2 ----- Released: 2023-12-12 * Fix #228: Clarify documentation for encryption/decryption. * Make I/O buffer size configurable via ``buffer_size`` attribute on a ``GPG`` instance. 0.5.1 ----- Released: 2023-07-22 * Added ``TRUST_EXPIRED`` to ``trust_keys``. Thanks to Leif Liddy for the patch. * Fix #206: Remove deprecated ``--always-trust`` in favour of ``--trust-model always`` * Fix #208: Add ``status_detail`` attribute to result objects which is populated when the status is ``'invalid recipient'`` (encryption/decryption) or ``'invalid signer'`` (signing). This attribute will be set when the result object's ``status`` attribute is set to ``invalid recipient`` and will contain more information about the failure in the form of ``reason:ident`` where ``reason`` is a text description of the reason, and ``ident`` identifies the recipient key. * Add ``scan_keys_mem()`` function to scan keys in a string. Thanks to Sky Moore for the patch. * Fix #214: Handle multiple signatures when one of them is invalid or unverified. * A ``problems`` attribute was added which holds problems reported by ``gpg`` during verification. This is a list of dictionaries, one for each reported problem. Each dictionary will have ``status`` and ``keyid`` keys indicating the problem and the corresponding key; other information in the dictionaries will be error specific. * Fix #217: Use machine-readable interface to query the ``gpg`` version. Thanks to Justus Winter for the patch. * Added the ability to export keys to a file. Thanks to Leif Liddy for the patch. 0.5.0 ----- Released: 2022-08-23 * Fixed #181: Added the ability to pass file paths to encrypt_file, decrypt_file, sign_file, verify_file, get_recipients_file and added import_keys_file. * Fixed #183: Handle FAILURE and UNEXPECTED conditions correctly. Thanks to sebbASF for the patch. * Fixed #185: Handle VALIDSIG arguments more robustly. * Fixed #188: Remove handling of DECRYPTION_FAILED from Verify code, as not required there. Thanks to sebbASF for the patch. * Fixed #190: Handle KEY_CREATED more robustly. * Fixed #191: Handle NODATA messages during verification. * Fixed #196: Don't log chunk data by default, as it could contain sensitive information (during decryption, for example). * Added the ability to pass an environment to the gpg executable. Thanks to Edvard Rejthar for the patch. 0.4.9 ----- Released: 2022-05-20 * Fixed #161: Added a status attribute to the returned object from gen_key() which is set to 'ok' if a key was successfully created, or 'key not created' if that was reported by gpg, or None in any other case. * Fixed #164: Provided the ability to add subkeys. Thanks to Daniel Kilimnik for the feature request and patch. * Fixed #166: Added keygrip values to the information collected when keys are listed. Thanks to Daniel Kilimnik for the feature request and patch. * Fixed #173: Added extra_args to send_keys(), recv_keys() and search_keys() to allow passing options relating to key servers. 0.4.8 ----- Released: 2021-11-24 * Fixed #147: Return gpg's return code in all result instances. * Fixed #152: Add check for invalid file objects. * Fixed #157: Provide more useful status message when a secret key is absent. * Fixed #158: Added a get_recipients() API to find the recipients of an encrypted message without decrypting it. 0.4.7 ----- Released: 2021-03-11 * Fixed #129, #141: Added support for no passphrase during key generation. * Fixed #143: Improved permission-denied test. Thanks to Elliot Cameron for the patch. * Fixed #144: Updated logging to only show partial results. * Fixed #146: Allowed a passphrase to be passed to import_keys(). Thanks to Chris de Graaf for the patch. 0.4.6 ----- Released: 2020-04-17 * Fixed #122: Updated documentation about gnupghome needing to be an existing directory. * Fixed #123: Handled error conditions from gpg when calling trust_keys(). * Fixed #124: Avoided an exception being raised when ImportResult.summary() was called after a failed recv_keys(). * Fixed #128: Added ECC support by changing key generation parameters. (The Key-Length value isn't added if a curve is specified.) * Fixed #130: Provided a mechanism to provide more complete error messages. Support for Python versions 3.5 and under is discontinued, except for Python 2.7. 0.4.5 ----- Released: 2019-08-12 * Fixed #107: Improved documentation. * Fixed #112: Raised a ValueError if a gnupghome is specified which is not an existing directory. * Fixed #113: Corrected stale link in the documentation. * Fixed #116: Updated documentation to clarify when spurious key-expired/ signature-expired messages might be seen. * Fixed #119: Added --yes to avoid pinentry when deleting secret keys with GnuPG >= 2.1. * A warning is logged if gpg returns a non-zero return code. * Added ``extra_args`` to ``import_keys``. * Added support for CI using AppVeyor. 0.4.4 ----- Released: 2019-01-24 * Fixed #108: Changed how any return value from the ``on_data`` callable is processed. In earlier versions, the return value was ignored. In this version, if the return value is ``False``, the data received from ``gpg`` is not buffered. Otherwise (if the value is ``None`` or ``True``, for example), the data is buffered as normal. This functionality can be used to do your own buffering, or to prevent buffering altogether. The ``on_data`` callable is also called once with an empty byte-string to signal the end of data from ``gpg``. * Fixed #97: Added an additional attribute ``check_fingerprint_collisions`` to ``GPG`` instances, which defaults to ``False``. It seems that ``gpg`` is happy to have duplicate keys and fingerprints in a keyring, so we can't be too strict. A user can set this attribute of an instance to ``True`` to trigger a check for collisions. * Fixed #111: With GnuPG 2.2.7 or later, provide the fingerprint of a signing key for a failed signature verification, if available. * Fixed #21: For verification where multiple signatures are involved, a mapping of signature_ids to fingerprint, keyid, username, creation date, creation timestamp and expiry timestamp is provided. * Added a check to disallow certain control characters ('\r', '\n', NUL) in passphrases. 0.4.3 ----- Released: 2018-06-13 * Added --no-verbose to the gpg command line, in case verbose is specified in gpg.conf - we don't need verbose output. 0.4.2 ----- Released: 2018-03-28 * Fixed #81: Subkey information is now collected and returned in a ``subkey_info`` dictionary keyed by the subkey's ID. * Fixed #84: GPG2 version is now correctly detected on OS X. * Fixed #94: Added ``expect_passphrase`` password for use on GnuPG >= 2.1 when passing passphrase to ``gpg`` via pinentry. * Fixed #95: Provided a ``trust_keys`` method to allow setting the trust level for keys. Thanks to William Foster for a suggested implementation. * Made the exception message when the gpg executable is not found contain the path of the executable that was tried. Thanks to Kostis Anagnostopoulos for the suggestion. * Fixed #100: Made the error message less categorical in the case of a failure with an unspecified reason, adding some information from gpg error codes when available. 0.4.1 ----- Released: 2017-07-06 * Updated message handling logic to no longer raise exceptions when a message isn't recognised. Thanks to Daniel Kahn Gillmor for the patch. * Always use always use ``--fixed-list-mode``, ``--batch`` and ``--with-colons``. Thanks to Daniel Kahn Gillmor for the patch. * Improved ``scan_keys()`` handling on GnuPG >= 2.1. Thanks to Daniel Kahn Gillmor for the patch. * Improved test behaviour with GnuPG >= 2.1. Failures when deleting test directory trees are now ignored. Thanks to Daniel Kahn Gillmor for the patch. * Added ``close_file`` keyword argument to verify_file to allow the file closing to be made optional. Current behaviour is maintained - ``close_file=False`` can be passed to skip closing the file being verified. * Added the ``extra_args`` keyword parameter to allow custom arguments to be passed to the ``gpg`` executable. * Instances of the ``GPG`` class now have an additional ``on_data`` attribute, which defaults to ``None``. It can be set to a callable which will be called with a single argument - a binary chunk of data received from the ``gpg`` executable. The callable can do whatever it likes with the chunks passed to it - e.g. write them to a separate stream. The callable should not raise any exceptions (unless it wants the current operation to fail). 0.4.0 ----- Released: 2017-01-29 * Added support for ``KEY_CONSIDERED`` in more places - encryption / decryption, signing, key generation and key import. * Partial fix for #32 (GPG 2.1 compatibility). Unfortunately, better support cannot be provided at this point, unless there are certain changes (relating to pinentry popups) in how GPG 2.1 works. * Fixed #60: An IndexError was being thrown by ``scan_keys()``. * Ensured that utf-8 encoding is used when the ``--with-column`` mode is used. Thanks to Yann Leboulanger for the patch. * ``list_keys()`` now uses ``--fixed-list-mode``. Thanks to Werner Koch for the pointer. 0.3.9 ----- Released: 2016-09-10 * Fixed #38: You can now request information about signatures against keys. Thanks to SunDwarf for the suggestion and patch, which was used as a basis for this change. * Fixed #49: When exporting keys, no attempt is made to decode the output when armor=False is specified. * Fixed #53: A ``FAILURE`` message caused by passing an incorrect passphrase is handled. * Handled ``EXPORTED`` and ``EXPORT_RES`` messages while exporting keys. Thanks to Marcel Pörner for the patch. * Fixed #54: Improved error message shown when gpg is not available. * Fixed #55: Added support for ``KEY_CONSIDERED`` while verifying. * Avoided encoding problems with filenames under Windows. Thanks to Kévin Bernard-Allies for the patch. * Fixed #57: Used a better mechanism for comparing keys. 0.3.8 ----- Released: 2015-09-24 * Fixed #22: handled ``PROGRESS`` messages during verification and signing. * Fixed #26: handled ``PINENTRY_LAUNCHED`` messages during verification, decryption and key generation. * Fixed #28: Allowed a default Name-Email to be computed even when neither of ``LOGNAME`` and ``USERNAME`` are in the environment. * Fixed #29: Included test files missing from the tarball in previous versions. * Fixed #39: On Python 3.x, passing a text instead of a binary stream caused file decryption to hang due to a ``UnicodeDecodeError``. This has now been correctly handled: The decryption fails with a "no data" status. * Fixed #41: Handled Unicode filenames correctly by encoding them on 2.x using the file system encoding. * Fixed #43: handled ``PINENTRY_LAUNCHED`` messages during key export. Thanks to Ian Denhardt for looking into this. * Hide the console window which appears on Windows when gpg is spawned. Thanks to Kévin Bernard-Allies for the patch. * Subkey fingerprints are now captured. * The returned value from the ``list_keys`` method now has a new attribute, ``key_map``, which is a dictionary mapping key and subkey fingerprints to the corresponding key's dictionary. With this change, you don't need to iterate over the (potentially large) returned list to search for a key with a given fingerprint - the ``key_map`` dict will take you straight to the key info, whether the fingerprint you have is for a key or a subkey. Thanks to Nick Daly for the initial suggestion. 0.3.7 ----- Released: 2014-12-07 Signed with PGP key: Vinay Sajip (CODE SIGNING KEY) Key Fingerprint : CA74 9061 914E AC13 8E66 EADB 9147 B477 339A 9B86 * Added an ``output`` keyword parameter to the ``sign`` and ``sign_file`` methods, to allow writing the signature to a file. Thanks to Jannis Leidel for the patch. * Allowed specifying ``True`` for the ``sign`` keyword parameter, which allows use of the default key for signing and avoids having to specify a key id when it's desired to use the default. Thanks to Fabian Beutel for the patch. * Used a uniform approach with subprocess on Windows and POSIX: shell=True is not used on either. * When signing/verifying, the status is updated to reflect any expired or revoked keys or signatures. * Handled 'NOTATION_NAME' and 'NOTATION_DATA' during verification. * Fixed #1, #16, #18, #20: Quoting approach changed, since now shell=False. * Fixed #14: Handled 'NEED_PASSPHRASE_PIN' message. * Fixed #8: Added a scan_keys method to allow scanning of keys without the need to import into a keyring. Thanks to Venzen Khaosan for the suggestion. * Fixed #5: Added '0x' prefix when searching for keys. Thanks to Aaron Toponce for the report. * Fixed #4: Handled 'PROGRESS' message during encryption. Thanks to Daniel Mills for the report. * Fixed #3: Changed default encoding to Latin-1. * Fixed #2: Raised ValueError if no recipients were specified for an asymmetric encryption request. * Handled 'UNEXPECTED' message during verification. Thanks to David Andersen for the patch. * Replaced old range(len(X)) idiom with enumerate(). * Refactored ``ListKeys`` / ``SearchKeys`` classes to maximise use of common functions. * Fixed GC94: Added ``export-minimal`` and ``armor`` options when exporting keys. This addition was inadvertently left out of 0.3.6. 0.3.6 ----- Released: 2014-02-05 * Fixed GC82: Enabled fast random tests on gpg as well as gpg2. * Fixed GC85: Avoided deleting temporary file to preserve its permissions. * Fixed GC87: Avoided writing passphrase to log. * Fixed GC95: Added ``verify_data()`` method to allow verification of signatures in memory. * Fixed GC96: Regularised end-of-line characters. * Fixed GC98: Rectified problems with earlier fix for shell injection. 0.3.5 ----- Released: 2013-08-30 * Added improved shell quoting to guard against shell injection. * Fixed GC76: Added ``search_keys()`` and ``send_keys()`` methods. * Fixed GC77: Allowed specifying a symmetric cipher algorithm. * Fixed GC78: Fell back to utf-8 encoding when no other could be determined. * Fixed GC79: Default key length is now 2048 bits. * Fixed GC80: Removed the Name-Comment default in key generation. 0.3.4 ----- Released: 2013-06-05 * Fixed GC65: Fixed encoding exception when getting version. * Fixed GC66: Now accepts sets and frozensets where appropriate. * Fixed GC67: Hash algorithm now captured in sign result. * Fixed GC68: Added support for ``--secret-keyring``. * Fixed GC70: Added support for multiple keyrings. 0.3.3 ----- Released: 2013-03-11 * Fixed GC57: Handled control characters in ``list_keys()``. * Fixed GC61: Enabled fast random for testing. * Fixed GC62: Handled ``KEYEXPIRED`` status. * Fixed GC63: Handled ``NO_SGNR`` status. 0.3.2 ----- Released: 2013-01-17 * Fixed GC56: Disallowed blank values in key generation. * Fixed GC57: Handled colons and other characters in ``list_keys()``. * Fixed GC59/GC60: Handled ``INV_SGNR`` status during verification and removed calls requiring interactive password input from doctests. 0.3.1 ----- Released: 2012-09-01 * Fixed GC45: Allowed additional arguments to gpg executable. * Fixed GC50: Used latin-1 encoding in tests when it's known to be required. * Fixed GC51: Test now returns non-zero exit status on test failure. * Fixed GC53: Now handles ``INV_SGNR`` and ``KEY_NOT_CREATED`` statuses. * Fixed GC55: Verification and decryption now return trust level of signer in integer and text form. 0.3.0 ----- Released: 2012-05-12 * Fixed GC49: Reinstated Yann Leboulanger's change to support subkeys (accidentally left out in 0.2.7). 0.2.9 ----- Released: 2012-03-29 * Fixed GC36: Now handles ``CARDCTRL`` and ``POLICY_URL`` messages. * Fixed GC40: Now handles ``DECRYPTION_INFO``, ``DECRYPTION_FAILED`` and ``DECRYPTION_OKAY`` messages. * The ``random_binary_data file`` is no longer shipped, but constructed by the test suite if needed. 0.2.8 ----- Released: 2011-09-02 * Fixed GC29: Now handles ``IMPORT_RES`` while verifying. * Fixed GC30: Fixed an encoding problem. * Fixed GC33: Quoted arguments for added safety. 0.2.7 ----- Released: 2011-04-10 * Fixed GC24: License is clarified as BSD. * Fixed GC25: Incorporated Daniel Folkinshteyn's changes. * Fixed GC26: Incorporated Yann Leboulanger's subkey change. * Fixed GC27: Incorporated hysterix's support for symmetric encryption. * Did some internal cleanups of Unicode handling. 0.2.6 ----- Released: 2011-01-25 * Fixed GC14: Should be able to accept passphrases from GPG-Agent. * Fixed GC19: Should be able to create a detached signature. * Fixed GC21/GC23: Better handling of less common responses from GPG. 0.2.5 ----- Released: 2010-10-13 * Fixed GC11/GC16: Detached signatures can now be created. * Fixed GC3: Detached signatures can be verified. * Fixed GC12: Better support for RSA and IDEA. * Fixed GC15/GC17: Better support for non-ASCII input. 0.2.4 ----- Released: 2010-03-01 * Fixed GC9: Now allows encryption without armor and the ability to encrypt and decrypt directly to/from files. 0.2.3 ----- Released: 2010-01-07 * Fixed GC7: Made sending data to process threaded and added a test case. With a test data file used by the test case, the archive size has gone up to 5MB (the size of the test file). 0.2.2 ----- Released: 2009-10-06 * Fixed GC5/GC6: Added ``--batch`` when specifying ``--passphrase-fd`` and changed the name of the distribution file to add the ``python-`` prefix. 0.2.1 ----- Released: 2009-08-07 * Fixed GC2: Added ``handle_status()`` method to the ``ListKeys`` class. 0.2.0 ----- Released: 2009-07-16 * Various changes made to support Python 3.0. 0.1.0 ----- Released: 2009-07-04 * Initial release. ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest remote apidocs help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." apidocs: docfrag --libs .. gnupg -f hovertip > hover.json remote: rsync -avz $(BUILDDIR)/html/* vopal:~/apps/rdc_docs/python-gnupg spelling: $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR) dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Distlib.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Distlib.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Distlib" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Distlib" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ================================================ FILE: docs/_static/sidebar.js ================================================ /* * sidebar.js * ~~~~~~~~~~ * * This script makes the Sphinx sidebar collapsible. * * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds in * .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton used to * collapse and expand the sidebar. * * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden and the * width of the sidebar and the margin-left of the document are decreased. * When the sidebar is expanded the opposite happens. This script saves a * per-browser/per-session cookie used to remember the position of the sidebar * among the pages. Once the browser is closed the cookie is deleted and the * position reset to the default (expanded). * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ $(function() { // global elements used by the functions. // the 'sidebarbutton' element is defined as global after its // creation, in the add_sidebar_button function var bodywrapper = $('.bodywrapper'); var sidebar = $('.sphinxsidebar'); var sidebarwrapper = $('.sphinxsidebarwrapper'); // original margin-left of the bodywrapper and width of the sidebar // with the sidebar expanded var bw_margin_expanded = bodywrapper.css('margin-left'); var ssb_width_expanded = sidebar.width(); // margin-left of the bodywrapper and width of the sidebar // with the sidebar collapsed var bw_margin_collapsed = '.8em'; var ssb_width_collapsed = '.8em'; // colors used by the current theme var dark_color = '#AAAAAA'; var light_color = '#CCCCCC'; function sidebar_is_collapsed() { return sidebarwrapper.is(':not(:visible)'); } function toggle_sidebar() { if (sidebar_is_collapsed()) expand_sidebar(); else collapse_sidebar(); } function collapse_sidebar() { sidebarwrapper.hide(); sidebar.css('width', ssb_width_collapsed); bodywrapper.css('margin-left', bw_margin_collapsed); sidebarbutton.css({ 'margin-left': '0', 'height': bodywrapper.height(), 'border-radius': '5px' }); sidebarbutton.find('span').text('»'); sidebarbutton.attr('title', _('Expand sidebar')); document.cookie = 'sidebar=collapsed'; } function expand_sidebar() { bodywrapper.css('margin-left', bw_margin_expanded); sidebar.css('width', ssb_width_expanded); sidebarwrapper.show(); sidebarbutton.css({ 'margin-left': ssb_width_expanded-12, 'height': bodywrapper.height(), 'border-radius': '0 5px 5px 0' }); sidebarbutton.find('span').text('«'); sidebarbutton.attr('title', _('Collapse sidebar')); //sidebarwrapper.css({'padding-top': // Math.max(window.pageYOffset - sidebarwrapper.offset().top, 10)}); document.cookie = 'sidebar=expanded'; } function add_sidebar_button() { sidebarwrapper.css({ 'float': 'left', 'margin-right': '0', 'width': ssb_width_expanded - 28 }); // create the button sidebar.append( '
«
' ); var sidebarbutton = $('#sidebarbutton'); // find the height of the viewport to center the '<<' in the page var viewport_height; if (window.innerHeight) viewport_height = window.innerHeight; else viewport_height = $(window).height(); var sidebar_offset = sidebar.offset().top; var sidebar_height = Math.max(bodywrapper.height(), sidebar.height()); sidebarbutton.find('span').css({ 'display': 'block', 'position': 'fixed', 'top': Math.min(viewport_height/2, sidebar_height/2 + sidebar_offset) - 10 }); sidebarbutton.click(toggle_sidebar); sidebarbutton.attr('title', _('Collapse sidebar')); sidebarbutton.css({ 'border-radius': '0 5px 5px 0', 'color': '#444444', 'background-color': '#CCCCCC', 'font-size': '1.2em', 'cursor': 'pointer', 'height': sidebar_height, 'padding-top': '1px', 'padding-left': '1px', 'margin-left': ssb_width_expanded - 12 }); sidebarbutton.hover( function () { $(this).css('background-color', dark_color); }, function () { $(this).css('background-color', light_color); } ); } function set_position_from_cookie() { if (!document.cookie) return; var items = document.cookie.split(';'); for(var k=0; k Comments powered by Disqus {% endblock %} {% block footer %} {{ super() }} {% endblock %} ================================================ FILE: docs/conf.py ================================================ # -*- coding: utf-8 -*- # # GnuPG Wrapper for Python documentation build configuration file, created by # sphinx-quickstart on Thu Jul 24 04:03:38 2008. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import datetime, os, sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', #'sphinx.ext.napoleon', #'sphinx.ext.imgmath', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinxcontrib.spelling' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Python Wrapper for GnuPG' copyright = u'2008-%s, Vinay Sajip' % datetime.date.today().year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from gnupg import __version__ as release # , __date__ as today version = '.'.join(release.split('.')[:2]) if '.dev' in release: today = datetime.date.today().strftime('%b %d, %Y') else: today = today.split()[0][1:].split('-') today = '%s %s, %s' % (today[1], today[0], today[2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] spelling_lang = 'en_GB' spelling_word_list_filename = 'spelling_wordlist.txt' # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = os.environ.get('DOCS_THEME', 'default') THEME_OPTIONS = {'sizzle': {}} if html_theme == 'sizzle' and os.path.isfile('hover.json'): import json with open('hover.json', encoding='utf-8') as f: THEME_OPTIONS['sizzle']['custom_data'] = {'hovers': json.load(f)} # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. if html_theme in THEME_OPTIONS: html_theme_options = THEME_OPTIONS[html_theme] #html_theme_options = {'collapsiblesidebar': True} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = { # '**': [ # 'localtoc.html', 'globaltoc.html', 'relations.html', # 'sourcelink.html', 'searchbox.html' # ], # } # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'GnuPGWrapperforPythondoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'GnuPGWrapperforPython.tex', u'GnuPG Wrapper for Python Documentation', u'Vinay Sajip', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [('index', 'python-gnupg', u'python-gnupg Documentation', [u'Vinay Sajip'], 1)] # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. epub_title = u'python-gnupg' epub_author = u'Vinay Sajip' epub_publisher = u'Vinay Sajip' epub_copyright = u'2019, Vinay Sajip' # The language of the text. It defaults to the language option # or en if the language is not set. #epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. #epub_identifier = '' # A unique identification for the text. #epub_uid = '' # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_post_files = [] # A list of files that should not be packed into the epub file. #epub_exclude_files = [] # The depth of the table of contents in toc.ncx. #epub_tocdepth = 3 # Allow duplicate toc entries. #epub_tocdup = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'python': ('http://docs.python.org/', None)} def skip_module_docstring(app, what, name, obj, options, lines): if (what, name) == ('module', 'distlib'): del lines[:] def setup(app): app.connect('autodoc-process-docstring', skip_module_docstring) ================================================ FILE: docs/index.rst ================================================ .. GnuPG Wrapper for Python documentation master file, created by sphinx-quickstart on Thu Jul 02 16:14:12 2009. ########################################### `python-gnupg` - A Python wrapper for GnuPG ########################################### .. rst-class:: release-info .. list-table:: :widths: auto :stub-columns: 1 * - Release: - |release| * - Date: - |today| .. module:: gnupg :synopsis: A Python wrapper for the GNU Privacy Guard (GnuPG) .. moduleauthor:: Vinay Sajip .. sectionauthor:: Vinay Sajip .. toctree:: :maxdepth: 4 The ``gnupg`` module allows Python programs to make use of the functionality provided by the `GNU Privacy Guard `_ (abbreviated GPG or GnuPG). Using this module, Python programs can encrypt and decrypt data, digitally sign documents and verify digital signatures, manage (generate, list and delete) encryption keys, using Public Key Infrastructure (PKI) encryption technology based on OpenPGP. This module is expected to be used with Python versions >= 3.6, or Python 2.7 for legacy code. Install this module using ``pip install python-gnupg``. You can then use this module in your own code by doing ``import gnupg`` or similar. .. _fork-note: .. note:: There is at least one fork of this project, which was apparently created because an earlier version of this software used the ``subprocess`` module with ``shell=True``, making it vulnerable to shell injection. **This is no longer the case**. Forks may not be drop-in compatible with this software, so take care to use the correct version, as indicated in the ``pip install`` command above. .. index:: Deployment .. _deployment: Deployment Requirements ======================= Apart from a recent-enough version of Python, in order to use this module you need to have access to a compatible version of the GnuPG executable. The system has been tested with GnuPG v1.4.9 on Windows and Ubuntu. On a Linux platform, this will typically be installed via your distribution's package manager (e.g. ``apt-get`` on Debian/Ubuntu). Windows binaries are available `here `_ -- use one of the ``gnupg-w32cli-1.4.x.exe`` installers for the simplest deployment options. .. note:: On Windows, it is *not* necessary to perform a full installation of GnuPG, using the standard installer, on each computer: it is normally sufficient to distribute only the executable, ``gpg.exe``, and a DLL which it depends on, ``iconv.dll``. These files do not need to be placed in system directories, nor are registry changes needed. The files need to be placed in a location such that implicit invocation will find them - such as the working directory of the application which uses the ``gnupg`` module, or on the system path if that is appropriate for your requirements. Alternatively, you can specify the full path to the ``gpg`` executable. *Note, however, that if you want to use GnuPG 2.0, then this simple deployment approach may not work, because there are more dependent files which you have to ship. For this reason, our recommendation is to stick with GnuPG 1.4.x on Windows, unless you specifically need 2.0 features - in which case, you may have to do a full installation rather than just relying on a couple of files).* Recent versions of GnuPG (>= 2.1.x) introduce a number of changes: * By default, passphrases cannot be passed via streams to ``gpg`` unless the line ``allow-loopback-pinentry`` is added to ``gpg-agent.conf`` in the home directory used by ``gpg`` (this is also where the keyring files are kept). If that file does not exist, you will need to create it with that single line. Note that even with this configuration, some versions of GnuPG 2.1.x won't work as expected. In our testing, we found, for example, that the 2.1.11 executable shipped with Ubuntu 16.04 didn't behave helpfully, whereas a GnuPG 2.1.15 executable compiled from source on the same machine worked as expected. * To export secret keys, a passphrase must be provided. .. index:: Acknowledgements Acknowledgements ================ This module is based on an earlier version, ``GPG.py``, written by Andrew Kuchling. This was further improved by Richard Jones, and then even further by Steve Traugott. The ``gnupg`` module is derived from `Steve Traugott's module `_ (the original site no longer exists - this link is to the Wayback Machine), and uses Python's ``subprocess`` module to communicate with the GnuPG executable, which it uses to spawn a subprocess to do the real work. I've gratefully incorporated improvements contributed or suggested by: * Paul Cunnane (detached signature support) * Daniel Folkinshteyn (``recv_keys``, handling of subkeys and SIGEXPIRED, KEYEXPIRED while verifying, EXPKEYSIG, REVKEYSIG) * Dmitry Gladkov (handle KEYEXPIRED when importing) * Abdul Karim (keyring patch) * Yann Leboulanger (handle ERRSIG and NO_PUBKEY while verifying, get subkeys) * Kirill Yakovenko (RSA and IDEA support) * Robert Leftwich (handle INV_SGNR, KEY_NOT_CREATED) * Michal Niklas (Trust levels for signature verification) * David Noël (``search_keys``, ``send_keys`` functionality) * David Andersen (handle UNEXPECTED during verification) * Jannis Leidel (output signature to a file) * Venzen Khaosan (scan_keys functionality) * Marcel Pörner (handle EXPORTED, EXPORT_RES) * Kévin Bernard-Allies (handle filename encoding under Windows) * Daniel Kahn Gillmor (various improvements which were released in 0.4.1) * William Foster (trust_key patch) and Google Code / BitBucket users * dprovins (ListKeys handle_status) * ernest0x (improved support for non-ASCII input) * eyepulp (additional options for encryption/decryption) * hysterix.is.slackin (symmetric encryption support) * natureshadow (improved status handling when smart cards in use) * SunDwarf (storing signatures against keys) (If I've missed anyone from this list, please let me know.) Before you Start ================ GnuPG works on the basis of a "home directory" which is used to store public and private keyring files as well as a trust database. You need to identify in advance which directory on the end-user system will be used as the home directory, as you will need to pass this information to ``gnupg``. .. index:: Getting started Getting Started =============== You interface to the GnuPG functionality through an instance of the ``GPG`` class:: >>> gpg = gnupg.GPG(gnupghome='/path/to/home/directory') If the home directory does not exist, a ValueError will be raised. Thereafter, all the operations available are accessed via methods of this instance. If the ``gnupghome`` parameter is omitted, GnuPG will use whatever directory is the default (consult the GnuPG documentation for more information on what this might be). The :class:`GPG` constructor also accepts the following additional optional keyword arguments: gpgbinary (defaults to "gpg") The path to the ``gpg`` executable. verbose (defaults to ``False``) Print information (e.g. the gpg command lines, and status messages returned by gpg) to the console. You don't generally need to set this option, since the module uses Python's ``logging`` package to provide more flexible functionality. The status messages from ``gpg`` are quite voluminous, especially during key generation. use_agent (defaults to ``False``) If specified as True, the ``--use-agent`` parameter is passed to ``gpg``, asking it to use any in-memory GPG agent (which remembers your credentials). keyring (defaults to ``None``) If specified, the value is used as the name of the keyring file. The default keyring is not used. A list of paths to keyring files can also be specified. options (defaults to ``None``) If specified, the value should be a list of additional command-line options to pass to ``gpg``. secret_keyring (defaults to ``None``) If specified, the value is used as the name of the secret keyring file. A list of paths to secret keyring files can also be specified. *Note that these files are not used by GnuPG >= 2.1.* env (defaults to ``None``) If specified, the value is used as the environment variables used when calling the GPG executable. .. versionchanged:: 0.3.4 The ``keyring`` argument can now also be a list of keyring filenames. .. versionadded:: 0.3.4 The ``secret_keyring`` argument was added. *Note that this argument is not used when working with GnuPG >= 2.1.* .. note:: If you specify values in ``options``, make sure you don't specify values which will conflict with other values added by ``python-gnupg``. You should be familiar with GPG command-line arguments and how they affect GPG's operation. .. versionchanged:: 0.3.7 The default encoding was changed to ``latin-1``. In earlier versions, it was either ``locale.getpreferredencoding()`` or, failing that, ``sys.stdin.encoding``, and failing that, ``utf-8``. .. versionadded:: 0.5.0 The ``env`` argument was added. If the ``gpgbinary`` executable cannot be found, a ``ValueError`` is raised in :meth:`GPG.__init__`. The low-level communication between the ``gpg`` executable and ``python-gnupg`` is in terms of bytes, and ``python-gnupg`` tries to convert gpg's ``stderr`` stream to text using an encoding. The default value of this is ``latin-1``, but you can override this by setting the encoding name in the GPG instance's ``encoding`` attribute after instantiation, like this:: >>> gpg = gnupg.GPG(gnupghome='/path/to/home/directory') >>> gpg.encoding = 'utf-8' .. note:: If you use the wrong encoding, you may get exceptions. The ``'latin-1'`` encoding leaves bytes as-is and shouldn't fail with encoding/decoding errors, though it may not decode text correctly (so you may see odd characters in the decoding output). The ``gpg`` executable will use an output encoding based on your environment settings (e.g. environment variables, code page etc.) but defaults to latin-1. From version 0.5.2 onwards, you can also control the buffer size for the I/O between ``gpg`` and ``python-gnupg`` by setting the ``buffer_size`` attribute on a GPG instance. It defaults to 16K. .. versionadded:: 0.5.2 The ``buffer_size`` attribute was added. .. index:: Key; management Key Management ============== The module provides functionality for generating (creating) keys, listing keys, deleting keys, and importing and exporting keys. .. index:: Key; generating Generating keys --------------- The first thing you typically want to do when starting with a PKI framework is to generate some keys. You can do this using the :meth:`~gnupg.GPG.gen_key` method:: >>> key = gpg.gen_key(input_data) where ``input_data`` is a special command string which tells GnuPG the parameters you want to use when creating the key. To make life easier, a helper method :meth:`~gnupg.GPG.gen_key_input` is provided which takes keyword arguments which allow you to specify individual parameters of the key, as in the following example:: >>> input_data = gpg.gen_key_input(key_type="RSA", key_length=1024) Sensible defaults are provided for parameters which you don't specify, as shown in the following table. .. cssclass:: generic-table table-bordered table-responsive-sm table-striped mx-auto mb-3 colwidths-auto smaller hpad +---------------+------------------+-------------------------+-----------------------------+---------------------------------------------+ | Parameter | Keyword Argument | Default value | Example values | Meaning of parameter | +===============+==================+=========================+=============================+=============================================+ | Key-Type | key_type | "RSA" | "RSA", "DSA" | The type of the primary key to generate. It | | | | | | must be capable of signing. | +---------------+------------------+-------------------------+-----------------------------+---------------------------------------------+ | Key-Length | key_length | 1024 | 1024, 2048 | The length of the primary key in bits. | +---------------+------------------+-------------------------+-----------------------------+---------------------------------------------+ | Name-Real | name_real | "Autogenerated Key" | "Fred Bloggs" | The real name of the user identity which | | | | | | is represented by the key. | +---------------+------------------+-------------------------+-----------------------------+---------------------------------------------+ | Name-Comment | name_comment | "Generated by gnupg.py" | "A test user" | A comment to attach to the user id. | +---------------+------------------+-------------------------+-----------------------------+---------------------------------------------+ | Name-Email | name_email | @ | "fred.bloggs\@domain.com" | An email address for the user. | +---------------+------------------+-------------------------+-----------------------------+---------------------------------------------+ If you don't specify any parameters, the values in the table above will be used with the defaults indicated. There is a whole set of other parameters you can specify; see `this GnuPG document `_ for more details. While use of RSA keys is common (they can be used for both signing and encryption), another popular option is to use a DSA primary key (for signing) together with a secondary El-Gamal key (for encryption). For this latter option, you could supply the following additional parameters: .. cssclass:: generic-table table-bordered table-responsive-sm table-striped mx-auto mb-3 colwidths-auto smaller hpad +---------------+------------------+--------------------------------+---------------------------------------------+ | Parameter | Keyword Argument | Example values | Meaning of parameter | +===============+==================+================================+=============================================+ | Subkey-Type | subkey_type | "RSA", "ELG-E" | The type of the secondary key to generate. | +---------------+------------------+--------------------------------+---------------------------------------------+ | Subkey-Length | subkey_length | 1024, 2048 | The length of the secondary key in bits. | +---------------+------------------+--------------------------------+---------------------------------------------+ | Expire-Date | expire_date | "2009-12-31", "365d", "3m", | The expiration date for the primary and any | | | | "6w", "5y", "seconds=", | secondary key. You can specify an ISO date, | | | | 0 | A number of days/weeks/months/years, an | | | | | epoch value, or 0 for a non-expiring key. | +---------------+------------------+--------------------------------+---------------------------------------------+ | Passphrase | passphrase | "secret" | The passphrase to use. If this parameter is | | | | | not specified, no passphrase is needed to | | | | | access the key. *Passphrases using newlines | | | | | are not supported*. **Note that for GnuPG | | | | | versions >= 2.1, a passphrase must be | | | | | provided, unless extra steps are taken**: | | | | | see the ``no_protection`` argument, below. | +---------------+------------------+--------------------------------+---------------------------------------------+ | %no-protection| no_protection | False (the default), True | If no passphrase is wanted for a key (which | | | | | might be the default for tests, say), or if | | | | | you want to use an empty string as a | | | | | passphrase, then you should specify ``True``| | | | | for this parameter. Otherwise, and if you | | | | | don't use pinentry to enter a passphrase, | | | | | then GnuPG >= 2.1 will not allow this. It | | | | | doesn't make sense to specify ``True`` if a | | | | | non-empty passphrase is being supplied. | +---------------+------------------+--------------------------------+---------------------------------------------+ A complete list of key generation parameters can be found in the GnuPG documentation `here `__. .. versionadded:: 0.4.7 The ``no_protection`` keyword argument was added. Whatever keyword arguments you pass to :meth:`~gnupg.GPG.gen_key_input` (other than ``no_protection``) will be converted to the parameters expected by GnuPG by replacing underscores with hyphens and title-casing the result. You can of course construct the parameters in your own dictionary ``params`` and then pass it as follows:: >>> input_data = gpg.gen_key_input(**params) The ``no_protection`` argument, if `True`, will be used to generate a `%no-protection` line which tells GnuPG that no protection with a passphrase is desired. The return value from :meth:`~gnupg.GPG.gen_key` is an object whose `type` and `fingerprint` attributes indicate the type and fingerprint of the created key. If no key was created, these will be `None`. .. versionadded:: 0.4.9 There is now also a `status` attribute to the returned object which will be `'ok'` if a key was created, `'key not created'` if that was reported by `gpg`, or `None` in other cases. .. index:: single: Key; Generating subkeys Generating subkeys ^^^^^^^^^^^^^^^^^^ To generate a subkey for an already generated key use the :meth:`~gnupg.GPG.add_subkey` method:: >>> subkey = gpg.add_subkey(master_key) # same as gpg.add_subkey(master_key, None) >>> subkey = gpg.add_subkey(master_key, master_key_password) The :meth:`~gnupg.GPG.add_subkey` method has some additional keyword arguments: * ``algorithm`` (defaulting to ``rsa``) * ``usage`` (defaulting to ``encrypt``) * ``expire`` (defaulting to ``-``) The parameters are explained with every possible value in `this GnuPG documentation `_ under ``quick-add-key``. If you use the default algorithm, you'll get the default key size, which is dependent upon the version of GnuPG that's used. If you want to specify the key size explicitly, you can use values for ``algorithm`` incorporating both the algorithm itself and the key size, as in the following examples. .. code-block:: python gpg.add_subkey(..., algorithm='rsa2048') gpg.add_subkey(..., algorithm='rsa3072') gpg.add_subkey(..., algorithm='rsa4096') .. versionadded:: 0.4.9 The ``add_subkey`` method was added. Specifying key usages ^^^^^^^^^^^^^^^^^^^^^ Keys can be used for some or all of encryption, signing or authentication. These usages map onto flags assigned to a key - one or more of 'encrypt', 'sign' and 'auth'. By default, nothing is specified, which assigns all flags to a key. But sometimes you may want to depart from this behaviour. For example, if you create a subkey for encryption, then you probably don't want encryption to be enabled for the master key. You can specify the flags associated with a key by passing a ``key_usage`` keyword argument to :meth:`~gnupg.GPG.gen_key_input` which provides one or more of the above flags in a space or comma-separated string, as in these example: .. code-block:: python gpg.gen_key_input(..., key_usage='sign') gpg.gen_key_input(..., key_usage='sign encrypt') gpg.gen_key_input(..., key_usage='sign, auth') This corresponds to the ``usage`` parameter of :meth:`~gnupg.GPG.add_subkey`, described earlier. Note that you still need to ensure that the key type of the key being created is appropriate for the usages. .. index:: single: Key; elliptic curves single: ECC keys Generating elliptic curve keys ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To generate keys with elliptic curves, pass a `key_curve` keyword parameter to :meth:`~gnupg.GPG.gen_key_input` and omit `key_length`. For example, `key_curve='cv25519'` or `key_type='ECDSA', key_curve='nistp384'`. Refer to `GnuPG resources `_ to see which options are supported. Note that you'll need GnuPG >= 2.1 for this to work. Supplemental information on the aliases used for key types and curves is given `here `__. You can use the curve type alias in the ``algorithm`` argument to :meth:`~gnupg.GPG.add_subkey`, as in the following example. .. code-block:: python input_data = gpg.gen_key_input(key_type='EDDSA', key_curve='ed25519' ...) master_key = gpg.gen_key(input_data) subkey = gpg.add_subkey(master_key.fingerprint, algorithm='cv25519' ...) .. index:: single: Key; performance issues single: Entropy Performance Issues ^^^^^^^^^^^^^^^^^^ Key generation requires the system to work with a source of random numbers. Systems which are better at generating random numbers than others are said to have higher *entropy*. This is typically obtained from the system hardware; the GnuPG documentation recommends that keys be generated *only* on a local machine (i.e. not one being accessed across a network), and that keyboard, mouse and disk activity be maximised during key generation to increase the entropy of the system. Unfortunately, there are some scenarios - for example, on virtual machines which don't have real hardware - where insufficient entropy causes key generation to be *extremely* slow. If you come across this problem, you should investigate means of increasing the system entropy. On virtualised Linux systems, this can often be achieved by installing the ``rng-tools`` package. This is available at least on RPM-based and APT-based systems (Red Hat/Fedora, Debian, Ubuntu and derivative distributions). .. index:: Key; exporting Exporting keys -------------- To export keys, use the :meth:`~gnupg.GPG.export_keys` method:: >>> ascii_armored_public_keys = gpg.export_keys(keyids) # same as gpg.export_keys(keyids, False) >>> ascii_armored_private_keys = gpg.export_keys(keyids, True) # True => private keys For the ``keyids`` parameter, you can use a sequence of anything which GnuPG itself accepts to identify a key - for example, the keyid or the fingerprint could be used. If you want to pass a single keyid, then you can just pass in a string which identifies the key. If you pass an empty list in ``keyids``, all keys are exported. The :meth:`~gnupg.GPG.export_keys` method has some additional keyword arguments: * ``armor`` (defaulting to ``True``) - when ``True``, passes ``--armor`` to ``gpg``. * ``minimal`` (defaulting to ``False``) - when ``True``, passes ``--export-options export-minimal`` to ``gpg``. * ``passphrase`` - if specified, sends the specified passphrase to ``gpg``. For GnuPG >= 2.1, exporting secret keys requires a passphrase to be provided. * ``expect_passphrase`` - defaults to ``True`` for backward compatibility. If the passphrase is to be passed to ``gpg`` via pinentry, you wouldn't pass it here - so specify ``expect_passphrase=False`` in that case. If you don't do that, and don't pass a passphrase, a ``ValueError`` will be raised. * ``output`` - defaults to ``None``, but if specified, should be the pathname of a file to which the exported keys should be written. .. versionadded:: 0.3.7 The ``armor`` and ``minimal`` keyword arguments were added. .. versionadded:: 0.4.0 The ``passphrase`` keyword argument was added. .. versionadded:: 0.4.2 The ``expect_passphrase`` keyword argument was added. .. versionadded:: 0.5.1 The ``output`` keyword argument was added. .. index:: Key; importing .. index:: Key; receiving Importing and receiving keys ---------------------------- To import keys, get the key data as an ASCII string, say ``key_data``. Then you can call :meth:`~gnupg.GPG.import_keys` with it:: >>> import_result = gpg.import_keys(key_data) This will import all the keys in ``key_data``. The number of keys imported will be available in ``import_result.count`` and the fingerprints of the imported keys will be in ``import_result.fingerprints``. In addition, ``extra_args`` and ``passphrase`` keyword parameter can be specified. If provided, ``extra_args`` is treated as a list of additional arguments to pass to the ``gpg`` executable. If ``passphrase`` is specified, it is passed to ``gpgg`` for when an imported secret key has a passphrase. .. versionadded:: 0.4.5 The ``extra_args`` keyword argument. .. versionadded:: 0.4.7 The ``passphrase`` keyword argument. To import keys from a file, use :meth:`~gnupg.GPG.import_keys_file` instead:: >>> import_result = gpg.import_keys_file(key_path) This also takes the keyword arguments specified for :meth:`~gnupg.GPG.import_keys`. .. versionadded:: 0.5.0 The :meth:`~gnupg.GPG.import_keys_file` method. To receive keys from a keyserver, use :meth:`~gnupg.GPG.recv_keys`:: >>> import_result = gpg.recv_keys('server-name', 'keyid1', 'keyid2', ...) This will fetch keys with all specified keyids and import them. Note that on Windows, you may require helper programs such as ``gpg_hkp.exe``, distributed with GnuPG, to successfully run ``recv_keys``. On Jython, security permissions may lead to failure of ``recv_keys``. Note that when you import keys, you may get spurious "key expired" / "signature expired" messages which are sent by ``gpg`` and collected by ``python-gnupg``. This may happen, for example, if there are subkey expiry dates which have been extended, so that the keys haven't actually expired, even when ``gpg`` sends messages that they have. Make sure you just look at the ``count`` and ``fingerprints`` attributes to identify the keys that were imported. .. index:: Key; listing Listing keys ------------ Now that we've seen how to generate, import and export keys, let's move on to finding which keys we have in our keyrings. This is fairly straightforward using the :meth:`~gnupg.GPG.list_keys` method:: >>> public_keys = gpg.list_keys() # same as gpg.list_keys(False) >>> private_keys = gpg.list_keys(True) # True => private keys The returned value from :meth:`~gnupg.GPG.list_keys` is a subclass of Python's ``list`` class. Each entry represents one key and is a Python dictionary which contains useful information about the corresponding key. The following entries are in the returned dictionary. Some of the key names are not ideal for describing the values, but they have been left as is for backward compatibility reasons. As `GnuPG documentation `_ has improved, a better understanding is possible of the information returned by ``gpg``. .. cssclass:: generic-table table-bordered table-responsive-sm table-striped mx-auto mb-3 colwidths-auto smaller hpad +-------------+--------------------------------------------------------------------------------+ | dict key | dict value (all string values) | +=============+================================================================================+ | type | Type of key | +-------------+--------------------------------------------------------------------------------+ | trust | The validity of the key | +-------------+--------------------------------------------------------------------------------+ | length | The length of the key in bits | +-------------+--------------------------------------------------------------------------------+ | algo | Public key algorithm | +-------------+--------------------------------------------------------------------------------+ | keyid | The key ID | +-------------+--------------------------------------------------------------------------------+ | date | The creation date of the key in UTC as a Unix timestamp | +-------------+--------------------------------------------------------------------------------+ | expires | The expiry date of the key in UTC as a timestamp, if specified | +-------------+--------------------------------------------------------------------------------+ | dummy | Certificate serial number, UID hash or trust signature info | +-------------+--------------------------------------------------------------------------------+ | ownertrust | The level of owner trust for the key | +-------------+--------------------------------------------------------------------------------+ | uid | The user ID | +-------------+--------------------------------------------------------------------------------+ | sig | Signature class | +-------------+--------------------------------------------------------------------------------+ | cap | Key capabilities | +-------------+--------------------------------------------------------------------------------+ | issuer | Issuer information | +-------------+--------------------------------------------------------------------------------+ | flag | A flag field | +-------------+--------------------------------------------------------------------------------+ | token | Token serial number | +-------------+--------------------------------------------------------------------------------+ | hash | Hash algorithm | +-------------+--------------------------------------------------------------------------------+ | curve | Curve name for elliptic curve cryptography (ECC) keys | +-------------+--------------------------------------------------------------------------------+ | compliance | Compliance flags | +-------------+--------------------------------------------------------------------------------+ | updated | Last updated timestamp | +-------------+--------------------------------------------------------------------------------+ | origin | Origin of keys | +-------------+--------------------------------------------------------------------------------+ | keygrip | Keygrip of keys (Note that you'll need GnuPG >= 2.1 for this to work.) | +-------------+--------------------------------------------------------------------------------+ | subkeys | A list containing [keyid, type, fingerprint, keygrip] elements for each subkey | +-------------+--------------------------------------------------------------------------------+ | subkey_info | A dictionary of subkey information keyed on subkey id | +-------------+--------------------------------------------------------------------------------+ Depending on the version of ``gpg`` used, some of these keys may have the value ``'unavailable'``. The last two keys are provided by ``python-gnupg`` rather than ``gpg``. For more information about the values in this dictionary, refer to the GnuPG documentation linked above. (Note that that documentation is not terribly user-friendly, but nevertheless it should be usable.) The returned value from :meth:`~gnupg.GPG.list_keys` has an attribute ``uids``, which is a list of userids associated with the listed keys, and an attribute ``fingerprints``, which is a list of the key fingerprints associated with the listed keys. .. versionadded:: 0.3.8 The returned value from :meth:`~gnupg.GPG.list_keys` now has a new attribute, ``key_map``, which is a dictionary mapping key and subkey fingerprints to the corresponding key's dictionary. With this change, you don't need to iterate over the (potentially large) returned list to search for a key with a given fingerprint - the ``key_map`` dict will take you straight to the key info, whether the fingerprint you have is for a key or a subkey. .. versionadded:: 0.3.8 You can also list a subset of keys by specifying a ``keys=`` keyword argument to :meth:`~gnupg.GPG.list_keys` whose value is either a single string matching a key, or a list of strings matching multiple keys. In this case, the return value only includes matching keys. .. versionadded:: 0.3.9 A new ``sigs=`` keyword argument has been added to :meth:`~gnupg.GPG.list_keys`, defaulting to ``False``. If you specify true, the ``sigs`` entry in the key information returned will contain a list of signatures which apply to the key. Each entry in the list is a 3-tuple of (``keyid``, ``user-id``, ``signature-class``) where the ``signature-class`` is as defined by RFC-4880_. It doesn't make sense to supply both ``secret=True`` *and* ``sigs=True`` (people can't sign your secret keys), so in case ``secret=True`` is specified, the ``sigs=`` value has no effect. .. versionadded:: 0.4.1 Instances of the ``GPG`` class now have an additional ``on_data`` attribute, which defaults to ``None``. It can be set to a callable which will be called with a single argument - a binary chunk of data received from the ``gpg`` executable. The callable can do whatever it likes with the chunks passed to it - e.g. write them to a separate stream. The callable should not raise any exceptions (unless it wants the current operation to fail). .. versionadded:: 0.4.2 Information on keys returned by :meth:`~gnupg.GPG.list_keys` or :meth:`~gnupg.GPG.scan_keys` now incudes a ``subkey_info`` dictionary, which contains any returned information on subkeys such as creation and expiry dates. The dictionary is keyed on the subkey ID. The following additional keys are present in key information dictionaries: ``cap``, ``issuer``, ``flag``, ``token``, ``hash``, ``curve``, ``compliance``, ``updated`` and ``origin``. .. versionadded:: 0.4.4 Instances of the ``GPG`` class now have an additional ``check_fingerprint_collisions`` attribute, which defaults to ``False``. If set to a truthy value, fingerprint collisions are checked for (and a ``ValueError`` raised if a collision is detected) when listing or scanning keys. It appears that ``gpg`` is quite lenient about allowing duplicated keys in keyrings, which would lead to collisions. .. versionchanged:: 0.4.4 The ``on_data`` callable will now be called with an empty chunk when the data stream from ``gpg`` is exhausted. It can now also return a value: if the value ``False`` is returned, the chunk will *not* be buffered within ``python-gnupg``. This might be useful if you want to do your own buffering or avoid buffering altogether. If any other value is returned (including the value ``None``, for backward compatibility) the chunk will be buffered as normal by ``python-gnupg``. .. versionadded:: 0.4.6 Instances of the ``GPG`` class now have an additional ``error_map`` attribute, which defaults to ``None``. If you set this, the value should be a dictionary mapping error codes to error messages. The source distribution includes a file ``messages.json`` which contains such a mapping, gleaned from the GnuPG library libgpg-error, version 1.37. The test suite shows how to convert that JSON to a form suitable for converting to an ``error_map`` value (basically, converting the string keys in the JSON to integers using base 16). .. versionadded:: 0.4.9 Information on keys returned by :meth:`~gnupg.GPG.list_keys` now includes the ``keygrip`` attribute. The ``subkeys`` attribute now also consists of four values with the ``keygrip`` being the fourth. Note that you'll need GnuPG >= 2.1 for this to work. .. versionadded:: 0.5.4 Instances of the result classes from operations now have an ``on_data_failure`` attribute, which defaults to ``None``. If an ``on_data`` callable raises an exception, the ``on_data_failure`` attribute of the returned object from a high-level operation is set to the first exception that was raised. The ``on_data`` callable will continue to be called with future chunks. If you use ``on_data`` with code that can raise any exceptions, be sure to check the ``on_data_failure`` attribute of a returned object before using any other aspects of the result. .. versionadded:: 0.5.5 The returned value from :meth:`~gnupg.GPG.list_keys` now has a new attribute, ``uid_map``, which is a dictionary mapping uids to dicts with detailed information about the corresponding uid. The keys of the information provided are listed in the table above. Refer to the `GnuPG documentation `_ for more information. You could use this information as in the following example: .. code-block:: pycon >>> from pprint import pprint >>> keys = gpg.list_keys() >>> pprint(keys.uids) ['Andrew Able (A test user) ', 'Barb Bruin ', 'Babs Broon ', 'Barbara Brown (A test user) ', 'Charlie Clark (A test user) ', 'Donna Davis (A test user) '] >>> pprint(keys.uid_map('Barbara Brown (A test user) ') {'algo': '', 'date': '1739485458', 'dummy': '8B989767967370B894C53279A3BDF655F00CD4DE', 'expires': '', 'keyid': '', 'length': '', 'ownertrust': '', 'sig': '', 'trust': 'u', 'type': 'uid', 'uid': 'Barbara Brown (A test user) '} >>> pprint(keys.uid_map['Barb Bruin ']) {'algo': '', 'date': '1739485886', 'dummy': '951261047308BCA0B45FD738AD8630B336B88ECF', 'expires': '', 'keyid': '', 'length': '', 'ownertrust': '', 'sig': '', 'trust': 'u', 'type': 'uid', 'uid': 'Barb Bruin '} >>> pprint(keys.uid_map['Babs Broon ']) {'algo': '', 'date': '', 'dummy': '2BDB74660AC54DF33DE523429386E2D460904E74', 'expires': '', 'keyid': '', 'length': '', 'ownertrust': '', 'sig': '', 'trust': 'r', 'type': 'uid', 'uid': 'Babs Broon '} >>> The first two of these dictionaries show normal uids (trust is 'u', for ultimate), whereas the third shows a revoked uid (trust is 'r', for revoked). .. _RFC-4880: https://tools.ietf.org/html/rfc4880#section-5.2.1 .. index:: Key; trusting Setting the trust level for imported keys ----------------------------------------- You can set the trust level for imported keys using :meth:`~gnupg.GPG.trust_keys`:: >>> gpg.trust_keys(fingerprints, trustlevel) where the ``fingerprints`` are a list of fingerprints of keys for which the trust level is to be set, and ``trustlevel`` is one of the string values ``'TRUST_EXPIRED'``, ``'TRUST_UNDEFINED'``, ``'TRUST_NEVER'``, ``'TRUST_MARGINAL'``, ``'TRUST_FULLY'`` or ``'TRUST_ULTIMATE'``. You can also specify a single fingerprint for the ``fingerprints`` parameter. .. versionadded:: 0.4.2 The ``trust_keys`` method was added. .. index:: Key; scanning Scanning keys ------------- We can also scan keys in files without importing them into a local keyring, by using :meth:`~gnupg.GPG.scan_keys`:: >>> keys = gpg.scan_keys(key_file_name) The returned value from :meth:`~gnupg.GPG.scan_keys` has the same format as for :meth:`~gnupg.GPG.list_keys`. .. versionadded:: 0.3.7 The :meth:`~gnupg.GPG.scan_keys` method was added. To scan keys in a string, we can use :meth:`~gnupg.GPG.scan_keys_mem` instead:: >>> keys = gpg.scan_keys_mem(key_text) The result will be the same as for :meth:`~gnupg.GPG.scan_keys`. .. versionadded:: 0.5.1 The :meth:`~gnupg.GPG.scan_keys_mem` method was added. .. index:: Key; deleting Deleting keys ------------- To delete keys, their key identifiers must be specified. If a public/private keypair has been created, a private key needs to be deleted before the public key can be deleted, and for both you use the :meth:`~gnupg.GPG.delete_keys` method:: >>> key = gpg.gen_key(gpg.gen_key_input()) >>> fp = key.fingerprint >>> str(gpg.delete_keys(fp)) # same as gpg.delete_keys(fp, False) 'Must delete secret key first' >>> str(gpg.delete_keys(fp, True))# True => private keys 'ok' >>> str(gpg.delete_keys(fp)) 'ok' >>> str(gpg.delete_keys("nosuchkey")) 'No such key' The argument you pass to :meth:`~gnupg.GPG.delete_keys` can be either a single key identifier (e.g. keyid or fingerprint) or a sequence of key identifiers. The :meth:`~gnupg.GPG.delete_keys` method has some additional keyword arguments: * ``passphrase`` - if specified, sends the specified passphrase to ``gpg``. For GnuPG >= 2.1, exporting secret keys requires a passphrase to be provided. * ``expect_passphrase`` - defaults to ``True`` for backward compatibility. If the passphrase is to be passed to ``gpg`` via pinentry, you wouldn't pass it here - so specify ``expect_passphrase=False`` in that case. If you don't do that, and don't pass a passphrase, a ``ValueError`` will be raised. * ``exclamation_mode`` - defaults to ``False`` for backward compatibility. If the exclamation mode is set, and a fingerprint of a subkey is passed only that subkey will be deleted. If the fingerprint is of a primary key the entire key will be deleted. .. versionadded:: 0.4.0 The ``passphrase`` keyword argument was added. .. versionadded:: 0.4.2 The ``expect_passphrase`` keyword argument was added. .. versionadded:: 0.4.9 The ``exclamation_mode`` keyword argument was added. .. index:: Key; searching Searching for keys ------------------ You can search for keys by passing a search query and optionally a keyserver name to the :meth:`~gnupg.GPG.search_keys`. If no keyserver is specified, ``pgp.mit.edu`` is used. A list of dictionaries describing keys that were found is returned (this list could be empty). For example:: >>> gpg.search_keys('vinay_sajip@hotmail.com', 'keyserver.ubuntu.com') [{'keyid': u'92905378', 'uids': [u'Vinay Sajip '], 'expires': u'', 'length': u'1024', 'algo': u'17', 'date': u'1221156445', 'type': u'pub'}] .. versionadded:: 0.3.5 The :meth:`~gnupg.GPG.search_keys` method was added. .. index:: Key; sending Sending keys ------------ You can send keys to a keyserver by passing its name and some key identifiers to the :meth:`~gnupg.GPG.send_keys`. For example:: >>> gpg.send_keys('keyserver.ubuntu.com', '6E4D5A2B') .. versionadded:: 0.3.5 The :meth:`~gnupg.GPG.send_keys` method was added. Encryption and Decryption ========================= Data intended for some particular recipients is encrypted with the public keys of those recipients. Each recipient can decrypt the encrypted data using the corresponding private key. .. index:: Encryption Encryption ---------- To encrypt a message, use the :meth:`~gnupg.GPG.encrypt` method:: >>> encrypted_ascii_data = gpg.encrypt(data, recipients) If you want to encrypt data in a file (or file-like object), use :meth:`~gnupg.GPG.encrypt_file` instead:: >>> encrypted_ascii_data = gpg.encrypt_file(stream, recipients) # e.g. after stream = open(filename, 'rb') These methods both return an object such that: * If encryption succeeded, the returned object's ``ok`` attribute is set to ``True`` and the ``data`` attribute holds the encrypted data. Otherwise, the returned object's ``ok`` attribute is set to ``False`` and its ``status`` attribute (a message string) provides more information as to the reason for failure (for example, ``'invalid recipient'`` or ``'key expired'``). * ``str(encrypted_ascii_data)`` gives the encrypted data in a non-binary format. In both cases, ``recipients`` is a list of key fingerprints for those recipients. For your convenience, if there is a single recipient, you can pass the fingerprint rather than a 1-element array containing the fingerprint. Both methods accept the following optional keyword arguments: sign (defaults to ``None``) Either the Boolean value ``True``, or the fingerprint of a key which is used to sign the encrypted data. If ``True`` is specified, the default key is used for signing. When not specified, the data is not signed. always_trust (defaults to ``False``) Skip key validation and assume that used keys are always fully trusted. passphrase (defaults to ``None``) A passphrase to use when accessing the keyrings. extra_args (defaults to ``None``) A list of additional arguments to pass to the ``gpg`` executable. For example, you could pass ``extra_args=['-z', '0']`` to disable compression, or you could pass ``extra_args=['--set-filename', 'name-to-embed-in-encrypted-file.txt']`` to embed a specific file name in the encrypted message. armor (defaults to ``True``) Whether to use ASCII armor. If ``False``, binary data is produced. output (defaults to ``None``) The name of an output file to write to. If a name is specified, the encrypted output is written directly to the file. .. index:: Encryption; symmetric symmetric (defaults to ``False``) If specified, symmetric encryption is used. In this case, specify recipients as ``None``. If ``True`` is specified, then the default cipher algorithm (``CAST5``) is used. Starting with version 0.3.5, you can also specify the cipher-algorithm to use (for example, ``'AES256'``). Check your ``gpg`` command line help to see what symmetric cipher algorithms are supported. Note that the default (``CAST5``) may not be the best available. .. versionchanged:: 0.3.5 A string can be passed for the ``symmetric`` argument, as well as ``True`` or ``False``. If a string is passed, it should be a symmetric cipher algorithm supported by the ``gpg`` you are using. .. versionadded:: 0.4.1 The ``extra_args`` keyword argument was added. .. versionadded:: 0.5.1 The ``status_detail`` attribute was added to the result object. This attribute will be set when the result object's ``status`` attribute is set to ``invalid recipient`` and will contain more information about the failure in the form of ``reason:ident`` where ``reason`` is a text description of the reason, and ``ident`` identifies the recipient key. .. note:: Any public key provided for encryption should be trusted, otherwise encryption fails but without any warning. This is because gpg just prints a message to the console, but does not provide a specific error indication that the Python wrapper can use. .. versionchanged:: 0.5.0 The `stream` argument to :meth:`~gnupg.GPG.encrypt_file` can be a pathname to an existing file as well as text or a file-like object. In the pathname case, ``python-gnupg`` will open and close the file for you. .. note:: ``python-gnupg`` assumes that any object with a :attr:`read` attribute is a file-like object. Otherwise, if it corresponds to an existing file, then it is taken as a filename, and otherwise it must be the actual data to be processed. .. index:: Decryption Decryption ---------- To decrypt a message, use the :meth:`~gnupg.GPG.decrypt` method:: >>> decrypted_data = gpg.decrypt(data) If you want to decrypt data in a file (or file-like object), use :meth:`~gnupg.GPG.decrypt_file` instead:: >>> decrypted_data = gpg.decrypt_file(stream) # e.g. after stream = open(filename, 'rb') These methods both return an object such that ``str(decrypted_data)`` gives the decrypted data in a non-binary format. If decryption succeeded, the returned object's ``ok`` attribute is set to ``True`` and the ``data`` attribute holds the decrypted data. Otherwise, the returned object's ``ok`` attribute is set to ``False`` and its ``status`` attribute (a message string) provides more information as to the reason for failure (for example, ``'bad passphrase'`` or ``'decryption failed'``). Both methods accept the following optional keyword arguments: always_trust (defaults to ``False``) Skip key validation and assume that used keys are always fully trusted. passphrase (defaults to ``None``) A passphrase to use when accessing the keyrings. extra_args (defaults to ``None``) A list of additional arguments to pass to the ``gpg`` executable. output (defaults to ``None``) The name of an output file to write to. If a name is specified, the decrypted output is written directly to the file. .. versionadded:: 0.4.1 The ``extra_args`` keyword argument was added. .. versionadded:: 0.4.2 Upon a successful decryption, the keyid of the decrypting key is stored in the ``key_id`` attribute of the result, if this information is provided by ``gpg``. .. versionchanged:: 0.5.0 The `stream` argument to :meth:`~gnupg.GPG.decrypt_file` can be a pathname to an existing file as well as text or a file-like object. In the pathname case, ``python-gnupg`` will open and close the file for you. .. _caching-warning: .. warning:: **Passphrase caching:** By default, ``gpg-agent`` caches passphrases, and this can lead to unexpected results such as successfully decrypting messages even when passing the wrong passphrase. To avoid this, disable caching by putting the following two lines in ``gpg-agent.conf``: * ``default-cache-ttl 0`` and either * ``maximum-cache-ttl 0`` for GnuPG < 2.1, or * ``max-cache-ttl 0`` for GnuPG >= 2.1. For more information, see the `GnuPG documentation on agent configuration `_. Using signing and encryption together ------------------------------------- If you want to use signing and encryption together, use the :meth:`~gnupg.GPG.encrypt` with a signer fingerprint and the corresponding passphrase:: >>> encrypted_data = gpg.encrypt(data, recipients, sign=signer_fingerprint, passphrase=signer_passphrase) The resulting encrypted data contains the signature. When decrypting the data, upon successful decryption, signature verification is also performed (assuming the relevant public keys are available at the recipient end). The results are stored in the object returned from the :meth:`~gnupg.GPG.decrypt` call:: >>> decrypted_data = gpg.decrypt(data, passphrase=recipient_passphrase) At this point, if a signature is verified, signer information is held in attributes of ``decrypted_data``: ``username``, ``key_id``, ``signature_id``, ``fingerprint``, ``trust_level`` and ``trust_text``. If the message wasn't signed, these attributes will all be set to ``None``. The trust levels are (in increasing order) ``TRUST_UNDEFINED``, ``TRUST_NEVER``, ``TRUST_MARGINAL``, ``TRUST_FULLY`` and ``TRUST_ULTIMATE``. If verification succeeded, you can test the trust level against known values as in the following example:: decrypted_data = gpg.decrypt(data, passphrase=recipient_passphrase)) if decrypted_data.trust_level is not None and decrypted_data.trust_level >= decrypted_data.TRUST_FULLY: print('Trust level: %s' % decrypted_data.trust_text) .. versionadded:: 0.3.1 The ``trust_level`` and ``trust_text`` attributes were added. Finding the recipients for an encrypted message ----------------------------------------------- Sometimes, it's desirable to find the recipients for an encrypted message, without actually performing decryption. You can do this using the :meth:`~gnupg.GPG.get_recipients` or :meth:`~gnupg.GPG.get_recipients_file` methods: >>> ids = gpg.get_recipients(data) or, with a file or file-like object: >>> ids = gpg.get_recipients_file(stream) # e.g. after stream = open(filename, 'rb') .. versionadded:: 0.4.8 The ``get_recipients`` and ``get_recipients_file`` methods were added. .. versionchanged:: 0.5.0 The `stream` argument to :meth:`~gnupg.GPG.get_recipients_file` can be a pathname to an existing file as well as text or a file-like object. In the pathname case, ``python-gnupg`` will open and close the file for you. Custom handling of data streams ------------------------------- During processing, ``gpg`` often sends output to its ``stdout`` stream, which is captured by ``python-gnupg`` buffered, and returned as part of an operation's result (usually in the ``data`` attribute). However, there might be times when you want to: * Avoid buffering, as the data sizes involved are large. * Process the data as it becomes available, before it's all available at the end of an operation. Most commonly, this will happen during decryption. In such cases, you can supply a callable in the ``on_data`` attribute of a :class:`GPG` instance before you invoke the operation. When an operation with ``gpg`` is initiated, if ``on_data`` is given a value, it will be called with each chunk of data (of type ``bytes``) received from ``gpg``, and its return value will be used to determine whether ``python-gnupg`` buffers the data. At the end of the data stream, it will be called with a zero-length bytestring (allowing you do any necessary clean-up). If the ``on_data`` callable returns ``False``, the data will *not* be buffered by ``python-gnupg``. For any other return value (including ``None``), the data *will* be buffered. (This slightly odd arrangement is for backwards compatibility.) Example usages (not tested, error handling omitted): .. code-block:: python # Doing your own buffering in memory chunks = [] def collector(chunk): chunks.append(chunk) return False # Tell python-gnupg not to buffer the chunk gpg = GPG(...) gpg.on_data = collector gpg.decrypt(...) # Doing your own buffering in a file class Collector: def __init__(self, fn): self.out = open(fn, 'wb') def __call__(self, chunk): self.out.write(chunk) if not chunk: self.out.close() return False # Tell python-gnupg not to buffer the chunk gpg = GPG(...) gpg.on_data = Collector('/tmp/plain.txt') gpg.decrypt(...) # Processing as you go (assuming the decrypted data is utf-8 encoded) import codecs class Processor: def __init__(self, fn): self.out = open(fn, 'w', encoding='utf-8') self.decoder = codecs.getincrementaldecoder('utf-8') self.result = '' def __call__(self, chunk): final = (len(chunk) == 0) self.result += self.decoder.decode(chunk, final) # Perhaps do custom processing of self.result here self.out.write(self.result) self.result = '' if final: self.out.close() return False # Tell python-gnupg not to buffer the chunk gpg = GPG(...) gpg.on_data = Processor('/tmp/plain.txt') gpg.decrypt(...) Threading constraints on processing data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The `on_data` callable is called from a background thread which is reading data from the `gpg` child process. Sometimes, there are constraints on where certain processing can be done (e.g. code involving SQLAlchemy sessions or Qt GUI updates needs to be run on a specific thread, not just any thread). To handle this, you can use a queue, as in this example (not tested, error handling omitted): .. code-block:: python import queue class ChunkForwarder: def __init__(self, queue): self.queue = queue def __call__(self, chunk): self.queue.put(chunk) return False # Tell python-gnupg not to buffer the chunk # In the thread where you need to process chunks gpg = GPG(...) q = queue.Queue() gpg.on_data = ChunkForwarder(q) # call the operation you want to perform. Chunks will be sent to the queue. gpg.decrypt(...) while True: chunk = q.get() q.task_done() # keep things tidy if not chunk: break # process the chunk using a SQLAlchemy session, Qt widget or whatever process_chunk(chunk) Signing and Verification ======================== Data intended for digital signing is signed with the private key of the signer. Each recipient can verify the signed data using the corresponding public key. .. index:: Signing Signing ------- To sign a message, use the :meth:`~gnupg.GPG.sign` method:: >>> signed_data = gpg.sign(message) or, for data in a file (or file-like object), you can use the :meth:`~gnupg.GPG.sign_file` method instead:: >>> signed_data = gpg.sign_file(stream) # e.g. after stream = open(filename, "rb") These methods both return an object such that ``str(signed_data)`` gives the signed data in a non-binary format. They accept the following optional keyword arguments: keyid (defaults to ``None``) The id for the key which will be used to do the signing. If not specified, the first key in the secret keyring is used. passphrase (defaults to ``None``) A passphrase to use when accessing the keyrings. clearsign (defaults to ``True``) Returns a clear text signature, i.e. one which can be read without any special software. detach (defaults to ``False``) Returns a detached signature. If you specify ``True`` for this, then the detached signature will not be clear text, i.e. it will be as if you had specified a ``False`` value for *clearsign*. This is because if both are specified, gpg ignores the request for a detached signature. binary (defaults to ``False``) If ``True``, a binary signature (rather than armored ASCII) is created. output (defaults to ``None``) If specified, this is used as the file path where GPG outputs the signature. Convention dictates a ``.asc`` or ``.sig`` file extension for this. extra_args (defaults to ``None``) A list of additional arguments to pass to the ``gpg`` executable. Note: If the data being signed is binary, calling ``str(signed_data)`` may raise exceptions. In that case, use the fact that ``signed_data.data`` holds the binary signed data. Usually the signature itself is ASCII; it's the message itself which may cause the exceptions to be raised. (Unless a detached signature is requested, the result of signing is the message with the signature appended.) The hash algorithm used when creating the signature can be found in the ``signed_data.hash_algo`` attribute. .. versionadded:: 0.2.5 The ``detach`` keyword argument was added in version 0.2.5. .. versionadded:: 0.2.6 The ``binary`` keyword argument was added in version 0.2.6. .. versionadded:: 0.3.7 The ``output`` keyword argument was added in version 0.3.7. .. versionadded:: 0.4.1 The ``extra_args`` keyword argument was added. .. versionadded:: 0.4.2 The keyid and username of the signing key are stored in the ``key_id`` and ``username`` attributes of the result, if this information is provided by ``gpg`` (which should happen if you specify ``extra_args=['--verbose']``). .. versionchanged:: 0.5.0 The *stream* argument to :meth:`~gnupg.GPG.sign_file` can be a pathname to an existing file as well as text or a file-like object. In the pathname case, ``python-gnupg`` will open and close the file for you. .. versionadded:: 0.5.1 The ``status_detail`` attribute was added to the result object. This attribute will be set when the result object's ``status`` attribute is set to ``invalid signer`` and will contain more information about the failure in the form of ``reason:ident`` where ``reason`` is a text description of the reason, and ``ident`` identifies the signing key. .. index:: Verification Verification ------------ To verify some data which you've received, use the :meth:`~gnupg.GPG.verify` method:: >>> verified = gpg.verify(data) To verify data in a file (or file-like object), use :meth:`~gnupg.GPG.verify_file`:: >>> verified = gpg.verify_file(stream) # e.g. after stream = open(filename, "rb") You can use the returned value in a Boolean context:: >>> if not verified: raise ValueError("Signature could not be verified!") Getting the signed data out while verifying ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you clearsign data, the signature envelops the signed data (whether text or binary) with the signature, but by default you won't get this data back from a :meth:`~gnupg.GPG.verify` or :meth:`~gnupg.GPG.verify_file` call. In order to extract the signed data, you need to pass more information to the ``verify`` methods about where you want that data (if none is specified, the data is discarded). To write it to ``gpg``'s standard output, specify ``extra_args=['-o', '-']``. In that case, it will be returned as a bytestring in ``verified.data``. Alternatively, to write to a file, you can pass ``extra_args=['-o', 'path/to/write/data.to']`` and it will be written to the file you specify. (Thanks to Mark Neil for this suggestion.) Verifying detached signatures on disk ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you want to verify a detached signature, use :meth:`~gnupg.GPG.verify_file`:: >>> verified = gpg.verify_file(stream, path_to_data_file) Note that in this case, the ``stream`` contains the *signature* to be verified. The data that was signed should be in a separate file whose path is indicated by ``path_to_data_file``. .. versionadded:: 0.2.5 The second argument to verify_file (``data_filename``) was added. .. versionadded:: 0.4.1 An optional keyword argument to verify_file (``close_file``) was added. This defaults to ``True``, but if set to ``False``, the signature stream is not closed. It's then left to the caller to close it when appropriate. An optional keyword argument ``extra_args`` was added. This defaults to ``None``, but if a value is specified, it should be a list of extra arguments to pass to the ``gpg`` executable. .. versionadded:: 0.4.4 When signature verification is performed, multiple signatures might be present. Information about all signatures is now captured in a ``sig_info`` attribute of the value returned from ``verify``. This is a dictionary keyed by the signature ID and whose values are dictionaries containing the following information (note - all are string values): * ``fingerprint`` - the fingerprint of the signing key. * ``pubkey_fingerprint`` - this is usually the same as ``fingerprint``, but it might be different if a subkey was used for the signing. * ``keyid`` - the key id. * ``username`` - user information for the signing key. * ``status`` - this indicates the status of the signature. * ``creation_date`` - the creation date of the signature in text format, YYYY-MM-DD. * ``timestamp`` - the signature creation time as a timestamp. * ``expiry`` - the signature expiry time as a timestamp, or ``'0'`` to indicate no expiry. * ``trust_level`` - the trust level, see below. * ``trust_text`` - the text corresponding to the trust level. Note that only information for valid signatures will be present in ``sig_info``. When a signature is verified, signer information is held in attributes of ``verified``: ``username``, ``key_id``, ``signature_id``, ``fingerprint``, ``trust_level`` and ``trust_text``. If the message wasn't signed, these attributes will all be set to ``None``. If there were multiple signatures, the last values seen will be shown. The trust levels are (in increasing order) TRUST_UNDEFINED, TRUST_NEVER, TRUST_MARGINAL, TRUST_FULLY and TRUST_ULTIMATE. If verification succeeded, you can test the trust level against known values as in the following example:: verified = gpg.verify(data) if verified.trust_level is not None and verified.trust_level >= verified.TRUST_FULLY: print('Trust level: %s' % verified.trust_text) .. versionadded:: 0.3.1 The ``trust_level`` and ``trust_text`` attributes were added. Note that even if you have a valid signature, you may want to not rely on that validity, if the key used for signing has expired or was revoked. If this information is available, it will be in the ``key_status`` attribute =, and the result will still be ``False`` in a Boolean context. If there is no problem detected with the signing key, the ``key_status`` attribute will be ``None``. .. versionadded:: 0.3.3 The ``key_status`` attribute was added. .. versionadded:: 0.4.2 The keyid and username of the signing key are stored in the ``key_id`` and ``username`` attributes of the result, if this information is provided by ``gpg``. .. versionchanged:: 0.5.0 The `stream` argument to :meth:`~gnupg.GPG.verify_file` can be a pathname to an existing file as well as text or a file-like object. In the pathname case, ``python-gnupg`` will open and close the file for you. .. versionadded:: 0.5.1 A ``problems`` attribute was added which holds problems reported by ``gpg`` during verification. This is a list of dictionaries, one for each reported problem. Each dictionary will have ``status`` and ``keyid`` keys indicating the problem and the corresponding key; other information in the dictionaries will be error specific. Verifying detached signatures in memory ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can also verify detached signatures where the data is in memory, using :meth:`~gnupg.GPG.verify_data`:: >>> verified = gpg.verify_data(path_to_signature_file, data) where *data* should be a byte string of the data to be verified against the signature in the file named by *path_to_signature_file*. The returned value is the same as for the other verification methods. In addition, an ``extra_args`` keyword parameter can be specified. If provided, this is treated as a list of additional arguments to pass to the ``gpg`` executable. .. versionadded:: 0.3.6 The :meth:`~gnupg.GPG.verify_data` method was added. .. versionadded:: 0.4.1 The ``extra_args`` keyword argument was added. Accessing gpg's Return Code =========================== Starting with version 0.4.8, return values to all calls which implement ``gpg`` operations, other than :meth:`~gnupg.GPG.export_keys`, will have a ``returncode`` attribute which is the return code returned by the ``gpg`` invocation made to perform the operation (the result of :meth:`~gnupg.GPG.export_keys` is the set of exported keys and doesn't have this attribute). .. versionadded:: 0.4.8 The ``returncode`` attribute was added to result instances. Passphrases =========== Passphrases provided to ``python-gnupg`` are not stored persistently, and just passed through to the ``GnuPG`` executable through a pipe. The user of ``python-gnupg`` is responsible for taking care not to store passphrases where they may become available to malicious code or malicious users, as well as the physical and security aspects of managing their private keys. .. index:: Logging .. _logging: Logging ======= The module makes use of the facilities provided by Python's ``logging`` package. A single logger is created with the module's ``__name__``, hence ``gnupg`` unless you rename the module. A ``NullHandler`` instance is added to this logger, so if you don't use logging in your application which uses this module, you shouldn't see any logging messages. If you do use logging in your application, just configure it in the normal way. .. index:: Download Test Harness ============ The distribution includes a test harness, ``test_gnupg.py``, which contains unit tests covering the functionality described above. You can invoke ``test_gnupg.py`` with one or more optional command-line arguments. If no arguments are provided, all tests are run. If arguments are provided, they collectively determine which of the tests will be run: import Run tests relating to key import crypt Run tests relating to encryption and decryption sign Run tests relating to signing and verification key Run tests relating to key management basic Run basic tests relating to environment setup, or which don't fit into one of the above categories Download ======== The latest version is available from the `PyPI `_ page. The source code repository can be found `here `__. Status and Further Work ======================= The ``gnupg`` module, being based on proven earlier versions, is quite usable, and comes packaged with Linux distributions such as Debian, Ubuntu and Fedora. However, there may be some features of GnuPG which this module does not take advantage of, or provide access to. How this module evolves will be determined by feedback from its user community. Support for GnuPG 2.1 is limited, because that version of GnuPG does not provide the ability to prevent pinentry popups in all cases. This package sends passphrases to the ``gpg`` executable via pipes, which is only possible under GnuPG 2.1 under limited conditions and requiring end-users to edit GnuPG configuration files. At present, functionality that requires interacting with the ``gpg`` executable (e.g. for key editing) is not available. This is because it requires essentially a state machine which manages the interaction - moreover, a state machine which varies according to the specific version of the ``gpg`` executable being used. If you find bugs and want to raise issues, please do so via the `project issue tracker `_. All feedback will be gratefully received; please send it to the `discussion group `_. .. cssclass:: hidden Index ===== * :ref:`genindex` ================================================ FILE: docs/requirements.txt ================================================ sphinxcontrib-spelling==7.6.2 sphinx<7 sphinx-rtd-theme>=1.2.2 ================================================ FILE: docs/spelling_wordlist.txt ================================================ Cunnane Folkinshteyn Dmitry Gladkov Abdul Karim Yann Leboulanger Kirill Yakovenko Leftwich Michal Niklas Noël Jannis Leidel Venzen Khaosan Pörner Kévin dprovins ernest eyepulp hysterix slackin natureshadow gpgbinary gpg env Kuchling Traugott Wayback gnupg etc latin Autogenerated Bloggs Gamal pinentry keyid keyids Jython algo ownertrust uid sig Keygrip keygrip truthy libgpg th armor clearsign armored args popups ================================================ FILE: gnupg.py ================================================ """ A wrapper for the GnuPG `gpg` command. Portions of this module are derived from A.M. Kuchling's well-designed GPG.py, using Richard Jones' updated version 1.3, which can be found in the pycrypto CVS repository on Sourceforge: http://pycrypto.cvs.sourceforge.net/viewvc/pycrypto/gpg/GPG.py This module is *not* forward-compatible with amk's; some of the old interface has changed. For instance, since I've added decrypt functionality, I elected to initialize with a 'gnupghome' argument instead of 'keyring', so that gpg can find both the public and secret keyrings. I've also altered some of the returned objects in order for the caller to not have to know as much about the internals of the result classes. While the rest of ISconf is released under the GPL, I am releasing this single file under the same terms that A.M. Kuchling used for pycrypto. Steve Traugott, stevegt@terraluna.org Thu Jun 23 21:27:20 PDT 2005 This version of the module has been modified from Steve Traugott's version (see http://trac.t7a.org/isconf/browser/trunk/lib/python/isconf/GPG.py) by Vinay Sajip to make use of the subprocess module (Steve's version uses os.fork() and so does not work on Windows). Renamed to gnupg.py to avoid confusion with the previous versions. Modifications Copyright (C) 2008-2026 Vinay Sajip. All rights reserved. For the full documentation, see https://docs.red-dove.com/python-gnupg/ or https://gnupg.readthedocs.io/ """ import codecs from datetime import datetime from email.utils import parseaddr from io import StringIO import logging import os try: from queue import Queue, Empty except ImportError: from Queue import Queue, Empty import re import socket from subprocess import Popen, PIPE import sys import threading __version__ = '0.5.7.dev0' __author__ = 'Vinay Sajip' __date__ = '$31-Dec-2025 16:41:34$' STARTUPINFO = None if os.name == 'nt': # pragma: no cover try: from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW, SW_HIDE except ImportError: STARTUPINFO = None try: unicode _py3k = False string_types = basestring text_type = unicode path_types = (bytes, str) except NameError: _py3k = True string_types = str text_type = str path_types = (str, ) logger = logging.getLogger(__name__) if not logger.handlers: logger.addHandler(logging.NullHandler()) # See gh-196: Logging could show sensitive data. It also produces some voluminous # output. Hence, split into two tiers - stuff that's always logged, and stuff that's # only logged if log_everything is True. (This is set by the test script.) # # For now, only debug logging of chunks falls into the optionally-logged category. log_everything = False # We use the test below because it works for Jython as well as CPython if os.path.__name__ == 'ntpath': # pragma: no cover # On Windows, we don't need shell quoting, other than worrying about # paths with spaces in them. def shell_quote(s): return '"%s"' % s else: # Section copied from sarge # This regex determines which shell input needs quoting # because it may be unsafe UNSAFE = re.compile(r'[^\w%+,./:=@-]') def shell_quote(s): """ Quote text so that it is safe for POSIX command shells. For example, "*.py" would be converted to "'*.py'". If the text is considered safe it is returned unquoted. Args: s (str): The value to quote Returns: str: A safe version of the input, from the point of view of POSIX command shells. """ if not isinstance(s, string_types): # pragma: no cover raise TypeError('Expected string type, got %s' % type(s)) if not s: # pragma: no cover result = "''" elif not UNSAFE.search(s): # pragma: no cover result = s else: result = "'%s'" % s.replace("'", r"'\''") return result # end of sarge code # Now that we use shell=False, we shouldn't need to quote arguments. # Use no_quote instead of shell_quote to remind us of where quoting # was needed. However, note that we still need, on 2.x, to encode any # Unicode argument with the file system encoding - see Issue #41 and # Python issue #1759845 ("subprocess.call fails with unicode strings in # command line"). # Allows the encoding used to be overridden in special cases by setting # this module attribute appropriately. fsencoding = sys.getfilesystemencoding() def no_quote(s): """ Legacy function which is a no-op on Python 3. """ if not _py3k and isinstance(s, text_type): s = s.encode(fsencoding) return s def _copy_data(instream, outstream, buffer_size, error_queue): # Copy one stream to another assert buffer_size > 0 sent = 0 if hasattr(sys.stdin, 'encoding'): enc = sys.stdin.encoding else: # pragma: no cover enc = 'ascii' while True: # See issue #39: read can fail when e.g. a text stream is provided # for what is actually a binary file try: data = instream.read(buffer_size) except Exception as e: # pragma: no cover logger.warning('Exception occurred while reading', exc_info=1) error_queue.put_nowait(e) break if not data: break sent += len(data) # logger.debug('sending chunk (%d): %r', sent, data[:256]) try: outstream.write(data) except UnicodeError: # pragma: no cover outstream.write(data.encode(enc)) except Exception as e: # pragma: no cover # Can sometimes get 'broken pipe' errors even when the data has all # been sent logger.exception('Error sending data') error_queue.put_nowait(e) break try: outstream.close() except IOError: # pragma: no cover logger.warning('Exception occurred while closing: ignored', exc_info=1) logger.debug('closed output, %d bytes sent', sent) def _threaded_copy_data(instream, outstream, buffer_size, error_queue): assert buffer_size > 0 wr = threading.Thread(target=_copy_data, args=(instream, outstream, buffer_size, error_queue)) wr.daemon = True logger.debug('data copier: %r, %r, %r', wr, instream, outstream) wr.start() return wr def _write_passphrase(stream, passphrase, encoding): passphrase = '%s\n' % passphrase passphrase = passphrase.encode(encoding) stream.write(passphrase) logger.debug('Wrote passphrase') def _is_sequence(instance): return isinstance(instance, (list, tuple, set, frozenset)) def _make_memory_stream(s): try: from io import BytesIO rv = BytesIO(s) except ImportError: # pragma: no cover rv = StringIO(s) return rv def _make_binary_stream(s, encoding): if _py3k: if isinstance(s, str): s = s.encode(encoding) else: if type(s) is not str: s = s.encode(encoding) return _make_memory_stream(s) class StatusHandler(object): """ The base class for handling status messages from `gpg`. """ on_data_failure = None # set at instance level when failures occur def __init__(self, gpg): """ Initialize an instance. Args: gpg (GPG): The :class:`GPG` instance in use. """ self.gpg = gpg def handle_status(self, key, value): """ Handle status messages from the `gpg` child process. These are lines of the format [GNUPG:] Args: key (str): Identifies what the status message is. value (str): Identifies additional data, which differs depending on the key. """ raise NotImplementedError class Verify(StatusHandler): """ This class handles status messages during signature verificaton. """ TRUST_EXPIRED = 0 TRUST_UNDEFINED = 1 TRUST_NEVER = 2 TRUST_MARGINAL = 3 TRUST_FULLY = 4 TRUST_ULTIMATE = 5 TRUST_LEVELS = { 'TRUST_EXPIRED': TRUST_EXPIRED, 'TRUST_UNDEFINED': TRUST_UNDEFINED, 'TRUST_NEVER': TRUST_NEVER, 'TRUST_MARGINAL': TRUST_MARGINAL, 'TRUST_FULLY': TRUST_FULLY, 'TRUST_ULTIMATE': TRUST_ULTIMATE, } # for now, just the most common error codes. This can be expanded as and # when reports come in of other errors. GPG_SYSTEM_ERROR_CODES = { 1: 'permission denied', 35: 'file exists', 81: 'file not found', 97: 'not a directory', } GPG_ERROR_CODES = { 11: 'incorrect passphrase', } returncode = None def __init__(self, gpg): StatusHandler.__init__(self, gpg) self.valid = False self.fingerprint = self.creation_date = self.timestamp = None self.signature_id = self.key_id = None self.username = None self.key_id = None self.key_status = None self.status = None self.pubkey_fingerprint = None self.expire_timestamp = None self.sig_timestamp = None self.trust_text = None self.trust_level = None self.sig_info = {} self.problems = [] def __nonzero__(self): # pragma: no cover return self.valid __bool__ = __nonzero__ def handle_status(self, key, value): def update_sig_info(**kwargs): sig_id = self.signature_id if sig_id: info = self.sig_info[sig_id] info.update(kwargs) else: logger.debug('Ignored due to missing sig iD: %s', kwargs) if key in self.TRUST_LEVELS: self.trust_text = key self.trust_level = self.TRUST_LEVELS[key] update_sig_info(trust_level=self.trust_level, trust_text=self.trust_text) # See Issue #214. Once we see this, we're done with the signature just seen. # Zap the signature ID, because we don't see a SIG_ID unless we have a new # good signature. self.signature_id = None elif key in ('WARNING', 'ERROR'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) elif key == 'BADSIG': # pragma: no cover self.valid = False self.status = 'signature bad' self.key_id, self.username = value.split(None, 1) self.problems.append({'status': self.status, 'keyid': self.key_id, 'user': self.username}) update_sig_info(keyid=self.key_id, username=self.username, status=self.status) elif key == 'ERRSIG': # pragma: no cover self.valid = False parts = value.split() (self.key_id, algo, hash_algo, cls, self.timestamp) = parts[:5] # Since GnuPG 2.2.7, a fingerprint is tacked on if len(parts) >= 7: self.fingerprint = parts[6] self.status = 'signature error' update_sig_info(keyid=self.key_id, timestamp=self.timestamp, fingerprint=self.fingerprint, status=self.status) self.problems.append({ 'status': self.status, 'keyid': self.key_id, 'timestamp': self.timestamp, 'fingerprint': self.fingerprint }) elif key == 'EXPSIG': # pragma: no cover self.valid = False self.status = 'signature expired' self.key_id, self.username = value.split(None, 1) update_sig_info(keyid=self.key_id, username=self.username, status=self.status) self.problems.append({'status': self.status, 'keyid': self.key_id, 'user': self.username}) elif key == 'GOODSIG': self.valid = True self.status = 'signature good' self.key_id, self.username = value.split(None, 1) update_sig_info(keyid=self.key_id, username=self.username, status=self.status) elif key == 'VALIDSIG': parts = value.split() fingerprint, creation_date, sig_ts, expire_ts = parts[:4] (self.fingerprint, self.creation_date, self.sig_timestamp, self.expire_timestamp) = (fingerprint, creation_date, sig_ts, expire_ts) # may be different if signature is made with a subkey if len(parts) >= 10: self.pubkey_fingerprint = parts[9] self.status = 'signature valid' update_sig_info(fingerprint=fingerprint, creation_date=creation_date, timestamp=sig_ts, expiry=expire_ts, pubkey_fingerprint=self.pubkey_fingerprint, status=self.status) elif key == 'SIG_ID': sig_id, creation_date, timestamp = value.split() self.sig_info[sig_id] = {'creation_date': creation_date, 'timestamp': timestamp} (self.signature_id, self.creation_date, self.timestamp) = (sig_id, creation_date, timestamp) elif key == 'NO_PUBKEY': # pragma: no cover self.valid = False self.key_id = value self.status = 'no public key' self.problems.append({'status': self.status, 'keyid': self.key_id}) elif key == 'NO_SECKEY': # pragma: no cover self.valid = False self.key_id = value self.status = 'no secret key' self.problems.append({'status': self.status, 'keyid': self.key_id}) elif key in ('EXPKEYSIG', 'REVKEYSIG'): # pragma: no cover # signed with expired or revoked key self.valid = False self.key_id, self.username = value.split(None, 1) if key == 'EXPKEYSIG': self.key_status = 'signing key has expired' else: self.key_status = 'signing key was revoked' self.status = self.key_status update_sig_info(status=self.status, keyid=self.key_id) self.problems.append({'status': self.status, 'keyid': self.key_id}) elif key in ('UNEXPECTED', 'FAILURE'): # pragma: no cover self.valid = False if key == 'UNEXPECTED': self.status = 'unexpected data' else: # N.B. there might be other reasons. For example, if an output # file can't be created - /dev/null/foo will lead to a # "not a directory" error, but which is not sent as a status # message with the [GNUPG:] prefix. Similarly if you try to # write to "/etc/foo" as a non-root user, a "permission denied" # error will be sent as a non-status message. message = 'error - %s' % value operation, code = value.rsplit(' ', 1) if code.isdigit(): code = int(code) & 0xFFFFFF # lose the error source if self.gpg.error_map and code in self.gpg.error_map: message = '%s: %s' % (operation, self.gpg.error_map[code]) else: system_error = bool(code & 0x8000) code = code & 0x7FFF if system_error: mapping = self.GPG_SYSTEM_ERROR_CODES else: mapping = self.GPG_ERROR_CODES if code in mapping: message = '%s: %s' % (operation, mapping[code]) if not self.status: self.status = message elif key == 'NODATA': # pragma: no cover # See issue GH-191 self.valid = False self.status = 'signature expected but not found' elif key in ('DECRYPTION_INFO', 'PLAINTEXT', 'PLAINTEXT_LENGTH', 'BEGIN_SIGNING', 'KEY_CONSIDERED'): pass elif key in ('NEWSIG', ): # Only sent in gpg2. Clear any signature ID, to be set by a following SIG_ID self.signature_id = None else: # pragma: no cover logger.debug('message ignored: %r, %r', key, value) class ImportResult(StatusHandler): """ This class handles status messages during key import. """ counts = '''count no_user_id imported imported_rsa unchanged n_uids n_subk n_sigs n_revoc sec_read sec_imported sec_dups not_imported'''.split() returncode = None def __init__(self, gpg): StatusHandler.__init__(self, gpg) self.results = [] self.fingerprints = [] for result in self.counts: setattr(self, result, 0) def __nonzero__(self): return bool(not self.not_imported and self.fingerprints) __bool__ = __nonzero__ ok_reason = { '0': 'Not actually changed', '1': 'Entirely new key', '2': 'New user IDs', '4': 'New signatures', '8': 'New subkeys', '16': 'Contains private key', } problem_reason = { '0': 'No specific reason given', '1': 'Invalid Certificate', '2': 'Issuer Certificate missing', '3': 'Certificate Chain too long', '4': 'Error storing certificate', } def handle_status(self, key, value): if key in ('WARNING', 'ERROR'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) elif key in ('IMPORTED', 'KEY_CONSIDERED'): # this duplicates info we already see in import_ok & import_problem pass elif key == 'NODATA': # pragma: no cover self.results.append({'fingerprint': None, 'problem': '0', 'text': 'No valid data found'}) elif key == 'IMPORT_OK': reason, fingerprint = value.split() reasons = [] for code, text in list(self.ok_reason.items()): if int(reason) | int(code) == int(reason): reasons.append(text) reasontext = '\n'.join(reasons) + '\n' self.results.append({'fingerprint': fingerprint, 'ok': reason, 'text': reasontext}) self.fingerprints.append(fingerprint) elif key == 'IMPORT_PROBLEM': # pragma: no cover try: reason, fingerprint = value.split() except Exception: reason = value fingerprint = '' self.results.append({'fingerprint': fingerprint, 'problem': reason, 'text': self.problem_reason[reason]}) elif key == 'IMPORT_RES': import_res = value.split() for i, count in enumerate(self.counts): setattr(self, count, int(import_res[i])) elif key == 'KEYEXPIRED': # pragma: no cover self.results.append({'fingerprint': None, 'problem': '0', 'text': 'Key expired'}) elif key == 'SIGEXPIRED': # pragma: no cover self.results.append({'fingerprint': None, 'problem': '0', 'text': 'Signature expired'}) elif key == 'FAILURE': # pragma: no cover self.results.append({'fingerprint': None, 'problem': '0', 'text': 'Other failure'}) else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) def summary(self): """ Return a summary indicating how many keys were imported and how many were not imported. """ result = [] result.append('%d imported' % self.imported) if self.not_imported: # pragma: no cover result.append('%d not imported' % self.not_imported) return ', '.join(result) ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) BASIC_ESCAPES = { r'\n': '\n', r'\r': '\r', r'\f': '\f', r'\v': '\v', r'\b': '\b', r'\0': '\0', } class SendResult(StatusHandler): """ This class handles status messages during key sending. """ returncode = None def handle_status(self, key, value): logger.debug('SendResult: %s: %s', key, value) def _set_fields(target, fieldnames, args): for i, var in enumerate(fieldnames): if i < len(args): target[var] = args[i] else: target[var] = 'unavailable' class SearchKeys(StatusHandler, list): """ This class handles status messages during key search. """ # Handle pub and uid (relating the latter to the former). # Don't care about the rest UID_INDEX = 1 FIELDS = 'type keyid algo length date expires'.split() returncode = None def __init__(self, gpg): StatusHandler.__init__(self, gpg) self.curkey = None self.fingerprints = [] self.uids = [] self.uid_map = {} def get_fields(self, args): """ Internal method used to update the instance from a `gpg` status message. """ result = {} _set_fields(result, self.FIELDS, args) result['uids'] = [] result['sigs'] = [] return result def pub(self, args): """ Internal method used to update the instance from a `gpg` status message. """ self.curkey = curkey = self.get_fields(args) self.append(curkey) def uid(self, args): """ Internal method used to update the instance from a `gpg` status message. """ uid = args[self.UID_INDEX] uid = ESCAPE_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), uid) for k, v in BASIC_ESCAPES.items(): uid = uid.replace(k, v) self.curkey['uids'].append(uid) self.uids.append(uid) uid_data = {} self.uid_map[uid] = uid_data for fn, fv in zip(self.FIELDS, args): uid_data[fn] = fv def handle_status(self, key, value): # pragma: no cover pass class ListKeys(SearchKeys): """ This class handles status messages during listing keys and signatures. Handle pub and uid (relating the latter to the former). We don't care about (info from GnuPG DETAILS file): crt = X.509 certificate crs = X.509 certificate and private key available uat = user attribute (same as user id except for field 10). sig = signature rev = revocation signature pkd = public key data (special field format, see below) grp = reserved for gpgsm rvk = revocation key """ UID_INDEX = 9 FIELDS = ('type trust length algo keyid date expires dummy ownertrust uid sig' ' cap issuer flag token hash curve compliance updated origin keygrip').split() def __init__(self, gpg): super(ListKeys, self).__init__(gpg) self.in_subkey = False self.key_map = {} def key(self, args): """ Internal method used to update the instance from a `gpg` status message. """ self.curkey = curkey = self.get_fields(args) if curkey['uid']: # pragma: no cover curkey['uids'].append(curkey['uid']) del curkey['uid'] curkey['subkeys'] = [] self.append(curkey) self.in_subkey = False pub = sec = key def fpr(self, args): """ Internal method used to update the instance from a `gpg` status message. """ fp = args[9] if fp in self.key_map and self.gpg.check_fingerprint_collisions: # pragma: no cover raise ValueError('Unexpected fingerprint collision: %s' % fp) if not self.in_subkey: self.curkey['fingerprint'] = fp self.fingerprints.append(fp) self.key_map[fp] = self.curkey else: self.curkey['subkeys'][-1][2] = fp self.key_map[fp] = self.curkey def grp(self, args): """ Internal method used to update the instance from a `gpg` status message. """ grp = args[9] if not self.in_subkey: self.curkey['keygrip'] = grp else: self.curkey['subkeys'][-1][3] = grp def _collect_subkey_info(self, curkey, args): info_map = curkey.setdefault('subkey_info', {}) info = {} _set_fields(info, self.FIELDS, args) info_map[args[4]] = info def sub(self, args): """ Internal method used to update the instance from a `gpg` status message. """ # See issue #81. We create a dict with more information about # subkeys, but for backward compatibility reason, have to add it in # as a separate entry 'subkey_info' subkey = [args[4], args[11], None, None] # keyid, type, fp, grp self.curkey['subkeys'].append(subkey) self._collect_subkey_info(self.curkey, args) self.in_subkey = True def ssb(self, args): """ Internal method used to update the instance from a `gpg` status message. """ subkey = [args[4], None, None, None] # keyid, type, fp, grp self.curkey['subkeys'].append(subkey) self._collect_subkey_info(self.curkey, args) self.in_subkey = True def sig(self, args): """ Internal method used to update the instance from a `gpg` status message. """ # keyid, uid, sigclass self.curkey['sigs'].append((args[4], args[9], args[10])) class ScanKeys(ListKeys): """ This class handles status messages during scanning keys. """ def sub(self, args): """ Internal method used to update the instance from a `gpg` status message. """ # --with-fingerprint --with-colons somehow outputs fewer colons, # use the last value args[-1] instead of args[11] subkey = [args[4], args[-1], None, None] self.curkey['subkeys'].append(subkey) self._collect_subkey_info(self.curkey, args) self.in_subkey = True class TextHandler(object): def _as_text(self): return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) if _py3k: __str__ = _as_text else: __unicode__ = _as_text def __str__(self): return self.data _INVALID_KEY_REASONS = { 0: 'no specific reason given', 1: 'not found', 2: 'ambiguous specification', 3: 'wrong key usage', 4: 'key revoked', 5: 'key expired', 6: 'no crl known', 7: 'crl too old', 8: 'policy mismatch', 9: 'not a secret key', 10: 'key not trusted', 11: 'missing certificate', 12: 'missing issuer certificate', 13: 'key disabled', 14: 'syntax error in specification', } def _determine_invalid_recipient_or_signer(s): # pragma: no cover parts = s.split() if len(parts) >= 2: code, ident = parts[:2] else: code = parts[0] ident = '' unexpected = 'unexpected return code %r' % code try: key = int(code) result = _INVALID_KEY_REASONS.get(key, unexpected) except ValueError: result = unexpected return '%s:%s' % (result, ident) class Crypt(Verify, TextHandler): """ This class handles status messages during encryption and decryption. """ def __init__(self, gpg): Verify.__init__(self, gpg) self.data = '' self.ok = False self.status = '' self.status_detail = '' self.key_id = None def __nonzero__(self): return bool(self.ok) __bool__ = __nonzero__ def handle_status(self, key, value): if key in ('WARNING', 'ERROR'): logger.warning('potential problem: %s: %s', key, value) elif key == 'NODATA': if self.status not in ('decryption failed', ): self.status = 'no data was provided' elif key in ('NEED_PASSPHRASE', 'BAD_PASSPHRASE', 'GOOD_PASSPHRASE', 'MISSING_PASSPHRASE', 'KEY_NOT_CREATED', 'NEED_PASSPHRASE_PIN'): # pragma: no cover self.status = key.replace('_', ' ').lower() elif key == 'DECRYPTION_FAILED': # pragma: no cover if self.status != 'no secret key': # don't overwrite more useful message self.status = 'decryption failed' elif key == 'NEED_PASSPHRASE_SYM': self.status = 'need symmetric passphrase' elif key == 'BEGIN_DECRYPTION': if self.status != 'no secret key': # don't overwrite more useful message self.status = 'decryption incomplete' elif key == 'BEGIN_ENCRYPTION': self.status = 'encryption incomplete' elif key == 'DECRYPTION_OKAY': self.status = 'decryption ok' self.ok = True elif key == 'END_ENCRYPTION': self.status = 'encryption ok' self.ok = True elif key == 'INV_RECP': # pragma: no cover if not self.status: self.status = 'invalid recipient' else: self.status = 'invalid recipient: %s' % self.status self.status_detail = _determine_invalid_recipient_or_signer(value) elif key == 'KEYEXPIRED': # pragma: no cover self.status = 'key expired' elif key == 'SIG_CREATED': # pragma: no cover self.status = 'sig created' elif key == 'SIGEXPIRED': # pragma: no cover self.status = 'sig expired' elif key == 'ENC_TO': # pragma: no cover # ENC_TO self.key_id = value.split(' ', 1)[0] elif key in ('USERID_HINT', 'GOODMDC', 'END_DECRYPTION', 'CARDCTRL', 'BADMDC', 'SC_OP_FAILURE', 'SC_OP_SUCCESS', 'PINENTRY_LAUNCHED'): pass else: Verify.handle_status(self, key, value) class GenKey(StatusHandler): """ This class handles status messages during key generation. """ returncode = None def __init__(self, gpg): StatusHandler.__init__(self, gpg) self.type = None self.fingerprint = '' self.status = None def __nonzero__(self): # pragma: no cover return bool(self.fingerprint) __bool__ = __nonzero__ def __str__(self): # pragma: no cover return self.fingerprint def handle_status(self, key, value): if key in ('WARNING', 'ERROR'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) elif key == 'KEY_CREATED': parts = value.split() (self.type, self.fingerprint) = parts[:2] self.status = 'ok' elif key == 'KEY_NOT_CREATED': self.status = key.replace('_', ' ').lower() elif key in ('PROGRESS', 'GOOD_PASSPHRASE'): # pragma: no cover pass else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) class AddSubkey(StatusHandler): """ This class handles status messages during subkey addition. """ returncode = None def __init__(self, gpg): StatusHandler.__init__(self, gpg) self.type = None self.fingerprint = '' self.status = None def __nonzero__(self): # pragma: no cover return bool(self.fingerprint) __bool__ = __nonzero__ def __str__(self): return self.fingerprint def handle_status(self, key, value): if key in ('WARNING', 'ERROR'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) elif key == 'KEY_CREATED': (self.type, self.fingerprint) = value.split() self.status = 'ok' else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) class ExportResult(GenKey): """ This class handles status messages during key export. """ # For now, just use an existing class to base it on - if needed, we # can override handle_status for more specific message handling. def handle_status(self, key, value): if key in ('EXPORTED', 'EXPORT_RES'): pass else: super(ExportResult, self).handle_status(key, value) class DeleteResult(StatusHandler): """ This class handles status messages during key deletion. """ returncode = None def __init__(self, gpg): StatusHandler.__init__(self, gpg) self.status = 'ok' def __str__(self): return self.status problem_reason = { '1': 'No such key', '2': 'Must delete secret key first', '3': 'Ambiguous specification', } def handle_status(self, key, value): if key == 'DELETE_PROBLEM': # pragma: no cover self.status = self.problem_reason.get(value, 'Unknown error: %r' % value) else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) def __nonzero__(self): # pragma: no cover return self.status == 'ok' __bool__ = __nonzero__ class TrustResult(DeleteResult): """ This class handles status messages during key trust setting. """ pass class Sign(StatusHandler, TextHandler): """ This class handles status messages during signing. """ returncode = None def __init__(self, gpg): StatusHandler.__init__(self, gpg) self.type = None self.hash_algo = None self.fingerprint = None self.status = None self.status_detail = None self.key_id = None self.username = None def __nonzero__(self): return self.fingerprint is not None __bool__ = __nonzero__ def handle_status(self, key, value): if key in ('WARNING', 'ERROR', 'FAILURE'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) elif key in ('KEYEXPIRED', 'SIGEXPIRED'): # pragma: no cover self.status = 'key expired' elif key == 'KEYREVOKED': # pragma: no cover self.status = 'key revoked' elif key == 'SIG_CREATED': (self.type, algo, self.hash_algo, cls, self.timestamp, self.fingerprint) = value.split() self.status = 'signature created' elif key == 'USERID_HINT': # pragma: no cover self.key_id, self.username = value.split(' ', 1) elif key == 'BAD_PASSPHRASE': # pragma: no cover self.status = 'bad passphrase' elif key in ('INV_SGNR', 'INV_RECP'): # pragma: no cover # INV_RECP is returned in older versions if not self.status: self.status = 'invalid signer' else: self.status = 'invalid signer: %s' % self.status self.status_detail = _determine_invalid_recipient_or_signer(value) elif key in ('NEED_PASSPHRASE', 'GOOD_PASSPHRASE', 'BEGIN_SIGNING'): pass else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) class AutoLocateKey(StatusHandler): """ This class handles status messages during key auto-locating. fingerprint: str key_length: int created_at: date email: str email_real_name: str """ def __init__(self, gpg): StatusHandler.__init__(self, gpg) self.fingerprint = None self.type = None self.created_at = None self.email = None self.email_real_name = None def handle_status(self, key, value): if key == "IMPORTED": _, email, display_name = value.split() self.email = email self.email_real_name = display_name[1:-1] elif key == "KEY_CONSIDERED": self.fingerprint = value.strip().split()[0] def pub(self, args): """ Internal method to handle the 'pub' status message. `pub` message contains the fingerprint of the public key, its type and its creation date. """ pass def uid(self, args): self.created_at = datetime.fromtimestamp(int(args[5])) raw_email_content = args[9] email, real_name = parseaddr(raw_email_content) self.email = email self.email_real_name = real_name def sub(self, args): self.key_length = int(args[2]) def fpr(self, args): # Only store the first fingerprint self.fingerprint = self.fingerprint or args[9] VERSION_RE = re.compile(r'\bcfg:version:(\d+(\.\d+)*)'.encode('ascii')) HEX_DIGITS_RE = re.compile(r'[0-9a-f]+$', re.I) PUBLIC_KEY_RE = re.compile(r'gpg: public key is (\w+)') class GPG(object): """ This class provides a high-level programmatic interface for `gpg`. """ error_map = None decode_errors = 'strict' buffer_size = 16384 # override in instance if needed result_map = { 'crypt': Crypt, 'delete': DeleteResult, 'generate': GenKey, 'addSubkey': AddSubkey, 'import': ImportResult, 'send': SendResult, 'list': ListKeys, 'scan': ScanKeys, 'search': SearchKeys, 'sign': Sign, 'trust': TrustResult, 'verify': Verify, 'export': ExportResult, 'auto-locate-key': AutoLocateKey, } "A map of GPG operations to result object types." def __init__(self, gpgbinary='gpg', gnupghome=None, verbose=False, use_agent=False, keyring=None, options=None, secret_keyring=None, env=None): """Initialize a GPG process wrapper. Args: gpgbinary (str): A pathname for the GPG binary to use. gnupghome (str): A pathname to where we can find the public and private keyrings. The default is whatever `gpg` defaults to. keyring (str|list): The name of alternative keyring file to use, or a list of such keyring files. If specified, the default keyring is not used. options (list): A list of additional options to pass to the GPG binary. secret_keyring (str|list): The name of an alternative secret keyring file to use, or a list of such keyring files. env (dict): A dict of environment variables to be used for the GPG subprocess. """ self.gpgbinary = gpgbinary self.gnupghome = gnupghome self.env = env # issue 112: fail if the specified value isn't a directory if gnupghome and not os.path.isdir(gnupghome): raise ValueError('gnupghome should be a directory (it isn\'t): %s' % gnupghome) if keyring: # Allow passing a string or another iterable. Make it uniformly # a list of keyring filenames if isinstance(keyring, string_types): keyring = [keyring] self.keyring = keyring if secret_keyring: # pragma: no cover # Allow passing a string or another iterable. Make it uniformly # a list of keyring filenames if isinstance(secret_keyring, string_types): secret_keyring = [secret_keyring] self.secret_keyring = secret_keyring self.verbose = verbose self.use_agent = use_agent if isinstance(options, str): # pragma: no cover options = [options] self.options = options self.on_data = None # or a callable - will be called with data chunks # Changed in 0.3.7 to use Latin-1 encoding rather than # locale.getpreferredencoding falling back to sys.stdin.encoding # falling back to utf-8, because gpg itself uses latin-1 as the default # encoding. self.encoding = 'latin-1' if gnupghome and not os.path.isdir(self.gnupghome): # pragma: no cover os.makedirs(self.gnupghome, 0o700) try: p = self._open_subprocess(['--list-config', '--with-colons']) except OSError: msg = 'Unable to run gpg (%s) - it may not be available.' % self.gpgbinary logger.exception(msg) raise OSError(msg) result = self.result_map['verify'](self) # any result will do for this self._collect_output(p, result, stdin=p.stdin) if p.returncode != 0: # pragma: no cover raise ValueError('Error invoking gpg: %s: %s' % (p.returncode, result.stderr)) m = VERSION_RE.search(result.data) if not m: # pragma: no cover self.version = None else: dot = '.'.encode('ascii') self.version = tuple([int(s) for s in m.groups()[0].split(dot)]) # See issue #97. It seems gpg allow duplicate keys in keyrings, so we # can't be too strict. self.check_fingerprint_collisions = False def make_args(self, args, passphrase): """ Make a list of command line elements for GPG. The value of ``args`` will be appended. The ``passphrase`` argument needs to be True if a passphrase will be sent to `gpg`, else False. Args: args (list[str]): A list of arguments. passphrase (str): The passphrase to use. """ cmd = [self.gpgbinary, '--status-fd', '2', '--no-tty', '--no-verbose'] if 'DEBUG_IPC' in os.environ: # pragma: no cover cmd.extend(['--debug', 'ipc']) if passphrase and hasattr(self, 'version'): if self.version >= (2, 1): cmd[1:1] = ['--pinentry-mode', 'loopback'] cmd.extend(['--fixed-list-mode', '--batch', '--with-colons']) if self.gnupghome: cmd.extend(['--homedir', no_quote(self.gnupghome)]) if self.keyring: cmd.append('--no-default-keyring') for fn in self.keyring: cmd.extend(['--keyring', no_quote(fn)]) if self.secret_keyring: # pragma: no cover for fn in self.secret_keyring: cmd.extend(['--secret-keyring', no_quote(fn)]) if passphrase: cmd.extend(['--passphrase-fd', '0']) if self.use_agent: # pragma: no cover cmd.append('--use-agent') if self.options: cmd.extend(self.options) cmd.extend(args) return cmd def _open_subprocess(self, args, passphrase=False): # Internal method: open a pipe to a GPG subprocess and return # the file objects for communicating with it. from subprocess import list2cmdline as debug_print cmd = self.make_args(args, passphrase) if self.verbose: # pragma: no cover print(debug_print(cmd)) if not STARTUPINFO: si = None else: # pragma: no cover si = STARTUPINFO() si.dwFlags = STARTF_USESHOWWINDOW si.wShowWindow = SW_HIDE result = Popen(cmd, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE, startupinfo=si, env=self.env) logger.debug('%s: %s', result.pid, debug_print(cmd)) return result def _read_response(self, stream, result): # Internal method: reads all the stderr output from GPG, taking notice # only of lines that begin with the magic [GNUPG:] prefix. # # Calls methods on the response object for each valid token found, # with the arg being the remainder of the status line. lines = [] while True: line = stream.readline() if len(line) == 0: break lines.append(line) line = line.rstrip() if self.verbose: # pragma: no cover print(line) logger.debug('%s', line) if line[0:9] == '[GNUPG:] ': # Chop off the prefix line = line[9:] L = line.split(None, 1) keyword = L[0] if len(L) > 1: value = L[1] else: value = '' result.handle_status(keyword, value) result.stderr = ''.join(lines) def _read_data(self, stream, result, on_data=None, buffer_size=1024): # Read the contents of the file from GPG's stdout assert buffer_size > 0 chunks = [] on_data_failure = None while True: data = stream.read(buffer_size) if len(data) == 0: if on_data: try: on_data(data) except Exception as e: if on_data_failure is None: on_data_failure = e break if log_everything: logger.debug('chunk: %r' % data[:256]) append = True if on_data: try: on_data_result = on_data(data) append = on_data_result is not False except Exception as e: if on_data_failure is None: on_data_failure = e if append: chunks.append(data) if _py3k: # Join using b'' or '', as appropriate result.data = type(data)().join(chunks) else: result.data = ''.join(chunks) if on_data_failure: result.on_data_failure = on_data_failure def _collect_output(self, process, result, writer=None, stdin=None): """ Drain the subprocesses output streams, writing the collected output to the result. If a writer thread (writing to the subprocess) is given, make sure it's joined before returning. If a stdin stream is given, close it before returning. """ stderr = codecs.getreader(self.encoding)(process.stderr) rr = threading.Thread(target=self._read_response, args=(stderr, result)) rr.daemon = True logger.debug('stderr reader: %r', rr) rr.start() stdout = process.stdout dr = threading.Thread(target=self._read_data, args=(stdout, result, self.on_data, self.buffer_size)) dr.daemon = True logger.debug('stdout reader: %r', dr) dr.start() dr.join() rr.join() if writer is not None: writer.join(0.01) process.wait() result.returncode = rc = process.returncode if rc != 0: logger.warning('gpg returned a non-zero error code: %d', rc) if stdin is not None: try: stdin.close() except IOError: # pragma: no cover pass stderr.close() stdout.close() return rc def is_valid_file(self, fileobj): """ A simplistic check for a file-like object. Args: fileobj (object): The object to test. Returns: bool: ``True`` if it's a file-like object, else ``False``. """ return hasattr(fileobj, 'read') def _get_fileobj(self, fileobj_or_path): if self.is_valid_file(fileobj_or_path): result = fileobj_or_path elif not isinstance(fileobj_or_path, path_types): raise TypeError('Not a valid file or path: %s' % fileobj_or_path) elif not os.path.exists(fileobj_or_path): raise ValueError('No such file: %s' % fileobj_or_path) else: result = open(fileobj_or_path, 'rb') return result def _handle_io(self, args, fileobj_or_path, result, passphrase=None, binary=False): "Handle a call to GPG - pass input data, collect output data" # Handle a basic data call - pass data to GPG, handle the output # including status information. Garbage In, Garbage Out :) fileobj = self._get_fileobj(fileobj_or_path) writer = None # See issue #237 try: p = self._open_subprocess(args, passphrase is not None) if not binary: # pragma: no cover stdin = codecs.getwriter(self.encoding)(p.stdin) else: stdin = p.stdin if passphrase: _write_passphrase(stdin, passphrase, self.encoding) error_queue = Queue() writer = _threaded_copy_data(fileobj, stdin, self.buffer_size, error_queue) self._collect_output(p, result, writer, stdin) try: exc = error_queue.get_nowait() # if we get here, that means an error occurred in the copying thread raise exc except Empty: pass return result finally: if writer: writer.join(0.01) if fileobj is not fileobj_or_path: fileobj.close() # # SIGNATURE METHODS # def sign(self, message, **kwargs): """ Sign a message. This method delegates most of the work to the `sign_file()` method. Args: message (str|bytes): The data to sign. kwargs (dict): Keyword arguments, which are passed to `sign_file()`: * keyid (str): The key id of the signer. * passphrase (str): The passphrase for the key. * clearsign (bool): Whether to use clear signing. * detach (bool): Whether to produce a detached signature. * binary (bool): Whether to produce a binary signature. * output (str): The path to write a detached signature to. * extra_args (list[str]): Additional arguments to pass to `gpg`. """ f = _make_binary_stream(message, self.encoding) result = self.sign_file(f, **kwargs) f.close() return result def set_output_without_confirmation(self, args, output): """ If writing to a file which exists, avoid a confirmation message by updating the *args* value in place to set the output path and avoid any cpmfirmation prompt. Args: args (list[str]): A list of arguments. output (str): The path to the outpur file. """ if os.path.exists(output): # We need to avoid an overwrite confirmation message args.extend(['--yes']) args.extend(['--output', no_quote(output)]) def is_valid_passphrase(self, passphrase): """ Confirm that the passphrase doesn't contain newline-type characters - it is passed in a pipe to `gpg`, and so not checking could lead to spoofing attacks by passing arbitrary text after passphrase and newline. Args: passphrase (str): The passphrase to test. Returns: bool: ``True`` if it's a valid passphrase, else ``False``. """ return ('\n' not in passphrase and '\r' not in passphrase and '\x00' not in passphrase) def sign_file(self, fileobj_or_path, keyid=None, passphrase=None, clearsign=True, detach=False, binary=False, output=None, extra_args=None): """ Sign data in a file or file-like object. Args: fileobj_or_path (str|file): The file or file-like object to sign. keyid (str): The key id of the signer. passphrase (str): The passphrase for the key. clearsign (bool): Whether to use clear signing. detach (bool): Whether to produce a detached signature. binary (bool): Whether to produce a binary signature. output (str): The path to write a detached signature to. extra_args (list[str]): Additional arguments to pass to `gpg`. """ if passphrase and not self.is_valid_passphrase(passphrase): raise ValueError('Invalid passphrase') logger.debug('sign_file: %s', fileobj_or_path) if binary: # pragma: no cover args = ['-s'] else: args = ['-sa'] # You can't specify detach-sign and clearsign together: gpg ignores # the detach-sign in that case. if detach: args.append('--detach-sign') elif clearsign: args.append('--clearsign') if keyid: args.extend(['--default-key', no_quote(keyid)]) if output: # pragma: no cover # write the output to a file with the specified name self.set_output_without_confirmation(args, output) if extra_args: # pragma: no cover args.extend(extra_args) result = self.result_map['sign'](self) # We could use _handle_io here except for the fact that if the # passphrase is bad, gpg bails and you can't write the message. fileobj = self._get_fileobj(fileobj_or_path) p = self._open_subprocess(args, passphrase is not None) writer = None try: stdin = p.stdin if passphrase: _write_passphrase(stdin, passphrase, self.encoding) error_queue = Queue() writer = _threaded_copy_data(fileobj, stdin, self.buffer_size, error_queue) try: exc = error_queue.get_nowait() # if we get here, that means an error occurred in the copying thread raise exc except Empty: pass except IOError: # pragma: no cover logging.exception('error writing message') finally: if writer: writer.join(0.01) if fileobj is not fileobj_or_path: fileobj.close() self._collect_output(p, result, writer, stdin) return result def verify(self, data, **kwargs): """ Verify the signature on the contents of the string *data*. This method delegates most of the work to `verify_file()`. Args: data (str|bytes): The data to verify. kwargs (dict): Keyword arguments, which are passed to `verify_file()`: * fileobj_or_path (str|file): A path to a signature, or a file-like object containing one. * data_filename (str): If the signature is a detached one, the path to the data that was signed. * close_file (bool): If a file-like object is passed in, whether to close it. * extra_args (list[str]): Additional arguments to pass to `gpg`. """ f = _make_binary_stream(data, self.encoding) result = self.verify_file(f, **kwargs) f.close() return result def verify_file(self, fileobj_or_path, data_filename=None, close_file=True, extra_args=None): """ Verify a signature. Args: fileobj_or_path (str|file): A path to a signature, or a file-like object containing one. data_filename (str): If the signature is a detached one, the path to the data that was signed. close_file (bool): If a file-like object is passed in, whether to close it. extra_args (list[str]): Additional arguments to pass to `gpg`. """ logger.debug('verify_file: %r, %r', fileobj_or_path, data_filename) result = self.result_map['verify'](self) args = ['--verify'] if extra_args: # pragma: no cover args.extend(extra_args) if data_filename is None: self._handle_io(args, fileobj_or_path, result, binary=True) else: logger.debug('Handling detached verification') import tempfile fileobj = self._get_fileobj(fileobj_or_path) fd, fn = tempfile.mkstemp(prefix='pygpg-') s = fileobj.read() if fileobj is not fileobj_or_path: fileobj.close() elif close_file: fileobj_or_path.close() logger.debug('Wrote to temp file: %r', s) os.write(fd, s) os.close(fd) args.append(no_quote(fn)) args.append(no_quote(data_filename)) try: p = self._open_subprocess(args) self._collect_output(p, result, stdin=p.stdin) finally: os.remove(fn) return result def verify_data(self, sig_filename, data, extra_args=None): """ Verify the signature in sig_filename against data in memory Args: sig_filename (str): The path to a signature. data (str|bytes): The data to be verified. extra_args (list[str]): Additional arguments to pass to `gpg`. """ logger.debug('verify_data: %r, %r ...', sig_filename, data[:16]) result = self.result_map['verify'](self) args = ['--verify'] if extra_args: # pragma: no cover args.extend(extra_args) args.extend([no_quote(sig_filename), '-']) stream = _make_memory_stream(data) self._handle_io(args, stream, result, binary=True) return result # # KEY MANAGEMENT # def import_keys(self, key_data, extra_args=None, passphrase=None): """ Import the key_data into our keyring. Args: key_data (str|bytes): The key data to import. passphrase (str): The passphrase to use. extra_args (list[str]): Additional arguments to pass to `gpg`. """ result = self.result_map['import'](self) logger.debug('import_keys: %r', key_data[:256]) data = _make_binary_stream(key_data, self.encoding) args = ['--import'] if extra_args: # pragma: no cover args.extend(extra_args) self._handle_io(args, data, result, passphrase=passphrase, binary=True) logger.debug('import_keys result: %r', result.__dict__) data.close() return result def import_keys_file(self, key_path, **kwargs): """ Import the key data in key_path into our keyring. Args: key_path (str): A path to the key data to be imported. """ with open(key_path, 'rb') as f: return self.import_keys(f.read(), **kwargs) def recv_keys(self, keyserver, *keyids, **kwargs): """ Import one or more keys from a keyserver. Args: keyserver (str): The key server hostname. keyids (str): A list of key ids to receive. """ result = self.result_map['import'](self) logger.debug('recv_keys: %r', keyids) data = _make_binary_stream('', self.encoding) args = ['--keyserver', no_quote(keyserver)] if 'extra_args' in kwargs: # pragma: no cover args.extend(kwargs['extra_args']) args.append('--recv-keys') args.extend([no_quote(k) for k in keyids]) self._handle_io(args, data, result, binary=True) logger.debug('recv_keys result: %r', result.__dict__) data.close() return result # This function isn't exercised by tests, to avoid polluting external # key servers with test keys def send_keys(self, keyserver, *keyids, **kwargs): # pragma: no cover """ Send one or more keys to a keyserver. Args: keyserver (str): The key server hostname. keyids (list[str]): A list of key ids to send. """ # Note: it's not practical to test this function without sending # arbitrary data to live keyservers. result = self.result_map['send'](self) logger.debug('send_keys: %r', keyids) data = _make_binary_stream('', self.encoding) args = ['--keyserver', no_quote(keyserver)] if 'extra_args' in kwargs: args.extend(kwargs['extra_args']) args.append('--send-keys') args.extend([no_quote(k) for k in keyids]) self._handle_io(args, data, result, binary=True) logger.debug('send_keys result: %r', result.__dict__) data.close() return result def delete_keys(self, fingerprints, secret=False, passphrase=None, expect_passphrase=True, exclamation_mode=False): """ Delete the indicated keys. Args: fingerprints (str|list[str]): The keys to delete. secret (bool): Whether to delete secret keys. passphrase (str): The passphrase to use. expect_passphrase (bool): Whether a passphrase is expected. exclamation_mode (bool): If specified, a `'!'` is appended to each fingerprint. This deletes only a subkey or an entire key, depending on what the fingerprint refers to. .. note:: Passphrases Since GnuPG 2.1, you can't delete secret keys without providing a passphrase. However, if you're expecting the passphrase to go to `gpg` via pinentry, you should specify expect_passphrase=False. (It's only checked for GnuPG >= 2.1). """ if passphrase and not self.is_valid_passphrase(passphrase): # pragma: no cover raise ValueError('Invalid passphrase') which = 'key' if secret: # pragma: no cover if self.version >= (2, 1) and passphrase is None and expect_passphrase: raise ValueError('For GnuPG >= 2.1, deleting secret keys ' 'needs a passphrase to be provided') which = 'secret-key' if _is_sequence(fingerprints): # pragma: no cover fingerprints = [no_quote(s) for s in fingerprints] else: fingerprints = [no_quote(fingerprints)] if exclamation_mode: fingerprints = [f + '!' for f in fingerprints] args = ['--delete-%s' % which] if secret and self.version >= (2, 1): args.insert(0, '--yes') args.extend(fingerprints) result = self.result_map['delete'](self) if not secret or self.version < (2, 1): p = self._open_subprocess(args) self._collect_output(p, result, stdin=p.stdin) else: # Need to send in a passphrase. f = _make_binary_stream('', self.encoding) try: self._handle_io(args, f, result, passphrase=passphrase, binary=True) finally: f.close() return result def export_keys(self, keyids, secret=False, armor=True, minimal=False, passphrase=None, expect_passphrase=True, output=None): """ Export the indicated keys. A 'keyid' is anything `gpg` accepts. Args: keyids (str|list[str]): A single keyid or a list of them. secret (bool): Whether to export secret keys. armor (bool): Whether to ASCII-armor the output. minimal (bool): Whether to pass `--export-options export-minimal` to `gpg`. passphrase (str): The passphrase to use. expect_passphrase (bool): Whether a passphrase is expected. output (str): If specified, the path to write the exported key(s) to. .. note:: Passphrases Since GnuPG 2.1, you can't export secret keys without providing a passphrase. However, if you're expecting the passphrase to go to `gpg` via pinentry, you should specify expect_passphrase=False. (It's only checked for GnuPG >= 2.1). """ if passphrase and not self.is_valid_passphrase(passphrase): # pragma: no cover raise ValueError('Invalid passphrase') which = '' if secret: which = '-secret-key' if self.version >= (2, 1) and passphrase is None and expect_passphrase: # pragma: no cover raise ValueError('For GnuPG >= 2.1, exporting secret keys ' 'needs a passphrase to be provided') if _is_sequence(keyids): keyids = [no_quote(k) for k in keyids] else: keyids = [no_quote(keyids)] args = ['--export%s' % which] if armor: args.append('--armor') if minimal: # pragma: no cover args.extend(['--export-options', 'export-minimal']) if output: # pragma: no cover # write the output to a file with the specified name self.set_output_without_confirmation(args, output) args.extend(keyids) # gpg --export produces no status-fd output; stdout will be # empty in case of failure result = self.result_map['export'](self) if not secret or self.version < (2, 1): p = self._open_subprocess(args) self._collect_output(p, result, stdin=p.stdin) else: # Need to send in a passphrase. f = _make_binary_stream('', self.encoding) try: self._handle_io(args, f, result, passphrase=passphrase, binary=True) finally: f.close() logger.debug('export_keys result[:100]: %r', result.data[:100]) # Issue #49: Return bytes if armor not specified, else text result = result.data if armor: result = result.decode(self.encoding, self.decode_errors) return result def _decode_result(self, result): lines = result.data.decode(self.encoding, self.decode_errors).splitlines() valid_keywords = 'pub uid sec fpr sub ssb sig grp'.split() for line in lines: if self.verbose: # pragma: no cover print(line) logger.debug('line: %r', line.rstrip()) if not line: # pragma: no cover break L = line.strip().split(':') if not L: # pragma: no cover continue keyword = L[0] if keyword in valid_keywords: getattr(result, keyword)(L) return result def _get_list_output(self, p, kind): # Get the response information result = self.result_map[kind](self) self._collect_output(p, result, stdin=p.stdin) return self._decode_result(result) def list_keys(self, secret=False, keys=None, sigs=False): """ List the keys currently in the keyring. Args: secret (bool): Whether to list secret keys. keys (str|list[str]): A list of key ids to match. sigs (bool): Whether to include signature information. Returns: list[dict]: A list of dictionaries with key information. """ if secret: which = 'secret-keys' else: which = 'sigs' if sigs else 'keys' args = ['--list-%s' % which, '--fingerprint', '--fingerprint'] # get subkey FPs, too if self.version >= (2, 1): args.append('--with-keygrip') if keys: if isinstance(keys, string_types): keys = [keys] args.extend(keys) p = self._open_subprocess(args) result = self._get_list_output(p, 'list') # Fix up subkey_info with fingerprint and grip values for key in result: # import pdb; pdb.set_trace() subkeys = key['subkeys'] subkey_info = key.get('subkey_info') if subkey_info: for sk in subkeys: skid, capability, fp, grp = sk d = subkey_info[skid] d['capability'] = capability d['fingerprint'] = fp d['keygrip'] = grp return result def scan_keys(self, filename): """ List details of an ascii armored or binary key file without first importing it to the local keyring. Args: filename (str): The path to the file containing the key(s). .. warning:: Warning: Care is needed. The function works on modern GnuPG by running: $ gpg --dry-run --import-options import-show --import filename On older versions, it does the *much* riskier: $ gpg --with-fingerprint --with-colons filename """ if self.version >= (2, 1): args = ['--dry-run', '--import-options', 'import-show', '--import'] else: logger.warning('Trying to list packets, but if the file is not a ' 'keyring, might accidentally decrypt') args = ['--with-fingerprint', '--with-colons', '--fixed-list-mode'] args.append(no_quote(filename)) p = self._open_subprocess(args) return self._get_list_output(p, 'scan') def scan_keys_mem(self, key_data): """ List details of an ascii armored or binary key without first importing it to the local keyring. Args: key_data (str|bytes): The key data to import. .. warning:: Warning: Care is needed. The function works on modern GnuPG by running: $ gpg --dry-run --import-options import-show --import filename On older versions, it does the *much* riskier: $ gpg --with-fingerprint --with-colons filename """ result = self.result_map['scan'](self) logger.debug('scan_keys: %r', key_data[:256]) data = _make_binary_stream(key_data, self.encoding) if self.version >= (2, 1): args = ['--dry-run', '--import-options', 'import-show', '--import'] else: logger.warning('Trying to list packets, but if the file is not a ' 'keyring, might accidentally decrypt') args = ['--with-fingerprint', '--with-colons', '--fixed-list-mode'] self._handle_io(args, data, result, binary=True) logger.debug('scan_keys result: %r', result.__dict__) data.close() return self._decode_result(result) def search_keys(self, query, keyserver='pgp.mit.edu', extra_args=None): """ search a keyserver by query (using the `--search-keys` option). Args: query(str): The query to use. keyserver (str): The key server hostname. extra_args (list[str]): Additional arguments to pass to `gpg`. """ query = query.strip() if HEX_DIGITS_RE.match(query): query = '0x' + query args = ['--fingerprint', '--keyserver', no_quote(keyserver)] if extra_args: # pragma: no cover args.extend(extra_args) args.extend(['--search-keys', no_quote(query)]) p = self._open_subprocess(args) # Get the response information result = self.result_map['search'](self) self._collect_output(p, result, stdin=p.stdin) lines = result.data.decode(self.encoding, self.decode_errors).splitlines() valid_keywords = ['pub', 'uid'] for line in lines: if self.verbose: # pragma: no cover print(line) logger.debug('line: %r', line.rstrip()) if not line: # sometimes get blank lines on Windows continue L = line.strip().split(':') if not L: # pragma: no cover continue keyword = L[0] if keyword in valid_keywords: getattr(result, keyword)(L) return result def auto_locate_key(self, email, mechanisms=None, **kwargs): """ Auto locate a public key by `email`. Args: email (str): The email address to search for. mechanisms (list[str]): A list of mechanisms to use. Valid mechanisms can be found here https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html under "--auto-key-locate". Default: ['wkd', 'ntds', 'ldap', 'cert', 'dane', 'local'] """ mechanisms = mechanisms or ['wkd', 'ntds', 'ldap', 'cert', 'dane', 'local'] args = ['--auto-key-locate', ','.join(mechanisms), '--locate-keys', email] result = self.result_map['auto-locate-key'](self) if 'extra_args' in kwargs: args.extend(kwargs['extra_args']) process = self._open_subprocess(args) self._collect_output(process, result, stdin=process.stdin) self._decode_result(result) return result def gen_key(self, input): """ Generate a key; you might use `gen_key_input()` to create the input. Args: input (str): The input to the key creation operation. """ args = ['--gen-key'] result = self.result_map['generate'](self) f = _make_binary_stream(input, self.encoding) self._handle_io(args, f, result, binary=True) f.close() return result def gen_key_input(self, **kwargs): """ Generate `--gen-key` input (see `gpg` documentation in DETAILS). Args: kwargs (dict): A list of keyword arguments. Returns: str: A string suitable for passing to the `gen_key()` method. """ parms = {} no_protection = kwargs.pop('no_protection', False) for key, val in list(kwargs.items()): key = key.replace('_', '-').title() if str(val).strip(): # skip empty strings parms[key] = val parms.setdefault('Key-Type', 'RSA') if 'key_curve' not in kwargs: parms.setdefault('Key-Length', 2048) parms.setdefault('Name-Real', 'Autogenerated Key') logname = (os.environ.get('LOGNAME') or os.environ.get('USERNAME') or 'unspecified') hostname = socket.gethostname() parms.setdefault('Name-Email', '%s@%s' % (logname.replace(' ', '_'), hostname)) out = 'Key-Type: %s\n' % parms.pop('Key-Type') for key, val in list(parms.items()): out += '%s: %s\n' % (key, val) if no_protection: # pragma: no cover out += '%no-protection\n' out += '%commit\n' return out # Key-Type: RSA # Key-Length: 1024 # Name-Real: ISdlink Server on %s # Name-Comment: Created by %s # Name-Email: isdlink@%s # Expire-Date: 0 # %commit # # # Key-Type: DSA # Key-Length: 1024 # Subkey-Type: ELG-E # Subkey-Length: 1024 # Name-Real: Joe Tester # Name-Comment: with stupid passphrase # Name-Email: joe@foo.bar # Expire-Date: 0 # Passphrase: abc # %pubring foo.pub # %secring foo.sec # %commit def add_subkey(self, master_key, master_passphrase=None, algorithm='rsa', usage='encrypt', expire='-'): """ Add subkeys to a master key, Args: master_key (str): The master key. master_passphrase (str): The passphrase for the master key. algorithm (str): The key algorithm to use. usage (str): The desired uses for the subkey. expire (str): The expiration date of the subkey. """ if self.version[0] < 2: raise NotImplementedError('Not available in GnuPG 1.x') if not master_key: # pragma: no cover raise ValueError('No master key fingerprint specified') if master_passphrase and not self.is_valid_passphrase(master_passphrase): # pragma: no cover raise ValueError('Invalid passphrase') args = ['--quick-add-key', master_key, algorithm, usage, str(expire)] result = self.result_map['addSubkey'](self) f = _make_binary_stream('', self.encoding) self._handle_io(args, f, result, passphrase=master_passphrase, binary=True) return result def quick_sign_key(self, certifier_fingerprint, recipient_fingerprint, certifier_passphrase=None): """ Certify a key using quick-sign-key function. Args: certifier_fingerprint (str): The fingerprint for the certifying key. recipient_fingerprint (str): The fingerprint of the key being signed. certifier_passphrase (str): The passphrase for the certifing key. """ if self.version[0] < 2: raise NotImplementedError('Not available in GnuPG 1.x') if not certifier_fingerprint: # pragma: no cover raise ValueError('No certifier key fingerprint specified') if not recipient_fingerprint: # pragma: no cover raise ValueError('No recipient key fingerprint specified') if certifier_passphrase and not self.is_valid_passphrase(certifier_passphrase): # pragma: no cover raise ValueError('Invalid passphrase') args = ['--local-user', certifier_fingerprint, '--quick-sign-key', recipient_fingerprint] result = self.result_map['sign'](self) f = _make_binary_stream('', self.encoding) self._handle_io(args, f, result, passphrase=certifier_passphrase, binary=True) return result # # ENCRYPTION # def encrypt_file(self, fileobj_or_path, recipients, hidden_recipients=None, sign=None, always_trust=False, passphrase=None, armor=True, output=None, symmetric=False, extra_args=None): """ Encrypt data in a file or file-like object. Args: fileobj_or_path (str|file): A path to a file or a file-like object containing the data to be encrypted. recipients (str|list): A key id of a recipient of the encrypted data, or a list of such key ids. hidden_recipients (str|list): A key id of a hidden recipient of the encrypted data, or a list of such key ids. sign (str): If specified, the key id of a signer to sign the encrypted data. always_trust (bool): Whether to always trust keys. passphrase (str): The passphrase to use for a signature. armor (bool): Whether to ASCII-armor the output. output (str): A path to write the encrypted output to. symmetric (bool): Whether to use symmetric encryption, extra_args (list[str]): A list of additional arguments to pass to `gpg`. """ if passphrase and not self.is_valid_passphrase(passphrase): raise ValueError('Invalid passphrase') args = ['--encrypt'] if symmetric: # can't be False or None - could be True or a cipher algo value # such as AES256 args = ['--symmetric'] if symmetric is not True: args.extend(['--cipher-algo', no_quote(symmetric)]) # else use the default, currently CAST5 else: if not recipients and not hidden_recipients: raise ValueError('No recipients or hidden recipients specified with ' 'asymmetric encryption') if recipients: if not _is_sequence(recipients): recipients = (recipients, ) for recipient in recipients: args.extend(['--recipient', no_quote(recipient)]) if hidden_recipients: if not _is_sequence(hidden_recipients): hidden_recipients = (hidden_recipients, ) for hidden_recipient in hidden_recipients: args.extend(['--hidden-recipient', no_quote(hidden_recipient)]) if armor: # create ascii-armored output - False for binary output args.append('--armor') if output: # pragma: no cover # write the output to a file with the specified name self.set_output_without_confirmation(args, output) if sign is True: # pragma: no cover args.append('--sign') elif sign: # pragma: no cover args.extend(['--sign', '--default-key', no_quote(sign)]) if always_trust: # pragma: no cover args.extend(['--trust-model', 'always']) if extra_args: # pragma: no cover args.extend(extra_args) result = self.result_map['crypt'](self) self._handle_io(args, fileobj_or_path, result, passphrase=passphrase, binary=True) logger.debug('encrypt result[:100]: %r', result.data[:100]) return result def encrypt(self, data, recipients, **kwargs): """ Encrypt the message contained in the string *data* for *recipients*. This method delegates most of the work to `encrypt_file()`. Args: data (str|bytes): The data to encrypt. recipients (str|list[str]): A key id of a recipient of the encrypted data, or a list of such key ids. kwargs (dict): Keyword arguments, which are passed to `encrypt_file()`: * sign (str): If specified, the key id of a signer to sign the encrypted data. * always_trust (bool): Whether to always trust keys. * passphrase (str): The passphrase to use for a signature. * armor (bool): Whether to ASCII-armor the output. * output (str): A path to write the encrypted output to. * symmetric (bool): Whether to use symmetric encryption, * extra_args (list[str]): A list of additional arguments to pass to `gpg`. """ data = _make_binary_stream(data, self.encoding) result = self.encrypt_file(data, recipients, **kwargs) data.close() return result def decrypt(self, message, **kwargs): """ Decrypt the data in *message*. This method delegates most of the work to `decrypt_file()`. Args: message (str|bytes): The data to decrypt. A default key will be used for decryption. kwargs (dict): Keyword arguments, which are passed to `decrypt_file()`: * always_trust: Whether to always trust keys. * passphrase (str): The passphrase to use. * output (str): If specified, the path to write the decrypted data to. * extra_args (list[str]): A list of extra arguments to pass to `gpg`. """ data = _make_binary_stream(message, self.encoding) result = self.decrypt_file(data, **kwargs) data.close() return result def decrypt_file(self, fileobj_or_path, always_trust=False, passphrase=None, output=None, extra_args=None): """ Decrypt data in a file or file-like object. Args: fileobj_or_path (str|file): A path to a file or a file-like object containing the data to be decrypted. always_trust: Whether to always trust keys. passphrase (str): The passphrase to use. output (str): If specified, the path to write the decrypted data to. extra_args (list[str]): A list of extra arguments to pass to `gpg`. """ if passphrase and not self.is_valid_passphrase(passphrase): raise ValueError('Invalid passphrase') args = ['--decrypt'] if output: # pragma: no cover # write the output to a file with the specified name self.set_output_without_confirmation(args, output) if always_trust: # pragma: no cover args.extend(['--trust-model', 'always']) if extra_args: # pragma: no cover args.extend(extra_args) result = self.result_map['crypt'](self) self._handle_io(args, fileobj_or_path, result, passphrase, binary=True) # logger.debug('decrypt result[:100]: %r', result.data[:100]) return result def get_recipients(self, message, **kwargs): """ Get the list of recipients for an encrypted message. This method delegates most of the work to `get_recipients_file()`. Args: message (str|bytes): The encrypted message. kwargs (dict): Keyword arguments, which are passed to `get_recipients_file()`: * extra_args (list[str]): A list of extra arguments to pass to `gpg`. """ data = _make_binary_stream(message, self.encoding) result = self.get_recipients_file(data, **kwargs) data.close() return result def get_recipients_file(self, fileobj_or_path, extra_args=None): """ Get the list of recipients for an encrypted message in a file or file-like object. Args: fileobj_or_path (str|file): A path to a file or file-like object containing the encrypted data. extra_args (list[str]): A list of extra arguments to pass to `gpg`. """ args = ['--decrypt', '--list-only', '-v'] if extra_args: # pragma: no cover args.extend(extra_args) result = self.result_map['crypt'](self) self._handle_io(args, fileobj_or_path, result, binary=True) ids = [] for m in PUBLIC_KEY_RE.finditer(result.stderr): ids.append(m.group(1)) return ids def trust_keys(self, fingerprints, trustlevel): """ Set the trust level for one or more keys. Args: fingerprints (str|list[str]): A key id for which to set the trust level, or a list of such key ids. trustlevel (str): The trust level. This is one of the following. * ``'TRUST_EXPIRED'`` * ``'TRUST_UNDEFINED'`` * ``'TRUST_NEVER'`` * ``'TRUST_MARGINAL'`` * ``'TRUST_FULLY'`` * ``'TRUST_ULTIMATE'`` """ levels = Verify.TRUST_LEVELS if trustlevel not in levels: poss = ', '.join(sorted(levels)) raise ValueError('Invalid trust level: "%s" (must be one of %s)' % (trustlevel, poss)) trustlevel = levels[trustlevel] + 1 import tempfile try: fd, fn = tempfile.mkstemp(prefix='pygpg-') lines = [] if isinstance(fingerprints, string_types): fingerprints = [fingerprints] for f in fingerprints: lines.append('%s:%s:' % (f, trustlevel)) # The trailing newline is required! s = os.linesep.join(lines) + os.linesep logger.debug('writing ownertrust info: %s', s) os.write(fd, s.encode(self.encoding)) os.close(fd) result = self.result_map['trust'](self) p = self._open_subprocess(['--import-ownertrust', fn]) self._collect_output(p, result, stdin=p.stdin) if p.returncode != 0: raise ValueError('gpg returned an error - return code %d' % p.returncode) finally: os.remove(fn) return result ================================================ FILE: messages.json ================================================ { "0000": "Success", "0001": "General error", "0002": "Unknown packet", "0003": "Unknown version in packet", "0004": "Invalid public key algorithm", "0005": "Invalid digest algorithm", "0006": "Bad public key", "0007": "Bad secret key", "0008": "Bad signature", "0009": "No public key", "000A": "Checksum error", "000B": "Bad passphrase", "000C": "Invalid cipher algorithm", "000D": "Cannot open keyring", "000E": "Invalid packet", "000F": "Invalid armor", "0010": "No user ID", "0011": "No secret key", "0012": "Wrong secret key used", "0013": "Bad session key", "0014": "Unknown compression algorithm", "0015": "Number is not prime", "0016": "Invalid encoding method", "0017": "Invalid encryption scheme", "0018": "Invalid signature scheme", "0019": "Invalid attribute", "001A": "No value", "001B": "Not found", "001C": "Value not found", "001D": "Syntax error", "001E": "Bad MPI value", "001F": "Invalid passphrase", "0020": "Invalid signature class", "0021": "Resources exhausted", "0022": "Invalid keyring", "0023": "Trust DB error", "0024": "Bad certificate", "0025": "Invalid user ID", "0026": "Unexpected error", "0027": "Time conflict", "0028": "Keyserver error", "0029": "Wrong public key algorithm", "002A": "Tribute to D. A.", "002B": "Weak encryption key", "002C": "Invalid key length", "002D": "Invalid argument", "002E": "Syntax error in URI", "002F": "Invalid URI", "0030": "Network error", "0031": "Unknown host", "0032": "Selftest failed", "0033": "Data not encrypted", "0034": "Data not processed", "0035": "Unusable public key", "0036": "Unusable secret key", "0037": "Invalid value", "0038": "Bad certificate chain", "0039": "Missing certificate", "003A": "No data", "003B": "Bug", "003C": "Not supported", "003D": "Invalid operation code", "003E": "Timeout", "003F": "Internal error", "0040": "EOF (gcrypt)", "0041": "Invalid object", "0042": "Provided object is too short", "0043": "Provided object is too large", "0044": "Missing item in object", "0045": "Not implemented", "0046": "Conflicting use", "0047": "Invalid cipher mode", "0048": "Invalid flag", "0049": "Invalid handle", "004A": "Result truncated", "004B": "Incomplete line", "004C": "Invalid response", "004D": "No agent running", "004E": "Agent error", "004F": "Invalid data", "0050": "Unspecific Assuan server fault", "0051": "General Assuan error", "0052": "Invalid session key", "0053": "Invalid S-expression", "0054": "Unsupported algorithm", "0055": "No pinentry", "0056": "pinentry error", "0057": "Bad PIN", "0058": "Invalid name", "0059": "Bad data", "005A": "Invalid parameter", "005B": "Wrong card", "005C": "No dirmngr", "005D": "dirmngr error", "005E": "Certificate revoked", "005F": "No CRL known", "0060": "CRL too old", "0061": "Line too long", "0062": "Not trusted", "0063": "Operation cancelled", "0064": "Bad CA certificate", "0065": "Certificate expired", "0066": "Certificate too young", "0067": "Unsupported certificate", "0068": "Unknown S-expression", "0069": "Unsupported protection", "006A": "Corrupted protection", "006B": "Ambiguous name", "006C": "Card error", "006D": "Card reset required", "006E": "Card removed", "006F": "Invalid card", "0070": "Card not present", "0071": "No PKCS15 application", "0072": "Not confirmed", "0073": "Configuration error", "0074": "No policy match", "0075": "Invalid index", "0076": "Invalid ID", "0077": "No SmartCard daemon", "0078": "SmartCard daemon error", "0079": "Unsupported protocol", "007A": "Bad PIN method", "007B": "Card not initialized", "007C": "Unsupported operation", "007D": "Wrong key usage", "007E": "Nothing found", "007F": "Wrong blob type", "0080": "Missing value", "0081": "Hardware problem", "0082": "PIN blocked", "0083": "Conditions of use not satisfied", "0084": "PINs are not synced", "0085": "Invalid CRL", "0086": "BER error", "0087": "Invalid BER", "0088": "Element not found", "0089": "Identifier not found", "008A": "Invalid tag", "008B": "Invalid length", "008C": "Invalid key info", "008D": "Unexpected tag", "008E": "Not DER encoded", "008F": "No CMS object", "0090": "Invalid CMS object", "0091": "Unknown CMS object", "0092": "Unsupported CMS object", "0093": "Unsupported encoding", "0094": "Unsupported CMS version", "0095": "Unknown algorithm", "0096": "Invalid crypto engine", "0097": "Public key not trusted", "0098": "Decryption failed", "0099": "Key expired", "009A": "Signature expired", "009B": "Encoding problem", "009C": "Invalid state", "009D": "Duplicated value", "009E": "Missing action", "009F": "ASN.1 module not found", "00A0": "Invalid OID string", "00A1": "Invalid time", "00A2": "Invalid CRL object", "00A3": "Unsupported CRL version", "00A4": "Invalid certificate object", "00A5": "Unknown name", "00A6": "A locale function failed", "00A7": "Not locked", "00A8": "Protocol violation", "00A9": "Invalid MAC", "00AA": "Invalid request", "00AB": "Unknown extension", "00AC": "Unknown critical extension", "00AD": "Locked", "00AE": "Unknown option", "00AF": "Unknown command", "00B0": "Not operational", "00B1": "No passphrase given", "00B2": "No PIN given", "00B3": "Not enabled", "00B4": "No crypto engine", "00B5": "Missing key", "00B6": "Too many objects", "00B7": "Limit reached", "00B8": "Not initialized", "00B9": "Missing issuer certificate", "00BA": "No keyserver available", "00BB": "Invalid elliptic curve", "00BC": "Unknown elliptic curve", "00BD": "Duplicated key", "00BE": "Ambiguous result", "00BF": "No crypto context", "00C0": "Wrong crypto context", "00C1": "Bad crypto context", "00C2": "Conflict in the crypto context", "00C3": "Broken public key", "00C4": "Broken secret key", "00C5": "Invalid MAC algorithm", "00C6": "Operation fully cancelled", "00C7": "Operation not yet finished", "00C8": "Buffer too short", "00C9": "Invalid length specifier in S-expression", "00CA": "String too long in S-expression", "00CB": "Unmatched parentheses in S-expression", "00CC": "S-expression not canonical", "00CD": "Bad character in S-expression", "00CE": "Bad quotation in S-expression", "00CF": "Zero prefix in S-expression", "00D0": "Nested display hints in S-expression", "00D1": "Unmatched display hints", "00D2": "Unexpected reserved punctuation in S-expression", "00D3": "Bad hexadecimal character in S-expression", "00D4": "Odd hexadecimal numbers in S-expression", "00D5": "Bad octal character in S-expression", "00D9": "All subkeys are expired or revoked", "00DA": "Database is corrupted", "00DB": "Server indicated a failure", "00DC": "No name", "00DD": "No key", "00DE": "Legacy key", "00DF": "Request too short", "00E0": "Request too long", "00E1": "Object is in termination state", "00E2": "No certificate chain", "00E3": "Certificate is too large", "00E4": "Invalid record", "00E5": "The MAC does not verify", "00E6": "Unexpected message", "00E7": "Compression or decompression failed", "00E8": "A counter would wrap", "00E9": "Fatal alert message received", "00EA": "No cipher algorithm", "00EB": "Missing client certificate", "00EC": "Close notification received", "00ED": "Ticket expired", "00EE": "Bad ticket", "00EF": "Unknown identity", "00F0": "Bad certificate message in handshake", "00F1": "Bad certificate request message in handshake", "00F2": "Bad certificate verify message in handshake", "00F3": "Bad change cipher message in handshake", "00F4": "Bad client hello message in handshake", "00F5": "Bad server hello message in handshake", "00F6": "Bad server hello done message in handshake", "00F7": "Bad finished message in handshake", "00F8": "Bad server key exchange message in handshake", "00F9": "Bad client key exchange message in handshake", "00FA": "Bogus string", "00FB": "Forbidden", "00FC": "Key disabled", "00FD": "Not possible with a card based key", "00FE": "Invalid lock object", "00FF": "True", "0100": "False", "0101": "General IPC error", "0102": "IPC accept call failed", "0103": "IPC connect call failed", "0104": "Invalid IPC response", "0105": "Invalid value passed to IPC", "0106": "Incomplete line passed to IPC", "0107": "Line passed to IPC too long", "0108": "Nested IPC commands", "0109": "No data callback in IPC", "010A": "No inquire callback in IPC", "010B": "Not an IPC server", "010C": "Not an IPC client", "010D": "Problem starting IPC server", "010E": "IPC read error", "010F": "IPC write error", "0111": "Too much data for IPC layer", "0112": "Unexpected IPC command", "0113": "Unknown IPC command", "0114": "IPC syntax error", "0115": "IPC call has been cancelled", "0116": "No input source for IPC", "0117": "No output source for IPC", "0118": "IPC parameter error", "0119": "Unknown IPC inquire", "012C": "Crypto engine too old", "012D": "Screen or window too small", "012E": "Screen or window too large", "012F": "Required environment variable not set", "0130": "User ID already exists", "0131": "Name already exists", "0132": "Duplicated name", "0133": "Object is too young", "0134": "Object is too old", "0135": "Unknown flag", "0136": "Invalid execution order", "0137": "Already fetched", "0138": "Try again later", "0139": "Wrong name", "013A": "Not authenticated", "013B": "Bad authentication", "013C": "No Keybox daemon running", "013D": "Keybox daemon error", "013E": "Service is not running", "013F": "Service error", "029A": "System bug detected", "02C7": "Unknown DNS error", "02C8": "Invalid DNS section", "02C9": "Invalid textual address form", "02CA": "Missing DNS query packet", "02CB": "Missing DNS answer packet", "02CC": "Connection closed in DNS", "02CD": "Verification failed in DNS", "02CE": "DNS Timeout", "02D1": "General LDAP error", "02D2": "General LDAP attribute error", "02D3": "General LDAP name error", "02D4": "General LDAP security error", "02D5": "General LDAP service error", "02D6": "General LDAP update error", "02D7": "Experimental LDAP error code", "02D8": "Private LDAP error code", "02D9": "Other general LDAP error", "02EE": "LDAP connecting failed (X)", "02EF": "LDAP referral limit exceeded", "02F0": "LDAP client loop", "02F2": "No LDAP results returned", "02F3": "LDAP control not found", "02F4": "Not supported by LDAP", "02F5": "LDAP connect error", "02F6": "Out of memory in LDAP", "02F7": "Bad parameter to an LDAP routine", "02F8": "User cancelled LDAP operation", "02F9": "Bad LDAP search filter", "02FA": "Unknown LDAP authentication method", "02FB": "Timeout in LDAP", "02FC": "LDAP decoding error", "02FD": "LDAP encoding error", "02FE": "LDAP local error", "02FF": "Cannot contact LDAP server", "0300": "LDAP success", "0301": "LDAP operations error", "0302": "LDAP protocol error", "0303": "Time limit exceeded in LDAP", "0304": "Size limit exceeded in LDAP", "0305": "LDAP compare false", "0306": "LDAP compare true", "0307": "LDAP authentication method not supported", "0308": "Strong(er) LDAP authentication required", "0309": "Partial LDAP results+referral received", "030A": "LDAP referral", "030B": "Administrative LDAP limit exceeded", "030C": "Critical LDAP extension is unavailable", "030D": "Confidentiality required by LDAP", "030E": "LDAP SASL bind in progress", "0310": "No such LDAP attribute", "0311": "Undefined LDAP attribute type", "0312": "Inappropriate matching in LDAP", "0313": "Constraint violation in LDAP", "0314": "LDAP type or value exists", "0315": "Invalid syntax in LDAP", "0320": "No such LDAP object", "0321": "LDAP alias problem", "0322": "Invalid DN syntax in LDAP", "0323": "LDAP entry is a leaf", "0324": "LDAP alias dereferencing problem", "032F": "LDAP proxy authorization failure (X)", "0330": "Inappropriate LDAP authentication", "0331": "Invalid LDAP credentials", "0332": "Insufficient access for LDAP", "0333": "LDAP server is busy", "0334": "LDAP server is unavailable", "0335": "LDAP server is unwilling to perform", "0336": "Loop detected by LDAP", "0340": "LDAP naming violation", "0341": "LDAP object class violation", "0342": "LDAP operation not allowed on non-leaf", "0343": "LDAP operation not allowed on RDN", "0344": "Already exists (LDAP)", "0345": "Cannot modify LDAP object class", "0346": "LDAP results too large", "0347": "LDAP operation affects multiple DSAs", "034C": "Virtual LDAP list view error", "0350": "Other LDAP error", "0371": "Resources exhausted in LCUP", "0372": "Security violation in LCUP", "0373": "Invalid data in LCUP", "0374": "Unsupported scheme in LCUP", "0375": "Reload required in LCUP", "0376": "LDAP cancelled", "0377": "No LDAP operation to cancel", "0378": "Too late to cancel LDAP", "0379": "Cannot cancel LDAP", "037A": "LDAP assertion failed", "037B": "Proxied authorization denied by LDAP", "0400": "User defined error code 1", "0401": "User defined error code 2", "0402": "User defined error code 3", "0403": "User defined error code 4", "0404": "User defined error code 5", "0405": "User defined error code 6", "0406": "User defined error code 7", "0407": "User defined error code 8", "0408": "User defined error code 9", "0409": "User defined error code 10", "040A": "User defined error code 11", "040B": "User defined error code 12", "040C": "User defined error code 13", "040D": "User defined error code 14", "040E": "User defined error code 15", "040F": "User defined error code 16", "05DC": "SQL success", "05DD": "SQL error", "05DE": "Internal logic error in SQL library", "05DF": "Access permission denied (SQL)", "05E0": "SQL abort was requested", "05E1": "SQL database file is locked", "05E2": "An SQL table in the database is locked", "05E3": "SQL library ran out of core", "05E4": "Attempt to write a readonly SQL database", "05E5": "SQL operation terminated by interrupt", "05E6": "I/O error during SQL operation", "05E7": "SQL database disk image is malformed", "05E8": "Unknown opcode in SQL file control", "05E9": "Insertion failed because SQL database is full", "05EA": "Unable to open the SQL database file", "05EB": "SQL database lock protocol error", "05EC": "(internal SQL code: empty)", "05ED": "SQL database schema changed", "05EE": "String or blob exceeds size limit (SQL)", "05EF": "SQL abort due to constraint violation", "05F0": "Data type mismatch (SQL)", "05F1": "SQL library used incorrectly", "05F2": "SQL library uses unsupported OS features", "05F3": "Authorization denied (SQL)", "05F4": "(unused SQL code: format)", "05F5": "SQL bind parameter out of range", "05F6": "File opened that is not an SQL database file", "05F7": "Notifications from SQL logger", "05F8": "Warnings from SQL logger", "0640": "SQL has another row ready", "0641": "SQL has finished executing", "3FFD": "System error w/o errno", "3FFF": "End of file", "8000": "Argument list too long", "8001": "Permission denied", "8002": "Address already in use", "8003": "Cannot assign requested address", "8004": "Advertise error", "8005": "Address family not supported by protocol", "8006": "Resource temporarily unavailable", "8007": "Operation already in progress", "800A": "Invalid exchange", "800B": "Bad file descriptor", "800C": "File descriptor in bad state", "800D": "Bad message", "800E": "Invalid request descriptor", "8010": "Invalid request code", "8011": "Invalid slot", "8012": "Bad font file format", "8013": "Device or resource busy", "8014": "Operation canceled", "8015": "No child processes", "8016": "Channel number out of range", "8017": "Communication error on send", "8018": "Software caused connection abort", "8019": "Connection refused", "801A": "Connection reset by peer", "801C": "Resource deadlock avoided", "801D": "Resource deadlock avoided", "801E": "Destination address required", "8020": "Numerical argument out of domain", "8021": "RFS specific error", "8022": "Disk quota exceeded", "8023": "File exists", "8024": "Bad address", "8025": "File too large", "8029": "Host is down", "802A": "No route to host", "802B": "Identifier removed", "802D": "Invalid or incomplete multibyte or wide character", "802E": "Operation now in progress", "802F": "Interrupted system call", "8030": "Invalid argument", "8031": "Input/output error", "8032": "Transport endpoint is already connected", "8033": "Is a directory", "8034": "Is a named type file", "8035": "Level 2 halted", "8036": "Level 2 not synchronized", "8037": "Level 3 halted", "8038": "Level 3 reset", "8039": "Can not access a needed shared library", "803A": "Accessing a corrupted shared library", "803B": "Cannot exec a shared library directly", "803C": "Attempting to link in too many shared libraries", "803D": ".lib section in a.out corrupted", "803E": "Link number out of range", "803F": "Too many levels of symbolic links", "8040": "Wrong medium type", "8041": "Too many open files", "8042": "Too many links", "8043": "Message too long", "8044": "Multihop attempted", "8045": "File name too long", "8046": "No XENIX semaphores available", "8048": "Network is down", "8049": "Network dropped connection on reset", "804A": "Network is unreachable", "804B": "Too many open files in system", "804C": "No anode", "804D": "No buffer space available", "804E": "No CSI structure available", "804F": "No data available", "8050": "No such device", "8051": "No such file or directory", "8052": "Exec format error", "8053": "No locks available", "8054": "Link has been severed", "8055": "No medium found", "8056": "Cannot allocate memory", "8057": "No message of desired type", "8058": "Machine is not on the network", "8059": "Package not installed", "805A": "Protocol not available", "805B": "No space left on device", "805C": "Out of streams resources", "805D": "Device not a stream", "805E": "Function not implemented", "805F": "Block device required", "8060": "Transport endpoint is not connected", "8061": "Not a directory", "8062": "Directory not empty", "8063": "Not a XENIX named type file", "8064": "Socket operation on non-socket", "8065": "Operation not supported", "8066": "Inappropriate ioctl for device", "8067": "Name not unique on network", "8068": "No such device or address", "8069": "Operation not supported", "806A": "Value too large for defined data type", "806B": "Operation not permitted", "806C": "Protocol family not supported", "806D": "Broken pipe", "8072": "Protocol error", "8073": "Protocol not supported", "8074": "Protocol wrong type for socket", "8075": "Numerical result out of range", "8076": "Remote address changed", "8077": "Object is remote", "8078": "Remote I/O error", "8079": "Interrupted system call should be restarted", "807A": "Read-only file system", "807C": "Cannot send after transport endpoint shutdown", "807D": "Socket type not supported", "807E": "Illegal seek", "807F": "No such process", "8080": "Srmount error", "8081": "Stale file handle", "8082": "Streams pipe error", "8083": "Timer expired", "8084": "Connection timed out", "8085": "Too many references: cannot splice", "8086": "Text file busy", "8087": "Structure needs cleaning", "8088": "Protocol driver not attached", "8089": "Too many users", "808A": "Resource temporarily unavailable", "808B": "Invalid cross-device link", "808C": "Exchange full" } ================================================ FILE: package.json ================================================ { "index-metadata": { "extensions": { "python.details": { "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Programming Language :: Python :: 2.4", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules" ], "license": "Copyright (C) 2008-2019 by Vinay Sajip. All Rights Reserved. See LICENSE.txt for license." }, "python.project": { "contacts": [ { "email": "vinay_sajip@red-dove.com", "name": "Vinay Sajip", "role": "author" }, { "email": "vinay_sajip@red-dove.com", "name": "Vinay Sajip", "role": "maintainer" } ], "project_urls": { "Home": "http://packages.python.org/python-gnupg/index.html" } } }, "metadata_version": "2.0", "name": "python-gnupg", "python.exports": { "modules": [ "gnupg" ] }, "source_url": "https://pypi.io/packages/source/p/python-gnupg/python-gnupg-0.4.5.tar.gz", "summary": "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)", "version": "0.4.5" }, "metadata": { "description": "This module allows easy access to GnuPG's key management, encryption and signature functionality from Python programs. It is intended for use with Python 2.4 or greater.", "name": "python-gnupg", "platform": "No particular restrictions", "version": "0.4.5" }, "source": { "modules": [ "gnupg" ] }, "version": 1 } ================================================ FILE: pyproject.toml ================================================ [build-system] requires = [ "setuptools >= 44", ] build-backend = 'setuptools.build_meta' ================================================ FILE: release ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2023 Red Dove Consultants Limited # import argparse import glob import logging import os import re import subprocess import sys DEBUGGING = 'PY_DEBUG' in os.environ logger = logging.getLogger(__name__) def main(): fn = os.path.basename(__file__) fn = os.path.splitext(fn)[0] lfn = os.path.expanduser('~/logs/%s.log' % fn) if os.path.isdir(os.path.dirname(lfn)): logging.basicConfig(level=logging.DEBUG, filename=lfn, filemode='w', format='%(message)s') adhf = argparse.ArgumentDefaultsHelpFormatter ap = argparse.ArgumentParser(formatter_class=adhf, prog=fn) aa = ap.add_argument aa('-b', '--build', default=False, action='store_true', help='Force a rebuild') aa('-u', '--upload', default=False, action='store_true', help='Upload to PyPI') options = ap.parse_args() with open('gnupg.py') as f: data = f.read() m = re.search(r"__version__\s*=\s*'(.*)'", data) assert m ver = m.groups()[0] sigs = list(glob.glob(f'dist/*{ver}*.asc')) # import pdb; pdb.set_trace() if sigs and not options.build: print(f'Signatures found: {", ".join(sigs)}') else: if not sigs: print('Signatures not found ...') files = [fn for fn in glob.glob(f'dist/*{ver}*') if not fn.endswith('.asc')] if files and not options.build: print(f'Archives found: {", ".join(files)}') else: if not files: print('Archives not found ...') subprocess.check_call(['pybuild']) files = [fn for fn in glob.glob(f'dist/*{ver}*') if not fn.endswith('.asc')] for fn in files: sfn = f'{fn}.asc' if os.path.exists(sfn): os.remove(sfn) cmd = ['gpg2', '-abs', fn] p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.communicate() assert p.returncode == 0 if options.upload: cmd = ['twine', 'upload', '-r', 'python-gnupg'] cmd.extend(files) subprocess.check_call(cmd) if __name__ == '__main__': try: rc = main() except KeyboardInterrupt: rc = 2 except Exception as e: if DEBUGGING: s = ' %s:' % type(e).__name__ else: s = '' sys.stderr.write('Failed:%s %s\n' % (s, e)) if DEBUGGING: import traceback; traceback.print_exc() rc = 1 sys.exit(rc) ================================================ FILE: setup.cfg ================================================ [metadata] name = python-gnupg version = attr: gnupg.__version__ description = A wrapper for the Gnu Privacy Guard (GPG or GnuPG) long_description = This module allows easy access to GnuPG's key management, encryption and signature functionality from Python programs. It is intended for use with Python 2.7 or greater. Releases are normally signed using a GnuPG key with the user id vinay_sajip@yahoo.co.uk and the following fingerprint: CA74 9061 914E AC13 8E66 EADB 9147 B477 339A 9B86 As PyPI no longer shows signatures, you should be able to download release archives and signatures from https://github.com/vsajip/python-gnupg/releases/ The archives should be the same as those uploaded to PyPI. url = https://github.com/vsajip/python-gnupg author = Vinay Sajip author_email = vinay_sajip@yahoo.co.uk maintainer = Vinay Sajip maintainer_email = vinay_sajip@yahoo.co.uk license = BSD license_files = LICENSE.txt platforms = any requires_python >= 2.7 classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: BSD License Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 3 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Programming Language :: Python :: 3.14 Operating System :: OS Independent Topic :: Software Development :: Libraries :: Python Modules project_urls = Documentation = https://gnupg.readthedocs.io/ Source = https://github.com/vsajip/python-gnupg Tracker = https://github.com/vsajip/python-gnupg/issues keywords = GnuPG,cryptography,encryption,decryption,signing,verification [options] py_modules = gnupg [bdist_wheel] python_tag = py2.py3 ================================================ FILE: test_gnupg.py ================================================ # -*- coding: utf-8 -*- """ A test harness for gnupg.py. Copyright (C) 2008-2026 Vinay Sajip. All rights reserved. """ import argparse import io import json import logging import os.path import os import re import shutil import stat import sys import tempfile import unittest try: unicode except NameError: unicode = str try: from unittest import skipIf except ImportError: # pragma: no cover # For now, for Python < 2.7 def skipIf(condition, message): if not condition: return lambda x: x else: return lambda x: None import gnupg __author__ = 'Vinay Sajip' __date__ = '$31-Dec-2025 16:42:39$' ALL_TESTS = True gnupg.log_everything = True logger = logging.getLogger(__name__) GPGBINARY = os.environ.get('GPGBINARY', 'gpg') KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.9 (MingW32) mQGiBEiH4QERBACm48JJsg2XGzWfL7f/fjp3wtrY+JIz6P07s7smr35kve+wl605 nqHtgjnIVpUVsbI9+xhIAPIkFIR6ZcQ7gRDhoT0bWKGkfdQ7YzXedVRPlQLdbpmR K2pKKySpF35pJsPAYa73EVaxu2KrII4CyBxVQgNWfGwEbtL5FfzuHhVOZwCg6JF7 bgOMPmEwBLEHLmgiXbb5K48D/2xsXtWMkvgRp/ubcLxzbNjaHH6gSb2IfDi1+W/o Bmfua6FksPnEDn7PWnBhCEO9rf1tV0FcrvkR9m2FGfx38tjssxDdLvX511gbfc/Q DJxZ00A63BxI3xav8RiXlqpfQGXpLJmCLdeCh5DXOsVMCfepqRbWyJF0St7LDcq9 SmuXA/47dzb8puo9dNxA5Nj48I5g4ke3dg6nPn7aiBUQ35PfXjIktXB6/sQJtWWx XNFX/GVUxqMM0/aCMPdtaoDkFtz1C6b80ngEz94vXzmON7PCgDY6LqZP1B1xbrkr 4jGSr68iq7ERT+7E/iF9xp+Ynl91KK7h8llY6zFw+yIe6vGlcLQvR2FyeSBHcm9z cyAoQSB0ZXN0IHVzZXIpIDxnYXJ5Lmdyb3NzQGdhbW1hLmNvbT6IYAQTEQIAIAUC SIfhAQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEJZ2Ekdc7S4UtEcAoJIA iZurfuzIUE9Dtn86o6vC14qoAJ9P79mxR88wRr/ac9h5/BIf5cZKMbkCDQRIh+EB EAgAyYCvtS43J/OfuGHPGPZT0q8C+Y15YLItSQ3H6IMZWFY+sX+ZocaIiM4noVRG +mrEqzO9JNh4KP1OdFju1ZC8HZXpPVur48XlTNSm0yjmvvfmi+aGSuyQ0NkfLyi1 aBeRvB4na/oFUgl908l7vpSYWYn4EY3xpvwJdyTWHTh4o7+zvrR1fByDt49k2b3z yTACoxYPVQfknt8gxqLqHZsbgn02Ml7HS17bSWr5Z7PlWqDlmsdqUikVU9d2RvIq R+YIJbOdHSklbVQQDhr+xgHPi39e7nXMxR/rMjMbz7E5vSNkge45n8Pzim8iyqy+ MTMW8psV/OyrHUJzBEA7M6hA1wADBwgAnB0HzI1iyiQmIymO0Hj0BgqU6/avFw9R ggBuE2v7KsvuLP6ohXDEhYopjw5hgeotobpg6tS15ynch+6L8uWsJ0rcY2X9dsJy O8/5mjrNDHwCKiYRuZfmRZjzW03vO/9+rjtZ0NzoWYMP3UR8lUTVp2LTygefBA88 Zgw6dWBVzn+/c0vdwcF4Y3njYKE7eq4VrfcwqRgD0hDyIJd1OpqzHfXXnTtLlAsm UwtdONzlwu7KkgafMo4vzKY6dCtUkR6pXAE/rLQfCTonwl9SnyusoYZgjDoj4Pvw ePxIl2q05dcn96NJGS+SfS/5B4H4irbfaEYmCfKps+45sjncYGhZ/ohJBBgRAgAJ BQJIh+EBAhsMAAoJEJZ2Ekdc7S4U2lkAoIwZLMHVldC0v9wse53xU0NsNIskAKDc Ft0XWUJ9yajOEUqCVHNs3F99t5kBogRIh+FVEQQAhk/ROtJ5/O+YERl4tZZBEhGH JendDBDfzmfRO9GIDcZI20nx5KJ1M/zGguqgKiVRlBy32NS/IRqwSI158npWYLfJ rYCWrC2duMK2i/8prOEfaktnqZXVCHudGtP4mTqNSs+867LnGhQ4w3HmB09zCIpD eIhhhPOb5H19H8UlojsAoLwsq5BACqUKoiz8lUufpTTFMbaDA/4v1fWmprYAxGq9 cZ9svae772ymN/RRPDb/D+UJoJCCJSjE8m4MukVchyJVT8GmpJM2+dlt62eYwtz8 bGNt+Yzzxr0N8rLutsSks7RaM16MaqiAlM20gAXEovxBiocgP/p5bO3FGKOBbrfd h47BZDEqLvfJefXjZEsElbZ9oL2zDgP9EsoDS9mbfesHDsagE5jCZRTY1C/FRLBO zhGgP2IlqBdOX8BYBYZiIlLM+pN5fU0Hcu3VOZY1Hnj6r3VbK1bOScQzqrZ7qgmw TRgyxUQalaOhMb5rUD0+dUFxa/mhTerx5POrX6zOWmmK0ldYTZO4/+nWr4FwmU8R 41nYYYdi0yS0MURhbm55IERhdmlzIChBIHRlc3QgdXNlcikgPGRhbm55LmRhdmlz QGRlbHRhLmNvbT6IYAQTEQIAIAUCSIfhVQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4B AheAAAoJEG7bKmS7rMYAEt8An2jxsmsE1MZVZc4Ev8RB9Gu1zbsCAJ9G5kkYIIf0 OoDqCjkDMDJcpd4MqLkCDQRIh+FVEAgAgHQ+EyseLw6A3BS2EUz6U1ZGzuJ5CXxY BY8xaQtE+9AJ0WHyzKeptnlnY1x9et3ny1BcVC5aR1OgsDiuVRvSFwpFfVxMKbRT kvERWADfB0N5EyWwyE0E4BT5hyEhW7fS0bucJL6UK5PKvfE5wexWlUI3yV4K1z6W 2gSNL60o3kmoGn9K5ICWO/jbi6MkPptSoDu/laCJHv/aid6Gf94ckDClQQyLsccj 0ibynm6rI3cIzpPMbimKIsKT1smAqZEBsTucBlOjIuIROANTZUN3reGIRh/kVNyg YTrkUnIqVS9FnbHa2wxeb6F/cO33fPiVfiCmZuKI1Uh4PMGaaSCh0wADBQf/SaXN WcuD0mrEnxqgEJRx67ZeFZjZM53Obu3JYQ++lqsthf8MxE7K4J/67xDpOh6waK0G 6GCLwEm3Z7wjCaz1DYg2uJp/3pispWxZio3PLVe7WrMY+oEBHEsiJXicS5dV620a uoaBnnc0aQWT/DREE5s35IrZCh4WDQgO9rl0i/qcIITm77TmQbq2Xdj5vt6s0cx7 oHKRaFBpQ8DBsCQ+D8Xz7i1oUygNp4Z5xPhItWeCfE9YoCoem4jSB4HGwmMOEicp VSpY43k01cd0Yfb1OMhA5C8OBwcwn3zvQB7nbxyxyQ9qphfwhMookIL4+tKKBIQL CnOGhApkAGbjRwuLi4hJBBgRAgAJBQJIh+FVAhsMAAoJEG7bKmS7rMYA+JQAn0E2 WdPQjKEfKnr+bW4yubwMUYKyAJ4uiE8Rv/oEED1oM3xeJqa+MJ9V1w== =sqld -----END PGP PUBLIC KEY BLOCK-----""" SIGNED_KEYS = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v2 mI0EVcnKUQEEAKWazmfM0kbvDdw7Kos2NARaX67c8iJ3GOBimUvYLj4VR3Mqrm34 ZdLlS8jCmid+qoisefvGW5uw5Q3gIs0mdEdUpFKlXNiIja/Dg/FHjjJPPCjfzDTh Q03EYA7QvOnXZXhYPBqK7NitsNXW4lPnIJdanLx7yMuL+2Xb+tF39mwnABEBAAG0 LUpvc2h1YSBDYWx2ZXJ0IChBIHRlc3QgdXNlcikgPGpjQGV4YW1wbGUuY29tPoi3 BBMBCAAhBQJVycpRAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJELxvNQ+z 0EB2jcED/0lHKaEkyd6cj0Zckf9luIkZ4Hno/vRCquTI7c3aPjS3qmE8mOvKSBCV +SamPdRM7DdjkdBrrKy2HtiDqbM+1/CdXuQka2SlJWyLCJe48+KWfBpqlY3N4t53 JjHRitDB+hC8njWTV5prli6EgsBPAF+ZkO0iZhlsMmWdDWgqDpGRiJwEEAEIAAYF AlXJym8ACgkQBXzPZYwHT9oiiQQAvPF8ubwRopnXIMDQgSxKyFDM1MI1w/wb4Okd /MkMeZSmdcHJ6pEymp5bYciCBuLW+jw0vZWza3YloO/HtuppnF6A9a1UvYcp/diI O5qkQqYPlui1PJl7hQ014ioniMfOcC4X/r6PDbC78Pczje0Yh9AOqNGeCyNyNdlc pjaHb0m4jQRVycpRAQQAo9JjW75F5wTVVO552cGCZWqZvDyBt9+IkoK9Bc+ggdn5 6R8QVCihYuaSzcSEN84zHaR3MmGKHraCmCSlfe7w0d41Dlns0P03KMdIZOGrm045 F8TXdSSPQOv5tA4bz3k2lGD0zB8l4NUWFaZ5fzw2i73FF4O/FwCU8xd/JCKVPkkA EQEAAYifBBgBCAAJBQJVycpRAhsMAAoJELxvNQ+z0EB2xLYD/i3tKirQlVB+32WP wggstqDp1BlUBmDb+4Gndpg4l7omJTTyOsF26SbYgXZqAdEd5T/UfpEla0DKiBYh 2/CFYXadkgX/ME+GTetTmD4hHoBNmdXau92buXsIXkwh+JR+RC3cl2U6tWb/MIRd zvJiok8W8/FT/QrEjIa2etN2d+KR =nNBX -----END PGP PUBLIC KEY BLOCK----- -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v2 mI0EVcnKNgEEANIVlIUyRXWHP/ljdMEA8B5NxecRCKusUIPxeapk2do5UCZgR1q8 5wOP4K/+W3Uj85ylOOCNTFYKRozAHsPMAmQ38W93DZYqFbG6d7rwMvz4pVe0wUtj SBINoKnoEDZwx3erxFKOkp/5fF3NoYSIx9a0Ds21ESk0TAuH5Tg934YhABEBAAG0 MVdpbnN0b24gU21pdGggKEEgdGVzdCB1c2VyKSA8d2luc3RvbkBleGFtcGxlLmNv bT6ItwQTAQgAIQUCVcnKNgIbAwULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRAF fM9ljAdP2h05A/4vmnxV1MwcOhJTHZys5g2/j5UoZG7V7lPGpJaojSAIVzYXZtwT 5A7OY8Nl21kIY6gnZlgbTRpHN8Qq2wRKAyW5o6wQvuN16CW4bmGjoHYRGPqkeM0w G40W/v88JXrYDNNe/68g4pnPsZ3J0oMLbRvCaDQQHXBuZNJrT1sOxl9Of7iNBFXJ yjYBBACmHbs0PdOF8NEGc+fEtmdKOSKOkrcvg1wTu1KFFTBFEbseHOCNpx+R6lfO ZiZmHGdKeJhTherfjHaY5jmvyDWq5TLZXK61quNsWxmY2zJ0SRwrIG/CWi4bMi5t JNc23vMumkz4X5g7x0Ea7xEWkcYBn0H6sZDAtb8d8mrlWkMekQARAQABiJ8EGAEI AAkFAlXJyjYCGwwACgkQBXzPZYwHT9pQIwP8D9/VroykSE2J3gy0S6HC287jXqXF 0zWejUAQtWUSSRx4esqfLE8lfae6+LDHO8D0Bf6YUJmu7ATOZP2/TIas7JrNvXWc NKWl2MHEAGUYq8utCjZ3dKKhaV7UvcY4PyLIpFteNkOz4wFe6C0Mm+1NYwokIFyh zPBq9eFk7Xx9Wrc= =HT6N -----END PGP PUBLIC KEY BLOCK----- """ SECRET_KEY = """ -----BEGIN PGP PRIVATE KEY BLOCK----- lQPGBFztd1UBCACiHhlEJIGfXNEiUX4GwamgdLOkJ3mbn5OyV4M/Ie3YvvHxveq/ TFYbuV63iuDVhNXpDUNmGsTq4vFaMsseLl7eESw8UTa3XklHHjh56kw0AVkJA75A Xq/VshFobLNxYZdtlOVkKe1a3uJVKs+BqFjhavEjQyhkpWvBY51OzCSc2AN/aQZA F3AltZ8luIHZPs8zVbgH90WIpze+vzAd9FyXD0wV6gylGSifHj8zIhac80evQgD9 50De7EPnSdgZSNwnlrhQtAIB5UnTETxXk34/W0Rq+BKn6SuchtaP7hXIHC0+B0C7 zBzPYKMQ7vXc/hceNwSGtgovhaQPCcv1byFBABEBAAH+BwMCUNdAVY/RMdJg1q5n FQOyVZl2tvd3krExjGYvhabwijbPz+TrVkPhKqdkp4Hbf3oXV/bcbQhG2dld4Ooc +xtEpTqYw08bNDuk4NEAvggasUkgssHZccDmHySGfA9U8C7B0Hj8xT4SifnuVNL+ xp9iv1BS03s+UIEVZ2rGjDQy7/G/U6/ZpLqFg+C113VQs6yz0VMsnnAQOMgN0+gQ aZb6VNPR7nZ5+/hRlx0DgXu++lei9HTmHRz+ZvbbYjeU9nj10eANhO0lEvlgtyXa v4Y5ERwk86gbkSRGtN88qVK/+GXK60Q33EoGMlwPZrfFGx+N5QuPEnCjT1vvz7E3 HhCpe4u5Idusgui+tDkxq8BEz6iTGMO1hcb75MDdIQBhJzeJ7OIxyBfqLReF4+Ut eNwy0wpN3xuEeYvP4ZIe7hj74WWIuKq2+lesPm4eWRPoaQ5MZXmEwbjr29e++V7D EkHgCYio6TVwrHA0LRSNfm8VVBV2cdsqFOLLutudHoC8BnjetEetmYaA99u0Pevz NscYwfaWLNW/d5FGyPUb+GQFYzmQWUfUzpg9hu7U79uA0kOwC+4nK6LEalILtoHn YO3PvvcCEnpWBlDhCR3n0zkNQCulvQKS/ww5q/MDNqvibKiMJHJ1xP89tEU3lnHl qgwHVmleqUR+yzdg5lo96Yey5yaDdhK5ZR1TFC4qK4Igcn2+WG109659bJUGpEre Vktu530JutX38ZoyKdHO0uPs/ft/hgBhNd6MKmh7eejo84Wn6/lxkfMydkfKm5QY dMHF3Ew+l7aACAs3l95V0YDNzA0FyOFkb/tqxyx8dP+O2NdZQZSvG+yxDav05bCq kwz+7H7sJnUj1JJtUgPTL9yVH+LyUhL8AU13UKVjBFJ4VL5+KDD9KwPkk6aN7zDW Qv0g8Cc7A8H0tB5BdXRvZ2VuZXJhdGVkIEtleSA8dXNlcjFAdGVzdD6JATgEEwEI ACIFAlztd1UCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEGP32fXSIJgg IXgH/3o1rUzbjjz1sMoBwRv4qLmgeqlB2YJSVzLWOn4AcrHbxup5O9nJkqG+YFwH OFmytuiPDKmA4ZXww8f+2rHXdDuwI5SWnfhuPpV863BulIhtjwiwqD9eIzQ9LX79 K7hXRJ4I0AkYEbDHOWlLHZCrjul/ZaS10QRVR21EYICha2I8tvxsRMPp0I93XnuB T+z7ykRxRjpMv6MfhWVcw5B0s7lPedLhcx657HfY49t36/CIZ9/zMKsduX7cTOAh tO8f06R3yfjxLRD8y89frVP3+tGMvt2yGOd5TT0zht5yYcG6QkiHlfdgXqeE8nsU 2392Xn/RETq6xCj3kG6K3wbWqh0= =2A5s -----END PGP PRIVATE KEY BLOCK----- """ CERTIFYING_KEY = """ -----BEGIN PGP PRIVATE KEY BLOCK----- lHcEaeE/WBMIKoZIzj0DAQcCAwQJX+QJbszp7FFHIaGY1ZOwLJCTnwjzy1Z5vnKw 1AZ9UnIRO+TMPEEUizEc4FO1nQBUgCS2nOccwXpnZtavc8d5AAD/TitHUDwl1CbF f2FGF4alBhMBuWohWcAUNOopbKgaNO4MubQfQ2VydGlmaWVyIFRlc3QgPGNlcnRp ZmllckB0ZXN0PoiQBBMTCAA4FiEEl+gdlmY3v7p/43AlhW+ORWgM16QFAmnhP1gC GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQhW+ORWgM16RwdQEA9CTM/Zz+ rWNl3ToKdsPKS7s3KaPfvGPKNIqVwUJzhT8BAN58ziizYcb85HREsFHtYOJs0Uti 7GYLD4MPZxhIz5sr =he2w -----END PGP PRIVATE KEY BLOCK----- """ RECIPIENT_KEY = """ -----BEGIN PGP PRIVATE KEY BLOCK----- lHcEaeE//hMIKoZIzj0DAQcCAwRraYDaESix05+l8b69fKzIvYmIoXbaOVPoCnjA Qe6hYEKQrO7p5zOUp6lLXhnZ6JWD6B7RcoGSHpAHQMWzpzkWAAEA92FWKM7TZolx Wpvuj+6lLf6wrg/gOVofvjKDoj9IbfARALQfUmVjaXBpZW50IFRlc3QgPFJlY2lw aWVudEB0ZXN0PoiQBBMTCAA4FiEEqHrbKqME4ufCGsGh2ILlpHJXa3EFAmnhP/4C GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ2ILlpHJXa3E2NAEAlbZ+sUpL j88bPK7sBA0pgiAItWYclDgZXAmqBCXz9ggBAM4khSii4pshJh0XavURaYnoC2SU qlWoXWGVJkVDlHla =1zeo -----END PGP PRIVATE KEY BLOCK----- """ def is_list_with_len(o, n): return isinstance(o, list) and len(o) == n BASE64_PATTERN = re.compile(r'^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$', re.I) def get_key_data(s): lines = s.split('\n') result = '' for line in lines: m = BASE64_PATTERN.match(line) if m: result += line return result def compare_keys(k1, k2): "Compare ASCII keys" # See issue #57: we need to compare only the actual key data, # ignoring things like spurious blank lines return get_key_data(k1) != get_key_data(k2) AGENT_CONFIG = '''allow-loopback-pinentry log-file socket:///tmp/S.my-gnupg-log verbose debug ipc ''' ENABLE_TOFU = 'ENABLE_TOFU' in os.environ if ENABLE_TOFU: # pragma: no cover GPG_CONFIG = 'trust-model tofu+pgp\ntofu-default-policy unknown\n' def prepare_homedir(hd): if not os.path.isdir(hd): # pragma: no cover os.makedirs(hd) os.chmod(hd, 0x1C0) fn = os.path.join(hd, 'gpg-agent.conf') with open(fn, 'w') as f: f.write(AGENT_CONFIG) if ENABLE_TOFU: # pragma: no cover fn = os.path.join(hd, 'gpg.conf') with open(fn, 'w') as f: f.write(GPG_CONFIG) class GPGTestCase(unittest.TestCase): def setUp(self): ident = self.id().rsplit('.', 1)[-1] logger.debug('-- %s starting ---------------------------' % ident) if 'STATIC_TEST_HOMEDIR' not in os.environ: hd = tempfile.mkdtemp(prefix='keys-') else: # pragma: no cover hd = os.path.join(os.getcwd(), 'keys') if os.path.exists(hd): self.assertTrue(os.path.isdir(hd), 'Not a directory: %s' % hd) shutil.rmtree(hd, ignore_errors=True) prepare_homedir(hd) self.homedir = hd self.gpg = gpg = gnupg.GPG(gnupghome=hd, gpgbinary=GPGBINARY) v = gpg.version if v: if v >= (2, ): # pragma: no cover gpg.options = ['--debug-quick-random'] else: gpg.options = ['--quick-random'] self.test_fn = test_fn = 'random_binary_data' if not os.path.exists(test_fn): # pragma: no cover data_file = open(test_fn, 'wb') data_file.write(os.urandom(5120 * 1024)) data_file.close() def tearDown(self): if 'STATIC_TEST_HOMEDIR' not in os.environ: shutil.rmtree(self.homedir, ignore_errors=True) ident = self.id().rsplit('.', 1)[-1] logger.debug('-- %s finished ---------------------------' % ident) def test_environment(self): "Test the environment by ensuring that setup worked" hd = self.homedir self.assertTrue(os.path.exists(hd) and os.path.isdir(hd), 'Not an existing directory: %s' % hd) def test_list_keys_initial(self): "Test that initially there are no keys" public_keys = self.gpg.list_keys() self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 0), 'Empty list expected') private_keys = self.gpg.list_keys(secret=True) self.assertTrue(is_list_with_len(private_keys, 0), 'Empty list expected') def generate_key(self, first_name, last_name, domain, passphrase=None, with_subkey=True): "Generate a key" params = { 'Key-Type': 'DSA', 'Key-Length': 1024, 'Name-Comment': 'A test user', 'Expire-Date': 0, } if with_subkey: params['Subkey-Type'] = 'ELG-E' params['Subkey-Length'] = 2048 options = self.gpg.options or [] if '--debug-quick-random' in options or '--quick-random' in options: # If using the fake RNG, a key isn't regarded as valid # unless its comment has the text (insecure!) in it. params['Name-Comment'] = 'A test user (insecure!)' params['Name-Real'] = '%s %s' % (first_name, last_name) params['Name-Email'] = ('%s.%s@%s' % (first_name, last_name, domain)).lower() if passphrase is None: passphrase = ('%s%s' % (first_name[0], last_name)).lower() params['Passphrase'] = passphrase cmd = self.gpg.gen_key_input(**params) return self.gpg.gen_key(cmd) def do_key_generation(self): "Test that key generation succeeds" result = self.generate_key('Barbara', 'Brown', 'beta.com') self.assertNotEqual(None, result, 'Non-null result') return result def test_key_generation_with_invalid_key_type(self): "Test that key generation handles invalid key type" params = { 'Key-Type': 'INVALID', 'Key-Length': 1024, 'Subkey-Type': 'ELG-E', 'Subkey-Length': 2048, 'Name-Comment': 'A test user', 'Expire-Date': 0, 'Name-Real': 'Test Name', 'Name-Email': 'test.name@example.com', } cmd = self.gpg.gen_key_input(**params) result = self.gpg.gen_key(cmd) self.assertFalse(result.data, 'Null data result') self.assertFalse(result.fingerprint, 'Null fingerprint result') self.assertEqual(2, result.returncode, 'Unexpected return code') def test_key_generation_with_colons(self): "Test that key generation handles colons in key fields" params = { 'key_type': 'RSA', 'name_real': 'urn:uuid:731c22c4-830f-422f-80dc-14a9fdae8c19', 'name_comment': 'dummy comment', 'name_email': 'test.name@example.com', } if self.gpg.version >= (2, 1): params['passphrase'] = 'foo' cmd = self.gpg.gen_key_input(**params) result = self.gpg.gen_key(cmd) self.assertEqual(0, result.returncode, 'Non-zero return code') keys = self.gpg.list_keys() self.assertEqual(0, keys.returncode, 'Non-zero return code') self.assertEqual(len(keys), 1) key = keys[0] uids = key['uids'] self.assertEqual(len(uids), 1) uid = uids[0] self.assertEqual(uid, 'urn:uuid:731c22c4-830f-422f-80dc-14a9fdae8c19 ' '(dummy comment) ') def test_key_generation_with_escapes(self): "Test that key generation handles escape characters" params = { 'name_real': 'Test Name', 'name_comment': 'Funny chars: \\r\\n\\f\\v\\0\\b', 'name_email': 'test.name@example.com', } if self.gpg.version >= (2, 1): params['passphrase'] = 'foo' cmd = self.gpg.gen_key_input(**params) result = self.gpg.gen_key(cmd) self.assertEqual(0, result.returncode, 'Non-zero return code') keys = self.gpg.list_keys() self.assertEqual(0, keys.returncode, 'Non-zero return code') self.assertEqual(len(keys), 1) key = keys[0] uids = key['uids'] self.assertEqual(len(uids), 1) uid = uids[0] self.assertEqual(uid, 'Test Name (Funny chars: ' '\r\n\x0c\x0b\x00\x08) ') @skipIf(os.name == 'nt', 'Test requires POSIX-style permissions') def test_key_generation_failure(self): if self.gpg.version < (2, 0): # pragma: no cover raise unittest.SkipTest('gpg 1.x hangs in this test') if not os.path.exists('rokeys'): # pragma: no cover os.mkdir('rokeys') os.chmod('rokeys', 0o400) # no one can write/search this directory gpg = gnupg.GPG(gnupghome='rokeys', gpgbinary=GPGBINARY) params = { 'Key-Type': 'RSA', 'Key-Length': 1024, 'Subkey-Type': 'ELG-E', 'Subkey-Length': 2048, 'Name-Comment': 'A test user', 'Expire-Date': 0, 'Name-Real': 'Test Name', 'Name-Email': 'test.name@example.com', } cmd = gpg.gen_key_input(**params) result = gpg.gen_key(cmd) self.assertNotEqual(result.returncode, 0) self.assertEqual(result.status, 'key not created') def test_key_generation_input(self): "Test that key generation input handles empty values, curves etc." params = { 'key_type': ' ', 'key_length': 2048, } cmd = self.gpg.gen_key_input(**params) self.assertTrue('Key-Type: RSA\n' in cmd) params['key_type'] = 'DSA' cmd = self.gpg.gen_key_input(**params) self.assertTrue('Key-Type: DSA\n' in cmd) params = { 'key_type': 'ECDSA', 'key_curve': 'nistp384', 'subkey_type': 'ECDH', 'subkey_curve': 'nistp384', 'name_comment': 'NIST P-384', } cmd = self.gpg.gen_key_input(**params) for s in ('Key-Type: ECDSA', 'Key-Curve: nistp384', 'Subkey-Type: ECDH', 'Subkey-Curve: nistp384', 'Name-Comment: NIST P-384'): self.assertTrue('%s\n' % s in cmd) self.assertFalse('Key-Length: ' in cmd) def test_add_subkey(self): "Test that subkeys can be added" if self.gpg.version[0] < 2: # pragma: no cover raise unittest.SkipTest('Feature unavailable in GnuPG 1.x') master_key = self.generate_key('Charlie', 'Clark', 'gamma.com', passphrase='123', with_subkey=False) self.assertEqual(0, master_key.returncode, 'Non-zero return code') result = self.gpg.add_subkey(master_key=master_key.fingerprint, master_passphrase='123', algorithm='dsa', usage='sign', expire=0) self.assertEqual(0, result.returncode, 'Non-zero return code') pubkeys = self.gpg.list_keys() for key in pubkeys: sklist = key['subkeys'] skmap = key['subkey_info'] self.assertEqual(len(sklist), 1) self.assertTrue(len(skmap), 1) for sk in sklist: skid, capability, fp, grp = sk self.assertEqual(skmap[skid]['fingerprint'], fp) self.assertEqual(skmap[skid]['keygrip'], grp) def test_add_subkey_with_invalid_key_type(self): "Test that subkey generation handles invalid key type" if self.gpg.version[0] < 2: # pragma: no cover raise unittest.SkipTest('Feature unavailable in GnuPG 1.x') master_key = self.generate_key('Charlie', 'Clark', 'gamma.com', passphrase='123', with_subkey=False) self.assertEqual(0, master_key.returncode, 'Non-zero return code') result = self.gpg.add_subkey(master_key=master_key.fingerprint, master_passphrase='123', algorithm='INVALID', usage='sign', expire=0) self.assertFalse(result.data, 'Null data result') self.assertEqual('', result.fingerprint, 'Empty fingerprint result') self.assertEqual(2, result.returncode, 'Unexpected return code') def test_deletion_subkey(self): "Test that subkey deletion works" if self.gpg.version[0] < 2: # pragma: no cover raise unittest.SkipTest('Feature unavailable in GnuPG 1.x') master_key = self.generate_key('Charlie', 'Clark', 'gamma.com', passphrase='123', with_subkey=False) self.assertEqual(0, master_key.returncode, 'Non-zero return code') subkey = self.gpg.add_subkey(master_key=master_key.fingerprint, master_passphrase='123', algorithm='dsa', usage='sign', expire=0) self.assertEqual(0, subkey.returncode, 'Non-zero return code') public_keys = self.gpg.list_keys() key_info = public_keys[0] private_keys = self.gpg.list_keys(secret=True) secret_key_info = private_keys[0] self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 1), '1-element list expected') self.assertEqual(len(key_info['subkeys']), 1, '1-element list expected') self.assertTrue(is_list_with_len(private_keys, 1), '1-element list expected') self.assertEqual(len(secret_key_info['subkeys']), 1, '1-element list expected') result = self.gpg.delete_keys(subkey.fingerprint, secret=True, passphrase='123', exclamation_mode=True) result = self.gpg.delete_keys(subkey.fingerprint, exclamation_mode=True) self.assertEqual(0, result.returncode, 'Non-zero return code') public_keys = self.gpg.list_keys() key_info = public_keys[0] private_keys = self.gpg.list_keys(secret=True) secret_key_info = private_keys[0] self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 1), '1-element list expected') self.assertEqual(len(key_info['subkeys']), 0, '0-element list expected') self.assertTrue(is_list_with_len(private_keys, 1), '1-element list expected') self.assertEqual(len(secret_key_info['subkeys']), 0, '1-element list expected') def test_list_subkey_after_generation(self): "Test that after subkey generation, the generated subkey is available" if self.gpg.version[0] < 2: # pragma: no cover raise unittest.SkipTest('Feature unavailable in GnuPG 1.x') self.test_list_keys_initial() master_key = self.generate_key('Charlie', 'Clark', 'gamma.com', passphrase='123', with_subkey=False) self.assertEqual(0, master_key.returncode, 'Non-zero return code') subkey_sign = self.gpg.add_subkey(master_key=master_key.fingerprint, master_passphrase='123', algorithm='dsa', usage='sign', expire=0) self.assertEqual(0, subkey_sign.returncode, 'Non-zero return code') subkey_encrypt = self.gpg.add_subkey(master_key=master_key.fingerprint, master_passphrase='123', algorithm='rsa', usage='encrypt', expire=0) self.assertEqual(0, subkey_encrypt.returncode, 'Non-zero return code') public_keys = self.gpg.list_keys() self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 1), '1-element list expected') key_info = public_keys[0] if self.gpg.version >= (2, 1): self.assertTrue(key_info['keygrip']) fp = key_info['fingerprint'] self.assertTrue(fp in public_keys.key_map) self.assertTrue(public_keys.key_map[fp] is key_info) self.assertEqual(fp, master_key.fingerprint) self.assertTrue('subkey_info' in key_info) skinfo = key_info['subkey_info'] self.assertEqual(len(skinfo), 2) self.assertEqual(key_info['subkeys'][0][1], 's') self.assertEqual(key_info['subkeys'][0][2], subkey_sign.fingerprint) self.assertEqual(key_info['subkeys'][1][1], 'e') self.assertEqual(key_info['subkeys'][1][2], subkey_encrypt.fingerprint) for skid, _, sfp, grp in key_info['subkeys']: self.assertTrue(skid in skinfo) info = skinfo[skid] self.assertEqual(info['keyid'], skid) self.assertEqual(info['type'], 'sub') self.assertTrue(sfp in public_keys.key_map) self.assertTrue(public_keys.key_map[sfp] is key_info) if self.gpg.version >= (2, 1): self.assertTrue(grp) def test_list_keys_after_generation(self): "Test that after key generation, the generated key is available" self.test_list_keys_initial() self.do_key_generation() public_keys = self.gpg.list_keys() self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 1), '1-element list expected') key_info = public_keys[0] if self.gpg.version >= (2, 1): self.assertTrue(key_info['keygrip']) fp = key_info['fingerprint'] self.assertTrue(fp in public_keys.key_map) self.assertTrue(public_keys.key_map[fp] is key_info) self.assertTrue('subkey_info' in key_info) skinfo = key_info['subkey_info'] for skid, _, sfp, grp in key_info['subkeys']: self.assertTrue(skid in skinfo) info = skinfo[skid] self.assertEqual(info['keyid'], skid) self.assertEqual(info['type'], 'sub') self.assertTrue(sfp in public_keys.key_map) self.assertTrue(public_keys.key_map[sfp] is key_info) if self.gpg.version >= (2, 1): self.assertTrue(grp) # now test with sigs=True public_keys_sigs = self.gpg.list_keys(sigs=True) self.assertEqual(0, public_keys_sigs.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys_sigs, 1), '1-element list expected') key_info = public_keys_sigs[0] if self.gpg.version >= (2, 1): self.assertTrue(key_info['keygrip']) fp = key_info['fingerprint'] self.assertTrue(fp in public_keys_sigs.key_map) self.assertTrue(public_keys_sigs.key_map[fp] is key_info) self.assertTrue(is_list_with_len(key_info['sigs'], 2)) self.assertTrue('subkey_info' in key_info) skinfo = key_info['subkey_info'] for siginfo in key_info['sigs']: self.assertTrue(len(siginfo), 3) for skid, _, sfp, grp in key_info['subkeys']: self.assertTrue(skid in skinfo) info = skinfo[skid] self.assertEqual(info['keyid'], skid) self.assertEqual(info['type'], 'sub') self.assertTrue(sfp in public_keys_sigs.key_map) self.assertTrue(public_keys_sigs.key_map[sfp] is key_info) if self.gpg.version >= (2, 1): self.assertTrue(grp) private_keys = self.gpg.list_keys(secret=True) self.assertEqual(0, private_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(private_keys, 1), '1-element list expected') self.assertEqual(len(private_keys.fingerprints), 1) key_info = private_keys[0] if self.gpg.version >= (2, 1): self.assertTrue(key_info['keygrip']) self.assertTrue('subkey_info' in key_info) skinfo = key_info['subkey_info'] self.assertTrue(skid in skinfo) info = skinfo[skid] self.assertEqual(info['keyid'], skid) self.assertEqual(info['type'], 'ssb') # Now do the same test, but using keyring and secret_keyring arguments if self.gpg.version < (2, 1): # pragma: no cover pkn = 'pubring.gpg' skn = 'secring.gpg' else: # On GnuPG >= 2.1, --secret-keyring is obsolete and ignored, # and the keyring file name has changed. pkn = 'pubring.kbx' skn = None hd = self.homedir if os.name == 'posix': pkn = os.path.join(hd, pkn) if skn: # pragma: no cover skn = os.path.join(hd, skn) gpg = gnupg.GPG(gnupghome=hd, gpgbinary=GPGBINARY, keyring=pkn, secret_keyring=skn) logger.debug('Using keyring and secret_keyring arguments') public_keys_2 = gpg.list_keys() self.assertEqual(0, public_keys_2.returncode, 'Non-zero return code') self.assertEqual(public_keys_2, public_keys) private_keys_2 = gpg.list_keys(secret=True) self.assertEqual(0, private_keys_2.returncode, 'Non-zero return code') self.assertEqual(private_keys_2, private_keys) # generate additional keys so that we can test listing a subset of # keys def get_names(key_map): result = set() for info in key_map.values(): for uid in info['uids']: uid = uid.replace(' (A test user (insecure!))', '') result.add(uid) return result result = self.generate_key('Charlie', 'Clark', 'gamma.com') self.assertNotEqual(None, result, 'Non-null result') self.assertEqual(0, result.returncode, 'Non-zero return code') result = self.generate_key('Donna', 'Davis', 'delta.com') self.assertNotEqual(None, result, 'Non-null result') self.assertEqual(0, result.returncode, 'Non-zero return code') public_keys = gpg.list_keys() self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertEqual(len(public_keys), 3) actual = get_names(public_keys.key_map) expected = set([ 'Barbara Brown ', 'Charlie Clark ', 'Donna Davis ' ]) self.assertEqual(actual, expected) # specify a single key as a string public_keys = gpg.list_keys(keys='Donna Davis') self.assertEqual(0, public_keys.returncode, 'Non-zero return code') actual = get_names(public_keys.key_map) expected = set(['Donna Davis ']) self.assertEqual(actual, expected) # specify multiple keys public_keys = gpg.list_keys(keys=['Donna', 'Barbara']) self.assertEqual(0, public_keys.returncode, 'Non-zero return code') actual = get_names(public_keys.key_map) expected = set(['Barbara Brown ', 'Donna Davis ']) self.assertEqual(actual, expected) def test_key_trust(self): "Test that trusting keys works" gpg = self.gpg result = gpg.import_keys(KEYS_TO_IMPORT) self.assertEqual(0, result.returncode, 'Non-zero return code') keys = gpg.list_keys() self.assertEqual(0, keys.returncode, 'Non-zero return code') fingerprints = [] for key in keys: self.assertEqual(key['ownertrust'], '-') fingerprints.append(key['fingerprint']) cases = ( ('TRUST_NEVER', 'n'), ('TRUST_MARGINAL', 'm'), ('TRUST_FULLY', 'f'), ('TRUST_ULTIMATE', 'u'), ('TRUST_UNDEFINED', 'q'), ('TRUST_EXPIRED', 'e'), ) for param, expected in cases: gpg.trust_keys(fingerprints, param) keys = gpg.list_keys(keys=fingerprints) for key in keys: self.assertEqual(key['ownertrust'], expected) self.assertRaises(ValueError, gpg.trust_keys, fingerprints, 'TRUST_FOOBAR') self.assertRaises(ValueError, gpg.trust_keys, 'NO_SUCH_FINGERPRINT', 'TRUST_NEVER') # gpg should raise an error for the following - but it doesn't! # self.assertRaises(ValueError, gpg.trust_keys, # 'BADF00DBADF00DBADF00DBADF00DBADF00DBADF0', # 'TRUST_NEVER') def test_list_signatures(self): imported = self.gpg.import_keys(SIGNED_KEYS) self.assertEqual(0, imported.returncode, 'Non-zero return code') keys = self.gpg.list_keys(keys=['18897CA2']) self.assertEqual(0, keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(keys, 1), 'importing test signed key') sigs = self.gpg.list_keys(keys=['18897CA2'], sigs=True)[0]['sigs'] logger.debug('testing self-signature') self.assertTrue(('BC6F350FB3D04076', 'Joshua Calvert (A test user) ', '13x') in sigs) logger.debug('testing subkey self-signature') self.assertTrue(('BC6F350FB3D04076', 'Joshua Calvert (A test user) ', '18x') in sigs) logger.debug('testing other signature') self.assertTrue(('057CCF658C074FDA', 'Winston Smith (A test user) ', '10x') in sigs) def test_scan_keys(self): "Test that external key files can be scanned" # Don't use SkipTest for now, as not available for Python < 2.7 if self.gpg.version < (2, 1): # pragma: no cover expected = set([ 'Andrew Able (A test user) ', 'Barbara Brown (A test user) ', 'Charlie Clark (A test user) ', ]) test_files = ('test_pubring.gpg', 'test_secring.gpg') key_fn = None else: expected = set([ 'Gary Gross (A test user) ', 'Danny Davis (A test user) ', ]) fd, key_fn = tempfile.mkstemp(prefix='pygpg-test-') os.write(fd, KEYS_TO_IMPORT.encode('ascii')) os.close(fd) test_files = (key_fn, ) try: for fn in test_files: logger.debug('scanning keys in %s', fn) data = self.gpg.scan_keys(fn) self.assertEqual(0, data.returncode, 'Non-zero return code') uids = set() for d in data: uids.add(d['uids'][0]) self.assertEqual(uids, expected) finally: if key_fn: os.remove(key_fn) def test_scan_keys_mem(self): "Test that external keys in memory can be scanned" expected = set([ 'Gary Gross (A test user) ', 'Danny Davis (A test user) ', ]) for key in (KEYS_TO_IMPORT, ): logger.debug('testing scan_keys') data = self.gpg.scan_keys_mem(key) self.assertEqual(0, data.returncode, 'Non-zero return code') uids = set() for d in data: uids.add(d['uids'][0]) self.assertEqual(uids, expected) def test_quick_sign_key(self): "Test the quick-sign-key functionality" if self.gpg.version < (2, 0): # pragma: no cover raise unittest.SkipTest('No support for feature in gpg 1.x') # GPG requires real random when signing keys self.gpg.options.remove('--debug-quick-random') recipient_key = self.gpg.import_keys(RECIPIENT_KEY) certifying_key = self.gpg.import_keys(CERTIFYING_KEY) self.assertEqual(len(set(recipient_key.fingerprints)), 1) self.assertEqual(len(set(certifying_key.fingerprints)), 1) certifying_fingerprint = certifying_key.fingerprints[0] recipient_fingerprint = recipient_key.fingerprints[0] sign_result = self.gpg.quick_sign_key(certifying_fingerprint, recipient_fingerprint) self.assertEqual(sign_result.returncode, 0) sigs = self.gpg.list_keys(keys=recipient_fingerprint, sigs=True)[0]['sigs'] key_id = sigs[1][0] self.assertIn(key_id, certifying_key.fingerprints[0]) # Revert our test environment changes self.gpg.options.append('--debug-quick-random') def test_encryption_and_decryption(self): "Test that encryption and decryption works" key = self.generate_key('Andrew', 'Able', 'alpha.com', passphrase='andy') self.assertEqual(0, key.returncode, 'Non-zero return code') andrew = key.fingerprint key = self.generate_key('Barbara', 'Brown', 'beta.com') self.assertEqual(0, key.returncode, 'Non-zero return code') barbara = key.fingerprint gpg = self.gpg if gnupg._py3k: data = 'Hello, André!' else: # pragma: no cover data = unicode('Hello, André', gpg.encoding) data = data.encode(gpg.encoding) result = gpg.encrypt(data, barbara) self.assertEqual(0, result.returncode, 'Non-zero return code') edata = str(result) self.assertNotEqual(data, edata, 'Data must have changed') self.assertRaises(ValueError, gpg.decrypt, edata, passphrase='bbr\x00own') self.assertRaises(ValueError, gpg.decrypt, edata, passphrase='bbr\rown') self.assertRaises(ValueError, gpg.decrypt, edata, passphrase='bbr\nown') ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(0, ddata.returncode, 'Non-zero return code') if data != ddata.data: # pragma: no cover logger.debug('was: %r', data) logger.debug('new: %r', ddata.data) self.assertEqual(data, ddata.data, 'Round-trip must work') result = gpg.encrypt(data, [andrew, barbara]) self.assertEqual(0, result.returncode, 'Non-zero return code') edata = str(result) self.assertNotEqual(data, edata, 'Data must have changed') ddata = gpg.decrypt(edata, passphrase='andy') self.assertEqual(0, ddata.returncode, 'Non-zero return code') self.assertEqual(data, ddata.data, 'Round-trip must work') ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(data, ddata.data, 'Round-trip must work') # Test with hidden recipients result = gpg.encrypt(data, andrew, hidden_recipients=barbara) self.assertEqual(0, result.returncode, 'Non-zero return code') edata = str(result) self.assertNotEqual(data, edata, 'Data must have changed') ddata = gpg.decrypt(edata, passphrase='andy') self.assertEqual(0, ddata.returncode, 'Non-zero return code') self.assertEqual(data, ddata.data, 'Round-trip must work') ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(data, ddata.data, 'Round-trip must work') # Test only hidden recipients result = gpg.encrypt(data, None, hidden_recipients=[andrew, barbara]) self.assertEqual(0, result.returncode, 'Non-zero return code') edata = str(result) self.assertNotEqual(data, edata, 'Data must have changed') ddata = gpg.decrypt(edata, passphrase='andy') self.assertEqual(0, ddata.returncode, 'Non-zero return code') self.assertEqual(data, ddata.data, 'Round-trip must work') ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(data, ddata.data, 'Round-trip must work') # Test with no recipients self.assertRaises(ValueError, gpg.encrypt, data, None) self.assertRaises(ValueError, gpg.encrypt, data, None, hidden_recipients=None) self.assertRaises(ValueError, gpg.encrypt, data, None, hidden_recipients=None, symmetric=False) # Test symmetric encryption data = 'chippy was here' self.assertRaises(ValueError, gpg.encrypt, data, None, passphrase='bbr\x00own', symmetric=True) self.assertRaises(ValueError, gpg.encrypt, data, None, passphrase='bbr\rown', symmetric=True) self.assertRaises(ValueError, gpg.encrypt, data, None, passphrase='bbr\nown', symmetric=True) result = gpg.encrypt(data, None, passphrase='bbrown', symmetric=True) self.assertEqual(0, result.returncode, 'Non-zero return code') edata = str(result) ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(0, ddata.returncode, 'Non-zero return code') self.assertEqual(data, str(ddata)) # Test symmetric encryption with non-default cipher result = gpg.encrypt(data, None, passphrase='bbrown', symmetric='AES256') self.assertEqual(0, result.returncode, 'Non-zero return code') edata = str(result) ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(0, ddata.returncode, 'Non-zero return code') self.assertEqual(data, str(ddata)) # Test that you can't encrypt with no recipients self.assertRaises(ValueError, self.gpg.encrypt, data, []) # Test extra_args parameter result = gpg.encrypt(data, barbara, extra_args=['-z', '0']) self.assertEqual(0, result.returncode, 'Non-zero return code') edata = str(result) ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(data.encode('ascii'), ddata.data, 'Round-trip must work') # Test on_data functionality chunks = [] def collector(data): chunks.append(data) gpg.on_data = collector result = gpg.encrypt(data, barbara) self.assertEqual(0, result.returncode, 'Non-zero return code') self.assertIsNone(result.on_data_failure) edata = str(result) self.assertTrue(chunks) expected = type(chunks[0])().join(chunks) self.assertEqual(expected.decode('ascii'), edata) chunks = [] ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(0, ddata.returncode, 'Non-zero return code') self.assertEqual(data.encode('ascii'), ddata.data, 'Round-trip must work') self.assertIsNone(result.on_data_failure) expected = type(chunks[0])().join(chunks) self.assertEqual(expected.decode('ascii'), data) # test with on-data generating an exception def exceptor(data): raise ValueError('exception in on_data') chunks = [] gpg.on_data = exceptor ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertIs(type(ddata.on_data_failure), ValueError) self.assertEqual(str(ddata.on_data_failure), 'exception in on_data') # test signing with encryption and verification during decryption logger.debug('encrypting with signature') gpg.on_data = None result = gpg.encrypt(data, barbara, sign=andrew, passphrase='andy') self.assertEqual(0, result.returncode, 'Non-zero return code') edata = str(result) logger.debug('decrypting with verification') ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(0, ddata.returncode, 'Non-zero return code') self.assertEqual(data.encode('ascii'), ddata.data, 'Round-trip must work') sig_values = list(ddata.sig_info.values()) self.assertTrue(sig_values) sig_info = sig_values[0] self.assertEqual(sig_info['fingerprint'], andrew) logger.debug('decrypting with verification succeeded') def test_import_and_export(self): "Test that key import and export works" self.test_list_keys_initial() gpg = self.gpg result = gpg.import_keys(KEYS_TO_IMPORT) self.assertEqual(0, result.returncode, 'Non-zero return code') self.assertTrue(bool(result)) self.assertEqual(result.summary(), '2 imported') public_keys = gpg.list_keys() self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 2), '2-element list expected') private_keys = gpg.list_keys(secret=True) self.assertEqual(0, private_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(private_keys, 0), 'Empty list expected') ascii = gpg.export_keys([k['keyid'] for k in public_keys]) self.assertTrue(ascii.find('PGP PUBLIC KEY BLOCK') >= 0, 'Exported key should be public') ascii = ascii.replace('\r', '').strip() match = compare_keys(ascii, KEYS_TO_IMPORT) if match: # pragma: no cover logger.debug('was: %r', KEYS_TO_IMPORT) logger.debug('now: %r', ascii) self.assertEqual(0, match, 'Keys must match') # Generate a key so we can test exporting private keys key = self.do_key_generation() if self.gpg.version < (2, 1): # pragma: no cover passphrase = None else: passphrase = 'bbrown' ascii = gpg.export_keys(key.fingerprint, True, passphrase=passphrase) self.assertTrue(isinstance(ascii, gnupg.text_type)) self.assertTrue(ascii.find('PGP PRIVATE KEY BLOCK') >= 0, 'Exported key should be private') binary = gpg.export_keys(key.fingerprint, True, armor=False, passphrase=passphrase) self.assertFalse(isinstance(binary, gnupg.text_type)) # import a secret key, and confirm that it's found in the list of # secret keys. result = gpg.import_keys(SECRET_KEY) self.assertEqual(0, result.returncode, 'Non-zero return code') self.assertEqual(result.summary(), '1 imported') private_keys = gpg.list_keys(secret=True) self.assertTrue(is_list_with_len(private_keys, 2)) found = False for pk in private_keys: if pk['keyid'].endswith('D2209820'): found = True break self.assertTrue(found) self.assertEqual(pk['uids'][0], 'Autogenerated Key ') def test_import_only(self): "Test that key import works" self.test_list_keys_initial() result = self.gpg.import_keys(KEYS_TO_IMPORT) self.assertEqual(0, result.returncode, 'Non-zero return code') public_keys = self.gpg.list_keys() self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 2), '2-element list expected') private_keys = self.gpg.list_keys(secret=True) self.assertEqual(0, private_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(private_keys, 0), 'Empty list expected') ascii = self.gpg.export_keys([k['keyid'] for k in public_keys]) self.assertTrue(ascii.find('PGP PUBLIC KEY BLOCK') >= 0, 'Exported key should be public') ascii = ascii.replace('\r', '').strip() match = compare_keys(ascii, KEYS_TO_IMPORT) if match: # pragma: no cover logger.debug('was: %r', KEYS_TO_IMPORT) logger.debug('now: %r', ascii) self.assertEqual(0, match, 'Keys must match') def test_signature_verification(self): "Test that signing and verification works" key = self.generate_key('Andrew', 'Able', 'alpha.com') if gnupg._py3k: data = 'Hello, André!' else: # pragma: no cover data = unicode('Hello, André', self.gpg.encoding) data = data.encode(self.gpg.encoding) self.assertRaises(ValueError, self.gpg.sign, data, keyid=key.fingerprint, passphrase='bbr\x00own') self.assertRaises(ValueError, self.gpg.sign, data, keyid=key.fingerprint, passphrase='bbr\rown') self.assertRaises(ValueError, self.gpg.sign, data, keyid=key.fingerprint, passphrase='bbr\nown') sig = self.gpg.sign(data, keyid=key.fingerprint, passphrase='bbrown') self.assertFalse(sig, 'Bad passphrase should fail') sig = self.gpg.sign(data, keyid=key.fingerprint, passphrase='aable') self.assertEqual(0, sig.returncode, 'Non-zero return code') self.assertTrue(sig, 'Good passphrase should succeed') if sig.username: # pragma: no cover # not set in recent versions of GnuPG e.g. 2.2.5 self.assertTrue(sig.username.startswith('Andrew Able')) if sig.key_id: # pragma: no cover self.assertTrue(key.fingerprint.endswith(sig.key_id)) self.assertTrue(sig.hash_algo) logger.debug('verification start') verified = self.gpg.verify(sig.data) self.assertEqual(0, verified.returncode, 'Non-zero return code') logger.debug('verification end') if key.fingerprint != verified.fingerprint: # pragma: no cover logger.debug('key: %r', key.fingerprint) logger.debug('ver: %r', verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, 'Fingerprints must match') self.assertEqual(verified.trust_level, verified.TRUST_ULTIMATE) self.assertEqual(verified.trust_text, 'TRUST_ULTIMATE') data_file = open(self.test_fn, 'rb') sig = self.gpg.sign_file(data_file, keyid=key.fingerprint, passphrase='aable') self.assertEqual(0, sig.returncode, 'Non-zero return code') data_file.close() self.assertTrue(sig, 'File signing should succeed') self.assertTrue(sig.hash_algo) try: stream = gnupg._make_binary_stream(sig.data, self.gpg.encoding) verified = self.gpg.verify_file(stream) except UnicodeDecodeError: # pragma: no cover # sometimes happens in Python 2.6 from io import BytesIO verified = self.gpg.verify_file(BytesIO(sig.data)) self.assertEqual(0, verified.returncode, 'Non-zero return code') if key.fingerprint != verified.fingerprint: # pragma: no cover logger.debug('key: %r', key.fingerprint) logger.debug('ver: %r', verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, 'Fingerprints must match') data_file = open(self.test_fn, 'rb') sig = self.gpg.sign_file(data_file, keyid=key.fingerprint, passphrase='aable', detach=True) self.assertEqual(0, sig.returncode, 'Non-zero return code') data_file.close() self.assertTrue(sig, 'File signing should succeed') self.assertTrue(sig.hash_algo) try: file = gnupg._make_binary_stream(sig.data, self.gpg.encoding) verified = self.gpg.verify_file(file, self.test_fn) except UnicodeDecodeError: # pragma: no cover # sometimes happens in Python 2.6 from io import BytesIO verified = self.gpg.verify_file(BytesIO(sig.data)) self.assertEqual(0, verified.returncode, 'Non-zero return code') if key.fingerprint != verified.fingerprint: # pragma: no cover logger.debug('key: %r', key.fingerprint) logger.debug('ver: %r', verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, 'Fingerprints must match') # Test in-memory verification data_file = open(self.test_fn, 'rb') data = data_file.read() data_file.close() fd, fn = tempfile.mkstemp(prefix='pygpg-test-') os.write(fd, sig.data) os.close(fd) try: verified = self.gpg.verify_data(fn, data) finally: os.remove(fn) self.assertEqual(0, verified.returncode, 'Non-zero return code') if key.fingerprint != verified.fingerprint: # pragma: no cover logger.debug('key: %r', key.fingerprint) logger.debug('ver: %r', verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, 'Fingerprints must match') def test_signature_file(self): "Test that signing and verification works via the GPG output" key = self.generate_key('Andrew', 'Able', 'alpha.com') data_file = open(self.test_fn, 'rb') sig_file = self.test_fn + '.asc' sig = self.gpg.sign_file(data_file, keyid=key.fingerprint, passphrase='aable', detach=True, output=sig_file) self.assertEqual(0, sig.returncode, 'Non-zero return code') data_file.close() self.assertTrue(sig, 'File signing should succeed') self.assertTrue(sig.hash_algo) self.assertTrue(os.path.exists(sig_file)) # Test in-memory verification data_file = open(self.test_fn, 'rb') data = data_file.read() data_file.close() try: verified = self.gpg.verify_data(sig_file, data) except Exception as e: os.remove(sig_file) self.fail(e) self.assertTrue(verified.username.startswith('Andrew Able')) self.assertTrue(key.fingerprint.endswith(verified.key_id)) self.assertEqual(0, verified.returncode, 'Non-zero return code') if key.fingerprint != verified.fingerprint: # pragma: no cover logger.debug('key: %r', key.fingerprint) logger.debug('ver: %r', verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, 'Fingerprints must match') # Test file path verification try: verified = self.gpg.verify_file(sig_file, self.test_fn) finally: os.remove(sig_file) self.assertTrue(verified.username.startswith('Andrew Able')) self.assertTrue(key.fingerprint.endswith(verified.key_id)) self.assertEqual(0, verified.returncode, 'Non-zero return code') if key.fingerprint != verified.fingerprint: # pragma: no cover logger.debug('key: %r', key.fingerprint) logger.debug('ver: %r', verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, 'Fingerprints must match') def test_subkey_signature_file(self): "Test that signing and verification works via the GPG output for subkeys" if self.gpg.version[0] < 2: # pragma: no cover raise unittest.SkipTest('Feature unavailable in GnuPG 1.x') master_key = self.generate_key('Charlie', 'Clark', 'gamma.com', passphrase='123', with_subkey=False) self.assertEqual(0, master_key.returncode, 'Non-zero return code') subkey = self.gpg.add_subkey(master_key=master_key.fingerprint, master_passphrase='123', algorithm='dsa', usage='sign', expire=0) self.assertEqual(0, subkey.returncode, 'Non-zero return code') data_file = open(self.test_fn, 'rb') sig_file = self.test_fn + '.asc' sig = self.gpg.sign_file(data_file, keyid=subkey.fingerprint, passphrase='123', detach=True, output=sig_file) self.assertEqual(0, sig.returncode, 'Non-zero return code') data_file.close() self.assertTrue(sig, 'File signing should succeed') self.assertTrue(sig.hash_algo) self.assertTrue(os.path.exists(sig_file)) # Test in-memory verification data_file = open(self.test_fn, 'rb') data = data_file.read() data_file.close() try: verified = self.gpg.verify_data(sig_file, data) self.assertTrue(verified.username.startswith('Charlie Clark')) self.assertTrue(subkey.fingerprint.endswith(verified.key_id)) finally: os.remove(sig_file) self.assertEqual(0, verified.returncode, 'Non-zero return code') if subkey.fingerprint != verified.fingerprint: # pragma: no cover logger.debug('key: %r', subkey.fingerprint) logger.debug('ver: %r', verified.fingerprint) self.assertEqual(subkey.fingerprint, verified.fingerprint, 'Fingerprints must match') def test_deletion(self): "Test that key deletion works" result = self.gpg.import_keys(KEYS_TO_IMPORT) self.assertEqual(0, result.returncode, 'Non-zero return code') public_keys = self.gpg.list_keys() self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 2), '2-element list expected') result = self.gpg.delete_keys(public_keys[0]['fingerprint']) self.assertEqual(0, result.returncode, 'Non-zero return code') public_keys = self.gpg.list_keys() self.assertEqual(0, public_keys.returncode, 'Non-zero return code') self.assertTrue(is_list_with_len(public_keys, 1), '1-element list expected') def test_nogpg(self): "Test that absence of gpg is handled correctly" with self.assertRaises(OSError) as ar: gnupg.GPG(gnupghome=self.homedir, gpgbinary='frob') self.assertIn('frob', str(ar.exception)) def test_invalid_home(self): "Test that any specified gnupghome directory actually is one" hd = tempfile.mkdtemp(prefix='keys-') shutil.rmtree(hd) # make sure it isn't there anymore with self.assertRaises(ValueError) as ar: gnupg.GPG(gnupghome=hd) self.assertTrue('gnupghome should be a directory' in str(ar.exception)) def test_make_args(self): "Test argument line construction" self.gpg.options = ['--foo', '--bar'] args = self.gpg.make_args(['a', 'b'], False) self.assertTrue(len(args) > 4) self.assertEqual(args[-4:], ['--foo', '--bar', 'a', 'b']) def do_file_encryption_and_decryption(self, encfname, decfname): "Do the actual encryption/decryption test using given filenames" mode = None if os.name == 'posix': # pick a mode that won't be already in effect via umask if os.path.exists(encfname) and os.path.exists(decfname): mode = os.stat(encfname).st_mode | stat.S_IXUSR os.chmod(encfname, mode) # assume same for decfname os.chmod(decfname, mode) logger.debug('Encrypting to: %r', encfname) logger.debug('Decrypting to: %r', decfname) try: key = self.generate_key('Andrew', 'Able', 'alpha.com', passphrase='andy') self.assertEqual(0, key.returncode, 'Non-zero return code') andrew = key.fingerprint key = self.generate_key('Barbara', 'Brown', 'beta.com') self.assertEqual(0, key.returncode, 'Non-zero return code') barbara = key.fingerprint data = 'Hello, world!' stream = gnupg._make_binary_stream(data, self.gpg.encoding) edata = self.gpg.encrypt_file(stream, [andrew, barbara], armor=False, output=encfname) self.assertEqual(0, edata.returncode, 'Non-zero return code') efile = open(encfname, 'rb') ddata = self.gpg.decrypt_file(efile, passphrase='bbrown', output=decfname) self.assertEqual(0, ddata.returncode, 'Non-zero return code') efile.seek(0, os.SEEK_SET) edata = efile.read() efile.close() self.assertTrue(os.path.exists(decfname)) dfile = open(decfname, 'rb') ddata = dfile.read() dfile.close() data = data.encode(self.gpg.encoding) if ddata != data: # pragma: no cover logger.debug('was: %r', data) logger.debug('new: %r', ddata) self.assertEqual(data, ddata, 'Round-trip must work') # Try opening the encrypted file in text mode (Issue #39) # this doesn't fail in 2.x if gnupg._py3k: logger.debug('about to pass text stream to decrypt_file') with open(encfname, 'r') as efile: self.assertRaises(UnicodeDecodeError, self.gpg.decrypt_file, efile, passphrase='bbrown', output=decfname) finally: for fn in (encfname, decfname): if os.name == 'posix' and mode is not None: # Check that the file wasn't deleted, and that the # mode bits we set are still in effect self.assertEqual(os.stat(fn).st_mode, mode) if os.path.exists(fn): os.remove(fn) def test_file_encryption_and_decryption(self): "Test that encryption/decryption to/from file works" encfno, encfname = tempfile.mkstemp(prefix='pygpg-test-') decfno, decfname = tempfile.mkstemp(prefix='pygpg-test-') # On Windows, if the handles aren't closed, the files can't be deleted os.close(encfno) os.close(decfno) self.do_file_encryption_and_decryption(encfname, decfname) @skipIf(os.name == 'nt', 'Test not suitable for Windows') def test_invalid_outputs(self): "Test encrypting to invalid output files" encfno, encfname = tempfile.mkstemp(prefix='pygpg-test-') os.close(encfno) os.chmod(encfname, 0o400) cases = ( ('/dev/null/foo', 'encrypt: not a directory'), (encfname, 'encrypt: permission denied'), ) key = self.generate_key('Barbara', 'Brown', 'beta.com') barbara = key.fingerprint data = 'Hello, world!' for badout, message in cases: stream = gnupg._make_binary_stream(data, self.gpg.encoding) try: # On Ubuntu and pypy-2.7, you often get an IOError "Broken pipe" # during the encrypt operation ... edata = self.gpg.encrypt_file(stream, barbara, armor=False, output=badout) self.assertEqual(2, edata.returncode, 'Unexpected return code') except IOError: pass # on GnuPG 1.4, you sometimes don't get any FAILURE messages, in # which case status will not be set if edata.status: self.assertEqual(edata.status, message) # now try with custom error map, if available if os.path.exists('messages.json'): with open('messages.json') as f: mdata = json.load(f) messages = {} for k, v in mdata.items(): messages[int(k, 16)] = v self.gpg.error_map = messages encfno, encfname = tempfile.mkstemp(prefix='pygpg-test-') os.close(encfno) os.chmod(encfname, 0o400) try: cases = ( ('/dev/null/foo', 'encrypt: Not a directory'), (encfname, 'encrypt: Permission denied'), ) for badout, message in cases: stream = gnupg._make_binary_stream(data, self.gpg.encoding) try: # On Ubuntu and pypy-2.7, you often get an IOError "Broken pipe" # during the encrypt operation ... edata = self.gpg.encrypt_file(stream, barbara, armor=False, output=badout) self.assertEqual(2, edata.returncode, 'Unexpected return code') except IOError: pass # on GnuPG 1.4, you sometimes don't get any FAILURE messages, in # which case status will not be set if edata.status: message = '%s (%s)' % (message, badout) self.assertIn(edata.status, message) finally: os.chmod(encfname, 0o700) os.remove(encfname) def test_filenames_with_spaces(self): # See Issue #16 "Test that filenames with spaces are correctly handled" d = tempfile.mkdtemp() try: encfname = os.path.join(d, 'encrypted file') decfname = os.path.join(d, 'decrypted file') self.do_file_encryption_and_decryption(encfname, decfname) finally: shutil.rmtree(d, ignore_errors=True) # This test does nothing on CI because it often leads to failures due to # external servers being down def test_search_keys(self): # pragma: no cover "Test that searching for keys works" if 'NO_EXTERNAL_TESTS' not in os.environ: r = self.gpg.search_keys('') self.assertEqual(0, r.returncode, 'Non-zero return code') self.assertTrue(r) self.assertTrue('Vinay Sajip ' in r[0]['uids']) r = self.gpg.search_keys('92905378') self.assertEqual(0, r.returncode, 'Non-zero return code') self.assertTrue(r) self.assertTrue('Vinay Sajip ' in r[0]['uids']) def test_quote_with_shell(self): "Test shell quoting with a real shell" if os.name != 'posix': return from subprocess import PIPE, Popen workdir = tempfile.mkdtemp() try: s = "'\\\"; touch %s/foo #'" % workdir cmd = 'echo %s' % gnupg.shell_quote(s) p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) p.communicate() self.assertEqual(p.returncode, 0) files = os.listdir(workdir) self.assertEqual(files, []) fn = "'ab?'" cmd = 'touch %s/%s' % (workdir, gnupg.shell_quote(fn)) p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) p.communicate() self.assertEqual(p.returncode, 0) files = os.listdir(workdir) self.assertEqual(files, ["'ab?'"]) finally: shutil.rmtree(workdir, ignore_errors=True) def disabled_test_signing_with_uid(self): # pragma: no cover "Test that signing with uids works. On hold for now." self.generate_key('Andrew', 'Able', 'alpha.com') uid = self.gpg.list_keys(True)[-1]['uids'][0] try: signfile = open(self.test_fn, 'rb') signed = self.gpg.sign_file(signfile, keyid=uid, passphrase='aable', detach=True) finally: signfile.close() self.assertEqual(0, signed.returncode, 'Non-zero return code') self.assertTrue(signed.data) def test_doctest_import_keys(self): """ Because GnuPG 2.1 requires passphrases for exporting and deleting secret keys, and because console-mode passphrase entry requires configuration changes, doctests can't always be used. This test replicates the original doctest for import_keys as a regular test. >>> import shutil >>> shutil.rmtree("keys", ignore_errors=True) >>> GPGBINARY = os.environ.get('GPGBINARY', 'gpg') >>> gpg = GPG(gpgbinary=GPGBINARY, gnupghome="keys") >>> input = gpg.gen_key_input(name_email='user1@test', passphrase='pp1') >>> result = gpg.gen_key(input) >>> fp1 = result.fingerprint >>> result = gpg.gen_key(input) >>> fp2 = result.fingerprint >>> pubkey1 = gpg.export_keys(fp1) >>> seckey1 = gpg.export_keys(fp1, secret=True, passphrase='pp1') >>> seckeys = gpg.list_keys(secret=True) >>> pubkeys = gpg.list_keys() >>> assert fp1 in seckeys.fingerprints >>> assert fp1 in pubkeys.fingerprints >>> str(gpg.delete_keys(fp1)) 'Must delete secret key first' >>> str(gpg.delete_keys(fp1, secret=True, passphrase='pp1')) 'ok' >>> str(gpg.delete_keys(fp1)) 'ok' >>> str(gpg.delete_keys("nosuchkey")) 'No such key' >>> seckeys = gpg.list_keys(secret=True) >>> pubkeys = gpg.list_keys() >>> assert not fp1 in seckeys.fingerprints >>> assert not fp1 in pubkeys.fingerprints >>> result = gpg.import_keys('foo') >>> assert not result >>> result = gpg.import_keys(pubkey1) >>> pubkeys = gpg.list_keys() >>> seckeys = gpg.list_keys(secret=True) >>> assert not fp1 in seckeys.fingerprints >>> assert fp1 in pubkeys.fingerprints >>> result = gpg.import_keys(seckey1) >>> assert result >>> seckeys = gpg.list_keys(secret=True) >>> pubkeys = gpg.list_keys() >>> assert fp1 in seckeys.fingerprints >>> assert fp1 in pubkeys.fingerprints >>> assert fp2 in pubkeys.fingerprints """ gpg = self.gpg inp = gpg.gen_key_input(name_email='user1@test', passphrase='pp1') result = gpg.gen_key(inp) fp1 = result.fingerprint inp = gpg.gen_key_input(name_email='user2@test', passphrase='pp2') result = gpg.gen_key(inp) self.assertEqual(0, result.returncode, 'Non-zero return code') fp2 = result.fingerprint pubkey1 = gpg.export_keys(fp1) self.assertTrue(pubkey1) if gpg.version >= (2, 1): passphrase = 'pp1' else: # pragma: no cover passphrase = None seckey1 = gpg.export_keys(fp1, secret=True, passphrase=passphrase) self.assertTrue(seckey1) seckeys = gpg.list_keys(secret=True) self.assertEqual(0, seckeys.returncode, 'Non-zero return code') pubkeys = gpg.list_keys() self.assertEqual(0, pubkeys.returncode, 'Non-zero return code') for fp in (fp1, fp2): for keys in (seckeys, pubkeys): self.assertIn(fp, keys.fingerprints) result = gpg.delete_keys(fp1) self.assertEqual(2, result.returncode, 'Unexpected return code') self.assertEqual(str(result), 'Must delete secret key first') if gpg.version < (2, 1): # pragma: no cover # Doesn't work on 2.1, and can't use SkipTest due to having # to support older Pythons result = gpg.delete_keys(fp1, secret=True, passphrase=passphrase) self.assertEqual(0, result.returncode, 'Non-zero return code') self.assertEqual(str(result), 'ok') result = gpg.delete_keys(fp1) self.assertEqual(0, result.returncode, 'Non-zero return code') self.assertEqual(str(result), 'ok') result = gpg.delete_keys('nosuchkey') self.assertEqual(2, result.returncode, 'Unexpected return code') self.assertEqual(str(result), 'No such key') seckeys = gpg.list_keys(secret=True) self.assertEqual(0, seckeys.returncode, 'Non-zero return code') pubkeys = gpg.list_keys() self.assertEqual(0, pubkeys.returncode, 'Non-zero return code') self.assertFalse(fp1 in seckeys.fingerprints) self.assertFalse(fp1 in pubkeys.fingerprints) result = gpg.import_keys('foo') self.assertFalse(result) def test_recv_keys_no_server(self): result = self.gpg.recv_keys('foo.bar.baz', '92905378') self.assertEqual(2, result.returncode, 'Unexpected return code') self.assertEqual(result.summary(), '0 imported') def test_invalid_fileobject(self): # accidentally on purpose pass in a filename rather than the file itself bad = b'foobar.txt' with self.assertRaises((TypeError, ValueError)) as ec: self.gpg.decrypt_file(bad, passphrase='', output='/tmp/decrypted.txt') if gnupg._py3k: expected = 'Not a valid file or path: %s' % bad else: expected = 'No such file: %s' % bad self.assertEqual(str(ec.exception), expected) def remove_all_existing_keys(self): for root, dirs, files in os.walk(self.homedir): for d in dirs: p = os.path.join(root, d) shutil.rmtree(p) for f in files: if f.endswith('.conf'): continue p = os.path.join(root, f) os.remove(p) def test_no_such_key(self): key = self.generate_key('Barbara', 'Brown', 'beta.com') barbara = key.fingerprint gpg = self.gpg if gnupg._py3k: data = 'Hello, André!' else: # pragma: no cover data = unicode('Hello, André', gpg.encoding) data = data.encode(gpg.encoding) encrypted = gpg.encrypt(data, barbara) self.remove_all_existing_keys() decrypted = gpg.decrypt(str(encrypted), passphrase='bbrown') self.assertFalse(decrypted.ok) expected = {'decryption failed', 'no secret key', 'no data was provided'} self.assertIn(decrypted.status, expected) def test_get_recipients(self): gpg = self.gpg inp = gpg.gen_key_input(name_email='user1@test', passphrase='pp1') key1 = gpg.gen_key(inp) inp = gpg.gen_key_input(name_email='user2@test', passphrase='pp2') key2 = gpg.gen_key(inp) data = 'super secret'.encode(gpg.encoding) edata = gpg.encrypt(data, (key1.fingerprint, key2.fingerprint)) logger.debug('Getting recipients') ids = gpg.get_recipients(edata.data.decode(gpg.encoding)) self.assertGreater(len(ids), 0) idlen = len(ids[0]) ids = set(ids) expected = set((key1.fingerprint[-idlen:], key2.fingerprint[-idlen:])) self.assertEqual(expected, ids) def test_passing_paths(self): key1 = self.generate_key('Andrew', 'Able', 'alpha.com', passphrase='andy') self.assertEqual(0, key1.returncode, 'Non-zero return code') andrew = key1.fingerprint key2 = self.generate_key('Barbara', 'Brown', 'beta.com') self.assertEqual(0, key2.returncode, 'Non-zero return code') barbara = key2.fingerprint data = b'Hello, world!' fd, fn = tempfile.mkstemp(prefix='pygpg-test-') os.write(fd, data) os.close(fd) gpg = self.gpg try: # Check encryption edata = gpg.encrypt_file(fn, [andrew, barbara], armor=False) self.assertEqual(0, edata.returncode, 'Non-zero return code') self.assertEqual(edata.status, 'encryption ok') with open(fn, 'wb') as f: f.write(edata.data) # Check getting recipients ids = gpg.get_recipients_file(fn) idlen = len(ids[0]) keys = gpg.list_keys() expected = set(d['subkeys'][0][0][-idlen:] for d in keys) self.assertEqual(set(ids), expected) # Check decryption ddata = gpg.decrypt_file(fn, passphrase='andy') self.assertEqual(0, ddata.returncode, 'Non-zero return code') self.assertEqual(ddata.status, 'decryption ok') self.assertEqual(ddata.data, data) # Check signing with open(fn, 'wb') as f: f.write(data) sig = gpg.sign_file(fn, keyid=andrew, passphrase='andy', binary=True) self.assertEqual(0, sig.returncode, 'Non-zero return code') self.assertEqual(sig.status, 'signature created') # Check verification with open(fn, 'wb') as f: f.write(sig.data) verified = gpg.verify_file(fn) self.assertEqual(0, verified.returncode, 'Non-zero return code') self.assertEqual(verified.status, 'signature valid') self.assertTrue(verified.valid) # Check importing keys with open(fn, 'wb') as f: f.write(KEYS_TO_IMPORT.encode('ascii')) result = gpg.import_keys_file(fn) self.assertEqual(0, result.returncode, 'Non-zero return code') self.assertEqual(result.imported, 2) finally: os.remove(fn) def test_multiple_signatures(self): gpg = self.gpg key1 = self.generate_key('Andrew', 'Able', 'alpha.com') key2 = self.generate_key('Barbara', 'Brown', 'beta.com') data = b'signed data' sig1 = gpg.sign(data, keyid=key1.fingerprint, passphrase='aable', detach=True) sig2 = gpg.sign(data, keyid=key2.fingerprint, passphrase='bbrown', detach=True) # Combine the signatures, then verify fd, fn = tempfile.mkstemp(prefix='pygpg-test-') os.write(fd, sig1.data) os.write(fd, sig2.data) os.close(fd) try: verified = self.gpg.verify_data(fn, data) sig_info = verified.sig_info self.assertEqual(len(sig_info), 2) actual = set(d['fingerprint'] for d in sig_info.values()) expected = set((key1.fingerprint, key2.fingerprint)) self.assertEqual(actual, expected) finally: os.remove(fn) def test_multiple_signatures_one_invalid(self): gpg = self.gpg key1 = self.generate_key('Andrew', 'Able', 'alpha.com') key2 = self.generate_key('Barbara', 'Brown', 'beta.com') data = b'signed data' other_data = b'other signed data' sig1 = gpg.sign(data, keyid=key1.fingerprint, passphrase='aable', detach=True) sig2 = gpg.sign(other_data, keyid=key2.fingerprint, passphrase='bbrown', detach=True) # Combine the signatures, then verify fd, fn = tempfile.mkstemp(prefix='pygpg-test-') os.write(fd, sig1.data) os.write(fd, sig2.data) os.close(fd) try: verified = self.gpg.verify_data(fn, data) sig_info = verified.sig_info self.assertEqual(len(sig_info), 1) actual = set(d['fingerprint'] for d in sig_info.values()) expected = set([key1.fingerprint]) self.assertEqual(actual, expected) problems = verified.problems self.assertEqual(len(problems), 1) d = problems[0] self.assertEqual(d['status'], 'signature bad') self.assertTrue(key2.fingerprint.endswith(d['keyid'])) finally: os.remove(fn) @skipIf('CI' not in os.environ, "Don't test locally") def test_auto_key_locating(self): # Let's hope ProtonMail doesn't change their key anytime soon expected_fingerprint = '90E619A84E85330A692F6D81A655882018DBFA9D' # expected_type = 'rsa2048' actual = self.gpg.auto_locate_key('no-reply@protonmail.com') self.assertEqual(actual.fingerprint, expected_fingerprint) def test_passphrase_encoding(self): self.assertRaises(UnicodeEncodeError, self.gpg.decrypt, 'foo', passphrase=u'I’ll') def test_configured_group(self): # See issue #249 conf = 'group somegroup = BADF00D15BAD\n' fn = os.path.join(self.homedir, 'gpg.conf') with open(fn, 'w') as f: f.write(conf) gpg = gnupg.GPG(gnupghome=self.homedir, gpgbinary=GPGBINARY) self.assertEqual(gpg.version, self.gpg.version) def test_exception_propagation(self): if sys.version_info[0] < 3: raise unittest.SkipTest('python 2 is too loose with Unicode') key = self.generate_key('Andrew', 'Able', 'alpha.com', passphrase='andy') self.assertEqual(0, key.returncode, 'Non-zero return code') andrew = key.fingerprint stream = io.StringIO(u'Hello, world!') # make the wrong type of stream self.assertRaises(TypeError, self.gpg.encrypt_file, stream, [andrew], armor=False) TEST_GROUPS = { 'sign': set(['test_signature_verification', 'test_signature_file', 'test_subkey_signature_file']), 'crypt': set([ 'test_encryption_and_decryption', 'test_file_encryption_and_decryption', 'test_filenames_with_spaces', 'test_invalid_outputs', 'test_no_such_key' ]), 'key': set([ 'test_deletion', 'test_import_and_export', 'test_list_keys_after_generation', 'test_list_signatures', 'test_key_generation_with_invalid_key_type', 'test_key_generation_with_escapes', 'test_key_generation_input', 'test_key_generation_with_colons', 'test_search_keys', 'test_scan_keys', 'test_scan_keys_mem', 'test_key_trust', 'test_add_subkey', 'test_add_subkey_with_invalid_key_type', 'test_deletion_subkey', 'test_list_subkey_after_generation', 'test_quick_sign_key' ]), 'import': set(['test_import_only', 'test_doctest_import_keys']), 'basic': set(['test_environment', 'test_list_keys_initial', 'test_nogpg', 'test_make_args', 'test_quote_with_shell']), 'test': set(['test_add_subkey']), } def suite(args=None): if args is None: # pragma: no cover args = sys.argv[1:] if not args: result = unittest.TestLoader().loadTestsFromTestCase(GPGTestCase) else: # pragma: no cover tests = set() for arg in args: if arg in TEST_GROUPS: tests.update(TEST_GROUPS[arg]) else: print('Ignoring unknown test group %r' % arg) result = unittest.TestSuite(list(map(GPGTestCase, tests))) return result def init_logging(): class PrimegenFilter(logging.Filter): def filter(self, record): arg = record.args if isinstance(arg, (list, tuple)) and len(arg) > 0: arg = arg[0] return not arg or not isinstance(arg, unicode) or '[GNUPG:] PROGRESS primegen' not in arg logging.basicConfig(level=logging.DEBUG, filename='test_gnupg.log', filemode='w', format='%(asctime)s %(levelname)-5s %(name)-10s ' '%(threadName)-10s %(lineno)4d %(message)s') logging.root.handlers[0].addFilter(PrimegenFilter()) def main(): init_logging() logger.debug('Python version: %s', sys.version.replace('\n', ' ')) adhf = argparse.ArgumentDefaultsHelpFormatter ap = argparse.ArgumentParser(formatter_class=adhf, prog='test_gnupg') aa = ap.add_argument aa('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') options, args = ap.parse_known_args() tests = suite(args) verbosity = 2 if options.verbose else 1 results = unittest.TextTestRunner(verbosity=verbosity).run(tests) failed = not results.wasSuccessful() if failed and 'TOXENV' in os.environ and os.name != 'posix': # pragma: no cover os.system('type test_gnupg.log') return failed if __name__ == '__main__': sys.exit(main()) ================================================ FILE: tox.ini ================================================ # tox (https://tox.readthedocs.io/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py{27, 38, 39, 310, 311, 312, 313, 314, py, 27gpg2, 38gpg2, 39gpg2, 310gpg2, 311gpg2, 312gpg2, 313gpg2, 314gpg2, pygpg2} isolated_build = True requires = virtualenv<20.22.0 [testenv] commands = {envpython} test_gnupg.py setenv = NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU STATIC_TEST_HOMEDIR deps = [testenv:py27gpg2] envdir = {toxinidir}/.tox/py27 basepython = python2.7 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:py36gpg2] envdir = {toxinidir}/.tox/py36 basepython = python3.6 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:py37gpg2] envdir = {toxinidir}/.tox/py37 basepython = python3.7 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:py38gpg2] envdir = {toxinidir}/.tox/py38 basepython = python3.8 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:py39gpg2] envdir = {toxinidir}/.tox/py39 basepython = python3.9 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:py310gpg2] envdir = {toxinidir}/.tox/py310 basepython = python3.10 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:py311gpg2] envdir = {toxinidir}/.tox/py311 basepython = python3.11 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:py312gpg2] envdir = {toxinidir}/.tox/py312 basepython = python3.12 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 [testenv:py313gpg2] envdir = {toxinidir}/.tox/py313 basepython = python3.13 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:py314gpg2] envdir = {toxinidir}/.tox/py314 basepython = python3.14 setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU [testenv:pypygpg2] envdir = {toxinidir}/.tox/pypy basepython = pypy setenv = LD_LIBRARY_PATH=/home/vinay/tmp/lib GPGBINARY=gpg2 NO_EXTERNAL_TESTS=1 passenv = HOME ENABLE_TOFU