pq_crypto 0.3.2 → 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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -0
  3. data/CHANGELOG.md +37 -0
  4. data/GET_STARTED.md +361 -40
  5. data/README.md +58 -241
  6. data/SECURITY.md +101 -82
  7. data/ext/pqcrypto/extconf.rb +40 -7
  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 +14 -1
  11. data/ext/pqcrypto/pqcrypto_ruby_secure.c +484 -81
  12. data/ext/pqcrypto/pqcrypto_secure.c +179 -72
  13. data/ext/pqcrypto/pqcrypto_secure.h +87 -7
  14. data/ext/pqcrypto/pqcrypto_version.h +7 -0
  15. data/ext/pqcrypto/vendor/.vendored +1 -1
  16. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/LICENSE +5 -0
  17. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile +19 -0
  18. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile.Microsoft_nmake +23 -0
  19. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/api.h +18 -0
  20. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.c +83 -0
  21. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.h +11 -0
  22. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.c +327 -0
  23. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.h +22 -0
  24. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.c +164 -0
  25. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.h +23 -0
  26. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.c +146 -0
  27. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.h +14 -0
  28. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/params.h +36 -0
  29. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.c +311 -0
  30. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.h +37 -0
  31. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.c +198 -0
  32. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.h +26 -0
  33. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.c +41 -0
  34. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.h +13 -0
  35. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric-shake.c +71 -0
  36. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric.h +30 -0
  37. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.c +67 -0
  38. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.h +13 -0
  39. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/LICENSE +5 -0
  40. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile +19 -0
  41. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile.Microsoft_nmake +23 -0
  42. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/api.h +18 -0
  43. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.c +108 -0
  44. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.h +11 -0
  45. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.c +327 -0
  46. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.h +22 -0
  47. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.c +164 -0
  48. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.h +23 -0
  49. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.c +146 -0
  50. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.h +14 -0
  51. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/params.h +36 -0
  52. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.c +299 -0
  53. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.h +37 -0
  54. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.c +188 -0
  55. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.h +26 -0
  56. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.c +41 -0
  57. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.h +13 -0
  58. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric-shake.c +71 -0
  59. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric.h +30 -0
  60. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.c +67 -0
  61. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.h +13 -0
  62. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/LICENSE +5 -0
  63. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile +19 -0
  64. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile.Microsoft_nmake +23 -0
  65. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/api.h +50 -0
  66. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.c +98 -0
  67. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.h +10 -0
  68. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.c +261 -0
  69. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.h +31 -0
  70. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/params.h +44 -0
  71. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.c +848 -0
  72. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.h +52 -0
  73. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.c +415 -0
  74. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.h +65 -0
  75. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.c +69 -0
  76. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.h +17 -0
  77. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.c +98 -0
  78. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.h +14 -0
  79. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.c +407 -0
  80. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.h +47 -0
  81. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric-shake.c +26 -0
  82. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric.h +34 -0
  83. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/LICENSE +5 -0
  84. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile +19 -0
  85. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile.Microsoft_nmake +23 -0
  86. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/api.h +50 -0
  87. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.c +98 -0
  88. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.h +10 -0
  89. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.c +261 -0
  90. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.h +31 -0
  91. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/params.h +44 -0
  92. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.c +823 -0
  93. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.h +52 -0
  94. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.c +415 -0
  95. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.h +65 -0
  96. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.c +69 -0
  97. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.h +17 -0
  98. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.c +92 -0
  99. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.h +14 -0
  100. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.c +407 -0
  101. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.h +47 -0
  102. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric-shake.c +26 -0
  103. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric.h +34 -0
  104. data/lib/pq_crypto/algorithm_registry.rb +200 -0
  105. data/lib/pq_crypto/hybrid_kem.rb +1 -12
  106. data/lib/pq_crypto/kem.rb +104 -13
  107. data/lib/pq_crypto/pkcs8.rb +387 -0
  108. data/lib/pq_crypto/serialization.rb +1 -14
  109. data/lib/pq_crypto/signature.rb +123 -17
  110. data/lib/pq_crypto/spki.rb +131 -0
  111. data/lib/pq_crypto/version.rb +1 -1
  112. data/lib/pq_crypto.rb +78 -19
  113. data/script/vendor_libs.rb +4 -0
  114. metadata +95 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cd88ab6ebe3042111e60711895a9563a1510057a321ac9dab510496634656b8
4
- data.tar.gz: 684f469f8be7912780e00d368989418499aae00bb530d7fc32f8b6c6d5576593
3
+ metadata.gz: 65d8d9a5d3aeecaba257218dcae5bedb56c82283a4d4d34cdea4a5946c5d2b80
4
+ data.tar.gz: 210cd374801dd9f8ce7beafa4f5eb528c807bcedd3762977445849e2def7e215
5
5
  SHA512:
6
- metadata.gz: 54c1f3b0b8a2f7141d3ec4c8649c003793a3469f212fed94b0dc7ef0e2b0ff3ddba510383a0b62813d9ee98700bf266547be1900693c9ad465fb36deec91ab7b
7
- data.tar.gz: 41c0bdea2d91bbd2a2e8884ee2b2a328c4c06d410fba8d92c9c1a9470b9d5597d0b8f5233b0ba87225535a80c9055033518ef1dd82e90101a9f5ffcd606b81a6
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,42 @@
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
+
3
40
  ## [0.3.2] — 2026-04-25
4
41
 
5
42
  ### Added — streaming ML-DSA for large inputs
data/GET_STARTED.md CHANGED
@@ -1,98 +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
14
69
  ```
15
70
 
16
- ## 3. Encapsulate and decapsulate
71
+ Encapsulate with the public key:
17
72
 
18
73
  ```ruby
