New Upstream Release - python-pem

Ready changes

Summary

Merged new upstream version: 21.2.0 (was: 19.1.0).

Resulting package

Built on 2023-05-31T18:48 (took 3m59s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-pem

Lintian Result

Diff

diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 3ce5b0f..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,12 +0,0 @@
-[run]
-branch = True
-source = pem
-
-[paths]
-source =
-   src/pem
-   .tox/*/lib/python*/site-packages/pem
-   .tox/pypy*/site-packages/pem
-
-[report]
-show_missing = True
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1e24855..86917c7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,27 +1,26 @@
+---
 repos:
-  - repo: https://github.com/ambv/black
-    rev: 19.3b0
+  - repo: https://github.com/psf/black
+    rev: 20.8b1
     hooks:
       - id: black
-        language_version: python3.7
-        # override until resolved: https://github.com/ambv/black/issues/402
-        files: \.pyi?$
-        types: []
-  - repo: https://github.com/asottile/seed-isort-config
-    rev: v1.7.0
-    hooks:
-      - id: seed-isort-config
-  - repo: https://github.com/pre-commit/mirrors-isort
-    rev: v4.3.15
+        language_version: python3.8
+
+  - repo: https://github.com/PyCQA/isort
+    rev: 5.8.0
     hooks:
       - id: isort
-        language_version: python3.7
+        additional_dependencies: [toml]
+
+  - repo: https://github.com/pycqa/flake8
+    rev: 3.9.0
+    hooks:
+    - id: flake8
+      language_version: python3.8
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v2.1.0
+    rev: v3.4.0
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
       - id: debug-statements
-      - id: flake8
-        language_version: python3.7
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index cd6e9ac..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,92 +0,0 @@
-dist: xenial
-group: travis_latest
-cache:
-  directories:
-    - $HOME/.cache/pip
-
-language: python
-
-
-matrix:
-  include:
-    # Lint
-    - python: "3.7"
-      stage: lint
-      env: TOXENV=lint
-    - python: "3.7"
-      env: TOXENV=mypy
-    - python: "3.7"
-      env: TOXENV=manifest
-
-    # Test
-    - python: "2.7"
-      stage: test
-      env: TOXENV=py27-twisted
-    - python: "2.7"
-      env: TOXENV=py27
-    - python: "3.4"
-      env: TOXENV=py34-twisted
-    - python: "3.4"
-      env: TOXENV=py34
-    - python: "3.5"
-      env: TOXENV=py35-twisted
-    - python: "3.5"
-      env: TOXENV=py35
-    - python: "3.6"
-      env: TOXENV=py36-twisted
-    - python: "3.6"
-      env: TOXENV=py36
-    - python: "3.7"
-      env: TOXENV=py37
-    - python: "3.7"
-      env: TOXENV=py37-twisted
-    - python: "pypy"
-      env: TOXENV=pypy-twisted
-      dist: trusty
-    - python: "pypy"
-      env: TOXENV=pypy
-      dist: trusty
-    - python: "pypy3"
-      env: TOXENV=pypy3
-      dist: trusty
-    - python: "pypy3"
-      env: TOXENV=pypy3-twisted
-      dist: trusty
-
-    # Prevent breakage by a new releases
-    - python: "3.7-dev"
-      env: TOXENV=py37
-    - python: "3.7-dev"
-      env: TOXENV=py37-twisted
-
-
-    # Docs
-    - python: "3.7"
-      stage: docs
-      env: TOXENV=docs
-    - python: "3.7"
-      env: TOXENV=pypi-description
-
-  allow_failures:
-    - python: "3.7-dev"
-
-
-install:
-  - pip install tox
-
-
-script:
-  - tox
-
-
-before_install:
-  - pip install codecov
-
-
-after_success:
-  - tox -e coverage-report
-  - codecov
-
-
-notifications:
-  email: false
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 200e809..01ec4e6 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -1,7 +1,7 @@
 Credits
 =======
 
-``pem`` is written and maintained by Hynek Schlawack.
+*pem* is written and maintained by Hynek Schlawack.
 
 The development is kindly supported by `Variomedia AG <https://www.variomedia.de/>`_.
 
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9aa381d..910d97f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,6 +7,143 @@ Versions are year-based with a strict backward compatibility policy.
 The third digit is only for regressions.
 
 
+21.2.0 (2021-04-07)
+-------------------
+
+Backward-incompatible changes:
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+*none*
+
+
+Deprecations:
+^^^^^^^^^^^^^
+
+*none*
+
+
+Changes:
+^^^^^^^^
+
+- Added support for ``pem.OpenSSLTrustedCertificate`` (``-----BEGIN TRUSTED CERTIFICATE-----``), as defined in `openssl x509 manual <https://www.openssl.org/docs/man1.1.1/man1/x509.html>`_.
+  `#28 <https://github.com/hynek/pem/issues/28>`_
+
+
+----
+
+
+21.1.0 (2021-01-22)
+-------------------
+
+
+Backward-incompatible changes:
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+*none*
+
+
+Deprecations:
+^^^^^^^^^^^^^
+
+*none*
+
+
+Changes:
+^^^^^^^^
+
+- Added support for DSA private keys (``BEGIN DSA PRIVATE``).
+  This is also the OpenSSH legacy PEM format.
+  `#49 <https://github.com/hynek/pem/issues/49>`_
+- Added support for ``pem.SSHPublicKey`` (``---- BEGIN SSH2 PUBLIC KEY ----``), as defined in `RFC 4716 <https://tools.ietf.org/html/rfc4716>`_.
+  `#46 <https://github.com/hynek/pem/pull/46>`_
+- Added support for ``pem.SSHCOMPrivateKey`` (``---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----``), the SSH.com / Tectia private key format (plain or encrypted).
+  `#46 <https://github.com/hynek/pem/pull/46>`_
+
+
+----
+
+
+20.1.0 (2020-01-06)
+-------------------
+
+
+Backward-incompatible changes:
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+*none*
+
+
+Deprecations:
+^^^^^^^^^^^^^
+
+*none*
+
+
+Changes:
+^^^^^^^^
+
+- Carriage returns (``\r``) are now stripped before hashing *pem* objects to provide consistent hashes across platforms.
+  `#40 <https://github.com/hynek/pem/issues/40>`_
+
+
+----
+
+
+19.3.0 (2019-10-16)
+-------------------
+
+
+Backward-incompatible changes:
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+- Python 3.4 is not supported anymore.
+  It has been unsupported by the Python core team for a while now and its PyPI downloads are negligible.
+
+  It's very unlikely that *pem* will break under 3.4 anytime soon, but we don't test it anymore.
+
+
+Deprecations:
+^^^^^^^^^^^^^
+
+*none*
+
+
+Changes:
+^^^^^^^^
+
+- Added support for ``pem.OpenSSHPrivateKey`` (``OPENSSH PRIVATE KEY``).
+  OpenSSH added a new ``BEGIN`` label when it switched to a proprietary key encoding.
+  `#39 <https://github.com/hynek/pem/pull/39>`_
+
+
+----
+
+
+19.2.0 (2019-08-06)
+-------------------
+
+
+Backward-incompatible changes:
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+*none*
+
+
+Deprecations:
+^^^^^^^^^^^^^
+
+*none*
+
+
+Changes:
+^^^^^^^^
+
+- Added support for ``pem.ECPrivateKey`` (``EC PRIVATE KEY``).
+
+
+----
+
+
 19.1.0 (2019-03-19)
 -------------------
 
@@ -81,7 +218,7 @@ Deprecations:
 Changes:
 ^^^^^^^^
 
-- ``pem`` now ships with typing information that can be used by type checkers like `mypy <http://mypy-lang.org>`_.
+- *pem* now ships with typing information that can be used by type checkers like `mypy <http://mypy-lang.org>`_.
 - PEM objects now have an ``obj.sha1_hexdigest`` property with the SHA-1 digest of the stored bytes  as a native string.
   This is the same digest as the one that is used by the PEM objects' ``__repr__``\ s.
 - PEM objects now have an ``obj.as_text()`` method that returns the PEM-encoded content as unicode, always.
@@ -151,7 +288,7 @@ Changes:
 
 - PKCS #8 keys are now supported.
   `#14 <https://github.com/hynek/pem/pull/14>`_
-- ``pem`` is now fully functional without installing Twisted.
+- *pem* is now fully functional without installing Twisted.
   `#16 <https://github.com/hynek/pem/pull/16>`_
 
 
diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst
deleted file mode 100644
index 56e8914..0000000
--- a/CODE_OF_CONDUCT.rst
+++ /dev/null
@@ -1,55 +0,0 @@
-Contributor Covenant Code of Conduct
-====================================
-
-Our Pledge
-----------
-
-In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
-
-Our Standards
--------------
-
-Examples of behavior that contributes to creating a positive environment include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a professional setting
-
-Our Responsibilities
---------------------
-
-Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
-
-Scope
------
-
-This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
-Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
-Representation of a project may be further defined and clarified by project maintainers.
-
-Enforcement
------------
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hs@ox.cx.
-All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
-The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
-
-Attribution
------------
-
-This Code of Conduct is adapted from the `Contributor Covenant <https://www.contributor-covenant.org>`_, version 1.4, available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
deleted file mode 100644
index dded34d..0000000
--- a/CONTRIBUTING.rst
+++ /dev/null
@@ -1,185 +0,0 @@
-How To Contribute
-=================
-
-First off, thank you for considering contributing to ``pem``!
-It's people like *you* who make it is such a great tool for everyone.
-
-This document is mainly to help you to get started by codifying tribal knowledge and expectations and make it more accessible to everyone.
-But don't be afraid to open half-finished PRs and ask questions if something is unclear!
-
-
-Workflow
---------
-
-- No contribution is too small!
-  Please submit as many fixes for typos and grammar bloopers as you can!
-- Try to limit each pull request to *one* change only.
-- Since we squash on merge, it's up to you how you handle updates to the master branch.
-  Whether you prefer to rebase on master or merge master into your branch, do whatever is more comfortable for you.
-- *Always* add tests and docs for your code.
-  This is a hard rule; patches with missing tests or documentation can't be merged.
-- Consider updating CHANGELOG.rst to reflect the changes as observed by people using this library.
-- Make sure your changes pass our CI_.
-  You won't get any feedback until it's green unless you ask for it.
-- Once you've addressed review feedback, make sure to bump the pull request with a short note, so we know you're done.
-- Don’t break `backward compatibility`_.
-
-
-Code
-----
-
-- Obey `PEP 8`_ and `PEP 257`_.
-  We use the ``"""``\ -on-separate-lines style for docstrings:
-
-  .. code-block:: python
-
-     def func(x):
-         """
-         Do something.
-
-         :param str x: A very important parameter.
-
-         :rtype: str
-         """
-- If you add or change public APIs, tag the docstring using ``..  versionadded:: 16.0.0 WHAT`` or ``..  versionchanged:: 17.1.0 WHAT``.
-- We use isort_ to sort our imports, and we follow the Black_ code style with a line length of 79 characters.
-  As long as you run our full tox suite before committing, or install our pre-commit_ hooks (ideally you'll do both -- see below "Local Development Environment"), you won't have to spend any time on formatting your code at all.
-  If you don't, CI will catch it for you -- but that seems like a waste of your time!
-
-
-Tests
------
-
-- Write your asserts as ``expected == actual`` to line them up nicely:
-
-  .. code-block:: python
-
-     x = f()
-
-     assert 42 == x.some_attribute
-     assert "foo" == x._a_private_attribute
-
-- To run the test suite, all you need is a recent tox_.
-  It will ensure the test suite runs with all dependencies against all Python versions just as it will on Travis CI.
-  If you lack some Python versions, you can can always limit the environments like ``tox -e py27,py35`` (in that case you may want to look into pyenv_, which makes it very easy to install many different Python versions in parallel).
-- Write `good test docstrings`_.
-
-
-Documentation
--------------
-
-- Use `semantic newlines`_ in reStructuredText_ files (files ending in ``.rst``):
-
-  .. code-block:: rst
-
-     This is a sentence.
-     This is another sentence.
-
-- If you start a new section, add two blank lines before and one blank line after the header except if two headers follow immediately after each other:
-
-  .. code-block:: rst
-
-     Last line of previous section.
-
-
-     Header of New Top Section
-     -------------------------
-
-     Header of New Section
-     ^^^^^^^^^^^^^^^^^^^^^
-
-     First line of new section.
-- If your change is noteworthy, add an entry to the changelog_.
-  Use `semantic newlines`_, and add a link to your pull request:
-
-  .. code-block:: rst
-
-     - Added ``pem.func()`` that does foo.
-       It's pretty cool.
-       [`#1 <https://github.com/hynek/pem/pull/1>`_]
-     - ``pem.func()`` now doesn't crash the Large Hadron Collider anymore.
-       That was a nasty bug!
-       [`#2 <https://github.com/hynek/pem/pull/2>`_]
-
-
-Local Development Environment
------------------------------
-
-You can (and should) run our test suite using tox_.
-However, you’ll probably want a more traditional environment as well.
-We highly recommend to develop using the latest Python 3 release because you're more likely to catch certain bugs earlier.
-
-First create a `virtual environment <https://virtualenv.pypa.io/>`_.
-It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like `pew <https://github.com/berdario/pew>`_, `virtualfish <http://virtualfish.readthedocs.io/>`_, and `virtualenvwrapper <http://virtualenvwrapper.readthedocs.io/>`_.
-
-Next get an up to date checkout of the ``pem`` repository:
-
-.. code-block:: bash
-
-    git clone git@github.com:hynek/pem.git
-
-Change into the newly created directory and **after activating your virtual environment** install an editable version of ``pem`` along with its tests and docs requirements:
-
-.. code-block:: bash
-
-    cd pem
-    pip install -e .[dev]
-
-At this point,
-
-.. code-block:: bash
-
-   $ python -m pytest
-
-should work and pass, as should:
-
-.. code-block:: bash
-
-   $ cd docs
-   $ make html
-
-The built documentation can then be found in ``docs/_build/html/``.
-
-To avoid committing code that violates our style guide, we strongly advice you to install pre-commit_ [#f1]_ hooks:
-
-.. code-block:: bash
-
-   $ pre-commit install
-
-You can also run them anytime (as our tox does) using:
-
-.. code-block:: bash
-
-   $ pre-commit run --all-files
-
-
-.. [#f1] pre-commit should have been installed into your virtualenv automatically when you ran ``pip install -e .[dev]`` above. If pre-commit is missing, it may be that you need to re-run ``pip install -e .[dev]``.
-
-
-****
-
-Again, this list is mainly to help you to get started by codifying tribal knowledge and expectations.
-If something is unclear, feel free to ask for help!
-
-Please note that this project is released with a Contributor `Code of Conduct`_.
-By participating in this project you agree to abide by its terms.
-Please report any harm to `Hynek Schlawack`_ in any way you find appropriate.
-
-Thank you for considering contributing to ``pem``!
-
-
-.. _`Hynek Schlawack`: https://hynek.me/about/
-.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
-.. _`PEP 257`: https://www.python.org/dev/peps/pep-0257/
-.. _`good test docstrings`: https://jml.io/pages/test-docstrings.html
-.. _`Code of Conduct`: https://github.com/hynek/pem/blob/master/CODE_OF_CONDUCT.rst
-.. _changelog: https://github.com/hynek/pem/blob/master/CHANGELOG.rst
-.. _`backward compatibility`: https://pem.readthedocs.io/en/latest/backward-compatibility.html
-.. _tox: https://tox.readthedocs.io/
-.. _pyenv: https://github.com/pyenv/pyenv
-.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
-.. _semantic newlines: https://rhodesmill.org/brandon/2012/one-sentence-per-line/
-.. _CI: https://travis-ci.org/hynek/pem/
-.. _black: https://github.com/ambv/black
-.. _pre-commit: https://pre-commit.com/
-.. _isort: https://github.com/timothycrosley/isort
diff --git a/MANIFEST.in b/MANIFEST.in
index b744430..cf04771 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,4 @@
-include *.rst LICENSE tox.ini dev-requirements.txt docs-requirements.txt .coveragerc pyproject.toml conftest.py *.yml *.yaml mypy.ini
+include *.rst *.yml *.yaml *.ini LICENSE pyproject.toml conftest.py
 recursive-include src *.typed
 recursive-include docs *.bat
 recursive-include docs *.py
diff --git a/PKG-INFO b/PKG-INFO
index e60194e..d979dee 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: pem
-Version: 19.1.0
+Version: 21.2.0
 Summary: Easy PEM file parsing in Python.
 Home-page: https://pem.readthedocs.io/
 Author: Hynek Schlawack
@@ -11,40 +11,42 @@ License: MIT
 Project-URL: Documentation, https://pem.readthedocs.io/
 Project-URL: Bug Tracker, https://github.com/hynek/pem/issues
 Project-URL: Source Code, https://github.com/hynek/pem
+Project-URL: Funding, https://github.com/sponsors/hynek
+Project-URL: Ko-fi, https://ko-fi.com/the_hynek
 Description: pem: Easy PEM file parsing
         ==========================
         
         .. image:: https://readthedocs.org/projects/pem/badge/?version=stable
-          :target: https://pem.readthedocs.io/en/stable/?badge=stable
-          :alt: Documentation Status
+           :target: https://pem.readthedocs.io/en/stable/?badge=stable
+           :alt: Documentation Status
         
-        .. image:: https://travis-ci.org/hynek/pem.svg?branch=master
-           :target: https://travis-ci.org/hynek/pem
-           :alt: CI status
+        .. image:: https://github.com/hynek/pem/workflows/CI/badge.svg?branch=main
+           :target: https://github.com/hynek/pem/actions
+           :alt: CI Status
         
-        .. image:: https://codecov.io/gh/hynek/pem/branch/master/graph/badge.svg
+        .. image:: https://codecov.io/gh/hynek/pem/branch/main/graph/badge.svg
            :target: https://codecov.io/github/hynek/pem
            :alt: Coverage
         
+        .. image:: http://www.mypy-lang.org/static/mypy_badge.svg
+           :target: http://mypy-lang.org
+           :alt: Checked with mypy
+        
         .. image:: https://www.irccloud.com/invite-svg?channel=%23cryptography-dev&amp;hostname=irc.freenode.net&amp;port=6697&amp;ssl=1
            :target: https://www.irccloud.com/invite?channel=%23cryptography-dev&amp;hostname=irc.freenode.net&amp;port=6697&amp;ssl=1
         
         .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-           :target: https://github.com/ambv/black
+           :target: https://github.com/psf/black
            :alt: Code style: black
         
-        .. image:: http://www.mypy-lang.org/static/mypy_badge.svg
-           :target: http://mypy-lang.org
-           :alt: Checked with mypy
-        
         .. teaser-begin
         
-        ``pem`` is an MIT_-licensed Python module for parsing and splitting of `PEM files`_, i.e. Base64 encoded DER keys and certificates.
+        *pem* is an MIT_-licensed Python module for parsing and splitting of `PEM files`_, i.e. Base64-encoded DER keys and certificates.
         
-        It runs on Python 2.7, 3.4+, and PyPy, has no dependencies, and does not attempt to interpret the certificate data in any way.
+        It runs on Python 2.7, and 3.5+, has no dependencies, and does not attempt to interpret the certificate data in any way.
         
         It’s born from the need to load keys, certificates, trust chains, and DH parameters from various certificate deployments: some servers (like Apache_) expect them to be a separate file, others (like nginx_) expect them concatenated to the server certificate and finally some (like HAProxy_) expect key, certificate, and chain to be in one file.
-        With ``pem``, your Python application can cope with all of those scenarios:
+        With *pem*, your Python application can cope with all of those scenarios:
         
         .. code-block:: pycon
         
@@ -55,9 +57,9 @@ Description: pem: Easy PEM file parsing
            >>> str(certs[0])
            '-----BEGIN CERTIFICATE-----\n...'
         
-        Additionally to the vanilla parsing code, ``pem`` also contains helpers for Twisted_ that save a lot of boilerplate code.
+        Additionally to the vanilla parsing code, *pem* also contains helpers for Twisted_ that save a lot of boilerplate code.
         
-        ``pem``\ ’s documentation lives at `Read the Docs <https://pem.readthedocs.io/>`_, the code on `GitHub <https://github.com/hynek/pem>`_.
+        *pem* is available from `PyPI <https://pypi.org/project/pem/>`_, its documentation lives at `Read the Docs <https://pem.readthedocs.io/>`_, the code on `GitHub <https://github.com/hynek/pem>`_.
         
         
         .. _MIT: https://choosealicense.com/licenses/mit/
@@ -71,10 +73,9 @@ Description: pem: Easy PEM file parsing
         Release Information
         ===================
         
-        19.1.0 (2019-03-19)
+        21.2.0 (2021-04-07)
         -------------------
         
-        
         Backward-incompatible changes:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         
@@ -90,16 +91,15 @@ Description: pem: Easy PEM file parsing
         Changes:
         ^^^^^^^^
         
-        - You can now load encrypted PKCS#8 PEM key as ``pem.Key``.
-        - Added support for ``pem.PublicKey`` (``PUBLIC KEY``).
-        - Added support for ``pem.RSAPublicKey`` (``RSA PUBLIC KEY``).
+        - Added support for ``pem.OpenSSLTrustedCertificate`` (``-----BEGIN TRUSTED CERTIFICATE-----``), as defined in `openssl x509 manual <https://www.openssl.org/docs/man1.1.1/man1/x509.html>`_.
+          `#28 <https://github.com/hynek/pem/issues/28>`_
         
         `Full changelog <https://pem.readthedocs.io/en/stable/changelog.html>`_.
         
         Credits
         =======
         
-        ``pem`` is written and maintained by Hynek Schlawack.
+        *pem* is written and maintained by Hynek Schlawack.
         
         The development is kindly supported by `Variomedia AG <https://www.variomedia.de/>`_.
         
@@ -118,11 +118,13 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Provides-Extra: dev
+Description-Content-Type: text/x-rst
 Provides-Extra: docs
 Provides-Extra: tests
+Provides-Extra: dev
diff --git a/README.rst b/README.rst
index a164661..3cb6890 100644
--- a/README.rst
+++ b/README.rst
@@ -2,36 +2,36 @@ pem: Easy PEM file parsing
 ==========================
 
 .. image:: https://readthedocs.org/projects/pem/badge/?version=stable
-  :target: https://pem.readthedocs.io/en/stable/?badge=stable
-  :alt: Documentation Status
+   :target: https://pem.readthedocs.io/en/stable/?badge=stable
+   :alt: Documentation Status
 
-.. image:: https://travis-ci.org/hynek/pem.svg?branch=master
-   :target: https://travis-ci.org/hynek/pem
-   :alt: CI status
+.. image:: https://github.com/hynek/pem/workflows/CI/badge.svg?branch=main
+   :target: https://github.com/hynek/pem/actions
+   :alt: CI Status
 
-.. image:: https://codecov.io/gh/hynek/pem/branch/master/graph/badge.svg
+.. image:: https://codecov.io/gh/hynek/pem/branch/main/graph/badge.svg
    :target: https://codecov.io/github/hynek/pem
    :alt: Coverage
 
+.. image:: http://www.mypy-lang.org/static/mypy_badge.svg
+   :target: http://mypy-lang.org
+   :alt: Checked with mypy
+
 .. image:: https://www.irccloud.com/invite-svg?channel=%23cryptography-dev&amp;hostname=irc.freenode.net&amp;port=6697&amp;ssl=1
    :target: https://www.irccloud.com/invite?channel=%23cryptography-dev&amp;hostname=irc.freenode.net&amp;port=6697&amp;ssl=1
 
 .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-   :target: https://github.com/ambv/black
+   :target: https://github.com/psf/black
    :alt: Code style: black
 
-.. image:: http://www.mypy-lang.org/static/mypy_badge.svg
-   :target: http://mypy-lang.org
-   :alt: Checked with mypy
-
 .. teaser-begin
 
-``pem`` is an MIT_-licensed Python module for parsing and splitting of `PEM files`_, i.e. Base64 encoded DER keys and certificates.
+*pem* is an MIT_-licensed Python module for parsing and splitting of `PEM files`_, i.e. Base64-encoded DER keys and certificates.
 
-It runs on Python 2.7, 3.4+, and PyPy, has no dependencies, and does not attempt to interpret the certificate data in any way.
+It runs on Python 2.7, and 3.5+, has no dependencies, and does not attempt to interpret the certificate data in any way.
 
 It’s born from the need to load keys, certificates, trust chains, and DH parameters from various certificate deployments: some servers (like Apache_) expect them to be a separate file, others (like nginx_) expect them concatenated to the server certificate and finally some (like HAProxy_) expect key, certificate, and chain to be in one file.
-With ``pem``, your Python application can cope with all of those scenarios:
+With *pem*, your Python application can cope with all of those scenarios:
 
 .. code-block:: pycon
 
@@ -42,9 +42,9 @@ With ``pem``, your Python application can cope with all of those scenarios:
    >>> str(certs[0])
    '-----BEGIN CERTIFICATE-----\n...'
 
-Additionally to the vanilla parsing code, ``pem`` also contains helpers for Twisted_ that save a lot of boilerplate code.
+Additionally to the vanilla parsing code, *pem* also contains helpers for Twisted_ that save a lot of boilerplate code.
 
-``pem``\ ’s documentation lives at `Read the Docs <https://pem.readthedocs.io/>`_, the code on `GitHub <https://github.com/hynek/pem>`_.
+*pem* is available from `PyPI <https://pypi.org/project/pem/>`_, its documentation lives at `Read the Docs <https://pem.readthedocs.io/>`_, the code on `GitHub <https://github.com/hynek/pem>`_.
 
 
 .. _MIT: https://choosealicense.com/licenses/mit/
diff --git a/debian/changelog b/debian/changelog
index f8e5f98..b80311b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-pem (21.2.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 31 May 2023 18:44:42 -0000
+
 python-pem (19.1.0-2) unstable; urgency=medium
 
   [ Ondřej Nový ]
diff --git a/docs/api.rst b/docs/api.rst
index 11f374f..2081e34 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -27,11 +27,17 @@ PEM Objects
 The following objects can be returned by the parsing functions.
 
 .. autoclass:: Certificate(AbstractPEMObject)
+.. autoclass:: OpenSSLTrustedCertificate(Certificate)
 .. autoclass:: Key(AbstractPEMObject)
 .. autoclass:: PrivateKey(Key)
 .. autoclass:: PublicKey(Key)
 .. autoclass:: RSAPrivateKey(PrivateKey)
 .. autoclass:: RSAPublicKey(PublicKey)
+.. autoclass:: ECPrivateKey(PrivateKey)
+.. autoclass:: DSAPrivateKey(PrivateKey)
+.. autoclass:: OpenSSHPrivateKey(PrivateKey)
+.. autoclass:: SSHPublicKey(Key)
+.. autoclass:: SSHCOMPrivateKey(PrivateKey)
 .. autoclass:: DHParameters(AbstractPEMObject)
 .. autoclass:: CertificateRequest(AbstractPEMObject)
 .. autoclass:: CertificateRevocationList(AbstractPEMObject)
diff --git a/docs/conf.py b/docs/conf.py
index ca21050..1c44af7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,10 +12,7 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import codecs
-import datetime
-import os
-import re
+import pem
 
 
 try:
@@ -24,30 +21,6 @@ except ImportError:
     sphinx_rtd_theme = None
 
 
-def read(*parts):
-    """
-    Build an absolute path from *parts* and and return the contents of the
-    resulting file.  Assume UTF-8 encoding.
-    """
-    here = os.path.abspath(os.path.dirname(__file__))
-    with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f:
-        return f.read()
-
-
-def find_version(*file_paths):
-    """
-    Build a path from *file_paths* and search for a ``__version__``
-    string inside.
-    """
-    version_file = read(*file_paths)
-    version_match = re.search(
-        r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M
-    )
-    if version_match:
-        return version_match.group(1)
-    raise RuntimeError("Unable to find version string.")
-
-
 # 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.
@@ -55,6 +28,10 @@ def find_version(*file_paths):
 
 # -- General configuration ------------------------------------------------
 
+linkcheck_ignore = [
+    r"https://github.com/.*/(issues|pull)/\d+",
+]
+
 # If your documentation needs a minimal Sphinx version, state it here.
 # needs_sphinx = '1.0'
 
@@ -82,16 +59,15 @@ source_suffix = ".rst"
 master_doc = "index"
 
 # General information about the project.
-project = u"pem"
-author = u"Hynek Schlawack"
-year = datetime.date.today().year
-copyright = u"2013{0}, {1}".format(u"-{0}".format(year), author)
+project = "pem"
+author = "Hynek Schlawack"
+copyright = "2013, " + author
 # 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.
 #
 # The short X.Y version.
-version = find_version("../src/pem/__init__.py")
+version = pem.__version__
 # The full version, including alpha/beta/rc tags.
 release = version
 
@@ -127,9 +103,6 @@ exclude_patterns = ["_build"]
 # 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 = []
 
@@ -142,15 +115,8 @@ todo_include_todos = False
 
 # -- Options for HTML output ----------------------------------------------
 
-html_theme = "alabaster"
-html_theme_options = {
-    "font_family": '"Avenir Next", Calibri, "PT Sans", sans-serif',
-    "head_font_family": '"Avenir Next", Calibri, "PT Sans", sans-serif',
-    "font_size": "18px",
-    "page_width": "980px",
-    "show_relbars": True,
-}
-
+html_theme = "furo"
+html_theme_options = {}
 
 # 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
@@ -261,7 +227,7 @@ latex_elements = {
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, "pem.tex", u"pem Documentation", author, "manual")
+    (master_doc, "pem.tex", "pem Documentation", author, "manual")
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -289,7 +255,7 @@ latex_documents = [
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [(master_doc, "pem", u"pem Documentation", [author], 1)]
+man_pages = [(master_doc, "pem", "pem Documentation", [author], 1)]
 
 # If true, show URL addresses after external links.
 # man_show_urls = False
@@ -304,7 +270,7 @@ texinfo_documents = [
     (
         master_doc,
         "pem",
-        u"pem Documentation",
+        "pem Documentation",
         author,
         "pem",
         "One line description of project.",
diff --git a/docs/contributing.rst b/docs/contributing.rst
index 4628182..8fbb03c 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -1,3 +1,3 @@
 .. _contributing:
 
-.. include:: ../CONTRIBUTING.rst
+.. include:: ../.github/CONTRIBUTING.rst
diff --git a/docs/core.rst b/docs/core.rst
index b52fa52..976901c 100644
--- a/docs/core.rst
+++ b/docs/core.rst
@@ -18,12 +18,3 @@ The function returns a list of valid :ref:`PEM objects <pem-objects>` found in t
 - or using ``obj.as_text()`` into Unicode text (``str`` on Python 3, ``unicode`` on Python 2),
 - or using ``obj.as_bytes()`` into bytes.
 - Additional you can obtain the SHA-1 hexdigest using ``obj.hashdigest()`` for quick comparison of objects.
-
-
-Files
-^^^^^
-
-For convenience, there's the helper function :func:`pem.parse_file` that reads a file and parses its contents.
-So the following example is equivalent with the first one::
-
-   certs = pem.parse_file("cert.pem")
diff --git a/docs/license.rst b/docs/license.rst
index b135920..d452eb3 100644
--- a/docs/license.rst
+++ b/docs/license.rst
@@ -1,8 +1,8 @@
 License and Hall of Fame
 ========================
 
-``pem`` is licensed under the permissive `MIT <https://choosealicense.com/licenses/mit/>`_ license.
-The full license text can be also found in the `source code repository <https://github.com/hynek/pem/blob/master/LICENSE>`_.
+*pem* is licensed under the permissive `MIT <https://choosealicense.com/licenses/mit/>`_ license.
+The full license text can be also found in the `source code repository <https://github.com/hynek/pem/blob/main/LICENSE>`_.
 
 .. _authors:
 
diff --git a/pyproject.toml b/pyproject.toml
index cc4975f..46c0230 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,5 +3,22 @@ requires = ["setuptools>=40.6.0", "wheel"]
 build-backend = "setuptools.build_meta"
 
 
+[tool.coverage.run]
+parallel = true
+branch = true
+source = ["pem"]
+
+[tool.coverage.paths]
+source = ["src", ".tox/*/site-packages"]
+
+[tool.coverage.report]
+show_missing = true
+skip_covered = true
+
+
 [tool.black]
 line-length = 79
+
+
+[tool.isort]
+profile = "attrs"
diff --git a/setup.cfg b/setup.cfg
index 9f90bda..8bfd5a1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,25 +1,3 @@
-[tool:pytest]
-minversion = 3.0
-strict = true
-addopts = -ra
-testpaths = tests
-
-[bdist_wheel]
-universal = 1
-
-[metadata]
-license_file = LICENSE
-
-[isort]
-atomic = true
-include_trailing_comma = true
-lines_after_imports = 2
-lines_between_types = 1
-multi_line_output = 3
-not_skip = __init__.py
-known_first_party = pem
-known_third_party = OpenSSL,certifi,cryptography,pretend,pytest,setuptools,twisted
-
 [egg_info]
 tag_build = 
 tag_date = 0
diff --git a/setup.py b/setup.py
index 54dc90e..caeada1 100644
--- a/setup.py
+++ b/setup.py
@@ -13,6 +13,8 @@ PROJECT_URLS = {
     "Documentation": "https://pem.readthedocs.io/",
     "Bug Tracker": "https://github.com/hynek/pem/issues",
     "Source Code": "https://github.com/hynek/pem",
+    "Funding": "https://github.com/sponsors/hynek",
+    "Ko-fi": "https://ko-fi.com/the_hynek",
 }
 CLASSIFIERS = [
     "Development Status :: 5 - Production/Stable",
@@ -26,16 +28,23 @@ CLASSIFIERS = [
     "Programming Language :: Python :: 2",
     "Programming Language :: Python :: 2.7",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.4",
     "Programming Language :: Python :: 3.5",
     "Programming Language :: Python :: 3.6",
     "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
     "Topic :: Software Development :: Libraries :: Python Modules",
 ]
 INSTALL_REQUIRES = []
 EXTRAS_REQUIRE = {
-    "docs": ["sphinx"],
-    "tests": ["coverage", "pytest", "certifi", "pretend", "pyopenssl"],
+    "docs": ["sphinx", "furo"],
+    "tests": [
+        "coverage[toml]>=5.0.2",
+        "pytest",
+        "certifi",
+        "pretend",
+        "pyopenssl",
+    ],
 }
 EXTRAS_REQUIRE["dev"] = (
     EXTRAS_REQUIRE["tests"]
@@ -91,7 +100,7 @@ LONG = (
     + "Release Information\n"
     + "===================\n\n"
     + re.search(
-        r"(\d+.\d.\d \(.*?\)\n.*?)\n\n\n----\n\n\n",
+        r"(\d+.\d.\d \(.*?\)\r?\n.*?)\r?\n\r?\n\r?\n----\r?\n\r?\n\r?\n",
         read("CHANGELOG.rst"),
         re.S,
     ).group(1)
@@ -113,6 +122,7 @@ if __name__ == "__main__":
         maintainer=find_meta("author"),
         maintainer_email=find_meta("email"),
         long_description=LONG,
+        long_description_content_type="text/x-rst",
         keywords=KEYWORDS,
         packages=PACKAGES,
         package_dir={"": "src"},
@@ -122,4 +132,5 @@ if __name__ == "__main__":
         classifiers=CLASSIFIERS,
         install_requires=INSTALL_REQUIRES,
         extras_require=EXTRAS_REQUIRE,
+        options={"bdist_wheel": {"universal": "1"}},
     )
diff --git a/src/pem.egg-info/PKG-INFO b/src/pem.egg-info/PKG-INFO
index e60194e..d979dee 100644
--- a/src/pem.egg-info/PKG-INFO
+++ b/src/pem.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: pem
-Version: 19.1.0
+Version: 21.2.0
 Summary: Easy PEM file parsing in Python.
 Home-page: https://pem.readthedocs.io/
 Author: Hynek Schlawack
@@ -11,40 +11,42 @@ License: MIT
 Project-URL: Documentation, https://pem.readthedocs.io/
 Project-URL: Bug Tracker, https://github.com/hynek/pem/issues
 Project-URL: Source Code, https://github.com/hynek/pem
+Project-URL: Funding, https://github.com/sponsors/hynek
+Project-URL: Ko-fi, https://ko-fi.com/the_hynek
 Description: pem: Easy PEM file parsing
         ==========================
         
         .. image:: https://readthedocs.org/projects/pem/badge/?version=stable
-          :target: https://pem.readthedocs.io/en/stable/?badge=stable
-          :alt: Documentation Status
+           :target: https://pem.readthedocs.io/en/stable/?badge=stable
+           :alt: Documentation Status
         
-        .. image:: https://travis-ci.org/hynek/pem.svg?branch=master
-           :target: https://travis-ci.org/hynek/pem
-           :alt: CI status
+        .. image:: https://github.com/hynek/pem/workflows/CI/badge.svg?branch=main
+           :target: https://github.com/hynek/pem/actions
+           :alt: CI Status
         
-        .. image:: https://codecov.io/gh/hynek/pem/branch/master/graph/badge.svg
+        .. image:: https://codecov.io/gh/hynek/pem/branch/main/graph/badge.svg
            :target: https://codecov.io/github/hynek/pem
            :alt: Coverage
         
+        .. image:: http://www.mypy-lang.org/static/mypy_badge.svg
+           :target: http://mypy-lang.org
+           :alt: Checked with mypy
+        
         .. image:: https://www.irccloud.com/invite-svg?channel=%23cryptography-dev&amp;hostname=irc.freenode.net&amp;port=6697&amp;ssl=1
            :target: https://www.irccloud.com/invite?channel=%23cryptography-dev&amp;hostname=irc.freenode.net&amp;port=6697&amp;ssl=1
         
         .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-           :target: https://github.com/ambv/black
+           :target: https://github.com/psf/black
            :alt: Code style: black
         
-        .. image:: http://www.mypy-lang.org/static/mypy_badge.svg
-           :target: http://mypy-lang.org
-           :alt: Checked with mypy
-        
         .. teaser-begin
         
-        ``pem`` is an MIT_-licensed Python module for parsing and splitting of `PEM files`_, i.e. Base64 encoded DER keys and certificates.
+        *pem* is an MIT_-licensed Python module for parsing and splitting of `PEM files`_, i.e. Base64-encoded DER keys and certificates.
         
-        It runs on Python 2.7, 3.4+, and PyPy, has no dependencies, and does not attempt to interpret the certificate data in any way.
+        It runs on Python 2.7, and 3.5+, has no dependencies, and does not attempt to interpret the certificate data in any way.
         
         It’s born from the need to load keys, certificates, trust chains, and DH parameters from various certificate deployments: some servers (like Apache_) expect them to be a separate file, others (like nginx_) expect them concatenated to the server certificate and finally some (like HAProxy_) expect key, certificate, and chain to be in one file.
-        With ``pem``, your Python application can cope with all of those scenarios:
+        With *pem*, your Python application can cope with all of those scenarios:
         
         .. code-block:: pycon
         
@@ -55,9 +57,9 @@ Description: pem: Easy PEM file parsing
            >>> str(certs[0])
            '-----BEGIN CERTIFICATE-----\n...'
         
-        Additionally to the vanilla parsing code, ``pem`` also contains helpers for Twisted_ that save a lot of boilerplate code.
+        Additionally to the vanilla parsing code, *pem* also contains helpers for Twisted_ that save a lot of boilerplate code.
         
-        ``pem``\ ’s documentation lives at `Read the Docs <https://pem.readthedocs.io/>`_, the code on `GitHub <https://github.com/hynek/pem>`_.
+        *pem* is available from `PyPI <https://pypi.org/project/pem/>`_, its documentation lives at `Read the Docs <https://pem.readthedocs.io/>`_, the code on `GitHub <https://github.com/hynek/pem>`_.
         
         
         .. _MIT: https://choosealicense.com/licenses/mit/
@@ -71,10 +73,9 @@ Description: pem: Easy PEM file parsing
         Release Information
         ===================
         
-        19.1.0 (2019-03-19)
+        21.2.0 (2021-04-07)
         -------------------
         
-        
         Backward-incompatible changes:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         
@@ -90,16 +91,15 @@ Description: pem: Easy PEM file parsing
         Changes:
         ^^^^^^^^
         
-        - You can now load encrypted PKCS#8 PEM key as ``pem.Key``.
-        - Added support for ``pem.PublicKey`` (``PUBLIC KEY``).
-        - Added support for ``pem.RSAPublicKey`` (``RSA PUBLIC KEY``).
+        - Added support for ``pem.OpenSSLTrustedCertificate`` (``-----BEGIN TRUSTED CERTIFICATE-----``), as defined in `openssl x509 manual <https://www.openssl.org/docs/man1.1.1/man1/x509.html>`_.
+          `#28 <https://github.com/hynek/pem/issues/28>`_
         
         `Full changelog <https://pem.readthedocs.io/en/stable/changelog.html>`_.
         
         Credits
         =======
         
-        ``pem`` is written and maintained by Hynek Schlawack.
+        *pem* is written and maintained by Hynek Schlawack.
         
         The development is kindly supported by `Variomedia AG <https://www.variomedia.de/>`_.
         
@@ -118,11 +118,13 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Provides-Extra: dev
+Description-Content-Type: text/x-rst
 Provides-Extra: docs
 Provides-Extra: tests
+Provides-Extra: dev
diff --git a/src/pem.egg-info/SOURCES.txt b/src/pem.egg-info/SOURCES.txt
index 9208c80..9140f49 100644
--- a/src/pem.egg-info/SOURCES.txt
+++ b/src/pem.egg-info/SOURCES.txt
@@ -1,11 +1,7 @@
-.coveragerc
 .pre-commit-config.yaml
 .readthedocs.yml
-.travis.yml
 AUTHORS.rst
 CHANGELOG.rst
-CODE_OF_CONDUCT.rst
-CONTRIBUTING.rst
 LICENSE
 MANIFEST.in
 README.rst
@@ -13,7 +9,6 @@ codecov.yml
 conftest.py
 mypy.ini
 pyproject.toml
-setup.cfg
 setup.py
 tox.ini
 docs/Makefile
diff --git a/src/pem.egg-info/requires.txt b/src/pem.egg-info/requires.txt
index 99b64f6..b8cb976 100644
--- a/src/pem.egg-info/requires.txt
+++ b/src/pem.egg-info/requires.txt
@@ -1,19 +1,21 @@
 
 [dev]
-coverage
+coverage[toml]>=5.0.2
 pytest
 certifi
 pretend
 pyopenssl
 sphinx
+furo
 twisted[tls]
 pre-commit
 
 [docs]
 sphinx
+furo
 
 [tests]
-coverage
+coverage[toml]>=5.0.2
 pytest
 certifi
 pretend
diff --git a/src/pem/__init__.py b/src/pem/__init__.py
index ea1afcc..a728f94 100644
--- a/src/pem/__init__.py
+++ b/src/pem/__init__.py
@@ -6,11 +6,17 @@ from ._core import (
     CertificateRequest,
     CertificateRevocationList,
     DHParameters,
+    DSAPrivateKey,
+    ECPrivateKey,
     Key,
+    OpenSSHPrivateKey,
+    OpenSSLTrustedCertificate,
     PrivateKey,
     PublicKey,
     RSAPrivateKey,
     RSAPublicKey,
+    SSHCOMPrivateKey,
+    SSHPublicKey,
     parse,
     parse_file,
 )
@@ -22,7 +28,7 @@ except ImportError:
     twisted = None  # type: ignore
 
 
-__version__ = "19.1.0"
+__version__ = "21.2.0"
 __author__ = "Hynek Schlawack"
 __license__ = "MIT"
 __description__ = "Easy PEM file parsing in Python."
@@ -37,12 +43,18 @@ __all__ = [
     "CertificateRequest",
     "CertificateRevocationList",
     "DHParameters",
+    "DSAPrivateKey",
+    "ECPrivateKey",
     "Key",
+    "OpenSSHPrivateKey",
+    "OpenSSLTrustedCertificate",
     "PrivateKey",
     "PublicKey",
     "RSAPrivateKey",
     "RSAPublicKey",
     "parse",
     "parse_file",
+    "SSHCOMPrivateKey",
+    "SSHPublicKey",
     "twisted",
 ]
diff --git a/src/pem/_core.py b/src/pem/_core.py
index 5239aca..145c3eb 100644
--- a/src/pem/_core.py
+++ b/src/pem/_core.py
@@ -15,7 +15,7 @@ from ._compat import ABC, PY2, text_type
 # mypy hack: Import typing information without actually importing anything.
 MYPY = False
 if MYPY:  # pragma: nocover
-    from typing import List, Any, Union, AnyStr, Optional, Dict, Type  # noqa
+    from typing import Any, AnyStr, Dict, List, Optional, Type, Union  # noqa
 
 
 class AbstractPEMObject(ABC):
@@ -28,7 +28,7 @@ class AbstractPEMObject(ABC):
         if isinstance(pem_bytes, text_type):
             self._pem_bytes = pem_bytes.encode("ascii")  # type: bytes
         else:
-            self._pem_bytes = pem_bytes  # type: bytes
+            self._pem_bytes = pem_bytes
         self._sha1_hexdigest = None  # type: Optional[str]
 
     def __str__(self):
@@ -53,9 +53,15 @@ class AbstractPEMObject(ABC):
         A SHA-1 digest of the whole object for easy differentiation.
 
         .. versionadded:: 18.1.0
+        .. versionchanged:: 20.1.0
+
+           Carriage returns are removed before hashing to give the same hashes
+           on Windows and UNIX-like operating systems.
         """
         if self._sha1_hexdigest is None:
-            self._sha1_hexdigest = hashlib.sha1(self._pem_bytes).hexdigest()
+            self._sha1_hexdigest = hashlib.sha1(
+                self._pem_bytes.replace(b"\r", b"")
+            ).hexdigest()
 
         return self._sha1_hexdigest
 
@@ -78,7 +84,7 @@ class AbstractPEMObject(ABC):
         return self._pem_bytes.decode("utf-8")
 
     def __eq__(self, other):
-        # type: (object) -> Union[NotImplemented, bool]
+        # type: (object) -> bool
         if not isinstance(other, type(self)):
             return NotImplemented
 
@@ -87,7 +93,7 @@ class AbstractPEMObject(ABC):
         )
 
     def __ne__(self, other):
