pq_crypto 0.3.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +27 -15
- data/GET_STARTED.md +3 -0
- data/README.md +15 -7
- data/SECURITY.md +33 -21
- data/ext/pqcrypto/extconf.rb +11 -1
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +4 -0
- data/ext/pqcrypto/pqcrypto_secure.c +101 -41
- data/ext/pqcrypto/pqcrypto_secure.h +10 -2
- data/lib/pq_crypto/kem.rb +7 -3
- data/lib/pq_crypto/serialization.rb +1 -1
- data/lib/pq_crypto/signature.rb +3 -3
- data/lib/pq_crypto/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 534c420f62323e9ddb49b48acdfa7af869731e3273bda56637d6bfee753de8d7
|
|
4
|
+
data.tar.gz: f003494b4d33702a1b718dc6ded7a9f0612b315e41f0d739a5b2fe3ebba9d1d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a039325a13cf8c074741fb70edc1e66f4e4939727b4c1369665794bc6ed71ef1ed2ef50d9debd9064d850e4856fac166f325889a9df4bfd3d8849a2d7a918e9
|
|
7
|
+
data.tar.gz: 304888656181149eed84eca063e24eeb6c862819f6c54022e77b287ca4030b8602e9f61eb17809ad86ca0eea84796c6275a2afa12018ffdeaafc489c15c8f24a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] — 2026-04-24
|
|
4
|
+
|
|
5
|
+
### Fixed — X-Wing draft-10 compatibility
|
|
6
|
+
|
|
7
|
+
- Changed `:ml_kem_768_x25519_xwing` secret keys to the draft-10 32-byte
|
|
8
|
+
X-Wing decapsulation seed and derive ML-KEM/X25519 private material with
|
|
9
|
+
SHAKE256 during key generation and decapsulation.
|
|
10
|
+
- Corrected the X-Wing combiner transcript to
|
|
11
|
+
`ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || XWingLabel )`.
|
|
12
|
+
- Updated the hybrid serialization OID to the X-Wing draft OID
|
|
13
|
+
`1.3.6.1.4.1.62253.25722`.
|
|
14
|
+
- Redacted key `inspect` output, removed public secret-key fingerprints,
|
|
15
|
+
improved native extension load diagnostics, switched the extension build
|
|
16
|
+
flag to C11, and aligned docs with the implementation.
|
|
17
|
+
|
|
3
18
|
## [0.3.0] — 2026-04-24
|
|
4
19
|
|
|
5
20
|
**Breaking release.** Hybrid KEM keys, ciphertexts, and `pqc_container_*`
|
|
@@ -9,17 +24,14 @@ and ML-DSA-65 material is unaffected.
|
|
|
9
24
|
### Changed — hybrid KEM (breaking)
|
|
10
25
|
|
|
11
26
|
- Replaced the 0.2.0 ad-hoc `HKDF-SHA256`-with-double-transcript combiner
|
|
12
|
-
with
|
|
13
|
-
|
|
14
|
-
`ss = SHA3-256( XWingLabel || ss_M || ss_X || ct_X || pk_X )`, where
|
|
15
|
-
`XWingLabel` is the 6-byte ASCII string `\.//^\`.
|
|
27
|
+
with a SHA3-256 X-Wing-inspired combiner. This was later corrected in
|
|
28
|
+
`0.3.1` to match draft-10 transcript order and 32-byte secret keys.
|
|
16
29
|
- Renamed the hybrid algorithm symbol
|
|
17
30
|
`:ml_kem_768_x25519_hkdf_sha256` → `:ml_kem_768_x25519_xwing`.
|
|
18
31
|
- Retired the 0.2.0 project-local hybrid OID
|
|
19
|
-
(`2.25.260242945110721168101139140490528778800`).
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
hybrid container now fails fast with `SerializationError`.
|
|
32
|
+
(`2.25.260242945110721168101139140490528778800`). 0.3.0 used
|
|
33
|
+
`2.25.318532651283923671095712569430174917109`; this was later replaced
|
|
34
|
+
in `0.3.1` by the X-Wing draft OID.
|
|
23
35
|
|
|
24
36
|
### Changed — native code hygiene
|
|
25
37
|
|
|
@@ -39,9 +51,8 @@ and ML-DSA-65 material is unaffected.
|
|
|
39
51
|
`hybrid_public_key_t`, `hybrid_secret_key_t`, and
|
|
40
52
|
`hybrid_ciphertext_t` so any future change that introduces padding
|
|
41
53
|
fails at compile time rather than silently shifting byte offsets.
|
|
42
|
-
- Migrated PEM codec
|
|
43
|
-
|
|
44
|
-
invalid base64 characters rather than treating them as zeros.
|
|
54
|
+
- Migrated PEM codec to OpenSSL `BIO_f_base64` with stricter PEM
|
|
55
|
+
header/footer framing and trailing-garbage checks.
|
|
45
56
|
- Deleted the entire internal HKDF and SHA-256 helper paths that 0.2.0
|
|
46
57
|
used for its combiner; the X-Wing combiner is a single SHA3-256
|
|
47
58
|
invocation through `EVP_DigestUpdate`.
|
|
@@ -61,9 +72,9 @@ and ML-DSA-65 material is unaffected.
|
|
|
61
72
|
`CRYPTO_memcmp` through a new `PQCrypto.ct_equals` native helper, so
|
|
62
73
|
key equality checks no longer leak timing information about a
|
|
63
74
|
prefix-match.
|
|
64
|
-
- `
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
- `PublicKey#hash` and `SecretKey#hash` now hash a SHA-256 fingerprint
|
|
76
|
+
of the bytes instead of the raw bytes. The public secret-key fingerprint
|
|
77
|
+
method is removed in `0.3.1` to reduce accidental logging risk.
|
|
67
78
|
- Native entrypoints and their `native_*` aliases are installed once via
|
|
68
79
|
the new `PQCrypto::NativeBindings` module instead of the ad-hoc
|
|
69
80
|
`unless method_defined?` guards on the singleton.
|
|
@@ -72,7 +83,8 @@ and ML-DSA-65 material is unaffected.
|
|
|
72
83
|
|
|
73
84
|
### Changed — packaging
|
|
74
85
|
|
|
75
|
-
- `required_ruby_version` from `">= 3.4.0.a"` to
|
|
86
|
+
- Intended to change `required_ruby_version` from `">= 3.4.0.a"` to
|
|
87
|
+
`">= 3.4"`; the gemspec is aligned in `0.3.1`.
|
|
76
88
|
- Version bumped to `0.3.0`.
|
|
77
89
|
- `VerificationError` class is still defined (and still raised by
|
|
78
90
|
`verify!`) for backward compatibility, but the native `verify`
|
data/GET_STARTED.md
CHANGED
|
@@ -43,6 +43,9 @@ result = hybrid.public_key.encapsulate
|
|
|
43
43
|
shared_secret = hybrid.secret_key.decapsulate(result.ciphertext)
|
|
44
44
|
```
|
|
45
45
|
|
|
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.
|
|
48
|
+
|
|
46
49
|
The hybrid mode follows `draft-connolly-cfrg-xwing-kem`. See
|
|
47
50
|
`SECURITY.md` for audit status.
|
|
48
51
|
|
data/README.md
CHANGED
|
@@ -13,8 +13,8 @@ It exposes three public building blocks:
|
|
|
13
13
|
The gem is backed by vendored `PQClean` sources for `ML-KEM-768` /
|
|
14
14
|
`ML-DSA-65` and by OpenSSL for `X25519` and `SHA3-256`. Every piece of
|
|
15
15
|
conventional-crypto functionality goes through standard library calls
|
|
16
|
-
(`EVP_*`, `RAND_bytes`, `CRYPTO_memcmp`,
|
|
17
|
-
|
|
16
|
+
(`EVP_*`, `RAND_bytes`, `CRYPTO_memcmp`, `BIO_f_base64`) — nothing
|
|
17
|
+
roll-your-own where a library primitive exists.
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
|
@@ -42,7 +42,7 @@ bundle exec rake compile
|
|
|
42
42
|
|
|
43
43
|
- Ruby 3.4.x
|
|
44
44
|
- a C toolchain with C11 support (for `_Static_assert` / `_Thread_local`)
|
|
45
|
-
- OpenSSL **3.0 or later** with SHA3-256 available (default provider)
|
|
45
|
+
- OpenSSL **3.0 or later** with SHA3-256 and SHAKE256 available (default provider)
|
|
46
46
|
|
|
47
47
|
## Async / Fiber scheduler support
|
|
48
48
|
|
|
@@ -120,14 +120,16 @@ result = keypair.public_key.encapsulate
|
|
|
120
120
|
shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
The
|
|
123
|
+
The implementation follows draft-10 key expansion: the X-Wing secret
|
|
124
|
+
decapsulation key is a 32-byte seed expanded with SHAKE256 into ML-KEM
|
|
125
|
+
and X25519 private material. The combiner is exactly:
|
|
124
126
|
|
|
125
127
|
```
|
|
126
|
-
ss = SHA3-256(
|
|
128
|
+
ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || "\.//^\" )
|
|
127
129
|
```
|
|
128
130
|
|
|
129
|
-
as specified by `draft-connolly-cfrg-xwing-kem`. See `SECURITY.md`
|
|
130
|
-
audit status and interoperability caveats.
|
|
131
|
+
as specified by `draft-connolly-cfrg-xwing-kem-10`. See `SECURITY.md`
|
|
132
|
+
for audit status and interoperability caveats.
|
|
131
133
|
|
|
132
134
|
## Serialization
|
|
133
135
|
|
|
@@ -172,6 +174,12 @@ key.wipe! # scrub the key's internal copy
|
|
|
172
174
|
`CRYPTO_memcmp` through a `PQCrypto.ct_equals` helper so comparisons
|
|
173
175
|
do not leak timing information about a prefix match.
|
|
174
176
|
|
|
177
|
+
Secret key `inspect` output is intentionally redacted and secret key
|
|
178
|
+
objects do not expose a public `fingerprint` method. `wipe!` remains
|
|
179
|
+
best-effort only: it clears the current Ruby string buffer owned by the
|
|
180
|
+
key object, not every possible copy made by Ruby, OpenSSL, serialization,
|
|
181
|
+
logging, or the garbage collector.
|
|
182
|
+
|
|
175
183
|
## Introspection
|
|
176
184
|
|
|
177
185
|
```ruby
|
data/SECURITY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Scope of the public API
|
|
4
4
|
|
|
5
|
-
`pq_crypto`
|
|
5
|
+
`pq_crypto` exposes a primitive-first public surface:
|
|
6
6
|
|
|
7
7
|
- `PQCrypto::KEM` (`ML-KEM-768`)
|
|
8
8
|
- `PQCrypto::Signature` (`ML-DSA-65`)
|
|
@@ -29,23 +29,25 @@ gem.
|
|
|
29
29
|
### HybridKEM
|
|
30
30
|
|
|
31
31
|
`PQCrypto::HybridKEM` implements the **X-Wing** construction from
|
|
32
|
-
[`draft-connolly-cfrg-xwing-kem`](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/)
|
|
32
|
+
[`draft-connolly-cfrg-xwing-kem-10`](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/).
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
The X-Wing secret decapsulation key is a 32-byte seed. It is expanded
|
|
35
|
+
with SHAKE256 into the ML-KEM-768 and X25519 private material used
|
|
36
|
+
internally for decapsulation. The public key and ciphertext are the
|
|
37
|
+
fixed-length concatenations specified by the draft.
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || XWingLabel )
|
|
40
|
+
|
|
41
|
+
where `XWingLabel = "\.//^\"` (6 ASCII bytes).
|
|
39
42
|
|
|
40
43
|
X-Wing as specified has a proof of classical IND-CCA security under
|
|
41
44
|
the strong Diffie-Hellman assumption for X25519 (in the ROM), and
|
|
42
45
|
post-quantum IND-CCA security in the standard model assuming ML-KEM-768
|
|
43
46
|
is IND-CCA secure and SHA3-256 behaves as a PRF.
|
|
44
47
|
|
|
45
|
-
This gem is
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
verified against the reference implementation before relying on it.
|
|
48
|
+
This gem is intended to match the X-Wing draft as of version 10. External
|
|
49
|
+
interoperability should still be verified against the reference
|
|
50
|
+
implementation before relying on it.
|
|
49
51
|
|
|
50
52
|
### Deterministic test hooks
|
|
51
53
|
|
|
@@ -68,16 +70,16 @@ They are:
|
|
|
68
70
|
- not real PKCS#8
|
|
69
71
|
- not advertised as interoperable with OpenSSL, Go, Java, or PKI tooling
|
|
70
72
|
|
|
71
|
-
The
|
|
72
|
-
OIDs under `2.25.*`.
|
|
73
|
-
|
|
73
|
+
The `pqc_container_*` envelope itself is project-specific. ML-KEM and
|
|
74
|
+
ML-DSA currently use project-local UUID-derived OIDs under `2.25.*`.
|
|
75
|
+
Hybrid X-Wing uses the draft X-Wing OID `1.3.6.1.4.1.62253.25722`.
|
|
74
76
|
|
|
75
77
|
The hybrid OID used by 0.2.0
|
|
76
|
-
(`2.25.260242945110721168101139140490528778800`) is retired
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
(`2.25.260242945110721168101139140490528778800`) is retired. The
|
|
79
|
+
intermediate 0.3.0 project-local hybrid OID
|
|
80
|
+
(`2.25.318532651283923671095712569430174917109`) is also retired in
|
|
81
|
+
favor of the draft X-Wing OID. Older hybrid containers are rejected at
|
|
82
|
+
decode time.
|
|
81
83
|
|
|
82
84
|
## Memory wiping
|
|
83
85
|
|
|
@@ -98,9 +100,19 @@ OpenSSL is used for:
|
|
|
98
100
|
- `SHA3-256` (X-Wing combiner, via `EVP_sha3_256`)
|
|
99
101
|
- `RAND_bytes` (production entropy source for `randombytes()`)
|
|
100
102
|
- `CRYPTO_memcmp` (constant-time comparison used by `PQCrypto.ct_equals`)
|
|
101
|
-
- Base64 encode/decode for PEM via
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
- Base64 encode/decode for PEM via OpenSSL `BIO_f_base64`, with strict
|
|
104
|
+
header/footer framing and trailing-garbage checks.
|
|
105
|
+
|
|
106
|
+
## Secret key display and wiping
|
|
107
|
+
|
|
108
|
+
Secret key objects redact `inspect` output and intentionally do not expose
|
|
109
|
+
a public `fingerprint` method. This avoids accidental logging of raw secret
|
|
110
|
+
bytes or stable secret-derived identifiers.
|
|
111
|
+
|
|
112
|
+
`wipe!` is best-effort only. It wipes the current Ruby string buffer held
|
|
113
|
+
by the key object; it cannot guarantee erasure of copies made by Ruby,
|
|
114
|
+
OpenSSL, native wrapper buffers, serialization, logging, crash dumps, or
|
|
115
|
+
the garbage collector.
|
|
104
116
|
|
|
105
117
|
## Threading
|
|
106
118
|
|
data/ext/pqcrypto/extconf.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "mkmf"
|
|
5
5
|
|
|
6
|
-
$CFLAGS << " -std=
|
|
6
|
+
$CFLAGS << " -std=c11 -Wall -Wextra -O2"
|
|
7
7
|
$CFLAGS << " -fstack-protector-strong -D_FORTIFY_SOURCE=2"
|
|
8
8
|
VENDOR_ONLY_CFLAGS = "-Wno-unused-parameter -Wno-unused-function -Wno-strict-prototypes -Wno-pedantic -Wno-c23-extensions -Wno-undef"
|
|
9
9
|
|
|
@@ -15,6 +15,7 @@ SANITIZE = ENV["PQCRYPTO_SANITIZE"]
|
|
|
15
15
|
|
|
16
16
|
if SANITIZE && !SANITIZE.strip.empty?
|
|
17
17
|
sanitize = SANITIZE.strip
|
|
18
|
+
$CFLAGS.gsub!(/\s-D_FORTIFY_SOURCE=\d+/, "")
|
|
18
19
|
$CFLAGS << " -O1 -g -fno-omit-frame-pointer -fsanitize=#{sanitize}"
|
|
19
20
|
$LDFLAGS << " -fsanitize=#{sanitize}"
|
|
20
21
|
end
|
|
@@ -72,6 +73,15 @@ def configure_openssl!
|
|
|
72
73
|
SRC
|
|
73
74
|
abort "OpenSSL SHA3-256 is required (X-Wing combiner)" unless try_compile(sha3_check)
|
|
74
75
|
|
|
76
|
+
shake_check = <<~SRC
|
|
77
|
+
#include <openssl/evp.h>
|
|
78
|
+
int main(void) {
|
|
79
|
+
const EVP_MD *md = EVP_shake256();
|
|
80
|
+
return md == NULL ? 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
SRC
|
|
83
|
+
abort "OpenSSL SHAKE256 is required (X-Wing key expansion)" unless try_compile(shake_check)
|
|
84
|
+
|
|
75
85
|
$CFLAGS << " -DHAVE_OPENSSL_EVP_H -DHAVE_OPENSSL_RAND_H"
|
|
76
86
|
end
|
|
77
87
|
|
|
@@ -99,6 +99,32 @@ cleanup:
|
|
|
99
99
|
return ret;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
static int x25519_public_from_private(uint8_t *pk, const uint8_t *sk) {
|
|
103
|
+
EVP_PKEY *pkey = NULL;
|
|
104
|
+
size_t pklen = X25519_PUBLICKEYBYTES;
|
|
105
|
+
int ret = PQ_ERROR_KEYPAIR;
|
|
106
|
+
|
|
107
|
+
if (!pk || !sk) {
|
|
108
|
+
return PQ_ERROR_BUFFER;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk, X25519_SECRETKEYBYTES);
|
|
112
|
+
if (!pkey)
|
|
113
|
+
goto cleanup;
|
|
114
|
+
|
|
115
|
+
if (EVP_PKEY_get_raw_public_key(pkey, pk, &pklen) <= 0)
|
|
116
|
+
goto cleanup;
|
|
117
|
+
if (pklen != X25519_PUBLICKEYBYTES)
|
|
118
|
+
goto cleanup;
|
|
119
|
+
|
|
120
|
+
ret = PQ_SUCCESS;
|
|
121
|
+
|
|
122
|
+
cleanup:
|
|
123
|
+
if (pkey)
|
|
124
|
+
EVP_PKEY_free(pkey);
|
|
125
|
+
return ret;
|
|
126
|
+
}
|
|
127
|
+
|
|
102
128
|
static int x25519_shared_secret(uint8_t *shared, const uint8_t *their_pk, const uint8_t *my_sk) {
|
|
103
129
|
EVP_PKEY_CTX *ctx = NULL;
|
|
104
130
|
EVP_PKEY *pkey = NULL;
|
|
@@ -159,8 +185,6 @@ static int xwing_combiner(uint8_t shared_secret[HYBRID_SHAREDSECRETBYTES],
|
|
|
159
185
|
|
|
160
186
|
if (EVP_DigestInit_ex(ctx, EVP_sha3_256(), NULL) != 1)
|
|
161
187
|
goto cleanup;
|
|
162
|
-
if (EVP_DigestUpdate(ctx, XWING_LABEL, sizeof(XWING_LABEL)) != 1)
|
|
163
|
-
goto cleanup;
|
|
164
188
|
if (EVP_DigestUpdate(ctx, ss_M, MLKEM_SHAREDSECRETBYTES) != 1)
|
|
165
189
|
goto cleanup;
|
|
166
190
|
if (EVP_DigestUpdate(ctx, ss_X, X25519_SHAREDSECRETBYTES) != 1)
|
|
@@ -169,6 +193,8 @@ static int xwing_combiner(uint8_t shared_secret[HYBRID_SHAREDSECRETBYTES],
|
|
|
169
193
|
goto cleanup;
|
|
170
194
|
if (EVP_DigestUpdate(ctx, pk_X, X25519_PUBLICKEYBYTES) != 1)
|
|
171
195
|
goto cleanup;
|
|
196
|
+
if (EVP_DigestUpdate(ctx, XWING_LABEL, sizeof(XWING_LABEL)) != 1)
|
|
197
|
+
goto cleanup;
|
|
172
198
|
if (EVP_DigestFinal_ex(ctx, shared_secret, &out_len) != 1)
|
|
173
199
|
goto cleanup;
|
|
174
200
|
if (out_len != HYBRID_SHAREDSECRETBYTES)
|
|
@@ -181,6 +207,55 @@ cleanup:
|
|
|
181
207
|
return ret;
|
|
182
208
|
}
|
|
183
209
|
|
|
210
|
+
static int xwing_expand_secret_key(hybrid_expanded_secret_key_t *expanded_key,
|
|
211
|
+
const uint8_t seed[HYBRID_SECRETKEYBYTES]) {
|
|
212
|
+
EVP_MD_CTX *ctx = NULL;
|
|
213
|
+
uint8_t expanded[XWING_EXPANDEDBYTES];
|
|
214
|
+
int ret = PQ_ERROR_OPENSSL;
|
|
215
|
+
|
|
216
|
+
if (!expanded_key || !seed) {
|
|
217
|
+
return PQ_ERROR_BUFFER;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
memset(expanded_key, 0, sizeof(*expanded_key));
|
|
221
|
+
memset(expanded, 0, sizeof(expanded));
|
|
222
|
+
|
|
223
|
+
ctx = EVP_MD_CTX_new();
|
|
224
|
+
if (!ctx)
|
|
225
|
+
goto cleanup;
|
|
226
|
+
|
|
227
|
+
if (EVP_DigestInit_ex(ctx, EVP_shake256(), NULL) != 1)
|
|
228
|
+
goto cleanup;
|
|
229
|
+
if (EVP_DigestUpdate(ctx, seed, HYBRID_SECRETKEYBYTES) != 1)
|
|
230
|
+
goto cleanup;
|
|
231
|
+
if (EVP_DigestFinalXOF(ctx, expanded, sizeof(expanded)) != 1)
|
|
232
|
+
goto cleanup;
|
|
233
|
+
|
|
234
|
+
ret = PQCLEAN_MLKEM768_CLEAN_crypto_kem_keypair_derand(expanded_key->mlkem_pk,
|
|
235
|
+
expanded_key->mlkem_sk, expanded);
|
|
236
|
+
if (ret != 0) {
|
|
237
|
+
ret = PQ_ERROR_KEYPAIR;
|
|
238
|
+
goto cleanup;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
memcpy(expanded_key->x25519_sk, expanded + 64, X25519_SECRETKEYBYTES);
|
|
242
|
+
ret = x25519_public_from_private(expanded_key->x25519_pk, expanded_key->x25519_sk);
|
|
243
|
+
if (ret != PQ_SUCCESS) {
|
|
244
|
+
goto cleanup;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
ret = PQ_SUCCESS;
|
|
248
|
+
|
|
249
|
+
cleanup:
|
|
250
|
+
if (ctx)
|
|
251
|
+
EVP_MD_CTX_free(ctx);
|
|
252
|
+
pq_secure_wipe(expanded, sizeof(expanded));
|
|
253
|
+
if (ret != PQ_SUCCESS && expanded_key) {
|
|
254
|
+
pq_secure_wipe(expanded_key, sizeof(*expanded_key));
|
|
255
|
+
}
|
|
256
|
+
return ret;
|
|
257
|
+
}
|
|
258
|
+
|
|
184
259
|
int pq_mlkem_keypair(uint8_t *pk, uint8_t *sk) {
|
|
185
260
|
return PQCLEAN_MLKEM768_CLEAN_crypto_kem_keypair(pk, sk) == 0 ? PQ_SUCCESS : PQ_ERROR_KEYPAIR;
|
|
186
261
|
}
|
|
@@ -278,35 +353,36 @@ int pq_testing_mldsa_sign_from_seed(uint8_t *signature, size_t *signature_len,
|
|
|
278
353
|
|
|
279
354
|
int pq_hybrid_kem_keypair(uint8_t *public_key, uint8_t *secret_key) {
|
|
280
355
|
hybrid_public_key_t pk;
|
|
281
|
-
|
|
282
|
-
|
|
356
|
+
hybrid_expanded_secret_key_t expanded;
|
|
357
|
+
uint8_t seed[HYBRID_SECRETKEYBYTES];
|
|
358
|
+
int ret = PQ_SUCCESS;
|
|
283
359
|
|
|
284
360
|
if (!public_key || !secret_key) {
|
|
285
361
|
return PQ_ERROR_BUFFER;
|
|
286
362
|
}
|
|
287
363
|
|
|
288
364
|
memset(&pk, 0, sizeof(pk));
|
|
289
|
-
memset(&
|
|
365
|
+
memset(&expanded, 0, sizeof(expanded));
|
|
366
|
+
memset(seed, 0, sizeof(seed));
|
|
290
367
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
: PQ_ERROR_KEYPAIR;
|
|
294
|
-
if (ret != PQ_SUCCESS) {
|
|
368
|
+
if (RAND_bytes(seed, sizeof(seed)) != 1) {
|
|
369
|
+
ret = PQ_ERROR_RANDOM;
|
|
295
370
|
goto cleanup;
|
|
296
371
|
}
|
|
297
372
|
|
|
298
|
-
ret =
|
|
373
|
+
ret = xwing_expand_secret_key(&expanded, seed);
|
|
299
374
|
if (ret != PQ_SUCCESS) {
|
|
300
375
|
goto cleanup;
|
|
301
376
|
}
|
|
302
377
|
|
|
378
|
+
memcpy(pk.mlkem_pk, expanded.mlkem_pk, MLKEM_PUBLICKEYBYTES);
|
|
379
|
+
memcpy(pk.x25519_pk, expanded.x25519_pk, X25519_PUBLICKEYBYTES);
|
|
303
380
|
memcpy(public_key, &pk, HYBRID_PUBLICKEYBYTES);
|
|
304
|
-
memcpy(secret_key,
|
|
381
|
+
memcpy(secret_key, seed, HYBRID_SECRETKEYBYTES);
|
|
305
382
|
|
|
306
383
|
cleanup:
|
|
307
|
-
pq_secure_wipe(
|
|
308
|
-
|
|
309
|
-
pq_secure_wipe(&pk, sizeof(pk));
|
|
384
|
+
pq_secure_wipe(seed, sizeof(seed));
|
|
385
|
+
pq_secure_wipe(&expanded, sizeof(expanded));
|
|
310
386
|
return ret;
|
|
311
387
|
}
|
|
312
388
|
|
|
@@ -357,20 +433,15 @@ cleanup:
|
|
|
357
433
|
pq_secure_wipe(mlkem_ss, sizeof(mlkem_ss));
|
|
358
434
|
pq_secure_wipe(x25519_ss, sizeof(x25519_ss));
|
|
359
435
|
pq_secure_wipe(x25519_ephemeral_sk, sizeof(x25519_ephemeral_sk));
|
|
360
|
-
pq_secure_wipe(&pk, sizeof(pk));
|
|
361
|
-
pq_secure_wipe(&ct, sizeof(ct));
|
|
362
436
|
return ret;
|
|
363
437
|
}
|
|
364
438
|
|
|
365
439
|
int pq_hybrid_kem_decapsulate(uint8_t *shared_secret, const uint8_t *ciphertext,
|
|
366
440
|
const uint8_t *secret_key) {
|
|
367
441
|
hybrid_ciphertext_t ct;
|
|
368
|
-
|
|
369
|
-
uint8_t recipient_x25519_pk[X25519_PUBLICKEYBYTES];
|
|
442
|
+
hybrid_expanded_secret_key_t expanded;
|
|
370
443
|
uint8_t mlkem_ss[MLKEM_SHAREDSECRETBYTES];
|
|
371
444
|
uint8_t x25519_ss[X25519_SHAREDSECRETBYTES];
|
|
372
|
-
EVP_PKEY *pkey = NULL;
|
|
373
|
-
size_t pklen = X25519_PUBLICKEYBYTES;
|
|
374
445
|
int ret = PQ_SUCCESS;
|
|
375
446
|
|
|
376
447
|
if (!shared_secret || !ciphertext || !secret_key) {
|
|
@@ -378,44 +449,34 @@ int pq_hybrid_kem_decapsulate(uint8_t *shared_secret, const uint8_t *ciphertext,
|
|
|
378
449
|
}
|
|
379
450
|
|
|
380
451
|
memcpy(&ct, ciphertext, HYBRID_CIPHERTEXTBYTES);
|
|
381
|
-
|
|
382
|
-
memset(recipient_x25519_pk, 0, sizeof(recipient_x25519_pk));
|
|
452
|
+
memset(&expanded, 0, sizeof(expanded));
|
|
383
453
|
memset(mlkem_ss, 0, sizeof(mlkem_ss));
|
|
384
454
|
memset(x25519_ss, 0, sizeof(x25519_ss));
|
|
385
455
|
|
|
386
|
-
|
|
387
|
-
ret = PQ_ERROR_DECAPSULATE;
|
|
388
|
-
goto cleanup;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
ret = x25519_shared_secret(x25519_ss, ct.x25519_ephemeral, sk.x25519_sk);
|
|
456
|
+
ret = xwing_expand_secret_key(&expanded, secret_key);
|
|
392
457
|
if (ret != PQ_SUCCESS) {
|
|
393
458
|
ret = PQ_ERROR_DECAPSULATE;
|
|
394
459
|
goto cleanup;
|
|
395
460
|
}
|
|
396
461
|
|
|
397
|
-
|
|
398
|
-
if (!pkey) {
|
|
462
|
+
if (PQCLEAN_MLKEM768_CLEAN_crypto_kem_dec(mlkem_ss, ct.mlkem_ct, expanded.mlkem_sk) != 0) {
|
|
399
463
|
ret = PQ_ERROR_DECAPSULATE;
|
|
400
464
|
goto cleanup;
|
|
401
465
|
}
|
|
402
|
-
|
|
403
|
-
|
|
466
|
+
|
|
467
|
+
ret = x25519_shared_secret(x25519_ss, ct.x25519_ephemeral, expanded.x25519_sk);
|
|
468
|
+
if (ret != PQ_SUCCESS) {
|
|
404
469
|
ret = PQ_ERROR_DECAPSULATE;
|
|
405
470
|
goto cleanup;
|
|
406
471
|
}
|
|
407
472
|
|
|
408
|
-
ret =
|
|
409
|
-
|
|
473
|
+
ret =
|
|
474
|
+
xwing_combiner(shared_secret, mlkem_ss, x25519_ss, ct.x25519_ephemeral, expanded.x25519_pk);
|
|
410
475
|
|
|
411
476
|
cleanup:
|
|
412
|
-
if (pkey)
|
|
413
|
-
EVP_PKEY_free(pkey);
|
|
414
|
-
pq_secure_wipe(recipient_x25519_pk, sizeof(recipient_x25519_pk));
|
|
415
477
|
pq_secure_wipe(mlkem_ss, sizeof(mlkem_ss));
|
|
416
478
|
pq_secure_wipe(x25519_ss, sizeof(x25519_ss));
|
|
417
|
-
pq_secure_wipe(&
|
|
418
|
-
pq_secure_wipe(&sk, sizeof(sk));
|
|
479
|
+
pq_secure_wipe(&expanded, sizeof(expanded));
|
|
419
480
|
return ret;
|
|
420
481
|
}
|
|
421
482
|
|
|
@@ -429,8 +490,7 @@ cleanup:
|
|
|
429
490
|
|
|
430
491
|
static const char PQC_OID_ML_KEM_768[] = "2.25.186599352125448088867056807454444238446";
|
|
431
492
|
|
|
432
|
-
static const char PQC_OID_ML_KEM_768_X25519_XWING[] =
|
|
433
|
-
"2.25.318532651283923671095712569430174917109";
|
|
493
|
+
static const char PQC_OID_ML_KEM_768_X25519_XWING[] = "1.3.6.1.4.1.62253.25722";
|
|
434
494
|
static const char PQC_OID_ML_DSA_65[] = "2.25.305232938483772195555080795650659207792";
|
|
435
495
|
static const char PQC_PUBLIC_KEY_PEM_LABEL[] = "PQC PUBLIC KEY CONTAINER";
|
|
436
496
|
static const char PQC_PRIVATE_KEY_PEM_LABEL[] = "PQC PRIVATE KEY CONTAINER";
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
#define X25519_PUBLICKEYBYTES 32
|
|
24
24
|
#define X25519_SECRETKEYBYTES 32
|
|
25
25
|
#define X25519_SHAREDSECRETBYTES 32
|
|
26
|
+
#define XWING_SEEDBYTES 32
|
|
27
|
+
#define XWING_EXPANDEDBYTES 96
|
|
26
28
|
|
|
27
29
|
#define HYBRID_PUBLICKEYBYTES (MLKEM_PUBLICKEYBYTES + X25519_PUBLICKEYBYTES)
|
|
28
|
-
#define HYBRID_SECRETKEYBYTES
|
|
30
|
+
#define HYBRID_SECRETKEYBYTES XWING_SEEDBYTES
|
|
29
31
|
#define HYBRID_CIPHERTEXTBYTES (MLKEM_CIPHERTEXTBYTES + X25519_PUBLICKEYBYTES)
|
|
30
32
|
#define HYBRID_SHAREDSECRETBYTES 32
|
|
31
33
|
|
|
@@ -48,10 +50,16 @@ typedef struct {
|
|
|
48
50
|
uint8_t x25519_pk[X25519_PUBLICKEYBYTES];
|
|
49
51
|
} hybrid_public_key_t;
|
|
50
52
|
|
|
53
|
+
typedef struct {
|
|
54
|
+
uint8_t seed[XWING_SEEDBYTES];
|
|
55
|
+
} hybrid_secret_key_t;
|
|
56
|
+
|
|
51
57
|
typedef struct {
|
|
52
58
|
uint8_t mlkem_sk[MLKEM_SECRETKEYBYTES];
|
|
53
59
|
uint8_t x25519_sk[X25519_SECRETKEYBYTES];
|
|
54
|
-
|
|
60
|
+
uint8_t mlkem_pk[MLKEM_PUBLICKEYBYTES];
|
|
61
|
+
uint8_t x25519_pk[X25519_PUBLICKEYBYTES];
|
|
62
|
+
} hybrid_expanded_secret_key_t;
|
|
55
63
|
|
|
56
64
|
typedef struct {
|
|
57
65
|
uint8_t mlkem_ct[MLKEM_CIPHERTEXTBYTES];
|
data/lib/pq_crypto/kem.rb
CHANGED
|
@@ -184,11 +184,11 @@ module PQCrypto
|
|
|
184
184
|
alias eql? ==
|
|
185
185
|
|
|
186
186
|
def hash
|
|
187
|
-
|
|
187
|
+
object_id.hash
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
-
def
|
|
191
|
-
|
|
190
|
+
def inspect
|
|
191
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} algorithm=#{algorithm.inspect}>"
|
|
192
192
|
end
|
|
193
193
|
|
|
194
194
|
private
|
|
@@ -206,6 +206,10 @@ module PQCrypto
|
|
|
206
206
|
@ciphertext = String(ciphertext).b
|
|
207
207
|
@shared_secret = String(shared_secret).b
|
|
208
208
|
end
|
|
209
|
+
|
|
210
|
+
def inspect
|
|
211
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} ciphertext_bytes=#{@ciphertext.bytesize} shared_secret_bytes=#{@shared_secret.bytesize}>"
|
|
212
|
+
end
|
|
209
213
|
end
|
|
210
214
|
end
|
|
211
215
|
end
|
data/lib/pq_crypto/signature.rb
CHANGED
|
@@ -188,11 +188,11 @@ module PQCrypto
|
|
|
188
188
|
alias eql? ==
|
|
189
189
|
|
|
190
190
|
def hash
|
|
191
|
-
|
|
191
|
+
object_id.hash
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
-
def
|
|
195
|
-
|
|
194
|
+
def inspect
|
|
195
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} algorithm=#{algorithm.inspect}>"
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
private
|
data/lib/pq_crypto/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pq_crypto
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Haydarov
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rake
|
|
@@ -163,14 +163,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
163
163
|
requirements:
|
|
164
164
|
- - ">="
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
|
-
version: 3.4.0
|
|
166
|
+
version: 3.4.0
|
|
167
167
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
168
168
|
requirements:
|
|
169
169
|
- - ">="
|
|
170
170
|
- !ruby/object:Gem::Version
|
|
171
171
|
version: '0'
|
|
172
172
|
requirements: []
|
|
173
|
-
rubygems_version: 3.6.
|
|
173
|
+
rubygems_version: 3.6.7
|
|
174
174
|
specification_version: 4
|
|
175
175
|
summary: Primitive-first post-quantum cryptography for Ruby
|
|
176
176
|
test_files: []
|