wecanencrypt 0.9.0

Simple Rust OpenPGP library for encryption, signing, and key management.
Documentation

wecanencrypt

Simple Rust OpenPGP library for encryption, signing, and key management, built on top of rpgp (pgp v0.19).

Features

  • Key Generation: Create OpenPGP keys with multiple cipher suites (Cv25519, Cv25519Modern, Cv448Modern, NIST P-256/P-384/P-521, RSA-2048/4096)
  • Encryption/Decryption: Encrypt to one or multiple recipients (SEIPD v1 default, SEIPD v2 AEAD available), reject legacy SED by default
  • Signing/Verification: Inline, cleartext, and detached signatures with subkey preference; verification filters to signing-capable subkeys only
  • Certificate Merging: Proper packet-level merge with signature deduplication, following the rpgpie/rsop algorithm
  • Key Management: Update expiration (primary and per-subkey), add/revoke UIDs, revoke keys, change passwords, third-party certification (4 levels)
  • Key Parsing: Parse certificates, extract subkey info, query available subkeys by capability
  • Keyring Support: Parse and export GPG keyrings with multiple certificates
  • SSH Key Export: Convert OpenPGP authentication keys to SSH public key format (Ed25519, RSA, ECDSA)
  • Network Operations: Fetch keys via WKD (Web Key Directory) and VKS keyservers
  • DNS DANE: Fetch keys via OPENPGPKEY DNS records (RFC 7929)
  • KeyStore: SQLite-backed key storage with search, card-key associations, and management capabilities
  • Smart Card Support: Upload keys to YubiKey/OpenPGP cards, sign and decrypt on-card, discover which cards hold keys for a certificate, configure touch policies

Cipher Suites

Suite Primary Key Encryption Subkey Speed
Cv25519 (default) EdDSA Legacy ECDH Curve25519 Fast
Cv25519Modern Ed25519 (RFC 9580) X25519 Fast
Cv448Modern Ed448 (RFC 9580) X448 Fast
NistP256 ECDSA P-256 ECDH P-256 Fast
NistP384 ECDSA P-384 ECDH P-384 Fast
NistP521 ECDSA P-521 ECDH P-521 Fast
Rsa2k RSA 2048-bit RSA 2048-bit Slow
Rsa4k RSA 4096-bit RSA 4096-bit Very Slow

Usage

use wecanencrypt::{create_key_simple, encrypt_bytes, decrypt_bytes, get_pub_key};

// Generate a new key
let key = create_key_simple("passphrase", &["Alice <alice@example.com>"])?;
let public_key = get_pub_key(&key.secret_key)?;

// Encrypt data
let plaintext = b"Hello, World!";
let encrypted = encrypt_bytes(public_key.as_bytes(), plaintext, true)?;

// Decrypt data
let decrypted = decrypt_bytes(&key.secret_key, &encrypted, "passphrase")?;
assert_eq!(decrypted, plaintext);

SEIPD v2 (AEAD) Encryption

The default encrypt_bytes uses SEIPD v1 (RFC 4880, integrity-protected with MDC). For AEAD-based encryption with V6 key support, use the v2 variants:

use wecanencrypt::encrypt_bytes_v2;

let encrypted = encrypt_bytes_v2(public_key.as_bytes(), plaintext, true)?;

Certificate Merging

Merge updated certificates (e.g., after fetching from a keyserver) with proper signature deduplication:

use wecanencrypt::merge_keys;

let merged = merge_keys(&original_cert, &updated_cert, false)?;

The merge handles direct key signatures, revocation signatures, subkeys, user IDs, user attributes, and their associated signatures. Deduplication is based on cryptographic signature bytes, with unhashed subpacket merging for signatures that differ only in advisory data.

Smart Card Usage

use wecanencrypt::card::{
    is_card_connected, get_card_details, find_cards_for_key,
    upload_key_to_card, set_touch_mode, KeySlot, TouchMode, CardKeySlot,
};

