Skip to main content

Python binding for rPGP, a Rust implementation of OpenPGP according to RFC 9580

Project description

rpgp-py

Supported versions PyPI Downloads GitHub stars pyrefly License: MIT

Python bindings for rPGP, exposed as the openpgp package.

  • support for RFC 9580
  • a typed Python surface (.pyi stubs ship with the package),
  • wheels for Python 3.10+,
  • high-level helpers for common signing/encryption workflows,
  • detailed inspection APIs for packets, signatures, key bindings, and generated key material.

Why use rpgp-py instead of PGPy or PGPy13?

Broadly:

  • RFC 9580 coverage: rpgp-py follows the Rust pgp crate, which targets newer OpenPGP work such as RFC 9580-compatible v6 key material and modern curves/packet handling. PGPy and PGPy13 are still RFC 4880.
  • Rust core: the cryptographic core is implemented in Rust and exposed through a Python-first API.
  • Typed builders and inspectors: the package exposes typed builders for key generation plus rich metadata for self-signatures, key flags, features, user bindings, S2K settings, and public-key parameters.
  • Python 3.13 support: PGPy still imports imghdr, which was removed from the standard library in Python 3.13. PGPy13 exists as a compatibility fork; rpgp-py targets current Python directly.

Installation

pip install rpgp-py

Reference documentation

When you need the underlying Rust semantics or want to compare behaviour against upstream docs, these are the useful references:

Use cases

1. Parse and inspect transferable keys

from openpgp import PublicKey, SecretKey

public_key, _ = PublicKey.from_armor(public_key_armor)
public_key.verify_bindings()

secret_key, _ = SecretKey.from_armor(secret_key_armor)
assert secret_key.to_public_key().fingerprint == public_key.fingerprint
assert public_key.public_subkey_count >= 0
assert secret_key.secret_subkey_count >= 0

if public_key.public_subkeys:
    assert public_key.public_subkeys[0].fingerprint == public_key.subkey_bindings()[0].fingerprint
if secret_key.secret_subkeys:
    assert (
        secret_key.secret_subkeys[0].signed_public_key().fingerprint
        == secret_key.public_subkeys[0].fingerprint
    )

2. Sign and verify messages and detached signatures

from openpgp import DetachedSignature, Message, sign_message, sign_message_many

signed = sign_message(b"hello world", secret_key)
message, _ = Message.from_armor(signed)
message.verify(public_key)
assert message.payload_text() == "hello world"

signature = DetachedSignature.sign_binary(b"hello world", secret_key)
signature.verify(public_key, b"hello world")
info = signature.signature_info()
assert info.signature_type == "binary"
assert info.hash_algorithm == "SHA256"

text_signature = DetachedSignature.sign_text(
    "hello\nworld\n",
    secret_key,
    hash_algorithm="sha512",
)
text_signature.verify_text(public_key, "hello\r\nworld\r\n")
assert text_signature.signature_info().hash_algorithm == "SHA512"

multi_signed = sign_message_many(
    b"hello world",
    [secret_key, other_secret_key],
    hash_algorithm="sha384",
)
multi_message, _ = Message.from_armor(multi_signed)
assert multi_message.signature_count() == 2

3. Work with cleartext signatures

from openpgp import (
    CleartextSignedMessage,
    sign_cleartext_message,
    sign_cleartext_message_many,
)

armored = sign_cleartext_message("hello\n-world\n", secret_key)
message, _ = CleartextSignedMessage.from_armor(armored)

assert message.signed_text() == "hello\r\n-world\r\n"
assert message.signature_count() == 1
message.verify(public_key)

multi_armored = sign_cleartext_message_many(
    "hello\n-world\n",
    [secret_key, other_secret_key],
    hash_algorithm="sha384",
)
multi_message, _ = CleartextSignedMessage.from_armor(multi_armored)
assert multi_message.signature_count() == 2

4. Encrypt and decrypt OpenPGP messages

Recipient encryption:

from openpgp import Message, encrypt_message_to_recipient, encrypt_message_to_recipients

recipient_encrypted = encrypt_message_to_recipient(b"secret", public_key)
recipient_message, _ = Message.from_armor(recipient_encrypted)
recipient_decrypted = recipient_message.decrypt(secret_key)
assert recipient_decrypted.payload_bytes() == b"secret"

shared_encrypted = encrypt_message_to_recipients(
    b"secret",
    [public_key, other_public_key],
    anonymous_recipient=True,
)
shared_message, _ = Message.from_armor(shared_encrypted)
assert len(shared_message.public_key_encrypted_session_key_packets()) == 2
assert all(
    packet.recipient_is_anonymous
    for packet in shared_message.public_key_encrypted_session_key_packets()
)