-        # type: (object) -> Union[NotImplemented, bool]
+        # type: (object) -> bool
         if not isinstance(other, type(self)):
             return NotImplemented
 
@@ -104,6 +110,14 @@ class Certificate(AbstractPEMObject):
     """
 
 
+class OpenSSLTrustedCertificate(Certificate):
+    """
+    An OpenSSL "trusted certificate".
+
+    .. versionadded:: 21.2.0
+    """
+
+
 class CertificateRequest(AbstractPEMObject):
     """
     A certificate signing request.
@@ -156,31 +170,84 @@ class RSAPublicKey(PublicKey):
     """
 
 
+class ECPrivateKey(PrivateKey):
+    """
+    A private EC key.
+
+    .. versionadded:: 19.2.0
+    """
+
+
+class DSAPrivateKey(PrivateKey):
+    """
+    A private DSA key.
+
+    Also private DSA key in OpenSSH legacy PEM format.
+
+    .. versionadded:: 21.1.0
+    """
+
+
 class DHParameters(AbstractPEMObject):
     """
     Diffie-Hellman parameters for DHE.
     """
 
 
+class OpenSSHPrivateKey(PrivateKey):
+    """
+    OpenSSH private key format
+
+    .. versionadded:: 19.3.0
+    """
+
+
+class SSHPublicKey(PublicKey):
+    """
+    A public key in SSH
+    `RFC 4716 <https://tools.ietf.org/html/rfc4716>`_ format.
+
+    The Secure Shell (SSH) Public Key File Format.
+
+    .. versionadded:: 21.1.0
+    """
+
+
+class SSHCOMPrivateKey(PrivateKey):
+    """
+    A private key in SSH.COM / Tectia format.
+
+    .. versionadded:: 21.1.0
+    """
+
+
 _PEM_TO_CLASS = {
     b"CERTIFICATE": Certificate,
+    b"TRUSTED CERTIFICATE": OpenSSLTrustedCertificate,
     b"PRIVATE KEY": PrivateKey,
     b"PUBLIC KEY": PublicKey,
     b"ENCRYPTED PRIVATE KEY": PrivateKey,
+    b"OPENSSH PRIVATE KEY": OpenSSHPrivateKey,
+    b"DSA PRIVATE KEY": DSAPrivateKey,
     b"RSA PRIVATE KEY": RSAPrivateKey,
     b"RSA PUBLIC KEY": RSAPublicKey,
+    b"EC PRIVATE KEY": ECPrivateKey,
     b"DH PARAMETERS": DHParameters,
     b"NEW CERTIFICATE REQUEST": CertificateRequest,
     b"CERTIFICATE REQUEST": CertificateRequest,
+    b"SSH2 PUBLIC KEY": SSHPublicKey,
+    b"SSH2 ENCRYPTED PRIVATE KEY": SSHCOMPrivateKey,
     b"X509 CRL": CertificateRevocationList,
 }  # type: Dict[bytes, Type[AbstractPEMObject]]
 
+# See https://tools.ietf.org/html/rfc1421
+# and https://tools.ietf.org/html/rfc4716 for space instead of fifth dash.
 _PEM_RE = re.compile(
-    b"-----BEGIN ("
+    b"----[- ]BEGIN ("
     + b"|".join(_PEM_TO_CLASS.keys())
-    + b""")-----\r?
+    + b""")[- ]----\r?
 .+?\r?
------END \\1-----\r?\n?""",
+----[- ]END \\1[- ]----\r?\n?""",
     re.DOTALL,
 )
 
@@ -188,7 +255,7 @@ _PEM_RE = re.compile(
 def parse(pem_str):
     # type: (bytes) -> List[AbstractPEMObject]
     """