19
- result = keypair.public_key.encapsulate
20
- shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
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
21
116
  ```
22
117
 
23
- ## 4. Generate an ML-DSA-65 keypair
118
+ The same API shape applies to other supported ML-DSA parameter sets:
24
119
 
25
120
  ```ruby
26
- sig = PQCrypto::Signature.generate(:ml_dsa_65)
121
+ PQCrypto::Signature.generate(:ml_dsa_44)
122
+ PQCrypto::Signature.generate(:ml_dsa_87)
27
123
  ```
28
124
 
29
- ## 5. Sign and verify
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.
30
129
 
31
130
  ```ruby
32
- signature = sig.secret_key.sign("message")
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
33
136
 
34
- sig.public_key.verify("message", signature)
35
- sig.public_key.verify!("message", signature)
137
+ ok = File.open("document.bin", "rb") do |io|
138
+ keypair.public_key.verify_io(io, signature, chunk_size: 1 << 20)
139
+ end
36
140
  ```
37
141
 
38
- For large files, use streaming ML-DSA:
142
+ With an optional FIPS 204 context:
39
143
 
40
144
  ```ruby
145
+ context = "invoice-v1".b
146
+
41
147
  signature = File.open("document.bin", "rb") do |io|
42
- sig.secret_key.sign_io(io, chunk_size: 1 << 20)
148
+ keypair.secret_key.sign_io(io, context: context)
43
149
  end
44
150
 
45
151
  ok = File.open("document.bin", "rb") do |io|
46
- sig.public_key.verify_io(io, signature, chunk_size: 1 << 20)
152
+ keypair.public_key.verify_io(io, signature, context: context)
47
153
  end
48
154
  ```
49
155
 
50
- With an optional context:
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
+
170
+ result = keypair.public_key.encapsulate
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
199
+ ```
200
+
201
+ DER form:
202
+
203
+ ```ruby
204
+ der = keypair.public_key.to_spki_der
205
+ imported = PQCrypto::KEM.public_key_from_spki_der(der)
206
+ ```
207
+
208
+ ML-DSA example:
51
209
 
52
210
  ```ruby
53
- ctx = "document-v1".b
54
- signature = File.open("document.bin", "rb") { |io| sig.secret_key.sign_io(io, context: ctx) }
55
- ok = File.open("document.bin", "rb") { |io| sig.public_key.verify_io(io, signature, context: ctx) }
211
+ keypair = PQCrypto::Signature.generate(:ml_dsa_65)
212
+
213
+ pem = keypair.public_key.to_spki_pem
214
+ imported = PQCrypto::Signature.public_key_from_spki_pem(pem)
215
+ ```
216
+
217
+ You can require an expected algorithm during import:
218
+
219
+ ```ruby
220
+ PQCrypto::KEM.public_key_from_spki_pem(pem, algorithm: :ml_kem_768)
221
+ ```
222
+
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
56
240
  ```
57
241
 
58
- `sign_io` / `verify_io` are pure ML-DSA streaming helpers, not prehash
59
- shortcuts. `verify_io!` raises on mismatch.
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
60
255
 
61
- ## 6. Hybrid KEM (X-Wing)
256
+ ML-DSA private keys support expanded-key PKCS#8 by default:
62
257
 
63
258
  ```ruby
64
- hybrid = PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_xwing)
65
- result = hybrid.public_key.encapsulate
66
- shared_secret = hybrid.secret_key.decapsulate(result.ciphertext)
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
+ ```
264
+
265
+ ML-DSA seed/both import is intentionally opt-in:
266
+
267
+ ```ruby
268
+ PQCrypto::PKCS8.allow_ml_dsa_seed_format = true
269
+ imported = PQCrypto::Signature.secret_key_from_pkcs8_pem(pem)
67
270
  ```
68
271
 
69
- The raw X-Wing secret key exported by this API is the draft-10 32-byte
70
- decapsulation seed, not the expanded ML-KEM/X25519 private material.
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.
71
276
 
72
- The hybrid mode follows `draft-connolly-cfrg-xwing-kem`. See
73
- `SECURITY.md` for audit status.
277
+ ## 9. pq_crypto-local container serialization
74
278
 
75
- ## 7. Serialize a key
279
+ The `pqc_container_*` APIs are retained for backward compatibility with older
280
+ `pq_crypto` releases.
76
281
 
77
282
  ```ruby
283
+ keypair = PQCrypto::KEM.generate(:ml_kem_768)
284
+
78
285
  der = keypair.public_key.to_pqc_container_der
79
286
  imported = PQCrypto::KEM.public_key_from_pqc_container_der(der)
80
287
  ```
81
288
 
82
- `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:
325
+
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.
336
+
337
+ ```ruby
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
368
+ ```
369
+
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`.
83
373
 
84
- ## 8. Inspect supported algorithms
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:
85
384
 
86
385
  ```ruby
87
- PQCrypto.supported_kems # => [:ml_kem_768]
88
- PQCrypto.supported_hybrid_kems # => [:ml_kem_768_x25519_xwing]
89
- PQCrypto.supported_signatures # => [:ml_dsa_65]
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
90
408
  ```
91
409
 
92
- ## 9. Practical notes
410
+ To intentionally change the upstream snapshot, override all pinning inputs
411
+ together:
93
412
 
94
- - OpenSSL 3.0+ with SHA3-256 is required.
95
- - `PQCrypto::Testing` exposes deterministic helpers only for
96
- regression tests.
97
- - Key equality uses constant-time comparison. `#hash` returns a
98
- hash derived from a SHA-256 fingerprint, not the raw bytes.
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
+ ```