Password encryption:

from openpgp import Message, encrypt_message_with_password

password_encrypted = encrypt_message_with_password(b"secret", "hunter2")
password_message, _ = Message.from_armor(password_encrypted)
password_decrypted = password_message.decrypt_with_password("hunter2")
assert password_decrypted.payload_text() == "secret"

Binary output, packet access, and caller-supplied session keys:

from openpgp import (
    Message,
    encrypt_message_to_recipient_bytes,
    encrypt_session_key_to_recipient,
)

session_key = bytes(range(16))
message_bytes = encrypt_message_to_recipient_bytes(
    b"secret",
    public_key,
    version="seipd-v2",
    symmetric_algorithm="aes128",
    session_key=session_key,
)

message = Message.from_bytes(message_bytes)
pkesk = message.public_key_encrypted_session_key_packets()[0]
edata = message.encrypted_data_packet()

assert pkesk.recipient_is_anonymous is False
assert edata.kind == "seipd-v2"
assert message.decrypt_with_session_key(session_key).payload_bytes() == b"secret"

raw_pkesk = encrypt_session_key_to_recipient(
    session_key,
    public_key,
    version="seipd-v2",
    symmetric_algorithm="aes128",
).to_bytes()
assert raw_pkesk

5. Build messages with the upstream-style MessageBuilder API

from openpgp import ArmorOptions, Message, MessageBuilder, StringToKey

armored = (
    MessageBuilder.from_bytes("hello.txt", b"Hello, world!")
    .compression("zlib")
    .seipd_v2("aes256", "ocb")
    .encrypt_with_password(StringToKey.argon2(1, 4, 21), "hunter2")
    .to_armored_string(
        ArmorOptions({"Comment": ["built with MessageBuilder"]}, include_checksum=False)
    )
)

message, headers = Message.from_armor(armored)
decrypted = message.decrypt_with_password("hunter2")

assert headers == {"Comment": ["built with MessageBuilder"]}
assert decrypted.kind == "compressed"
assert decrypted.payload_text() == "Hello, world!"
assert "\n=" not in armored

The same builder surface also accepts operational subkey objects when you want the Rust docs' subkey-oriented examples to translate directly:

subkey_signed_and_encrypted = (
    MessageBuilder.from_bytes("hello.txt", b"Hello, world!")
    .sign(secret_key.secret_subkeys[0])
    .seipd_v2("aes256", "ocb")
    .encrypt_to_key(public_key.public_subkeys[0])
    .to_armored_string()
)

It also exposes the remaining simple builder workflow methods for file-like objects and literal/signature mode selection:

import io

from openpgp import Message, MessageBuilder

writer = io.StringIO()

(
    MessageBuilder.from_reader("notes.txt", io.BytesIO(b"hello\r\nworld\r\n"))
    .data_mode("utf8")
    .sign_text()
    .sign(secret_key)
    .to_armored_writer(writer)
)

message, _ = Message.from_armor(writer.getvalue())
assert message.literal_mode() == "utf8"
assert message.signature_infos()[0].signature_type == "text"

6. Generate modern RFC 9580-compatible key material

from openpgp import (
    EncryptionCaps,
    KeyType,
    Message,
    PacketHeaderVersion,
    SecretKeyParamsBuilder,
    SubkeyParamsBuilder,
    UserAttribute,
    encrypt_message_to_recipient,
    sign_message,
)

secret_key = (
    SecretKeyParamsBuilder()
    .version(6)
    .created_at(1_700_000_000)
    .key_type(KeyType.ed25519())
    .can_certify(True)
    .can_sign(True)
    .packet_version(PacketHeaderVersion.new())
    .feature_seipd_v2(True)
    .primary_user_id("Me <me@example.com>")
    .preferred_symmetric_algorithms(["aes256", "aes192", "aes128"])
    .preferred_hash_algorithms(["sha256", "sha384", "sha512", "sha224"])
    .preferred_compression_algorithms(["zlib", "zip"])
    .user_attribute(UserAttribute.image_jpeg(bytes.fromhex("ffd8ffe000104a464946000101")))
    .subkey(
        SubkeyParamsBuilder()
        .version(6)
        .created_at(1_700_000_123)
        .key_type(KeyType.x25519())
        .packet_version(PacketHeaderVersion.new())
        .can_encrypt(EncryptionCaps.all())
        .build()
    )
    .build()
    .generate()
)