-    Extract PEM objects from *pem_str*.
+    Extract PEM-like objects from *pem_str*.
 
     :param pem_str: String to parse.
     :type pem_str: bytes
diff --git a/src/pem/twisted.py b/src/pem/twisted.py
index 0786ab6..0f26cb7 100644
--- a/src/pem/twisted.py
+++ b/src/pem/twisted.py
@@ -15,8 +15,9 @@ from ._core import Certificate, DHParameters, Key, parse_file
 # mypy hack: Import typing information without actually importing anything.
 MYPY = False
 if MYPY:  # pragma: nocover
-    from ._core import AbstractPEMObject  # noqa
-    from typing import List, Any  # noqa
+    from typing import Any, List
+
+    from ._core import AbstractPEMObject
 
 
 def certificateOptionsFromPEMs(pemObjects, **kw):
diff --git a/tests/data.py b/tests/data.py
index fa63e5a..3a7bf59 100644
--- a/tests/data.py
+++ b/tests/data.py
@@ -33,6 +33,23 @@ i3qou3qkVXNKuiAFe9dBvz0nhcpAZpXrpwc9R4Qk+rirEqkdCZI1feQKBz4J3ikm
 -----END CERTIFICATE-----
 """,
 ]
+
+# This is the first certificate from CERT_PEMS on which the
+# trusted certificate extesion was applied using:
+# openssl x509 -in cert.pem -trustout -addtrust emailProtection
+CERT_PEM_OPENSSL_TRUSTED = b"""-----BEGIN TRUSTED CERTIFICATE-----
+MIIBfDCCATagAwIBAgIJAK94OSlzVBsWMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV
+BAMTC3BlbS5pbnZhbGlkMB4XDTEzMDcxNzE0NDAyMFoXDTIzMDcxNTE0NDAyMFow
+FjEUMBIGA1UEAxMLcGVtLmludmFsaWQwTDANBgkqhkiG9w0BAQEFAAM7ADA4AjEA
+vtIM2QADJDHcqxZugx7MULbenrNUFrmoMDfEaedYveWY3wBxOw642L4nFWxN/fwL
+AgMBAAGjdzB1MB0GA1UdDgQWBBQ4O0ZSUfTA6C+Y+QZ3MpeMhysxYjBGBgNVHSME
+PzA9gBQ4O0ZSUfTA6C+Y+QZ3MpeMhysxYqEapBgwFjEUMBIGA1UEAxMLcGVtLmlu
+dmFsaWSCCQCveDkpc1QbFjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAAzEA
+XwKIF+Kf4OhcqbdQp253HG2KBt/WZwvNLo/bBlkrGYwfacbGuWT8nKJG70ujdKKf
+MAwwCgYIKwYBBQUHAwQ=
+-----END TRUSTED CERTIFICATE-----
+"""
+
 KEY_PEM = b"""-----BEGIN RSA PRIVATE KEY-----
 MIHyAgEAAjEAvtIM2QADJDHcqxZugx7MULbenrNUFrmoMDfEaedYveWY3wBxOw64
 2L4nFWxN/fwLAgMBAAECMCwqsCCV+SQqilnrQj8FJONVwGdZOJBd/iHi6ZXI2zbD
