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

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. 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"

6. 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.7.tar.gz (114.1 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.7-cp310-abi3-win_amd64.whl (3.2 MB view details)

Uploaded CPython 3.10+Windows x86-64

rpgp_py-0.19.7-cp310-abi3-win32.whl (3.0 MB view details)

Uploaded CPython 3.10+Windows x86

rpgp_py-0.19.7-cp310-abi3-musllinux_1_2_x86_64.whl (3.9 MB view details)

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

rpgp_py-0.19.7-cp310-abi3-musllinux_1_2_aarch64.whl (3.6 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

rpgp_py-0.19.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.6 MB view details)

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

rpgp_py-0.19.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (3.5 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

rpgp_py-0.19.7-cp310-abi3-macosx_11_0_arm64.whl (3.2 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

rpgp_py-0.19.7-cp310-abi3-macosx_10_12_x86_64.whl (3.4 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: rpgp_py-0.19.7.tar.gz
  • Upload date:
  • Size: 114.1 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.7.tar.gz
Algorithm Hash digest
SHA256 fc5b47d45756627e129f537ad903e36599b02cc58547b4961b700d3508d23385
MD5 fea8d09122ff320906437e5804d8eb73
BLAKE2b-256 627e0b9d978992972c2ac1509088dc738ce2e00a195185cbf44095f44edc1b0e

See more details on using hashes here.

File details

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

File metadata

  • Download URL: rpgp_py-0.19.7-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 3.2 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.7-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 96259ff8d84bb1c23cadf74350b00b4afce748a99757c322f7ee286db6c27878
MD5 18240f6c41fede8b5b0f9345923c7e41
BLAKE2b-256 6d58748814d8a584be4f94a4a15f02f2387ceded4a69c11913812d57427f381d

See more details on using hashes here.

File details

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

File metadata

  • Download URL: rpgp_py-0.19.7-cp310-abi3-win32.whl
  • Upload date:
  • Size: 3.0 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.7-cp310-abi3-win32.whl
Algorithm Hash digest
SHA256 d57444c7663f8c78475ae716c167d9ac21fa6d1f7a10afdc1b788ea5a3453975
MD5 c87da643890b463318f339dfbfb702b4
BLAKE2b-256 89e4ebbb8108b914a217ed487c9bbff7b3cabd6cf44e743dd81dc0370d25784d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.7-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 ac8d683aa393369365cb7f99f7db71b5759c094f1bff4ba6600505b67da9877a
MD5 1f0f18888523fcfef32468226dbf5ae4
BLAKE2b-256 22fe2a6c0c59e7a5adc38b32dc10476b0c74aa6dce08125277ab22e00ef682b3

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.7-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 f43576e440a7ff64f1e022e8ce5db54d448ec43d0e9e52a860251040cd249908
MD5 32a7a0774bf4fe4ab6a7ffee99fb8e56
BLAKE2b-256 b73067cec0edbc807335fe3fccf901dd7e876b4a103fd4626f50f510d48d4d40

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 7a53dc9c6c266f16d0e02e4c0af57361265e9eaacb39e8cc762345c3634f38e9
MD5 92e899561a265942c5040163e83abc42
BLAKE2b-256 75483a7bf258fc4f0e8cab119ca4163fb14df90bbb00c19abdd5bc9518e72f53

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 8782efc158a22cf471fd215f5362e1272855c243976a7db920d2eb41b9d2d864
MD5 9a4f3dc919285a6d41dab8607f9dcc4f
BLAKE2b-256 482f838a76325a394a59c6b95e1256b2533701f74db5ec447e2bea591ee49e20

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.7-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 398979c34838c85055592bfb4bf362e29eeaad549934fda39e4c0c08e3ee4e3e
MD5 aed370ae4633d72bc95515698e2a504c
BLAKE2b-256 df4bf5ceca5b893486a951bc0f54066ddd416b0b5158212c53a1d4fe03df8e3e

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.7-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 ea7aa88d6d36b57f53e4253f408f0389ec5a3ac27d8935b25387e4d49f7d46f8
MD5 0cedc5a021d795abc88f9d3843d9f2c6
BLAKE2b-256 212075b539ad976ff24bcfa067c7f38c94a08714ff87e668c954166d88320c38

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