pq_crypto 0.3.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -0
  3. data/CHANGELOG.md +50 -0
  4. data/GET_STARTED.md +374 -30
  5. data/README.md +59 -195
  6. data/SECURITY.md +101 -82
  7. data/ext/pqcrypto/extconf.rb +85 -9
  8. data/ext/pqcrypto/mldsa_api.h +71 -1
  9. data/ext/pqcrypto/mlkem_api.h +24 -0
  10. data/ext/pqcrypto/pq_externalmu.c +310 -0
  11. data/ext/pqcrypto/pqcrypto_ruby_secure.c +784 -85
  12. data/ext/pqcrypto/pqcrypto_secure.c +179 -72
  13. data/ext/pqcrypto/pqcrypto_secure.h +103 -7
  14. data/ext/pqcrypto/pqcrypto_version.h +7 -0
  15. data/ext/pqcrypto/vendor/.vendored +1 -1
  16. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile +8 -0
  17. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/LICENSE +5 -0
  18. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile +19 -0
  19. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile.Microsoft_nmake +23 -0
  20. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/api.h +18 -0
  21. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.c +83 -0
  22. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.h +11 -0
  23. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.c +327 -0
  24. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.h +22 -0
  25. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.c +164 -0
  26. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.h +23 -0
  27. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.c +146 -0
  28. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.h +14 -0
  29. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/params.h +36 -0
  30. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.c +311 -0
  31. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.h +37 -0
  32. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.c +198 -0
  33. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.h +26 -0
  34. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.c +41 -0
  35. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.h +13 -0
  36. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric-shake.c +71 -0
  37. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric.h +30 -0
  38. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.c +67 -0
  39. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.h +13 -0
  40. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/LICENSE +5 -0
  41. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile +19 -0
  42. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile.Microsoft_nmake +23 -0
  43. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/api.h +18 -0
  44. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.c +108 -0
  45. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.h +11 -0
  46. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.c +327 -0
  47. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.h +22 -0
  48. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.c +164 -0
  49. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.h +23 -0
  50. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.c +146 -0
  51. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.h +14 -0
  52. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/params.h +36 -0
  53. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.c +299 -0
  54. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.h +37 -0
  55. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.c +188 -0
  56. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.h +26 -0
  57. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.c +41 -0
  58. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.h +13 -0
  59. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric-shake.c +71 -0
  60. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric.h +30 -0
  61. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.c +67 -0
  62. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.h +13 -0
  63. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile +19 -0
  64. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/LICENSE +5 -0
  65. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile +19 -0
  66. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile.Microsoft_nmake +23 -0
  67. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/api.h +50 -0
  68. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.c +98 -0
  69. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.h +10 -0
  70. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.c +261 -0
  71. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.h +31 -0
  72. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/params.h +44 -0
  73. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.c +848 -0
  74. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.h +52 -0
  75. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.c +415 -0
  76. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.h +65 -0
  77. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.c +69 -0
  78. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.h +17 -0
  79. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.c +98 -0
  80. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.h +14 -0
  81. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.c +407 -0
  82. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.h +47 -0
  83. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric-shake.c +26 -0
  84. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric.h +34 -0
  85. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile +19 -0
  86. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/LICENSE +5 -0
  87. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile +19 -0
  88. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile.Microsoft_nmake +23 -0
  89. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/api.h +50 -0
  90. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.c +98 -0
  91. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.h +10 -0
  92. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.c +261 -0
  93. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.h +31 -0
  94. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/params.h +44 -0
  95. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.c +823 -0
  96. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.h +52 -0
  97. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.c +415 -0
  98. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.h +65 -0
  99. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.c +69 -0
  100. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.h +17 -0
  101. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.c +92 -0
  102. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.h +14 -0
  103. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.c +407 -0
  104. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.h +47 -0
  105. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric-shake.c +26 -0
  106. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric.h +34 -0
  107. data/lib/pq_crypto/algorithm_registry.rb +200 -0
  108. data/lib/pq_crypto/hybrid_kem.rb +1 -12
  109. data/lib/pq_crypto/kem.rb +104 -13
  110. data/lib/pq_crypto/pkcs8.rb +387 -0
  111. data/lib/pq_crypto/serialization.rb +1 -14
  112. data/lib/pq_crypto/signature.rb +231 -13
  113. data/lib/pq_crypto/spki.rb +131 -0
  114. data/lib/pq_crypto/version.rb +1 -1
  115. data/lib/pq_crypto.rb +90 -19
  116. data/script/vendor_libs.rb +4 -0
  117. metadata +99 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 534c420f62323e9ddb49b48acdfa7af869731e3273bda56637d6bfee753de8d7