@@ -223,3 +240,71 @@ Tl25XucMcG/ALE/KNY6pqC2AQ6R2ERlVgPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeu
 lmCpGSynXNcpZ/06+vofGi/2MlpQZNhHAo8eayMp6FcvNucIpUndo1X8dKMv3Y26
 ZQIDAQAB
 -----END PUBLIC KEY-----"""
+
+# generated with:
+# openssl ecparam -name secp256k1 -genkey -noout -out key.pem
+# Documented at https://tools.ietf.org/html/rfc5915
+KEY_PEM_EC_PRIVATE = b"""\
+-----BEGIN EC PRIVATE KEY-----
+MHQCAQEEIGTpm0NjJRU5dYDrRPh+C9agdudJvCGSBd1hah5jnMYPoAcGBSuBBAAK
+oUQDQgAEyEVc22cgFbf0Ey4W7tNawHEW4o+4xNVznhP4et9g53Puw1KSVD/GOfl1
+95dwvaIyx2XZG8tn9DRsVyjy8fjgFA==
+-----END EC PRIVATE KEY-----"""
+
+# generated with:
+# ssh-keygen -t ecdsa
+KEY_PEM_OPENSSH = b"""\
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTw5dr6HzmoqoF1tJkUiCrFvbPbe18u
+iJ2gmS+0mmdwledpUh8g9VWeCGUpa50MYWH83Y+cT0TY9jjRUhkNCchzAAAAuB5SwggeUs
+IIAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPDl2vofOaiqgXW0
+mRSIKsW9s9t7Xy6InaCZL7SaZ3CV52lSHyD1VZ4IZSlrnQxhYfzdj5xPRNj2ONFSGQ0JyH
+MAAAAhAMP/HkDnx5kbDXrh2EMYhj5FFAB2jbwXRVvJqeM6jD09AAAAHmJ1ZGR5QEJ1ZGR5
+cy1NYWNCb29rLVByby5sb2NhbAE=
+-----END OPENSSH PRIVATE KEY-----"""
+
+# OpenSSH legacy PEM private key format:
+# ssh-keygen -t dsa -m PEM
+# OpenSSL DSA key.
+# openssl dsaparam -out key.pem -genkey 1024:
+KEY_PEM_DSA_PRIVATE = b"""\
+-----BEGIN DSA PRIVATE KEY-----
+MIIBugIBAAKBgQCMaLmHH3HlQwVVp2mJq2Peblj+rjeLfN20fFHNm5LecTP0XIO9
+48chkzea4Ma2lv/hGTkYLlA0dQwYoAFO9rwrZo9HU+CXpx3A9BYVk3IWif7rSXZP
+3HCmQ0vN7nZhzW32Us6LM8MW5ZW1tJYQLPsDpGKgNVdKLZt+nhzJ3MxF3QIVAJKB
+6g66zmFYO+SN6zFYkco0wrNHAoGABZ0tQnYYj6uQoTK0mE90jsnUA3WpXenZDKBt
+TcryHe0ijwr4hzqGRJNmxBKgCX7mhYP2j5Kyd91BQDDNc9K41xeH3ikTal6O2b4J
+ckkxAyjhZccxwkvBKJVXC/g9I5ePbWGDheq3TO76sJNOcHHt0/KTGKb0Zy5rtgOn
+CSJ5eD0CgYBX0u5FjDreCn+4vqIwQyPgIvIIsq4S2WNWMEp1JvxA5OB+2BZh83Ua
+Qrb0riZLOXc966m9uXkBJE+Eimh+Jed/qfbwNuTZbxVz9rmsnbGHj8kvJT4c3J27
+NRrjPxY+c3X65vSaThscOQ0SHm5bRhX2YNRhgnZPznUnMXfE8yRLdgIUUS6kFIid
+HhSy7IHLTHWGoNdmwLo=
+-----END DSA PRIVATE KEY-----"""
+
+# Taken from https://tools.ietf.org/html/rfc4716#section-3.6.
+KEY_PEM_RFC4716_PUBLIC = br"""---- BEGIN SSH2 PUBLIC KEY ----
+Subject: me
+Comment: 1024-bit rsa, created by me@example.com Mon Jan 15 \
+08:31:24 2001
+AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4
+596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4
+soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=
+---- END SSH2 PUBLIC KEY ----
+"""
+
+# SSH.COM / Tectia private (encrypted or plain) key.
+# The non-encrypted key has the same armor.
+# puttygen -t rsa -O private-sshcom
+KEY_PEM_SSHCOM_PRIVATE = b"""\
+---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----
+Comment: "rsa-key-20210120"
+P2/56wAAAUwAAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS
+1wa2NzMXYyLW9hZXB9fQAAAARub25lAAAA/QAAAPkAAAAGJQAAAf0VIAK0MvdpUXEG6obL
+3F5n0UimJWvwhJIb5AGyd++EdYvimCOg9iM2E75dDj89Ap7S5l4IS40fZO/5UjzYQxitAA
+ACAMNoGQLXcI4xVX/5Xt22aUBP4ADaJnDKR4H9D7LVZ4lBDUP8RBTmowCv9p3Hz7KvVw3R
+TX8BNF72gEuSOvruUAUAAAD9Hs7Zn1KbFR29ujFEv+d50/7rjMU7Ox4tzDeTSE6PBhsAAA
+EA3m/0JWkf61807iZ7AV8umYJMmNQ35HadG53n9nitpFEAAAEA4OQI1Rrh8e1EZ5qJBV8o
+gGyxzt4OdoXzuOtxkbHUB3U=
+---- END SSH2 ENCRYPTED PRIVATE KEY ----
+"""
diff --git a/tests/test_core.py b/tests/test_core.py
index 177914a..a288465 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function
 from itertools import combinations
 
 import certifi
+import pytest
 
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import serialization
@@ -16,16 +17,22 @@ from pem._compat import text_type
 
 from .data import (
     CERT_NO_NEW_LINE,
+    CERT_PEM_OPENSSL_TRUSTED,
     CERT_PEMS,
     CERT_PEMS_NO_NEW_LINE,
     CRL_PEMS,
     DH_PEM,
+    KEY_PEM_DSA_PRIVATE,
+    KEY_PEM_EC_PRIVATE,
+    KEY_PEM_OPENSSH,
     KEY_PEM_PKCS5_ENCRYPTED,
     KEY_PEM_PKCS5_UNENCRYPTED,
     KEY_PEM_PKCS8_ENCRYPTED,
     KEY_PEM_PKCS8_UNENCRYPTED,
     KEY_PEM_PUBLIC,
+    KEY_PEM_RFC4716_PUBLIC,
     KEY_PEM_RSA_PUBLIC,
+    KEY_PEM_SSHCOM_PRIVATE,
 )
 
 
@@ -64,11 +71,14 @@ class TestPEMObjects(object):
             cert_req
         )
 
-    def test_sha1_hexdigest(self):
+    @pytest.mark.parametrize("pem_bytes", (b"test", b"test\r"))
+    def test_sha1_hexdigest(self, pem_bytes):
         """
         obj.sha1_digest contains the correct digest and caches it properly.