// Check for connected card
if is_card_connected() {
    let info = get_card_details(None)?;
    println!("Card serial: {}", info.serial_number);

    // Upload a key to the signing slot
    let secret_key = std::fs::read("secret.asc")?;
    upload_key_to_card(&secret_key, b"password", CardKeySlot::Signing, b"12345678")?;

    // Find which cards hold keys for a certificate
    let public_key = std::fs::read("pubkey.asc")?;
    let matches = find_cards_for_key(&public_key)?;
    for m in &matches {
        println!("Card {} has {} matching slots", m.card.ident, m.matching_slots.len());
    }

    // Configure touch policy (YubiKey 4.2+)
    // Warning: TouchMode::Fixed is permanent and cannot be changed!
    set_touch_mode(KeySlot::Signature, TouchMode::Fixed, b"12345678", None)?;
    set_touch_mode(KeySlot::Encryption, TouchMode::Fixed, b"12345678", None)?;
    set_touch_mode(KeySlot::Authentication, TouchMode::On, b"12345678", None)?;
}

DNS DANE Key Discovery

Fetch OpenPGP keys published in DNS via OPENPGPKEY records (RFC 7929):

use wecanencrypt::fetch_key_by_email_from_dane;

// Uses the system DNS resolver by default
let cert = fetch_key_by_email_from_dane("user@example.com", None)?;

// Or specify a resolver explicitly
let cert = fetch_key_by_email_from_dane("user@example.com", Some("8.8.8.8:53"))?;

Security Policy

The library enforces RFC 4880 section 5.2.3.3: when multiple self-signatures exist on a component (User ID or subkey), only the most recent one (by creation timestamp) is authoritative for key flags, expiration, and preferences. This matters after certificate merges where old and new self-signatures coexist.

See docs/adr/0001-rfc4880-latest-self-signature-wins.md for details.

Other policy decisions:

  • Symmetric algorithm allowlist per RFC 9580 section 9.3 (AES, Twofish, Camellia only)
  • Legacy SED packets (no integrity protection) rejected by default; opt-in via decrypt_bytes_legacy
  • Revoked keys rejected for signing and verification; expired keys can still verify old signatures
  • Secret key material wrapped in Zeroizing<Vec<u8>> for secure memory erasure

Running Tests

Run all tests in the tests/ directory

cargo test --features card --test '*'

Or run specific test files:

# Individual test files
cargo test --features card --test integration_tests
cargo test --features card --test keystore_tests
cargo test --features card --test fixture_tests

Or combine them:

cargo test --features card --test integration_tests --test keystore_tests --test fixture_tests

Smart Card Tests

Smart card tests require a physical YubiKey or compatible OpenPGP smart card. These tests are ignored by default:

cargo test --features card --test card_tests -- --ignored --test-threads=1

Note: Card tests automatically reset the card to factory defaults before each test.

Optional Features

  • keystore (default): SQLite-backed key storage with card-key associations
  • network (default): WKD and VKS key fetching
  • card: Smart card support (requires libpcsclite-dev on Linux)
  • dane: DNS DANE OPENPGPKEY key discovery (RFC 7929)
  • draft-pqc: Post-quantum cryptography support (experimental)

Release

GitHub publishing is wired through the tag-driven workflow in .github/workflows/publish.yml.

Before tagging a release, run:

cargo fmt --check
cargo clippy --all-targets --features card -- -D warnings
cargo test --features card --test '*'
cargo test --features dane --lib
cargo publish --dry-run

Then:

  1. Update version in Cargo.toml.
  2. Push the version commit.
  3. Create and push a tag (e.g. v0.9.0).

The publish workflow verifies that the tag matches the crate version, runs cargo publish --dry-run, and then publishes via crates.io trusted publishing. The repository must be configured as a trusted publisher in crates.io before the first tagged release, with the GitHub Actions environment set to release to match .github/workflows/publish.yml.

The docs.rs build is configured to document keystore, network, card, and dane, while leaving draft-pqc out of the default published docs because it is experimental.

License

MIT