4
- data.tar.gz: f003494b4d33702a1b718dc6ded7a9f0612b315e41f0d739a5b2fe3ebba9d1d1
3
+ metadata.gz: 65d8d9a5d3aeecaba257218dcae5bedb56c82283a4d4d34cdea4a5946c5d2b80
4
+ data.tar.gz: 210cd374801dd9f8ce7beafa4f5eb528c807bcedd3762977445849e2def7e215
5
5
  SHA512:
6
- metadata.gz: 1a039325a13cf8c074741fb70edc1e66f4e4939727b4c1369665794bc6ed71ef1ed2ef50d9debd9064d850e4856fac166f325889a9df4bfd3d8849a2d7a918e9
7
- data.tar.gz: 304888656181149eed84eca063e24eeb6c862819f6c54022e77b287ca4030b8602e9f61eb17809ad86ca0eea84796c6275a2afa12018ffdeaafc489c15c8f24a
6
+ metadata.gz: a2085b6a3b6b48389219b81fa8b7a0e656c6583587df9a52817fcf28e663210be744e9e6a121dbaccdb9d06e8ce12fc2ee7b84f581fb9ce4e4bd74273df6a4b5
7
+ data.tar.gz: b6e7e3737f0d052d045b6fd4c424f188f6eab01333bc86b8685c707b6a90c14f4af0ecc3cacad6e8ca36750d71694d11481e5d4e617fa41bdbfe68a19a5ea0c3
@@ -35,3 +35,59 @@ jobs:
35
35
 
36
36
  - name: Run tests
37
37
  run: bundle exec rake test