+
+        CRs are ignored.
         """
-        cert = pem.Certificate(b"test")
+        cert = pem.Certificate(pem_bytes)
 
         assert (
             "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
@@ -372,7 +382,7 @@ class TestParse(object):
         as an RSAPrivateKey.
         """
         rv = pem.parse(KEY_PEM_PKCS5_UNENCRYPTED)
-        key, = rv
+        (key,) = rv
 
         assert isinstance(key, pem.RSAPrivateKey)
         assert KEY_PEM_PKCS5_UNENCRYPTED == key.as_bytes()
@@ -389,7 +399,7 @@ class TestParse(object):
         """
 
         rv = pem.parse(KEY_PEM_PKCS5_ENCRYPTED)
-        key, = rv
+        (key,) = rv
 
         assert isinstance(key, pem.RSAPrivateKey)
         assert KEY_PEM_PKCS5_ENCRYPTED == key.as_bytes()
@@ -405,7 +415,7 @@ class TestParse(object):
         as an Key.
         """
         rv = pem.parse(KEY_PEM_PKCS8_UNENCRYPTED)
-        key, = rv
+        (key,) = rv
 
         assert isinstance(key, pem.Key)
         assert KEY_PEM_PKCS8_UNENCRYPTED == key.as_bytes()
@@ -421,7 +431,7 @@ class TestParse(object):
         as an Key.
         """
         rv = pem.parse(KEY_PEM_PKCS8_ENCRYPTED)
-        key, = rv
+        (key,) = rv
 
         assert isinstance(key, pem.Key)
         assert KEY_PEM_PKCS8_ENCRYPTED == key.as_bytes()
@@ -445,7 +455,7 @@ class TestParse(object):
         """
         Parses a PEM string without a new line at the end
         """
-        cert, = pem.parse(CERT_NO_NEW_LINE)
+        (cert,) = pem.parse(CERT_NO_NEW_LINE)
 
         assert isinstance(cert, pem.Certificate)
         assert CERT_NO_NEW_LINE == cert.as_bytes()
@@ -460,12 +470,21 @@ class TestParse(object):
         assert all(isinstance(c, pem.Certificate) for c in certs)
         assert CERT_PEMS_NO_NEW_LINE == [cert.as_bytes() for cert in certs]
 
+    def test_certificate_openssl_trusted(self):
+        """
+        Parses a PEM string with with an OpenSSL trusted certificate.
+        """
+        (cert,) = pem.parse(CERT_PEM_OPENSSL_TRUSTED)
+
+        assert isinstance(cert, pem.OpenSSLTrustedCertificate)
+        assert CERT_PEM_OPENSSL_TRUSTED == cert.as_bytes()
+
     def test_dh(self):
         """
         Parses a PEM string with with DH parameters into a DHParameters.
         """
         rv = pem.parse(DH_PEM)
-        dh, = rv
+        (dh,) = rv
 
         assert isinstance(dh, pem.DHParameters)
         assert DH_PEM == dh.as_bytes()
@@ -506,7 +525,7 @@ class TestParse(object):
         \n and \r\n are treated equal.
         """
         lf_pem = KEY_PEM_PKCS5_UNENCRYPTED.replace(b"\n", b"\r\n")
