pq_crypto 0.1.0

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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +37 -0
  3. data/CHANGELOG.md +29 -0
  4. data/GET_STARTED.md +65 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +135 -0
  7. data/SECURITY.md +57 -0
  8. data/ext/pqcrypto/extconf.rb +157 -0
  9. data/ext/pqcrypto/mldsa_api.h +51 -0
  10. data/ext/pqcrypto/mlkem_api.h +21 -0
  11. data/ext/pqcrypto/pqcrypto_ruby_secure.c +889 -0
  12. data/ext/pqcrypto/pqcrypto_secure.c +1178 -0
  13. data/ext/pqcrypto/pqcrypto_secure.h +135 -0
  14. data/ext/pqcrypto/vendor/.vendored +5 -0
  15. data/ext/pqcrypto/vendor/pqclean/common/aes.c +639 -0
  16. data/ext/pqcrypto/vendor/pqclean/common/aes.h +64 -0
  17. data/ext/pqcrypto/vendor/pqclean/common/compat.h +73 -0
  18. data/ext/pqcrypto/vendor/pqclean/common/crypto_declassify.h +7 -0
  19. data/ext/pqcrypto/vendor/pqclean/common/fips202.c +928 -0
  20. data/ext/pqcrypto/vendor/pqclean/common/fips202.h +166 -0
  21. data/ext/pqcrypto/vendor/pqclean/common/keccak2x/feat.S +168 -0
  22. data/ext/pqcrypto/vendor/pqclean/common/keccak2x/fips202x2.c +684 -0
  23. data/ext/pqcrypto/vendor/pqclean/common/keccak2x/fips202x2.h +60 -0
  24. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SIMD256.c +1028 -0
  25. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SnP.h +50 -0
  26. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-unrolling.macros +198 -0
  27. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile +8 -0
  28. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile.Microsoft_nmake +8 -0
  29. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/SIMD256-config.h +3 -0
  30. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/align.h +34 -0
  31. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/brg_endian.h +142 -0
  32. data/ext/pqcrypto/vendor/pqclean/common/nistseedexpander.c +101 -0
  33. data/ext/pqcrypto/vendor/pqclean/common/nistseedexpander.h +39 -0
  34. data/ext/pqcrypto/vendor/pqclean/common/randombytes.c +355 -0
  35. data/ext/pqcrypto/vendor/pqclean/common/randombytes.h +27 -0
  36. data/ext/pqcrypto/vendor/pqclean/common/sha2.c +769 -0
  37. data/ext/pqcrypto/vendor/pqclean/common/sha2.h +173 -0
  38. data/ext/pqcrypto/vendor/pqclean/common/sp800-185.c +156 -0
  39. data/ext/pqcrypto/vendor/pqclean/common/sp800-185.h +27 -0
  40. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/LICENSE +5 -0
  41. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile +19 -0
  42. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile.Microsoft_nmake +23 -0
  43. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/api.h +18 -0
  44. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.c +83 -0
  45. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.h +11 -0
  46. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/indcpa.c +327 -0
  47. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/indcpa.h +22 -0
  48. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/kem.c +164 -0
  49. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/kem.h +23 -0
  50. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/ntt.c +146 -0
  51. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/ntt.h +14 -0
  52. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/params.h +36 -0
  53. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/poly.c +299 -0
  54. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/poly.h +37 -0
  55. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/polyvec.c +188 -0
  56. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/polyvec.h +26 -0
  57. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/reduce.c +41 -0
  58. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/reduce.h +13 -0
  59. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/symmetric-shake.c +71 -0
  60. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/symmetric.h +30 -0
  61. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.c +67 -0
  62. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.h +13 -0
  63. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/LICENSE +5 -0
  64. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile +19 -0
  65. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile.Microsoft_nmake +23 -0
  66. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/api.h +50 -0
  67. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.c +98 -0
  68. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.h +10 -0
  69. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.c +261 -0
  70. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.h +31 -0
  71. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/params.h +44 -0
  72. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.c +799 -0
  73. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.h +52 -0
  74. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.c +415 -0
  75. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.h +65 -0
  76. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/reduce.c +69 -0
  77. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/reduce.h +17 -0
  78. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/rounding.c +92 -0
  79. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/rounding.h +14 -0
  80. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/sign.c +407 -0
  81. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/sign.h +47 -0
  82. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric-shake.c +26 -0
  83. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric.h +34 -0
  84. data/lib/pq_crypto/errors.rb +10 -0
  85. data/lib/pq_crypto/hybrid_kem.rb +106 -0
  86. data/lib/pq_crypto/kem.rb +199 -0
  87. data/lib/pq_crypto/serialization.rb +102 -0
  88. data/lib/pq_crypto/signature.rb +198 -0
  89. data/lib/pq_crypto/version.rb +5 -0
  90. data/lib/pq_crypto.rb +177 -0
  91. data/lib/pqcrypto.rb +3 -0
  92. data/script/vendor_libs.rb +199 -0
  93. metadata +195 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dcc40bb87fcd74ce1d16bb32d99df894cb127e0c77c879cba7ec7d6ef8e099e8