38
+
39
+ interop-openssl-3-5-required:
40
+ name: interop-openssl-3.5-required
41
+ runs-on: ubuntu-24.04
42
+
43
+ steps:
44
+ - name: Checkout
45
+ uses: actions/checkout@v4
46
+
47
+ - name: Build OpenSSL 3.5
48
+ run: |
49
+ set -euxo pipefail
50
+ sudo apt-get update
51
+ sudo apt-get install -y build-essential perl pkg-config curl ca-certificates
52
+ curl -fsSLO https://www.openssl.org/source/openssl-3.5.0.tar.gz
53
+ tar -xzf openssl-3.5.0.tar.gz
54
+ cd openssl-3.5.0
55
+ ./Configure linux-x86_64 --prefix=/opt/openssl-3.5 --openssldir=/opt/openssl-3.5 shared
56
+ make -s -j"$(nproc)"
57
+ sudo make -s install_sw
58
+ echo /opt/openssl-3.5/lib64 | sudo tee /etc/ld.so.conf.d/openssl-3.5.conf
59
+ sudo ldconfig
60
+ /opt/openssl-3.5/bin/openssl version
61
+ /opt/openssl-3.5/bin/openssl version | grep -E '^OpenSSL 3\.(5|[6-9]|[1-9][0-9])\.'
62
+ echo /opt/openssl-3.5/bin >> "$GITHUB_PATH"
63
+ {
64
+ echo "OPENSSL_ROOT_DIR=/opt/openssl-3.5"
65
+ echo "PKG_CONFIG_PATH=/opt/openssl-3.5/lib64/pkgconfig"
66
+ echo "LD_LIBRARY_PATH=/opt/openssl-3.5/lib64"
67
+ } >> "$GITHUB_ENV"
68
+
69
+ - name: Set up Ruby
70
+ uses: ruby/setup-ruby@v1
71
+ with:
72
+ ruby-version: "3.4"
73
+ bundler-cache: true
74
+
75
+ - name: Set up Go
76
+ uses: actions/setup-go@v5
77
+ with:
78
+ go-version: "1.26.0"
79
+
80
+ - name: Compile extension
81
+ run: bundle exec rake compile
82
+
83
+ - name: Run tests
84
+ run: |
85
+ set -o pipefail
86
+ bundle exec rake test 2>&1 | tee rake_test.log
87
+
88
+ - name: Refuse skipped OpenSSL 3.5 interop tests
89
+ run: |
90
+ if grep -E "Skipped: .* (mlkem|mldsa)_spki" rake_test.log; then
91
+ echo "OpenSSL 3.5 interop tests must NOT skip on this matrix entry."
92
+ exit 1
93
+ fi
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.2] - 2026-04-29
4
+
5
+ ### Fixed
6
+ - Fixed native extension build from packaged gem by keeping generated
7
+ `pqcrypto_version.h` available after `make clean`.
8
+
9
+ ## [0.4.1] — 2026-04-29
10
+
11
+ ### Fixed
12
+
13
+ - Hardened native ML-DSA signing error handling by checking RNG failures.
14
+ - Improved streaming ML-DSA validation and context handling without regressing throughput.
15
+
16
+ ## [0.4.0] — 2026-04-28
17
+
18
+ ### Added — standard serialization and expanded parameter sets
19
+
20
+ - Added a central algorithm registry with legacy `pqc_container_*` OIDs kept separate from RFC 9935 / RFC 9881 standard OIDs.
21
+ - Added RFC 9935 SPKI public-key and PKCS#8 private-key support for ML-KEM.
22
+ - Added RFC 9881 SPKI public-key and expanded-key PKCS#8 support for ML-DSA.
23
+ - Added public API support for ML-KEM-512, ML-KEM-1024, ML-DSA-44, and ML-DSA-87 alongside the existing ML-KEM-768 and ML-DSA-65 support.
24
+ - Added OpenSSL 3.5+ interoperability tests for standard SPKI / PKCS#8 encodings where supported by the linked OpenSSL.
25
+ - Added NIST ACVP KAT test infrastructure for ML-KEM and ML-DSA parameter sets.
26
+ - Added opt-in ML-DSA seed/both PKCS#8 import support. The default remains off; callers must set `PQCrypto::PKCS8.allow_ml_dsa_seed_format = true`.
27
+
28
+ ### Changed
29
+
30
+ - Bumped the gem version to `0.4.0`.
31
+ - `pqc_container_*` remains frozen and project-local. It is still limited to the original three algorithms: `:ml_kem_768`, `:ml_dsa_65`, and `:ml_kem_768_x25519_xwing`.
32
+ - `DETAILS[:oid]` continues to expose the legacy `pqc_container_*` OID for backward compatibility. Standard OIDs are available via `PQCrypto::AlgorithmRegistry.standard_oid`.
33
+ - The native `PQCrypto.version` path now derives from `lib/pq_crypto/version.rb` through a generated C header, avoiding a second manually maintained version string.
34
+
35
+ ### Security notes
36
+
37
+ - ML-DSA seed-format imports are opt-in because PQClean does not expose a public ML-DSA derandomized keypair entrypoint. The implementation reuses the thread-local seed-replay path documented in `SECURITY.md`.
38
+ - The project remains unaudited and experimental.
39
+
40
+ ## [0.3.2] — 2026-04-25
41
+
42
+ ### Added — streaming ML-DSA for large inputs
43
+
44
+ - Added `PQCrypto::Signature::SecretKey#sign_io(io, chunk_size: 1 << 20, context: "".b)`.
45
+ - Added `PQCrypto::Signature::PublicKey#verify_io(io, signature, chunk_size: 1 << 20, context: "".b)` and `verify_io!`.
46
+ - Implemented streaming pure ML-DSA through an internal FIPS 204 ExternalMu path. Existing one-shot `sign` / `verify` semantics are unchanged; no public `sign_mu` / `verify_mu` API is exposed.
47
+
48
+ ### Notes
49
+
50
+ - Streaming is primarily for large IO inputs and lower peak memory pressure. It is not a HashML-DSA/prehash speed mode; CPU cost is still dominated by SHAKE/Keccak.
51
+ - Default empty-context streaming signatures interoperate with the existing one-shot `verify(message, signature)` API. Non-empty `context:` must be supplied again during `verify_io`.
52
+
3
53
  ## [0.3.1] — 2026-04-24
