Project

pq_crypto

0.0
The project is in a healthy, maintained state
Native Ruby wrapper around ML-KEM-768, ML-DSA-65, and an optional hybrid ML-KEM-768+X25519 KEM, backed by PQClean and OpenSSL.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0
 Project Readme

pq_crypto

pq_crypto is a primitive-first Ruby gem for post-quantum cryptography.

It currently exposes three public building blocks:

  • PQCrypto::KEM — pure ML-KEM-768
  • PQCrypto::SignatureML-DSA-65
  • PQCrypto::HybridKEM — an optional custom hybrid KEM that combines ML-KEM-768 and X25519 with transcript-bound HKDF-SHA256

The gem is backed by vendored PQClean sources for ML-KEM-768 / ML-DSA-65 and OpenSSL for conventional primitives such as X25519 and HKDF-SHA256.

Status

  • first public release
  • primitive-first API only
  • no protocol/session helpers in the public surface
  • serialization uses pq_crypto-specific pqc_container_* wrappers
  • not audited
  • not yet positioned as production-ready

Installation

Add the gem to your project and compile the extension:

# Gemfile
gem "pq_crypto"
bundle install
bundle exec rake compile

Native dependencies

  • Ruby 3.4.x
  • a C toolchain
  • OpenSSL 3.0 or later

Async / Fiber scheduler support

pq_crypto does not require any gem-specific Async configuration. On Ruby 3.4, sign and verify use Ruby's scheduler-aware rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE) path automatically.

That means:

  • without a Fiber scheduler, these methods fall back to the ordinary no-GVL behavior;
  • with a scheduler that implements blocking_operation_wait (for example Async with a worker pool), the blocking native work can be moved off the event loop.

This integration is intentionally limited to sign and verify; the faster primitive operations keep the lower-overhead path.

Example with Async:

require "async"
require "pq_crypto"

keypair = PQCrypto::Signature.generate(:ml_dsa_65)
message = "hello" * 100_000

reactor = Async::Reactor.new(worker_pool: true)
root = reactor.async do |task|
  task.async do
    signature = keypair.secret_key.sign(message)
    keypair.public_key.verify(message, signature)
  end

  task.async do
    sleep 0.01
    puts "event loop stayed responsive"
  end
end

reactor.run
root.wait
reactor.close

Primitive API

ML-KEM-768

keypair = PQCrypto::KEM.generate(:ml_kem_768)
result = keypair.public_key.encapsulate
shared_secret = keypair.secret_key.decapsulate(result.ciphertext)

ML-DSA-65

keypair = PQCrypto::Signature.generate(:ml_dsa_65)
signature = keypair.secret_key.sign("hello")
keypair.public_key.verify!("hello", signature)

Hybrid ML-KEM-768 + X25519

keypair = PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_hkdf_sha256)
result = keypair.public_key.encapsulate
shared_secret = keypair.secret_key.decapsulate(result.ciphertext)

PQCrypto::HybridKEM is a custom pq_crypto construction. It is not advertised as compatible with HPKE, TLS hybrid drafts, X-Wing, OpenSSL native PQ APIs, or any other external wire format.

Serialization

Key import/export is available through pq_crypto-specific containers:

  • to_pqc_container_der
  • to_pqc_container_pem
  • *_from_pqc_container_der
  • *_from_pqc_container_pem

Example:

keypair = PQCrypto::KEM.generate(:ml_kem_768)
der = keypair.public_key.to_pqc_container_der
imported = PQCrypto::KEM.public_key_from_pqc_container_der(der)

These containers are not real ASN.1 SPKI or PKCS#8. They are intended for stable import/export inside pq_crypto itself and are not advertised as interoperable with external PKI tooling.

Introspection

PQCrypto.version
PQCrypto.backend
PQCrypto.supported_kems
PQCrypto.supported_hybrid_kems
PQCrypto.supported_signatures
PQCrypto::KEM.details(:ml_kem_768)
PQCrypto::HybridKEM.details(:ml_kem_768_x25519_hkdf_sha256)
PQCrypto::Signature.details(:ml_dsa_65)

Testing helpers

Deterministic test hooks are exposed under PQCrypto::Testing for regression coverage:

  • ml_kem_keypair_from_seed
  • ml_kem_encapsulate_from_seed
  • ml_dsa_keypair_from_seed
  • ml_dsa_sign_from_seed

These helpers are intended for tests only.

Development

Run the test suite with:

bundle exec rake test

Refresh vendored PQClean sources manually only when you intentionally update the vendor snapshot. The refresh script now has a safe pinned default and records the exact vendored snapshot in ext/pqcrypto/vendor/.vendored:

bundle exec ruby script/vendor_libs.rb

To intentionally change the upstream snapshot, override all four pinning inputs together:

PQCLEAN_VERSION=<full-git-commit> \
PQCLEAN_URL=https://github.com/PQClean/PQClean/archive/<full-git-commit>.tar.gz \
PQCLEAN_SHA256=<archive-sha256> \
PQCLEAN_STRIP=PQClean-<full-git-commit> \
  bundle exec ruby script/vendor_libs.rb