4
+ data.tar.gz: 6baf54010794cca2d1a9f81f884f1a6db16355515f83ed1812baa57feeaf63dc
5
+ SHA512:
6
+ metadata.gz: feb14e5bc0aefc8fa3b9b3d24083cd93923466d9bc1c5a1e4e527325f0290124be785da63aca97e342c9885e1e56b92e2637007a93c47d85ac950bee2317af71
7
+ data.tar.gz: 1a7773c9850dfea54df3897a6ee092f99258e25b0fa5393c977fa7e70c3eed309bca54044711501a6475f0d30ca56753eecaa768e87c9620a4e581b015c3332a
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, macos-latest]
16
+ ruby: ["3.1", "3.2", "3.3", "3.4"]
17
+
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ bundler-cache: true
27
+
28
+ - name: Set up Go
29
+ uses: actions/setup-go@v5
30
+ with:
31
+ go-version: "1.26.0"
32
+
33
+ - name: Compile extension
34
+ run: bundle exec rake compile
35
+
36
+ - name: Run tests
37
+ run: bundle exec rake test
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0]
4
+
5
+ Initial public release.
6
+
7
+ ### Public API
8
+
9
+ - Added primitive-first `PQCrypto::KEM` for pure `ML-KEM-768`.
10
+ - Added primitive-first `PQCrypto::Signature` for `ML-DSA-65`.
11
+ - Added `PQCrypto::HybridKEM` for the pq_crypto-specific `ML-KEM-768 + X25519 + HKDF-SHA256` hybrid combiner.
12
+ - Added typed key objects with raw-byte import/export and `details`/supported-algorithm introspection.
13
+ - Added `pqc_container_*` DER/PEM import/export for pq_crypto-specific key containers.
14
+ - Documented that `pqc_container_*` containers use pq_crypto-local OIDs and are not a long-term external interoperability guarantee.
15
+ - Added `PQCrypto::Testing` deterministic hooks for regression coverage.
16
+
17
+ ### Native / build
18
+
19
+ - Vendored `PQClean` sources for `ML-KEM-768` and `ML-DSA-65`.
20
+ - Integrated OpenSSL-backed conventional primitives for hybrid mode and utility operations.
21
+ - Require OpenSSL 3.0 or later.
22
+
23
+ ### Testing
24
+
25
+ - Added deterministic regression coverage for `ML-KEM-768` and `ML-DSA-65`.
26
+ - Hardened native bindings by copying Ruby string inputs before running no-GVL native operations.
27
+ - Tightened manual vendoring workflow to require an explicit pinned upstream URL, version label, strip prefix, and SHA-256.
28
+ - Added primitive interop tests for OpenSSL and Go where toolchain support is available.
29
+ - Added serialization hardening tests.
data/GET_STARTED.md ADDED
@@ -0,0 +1,65 @@
1
+ # Getting started with pq_crypto
2
+
3
+ ## 1. Build the extension
4
+
5
+ ```bash
6
+ bundle install
7
+ bundle exec rake compile
8
+ ```
9
+
10
+ ## 2. Generate an ML-KEM-768 keypair
11
+
12
+ ```ruby
13
+ keypair = PQCrypto::KEM.generate(:ml_kem_768)
14
+ ```
15
+
16
+ ## 3. Encapsulate and decapsulate
17
+
18
+ ```ruby
19
+ result = keypair.public_key.encapsulate
20
+ shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
21
+ ```
22
+
23
+ ## 4. Generate an ML-DSA-65 keypair
24
+
25
+ ```ruby
26
+ sig = PQCrypto::Signature.generate(:ml_dsa_65)
27
+ ```
28
+
29
+ ## 5. Sign and verify
30
+
31
+ ```ruby
32
+ signature = sig.secret_key.sign("message")
33
+ sig.public_key.verify!("message", signature)
34
+ ```
35
+
36
+ ## 6. Optional hybrid KEM
37
+
38
+ ```ruby
39
+ hybrid = PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_hkdf_sha256)
40
+ result = hybrid.public_key.encapsulate
41
+ shared_secret = hybrid.secret_key.decapsulate(result.ciphertext)
42
+ ```
43
+
44
+ This hybrid mode is pq_crypto-specific and not a general interoperability format.
45
+
46
+ ## 7. Serialize a key
47
+
48
+ ```ruby
49
+ der = keypair.public_key.to_pqc_container_der
50
+ imported = PQCrypto::KEM.public_key_from_pqc_container_der(der)
51
+ ```
52
+
53
+ ## 8. Inspect supported algorithms
54
+
55
+ ```ruby
56
+ PQCrypto.supported_kems
57
+ PQCrypto.supported_hybrid_kems
58
+ PQCrypto.supported_signatures
59
+ ```
60
+
61
+ ## 9. Practical notes
62
+
63
+ - OpenSSL 3.0+ is required.
64
+ - `pqc_container_*` formats are pq_crypto-specific.
65
+ - `PQCrypto::Testing` exposes deterministic helpers only for regression tests.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roman Haydarov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # pq_crypto
2
+
3
+ `pq_crypto` is a primitive-first Ruby gem for post-quantum cryptography.
4
+
5
+ It currently exposes three public building blocks:
6
+
7
+ - `PQCrypto::KEM` — pure `ML-KEM-768`
8
+ - `PQCrypto::Signature` — `ML-DSA-65`
9
+ - `PQCrypto::HybridKEM` — an optional custom hybrid KEM that combines `ML-KEM-768` and `X25519` with transcript-bound `HKDF-SHA256`
10
+
11
+ 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`.
12
+
13
+ ## Status
14
+
15
+ - first public release
16
+ - primitive-first API only
17
+ - no protocol/session helpers in the public surface
18
+ - serialization uses pq_crypto-specific `pqc_container_*` wrappers
19
+ - not audited
20
+ - not yet positioned as production-ready
21
+
22
+ ## Installation
23
+
24
+ Add the gem to your project and compile the extension:
25
+
26
+ ```ruby
27
+ # Gemfile
28
+ gem "pq_crypto"
29
+ ```
30
+
31
+ ```bash
32
+ bundle install
33
+ bundle exec rake compile
34
+ ```
35
+
36
+ ### Native dependencies
37
+
38
+ - Ruby 3.1+
39
+ - a C toolchain
40
+ - OpenSSL **3.0 or later**
41
+
42
+ ## Primitive API
43
+
44
+ ### ML-KEM-768
45
+
46
+ ```ruby
47
+ keypair = PQCrypto::KEM.generate(:ml_kem_768)
48
+ result = keypair.public_key.encapsulate
49
+ shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
50
+ ```
51
+
52
+ ### ML-DSA-65
53
+
54
+ ```ruby
55
+ keypair = PQCrypto::Signature.generate(:ml_dsa_65)
56
+ signature = keypair.secret_key.sign("hello")
57
+ keypair.public_key.verify!("hello", signature)
58
+ ```
59
+
60
+ ### Hybrid ML-KEM-768 + X25519
61
+
62
+ ```ruby
63
+ keypair = PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_hkdf_sha256)
64
+ result = keypair.public_key.encapsulate
65
+ shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
66
+ ```
67
+
68
+ `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.
69
+
70
+ ## Serialization
71
+
72
+ Key import/export is available through pq_crypto-specific containers:
73
+
74
+ - `to_pqc_container_der`
75
+ - `to_pqc_container_pem`
76
+ - `*_from_pqc_container_der`
77
+ - `*_from_pqc_container_pem`
78
+
79
+ Example:
80
+
81
+ ```ruby
82
+ keypair = PQCrypto::KEM.generate(:ml_kem_768)
83
+ der = keypair.public_key.to_pqc_container_der
84
+ imported = PQCrypto::KEM.public_key_from_pqc_container_der(der)
85
+ ```
86
+
87
+ 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.
88
+
89
+ ## Introspection
90
+
91
+ ```ruby
92
+ PQCrypto.version
93
+ PQCrypto.backend
94
+ PQCrypto.supported_kems
95
+ PQCrypto.supported_hybrid_kems
96
+ PQCrypto.supported_signatures
97
+ PQCrypto::KEM.details(:ml_kem_768)
98
+ PQCrypto::HybridKEM.details(:ml_kem_768_x25519_hkdf_sha256)
99
+ PQCrypto::Signature.details(:ml_dsa_65)
100
+ ```
101
+
102
+ ## Testing helpers
103
+
104
+ Deterministic test hooks are exposed under `PQCrypto::Testing` for regression coverage:
105
+
106
+ - `ml_kem_keypair_from_seed`
107
+ - `ml_kem_encapsulate_from_seed`
108
+ - `ml_dsa_keypair_from_seed`
109
+ - `ml_dsa_sign_from_seed`
110
+
111
+ These helpers are intended for tests only.
112
+
113
+ ## Development
114
+
115
+ Run the test suite with:
116
+
117
+ ```bash
118
+ bundle exec rake test
119
+ ```
120
+
121
+ 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`:
122
+
123
+ ```bash
124
+ bundle exec ruby script/vendor_libs.rb
125
+ ```
126
+
127
+ To intentionally change the upstream snapshot, override all four pinning inputs together:
128
+
129
+ ```bash
130
+ PQCLEAN_VERSION=<full-git-commit> \
131
+ PQCLEAN_URL=https://github.com/PQClean/PQClean/archive/<full-git-commit>.tar.gz \
132
+ PQCLEAN_SHA256=<archive-sha256> \
133
+ PQCLEAN_STRIP=PQClean-<full-git-commit> \
134
+ bundle exec ruby script/vendor_libs.rb
135
+ ```
data/SECURITY.md ADDED
@@ -0,0 +1,57 @@
1
+ # Security notes
2
+
3
+ ## Scope of the public API
4
+
5
+ `pq_crypto` 0.1.0 exposes a primitive-first public surface:
6
+
7
+ - `PQCrypto::KEM` (`ML-KEM-768`)
8
+ - `PQCrypto::Signature` (`ML-DSA-65`)
9
+ - `PQCrypto::HybridKEM` (custom `ML-KEM-768 + X25519 + HKDF-SHA256` combiner)
10
+ - `PQCrypto.secure_wipe`
11
+
12
+ The gem does **not** publish protocol/session helpers as part of the supported public API in this release.
13
+
14
+ ## Audit status
15
+
16
+ This project has not been audited. Treat it as experimental software.
17
+
18
+ ## Algorithm notes
19
+
20
+ ### ML-KEM-768 / ML-DSA-65
21
+
22
+ The core post-quantum primitives are backed by vendored `PQClean` sources.
23
+
24
+ ### HybridKEM
25
+
26
+ `PQCrypto::HybridKEM` is a pq_crypto-specific hybrid combiner. It is **not** claimed to be HPKE-, TLS-, or X-Wing-compatible.
27
+
28
+ Use it only if you explicitly want this project-local construction.
29
+
30
+ ## Serialization
31
+
32
+ `pqc_container_*` DER/PEM wrappers are pq_crypto-specific containers.
33
+
34
+ They are:
35
+ - not real SPKI
36
+ - not real PKCS#8
37
+ - not advertised as interoperable with OpenSSL, Go, Java, or PKI tooling
38
+
39
+ The OIDs embedded in these containers are project-local UUID-derived OIDs under `2.25.*`. They are not registrations for interoperable standard key formats. Within the `pqc_container_*` format they are treated as part of pq_crypto's own serialized container schema, not as external interoperability identifiers.
40
+
41
+ Future releases may replace these project-local identifiers if pq_crypto adopts standardized external container formats. Persisted `pqc_container_*` blobs should therefore be treated as pq_crypto-local artifacts, not as a long-term interoperability format.
42
+
43
+ ## Memory wiping
44
+
45
+ `PQCrypto.secure_wipe` clears mutable Ruby strings in place. Ruby copies, GC behavior, and prior derived copies may still leave sensitive material elsewhere in process memory.
46
+
47
+ ## OpenSSL baseline
48
+
49
+ `pq_crypto` requires OpenSSL **3.0 or later**.
50
+
51
+ OpenSSL is used for conventional primitives and plumbing such as:
52
+ - `X25519`
53
+ - `HKDF-SHA256`
54
+
55
+ ## Threading
56
+
57
+ Concurrent read-only operations on primitive key objects are supported. Native calls copy Ruby string inputs before releasing the GVL, so normal concurrent use does not rely on Ruby string storage remaining pinned in place. Deterministic testing helpers remain test-only utilities and should not be treated as a general multi-threading contract.
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "mkmf"
5
+
6
+ $CFLAGS << " -std=c99 -Wall -Wextra -O2"
7
+ $CFLAGS << " -fstack-protector-strong -D_FORTIFY_SOURCE=2"
8
+ $CFLAGS << " -Wno-c23-extensions -Wno-strict-prototypes -Wno-pedantic"
9
+ $CFLAGS << " -Wno-unused-parameter -Wno-unused-function"
10
+
11
+ $LDFLAGS << " -Wl,-no_warn_duplicate_libraries" if RbConfig::CONFIG["host_os"] =~ /darwin/
12
+
13
+ USE_SYSTEM = arg_config("--use-system-libraries") || ENV["PQCRYPTO_USE_SYSTEM_LIBRARIES"]
14
+
15
+ SANITIZE = ENV["PQCRYPTO_SANITIZE"]
16
+
17
+ if SANITIZE && !SANITIZE.strip.empty?
18
+ sanitize = SANITIZE.strip
19
+ $CFLAGS << " -O1 -g -fno-omit-frame-pointer -fsanitize=#{sanitize}"
20
+ $LDFLAGS << " -fsanitize=#{sanitize}"
21
+ end
22
+
23
+ def configure_compiler_environment
24
+ return unless RUBY_PLATFORM.include?("darwin")
25
+
26
+ dir_config("homebrew", "/opt/homebrew")
27
+ $CPPFLAGS << " -I/opt/homebrew/include"
28
+ $LDFLAGS << " -L/opt/homebrew/lib"
29
+ end
30
+
31
+ def find_vendor_dir
32
+ candidates = [
33
+ File.join(__dir__, "vendor"),
34
+ File.expand_path("../../ext/pqcrypto/vendor", __dir__),
35
+ File.join(Dir.pwd, "ext", "pqcrypto", "vendor")
36
+ ]
37
+
38
+ dir = __dir__
39
+ 6.times do
40
+ candidates << File.join(dir, "ext", "pqcrypto", "vendor")
41
+ dir = File.dirname(dir)
42
+ end
43
+
44
+ candidates.find { |path| File.exist?(File.join(path, ".vendored")) }
45
+ &.then { |path| File.expand_path(path) }
46
+ end
47
+
48
+ def configure_openssl!
49
+ configure_compiler_environment
50
+
51
+ abort "OpenSSL libcrypto is required" unless have_library("crypto")
52
+ abort "OpenSSL libssl is required" unless have_library("ssl")
53
+ abort "openssl/evp.h is required" unless have_header("openssl/evp.h")
54
+ abort "openssl/rand.h is required" unless have_header("openssl/rand.h")
55
+ abort "openssl/kdf.h is required" unless have_header("openssl/kdf.h")
56
+
57
+ version_check = <<~SRC
58
+ #include <openssl/opensslv.h>
59
+ #if OPENSSL_VERSION_NUMBER < 0x30000000L
60
+ #error "OpenSSL 3.0 or later is required"
61
+ #endif
62
+ int main(void) { return 0; }
63
+ SRC
64
+
65
+ abort "OpenSSL 3.0 or later is required" unless try_compile(version_check)
66
+
67
+ $CFLAGS << " -DHAVE_OPENSSL_EVP_H -DHAVE_OPENSSL_RAND_H -DHAVE_OPENSSL_KDF_H"
68
+ end
69
+
70
+ def configure_pqclean(vendor_dir)
71
+ return nil unless vendor_dir
72
+
73
+ pqclean_dir = File.join(vendor_dir, "pqclean")
74
+ return nil unless Dir.exist?(pqclean_dir)
75
+
76
+ mlkem_dir = File.join(pqclean_dir, "crypto_kem", "ml-kem-768", "clean")
77
+ mldsa_dir = File.join(pqclean_dir, "crypto_sign", "ml-dsa-65", "clean")
78
+ common_dir = File.join(pqclean_dir, "common")
79
+
80
+ include_dirs = [mlkem_dir, mldsa_dir, common_dir]
81
+ return nil unless include_dirs.all? { |dir| Dir.exist?(dir) }
82
+
83
+ mlkem_sources = Dir.glob(File.join(mlkem_dir, "*.c")).sort
84
+ mldsa_sources = Dir.glob(File.join(mldsa_dir, "*.c")).sort
85
+ common_sources = %w[fips202.c sha2.c sp800-185.c randombytes.c].map { |name| File.join(common_dir, name) }
86
+
87
+ source_groups = [
88
+ ["pqclean_mlkem", mlkem_sources],
89
+ ["pqclean_mldsa", mldsa_sources],
90
+ ["pqclean_common", common_sources]
91
+ ]
92
+
93
+ return nil unless source_groups.all? { |_, sources| sources.all? { |path| File.exist?(path) } }
94
+
95
+ $CFLAGS << " -DHAVE_PQCLEAN -Wno-undef"
96
+ include_dirs.each { |dir| $CPPFLAGS << " -I#{dir}" }
97
+
98
+ {
99
+ include_dirs: include_dirs,
100
+ source_groups: source_groups
101
+ }
102
+ end
103
+
104
+ def inject_pqclean_sources!(pqclean_config)
105
+ return unless pqclean_config
106
+
107
+ makefile = File.read("Makefile")
108
+
109
+ vendor_objects = []
110
+ build_rules = []
111
+
112
+ pqclean_config[:source_groups].each do |prefix, sources|
113
+ sources.each do |source|
114
+ base = File.basename(source, ".c").tr("-", "_")
115
+ object = "#{prefix}_#{base}.o"
116
+ vendor_objects << object
117
+ build_rules << <<~RULE
118
+ #{object}: #{source}
119
+ $(ECHO) compiling #{source}
120
+ $(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
121
+ RULE
122
+ end
123
+ end
124
+
125
+ objects_line = makefile.lines.find { |line| line.start_with?("OBJS = ") }
126
+ raise "Could not find OBJS line in generated Makefile" unless objects_line
127
+
128
+ makefile.sub!(objects_line, objects_line.chomp + " #{vendor_objects.join(' ')}\n")
129
+
130
+ unless makefile.include?("# vendored pqclean objects")
131
+ rules_block = "\n# vendored pqclean objects\n" + build_rules.join("\n") + "\n"
132
+ anchor = "$(OBJS): $(HDRS) $(ruby_headers)\n"
133
+ raise "Could not find OBJS dependency anchor in generated Makefile" unless makefile.include?(anchor)
134
+
135
+ makefile.sub!(anchor, anchor + rules_block)
136
+ end
137
+
138
+ File.write("Makefile", makefile)
139
+ end
140
+
141
+ have_func("getrandom", "sys/random.h")
142
+ have_func("arc4random_buf", "stdlib.h")
143
+
144
+ vendor_dir = USE_SYSTEM ? nil : find_vendor_dir
145
+
146
+ puts
147
+ puts "=== PQCrypto build configuration ==="
148
+ configure_openssl!
149
+ pqclean_config = configure_pqclean(vendor_dir)
150
+ puts "OpenSSL: system"
151
+ abort "PQClean vendored sources are required. Run: bundle exec rake vendor" unless pqclean_config
152
+ puts "PQClean: vendored"
153
+ puts "Output: pqcrypto/pqcrypto_secure"
154
+ puts "===================================="
155
+
156
+ create_makefile("pqcrypto/pqcrypto_secure")
157
+ inject_pqclean_sources!(pqclean_config)
@@ -0,0 +1,51 @@
1
+ #ifndef MLDSA_API_H
2
+ #define MLDSA_API_H
3
+
4
+ #ifdef HAVE_PQCLEAN
5
+ #include <stddef.h>
6
+ #include <stdint.h>
7
+
8
+ #define PQCLEAN_MLDSA65_CLEAN_CRYPTO_ALGNAME "ML-DSA-65"
9
+
10
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign_keypair(uint8_t *pk, uint8_t *sk);
11
+
12
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign_signature_ctx(uint8_t *sig, size_t *siglen,
13
+ const uint8_t *m, size_t mlen,
14
+ const uint8_t *ctx, size_t ctxlen,
15
+ const uint8_t *sk);
16
+
17
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign_ctx(uint8_t *sm, size_t *smlen,
18
+ const uint8_t *m, size_t mlen,
19
+ const uint8_t *ctx, size_t ctxlen,
20
+ const uint8_t *sk);
21
+
22
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign_verify_ctx(const uint8_t *sig, size_t siglen,
23
+ const uint8_t *m, size_t mlen,
24
+ const uint8_t *ctx, size_t ctxlen,
25
+ const uint8_t *pk);
26
+
27
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign_open_ctx(uint8_t *m, size_t *mlen,
28
+ const uint8_t *sm, size_t smlen,
29
+ const uint8_t *ctx, size_t ctxlen,
30
+ const uint8_t *pk);
31
+
32
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign_signature(uint8_t *sig, size_t *siglen,
33
+ const uint8_t *m, size_t mlen,
34
+ const uint8_t *sk);
35
+
36
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign(uint8_t *sm, size_t *smlen,
37
+ const uint8_t *m, size_t mlen,
38
+ const uint8_t *sk);
39
+
40
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign_verify(const uint8_t *sig, size_t siglen,
41
+ const uint8_t *m, size_t mlen,
42
+ const uint8_t *pk);
43
+
44
+ int PQCLEAN_MLDSA65_CLEAN_crypto_sign_open(uint8_t *m, size_t *mlen,
45
+ const uint8_t *sm, size_t smlen,
46
+ const uint8_t *pk);
47
+
48
+
49
+ #endif
50
+
51
+ #endif
@@ -0,0 +1,21 @@
1
+ #ifndef MLKEM_API_H
2
+ #define MLKEM_API_H
3
+
4
+ #ifdef HAVE_PQCLEAN
5
+ #include <stdint.h>
6
+
7
+ #define PQCLEAN_MLKEM768_CLEAN_CRYPTO_SECRETKEYBYTES 2400
8
+ #define PQCLEAN_MLKEM768_CLEAN_CRYPTO_PUBLICKEYBYTES 1184
9
+ #define PQCLEAN_MLKEM768_CLEAN_CRYPTO_CIPHERTEXTBYTES 1088
10
+ #define PQCLEAN_MLKEM768_CLEAN_CRYPTO_BYTES 32
11
+ #define PQCLEAN_MLKEM768_CLEAN_CRYPTO_ALGNAME "ML-KEM-768"
12
+
13
+ int PQCLEAN_MLKEM768_CLEAN_crypto_kem_keypair(uint8_t *pk, uint8_t *sk);
14
+ int PQCLEAN_MLKEM768_CLEAN_crypto_kem_enc(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
15
+ int PQCLEAN_MLKEM768_CLEAN_crypto_kem_dec(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
16
+ int PQCLEAN_MLKEM768_CLEAN_crypto_kem_keypair_derand(uint8_t *pk, uint8_t *sk, const uint8_t *coins);
17
+ int PQCLEAN_MLKEM768_CLEAN_crypto_kem_enc_derand(uint8_t *ct, uint8_t *ss, const uint8_t *pk, const uint8_t *coins);
18
+
19
+ #endif
20
+
21
+ #endif