4
54
 
5
55
  ### Fixed — X-Wing draft-10 compatibility
data/GET_STARTED.md CHANGED
@@ -1,75 +1,419 @@
1
1
  # Getting started with pq_crypto
2
2
 
3
- ## 1. Build the extension
3
+ This guide shows the common `pq_crypto` workflows. The README is intentionally
4
+ short; examples and practical details live here.
5
+
6
+ ## 1. Install
7
+
8
+ Add the gem to your application:
9
+
10
+ ```ruby
11
+ # Gemfile
12
+ gem "pq_crypto"
13
+ ```
14
+
15
+ Install it:
4
16
 
5
17
  ```bash
6
18
  bundle install
19
+ ```
20
+
21
+ If you are working from a repository checkout, compile the native extension:
22
+
23
+ ```bash
7
24
  bundle exec rake compile
8
25
  ```
9
26
 
10
- ## 2. Generate an ML-KEM-768 keypair
27
+ Use the gem from Ruby:
28
+
29
+ ```ruby
30
+ require "pq_crypto"
31
+ ```
32
+
33
+ ## 2. Check the available algorithms
34
+
35
+ ```ruby
36
+ PQCrypto.supported_kems
37
+ # => [:ml_kem_512, :ml_kem_768, :ml_kem_1024]
38
+
39
+ PQCrypto.supported_signatures
40
+ # => [:ml_dsa_44, :ml_dsa_65, :ml_dsa_87]
41
+
42
+ PQCrypto.supported_hybrid_kems
43
+ # => [:ml_kem_768_x25519_xwing]
44
+ ```
45
+
46
+ Useful metadata:
47
+
48
+ ```ruby
49
+ PQCrypto.version
50
+ PQCrypto.backend
51
+
52
+ PQCrypto::KEM.details(:ml_kem_768)
53
+ PQCrypto::Signature.details(:ml_dsa_65)
54
+ PQCrypto::AlgorithmRegistry.standard_oid(:ml_kem_768)
55
+ ```
56
+
57
+ `DETAILS[:oid]` remains the legacy `pqc_container_*` OID for backward
58
+ compatibility. Use `AlgorithmRegistry.standard_oid` when you need the standard
59
+ RFC OID.
60
+
61
+ ## 3. ML-KEM: generate, encapsulate, decapsulate
62
+
63
+ Generate a keypair:
11
64
 
