kpqc 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/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/Rakefile +57 -0
- data/THIRD_PARTY_NOTICES.md +53 -0
- data/ext/kpqc/aimer128f/extconf.rb +4 -0
- data/ext/kpqc/aimer128s/extconf.rb +4 -0
- data/ext/kpqc/aimer192f/extconf.rb +4 -0
- data/ext/kpqc/aimer192s/extconf.rb +4 -0
- data/ext/kpqc/aimer256f/extconf.rb +4 -0
- data/ext/kpqc/aimer256s/extconf.rb +4 -0
- data/ext/kpqc/extconf_helper.rb +93 -0
- data/ext/kpqc/haetae2/extconf.rb +4 -0
- data/ext/kpqc/haetae3/extconf.rb +4 -0
- data/ext/kpqc/haetae5/extconf.rb +4 -0
- data/ext/kpqc/module.c +195 -0
- data/ext/kpqc/ntruplus1152/extconf.rb +4 -0
- data/ext/kpqc/ntruplus768/extconf.rb +4 -0
- data/ext/kpqc/ntruplus864/extconf.rb +4 -0
- data/ext/kpqc/randombytes.c +107 -0
- data/ext/kpqc/smaugt128/extconf.rb +4 -0
- data/ext/kpqc/smaugt192/extconf.rb +4 -0
- data/ext/kpqc/smaugt256/extconf.rb +4 -0
- data/ext/kpqc/timer/extconf.rb +4 -0
- data/kpqc.gemspec +41 -0
- data/lib/kpqc/aimer.rb +38 -0
- data/lib/kpqc/api.rb +129 -0
- data/lib/kpqc/haetae.rb +25 -0
- data/lib/kpqc/ntruplus.rb +25 -0
- data/lib/kpqc/smaugt.rb +29 -0
- data/lib/kpqc/types.rb +103 -0
- data/lib/kpqc/version.rb +6 -0
- data/lib/kpqc.rb +33 -0
- data/test/algorithm_test.rb +111 -0
- data/test/api_test.rb +52 -0
- data/test/interop.mjs +34 -0
- data/test/interop_test.rb +82 -0
- data/test/kat/aimer128f.rsp +10 -0
- data/test/kat/aimer128s.rsp +10 -0
- data/test/kat/aimer192f.rsp +10 -0
- data/test/kat/aimer192s.rsp +10 -0
- data/test/kat/aimer256f.rsp +10 -0
- data/test/kat/aimer256s.rsp +10 -0
- data/test/kat/haetae2.rsp +10 -0
- data/test/kat/haetae3.rsp +10 -0
- data/test/kat/haetae5.rsp +10 -0
- data/test/kat/ntruplus1152.rsp +8 -0
- data/test/kat/ntruplus768.rsp +8 -0
- data/test/kat/ntruplus864.rsp +8 -0
- data/test/kat/smaugt128.rsp +8 -0
- data/test/kat/smaugt192.rsp +8 -0
- data/test/kat/smaugt256.rsp +8 -0
- data/test/kat/timer.rsp +8 -0
- data/test/kat_drbg.rb +155 -0
- data/test/kat_test.rb +99 -0
- data/test/test_helper.rb +21 -0
- data/vendor/AIMer/LICENSE +21 -0
- data/vendor/AIMer/aim2.c +163 -0
- data/vendor/AIMer/aim2.h +36 -0
- data/vendor/AIMer/aim2_constant.h +74 -0
- data/vendor/AIMer/api.h +42 -0
- data/vendor/AIMer/common/crypto_declassify.h +7 -0
- data/vendor/AIMer/common/fips202.c +878 -0
- data/vendor/AIMer/common/fips202.h +166 -0
- data/vendor/AIMer/common/rng.h +56 -0
- data/vendor/AIMer/field.h +43 -0
- data/vendor/AIMer/field128.c +454 -0
- data/vendor/AIMer/field192.c +485 -0
- data/vendor/AIMer/field256.c +564 -0
- data/vendor/AIMer/hash.c +50 -0
- data/vendor/AIMer/hash.h +37 -0
- data/vendor/AIMer/params/params-aimer-128f.h +34 -0
- data/vendor/AIMer/params/params-aimer-128s.h +34 -0
- data/vendor/AIMer/params/params-aimer-192f.h +34 -0
- data/vendor/AIMer/params/params-aimer-192s.h +34 -0
- data/vendor/AIMer/params/params-aimer-256f.h +34 -0
- data/vendor/AIMer/params/params-aimer-256s.h +34 -0
- data/vendor/AIMer/params.h +5 -0
- data/vendor/AIMer/sign.c +631 -0
- data/vendor/AIMer/sign.h +40 -0
- data/vendor/AIMer/tree.c +75 -0
- data/vendor/AIMer/tree.h +26 -0
- data/vendor/HAETAE/LICENSE +22 -0
- data/vendor/HAETAE/include/api.h +54 -0
- data/vendor/HAETAE/include/common.h +12 -0
- data/vendor/HAETAE/include/config.h +42 -0
- data/vendor/HAETAE/include/decompose.h +19 -0
- data/vendor/HAETAE/include/encoding.h +20 -0
- data/vendor/HAETAE/include/fft.h +26 -0
- data/vendor/HAETAE/include/fips202.h +40 -0
- data/vendor/HAETAE/include/fixpoint.h +125 -0
- data/vendor/HAETAE/include/ntt.h +16 -0
- data/vendor/HAETAE/include/packing.h +58 -0
- data/vendor/HAETAE/include/params.h +117 -0
- data/vendor/HAETAE/include/poly.h +65 -0
- data/vendor/HAETAE/include/polyfix.h +65 -0
- data/vendor/HAETAE/include/polymat.h +32 -0
- data/vendor/HAETAE/include/polyvec.h +131 -0
- data/vendor/HAETAE/include/randombytes.h +39 -0
- data/vendor/HAETAE/include/rans_byte.h +335 -0
- data/vendor/HAETAE/include/reduce.h +31 -0
- data/vendor/HAETAE/include/sampler.h +23 -0
- data/vendor/HAETAE/include/symmetric.h +52 -0
- data/vendor/HAETAE/src/decompose.c +73 -0
- data/vendor/HAETAE/src/encoding.c +700 -0
- data/vendor/HAETAE/src/fft.c +235 -0
- data/vendor/HAETAE/src/fips202.c +745 -0
- data/vendor/HAETAE/src/fixpoint.c +143 -0
- data/vendor/HAETAE/src/ntt.c +98 -0
- data/vendor/HAETAE/src/packing.c +595 -0
- data/vendor/HAETAE/src/poly.c +371 -0
- data/vendor/HAETAE/src/polyfix.c +298 -0
- data/vendor/HAETAE/src/polymat.c +82 -0
- data/vendor/HAETAE/src/polyvec.c +534 -0
- data/vendor/HAETAE/src/reduce.c +96 -0
- data/vendor/HAETAE/src/sampler.c +310 -0
- data/vendor/HAETAE/src/sign.c +558 -0
- data/vendor/HAETAE/src/symmetric-shake.c +50 -0
- data/vendor/NTRUplus/LICENSE +21 -0
- data/vendor/NTRUplus/NTRU+1152/api.h +24 -0
- data/vendor/NTRUplus/NTRU+1152/fips202/fips202.c +878 -0
- data/vendor/NTRUplus/NTRU+1152/fips202/fips202.h +166 -0
- data/vendor/NTRUplus/NTRU+1152/kem.c +286 -0
- data/vendor/NTRUplus/NTRU+1152/ntt.c +394 -0
- data/vendor/NTRUplus/NTRU+1152/ntt.h +16 -0
- data/vendor/NTRUplus/NTRU+1152/params.h +18 -0
- data/vendor/NTRUplus/NTRU+1152/poly.c +318 -0
- data/vendor/NTRUplus/NTRU+1152/poly.h +31 -0
- data/vendor/NTRUplus/NTRU+1152/randombytes.h +9 -0
- data/vendor/NTRUplus/NTRU+1152/symmetric.c +39 -0
- data/vendor/NTRUplus/NTRU+1152/symmetric.h +12 -0
- data/vendor/NTRUplus/NTRU+768/api.h +24 -0
- data/vendor/NTRUplus/NTRU+768/fips202/fips202.c +878 -0
- data/vendor/NTRUplus/NTRU+768/fips202/fips202.h +166 -0
- data/vendor/NTRUplus/NTRU+768/kem.c +286 -0
- data/vendor/NTRUplus/NTRU+768/ntt.c +379 -0
- data/vendor/NTRUplus/NTRU+768/ntt.h +16 -0
- data/vendor/NTRUplus/NTRU+768/params.h +18 -0
- data/vendor/NTRUplus/NTRU+768/poly.c +320 -0
- data/vendor/NTRUplus/NTRU+768/poly.h +31 -0
- data/vendor/NTRUplus/NTRU+768/randombytes.h +9 -0
- data/vendor/NTRUplus/NTRU+768/symmetric.c +39 -0
- data/vendor/NTRUplus/NTRU+768/symmetric.h +12 -0
- data/vendor/NTRUplus/NTRU+864/api.h +24 -0
- data/vendor/NTRUplus/NTRU+864/fips202/fips202.c +878 -0
- data/vendor/NTRUplus/NTRU+864/fips202/fips202.h +166 -0
- data/vendor/NTRUplus/NTRU+864/kem.c +286 -0
- data/vendor/NTRUplus/NTRU+864/ntt.c +383 -0
- data/vendor/NTRUplus/NTRU+864/ntt.h +16 -0
- data/vendor/NTRUplus/NTRU+864/params.h +18 -0
- data/vendor/NTRUplus/NTRU+864/poly.c +314 -0
- data/vendor/NTRUplus/NTRU+864/poly.h +31 -0
- data/vendor/NTRUplus/NTRU+864/randombytes.h +9 -0
- data/vendor/NTRUplus/NTRU+864/symmetric.c +39 -0
- data/vendor/NTRUplus/NTRU+864/symmetric.h +12 -0
- data/vendor/SMAUG-T/LICENSE +22 -0
- data/vendor/SMAUG-T/include/api.h +37 -0
- data/vendor/SMAUG-T/include/cbd.h +15 -0
- data/vendor/SMAUG-T/include/ciphertext.h +45 -0
- data/vendor/SMAUG-T/include/common.h +12 -0
- data/vendor/SMAUG-T/include/config.h +48 -0
- data/vendor/SMAUG-T/include/dg.h +28 -0
- data/vendor/SMAUG-T/include/fips202.h +41 -0
- data/vendor/SMAUG-T/include/hash.h +21 -0
- data/vendor/SMAUG-T/include/hwt.h +28 -0
- data/vendor/SMAUG-T/include/indcpa.h +33 -0
- data/vendor/SMAUG-T/include/key.h +34 -0
- data/vendor/SMAUG-T/include/pack.h +78 -0
- data/vendor/SMAUG-T/include/packring.h +50 -0
- data/vendor/SMAUG-T/include/params.h +84 -0
- data/vendor/SMAUG-T/include/poly.h +41 -0
- data/vendor/SMAUG-T/include/randombytes.h +11 -0
- data/vendor/SMAUG-T/include/toomcook.h +14 -0
- data/vendor/SMAUG-T/include/verify.h +16 -0
- data/vendor/SMAUG-T/src/cbd.c +159 -0
- data/vendor/SMAUG-T/src/ciphertext.c +95 -0
- data/vendor/SMAUG-T/src/dg.c +112 -0
- data/vendor/SMAUG-T/src/fips202.c +745 -0
- data/vendor/SMAUG-T/src/hash.c +15 -0
- data/vendor/SMAUG-T/src/hwt.c +121 -0
- data/vendor/SMAUG-T/src/indcpa.c +187 -0
- data/vendor/SMAUG-T/src/kem.c +194 -0
- data/vendor/SMAUG-T/src/key.c +102 -0
- data/vendor/SMAUG-T/src/pack.c +349 -0
- data/vendor/SMAUG-T/src/packring.c +503 -0
- data/vendor/SMAUG-T/src/poly.c +200 -0
- data/vendor/SMAUG-T/src/toomcook.c +256 -0
- data/vendor/SMAUG-T/src/verify.c +48 -0
- data/vendor/SOURCES.json +130 -0
- metadata +280 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f95c1887b2a5ec7382d85fc701193a27404ba01a3618d69686530a310cd48259
|
|
4
|
+
data.tar.gz: 71e65d041b8a9c828db977f7676aaf26c3d7e330278498867a6823b4146696a3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6cfa24eef04e809b60842ee49dd858c950797b33a462dcb87eb59c5db46b7df97fc1180f941a55f84e4ccc96a2168bc07c511591fed9b8876089ba2006a6cd6c
|
|
7
|
+
data.tar.gz: ee190f6a25021501395dbf6403027521bac354433beb5fa7a5f0d5b9cc4db6f4afc530df309b7a1708c0513d48a77bd9623f32f4ffaf1e431d98028025f49b63
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 osuolfou
|
|
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,174 @@
|
|
|
1
|
+
# KpqC
|
|
2
|
+
|
|
3
|
+
KpqC provides synchronous Ruby APIs for AIMer, HAETAE, NTRU+, and SMAUG-T.
|
|
4
|
+
|
|
5
|
+
## Runtime support
|
|
6
|
+
|
|
7
|
+
- Ruby 2.6 or newer
|
|
8
|
+
- Windows, macOS, or Linux
|
|
9
|
+
- A C11 compiler and Ruby development headers when building from source
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
gem install kpqc
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Available schemes
|
|
18
|
+
|
|
19
|
+
| Algorithm | Type | Exports |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| **AIMer** | Signature | `aimer128f`, `aimer128s`, `aimer192f`, `aimer192s`, `aimer256f`, `aimer256s` |
|
|
22
|
+
| **HAETAE** | Signature | `haetae2`, `haetae3`, `haetae5` |
|
|
23
|
+
| **NTRU+** | Key encapsulation | `ntruplus768`, `ntruplus864`, `ntruplus1152` |
|
|
24
|
+
| **SMAUG‑T** | Key encapsulation | `smaugt128`, `smaugt192`, `smaugt256`, `timer` |
|
|
25
|
+
|
|
26
|
+
Requiring an algorithm family keeps the entry point focused:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require "kpqc/aimer"
|
|
30
|
+
|
|
31
|
+
aimer128f = KpqC::Aimer.aimer128f
|
|
32
|
+
payload = "release-manifest:v3".b
|
|
33
|
+
keys = aimer128f.generate_key_pair
|
|
34
|
+
proof = aimer128f.sign(payload, keys.secret_key)
|
|
35
|
+
|
|
36
|
+
unless aimer128f.verify(payload, proof, keys.public_key)
|
|
37
|
+
raise "Signature verification failed"
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Signature contexts
|
|
42
|
+
|
|
43
|
+
AIMer and HAETAE accept an optional context. A context separates signatures
|
|
44
|
+
created for different application purposes and may contain up to 255 bytes.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
require "kpqc/haetae"
|
|
48
|
+
|
|
49
|
+
haetae3 = KpqC::Haetae.haetae3
|
|
50
|
+
payload = "account=42".b
|
|
51
|
+
context = "audit-record".b
|
|
52
|
+
keys = haetae3.generate_key_pair
|
|
53
|
+
|
|
54
|
+
signature = haetae3.sign(payload, keys.secret_key, context: context)
|
|
55
|
+
valid = haetae3.verify(
|
|
56
|
+
payload,
|
|
57
|
+
signature,
|
|
58
|
+
keys.public_key,
|
|
59
|
+
context: context
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
puts valid # true
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Verification fails when the supplied context does not match the one used for
|
|
66
|
+
signing.
|
|
67
|
+
|
|
68
|
+
### Key encapsulation
|
|
69
|
+
|
|
70
|
+
A KEM creates a shared secret for a sender and a recipient. The public key may
|
|
71
|
+
be distributed; the secret key and resulting shared secret must remain private.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
require "kpqc/smaugt"
|
|
75
|
+
|
|
76
|
+
smaugt192 = KpqC::SmaugT.smaugt192
|
|
77
|
+
recipient = smaugt192.generate_key_pair
|
|
78
|
+
|
|
79
|
+
outbound = smaugt192.encapsulate(recipient.public_key)
|
|
80
|
+
# Send outbound.ciphertext to the recipient.
|
|
81
|
+
|
|
82
|
+
inbound_secret = smaugt192.decapsulate(
|
|
83
|
+
outbound.ciphertext,
|
|
84
|
+
recipient.secret_key
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
puts inbound_secret == outbound.shared_secret # true
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Imports
|
|
91
|
+
|
|
92
|
+
Each family has a dedicated require path:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
require "kpqc/aimer"
|
|
96
|
+
require "kpqc/haetae"
|
|
97
|
+
require "kpqc/ntruplus"
|
|
98
|
+
require "kpqc/smaugt"
|
|
99
|
+
|
|
100
|
+
signer = KpqC::Aimer.aimer256s
|
|
101
|
+
another_signer = KpqC::Haetae.haetae5
|
|
102
|
+
key_exchange = KpqC::NtruPlus.ntruplus1152
|
|
103
|
+
another_key_exchange = KpqC::SmaugT.timer
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
All named algorithms are also available from the package root:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
require "kpqc"
|
|
110
|
+
|
|
111
|
+
signer = KpqC.aimer192f
|
|
112
|
+
key_exchange = KpqC.ntruplus864
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Data and failures
|
|
116
|
+
|
|
117
|
+
Inputs and outputs are binary `String` instances. Each algorithm exposes an
|
|
118
|
+
`id` and an immutable `sizes` object.
|
|
119
|
+
|
|
120
|
+
### Parameter sizes
|
|
121
|
+
|
|
122
|
+
All sizes are in bytes.
|
|
123
|
+
|
|
124
|
+
#### Signatures
|
|
125
|
+
|
|
126
|
+
| Algorithm | Public key | Secret key | Signature |
|
|
127
|
+
| --- | ---: | ---: | ---: |
|
|
128
|
+
| `aimer128f` | 32 | 48 | 5,888 |
|
|
129
|
+
| `aimer128s` | 32 | 48 | 4,160 |
|
|
130
|
+
| `aimer192f` | 48 | 72 | 13,056 |
|
|
131
|
+
| `aimer192s` | 48 | 72 | 9,120 |
|
|
132
|
+
| `aimer256f` | 64 | 96 | 25,120 |
|
|
133
|
+
| `aimer256s` | 64 | 96 | 17,056 |
|
|
134
|
+
| `haetae2` | 992 | 1,408 | 1,474 |
|
|
135
|
+
| `haetae3` | 1,472 | 2,112 | 2,349 |
|
|
136
|
+
| `haetae5` | 2,080 | 2,752 | 2,948 |
|
|
137
|
+
|
|
138
|
+
#### Key encapsulation
|
|
139
|
+
|
|
140
|
+
| Algorithm | Public key | Secret key | Ciphertext | Shared secret |
|
|
141
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
142
|
+
| `ntruplus768` | 1,152 | 2,336 | 1,152 | 32 |
|
|
143
|
+
| `ntruplus864` | 1,296 | 2,624 | 1,296 | 32 |
|
|
144
|
+
| `ntruplus1152` | 1,728 | 3,488 | 1,728 | 32 |
|
|
145
|
+
| `smaugt128` | 672 | 832 | 672 | 32 |
|
|
146
|
+
| `smaugt192` | 1,088 | 1,312 | 992 | 32 |
|
|
147
|
+
| `smaugt256` | 1,440 | 1,728 | 1,376 | 32 |
|
|
148
|
+
| `timer` | 672 | 832 | 608 | 32 |
|
|
149
|
+
|
|
150
|
+
Methods reject values of the wrong type or size. Signature verification returns
|
|
151
|
+
`false` for an invalid signature. NTRU+ rejects an invalid ciphertext.
|
|
152
|
+
SMAUG-T performs implicit rejection and returns a replacement secret instead;
|
|
153
|
+
that value will not equal the sender's shared secret.
|
|
154
|
+
|
|
155
|
+
## Distribution
|
|
156
|
+
|
|
157
|
+
The published gem includes Ruby modules, native extension sources, and the
|
|
158
|
+
bundled algorithm sources. Each parameter set is loaded on first use. The gem
|
|
159
|
+
has no runtime dependencies.
|
|
160
|
+
|
|
161
|
+
## Security
|
|
162
|
+
|
|
163
|
+
The native cores are compiled from the upstream algorithm implementations.
|
|
164
|
+
This gem has not received an independent security audit and does not provide a
|
|
165
|
+
constant-time execution guarantee. Assess those constraints before using it
|
|
166
|
+
with sensitive production keys.
|
|
167
|
+
|
|
168
|
+
Secret keys stored in `KeyPair` and shared secrets stored in
|
|
169
|
+
`EncapsulatedSecret` can be wiped in place on a best-effort basis with
|
|
170
|
+
`clear!`. A `String` returned by `decapsulate` remains the caller's
|
|
171
|
+
responsibility.
|
|
172
|
+
|
|
173
|
+
Third-party licenses and attributions are listed in
|
|
174
|
+
[THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "rake/clean"
|
|
5
|
+
require "rake/testtask"
|
|
6
|
+
require_relative "lib/kpqc/version"
|
|
7
|
+
|
|
8
|
+
EXTENSION_DIRS = Dir[File.expand_path("ext/kpqc/*", __dir__)]
|
|
9
|
+
.select { |directory| File.file?(File.join(directory, "extconf.rb")) }
|
|
10
|
+
.sort
|
|
11
|
+
|
|
12
|
+
CLEAN.include(*EXTENSION_DIRS.flat_map do |directory|
|
|
13
|
+
[File.join(directory, "Makefile"), File.join(directory, "mkmf.log")]
|
|
14
|
+
end)
|
|
15
|
+
CLOBBER.include(*EXTENSION_DIRS.flat_map do |directory|
|
|
16
|
+
[File.join(directory, "**", "*.o"), File.join(directory, "**", "*.bundle"),
|
|
17
|
+
File.join(directory, "**", "*.so"), File.join(directory, "**", "*.dSYM")]
|
|
18
|
+
end)
|
|
19
|
+
CLOBBER.include("pkg")
|
|
20
|
+
|
|
21
|
+
def compile_extensions(test_entropy: false)
|
|
22
|
+
make = RbConfig::CONFIG["MAKE"] || "make"
|
|
23
|
+
EXTENSION_DIRS.each do |directory|
|
|
24
|
+
Dir.chdir(directory) do
|
|
25
|
+
environment = test_entropy ? { "KPQC_TEST_ENTROPY" => "1" } : {}
|
|
26
|
+
sh environment, RbConfig.ruby, "extconf.rb"
|
|
27
|
+
sh make, "clean"
|
|
28
|
+
sh make
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "Compile all native parameter-set extensions"
|
|
34
|
+
task :compile do
|
|
35
|
+
compile_extensions
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
task :compile_test do
|
|
39
|
+
compile_extensions(test_entropy: true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Rake::TestTask.new(:test => :compile_test) do |task|
|
|
43
|
+
task.libs << "lib"
|
|
44
|
+
EXTENSION_DIRS.each { |directory| task.libs << directory }
|
|
45
|
+
task.pattern = "test/**/*_test.rb"
|
|
46
|
+
task.warning = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "Build the gem package"
|
|
50
|
+
task :build do
|
|
51
|
+
mkdir_p "pkg"
|
|
52
|
+
package = File.join("pkg", "kpqc-#{KpqC::VERSION}.gem")
|
|
53
|
+
sh RbConfig.ruby, "-S", "gem", "build", "kpqc.gemspec",
|
|
54
|
+
"--output", package
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
task :default => :test
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
The native extensions published in this gem contain code from the following
|
|
4
|
+
KpqC reference implementations:
|
|
5
|
+
|
|
6
|
+
- AIMer reference implementation, by the AIMer team / Samsung SDS
|
|
7
|
+
- HAETAE reference implementation, Copyright (c) 2026 Team HAETAE
|
|
8
|
+
- NTRU+ reference implementation, Copyright (c) 2024–2026 NTRU+ TEAM
|
|
9
|
+
- SMAUG-T reference implementation, Copyright (c) 2026 Team SMAUG-T
|
|
10
|
+
|
|
11
|
+
These components are distributed under the MIT License. Full original license
|
|
12
|
+
texts are included in the gem source distribution.
|
|
13
|
+
|
|
14
|
+
Imported on 2026-09-06. `vendor/SOURCES.json` records the SHA-256 of each
|
|
15
|
+
source file before the shared KpqC binding adaptations, under its destination
|
|
16
|
+
path. Original NIST notices in AIMer headers are preserved; we acknowledge
|
|
17
|
+
NIST as the source of the software identified by those notices.
|
|
18
|
+
|
|
19
|
+
The SHA-3/SHAKE code is based on public-domain implementations by Ronny Van
|
|
20
|
+
Keer and on TweetFips202 by Gilles Van Assche, Daniel J. Bernstein and Peter
|
|
21
|
+
Schwabe.
|
|
22
|
+
|
|
23
|
+
## Source and Ruby binding adaptations
|
|
24
|
+
|
|
25
|
+
- Production entropy uses operating-system random-number sources through
|
|
26
|
+
`ext/kpqc/randombytes.c`. Upstream RNG/CTR-DRBG/AES implementations and
|
|
27
|
+
benchmark/KAT executables are not compiled or shipped as runtime components.
|
|
28
|
+
- AIMer, HAETAE, NTRU+, and SMAUG-T public operations check RNG return codes.
|
|
29
|
+
NTRU+ RNG declarations return `int` to propagate failure. The optional
|
|
30
|
+
random-seed branch in SMAUG-T's `indcpa_enc` also stops on RNG failure.
|
|
31
|
+
- AIMer and NTRU+ SHA-3/SHAKE contexts use embedded storage and wipe it on
|
|
32
|
+
release, avoiding process termination when heap allocation fails. AIMer
|
|
33
|
+
signing and verification allocation failures propagate to Ruby as
|
|
34
|
+
`KpqC::Error` or `NoMemoryError`.
|
|
35
|
+
- The imported SMAUG-T sources include full-vector initialization in `key.c`,
|
|
36
|
+
corrected output indexing in `dg.c`, and the correct output buffer size in
|
|
37
|
+
`packring.c`.
|
|
38
|
+
- Unused HAETAE helper code and local variables were removed, and a SMAUG-T
|
|
39
|
+
loop counter was changed to `size_t` to match its bound.
|
|
40
|
+
- Parameter sets are built as separate Ruby extensions with their upstream
|
|
41
|
+
configuration macros and hidden C symbols. The binding validates input sizes
|
|
42
|
+
before entering C and wipes unpublished outputs on failure.
|
|
43
|
+
|
|
44
|
+
The bundled implementations are the same adapted cores used to generate the
|
|
45
|
+
npm package's JavaScript runtime.
|
|
46
|
+
|
|
47
|
+
## Known-answer tests
|
|
48
|
+
|
|
49
|
+
`test/kat` contains the first KAT record for every parameter set. AIMer,
|
|
50
|
+
HAETAE, and NTRU+ records come from their reference implementations. SMAUG-T
|
|
51
|
+
records reflect the documented source fixes also used by the npm and PyPI
|
|
52
|
+
packages. The tests reproduce the NIST AES-256 CTR DRBG and compare keys,
|
|
53
|
+
signatures, ciphertexts, and shared secrets byte-for-byte.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
|
|
6
|
+
module KpqCBuild
|
|
7
|
+
ROOT = File.expand_path("../..", __dir__)
|
|
8
|
+
EXT_ROOT = __dir__
|
|
9
|
+
VENDOR = File.join(ROOT, "vendor")
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def aimer(name, parameter)
|
|
14
|
+
root = File.join(VENDOR, "AIMer")
|
|
15
|
+
configure(
|
|
16
|
+
name,
|
|
17
|
+
[root, File.join(root, "common")],
|
|
18
|
+
["aim2.c", "hash.c", "sign.c", "tree.c",
|
|
19
|
+
"field#{parameter[0, 3]}.c", "fips202.c"],
|
|
20
|
+
"PARAMS" => parameter,
|
|
21
|
+
"KPQC_SIGNATURE" => 1,
|
|
22
|
+
"KPQC_AIMER" => 1
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def haetae(name, mode)
|
|
27
|
+
root = File.join(VENDOR, "HAETAE")
|
|
28
|
+
sources = %w[
|
|
29
|
+
decompose.c encoding.c fft.c fixpoint.c ntt.c packing.c poly.c
|
|
30
|
+
polyfix.c polymat.c polyvec.c reduce.c sampler.c sign.c fips202.c
|
|
31
|
+
symmetric-shake.c
|
|
32
|
+
]
|
|
33
|
+
configure(
|
|
34
|
+
name,
|
|
35
|
+
[File.join(root, "include"), File.join(root, "src")],
|
|
36
|
+
sources,
|
|
37
|
+
"HAETAE_CONFIG_MODE" => mode,
|
|
38
|
+
"KPQC_SIGNATURE" => 1
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ntruplus(name, parameter)
|
|
43
|
+
root = File.join(VENDOR, "NTRUplus", "NTRU+#{parameter}")
|
|
44
|
+
configure(
|
|
45
|
+
name,
|
|
46
|
+
[root, File.join(root, "fips202")],
|
|
47
|
+
%w[kem.c ntt.c poly.c symmetric.c fips202.c],
|
|
48
|
+
{}
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def smaugt(name, mode)
|
|
53
|
+
root = File.join(VENDOR, "SMAUG-T")
|
|
54
|
+
sources = %w[
|
|
55
|
+
cbd.c ciphertext.c dg.c hash.c hwt.c indcpa.c kem.c key.c pack.c
|
|
56
|
+
packring.c poly.c toomcook.c verify.c fips202.c
|
|
57
|
+
]
|
|
58
|
+
configure(
|
|
59
|
+
name,
|
|
60
|
+
[File.join(root, "include"), File.join(root, "src")],
|
|
61
|
+
sources,
|
|
62
|
+
"SMAUGT_CONFIG_MODE" => mode
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def configure(name, source_directories, sources, definitions)
|
|
67
|
+
extension_constant = name.sub(/\A([a-z])/) { Regexp.last_match(1).upcase }
|
|
68
|
+
definitions = definitions.merge(
|
|
69
|
+
"KPQC_NATIVE_INIT" => "#{name}_native",
|
|
70
|
+
"KPQC_NATIVE_CONST" => extension_constant
|
|
71
|
+
)
|
|
72
|
+
definitions["KPQC_TEST_ENTROPY"] = 1 if ENV["KPQC_TEST_ENTROPY"] == "1"
|
|
73
|
+
|
|
74
|
+
$VPATH << EXT_ROOT
|
|
75
|
+
source_directories.each do |directory|
|
|
76
|
+
$VPATH << directory
|
|
77
|
+
$INCFLAGS << " -I#{directory}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
$srcs = ["module.c", "randombytes.c"] + sources
|
|
81
|
+
definitions.each { |key, value| $defs << "-D#{key}=#{value}" }
|
|
82
|
+
|
|
83
|
+
if RbConfig::CONFIG["CC"] =~ /(?:^|\s)cl(?:\.exe)?(?:\s|$)/i
|
|
84
|
+
$CFLAGS << " /O2 /W0"
|
|
85
|
+
else
|
|
86
|
+
$CFLAGS << " -std=c11 -O2 -fvisibility=hidden -fno-strict-aliasing -w"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
have_library("bcrypt") if Gem.win_platform?
|
|
90
|
+
have_library("m") unless Gem.win_platform?
|
|
91
|
+
create_makefile("kpqc/#{name}_native")
|
|
92
|
+
end
|
|
93
|
+
end
|
data/ext/kpqc/module.c
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MIT */
|
|
2
|
+
#include <stddef.h>
|
|
3
|
+
#include <stdint.h>
|
|
4
|
+
#include <ruby.h>
|
|
5
|
+
#include "api.h"
|
|
6
|
+
|
|
7
|
+
#define KPQC_JOIN_(left, right) left##right
|
|
8
|
+
#define KPQC_JOIN(left, right) KPQC_JOIN_(left, right)
|
|
9
|
+
#define KPQC_STRING_(value) #value
|
|
10
|
+
#define KPQC_STRING(value) KPQC_STRING_(value)
|
|
11
|
+
#define KPQC_INIT(name) KPQC_JOIN(Init_, name)
|
|
12
|
+
#define KPQC_DATA(value) ((uint8_t *)RSTRING_PTR(value))
|
|
13
|
+
|
|
14
|
+
static VALUE kpqc_error;
|
|
15
|
+
#ifdef KPQC_SIGNATURE
|
|
16
|
+
static const uint8_t kpqc_empty_input = 0;
|
|
17
|
+
|
|
18
|
+
static const uint8_t *kpqc_input(VALUE value) {
|
|
19
|
+
return RSTRING_LEN(value) == 0 ? &kpqc_empty_input : KPQC_DATA(value);
|
|
20
|
+
}
|
|
21
|
+
#endif
|
|
22
|
+
|
|
23
|
+
static void kpqc_require_string(VALUE value, const char *label) {
|
|
24
|
+
if (!RB_TYPE_P(value, T_STRING)) {
|
|
25
|
+
rb_raise(rb_eTypeError, "%s must be a String", label);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static void kpqc_require_size(VALUE value, long expected, const char *label) {
|
|
30
|
+
kpqc_require_string(value, label);
|
|
31
|
+
if (RSTRING_LEN(value) != expected) {
|
|
32
|
+
rb_raise(rb_eArgError, "%s must be %ld bytes, received %ld",
|
|
33
|
+
label, expected, RSTRING_LEN(value));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#ifdef KPQC_SIGNATURE
|
|
38
|
+
static void kpqc_require_context(VALUE context) {
|
|
39
|
+
kpqc_require_string(context, "context");
|
|
40
|
+
if (RSTRING_LEN(context) > 255) {
|
|
41
|
+
rb_raise(rb_eArgError,
|
|
42
|
+
"context cannot exceed 255 bytes, received %ld",
|
|
43
|
+
RSTRING_LEN(context));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
#endif
|
|
47
|
+
|
|
48
|
+
static void kpqc_wipe(VALUE value) {
|
|
49
|
+
volatile uint8_t *bytes = KPQC_DATA(value);
|
|
50
|
+
long length = RSTRING_LEN(value);
|
|
51
|
+
while (length-- > 0) *bytes++ = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#if defined(_MSC_VER)
|
|
55
|
+
#define KPQC_NORETURN __declspec(noreturn)
|
|
56
|
+
#elif defined(__GNUC__) || defined(__clang__)
|
|
57
|
+
#define KPQC_NORETURN __attribute__((noreturn))
|
|
58
|
+
#else
|
|
59
|
+
#define KPQC_NORETURN
|
|
60
|
+
#endif
|
|
61
|
+
|
|
62
|
+
static KPQC_NORETURN void kpqc_failure(int status, const char *operation) {
|
|
63
|
+
if (status == -2) rb_memerror();
|
|
64
|
+
rb_raise(kpqc_error, "%s failed with status %d", operation, status);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static VALUE kpqc_keygen(VALUE self) {
|
|
68
|
+
VALUE public_key = rb_str_new(NULL, CRYPTO_PUBLICKEYBYTES);
|
|
69
|
+
VALUE secret_key = rb_str_new(NULL, CRYPTO_SECRETKEYBYTES);
|
|
70
|
+
int status;
|
|
71
|
+
(void)self;
|
|
72
|
+
|
|
73
|
+
#ifdef KPQC_SIGNATURE
|
|
74
|
+
status = crypto_sign_keypair(KPQC_DATA(public_key), KPQC_DATA(secret_key));
|
|
75
|
+
#else
|
|
76
|
+
status = crypto_kem_keypair(KPQC_DATA(public_key), KPQC_DATA(secret_key));
|
|
77
|
+
#endif
|
|
78
|
+
if (status != 0) {
|
|
79
|
+
kpqc_wipe(public_key);
|
|
80
|
+
kpqc_wipe(secret_key);
|
|
81
|
+
kpqc_failure(status, "key generation");
|
|
82
|
+
}
|
|
83
|
+
return rb_ary_new_from_args(2, public_key, secret_key);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#ifdef KPQC_SIGNATURE
|
|
87
|
+
static VALUE kpqc_sign(VALUE self, VALUE message, VALUE secret_key,
|
|
88
|
+
VALUE context) {
|
|
89
|
+
VALUE signature;
|
|
90
|
+
size_t signature_length = 0;
|
|
91
|
+
int status;
|
|
92
|
+
(void)self;
|
|
93
|
+
|
|
94
|
+
kpqc_require_string(message, "message");
|
|
95
|
+
kpqc_require_size(secret_key, CRYPTO_SECRETKEYBYTES, "secret_key");
|
|
96
|
+
kpqc_require_context(context);
|
|
97
|
+
signature = rb_str_new(NULL, CRYPTO_BYTES);
|
|
98
|
+
|
|
99
|
+
status = crypto_sign_signature(
|
|
100
|
+
KPQC_DATA(signature), &signature_length,
|
|
101
|
+
kpqc_input(message), (size_t)RSTRING_LEN(message),
|
|
102
|
+
kpqc_input(context), (size_t)RSTRING_LEN(context),
|
|
103
|
+
KPQC_DATA(secret_key));
|
|
104
|
+
if (status != 0) {
|
|
105
|
+
kpqc_wipe(signature);
|
|
106
|
+
kpqc_failure(status, "signing");
|
|
107
|
+
}
|
|
108
|
+
if (signature_length != CRYPTO_BYTES) {
|
|
109
|
+
kpqc_wipe(signature);
|
|
110
|
+
rb_raise(rb_eRuntimeError,
|
|
111
|
+
"core returned an invalid signature size: expected %d, received %lu",
|
|
112
|
+
CRYPTO_BYTES, (unsigned long)signature_length);
|
|
113
|
+
}
|
|
114
|
+
return signature;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static VALUE kpqc_verify(VALUE self, VALUE message, VALUE signature,
|
|
118
|
+
VALUE public_key, VALUE context) {
|
|
119
|
+
int status;
|
|
120
|
+
(void)self;
|
|
121
|
+
|
|
122
|
+
kpqc_require_string(message, "message");
|
|
123
|
+
kpqc_require_string(signature, "signature");
|
|
124
|
+
kpqc_require_size(public_key, CRYPTO_PUBLICKEYBYTES, "public_key");
|
|
125
|
+
kpqc_require_context(context);
|
|
126
|
+
if (RSTRING_LEN(signature) != CRYPTO_BYTES) return Qfalse;
|
|
127
|
+
|
|
128
|
+
status = crypto_sign_verify(
|
|
129
|
+
KPQC_DATA(signature), (size_t)RSTRING_LEN(signature),
|
|
130
|
+
kpqc_input(message), (size_t)RSTRING_LEN(message),
|
|
131
|
+
kpqc_input(context), (size_t)RSTRING_LEN(context),
|
|
132
|
+
KPQC_DATA(public_key));
|
|
133
|
+
if (status == -2) kpqc_failure(status, "verification");
|
|
134
|
+
return status == 0 ? Qtrue : Qfalse;
|
|
135
|
+
}
|
|
136
|
+
#else
|
|
137
|
+
static VALUE kpqc_encapsulate(VALUE self, VALUE public_key) {
|
|
138
|
+
VALUE ciphertext;
|
|
139
|
+
VALUE shared_secret;
|
|
140
|
+
int status;
|
|
141
|
+
(void)self;
|
|
142
|
+
|
|
143
|
+
kpqc_require_size(public_key, CRYPTO_PUBLICKEYBYTES, "public_key");
|
|
144
|
+
ciphertext = rb_str_new(NULL, CRYPTO_CIPHERTEXTBYTES);
|
|
145
|
+
shared_secret = rb_str_new(NULL, CRYPTO_BYTES);
|
|
146
|
+
status = crypto_kem_enc(KPQC_DATA(ciphertext), KPQC_DATA(shared_secret),
|
|
147
|
+
KPQC_DATA(public_key));
|
|
148
|
+
if (status != 0) {
|
|
149
|
+
kpqc_wipe(ciphertext);
|
|
150
|
+
kpqc_wipe(shared_secret);
|
|
151
|
+
kpqc_failure(status, "encapsulation");
|
|
152
|
+
}
|
|
153
|
+
return rb_ary_new_from_args(2, ciphertext, shared_secret);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static VALUE kpqc_decapsulate(VALUE self, VALUE ciphertext, VALUE secret_key) {
|
|
157
|
+
VALUE shared_secret;
|
|
158
|
+
int status;
|
|
159
|
+
(void)self;
|
|
160
|
+
|
|
161
|
+
kpqc_require_size(ciphertext, CRYPTO_CIPHERTEXTBYTES, "ciphertext");
|
|
162
|
+
kpqc_require_size(secret_key, CRYPTO_SECRETKEYBYTES, "secret_key");
|
|
163
|
+
shared_secret = rb_str_new(NULL, CRYPTO_BYTES);
|
|
164
|
+
status = crypto_kem_dec(KPQC_DATA(shared_secret), KPQC_DATA(ciphertext),
|
|
165
|
+
KPQC_DATA(secret_key));
|
|
166
|
+
if (status != 0) {
|
|
167
|
+
kpqc_wipe(shared_secret);
|
|
168
|
+
kpqc_failure(status, "decapsulation");
|
|
169
|
+
}
|
|
170
|
+
return shared_secret;
|
|
171
|
+
}
|
|
172
|
+
#endif
|
|
173
|
+
|
|
174
|
+
RUBY_FUNC_EXPORTED void KPQC_INIT(KPQC_NATIVE_INIT)(void) {
|
|
175
|
+
VALUE kpqc = rb_define_module("KpqC");
|
|
176
|
+
VALUE native = rb_define_module_under(kpqc, "Native");
|
|
177
|
+
VALUE algorithm = rb_define_module_under(
|
|
178
|
+
native, KPQC_STRING(KPQC_NATIVE_CONST));
|
|
179
|
+
|
|
180
|
+
kpqc_error = rb_define_class_under(kpqc, "Error", rb_eStandardError);
|
|
181
|
+
rb_define_singleton_method(algorithm, "keygen", kpqc_keygen, 0);
|
|
182
|
+
#ifdef KPQC_SIGNATURE
|
|
183
|
+
rb_define_singleton_method(algorithm, "sign", kpqc_sign, 3);
|
|
184
|
+
rb_define_singleton_method(algorithm, "verify", kpqc_verify, 4);
|
|
185
|
+
#else
|
|
186
|
+
rb_define_singleton_method(algorithm, "encapsulate", kpqc_encapsulate, 1);
|
|
187
|
+
rb_define_singleton_method(algorithm, "decapsulate", kpqc_decapsulate, 2);
|
|
188
|
+
#endif
|
|
189
|
+
#ifdef KPQC_TEST_ENTROPY
|
|
190
|
+
{
|
|
191
|
+
void kpqc_randombytes_test_init(VALUE);
|
|
192
|
+
kpqc_randombytes_test_init(algorithm);
|
|
193
|
+
}
|
|
194
|
+
#endif
|
|
195
|
+
}
|