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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +37 -0
- data/CHANGELOG.md +29 -0
- data/GET_STARTED.md +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +135 -0
- data/SECURITY.md +57 -0
- data/ext/pqcrypto/extconf.rb +157 -0
- data/ext/pqcrypto/mldsa_api.h +51 -0
- data/ext/pqcrypto/mlkem_api.h +21 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +889 -0
- data/ext/pqcrypto/pqcrypto_secure.c +1178 -0
- data/ext/pqcrypto/pqcrypto_secure.h +135 -0
- data/ext/pqcrypto/vendor/.vendored +5 -0
- data/ext/pqcrypto/vendor/pqclean/common/aes.c +639 -0
- data/ext/pqcrypto/vendor/pqclean/common/aes.h +64 -0
- data/ext/pqcrypto/vendor/pqclean/common/compat.h +73 -0
- data/ext/pqcrypto/vendor/pqclean/common/crypto_declassify.h +7 -0
- data/ext/pqcrypto/vendor/pqclean/common/fips202.c +928 -0
- data/ext/pqcrypto/vendor/pqclean/common/fips202.h +166 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak2x/feat.S +168 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak2x/fips202x2.c +684 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak2x/fips202x2.h +60 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SIMD256.c +1028 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SnP.h +50 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-unrolling.macros +198 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile +8 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile.Microsoft_nmake +8 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/SIMD256-config.h +3 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/align.h +34 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/brg_endian.h +142 -0
- data/ext/pqcrypto/vendor/pqclean/common/nistseedexpander.c +101 -0
- data/ext/pqcrypto/vendor/pqclean/common/nistseedexpander.h +39 -0
- data/ext/pqcrypto/vendor/pqclean/common/randombytes.c +355 -0
- data/ext/pqcrypto/vendor/pqclean/common/randombytes.h +27 -0
- data/ext/pqcrypto/vendor/pqclean/common/sha2.c +769 -0
- data/ext/pqcrypto/vendor/pqclean/common/sha2.h +173 -0
- data/ext/pqcrypto/vendor/pqclean/common/sp800-185.c +156 -0
- data/ext/pqcrypto/vendor/pqclean/common/sp800-185.h +27 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/api.h +18 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.c +83 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.h +11 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/indcpa.c +327 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/indcpa.h +22 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/kem.c +164 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/kem.h +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/ntt.c +146 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/ntt.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/params.h +36 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/poly.c +299 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/poly.h +37 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/polyvec.c +188 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/polyvec.h +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/reduce.c +41 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/reduce.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/symmetric-shake.c +71 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/symmetric.h +30 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.c +67 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/api.h +50 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.c +98 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.h +10 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.c +261 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.h +31 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/params.h +44 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.c +799 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.h +52 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.c +415 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.h +65 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/reduce.c +69 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/reduce.h +17 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/rounding.c +92 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/rounding.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/sign.c +407 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/sign.h +47 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric-shake.c +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric.h +34 -0
- data/lib/pq_crypto/errors.rb +10 -0
- data/lib/pq_crypto/hybrid_kem.rb +106 -0
- data/lib/pq_crypto/kem.rb +199 -0
- data/lib/pq_crypto/serialization.rb +102 -0
- data/lib/pq_crypto/signature.rb +198 -0
- data/lib/pq_crypto/version.rb +5 -0
- data/lib/pq_crypto.rb +177 -0
- data/lib/pqcrypto.rb +3 -0
- data/script/vendor_libs.rb +199 -0
- 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
|