12
65
  ```ruby
13
66
  keypair = PQCrypto::KEM.generate(:ml_kem_768)
67
+ public_key = keypair.public_key
68
+ secret_key = keypair.secret_key
69
+ ```
70
+
71
+ Encapsulate with the public key:
72
+
73
+ ```ruby
74
+ result = public_key.encapsulate
75
+ ciphertext = result.ciphertext
76
+ sender_shared_secret = result.shared_secret
77
+ ```
78
+
79
+ Decapsulate with the secret key:
80
+
81
+ ```ruby
82
+ receiver_shared_secret = secret_key.decapsulate(ciphertext)
83
+
84
+ sender_shared_secret == receiver_shared_secret
85
+ # => true
86
+ ```
87
+
88
+ The same API shape applies to other supported ML-KEM parameter sets:
89
+
90
+ ```ruby
91
+ PQCrypto::KEM.generate(:ml_kem_512)
92
+ PQCrypto::KEM.generate(:ml_kem_1024)
93
+ ```
94
+
95
+ ## 4. ML-DSA: sign and verify
96
+
97
+ Generate a signature keypair:
98
+
99
+ ```ruby
100
+ keypair = PQCrypto::Signature.generate(:ml_dsa_65)
101
+ public_key = keypair.public_key
102
+ secret_key = keypair.secret_key
103
+ ```
104
+
105
+ Sign and verify a message:
106
+
107
+ ```ruby
108
+ message = "hello".b
109
+ signature = secret_key.sign(message)
110
+
111
+ public_key.verify(message, signature)
112
+ # => true
113
+
114
+ public_key.verify!(message, signature)
115
+ # returns true, or raises on mismatch
116
+ ```
117
+
118
+ The same API shape applies to other supported ML-DSA parameter sets:
119
+
120
+ ```ruby
121
+ PQCrypto::Signature.generate(:ml_dsa_44)
122
+ PQCrypto::Signature.generate(:ml_dsa_87)
123
+ ```
124
+
125
+ ## 5. ML-DSA for large files
126
+
127
+ For large inputs, use the streaming helpers so the whole message does not need
128
+ to be materialized as one Ruby string.
129
+
130
+ ```ruby
131
+ keypair = PQCrypto::Signature.generate(:ml_dsa_65)
132
+
133
+ signature = File.open("document.bin", "rb") do |io|
134
+ keypair.secret_key.sign_io(io, chunk_size: 1 << 20)
135
+ end
136
+
137
+ ok = File.open("document.bin", "rb") do |io|
138
+ keypair.public_key.verify_io(io, signature, chunk_size: 1 << 20)
139
+ end
14
140
  ```
15
141
 
16
- ## 3. Encapsulate and decapsulate
142
+ With an optional FIPS 204 context:
17
143
 
18
144
  ```ruby
145
+ context = "invoice-v1".b
146
+
147
+ signature = File.open("document.bin", "rb") do |io|
148
+ keypair.secret_key.sign_io(io, context: context)
149
+ end
150
+
151
+ ok = File.open("document.bin", "rb") do |io|
152
+ keypair.public_key.verify_io(io, signature, context: context)
153
+ end
154
+ ```
155
+
156
+ Notes:
157
+
158
+ - `context` must match during verification.
159
+ - `context` is limited to 255 bytes by FIPS 204.
160
+ - `chunk_size` must be positive.
161
+ - `sign_io` / `verify_io` are pure ML-DSA streaming helpers. They are not
162
+ HashML-DSA/prehash shortcuts and do not expose public `sign_mu` /
163
+ `verify_mu` APIs.
164
+
165
+ ## 6. Hybrid KEM: ML-KEM-768 + X25519 X-Wing
166
+
167
+ ```ruby
168
+ keypair = PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_xwing)
169
+
19
170
  result = keypair.public_key.encapsulate
20
- shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
171
+ ciphertext = result.ciphertext
172
+ sender_shared_secret = result.shared_secret
173
+
174
+ receiver_shared_secret = keypair.secret_key.decapsulate(ciphertext)
175
+
176
+ sender_shared_secret == receiver_shared_secret
177
+ # => true
178
+ ```
179
+
180
+ The raw X-Wing secret key exported by this API is the draft-style 32-byte
181
+ secret seed, not the expanded ML-KEM/X25519 private material.
182
+
183
+ See `SECURITY.md` for audit status and hybrid interoperability caveats.
184
+
185
+ ## 7. SPKI public-key serialization
186
+
187
+ Use SPKI when you need standard public-key serialization for ML-KEM or ML-DSA.
188
+
189
+ ML-KEM example:
190
+
191
+ ```ruby
192
+ keypair = PQCrypto::KEM.generate(:ml_kem_768)
193
+
194
+ pem = keypair.public_key.to_spki_pem
195
+ imported = PQCrypto::KEM.public_key_from_spki_pem(pem)
196
+
197
+ imported == keypair.public_key
198
+ # => true
21
199
  ```