public_key = secret_key.to_public_key()
secret_key.verify_bindings()
public_key.verify_bindings()

assert secret_key.version == 6
assert public_key.public_key_algorithm == "ed25519"
assert public_key.public_params.kind == "ed25519"
assert public_key.public_params.curve == "ed25519"
assert public_key.packet_version == PacketHeaderVersion.new()

signed = sign_message(b"generated payload", secret_key)
message, _ = Message.from_armor(signed)
message.verify(public_key)
assert message.payload_bytes() == b"generated payload"

encrypted = encrypt_message_to_recipient(b"secret", public_key)
encrypted_message, _ = Message.from_armor(encrypted)
assert encrypted_message.decrypt(secret_key).payload_bytes() == b"secret"

7. Customize secret-key S2K protection for generated keys

from openpgp import (
    EncryptionCaps,
    KeyType,
    S2kParams,
    SecretKeyParamsBuilder,
    StringToKey,
    SubkeyParamsBuilder,
)

secret_key = (
    SecretKeyParamsBuilder()
    .version(6)
    .key_type(KeyType.ed25519())
    .can_certify(True)
    .can_sign(True)
    .primary_user_id("Me <me@example.com>")
    .passphrase("hunter2")
    .s2k(
        S2kParams.aead(
            "aes256",
            "ocb",
            StringToKey.argon2(3, 4, 16),
        )
    )
    .subkey(
        SubkeyParamsBuilder()
        .version(6)
        .key_type(KeyType.x25519())
        .can_encrypt(EncryptionCaps.all())
        .passphrase("hunter2")
        .s2k(
            S2kParams.cfb(
                "aes128",
                StringToKey.iterated("sha256", 96),
            )
        )
        .build()
    )
    .build()
    .generate()
)

primary_s2k = secret_key.primary_secret_s2k()
assert primary_s2k.usage == "aead"
assert primary_s2k.aead_algorithm == "ocb"
assert primary_s2k.string_to_key is not None
assert primary_s2k.string_to_key.kind == "argon2"

Benchmarks

Median runtime graph (1 KiB payload, lower is better)

Grouped benchmark chart for the shared workflows

rpgp-py is substantially faster: roughly 9x–71x faster for key parsing and 25x–48x faster for the sign/verify and recipient-encryption loops.

Password-encryption benchmark

Grouped benchmark chart for password encryption and decryption

This result is shown separately: rpgp-py defaults to modern SEIPDv2 + AEAD (OCB) password-protected messages, while PGPy/PGPy13 remain RFC 4880-era implementations.

Table of results

Operation rpgp-py PGPy13 PGPy
Parse armored public key 0.011 ms 0.786 ms 0.776 ms
Parse armored secret key 0.156 ms 1.473 ms 1.455 ms
Detached sign + verify 2.453 ms 61.329 ms 61.420 ms
Encrypt + decrypt to recipient 2.537 ms 122.726 ms 120.701 ms
Encrypt + decrypt with password 62.369 ms 50.346 ms 50.289 ms

Reproduction

To make that comparison reproducible, the repository now ships:

  • scripts/benchmark.py – an isolated benchmark runner,
  • docs/benchmarks/results.json – the committed raw results used below.
uv run --python 3.12 python scripts/benchmark.py

Versioning

rpgp-py's version will reflect the major and minor version of the underlying pgp crate. The patch version will be incremented for both Python-facing API changes and for any internal changes that require a new build of the Rust core, such as dependency updates or bug fixes.

Development

See the list of useful commands by running:

just

Acknowledgements

Many thanks to the rPGP contributors and maintainers for building and documenting the Rust OpenPGP implementation that powers this package.

License

