Repository: hynek/pem

Issue:
Add support for OpenPGP - RFC 4880
I think that it would be nice if PEM could also support the armored  OpenPGP files from https://tools.ietf.org/html/rfc4880

As a start without the support for headers.

Below are the message types for reference,

```
   BEGIN PGP MESSAGE
       Used for signed, encrypted, or compressed files.

   BEGIN PGP PUBLIC KEY BLOCK
       Used for armoring public keys.

   BEGIN PGP PRIVATE KEY BLOCK
       Used for armoring private keys.

   BEGIN PGP MESSAGE, PART X/Y
       Used for multi-part messages, where the armor is split amongst Y
       parts, and this is the Xth part out of Y.

   BEGIN PGP MESSAGE, PART X
       Used for multi-part messages, where this is the Xth part of an
       unspecified number of parts.  Requires the MESSAGE-ID Armor
       Header to be used.

   BEGIN PGP SIGNATURE
       Used for detached signatures, OpenPGP/MIME signatures, and
       cleartext signatures.  Note that PGP 2.x uses BEGIN PGP MESSAGE
       for detached signatures.

```

And this is how a PGP public key can look like

```
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQGiBF6PHJoRBAD5netlVGrX5TKVQ9eoWKD67cwROI0LQHMJAAMxnrhcPogeNcIX
mo8WY+heFg8onJ5aV+bAhzpMRmC2DcVR3Yvoq64eAx8hSUI6eW5lsc+CseakV8Jc
BOJLI+4FVhJf1qgFmTHyBvKhL8IXy80E3bBApVcNTbCHCpBcsys/ga4stwCg3BUl
IFWMdNTkffrNlupUwfqmp8UD/j7ZkusPO0RjAO0gD3FBZMqeR1IfrYlsdW3oTglr
NdjNuPYe1mvT3EiBAoEEthaPE3XR3kY1HQBzoFyCK7DyBUI7AmVFMzybTxm89ufQ
rAr/kWSMY20fYjQZxX4705NvuLa3cGdKnmLP+cPmTHl5jV4+9d3NC58+38lpcfl7
UHk+A/wIvjiVzZgpRGF+BQccpo7JcCzJcXgxACx1lqRKNGDrSnJHUDWDXogy70xE
/kwUPuLTb78mAZsJPsB+d3aL1/SoKeTcveDc+EFfGPbrA+gzLhXj1GVmK4YbCcBz
TCTmVkm1OALfj0GpsP7e91wqLaAvf85sx097SDPkYanEbrE8jbQyRFNBIDEwMjQg
KFdpdGggRUxHLUUgc3Via2V5KSA8ZHNhLTEwMjRAY2hldmFoLmNvbT6IYQQTEQIA
IQUCXo8cmgIbIwYLCQgHAwIGFQgCCQoLAxYCAQIeAQIXgAAKCRC9K1lIgMBFqjh9
AJ9b/ezL1h0vv8WlkWA6wqnos+8tjACfWAGMDA0hXoEyL51G7dhLUyi2+5+5AQ0E
Xo8cmhAEAKRKf9/t3LPvCVrdOo3smQ8+QjB5GTSXFr+9CF7mbfPYp2vq6jcAlK5g
kOAXLdPvWE4DKgZBrK+kRJzs7CC+n5xo7i7nFH634lcsIIi56sYYgvz4NOahd10w
yNavtcIBBCi4qWv+L9UtDld1eXoZxFYyOGrQ6tOKZq0sV30/GZ+3AAMFA/9xxj4G
y72HAKlqP1Bv3n39c6cJZobbXjbG4YOFNfxjiAk9Y1kr857X1GeR4TV/mK4Zg0pE
4vLakIxoJfdYhYR54LdDeHeUC/UM6uiqkYhAsWNukjpQa6WxJT4+X5Vq5feYQRCV
2um4Aj9gEEHckw7RCAS0kvd9y+s5sgDpiVvmy4hJBBgRAgAJBQJejxyaAhsMAAoJ
EL0rWUiAwEWq6LUAoJXpzDlp/9bIQe1gMf39y5FCAZ74AJ4w+IZ6mMMBZKmkq/fD
4i34eph/pg==
=9ybR
-----END PGP PUBLIC KEY BLOCK-----
```


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ffa157c..b1bb543 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,17 +12,17 @@ You can find out backwards-compatibility policy [here](https://github.com/hynek/
 
 ## [UNRELEASED](https://github.com/hynek/pem/compare/21.2.0...HEAD)
 
-### Backwards-incompatible changes:
+### Removed
 
 - Support for Python 2.7, 3.5, and 3.6 has been dropped.
 
-### Deprecations:
 
-*none*
+### Added
 
-### Changes:
+- Support for RFC 4880 OpenPGP private & public keys: `pem.OpenPGPPublicKey` and `pem.OpenPGPPrivateKey`.
+  [#72](https://github.com/hynek/pem/issues/72)
+- `pem.parse_file()` now accepts also [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects.
 
-- `pem.parse_file()` now accepts [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects.
 
 ## [21.2.0](https://github.com/hynek/pem/compare/21.1.0...21.2.0) - 2021-04-07
 
diff --git a/docs/api.rst b/docs/api.rst
index 2081e34..980d4ef 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -38,6 +38,8 @@ The following objects can be returned by the parsing functions.
 .. autoclass:: OpenSSHPrivateKey(PrivateKey)
 .. autoclass:: SSHPublicKey(Key)
 .. autoclass:: SSHCOMPrivateKey(PrivateKey)
+.. autoclass:: OpenPGPPublicKey(PublicKey)
+.. autoclass:: OpenPGPPrivateKey(PrivateKey)
 .. autoclass:: DHParameters(AbstractPEMObject)
 .. autoclass:: CertificateRequest(AbstractPEMObject)
 .. autoclass:: CertificateRevocationList(AbstractPEMObject)
diff --git a/src/pem/__init__.py b/src/pem/__init__.py
index d297906..c9849c9 100644
--- a/src/pem/__init__.py
+++ b/src/pem/__init__.py
@@ -7,6 +7,8 @@ from ._core import (
     DSAPrivateKey,
     ECPrivateKey,
     Key,
+    OpenPGPPrivateKey,
+    OpenPGPPublicKey,
     OpenSSHPrivateKey,
     OpenSSLTrustedCertificate,
     PrivateKey,
@@ -44,14 +46,16 @@ __all__ = [
     "DSAPrivateKey",
     "ECPrivateKey",
     "Key",
+    "OpenPGPPrivateKey",
+    "OpenPGPPublicKey",
     "OpenSSHPrivateKey",
     "OpenSSLTrustedCertificate",
+    "parse_file",
+    "parse",
     "PrivateKey",
     "PublicKey",
     "RSAPrivateKey",
     "RSAPublicKey",
-    "parse",
-    "parse_file",
     "SSHCOMPrivateKey",
     "SSHPublicKey",
     "twisted",
diff --git a/src/pem/_core.py b/src/pem/_core.py
index c6eeffa..9cbdd41 100644
--- a/src/pem/_core.py
+++ b/src/pem/_core.py
@@ -189,8 +189,7 @@ class OpenSSHPrivateKey(PrivateKey):
 
 class SSHPublicKey(PublicKey):
     """
-    A public key in SSH
-    `RFC 4716 <https://tools.ietf.org/html/rfc4716>`_ format.
+    A public key in SSH :rfc:`4716` format.
 
     The Secure Shell (SSH) Public Key File Format.
 
@@ -206,6 +205,22 @@ class SSHCOMPrivateKey(PrivateKey):
     """
 
 
+class OpenPGPPublicKey(PublicKey):
+    """
+    An :rfc:`4880` armored OpenPGP public key.
+
+    .. versionadded:: 23.1.0
+    """
+
+
+class OpenPGPPrivateKey(PrivateKey):
+    """
+    An :rfc:`4880` armored OpenPGP private key.
+
+    .. versionadded:: 23.1.0
+    """
+
+
 _PEM_TO_CLASS: dict[bytes, type[AbstractPEMObject]] = {
     b"CERTIFICATE": Certificate,
     b"TRUSTED CERTIFICATE": OpenSSLTrustedCertificate,
@@ -223,6 +238,8 @@ _PEM_TO_CLASS: dict[bytes, type[AbstractPEMObject]] = {
     b"SSH2 PUBLIC KEY": SSHPublicKey,
     b"SSH2 ENCRYPTED PRIVATE KEY": SSHCOMPrivateKey,
     b"X509 CRL": CertificateRevocationList,
+    b"PGP PUBLIC KEY BLOCK": OpenPGPPublicKey,
+    b"PGP PRIVATE KEY BLOCK": OpenPGPPrivateKey,
 }
 
 # See https://tools.ietf.org/html/rfc1421


Test patch:
diff --git a/tests/data.py b/tests/data.py
index e9a682e..8348840 100644
--- a/tests/data.py
+++ b/tests/data.py
@@ -308,3 +308,108 @@ EA3m/0JWkf61807iZ7AV8umYJMmNQ35HadG53n9nitpFEAAAEA4OQI1Rrh8e1EZ5qJBV8o
 gGyxzt4OdoXzuOtxkbHUB3U=
 ---- END SSH2 ENCRYPTED PRIVATE KEY ----
 """
+
+# https://www.intel.com/content/www/us/en/security-center/pgp-public-key.html
+KEY_PEM_OPENPGP_PUBLIC = b"""\
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: Encryption Desktop 10.4.2 (Build 289)
+
+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
+kss=
+=hCPU
+-----END PGP PUBLIC KEY BLOCK-----
+"""
+
+# From https://www.ietf.org/archive/id/draft-bre-openpgp-samples-01.html
+KEY_PEM_OPENPGP_PRIVATE = b"""\
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Comment: Alice's OpenPGP Transferable Secret Key
+Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html
+
+lFgEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U
+b7O1u10AAP9XBeW6lzGOLx7zHH9AsUDUTb2pggYGMzd0P3ulJ2AfvQ4RtCZBbGlj
+ZSBMb3ZlbGFjZSA8YWxpY2VAb3BlbnBncC5leGFtcGxlPoiQBBMWCAA4AhsDBQsJ
+CAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE64W7X6M6deFelE5j8jFVDE9H444FAl2l
+nzoACgkQ8jFVDE9H447pKwD6A5xwUqIDprBzrHfahrImaYEZzncqb25vkLV2arYf
+a78A/R3AwtLQvjxwLDuzk4dUtUwvUYibL2sAHwj2kGaHnfICnF0EXEcE6RIKKwYB
+BAGXVQEFAQEHQEL/BiGtq0k84Km1wqQw2DIikVYrQrMttN8d7BPfnr4iAwEIBwAA
+/3/xFPG6U17rhTuq+07gmEvaFYKfxRB6sgAYiW6TMTpQEK6IeAQYFggAIBYhBOuF
+u1+jOnXhXpROY/IxVQxPR+OOBQJcRwTpAhsMAAoJEPIxVQxPR+OOWdABAMUdSzpM
+hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb
+Pnn+We1aTBhaGa86AQ==
+=n8OM
+-----END PGP PRIVATE KEY BLOCK-----
+"""
diff --git a/tests/test_core.py b/tests/test_core.py
index e9f8293..55320de 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -18,6 +18,8 @@ from .data import (
     DH_PEM,
     KEY_PEM_DSA_PRIVATE,
     KEY_PEM_EC_PRIVATE,
+    KEY_PEM_OPENPGP_PRIVATE,
+    KEY_PEM_OPENPGP_PUBLIC,
     KEY_PEM_OPENSSH,
     KEY_PEM_PKCS5_ENCRYPTED,
     KEY_PEM_PKCS5_UNENCRYPTED,
@@ -591,3 +593,21 @@ class TestParse:
 
         assert isinstance(key, pem.SSHCOMPrivateKey)
         assert KEY_PEM_SSHCOM_PRIVATE == key.as_bytes()
+
+    def test_openpgp_public_key(self):
+        """
+        Detects and loads OpenPGP public keys.
+        """
+        (key,) = pem.parse(KEY_PEM_OPENPGP_PUBLIC)
+
+        assert isinstance(key, pem.OpenPGPPublicKey)
+        assert KEY_PEM_OPENPGP_PUBLIC == key.as_bytes()
+
+    def test_openpgp_private_key(self):
+        """
+        Detects and loads OpenPGP private keys.
+        """
+        (key,) = pem.parse(KEY_PEM_OPENPGP_PRIVATE)
+
+        assert isinstance(key, pem.OpenPGPPrivateKey)
+        assert KEY_PEM_OPENPGP_PRIVATE == key.as_bytes()