22
200
 
23
- ## 4. Generate an ML-DSA-65 keypair
201
+ DER form:
24
202
 
25
203
  ```ruby
26
- sig = PQCrypto::Signature.generate(:ml_dsa_65)
204
+ der = keypair.public_key.to_spki_der
205
+ imported = PQCrypto::KEM.public_key_from_spki_der(der)
27
206
  ```
28
207
 
29
- ## 5. Sign and verify
208
+ ML-DSA example:
30
209
 
31
210
  ```ruby
32
- signature = sig.secret_key.sign("message")
211
+ keypair = PQCrypto::Signature.generate(:ml_dsa_65)
33
212
 
34
- sig.public_key.verify("message", signature)
35
- sig.public_key.verify!("message", signature)
213
+ pem = keypair.public_key.to_spki_pem
214
+ imported = PQCrypto::Signature.public_key_from_spki_pem(pem)
36
215
  ```
37
216
 
38
- ## 6. Hybrid KEM (X-Wing)
217
+ You can require an expected algorithm during import:
39
218
 
40
219
  ```ruby
41
- hybrid = PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_xwing)
42
- result = hybrid.public_key.encapsulate
43
- shared_secret = hybrid.secret_key.decapsulate(result.ciphertext)
220
+ PQCrypto::KEM.public_key_from_spki_pem(pem, algorithm: :ml_kem_768)
44
221
  ```
45
222
 
46
- The raw X-Wing secret key exported by this API is the draft-10 32-byte
47
- decapsulation seed, not the expanded ML-KEM/X25519 private material.
223
+ ## 8. PKCS#8 private-key serialization
224
+
225
+ Use PKCS#8 when you need standard private-key serialization for ML-KEM or
226
+ ML-DSA.
227
+
228
+ ### ML-KEM PKCS#8
229
+
230
+ `SecretKey#to_pkcs8_pem` exports the expanded private key by default:
231
+
232
+ ```ruby
233
+ keypair = PQCrypto::KEM.generate(:ml_kem_768)
234
+
235
+ pem = keypair.secret_key.to_pkcs8_pem
236
+ imported = PQCrypto::KEM.secret_key_from_pkcs8_pem(pem)
237
+
238
+ imported == keypair.secret_key
239
+ # => true
240
+ ```
48
241
 
49
- The hybrid mode follows `draft-connolly-cfrg-xwing-kem`. See
50
- `SECURITY.md` for audit status.
242
+ DER form:
243
+
244
+ ```ruby
245
+ der = keypair.secret_key.to_pkcs8_der
246
+ imported = PQCrypto::KEM.secret_key_from_pkcs8_der(der)
247
+ ```
248
+
249
+ ML-KEM PKCS#8 supports `:seed`, `:expanded`, and `:both` formats. A generated
250
+ `SecretKey` does not retain the original seed, so exporting `:seed` or `:both`
251
+ from `SecretKey#to_pkcs8_*` is intentionally unavailable. If you explicitly
252
+ have the seed material, use the low-level PKCS#8 encoder.
253
+
254
+ ### ML-DSA PKCS#8
255
+
256
+ ML-DSA private keys support expanded-key PKCS#8 by default:
257
+
258
+ ```ruby
259
+ keypair = PQCrypto::Signature.generate(:ml_dsa_65)
260
+
261
+ pem = keypair.secret_key.to_pkcs8_pem
262
+ imported = PQCrypto::Signature.secret_key_from_pkcs8_pem(pem)
263
+ ```
51
264
 