This repository is distributed under the MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rpgp_py-0.19.8.tar.gz (131.7 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

rpgp_py-0.19.8-cp310-abi3-win_amd64.whl (3.4 MB view details)

Uploaded CPython 3.10+Windows x86-64

rpgp_py-0.19.8-cp310-abi3-win32.whl (3.1 MB view details)

Uploaded CPython 3.10+Windows x86

rpgp_py-0.19.8-cp310-abi3-musllinux_1_2_x86_64.whl (4.0 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ x86-64

rpgp_py-0.19.8-cp310-abi3-musllinux_1_2_aarch64.whl (3.8 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

rpgp_py-0.19.8-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.8 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

rpgp_py-0.19.8-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (3.6 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

rpgp_py-0.19.8-cp310-abi3-macosx_11_0_arm64.whl (3.3 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

rpgp_py-0.19.8-cp310-abi3-macosx_10_12_x86_64.whl (3.5 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file rpgp_py-0.19.8.tar.gz.

File metadata

  • Download URL: rpgp_py-0.19.8.tar.gz
  • Upload date:
  • Size: 131.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for rpgp_py-0.19.8.tar.gz
Algorithm Hash digest
SHA256 968b73a9c4a6dff06c106945201b147413111f0cce48b8e5adf4ab9e86142d14
MD5 1e41d5eb1d50b0d7bfadd93a347f22de
BLAKE2b-256 e93b7d627a66b12066fc38edf517cba7558e6c1daa141a5a00a4735529e160aa

See more details on using hashes here.

File details

Details for the file rpgp_py-0.19.8-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: rpgp_py-0.19.8-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 3.4 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for rpgp_py-0.19.8-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 6ca6d22f98baa175faad7bc92c21c6ebcd64be6fd499524320dcb560ea31001f
MD5 637f85d3bc518aea241c39284b642d30
BLAKE2b-256 82f5cb03284487e6d14a7d4df280fde001e0effef6be1c8ca479d007c977ac37

See more details on using hashes here.

File details

Details for the file rpgp_py-0.19.8-cp310-abi3-win32.whl.

File metadata

  • Download URL: rpgp_py-0.19.8-cp310-abi3-win32.whl
  • Upload date:
  • Size: 3.1 MB
  • Tags: CPython 3.10+, Windows x86
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for rpgp_py-0.19.8-cp310-abi3-win32.whl
Algorithm Hash digest
SHA256 035ab197298cbb979ea784c27262bfcde90f544b8d8bd2cf9ce86ef8e235cde5
MD5 2bdec0faea9edcaac8cf51b9548c6b6e
BLAKE2b-256 792606caaee58db0fcb16fdfdc16070ef535eb333d0219fa33def27df3f2d81b

See more details on using hashes here.

File details

Details for the file rpgp_py-0.19.8-cp310-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for rpgp_py-0.19.8-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 0d2152067d02ddcc623288f688953ba6a9d6a95818e3e72b5913b0f2e52b3170
MD5 98e56b3fadad06c1eef4de8385fac239
BLAKE2b-256 56812844751c10718bd773d00fbe653f037bf88ef78289b6f56483488c452373

See more details on using hashes here.

File details

Details for the file rpgp_py-0.19.8-cp310-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for rpgp_py-0.19.8-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 979fda6b9d33af2406a3341c377716dd488edb1ab391f30c24dcd245c9927b8d
MD5 7fe34a8fb04a674e59796337b77c2205
BLAKE2b-256 66b22341b1248d93c50130732d933031dacd474d990e8545c9f1c8826034bad1

See more details on using hashes here.

File details

Details for the file rpgp_py-0.19.8-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for rpgp_py-0.19.8-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f89a2dfa972840b5997a6246774e9c385d3f81a04d64eccb1ed479e280d137e3
MD5 2da3ff5215ccc175bc92f7bea8198d03
BLAKE2b-256 f7c2c96e894b4986a9b49aa28ed99b54cda6eae7cb2675f6536ad7a54d863da9

See more details on using hashes here.

File details

Details for the file rpgp_py-0.19.8-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for rpgp_py-0.19.8-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 3dbf89c162cc50e1b4a0ce2411978cdf79f64cfb2df72494ee41ad9a5552c28e
MD5 fba3c1a6b79b1366c9af111ad15089f6
BLAKE2b-256 64d85248a999469edc8a67fcf35a4d7efcc40246412347179f2b8e8b3a54d2b8

See more details on using hashes here.

File details

Details for the file rpgp_py-0.19.8-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for rpgp_py-0.19.8-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 097b10c6b31c4fba71f71415723168f8dc73f7df7e756626ccea9f2e2e6d4bc7
MD5 910622c6898feaa4394e75d11f7a9757
BLAKE2b-256 1d80e227c688d212a7f7f86e86957b204501c836ba7196c9f2d43607bb4b8f4e

See more details on using hashes here.

File details

Details for the file rpgp_py-0.19.8-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for rpgp_py-0.19.8-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 3800b63bf0a7899f9f531a60d943902d56f8775f49a7c7af8c5dfa5b58f36c0b
MD5 09a6b6c75e926af712303cf43cf20984
BLAKE2b-256 bbcb3d049b94d36c75f10fc6ff30aaa7a242c9d4ba8fc4b9064eb1427534916d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page