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

This is the core entry point when you want to inspect fingerprints, key IDs, OpenPGP key versions, user IDs, subkeys, self-signatures, revocation signatures, or packet-level metadata.

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

For inline or detached signatures, SignatureInfo exposes the signature packet metadata that is often needed for debugging or auditing, including issuer data, notations, and revocation-key metadata when those subpackets are present.

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

encrypt_session_key_to_recipient, encrypt_message_to_recipient, and the multi-recipient helpers all accept anonymous_recipient=True when you want PKESK packets without recipient identifiers.

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

Uploaded CPython 3.10+Windows x86-64

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

Uploaded CPython 3.10+Windows x86

rpgp_py-0.19.5-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.5-cp310-abi3-musllinux_1_2_aarch64.whl (3.7 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

rpgp_py-0.19.5-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.5-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.5-cp310-abi3-macosx_11_0_arm64.whl (3.2 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

rpgp_py-0.19.5-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.5.tar.gz.

File metadata

  • Download URL: rpgp_py-0.19.5.tar.gz
  • Upload date:
  • Size: 110.4 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.5.tar.gz
Algorithm Hash digest
SHA256 c3dbd809e4382d7ac74bbc38c66244ff1d729d9255c5417b298d85143e39f2d0
MD5 c8a60374e0d997e8ff9eba2332f08f01
BLAKE2b-256 136f5a8f9d486cb897a9858c2e166db9c85f849f934351cf4724f9127e3797c5

See more details on using hashes here.

File details

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

File metadata

  • Download URL: rpgp_py-0.19.5-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.5-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 7e64ead58fd49a2d3176b1b657d3e75f73bb0b27898b6a677e3ad42872b95acb
MD5 9e5b02f9d52c78a6cec8d989d0ef90cb
BLAKE2b-256 7c8feeb35ff96ada300efecf8aecca0320711e61c14b35d437bfe933bb338875

See more details on using hashes here.

File details

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

File metadata

  • Download URL: rpgp_py-0.19.5-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.5-cp310-abi3-win32.whl
Algorithm Hash digest
SHA256 9898ebb8a4d634bc3cdd69611a73decf2b754950053f8babd034341e53b55331
MD5 34f0a48258aee7fb379c8761a2f63333
BLAKE2b-256 41b922d97d7a6b45fbd249c70c291bd377d6f2bb4a00643645a513633e2e34ad

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.5-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 7f9ef209b305bae733d02aa8975ef54d8bc6afa78f0c5ed7ab0524a6c5034100
MD5 3bf003083ad787ce2a9736ef37eb9dde
BLAKE2b-256 acfb4fce1655e562e87002e979a3a8e7d88b4f34bba0bcfead1a5dc3b6b39d0d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.5-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 45e27bc2040f54bcf589706f6aeb408c5e6ce52131199980633d6bd2712a57b6
MD5 0481efffb73be3847b4e14f136224a0c
BLAKE2b-256 a453e1166c14a7d3a0327bcc5dd17707013110ce8a6e03de70ada92493666d57

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.5-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 fa95988875a3f3f4b36c7f7edf105c9ecb9097a8e89bc3b924c61b9192f6d3c5
MD5 5ec651a426c8e09b0ed65dc9809e414f
BLAKE2b-256 957683b83527ca3c4790713237422431fbd8c3b18f6c55e27c1c8b0d2f5485e1

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.5-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 54a8f992c9ab5ed677a02ab31a3c3dd9d09758ba812f8ae65f0591a1839b0ddc
MD5 d247b004e67cc3591d54f7919dc1d2c4
BLAKE2b-256 7d3aa690219227502a156b7715303cba4c93bcc7864ee131219f8b30aa6770fb

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.5-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f1067d75d13b5dfd6c8174ad33560ee937f2dca1c5bd6c33a7088a988729ccdd
MD5 16af74078ea5aa61eb4b48af2abe04e9
BLAKE2b-256 2aeabf27a0feeabe9674b8b3a1cce6f1cb1e958875dc5fd8cfb4e75090bb7eb1

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for rpgp_py-0.19.5-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 9c12df00d4da1f02dddec1622251412e63c5b3947d488e58dcf8706f27c0101e
MD5 7a0868ea8a7daec28cab8ecdc3b7ea8e
BLAKE2b-256 d5cf5aeb8f8217692455d2a9b4b3005eab41297614b8cba6a757e7f9072fd5a1

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