52
- ## 7. Serialize a key
265
+ ML-DSA seed/both import is intentionally opt-in:
53
266
 
54
267
  ```ruby
268
+ PQCrypto::PKCS8.allow_ml_dsa_seed_format = true
269
+ imported = PQCrypto::Signature.secret_key_from_pkcs8_pem(pem)
270
+ ```
271
+
272
+ Seed/both export from an existing ML-DSA `SecretKey` is intentionally not
273
+ available because the object does not retain the original seed material. When
274
+ you explicitly have seed material, call `PQCrypto::PKCS8.encode_der` /
275
+ `encode_pem` directly.
276
+
277
+ ## 9. pq_crypto-local container serialization
278
+
279
+ The `pqc_container_*` APIs are retained for backward compatibility with older
280
+ `pq_crypto` releases.
281
+
282
+ ```ruby
283
+ keypair = PQCrypto::KEM.generate(:ml_kem_768)
284
+
55
285
  der = keypair.public_key.to_pqc_container_der
56
286
  imported = PQCrypto::KEM.public_key_from_pqc_container_der(der)
57
287
  ```
58
288
 
59
- `pqc_container_*` formats are pq_crypto-specific.
289
+ PEM form:
290
+
291
+ ```ruby
292
+ pem = keypair.secret_key.to_pqc_container_pem
293
+ imported = PQCrypto::KEM.secret_key_from_pqc_container_pem(pem)
294
+ ```
295
+
296
+ Important caveats:
297
+
298
+ - `pqc_container_*` is project-local.
299
+ - It is not ASN.1 SPKI or PKCS#8.
300
+ - It is not advertised as interoperable with external PKI tooling.
301
+ - It remains limited to the original algorithms:
302
+ - `:ml_kem_768`
303
+ - `:ml_dsa_65`
304
+ - `:ml_kem_768_x25519_xwing`
305
+
306
+ For external interoperability, prefer SPKI for public keys and PKCS#8 for
307
+ private keys.
308
+
309
+ ## 10. Raw key import/export
310
+
311
+ Raw bytes can be useful when integrating with storage or protocols that already
312
+ handle framing.
313
+
314
+ ```ruby
315
+ keypair = PQCrypto::KEM.generate(:ml_kem_768)
316
+
317
+ public_bytes = keypair.public_key.to_bytes
318
+ secret_bytes = keypair.secret_key.to_bytes
319
+
320
+ public_key = PQCrypto::KEM.public_key_from_bytes(:ml_kem_768, public_bytes)
321
+ secret_key = PQCrypto::KEM.secret_key_from_bytes(:ml_kem_768, secret_bytes)
322
+ ```
323
+
324
+ Signature keys have the same shape:
60
325
 
61
- ## 8. Inspect supported algorithms
326
+ ```ruby
327
+ keypair = PQCrypto::Signature.generate(:ml_dsa_65)
328
+
329
+ public_key = PQCrypto::Signature.public_key_from_bytes(:ml_dsa_65, keypair.public_key.to_bytes)
330
+ secret_key = PQCrypto::Signature.secret_key_from_bytes(:ml_dsa_65, keypair.secret_key.to_bytes)
331
+ ```
332
+
333
+ ## 11. Secure wiping
334
+
335
+ `PQCrypto.secure_wipe(str)` zeroes a mutable Ruby string in place.
62
336
 
