OpenPGP
OpenPGP through the pure-Rust pgp crate: keys made and read, messages encrypted to keys or a password and decrypted, cleartext and detached signatures made and verified, signed and encrypted together, and what an armored block holds.
Version 1.0.0 · 14 functions · 3 crates
Functions
What use openpgp.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use openpgp_generate, openpgp_key_info from openpgp (Quillo 0.6).
openpgp_generate(user_id: str, passphrase: str) → anyor an errorMakes a key for user_id ("Ada ada@example.com"): Ed25519 to sign and Curve25519 to decrypt, which every current OpenPGP program reads. passphrase empty leaves the private key unprotected. Gives { public, private, fingerprint, key_id }, armored; keep private with care.
openpgp_key_info(key: str) → anyor an errorWhat a key is, public or private: { private, fingerprint, key_id, version, algorithm, created, users, subkeys }, the key's own signatures checked. created is seconds since 1970.
openpgp_public_of(private_key: str) → stror an errorThe public half of a private key, armored, to hand out.
openpgp_encrypt(message: str, public_keys: list) → stror an errorEncrypts text to one or more public keys, the first of each that can decrypt: an armored message, in SEIPD v1 (AES-256) for v4 keys, which GnuPG reads, and in v2 for v6 keys.
openpgp_encrypt_signed(message: str, public_key: str, private_key: str, passphrase: str) → stror an errorEncrypts and signs: the message opens only with the receiver's key, and proves who sent it. passphrase is the signer's.
openpgp_encrypt_password(message: str, password: str) → stror an errorEncrypts text with a password alone, for a receiver with no key: an armored message any OpenPGP program opens with the password.
openpgp_decrypt(armored: str, private_key: str, passphrase: str) → stror an errorOpens a message with a private key: its text. The wrong key or passphrase, or a message changed on the way, is an error, as the integrity check is part of decrypting.
openpgp_decrypt_password(armored: str, password: str) → stror an errorOpens a message made with a password.
openpgp_decrypt_verify(armored: str, private_key: str, passphrase: str, sender_public_key: str) → anyor an errorOpens a signed and encrypted message, and checks the signature against the sender's public key: { text, signer }. A message from someone else is an error, the text kept from the caller.
openpgp_sign(message: str, private_key: str, passphrase: str) → stror an errorA cleartext signed message: readable as it stands, signature below it.
openpgp_verify(signed: str, public_key: str) → anyor an errorChecks a cleartext signed message against a public key: { text, signer }, signer being the fingerprint of the key or subkey that signed. A changed text or another key is an error.
openpgp_sign_detached(data: str, private_key: str, passphrase: str) → stror an errorA detached signature over text or a file's bytes, armored: the data stays as it is.
openpgp_verify_detached(data: str, signature: str, public_key: str) → stror an errorChecks a detached signature: the signer's fingerprint, or an error.
openpgp_inspect(armored: str) → anyor an errorWhat an armored block is: { type, headers }, for telling what someone has sent. A bad armor checksum or a broken packet is an error.
Get it
$ quillo install openpgp # openpgp.qwer into this folder$ quillo install openpgp --example # and its example, openpgp.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/openpgp.qwer$ curl -O https://quillolang.org/plugins/files/openpgp.qwe$ curl -O https://quillolang.org/plugins/files/openpgp_rfc9580_cert.asc$ curl -O https://quillolang.org/plugins/files/openpgp_rfc9580_signed.ascThen, in your program, all of it or only what you call:
use openpgp.qweruse openpgp_generate, openpgp_key_info from openpgp // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }pgp = { version = "=0.21.0", default-features = false }rand = "0.8"Example
// OpenPGP mail and files: a key pair made, a message encrypted to it, text signed in the clear, a// detached signature, and a signature from the OpenPGP specification itself checked.//// quillo run openpgp.qwe then open http://localhost:8080/key, /secret, /signed, /detached, /sealed, /password, /standard and /refused//// openpgp_rfc9580_cert.asc and openpgp_rfc9580_signed.asc are RFC 9580's sample certificate and a// message signed with it, from another implementation's hand. The keys here live as long as the// program runs: a real one keeps `private` somewhere safe and shares `public`.use "openpgp.qwer"let ADA = openpgp_generate("Ada Lovelace <ada@example.com>", "correct horse")let BOB = openpgp_generate("Bob <bob@example.com>", "")route GET /key { let info = openpgp_key_info(ADA?.public)? { users: info.users, version: info.version, algorithm: info.algorithm, private: info.private, subkeys: info.subkeys | map((s) => { algorithm: s.algorithm, can_encrypt: s.can_encrypt }), same_print: info.fingerprint == ADA?.fingerprint, id_len: len(ADA?.key_id), private_info: openpgp_key_info(ADA?.private)?.private, derived: openpgp_key_info(openpgp_public_of(ADA?.private)?)?.fingerprint == ADA?.fingerprint, }}route GET /secret { let box = openpgp_encrypt("Meet at dawn. Ünïcödé too.", [ADA?.public, BOB?.public])? { armored: slice(box, 0, 27), kind: openpgp_inspect(box)?.type, ada: openpgp_decrypt(box, ADA?.private, "correct horse")?, bob: openpgp_decrypt(box, BOB?.private, "")?, wrong_pass: openpgp_decrypt(box, ADA?.private, "battery staple").error != null, }}route GET /signed { let s = openpgp_sign("I owe Bob 10 euros.\nSigned today.", ADA?.private, "correct horse")? { kind: openpgp_inspect(s)?.type, good: openpgp_verify(s, ADA?.public)?.text, signer_ok: openpgp_verify(s, ADA?.public)?.signer == ADA?.fingerprint, changed: openpgp_verify(replace(s, "10 euros", "99 euros"), ADA?.public).error, other_key: openpgp_verify(s, BOB?.public).error, }}route GET /detached { let sig = openpgp_sign_detached("release-1.2.3.tar.gz contents", BOB?.private, "")? { kind: openpgp_inspect(sig)?.type, good: openpgp_verify_detached("release-1.2.3.tar.gz contents", sig, BOB?.public)? == BOB?.fingerprint, changed: openpgp_verify_detached("release-1.2.4.tar.gz contents", sig, BOB?.public).error, }}route GET /sealed { let box = openpgp_encrypt_signed("Only you, and from me.", BOB?.public, ADA?.private, "correct horse")? let read = openpgp_decrypt_verify(box, BOB?.private, "", ADA?.public)? { text: read.text, from_ada: read.signer == ADA?.fingerprint, wrong_sender: openpgp_decrypt_verify(box, BOB?.private, "", BOB?.public).error, }}route GET /password { let box = openpgp_encrypt_password("the safe's code", "swordfish")? { open: openpgp_decrypt_password(box, "swordfish")?, wrong: openpgp_decrypt_password(box, "herring").error != null, empty: openpgp_encrypt_password("x", "").error, }}// From RFC 9580, Appendix A: a signature this plugin did not make, on a version 6 key.route GET /standard { let cert = read_file("openpgp_rfc9580_cert.asc") let signed = read_file("openpgp_rfc9580_signed.asc") let info = openpgp_key_info(cert)? { fingerprint: info.fingerprint, version: info.version, algorithm: info.algorithm, text: openpgp_verify(signed, cert)?.text, tampered: openpgp_verify(replace(signed, "hello world", "hello there"), cert).error, to_it: len(openpgp_encrypt("to a version 6 key", [cert])?) > 100, }}route GET /refused = { not_key: openpgp_key_info("hello").error != null, not_message: openpgp_decrypt("hello", BOB?.private, "").error != null, no_user: openpgp_generate(" ", "").error, no_keys: openpgp_encrypt("x", []).error,}$ quillo run openpgp.qwe$ curl 'localhost:8080/key'$ curl 'localhost:8080/secret'$ curl 'localhost:8080/signed'$ curl 'localhost:8080/detached'$ curl 'localhost:8080/sealed'$ curl 'localhost:8080/password'$ curl 'localhost:8080/standard'$ curl 'localhost:8080/refused'Source
//! OpenPGP through the pure-Rust `pgp` crate: keys made and read, messages encrypted, decrypted, signed and verified, detached signatures, and armor.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }//! pgp = { version = "=0.21.0", default-features = false }//! rand = "0.8"use pgp::composed::{ArmorOptions, CleartextSignedMessage, Deserializable, DetachedSignature, EncryptionCaps, KeyType, Message, MessageBuilder, SecretKeyParamsBuilder, SignedPublicKey, SignedSecretKey, SubkeyParamsBuilder};use pgp::crypto::ecc_curve::ECCCurve;use pgp::crypto::hash::HashAlgorithm;use pgp::crypto::sym::SymmetricKeyAlgorithm;use pgp::types::{KeyDetails, KeyVersion, Password};use rand::thread_rng;use serde_json::{json, Value};fn e<T: std::fmt::Display>(what: &str) -> impl Fn(T) -> String + '_ { move |x| format!("{}: {}", what, x)}fn read_public(key: &str) -> Result<SignedPublicKey, String> { let (k, _) = SignedPublicKey::from_string(key).map_err(e("not an OpenPGP public key"))?; k.verify_bindings().map_err(e("the key's own signatures do not hold"))?; Ok(k)}fn read_secret(key: &str) -> Result<SignedSecretKey, String> { let (k, _) = SignedSecretKey::from_string(key).map_err(e("not an OpenPGP private key"))?; k.verify_bindings().map_err(e("the key's own signatures do not hold"))?; Ok(k)}fn hex(x: impl std::fmt::UpperHex) -> String { format!("{:X}", x)}fn algorithm(k: &impl KeyDetails) -> String { format!("{:?}", k.algorithm())}fn sub_info(k: &impl KeyDetails) -> Value { json!({ "fingerprint": hex(k.fingerprint()), "algorithm": algorithm(k), "created": k.created_at().as_secs(), "can_encrypt": k.algorithm().can_encrypt(), "can_sign": k.algorithm().can_sign(), })}/// Makes a key for `user_id` ("Ada <ada@example.com>"): Ed25519 to sign and Curve25519 to decrypt, which every current OpenPGP program reads. `passphrase` empty leaves the private key unprotected. Gives `{ public, private, fingerprint, key_id }`, armored; keep `private` with care.pub fn openpgp_generate(user_id: &str, passphrase: &str) -> Result<Value, String> { if user_id.trim().is_empty() { return Err("a key needs a user id".into()); } let pass = (!passphrase.is_empty()).then(|| passphrase.to_string()); let mut sub = SubkeyParamsBuilder::default(); sub.key_type(KeyType::ECDH(ECCCurve::Curve25519Legacy)).can_sign(false).can_encrypt(EncryptionCaps::All).can_authenticate(false).passphrase(pass.clone()); let mut p = SecretKeyParamsBuilder::default(); p.key_type(KeyType::Ed25519Legacy).can_certify(true).can_sign(true).can_encrypt(EncryptionCaps::None).primary_user_id(user_id.into()).passphrase(pass).subkeys(vec![sub.build().map_err(e("the subkey"))?]); let k = p.build().map_err(e("the key"))?.generate(thread_rng()).map_err(e("making the key"))?; let public = SignedPublicKey::from(k.clone()); Ok(json!({ "public": public.to_armored_string(ArmorOptions::default()).map_err(e("armor"))?, "private": k.to_armored_string(ArmorOptions::default()).map_err(e("armor"))?, "fingerprint": hex(k.fingerprint()), "key_id": k.legacy_key_id().to_string().to_uppercase(), }))}/// What a key is, public or private: `{ private, fingerprint, key_id, version, algorithm, created, users, subkeys }`, the key's own signatures checked. `created` is seconds since 1970.pub fn openpgp_key_info(key: &str) -> Result<Value, String> { let private = key.contains("PRIVATE KEY BLOCK"); let public = if private { SignedPublicKey::from(read_secret(key)?) } else { read_public(key)? }; let users: Vec<String> = public.details.users.iter().filter_map(|u| u.id.as_str().map(str::to_string)).collect(); let subkeys: Vec<Value> = public.public_subkeys.iter().map(|s| sub_info(&s.key)).collect(); let version = match public.version() { KeyVersion::V4 => 4, KeyVersion::V6 => 6, _ => 0 }; Ok(json!({ "private": private, "fingerprint": hex(public.fingerprint()), "key_id": public.legacy_key_id().to_string().to_uppercase(), "version": version, "algorithm": algorithm(&public), "created": public.created_at().as_secs(), "users": users, "subkeys": subkeys, }))}/// The public half of a private key, armored, to hand out.pub fn openpgp_public_of(private_key: &str) -> Result<String, String> { SignedPublicKey::from(read_secret(private_key)?).to_armored_string(ArmorOptions::default()).map_err(e("armor"))}/// Encrypts text to one or more public keys, the first of each that can decrypt: an armored message, in SEIPD v1 (AES-256) for v4 keys, which GnuPG reads, and in v2 for v6 keys.pub fn openpgp_encrypt(message: &str, public_keys: Vec<String>) -> Result<String, String> { if public_keys.is_empty() { return Err("no public keys".into()); } let keys: Vec<SignedPublicKey> = public_keys.iter().map(|k| read_public(k)).collect::<Result<_, _>>()?; let targets = keys.iter().map(|k| k.public_subkeys.iter().find(|s| s.key.algorithm().can_encrypt()).ok_or(format!("key {} has no encryption subkey", hex(k.fingerprint())))).collect::<Result<Vec<_>, _>>()?; let mut rng = thread_rng(); if targets.iter().all(|t| t.key.version() == KeyVersion::V6) { let mut b = MessageBuilder::from_bytes("", message.as_bytes().to_vec()).seipd_v2(&mut rng, SymmetricKeyAlgorithm::AES256, pgp::crypto::aead::AeadAlgorithm::Ocb, pgp::crypto::aead::ChunkSize::default()); for t in &targets { b.encrypt_to_key(&mut rng, &t.key).map_err(e("encrypting"))?; } return b.to_armored_string(&mut rng, ArmorOptions::default()).map_err(e("encrypting")); } let mut b = MessageBuilder::from_bytes("", message.as_bytes().to_vec()).seipd_v1(&mut rng, SymmetricKeyAlgorithm::AES256); for t in &targets { b.encrypt_to_key(&mut rng, &t.key).map_err(e("encrypting"))?; } b.to_armored_string(&mut rng, ArmorOptions::default()).map_err(e("encrypting"))}/// Encrypts and signs: the message opens only with the receiver's key, and proves who sent it. `passphrase` is the signer's.pub fn openpgp_encrypt_signed(message: &str, public_key: &str, private_key: &str, passphrase: &str) -> Result<String, String> { let key = read_public(public_key)?; let signer = read_secret(private_key)?; let target = key.public_subkeys.iter().find(|s| s.key.algorithm().can_encrypt()).ok_or("the key has no encryption subkey")?; let mut rng = thread_rng(); let mut b = MessageBuilder::from_bytes("", message.as_bytes().to_vec()).seipd_v1(&mut rng, SymmetricKeyAlgorithm::AES256); b.sign(&signer.primary_key, Password::from(passphrase), HashAlgorithm::Sha256); b.encrypt_to_key(&mut rng, &target.key).map_err(e("encrypting"))?; b.to_armored_string(&mut rng, ArmorOptions::default()).map_err(e("encrypting"))}/// Encrypts text with a password alone, for a receiver with no key: an armored message any OpenPGP program opens with the password.pub fn openpgp_encrypt_password(message: &str, password: &str) -> Result<String, String> { if password.is_empty() { return Err("the password is empty".into()); } let mut rng = thread_rng(); let mut b = MessageBuilder::from_bytes("", message.as_bytes().to_vec()).seipd_v1(&mut rng, SymmetricKeyAlgorithm::AES256); b.encrypt_with_password(pgp::types::StringToKey::new_default(&mut rng), &Password::from(password)).map_err(e("encrypting"))?; b.to_armored_string(&mut rng, ArmorOptions::default()).map_err(e("encrypting"))}fn opened(mut m: Message<'_>) -> Result<Message<'_>, String> { if m.is_compressed() { m = m.decompress().map_err(e("decompressing"))?; } Ok(m)}fn text_of(m: &mut Message<'_>) -> Result<String, String> { let data = m.as_data_vec().map_err(e("reading the message"))?; String::from_utf8(data).map_err(|_| "the message is not text".to_string())}/// Opens a message with a private key: its text. The wrong key or passphrase, or a message changed on the way, is an error, as the integrity check is part of decrypting.pub fn openpgp_decrypt(armored: &str, private_key: &str, passphrase: &str) -> Result<String, String> { let key = read_secret(private_key)?; let (m, _) = Message::from_string(armored).map_err(e("not an OpenPGP message"))?; let m = m.decrypt(&Password::from(passphrase), &key).map_err(e("cannot decrypt"))?; let mut m = opened(m)?; text_of(&mut m)}/// Opens a message made with a password.pub fn openpgp_decrypt_password(armored: &str, password: &str) -> Result<String, String> { let (m, _) = Message::from_string(armored).map_err(e("not an OpenPGP message"))?; let m = m.decrypt_with_password(&Password::from(password)).map_err(e("cannot decrypt"))?; let mut m = opened(m)?; text_of(&mut m)}/// Opens a signed and encrypted message, and checks the signature against the sender's public key: `{ text, signer }`. A message from someone else is an error, the text kept from the caller.pub fn openpgp_decrypt_verify(armored: &str, private_key: &str, passphrase: &str, sender_public_key: &str) -> Result<Value, String> { let key = read_secret(private_key)?; let sender = read_public(sender_public_key)?; let (m, _) = Message::from_string(armored).map_err(e("not an OpenPGP message"))?; let m = m.decrypt(&Password::from(passphrase), &key).map_err(e("cannot decrypt"))?; let mut m = opened(m)?; let text = text_of(&mut m)?; if !m.is_signed() { return Err("the message is not signed".into()); } if m.verify(&sender.primary_key).is_err() && !sender.public_subkeys.iter().any(|s| m.verify(&s.key).is_ok()) { return Err("the signature is not the sender's".into()); } Ok(json!({ "text": text, "signer": hex(sender.fingerprint()) }))}/// A cleartext signed message: readable as it stands, signature below it.pub fn openpgp_sign(message: &str, private_key: &str, passphrase: &str) -> Result<String, String> { let key = read_secret(private_key)?; let m = CleartextSignedMessage::sign(thread_rng(), message, &key.primary_key, &Password::from(passphrase)).map_err(e("cannot sign"))?; m.to_armored_string(ArmorOptions::default()).map_err(e("armor"))}/// Checks a cleartext signed message against a public key: `{ text, signer }`, `signer` being the fingerprint of the key or subkey that signed. A changed text or another key is an error.pub fn openpgp_verify(signed: &str, public_key: &str) -> Result<Value, String> { let key = read_public(public_key)?; let (m, _) = CleartextSignedMessage::from_string(signed).map_err(e("not a cleartext signed message"))?; let signer = if m.verify(&key.primary_key).is_ok() { hex(key.fingerprint()) } else { key.public_subkeys.iter().find(|s| m.verify(&s.key).is_ok()).map(|s| hex(s.key.fingerprint())).ok_or("the signature does not match this key and text")? }; Ok(json!({ "text": m.text(), "signer": signer }))}/// A detached signature over text or a file's bytes, armored: the data stays as it is.pub fn openpgp_sign_detached(data: &str, private_key: &str, passphrase: &str) -> Result<String, String> { let key = read_secret(private_key)?; let s = DetachedSignature::sign_binary_data(thread_rng(), &key.primary_key, &Password::from(passphrase), HashAlgorithm::Sha256, data.as_bytes()).map_err(e("cannot sign"))?; s.to_armored_string(ArmorOptions::default()).map_err(e("armor"))}/// Checks a detached signature: the signer's fingerprint, or an error.pub fn openpgp_verify_detached(data: &str, signature: &str, public_key: &str) -> Result<String, String> { let key = read_public(public_key)?; let (s, _) = DetachedSignature::from_string(signature).map_err(e("not a detached signature"))?; if s.verify(&key.primary_key, data.as_bytes()).is_ok() { return Ok(hex(key.fingerprint())); } key.public_subkeys.iter().find(|x| s.verify(&x.key, data.as_bytes()).is_ok()).map(|x| hex(x.key.fingerprint())).ok_or("the signature does not match this key and data".into())}/// What an armored block is: `{ type, headers }`, for telling what someone has sent. A bad armor checksum or a broken packet is an error.pub fn openpgp_inspect(armored: &str) -> Result<Value, String> { let (m, h) = pgp::composed::Any::from_string(armored).map_err(e("not OpenPGP armor"))?; let kind = match &m { pgp::composed::Any::Cleartext(_) => "cleartext signed message", pgp::composed::Any::PublicKey(_) => "public key", pgp::composed::Any::SecretKey(_) => "private key", pgp::composed::Any::Message(_) => "message", pgp::composed::Any::Signature(_) => "signature", }; let headers: serde_json::Map<String, Value> = h.iter().map(|(k, v)| (k.clone(), json!(v))).collect(); Ok(json!({ "type": kind, "headers": headers }))}Related
email_auth.qwer · 1.0.0Messages signed and checked with DKIM (RSA and Ed25519), SPF records read and evaluated for a sending IP, and DMARC policies applied, with DNS records given as a map so it runs offline.6 views · 5 downloadsRSA rsa.qwer · 1.0.0RSA key pairs as PEM, PKCS#1 v1.5 SHA-256 signatures for partner and bank APIs, and OAEP encryption of short secrets.9 views · 8 downloadsage age.qwer · 1.0.0age encryption: text and files encrypted to people's public keys and opened only with their identity, compatible with the age command.7 views · 8 downloadsBiscuit tokens biscuit.qwer · 1.0.0Biscuit authorization tokens: issued with Datalog facts and checks signed by your key, narrowed by their holder without asking the issuer, and authorized offline against each request.7 views · 8 downloadsCrypto box crypto_box.qwer · 1.0.0Public-key encryption between users (libsodium's crypto_box): only the recipient reads it and knows the sender; and anonymous sealed boxes.7 views · 8 downloadsDMARC reports dmarc_report.qwer · 1.0.0Read DMARC aggregate reports from their XML or their gzip and zip attachments, with a limit on what they unpack to, into records and summaries by source and alignment that show who sends as your domain and what fails.2 views · 6 downloadsThread 0 posts
On OpenPGP: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.