-        rv, = pem.parse(lf_pem)
+        (rv,) = pem.parse(lf_pem)
 
         assert rv.as_bytes() == lf_pem
 
@@ -518,6 +537,7 @@ class TestParse(object):
 
         assert isinstance(key, pem.PublicKey)
         assert isinstance(key, pem.RSAPublicKey)
+        assert KEY_PEM_RSA_PUBLIC == key.as_bytes()
 
     def test_generic_public_key(self):
         """
@@ -526,3 +546,54 @@ class TestParse(object):
         key = pem.parse(KEY_PEM_PUBLIC)[0]
 
         assert isinstance(key, pem.PublicKey)
+        assert KEY_PEM_PUBLIC == key.as_bytes()
+
+    def test_ec_private_key(self):
+        """
+        Detects and loads EC private keys.
+        """
+        key = pem.parse(KEY_PEM_EC_PRIVATE)[0]
+
+        assert isinstance(key, pem.ECPrivateKey)
+        assert KEY_PEM_EC_PRIVATE == key.as_bytes()
+
+    def test_openshh_private_key(self):
+        """
+        Detects and loads private keys in the new OpenSSH private key format.
+        """
+        (key,) = pem.parse(KEY_PEM_OPENSSH)
+
+        assert isinstance(key, pem.OpenSSHPrivateKey)
+        assert KEY_PEM_OPENSSH == key.as_bytes()
+
+    def test_dsa_private_key(self):
+        """
+        Detects and loads private DSA keys.
+        This is also the legacy OpenSSH private key format.
+        """
+        (key,) = pem.parse(KEY_PEM_DSA_PRIVATE)
+
+        assert isinstance(key, pem.DSAPrivateKey)
+        assert KEY_PEM_DSA_PRIVATE == key.as_bytes()
+
+    def test_rfc4716_public_key_(self):
+        """
+        Detects and loads public SSH keys in RFC 4716 format.
+        """
+        (key,) = pem.parse(
+            b"PREAMBLE \n" + KEY_PEM_RFC4716_PUBLIC + b"\n TRAILING"
+        )
+
+        assert isinstance(key, pem.SSHPublicKey)
+        assert KEY_PEM_RFC4716_PUBLIC == key.as_bytes()
+
+    def test_sshcom_private(self):
+        """
+        Detects and loads public SSH keys in RFC 4716 format.
+        """
+        (key,) = pem.parse(
+            b"PREAMBLE \n" + KEY_PEM_SSHCOM_PRIVATE + b"\n TRAILING"
+        )
+
+        assert isinstance(key, pem.SSHCOMPrivateKey)
+        assert KEY_PEM_SSHCOM_PRIVATE == key.as_bytes()
diff --git a/tox.ini b/tox.ini
index 08c2c0f..24f15e8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,28 @@
+[pytest]
+addopts = -ra
+testpaths = tests
+filterwarnings =
+    once::Warning
+
+
+[gh-actions]
+python =
+    2.7: py27
+    3.5: py35
+    3.6: py36
+    3.7: py37, docs
+    3.8: py38, mypy, lint
+    3.9: py39, manifest
+
+
 [tox]
-envlist = lint,mypy,{py27,py34,py35,py36,py37,pypy,pypy3}{-twisted,},manifest,docs,pypi-description,coverage-report
+envlist = lint, mypy, {py27,py35,py36,py37,py38,py39}{-twisted,}, manifest, docs, pypi-description, coverage-report
 isolated_build = true
 
 
 [testenv:lint]
 description = Run all pre-commit hooks.
-basepython = python3.7
+basepython = python3.8
 skip_install = true
 deps = pre-commit
 passenv = HOMEPATH  # needed on Windows
@@ -17,13 +34,13 @@ description = Run tests and measure coverage.
 extras = tests
 deps = twisted: twisted[tls]
 commands =
-    {py27,py37}{-twisted,}: coverage run --parallel -m pytest {posargs}
-    {py34,py35,py36,pypy,pypy3}{-twisted,}: python -m pytest {posargs}
+    {py27,py38}{-twisted,}: coverage run -m pytest {posargs}
+    {py35,py36,py37,py39}{-twisted,}: python -m pytest {posargs}
 
 
 [testenv:mypy]
 description = Check types
-basepython = python3.7
+basepython = python3.8
 extras = tests
 deps = mypy
 commands = mypy src
@@ -31,7 +48,6 @@ commands = mypy src
 
 [testenv:manifest]
 description = Ensure MANIFEST.in is up to date.
-basepython = python3.7
 deps = check-manifest
 commands = check-manifest
 
@@ -48,7 +64,7 @@ commands =
 
 [testenv:pypi-description]
 description = Ensure README.rst renders on PyPI.
-basepython = python3.7
+basepython = python3.8
 skip_install = true
 deps =
     twine
@@ -60,8 +76,8 @@ commands =
 
 [testenv:coverage-report]
 description = Report coverage over all test runs.
-basepython = python3.7
-deps = coverage
+basepython = python3.8
+deps = coverage[toml]>=5.0.2
 skip_install = true
 commands =
     coverage combine

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-21.2.0.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-21.2.0.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-21.2.0.egg-info/not-zip-safe
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-21.2.0.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-21.2.0.egg-info/top_level.txt

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-19.1.0.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-19.1.0.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-19.1.0.egg-info/not-zip-safe
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-19.1.0.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pem-19.1.0.egg-info/top_level.txt

No differences were encountered in the control files

More details

Full run details