63
337
  ```ruby
64
- PQCrypto.supported_kems # => [:ml_kem_768]
65
- PQCrypto.supported_hybrid_kems # => [:ml_kem_768_x25519_xwing]
66
- PQCrypto.supported_signatures # => [:ml_dsa_65]
338
+ raw = File.binread("secret-key.bin")
339
+ key = PQCrypto::KEM.secret_key_from_bytes(:ml_kem_768, raw)
340
+
341
+ PQCrypto.secure_wipe(raw)
342
+ key.wipe!
343
+ ```
344
+
345
+ `wipe!` is best-effort only. It clears the current Ruby string buffer owned by
346
+ the key object. It cannot erase every copy that may have been created by Ruby,
347
+ OpenSSL, serialization, logging, or the garbage collector.
348
+
349
+ ## 12. Equality and inspection
350
+
351
+ Key equality uses constant-time comparison through OpenSSL `CRYPTO_memcmp` via
352
+ `PQCrypto.ct_equals`.
353
+
354
+ ```ruby
355
+ keypair.public_key == imported_public_key
356
+ keypair.secret_key == imported_secret_key
357
+ ```
358
+
359
+ Secret key `inspect` output is intentionally redacted, and secret key objects
360
+ do not expose a public fingerprint method.
361
+
362
+ ## 13. Build-time Keccak backend
363
+
364
+ The default build uses PQClean's scalar `common/fips202.c` backend:
365
+
366
+ ```bash
367
+ PQCRYPTO_KECCAK_BACKEND=clean bundle exec rake compile
67
368
  ```
68
369
 
69
- ## 9. Practical notes
370
+ `PQCRYPTO_KECCAK_BACKEND=xkcp` is reserved for a separately vendored, reviewed,
371
+ `fips202.h`-compatible XKCP adapter. If requested without that adapter, the
372
+ build aborts instead of silently falling back to `clean`.
70
373
 
71
- - OpenSSL 3.0+ with SHA3-256 is required.
72
- - `PQCrypto::Testing` exposes deterministic helpers only for
73
- regression tests.
74
- - Key equality uses constant-time comparison. `#hash` returns a
75
- hash derived from a SHA-256 fingerprint, not the raw bytes.
374
+ ## 14. Async / Fiber scheduler behavior
375
+
376
+ On Ruby 3.4, signing and verification use Ruby's scheduler-aware
377
+ `rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` path automatically. With a scheduler
378
+ that implements `blocking_operation_wait`, blocking native work can be moved
379
+ off the event loop.
380
+
381
+ ## 15. Test-only deterministic helpers
382
+
383
+ `PQCrypto::Testing` exposes deterministic helpers for regression tests:
384
+
385
+ ```ruby
386
+ PQCrypto::Testing.ml_kem_keypair_from_seed(seed) # 64-byte d||z seed
387
+ PQCrypto::Testing.ml_kem_encapsulate_from_seed(pk, seed) # 32-byte seed
388
+ PQCrypto::Testing.ml_dsa_keypair_from_seed(seed) # 32-byte seed
389
+ PQCrypto::Testing.ml_dsa_sign_from_seed(message, sk, seed)
390
+ ```
391
+
392
+ These helpers are intended for tests only. They drive stock PQClean entrypoints
393
+ and are not part of the normal application API.
394
+
395
+ ## 16. Development commands
396
+
397
+ Run the test suite:
398
+
399
+ ```bash
400
+ bundle exec rake test
401
+ ```
402
+
403
+ Refresh the pinned PQClean vendor snapshot only when intentionally updating
404
+ vendored sources:
405
+
406
+ ```bash
407
+ bundle exec ruby script/vendor_libs.rb
408
+ ```
409
+
410
+ To intentionally change the upstream snapshot, override all pinning inputs
411
+ together:
412
+
413
+ ```bash
414
+ PQCLEAN_VERSION=<full-git-commit> \
415
+ PQCLEAN_URL=https://github.com/PQClean/PQClean/archive/<full-git-commit>.tar.gz \
416
+ PQCLEAN_SHA256=<archive-sha256> \
417
+ PQCLEAN_STRIP=PQClean-<full-git-commit> \
418
+ bundle exec ruby script/vendor_libs.rb
419
+ ```