Package: pysequoia
PySequoiaThis library providesOpenPGPfacilities in Python through theSequoia PGPlibrary. If you need to work with encryption and
digital signatures using anIETF standardized protocol, this
package is for you!Note: This is a work in progress. The API isnotstable!Buildingset-euxopipefail
python-mvenv.envsource.env/bin/activate
pipinstallmaturin
maturindevelopInstallingPySequoia can be installed throughpip:pipinstallpysequoiaNote that sincepysequoiais implemented largely in Rust, aRust
toolchainis necessary for the installation to succeed.TestingThis entire document is used for end-to-end integration tests that
exercise the package's API surface.The tests assume that these keys and cards exist:# generate a key with passwordgpg--batch--pinentry-modeloopback--passphrasehunter22--quick-gen-keypasswd@example.com
gpg--batch--pinentry-modeloopback--passphrasehunter22--export-secret-keypasswd@example.com>passwd.pgp# generate a key without passwordgpg--batch--pinentry-modeloopback--passphrase''--quick-gen-keyno-passwd@example.comfuture-default
gpg--batch--pinentry-modeloopback--passphrase''--export-secret-keyno-passwd@example.com>no-passwd.pgp# initialize dummy OpenPGP Cardsh/start.shecho12345678>pinCARD_ADMIN="opgpcard admin --card 0000:00000000 --admin-pin pin"$CARD_ADMINimportfull-key.asc$CARD_ADMINname"John Doe"$CARD_ADMINurl"https://example.com/key.pgp"$CARD_ADMINtouch--keySIG--policyFixed$CARD_ADMINtouch--keyDEC--policyOff$CARD_ADMINtouch--keyAUT--policyFixedFunctionsAll examples assume that these basic classes have been imported:frompysequoiaimportCertencryptSigns and encrypts a string to one or more recipients:frompysequoiaimportencrypts=Cert.from_file("passwd.pgp")r=Cert.from_bytes(open("wiktor.asc","rb").read())bytes="content to encrypt".encode("utf8")encrypted=encrypt(signer=s.secrets.signer("hunter22"),recipients=[r],bytes=bytes).decode("utf8")print(f"Encrypted data:{encrypted}")decryptDecrypts data:frompysequoiaimportdecryptsender=Cert.from_file("no-passwd.pgp")receiver=Cert.from_file("passwd.pgp")content="Red Green Blue"encrypted=encrypt(signer=sender.secrets.signer(),recipients=[receiver],bytes=content.encode("utf8"))decrypted=decrypt(decryptor=receiver.secrets.decryptor("hunter22"),bytes=encrypted)assertcontent==decrypted.bytes.decode("utf8");signSigns data and returns armored output:frompysequoiaimportsigns=Cert.from_file("signing-key.asc")signed=sign(s.secrets.signer(),"data to be signed".encode("utf8"))print(f"Signed data:{signed}")assert"PGP MESSAGE"instr(signed)verifyVerifies signed data and returns verified data:frompysequoiaimportStore,verify# sign some datasigning_key=Cert.from_file("signing-key.asc")signed=sign(s.secrets.signer(),"data to be signed".encode("utf8"))# verify the datastore=Store("/tmp/store")store.put(signing_key)result=verify(signed,store)assertresult.bytes.decode("utf8")=="data to be signed"CertificatesTheCertclass represents one OpenPGP certificate (commonly called a
"public key").This package additionally verifies the certificate using Sequoia PGP'sStandardPolicy. This means that certificates using weak
cryptography can fail to load, or present a different view than in
other OpenPGP software (e.g. if a User ID uses SHA-1 in its
back-signature, it may be missing from the list of User IDs returned
by this package).Certificates have two forms, one is ASCII armored and one is raw bytes:cert=Cert.generate("Test <test@example.com>")print(f"Armored cert:{cert}")print(f"Bytes of the cert:{cert.bytes()}")ParsingCertificates can be parsed from files (Cert.from_file) or bytes in
memory (Cert.from_bytes).cert1=Cert.generate("Test <test@example.com>")buffer=cert1.bytes()parsed_cert=Cert.from_bytes(buffer)assertstr(parsed_cert.user_ids[0])=="Test <test@example.com>"They can also be picked from "keyring" files (Cert.split_file) or
bytes in memory (Cert.split_bytes) which are collections of binary
certificates.cert1=Cert.generate("Test 1 <test-1@example.com>")cert2=Cert.generate("Test 2 <test-2@example.com>")cert3=Cert.generate("Test 3 <test-3@example.com>")buffer=cert1.bytes()+cert2.bytes()+cert3.bytes()certs=Cert.split_bytes(buffer)assertlen(certs)==3generateCreates a new general purpose key with a given User ID:alice=Cert.generate("Alice <alice@example.com>")fpr=alice.fingerprintprint(f"Generated cert with fingerprint{fpr}:\n{alice}")Multiple User IDs can be passed as a list to thegeneratefunction:cert=Cert.generate(user_ids=["First","Second","Third"])assertlen(cert.user_ids)==3Newly generated certificates are usable in both encryption and signing
contexts:alice=Cert.generate("Alice <alice@example.com>")bob=Cert.generate("Bob <bob@example.com>")bytes="content to encrypt".encode("utf8")encrypted=encrypt(signer=alice.secrets.signer(),recipients=[bob],bytes=bytes)print(f"Encrypted data:{encrypted}")mergeMerges packets from a new version into an old version of a certificate:old=Cert.from_file("wiktor.asc")new=Cert.from_file("wiktor-fresh.asc")merged=old.merge(new)print(f"Merged, updated cert:{merged}")User IDsListing existing User IDs:cert=Cert.from_file("wiktor.asc")user_id=cert.user_ids[0]assertstr(user_id).startswith("Wiktor Kwapisiewicz")Adding new User IDs:cert=Cert.generate("Alice <alice@example.com>")assertlen(cert.user_ids)==1;cert=cert.add_user_id(value="Alice <alice@company.invalid>",certifier=cert.secrets.certifier())assertlen(cert.user_ids)==2;Revoking User IDs:cert=Cert.generate("Bob <bob@example.com>")cert=cert.add_user_id(value="Bob <bob@company.invalid>",certifier=cert.secrets.certifier())assertlen(cert.user_ids)==2# create User ID revocationrevocation=cert.revoke_user_id(user_id=cert.user_ids[1],certifier=cert.secrets.certifier())# merge the revocation with the certcert=Cert.from_bytes(cert.bytes()+revocation.bytes())assertlen(cert.user_ids)==1NotationsNotations are small pieces of data that can be attached to signatures (and, indirectly, to User IDs).The following example reads and displays aKeyoxideproof URI:cert=Cert.from_file("wiktor.asc")user_id=cert.user_ids[0]notation=user_id.notations[0]assertnotation.key=="proof@metacode.biz";assertnotation.value=="dns:metacode.biz?type=TXT";Notations can also be added:frompysequoiaimportNotationcert=Cert.from_file("signing-key.asc")# No notations initiallyassertlen(cert.user_ids[0].notations)==0;cert=cert.set_notations(cert.secrets.certifier(),[Notation("proof@metacode.biz","dns:metacode.biz")])# Has one notation nowprint(str(cert.user_ids[0].notations))assertlen(cert.user_ids[0].notations)==1;# Check the notation datanotation=cert.user_ids[0].notations[0]assertnotation.key=="proof@metacode.biz";assertnotation.value=="dns:metacode.biz";Key expirationCerts have anexpirationgetter for retrieving the current key
expiry time:cert=Cert.from_file("signing-key.asc")# Cert does not have any expiration date:assertcert.expirationisNonecert=Cert.from_file("wiktor.asc")# Cert expires on New Year's Eveassertstr(cert.expiration)=="2022-12-31 12:00:02+00:00"Key expiration can also be adjusted withset_expiration:fromdatetimeimportdatetimecert=Cert.from_file("signing-key.asc")# Cert does not have any expiration date:assertcert.expirationisNone# Set the expiration to some specified point in timeexpiration=datetime.fromisoformat("2021-11-04T00:05:23+00:00")cert=cert.set_expiration(expiration=expiration,certifier=cert.secrets.certifier())assertstr(cert.expiration)=="2021-11-04 00:05:23+00:00"Key revocationCerts can be revoked. Whileexpiration makes the key unusable
temporarilyto encourage the user to refresh a copy revocation is
irreversible.cert=Cert.generate("Test Revocation <revoke@example.com>")revocation=cert.revoke(certifier=cert.secrets.certifier())# creating revocation signature does not revoke the keyassertnotcert.is_revoked# importing revocation signature marks the key as revokedrevoked_cert=Cert.from_bytes(cert.bytes()+revocation.bytes())assertrevoked_cert.is_revokedSecret keysCertificates generated throughCert.generate()contain secret keys
and can be used for signing and decryption.To avoid accidental leakage secret keys are never directly printed
when the Cert is written to a string. To enable this behavior useCert.secrets.secretsreturnsNoneon certificates which do
not contain any secret key material ("public keys").c=Cert.generate("Testing key <test@example.com>")assertc.has_secret_keys# by default only public parts are exportedpublic_parts=Cert.from_bytes(f"{c}".encode("utf8"))assertnotpublic_parts.has_secret_keysassertpublic_parts.secretsisNone# to export secret parts use the following:private_parts=Cert.from_bytes(f"{c.secrets}".encode("utf8"))assertprivate_parts.has_secret_keysCertificate managementCertD integrationThis library exposesOpenPGP Certificate Directoryintegration, which allows storing and retrieving OpenPGP certificates
in a persistent way directly in the file system.Note that this willnotallow you to read GnuPG-specific key
directories. Cert-Ddoes not allow certificate removal.frompysequoiaimportStorecert=Cert.from_file("wiktor.asc")s=Store("/tmp/store")s.put(cert)asserts.get(cert.fingerprint)!=NoneThe certificate is now stored in the given directory and can be
retrieved later by its fingerprint:s=Store("/tmp/store")asserts.get("653909a2f0e37c106f5faf546c8857e0d8e8f074")!=NoneOpenPGP CardsThere's an experimental feature allowing communication with OpenPGP
Cards (like YubiKey or Nitrokey).frompysequoiaimportCard# enumerate all cardsall=Card.all()# open card by card identcard=Card.open("0000:00000000")print(f"Card ident:{card.ident}")assertcard.cardholder=="John Doe"assertcard.cert_url=="https://example.com/key.pgp"Cards providekeysproperty that can be used to see which keys are imported
on the card:keys=card.keysprint(f"Keys:{keys}")assertlen(keys)==3assertkeys[0].fingerprint=="ddc3e03c91fb52ca2d95c2444566f2743ed5f382"assert"sign"inkeys[0].usageassertkeys[0].touch_requiredassertkeys[1].fingerprint=="689e152a7420be13dcaf2c142ac27adc1db9395e"assert"decrypt"inkeys[1].usageassertnotkeys[1].touch_requiredassertkeys[2].fingerprint=="731fbca93ce9821347bf8e696444723371d3c650"assert"authenticate"inkeys[2].usageassertkeys[2].touch_requiredCards can be used for signing data:signer=card.signer("123456")signed=sign(signer,"data to be signed".encode("utf8"))print(f"Signed data:{signed}")As well as for decryption:decryptor=card.decryptor("123456")sender=Cert.from_file("passwd.pgp")receiver=Cert.from_file("full-key.asc")content="Red Green Blue"encrypted=encrypt(signer=sender.secrets.signer("hunter22"),recipients=[receiver],bytes=content.encode("utf8"))print(f"Encrypted data:{encrypted}")decrypted=decrypt(decryptor=decryptor,bytes=encrypted)assertcontent==decrypted.bytes.decode("utf8");Note that while this package allows using cards for signing and
decryption, the provisioning process is not supported.OpenPGP card
toolscan be used to initialize the card.LicenseThis project is licensed underApache License, Version 2.0.ContributionUnless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the package by you shall be under the terms
and conditions of this license, without any additional terms or
conditions.SponsorsMy work was supported by these generous organizations (alphabetical
order):nlnet.nlpep.foundationsovereigntechfund.de