Skip to content

Commit

Permalink
Add support for headers
Browse files Browse the repository at this point in the history
Ref #45
  • Loading branch information
hynek committed Jun 20, 2023
1 parent 0a9780a commit 8409ac1
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ You can find out backwards-compatibility policy [here](https://github.com/hynek/

- Support for RFC 4880 OpenPGP private & public keys: `pem.OpenPGPPublicKey` and `pem.OpenPGPPrivateKey`.
[#72](https://github.com/hynek/pem/issues/72)
- Support for intra-payload headers like the ones used in OpenPGP keys using the `meta_headers` property.
- `pem.parse_file()` now accepts also [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects.
- Added `payload_as_text()`, `payload_as_bytes()` and `payload_decoded()` to all PEM objects that allow to directly access the payload without the envelope and possible headers.
[#74](https://github.com/hynek/pem/pull/74)
Expand Down
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The following objects can be returned by the parsing functions.
Their shared provided API is minimal:

.. autoclass:: AbstractPEMObject
:members: __str__, as_bytes, as_text, sha1_hexdigest, payload_as_bytes, payload_as_text, payload_decoded
:members: __str__, as_bytes, as_text, sha1_hexdigest, payload_as_bytes, payload_as_text, payload_decoded, meta_headers


Twisted
Expand Down
22 changes: 22 additions & 0 deletions src/pem/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@ def payload_decoded(self) -> bytes:
"""
return b64decode(self._extracted_payload)

@cached_property
def meta_headers(self) -> dict[str, str]:
"""
Return a dictionary of payload headers.
.. versionadded:: 23.1.0
"""
expl = {}
for line in self._pem_bytes.decode().splitlines()[1:-1]:
if ":" not in line:
break

key, val = line.split(": ", 1)

# Strip quotes if they're only at the beginning and end.
if val.count('"') == 2 and val[0] == '"' and val[-1] == '"':
val = val[1:-1]

expl[key] = val

return expl

def __eq__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
Expand Down
21 changes: 19 additions & 2 deletions tests/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,7 @@
# Taken from https://tools.ietf.org/html/rfc4716#section-3.6.
KEY_PEM_RFC4716_PUBLIC = rb"""---- BEGIN SSH2 PUBLIC KEY ----
Subject: me
Comment: 1024-bit rsa, created by [email protected] Mon Jan 15 \
08:31:24 2001
Comment: 1024-bit rsa, created by [email protected] Mon Jan 15 08:31:24 2001
AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4
596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4
soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=
Expand Down Expand Up @@ -436,3 +435,21 @@
=n8OM
-----END PGP PRIVATE KEY BLOCK-----
"""

# From https://datatracker.ietf.org/doc/html/rfc7468#section-5.2
CERT_PEM_CERTIFICATE_EXPLANATORY = b"""\
Subject: CN=Atlantis
Issuer: CN=Atlantis
Validity: from 7/9/2012 3:10:38 AM UTC to 7/9/2013 3:10:37 AM UTC
-----BEGIN CERTIFICATE-----
MIIBmTCCAUegAwIBAgIBKjAJBgUrDgMCHQUAMBMxETAPBgNVBAMTCEF0bGFudGlz
MB4XDTEyMDcwOTAzMTAzOFoXDTEzMDcwOTAzMTAzN1owEzERMA8GA1UEAxMIQXRs
YW50aXMwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAu+BXo+miabDIHHx+yquqzqNh
Ryn/XtkJIIHVcYtHvIX+S1x5ErgMoHehycpoxbErZmVR4GCq1S2diNmRFZCRtQID
AQABo4GJMIGGMAwGA1UdEwEB/wQCMAAwIAYDVR0EAQH/BBYwFDAOMAwGCisGAQQB
gjcCARUDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDAzA1BgNVHQEE
LjAsgBA0jOnSSuIHYmnVryHAdywMoRUwEzERMA8GA1UEAxMIQXRsYW50aXOCASow
CQYFKw4DAh0FAANBAKi6HRBaNEL5R0n56nvfclQNaXiDT174uf+lojzA4lhVInc0
ILwpnZ1izL4MlI9eCSHhVQBHEp2uQdXJB+d5Byg=
-----END CERTIFICATE-----
"""
39 changes: 38 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def test_payload_as_text(self):
(KEY_PEM_SSHCOM_PRIVATE, b"Comment:"),
(KEY_PEM_OPENPGP_PUBLIC, b"Version:"),
(KEY_PEM_OPENPGP_PRIVATE, b"Comment:"),
(KEY_PEM_PKCS5_ENCRYPTED, (b"Proc-Type:")),
(KEY_PEM_PKCS5_ENCRYPTED, b"Proc-Type:"),
],
)
def test_payload_with_headers(self, bs, forbidden):
Expand Down Expand Up @@ -663,3 +663,40 @@ def test_openpgp_private_key(self):

assert isinstance(key, pem.OpenPGPPrivateKey)
assert KEY_PEM_OPENPGP_PRIVATE == key.as_bytes()

@pytest.mark.parametrize(
"bs, hdrs",
[
(KEY_PEM_SSHCOM_PRIVATE, {"Comment": "rsa-key-20210120"}),
(
KEY_PEM_OPENPGP_PUBLIC,
{"Version": "Encryption Desktop 10.4.2 (Build 289)"},
),
(
KEY_PEM_OPENPGP_PRIVATE,
{
"Comment": "https://www.ietf.org/id/draft-bre-openpgp-samples-01.html"
},
),
(
KEY_PEM_PKCS5_ENCRYPTED,
{
"DEK-Info": "DES-EDE3-CBC,8A72BD2DC1C9092F",
"Proc-Type": "4,ENCRYPTED",
},
),
],
)
def test_headers(self, bs, hdrs):
"""
Headers are preserved.
"""
assert hdrs == pem.parse(bs)[0].meta_headers

def test_no_headers(self):
"""
No headers, no problem.
"""
cert = pem.parse(CERT_PEM_OPENSSL_TRUSTED)[0]

assert {} == cert.meta_headers

0 comments on commit 8409ac1

Please sign in to comment.