Python binding for rPGP, a Rust implementation of OpenPGP according to RFC 9580
Project description
rpgp-py
Python bindings for rPGP, exposed as the openpgp package.
- support for RFC 9580
- a typed Python surface (
.pyistubs 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-pyfollows the Rustpgpcrate, which targets newer OpenPGP work such as RFC 9580-compatible v6 key material and modern curves/packet handling.PGPyandPGPy13are 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:
PGPystill importsimghdr, which was removed from the standard library in Python 3.13.PGPy13exists as a compatibility fork;rpgp-pytargets 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)
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
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
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc5b47d45756627e129f537ad903e36599b02cc58547b4961b700d3508d23385
|
|
| MD5 |
fea8d09122ff320906437e5804d8eb73
|
|
| BLAKE2b-256 |
627e0b9d978992972c2ac1509088dc738ce2e00a195185cbf44095f44edc1b0e
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96259ff8d84bb1c23cadf74350b00b4afce748a99757c322f7ee286db6c27878
|
|
| MD5 |
18240f6c41fede8b5b0f9345923c7e41
|
|
| BLAKE2b-256 |
6d58748814d8a584be4f94a4a15f02f2387ceded4a69c11913812d57427f381d
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d57444c7663f8c78475ae716c167d9ac21fa6d1f7a10afdc1b788ea5a3453975
|
|
| MD5 |
c87da643890b463318f339dfbfb702b4
|
|
| BLAKE2b-256 |
89e4ebbb8108b914a217ed487c9bbff7b3cabd6cf44e743dd81dc0370d25784d
|
File details
Details for the file rpgp_py-0.19.7-cp310-abi3-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: rpgp_py-0.19.7-cp310-abi3-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 3.9 MB
- Tags: CPython 3.10+, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac8d683aa393369365cb7f99f7db71b5759c094f1bff4ba6600505b67da9877a
|
|
| MD5 |
1f0f18888523fcfef32468226dbf5ae4
|
|
| BLAKE2b-256 |
22fe2a6c0c59e7a5adc38b32dc10476b0c74aa6dce08125277ab22e00ef682b3
|
File details
Details for the file rpgp_py-0.19.7-cp310-abi3-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: rpgp_py-0.19.7-cp310-abi3-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 3.6 MB
- Tags: CPython 3.10+, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f43576e440a7ff64f1e022e8ce5db54d448ec43d0e9e52a860251040cd249908
|
|
| MD5 |
32a7a0774bf4fe4ab6a7ffee99fb8e56
|
|
| BLAKE2b-256 |
b73067cec0edbc807335fe3fccf901dd7e876b4a103fd4626f50f510d48d4d40
|
File details
Details for the file rpgp_py-0.19.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: rpgp_py-0.19.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 3.6 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a53dc9c6c266f16d0e02e4c0af57361265e9eaacb39e8cc762345c3634f38e9
|
|
| MD5 |
92e899561a265942c5040163e83abc42
|
|
| BLAKE2b-256 |
75483a7bf258fc4f0e8cab119ca4163fb14df90bbb00c19abdd5bc9518e72f53
|
File details
Details for the file rpgp_py-0.19.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: rpgp_py-0.19.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 3.5 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8782efc158a22cf471fd215f5362e1272855c243976a7db920d2eb41b9d2d864
|
|
| MD5 |
9a4f3dc919285a6d41dab8607f9dcc4f
|
|
| BLAKE2b-256 |
482f838a76325a394a59c6b95e1256b2533701f74db5ec447e2bea591ee49e20
|
File details
Details for the file rpgp_py-0.19.7-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: rpgp_py-0.19.7-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
398979c34838c85055592bfb4bf362e29eeaad549934fda39e4c0c08e3ee4e3e
|
|
| MD5 |
aed370ae4633d72bc95515698e2a504c
|
|
| BLAKE2b-256 |
df4bf5ceca5b893486a951bc0f54066ddd416b0b5158212c53a1d4fe03df8e3e
|
File details
Details for the file rpgp_py-0.19.7-cp310-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: rpgp_py-0.19.7-cp310-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 3.4 MB
- Tags: CPython 3.10+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea7aa88d6d36b57f53e4253f408f0389ec5a3ac27d8935b25387e4d49f7d46f8
|
|
| MD5 |
0cedc5a021d795abc88f9d3843d9f2c6
|
|
| BLAKE2b-256 |
212075b539ad976ff24bcfa067c7f38c94a08714ff87e668c954166d88320c38
|