hpke 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/.rspec +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +37 -0
- data/LICENSE.txt +21 -0
- data/README.md +120 -0
- data/Rakefile +8 -0
- data/lib/hpke/dhkem.rb +368 -0
- data/lib/hpke/hkdf.rb +100 -0
- data/lib/hpke/util.rb +26 -0
- data/lib/hpke/version.rb +5 -0
- data/lib/hpke.rb +300 -0
- data/sig/hpke.rbs +4 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c0ff20003913867f648466cb8136e73218c7dd917087c98f7d86ef8fd2049a11
|
4
|
+
data.tar.gz: 47901efa463ad51eebe04bface377e9dda4c24497a4b12039c64bc966b98c2d9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1bbd503dd86c43bcb19fc188338cc9119b35a2bc5e6812a1404c19f8e198b7c71c0a18d3e1ff81d9ee3658575044995dba5ac092229b61298c7e79ed2e679faf
|
7
|
+
data.tar.gz: ac0ac0b079d9a8b8c3d7e60d6d90e0109d31150ae44f433b815d1a62fce9b3bb70dc707112c7a7155f94f3e7df171a11fa3ef97ba4fccbf509a7d911866b170b
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
hpke (0.1.0)
|
5
|
+
openssl (~> 3.0.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.5.0)
|
11
|
+
openssl (3.0.2)
|
12
|
+
rake (13.0.6)
|
13
|
+
rspec (3.12.0)
|
14
|
+
rspec-core (~> 3.12.0)
|
15
|
+
rspec-expectations (~> 3.12.0)
|
16
|
+
rspec-mocks (~> 3.12.0)
|
17
|
+
rspec-core (3.12.2)
|
18
|
+
rspec-support (~> 3.12.0)
|
19
|
+
rspec-expectations (3.12.3)
|
20
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
21
|
+
rspec-support (~> 3.12.0)
|
22
|
+
rspec-mocks (3.12.6)
|
23
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
24
|
+
rspec-support (~> 3.12.0)
|
25
|
+
rspec-support (3.12.1)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
arm64-darwin-22
|
29
|
+
x86_64-linux
|
30
|
+
|
31
|
+
DEPENDENCIES
|
32
|
+
hpke!
|
33
|
+
rake (~> 13.0)
|
34
|
+
rspec (~> 3.0)
|
35
|
+
|
36
|
+
BUNDLED WITH
|
37
|
+
2.4.10
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Ryo Kajiwara
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# hpke-rb
|
2
|
+
|
3
|
+
Hybrid Public Key Encryption (HPKE; [RFC 9180](https://datatracker.ietf.org/doc/html/rfc9180)) in Ruby
|
4
|
+
|
5
|
+
## Note
|
6
|
+
|
7
|
+
This is still in very early development, so:
|
8
|
+
|
9
|
+
- APIs are subject to change
|
10
|
+
- Especially the instantation interface of KEM and HPKE suite
|
11
|
+
- This is tested against test vectors supplied by the authors of the RFC, but is not formally audited for security. Please be aware of this when using in production.
|
12
|
+
|
13
|
+
## Supported Features
|
14
|
+
|
15
|
+
Supports all modes, KEMs, AEAD functions in RFC 9180.
|
16
|
+
|
17
|
+
- HPKE Modes
|
18
|
+
- Base
|
19
|
+
- PSK
|
20
|
+
- Auth
|
21
|
+
- AuthPSK
|
22
|
+
- Key Encapsulation Mechanisms (KEMs)
|
23
|
+
- DHKEM(P-256, HKDF-SHA256)
|
24
|
+
- DHKEM(P-384, HKDF-SHA384)
|
25
|
+
- DHKEM(P-521, HKDF-SHA512)
|
26
|
+
- DHKEM(X25519, HKDF-SHA256)
|
27
|
+
- DHKEM(X448, HKDF-SHA512)
|
28
|
+
- Key Derivation Functions (KDFs)
|
29
|
+
- HKDF-SHA256
|
30
|
+
- HKDF-SHA384
|
31
|
+
- HKDF-SHA512
|
32
|
+
- AEAD Functions
|
33
|
+
- AES-128-GCM
|
34
|
+
- AES-256-GCM
|
35
|
+
- ChaCha20-Poly1305
|
36
|
+
- Export Only
|
37
|
+
|
38
|
+
## Supported Environments
|
39
|
+
|
40
|
+
- OpenSSL 3.0 or higher
|
41
|
+
- This is due to the changes in instantiation of public/private key pairs from OpenSSL 1.1 series to OpenSSL 3.0 series
|
42
|
+
- Ruby 3.1 or higher
|
43
|
+
- Ruby 3.1 comes with OpenSSL 3.0 support
|
44
|
+
|
45
|
+
## Installation
|
46
|
+
|
47
|
+
Install the gem and add to the application's Gemfile by executing:
|
48
|
+
|
49
|
+
$ bundle add hpke
|
50
|
+
|
51
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
52
|
+
|
53
|
+
$ gem install hpke
|
54
|
+
|
55
|
+
## Usage
|
56
|
+
|
57
|
+
(example shows Base mode)
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
# instantiate HPKE suite
|
61
|
+
# first 2 parameters specify the curve and hash to be used in the KEM,
|
62
|
+
# third parameter specifies the hash to be used in the KDF (of HPKE suite),
|
63
|
+
# fourth parameter specifies the AEAD function
|
64
|
+
|
65
|
+
# we will generate a different instance just for demonstration to show that nothing secret is stored in the HPKE suite instance
|
66
|
+
hpke_s = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm)
|
67
|
+
hpke_r = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm)
|
68
|
+
|
69
|
+
# get a OpenSSL::PKey::PKey instance by either generating a key or loading a key from a PEM
|
70
|
+
# see https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/PKey/PKey.html
|
71
|
+
# on the sender's end
|
72
|
+
sender_key_pair = OpenSSL::PKey.generate_key('X25519')
|
73
|
+
receiver_key_pair = OpenSSL::PKey.generate_key('X25519')
|
74
|
+
|
75
|
+
# Sender setup
|
76
|
+
# Sender knows the receiver's public key (in PEM format, in most cases), so load that into a PKey
|
77
|
+
receiver_public_key = OpenSSL::PKey.read(receiver_key_pair.public_to_pem)
|
78
|
+
encap_result = hpke_s.setup_base_s(receiver_public_key, 'info')
|
79
|
+
# This returns a hash where :enc key contains the key encapsulation,
|
80
|
+
# and :context_s contains a HPKE::ContextS instance, which is used for encryption later on.
|
81
|
+
context_s = encap_result[:context_s]
|
82
|
+
# Note that :enc contains raw bytes, so when passing to the receiver, it is advised to pass the encapsulation using Base64-encoded values
|
83
|
+
enc_base64 = Base64.encode64(encap_result[:enc])
|
84
|
+
|
85
|
+
# Then on the receiver's end
|
86
|
+
# decode the encapsulated value
|
87
|
+
enc = Base64.decode64(enc_base64)
|
88
|
+
# then use that value to generate a HPKE::ContextR instance to use for decryption
|
89
|
+
context_r = hpke_r.setup_base_r(enc, receiver_key_pair, 'info')
|
90
|
+
|
91
|
+
# sender encrypts a message
|
92
|
+
# note that the "sequence number" is incremented each time `seal` and `open` is used
|
93
|
+
ciphertext = context_s.seal('authentication_associated_data', 'plaintext')
|
94
|
+
# this is also in raw bytes, so when sending, encoding with Base64 is advised
|
95
|
+
|
96
|
+
# then receiver decrypts the ciphertext
|
97
|
+
context_r.open('authentication_associated_data', ciphertext)
|
98
|
+
```
|
99
|
+
|
100
|
+
- Curve names (parameter 1)
|
101
|
+
- `:p_256`, `:p_384`, `:p_521`, `:x25519`, `:x448`
|
102
|
+
- Note: `:p_256` corresponds to `prime256v1`, `:p_384` corresponds to `secp384r1`, and `:p_521` corresponds to `secp521r1` in OpenSSL
|
103
|
+
- Hash names (parameter 2 and 3)
|
104
|
+
- `:sha256`, `:sha384`, `:sha512`
|
105
|
+
- AEAD function names (parameter 4)
|
106
|
+
- `:aes_128_gcm`, `:aes_256_gcm`, `:chacha20_poly1305`, `:`
|
107
|
+
|
108
|
+
## Development
|
109
|
+
|
110
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
111
|
+
|
112
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
113
|
+
|
114
|
+
## Contributing
|
115
|
+
|
116
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sylph01/hpke-rb.
|
117
|
+
|
118
|
+
## License
|
119
|
+
|
120
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/lib/hpke/dhkem.rb
ADDED
@@ -0,0 +1,368 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'securerandom'
|
3
|
+
require_relative 'hkdf'
|
4
|
+
require_relative 'util'
|
5
|
+
|
6
|
+
class HPKE::DHKEM
|
7
|
+
include HPKE::Util
|
8
|
+
|
9
|
+
def initialize(hash_name)
|
10
|
+
@hkdf = HPKE::HKDF.new(hash_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def encap(pk_r)
|
14
|
+
pkey_e = generate_key_pair()
|
15
|
+
dh = pkey_e.derive(pk_r)
|
16
|
+
enc = serialize_public_key(pkey_e)
|
17
|
+
|
18
|
+
pkrm = serialize_public_key(pk_r)
|
19
|
+
kem_context = enc + pkrm
|
20
|
+
|
21
|
+
shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
|
22
|
+
{
|
23
|
+
shared_secret: shared_secret,
|
24
|
+
enc: enc
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def auth_encap(pk_r, sk_s)
|
29
|
+
pkey_e = generate_key_pair()
|
30
|
+
dh = pkey_e.derive(pk_r) + sk_s.derive(pk_r)
|
31
|
+
enc = serialize_public_key(pkey_e)
|
32
|
+
|
33
|
+
pkrm = serialize_public_key(pk_r)
|
34
|
+
pksm = serialize_public_key(sk_s)
|
35
|
+
kem_context = enc + pkrm + pksm
|
36
|
+
|
37
|
+
shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
|
38
|
+
{
|
39
|
+
shared_secret: shared_secret,
|
40
|
+
enc: enc
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def decap(enc, sk_r)
|
45
|
+
pk_e = deserialize_public_key(enc)
|
46
|
+
dh = sk_r.derive(pk_e)
|
47
|
+
|
48
|
+
pkrm = serialize_public_key(sk_r)
|
49
|
+
kem_context = enc + pkrm
|
50
|
+
|
51
|
+
shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
|
52
|
+
shared_secret
|
53
|
+
end
|
54
|
+
|
55
|
+
def auth_decap(enc, sk_r, pk_s)
|
56
|
+
pk_e = deserialize_public_key(enc)
|
57
|
+
dh = sk_r.derive(pk_e) + sk_r.derive(pk_s)
|
58
|
+
|
59
|
+
pkrm = serialize_public_key(sk_r)
|
60
|
+
pksm = serialize_public_key(pk_s)
|
61
|
+
kem_context = enc + pkrm + pksm
|
62
|
+
|
63
|
+
shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
|
64
|
+
shared_secret
|
65
|
+
end
|
66
|
+
|
67
|
+
def encap_fixed(pk_r, ikm_e)
|
68
|
+
pkey_e = derive_key_pair(ikm_e)
|
69
|
+
dh = pkey_e.derive(pk_r)
|
70
|
+
enc = serialize_public_key(pkey_e)
|
71
|
+
|
72
|
+
pkrm = serialize_public_key(pk_r)
|
73
|
+
kem_context = enc + pkrm
|
74
|
+
|
75
|
+
shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
|
76
|
+
{
|
77
|
+
shared_secret: shared_secret,
|
78
|
+
enc: enc
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def auth_encap_fixed(pk_r, sk_s, ikm_e)
|
83
|
+
pkey_e = derive_key_pair(ikm_e)
|
84
|
+
dh = pkey_e.derive(pk_r) + sk_s.derive(pk_r)
|
85
|
+
enc = serialize_public_key(pkey_e)
|
86
|
+
|
87
|
+
pkrm = serialize_public_key(pk_r)
|
88
|
+
pksm = serialize_public_key(sk_s)
|
89
|
+
kem_context = enc + pkrm + pksm
|
90
|
+
|
91
|
+
shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
|
92
|
+
{
|
93
|
+
shared_secret: shared_secret,
|
94
|
+
enc: enc
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def generate_key_pair
|
99
|
+
derive_key_pair(SecureRandom.random_bytes(n_sk))
|
100
|
+
end
|
101
|
+
|
102
|
+
# ---- functions for Edwards curves (X25519, X448) ----
|
103
|
+
def derive_key_pair(ikm)
|
104
|
+
dkp_prk = @hkdf.labeled_extract('', 'dkp_prk', ikm, kem_suite_id)
|
105
|
+
sk = @hkdf.labeled_expand(dkp_prk, 'sk', '', n_sk, kem_suite_id)
|
106
|
+
|
107
|
+
create_key_pair_from_secret(sk)
|
108
|
+
end
|
109
|
+
|
110
|
+
def serialize_public_key(pk)
|
111
|
+
pk.public_to_der[-n_pk, n_pk]
|
112
|
+
end
|
113
|
+
|
114
|
+
def deserialize_public_key(serialized_pk)
|
115
|
+
asn1_seq_pub = OpenSSL::ASN1.Sequence([
|
116
|
+
OpenSSL::ASN1.Sequence([
|
117
|
+
OpenSSL::ASN1.ObjectId(asn1_oid)
|
118
|
+
]),
|
119
|
+
OpenSSL::ASN1.BitString(serialized_pk)
|
120
|
+
])
|
121
|
+
|
122
|
+
OpenSSL::PKey.read(asn1_seq_pub.to_der)
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def kem_suite_id
|
128
|
+
'KEM' + i2osp(kem_id, 2)
|
129
|
+
end
|
130
|
+
|
131
|
+
def extract_and_expand(dh, kem_context, suite_id)
|
132
|
+
eae_prk = @hkdf.labeled_extract('', 'eae_prk', dh, suite_id)
|
133
|
+
|
134
|
+
@hkdf.labeled_expand(eae_prk, 'shared_secret', kem_context, n_secret, suite_id)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class HPKE::DHKEM::EC < HPKE::DHKEM
|
139
|
+
def derive_key_pair(ikm)
|
140
|
+
dkp_prk = @hkdf.labeled_extract('', 'dkp_prk', ikm, kem_suite_id)
|
141
|
+
sk = 0
|
142
|
+
counter = 0
|
143
|
+
while sk == 0 || sk >= order do
|
144
|
+
raise Exception.new('DeriveKeyPairError') if counter > 255
|
145
|
+
|
146
|
+
bytes = @hkdf.labeled_expand(dkp_prk, 'candidate', i2osp(counter, 1), n_sk, kem_suite_id)
|
147
|
+
bytes[0] = (bytes[0].ord & bitmask).chr
|
148
|
+
sk = os2ip(bytes)
|
149
|
+
counter += 1
|
150
|
+
end
|
151
|
+
|
152
|
+
create_key_pair_from_secret(bytes)
|
153
|
+
end
|
154
|
+
|
155
|
+
def create_key_pair_from_secret(secret)
|
156
|
+
asn1_seq = OpenSSL::ASN1.Sequence([
|
157
|
+
OpenSSL::ASN1.Integer(1),
|
158
|
+
OpenSSL::ASN1.OctetString(secret),
|
159
|
+
OpenSSL::ASN1.ObjectId(curve_name, 0, :EXPLICIT)
|
160
|
+
])
|
161
|
+
|
162
|
+
OpenSSL::PKey.read(asn1_seq.to_der)
|
163
|
+
end
|
164
|
+
|
165
|
+
def serialize_public_key(pk)
|
166
|
+
pk.public_key.to_bn.to_s(2)
|
167
|
+
end
|
168
|
+
|
169
|
+
def deserialize_public_key(serialized_pk)
|
170
|
+
asn1_seq = OpenSSL::ASN1.Sequence([
|
171
|
+
OpenSSL::ASN1.Sequence([
|
172
|
+
OpenSSL::ASN1.ObjectId("id-ecPublicKey"),
|
173
|
+
OpenSSL::ASN1.ObjectId(curve_name)
|
174
|
+
]),
|
175
|
+
OpenSSL::ASN1.BitString(serialized_pk)
|
176
|
+
])
|
177
|
+
|
178
|
+
OpenSSL::PKey.read(asn1_seq.to_der)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class HPKE::DHKEM::EC::P_256 < HPKE::DHKEM::EC
|
183
|
+
def kem_id
|
184
|
+
0x0010
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def n_secret
|
190
|
+
32
|
191
|
+
end
|
192
|
+
|
193
|
+
def n_enc
|
194
|
+
65
|
195
|
+
end
|
196
|
+
|
197
|
+
def n_pk
|
198
|
+
65
|
199
|
+
end
|
200
|
+
|
201
|
+
def n_sk
|
202
|
+
32
|
203
|
+
end
|
204
|
+
|
205
|
+
def order
|
206
|
+
0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
|
207
|
+
end
|
208
|
+
|
209
|
+
def curve_name
|
210
|
+
'prime256v1'
|
211
|
+
end
|
212
|
+
|
213
|
+
def bitmask
|
214
|
+
0xff
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class HPKE::DHKEM::EC::P_384 < HPKE::DHKEM::EC
|
219
|
+
def kem_id
|
220
|
+
0x0011
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
def n_secret
|
226
|
+
48
|
227
|
+
end
|
228
|
+
|
229
|
+
def n_enc
|
230
|
+
97
|
231
|
+
end
|
232
|
+
|
233
|
+
def n_pk
|
234
|
+
97
|
235
|
+
end
|
236
|
+
|
237
|
+
def n_sk
|
238
|
+
48
|
239
|
+
end
|
240
|
+
|
241
|
+
def order
|
242
|
+
0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973
|
243
|
+
end
|
244
|
+
|
245
|
+
def curve_name
|
246
|
+
'secp384r1'
|
247
|
+
end
|
248
|
+
|
249
|
+
def bitmask
|
250
|
+
0xff
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
class HPKE::DHKEM::EC::P_521 < HPKE::DHKEM::EC
|
255
|
+
def kem_id
|
256
|
+
0x0012
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
|
261
|
+
def n_secret
|
262
|
+
64
|
263
|
+
end
|
264
|
+
|
265
|
+
def n_enc
|
266
|
+
133
|
267
|
+
end
|
268
|
+
|
269
|
+
def n_pk
|
270
|
+
133
|
271
|
+
end
|
272
|
+
|
273
|
+
def n_sk
|
274
|
+
66
|
275
|
+
end
|
276
|
+
|
277
|
+
def order
|
278
|
+
0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409
|
279
|
+
end
|
280
|
+
|
281
|
+
def curve_name
|
282
|
+
'secp521r1'
|
283
|
+
end
|
284
|
+
|
285
|
+
def bitmask
|
286
|
+
0x01
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
class HPKE::DHKEM::X25519 < HPKE::DHKEM
|
291
|
+
def kem_id
|
292
|
+
0x0020
|
293
|
+
end
|
294
|
+
|
295
|
+
def create_key_pair_from_secret(secret)
|
296
|
+
asn1_seq = OpenSSL::ASN1.Sequence([
|
297
|
+
OpenSSL::ASN1.Integer(0),
|
298
|
+
OpenSSL::ASN1.Sequence([
|
299
|
+
OpenSSL::ASN1.ObjectId(asn1_oid)
|
300
|
+
]),
|
301
|
+
OpenSSL::ASN1.OctetString("\x04\x20" + secret)
|
302
|
+
])
|
303
|
+
|
304
|
+
OpenSSL::PKey.read(asn1_seq.to_der)
|
305
|
+
end
|
306
|
+
|
307
|
+
private
|
308
|
+
|
309
|
+
def n_secret
|
310
|
+
32
|
311
|
+
end
|
312
|
+
|
313
|
+
def n_enc
|
314
|
+
32
|
315
|
+
end
|
316
|
+
|
317
|
+
def n_pk
|
318
|
+
32
|
319
|
+
end
|
320
|
+
|
321
|
+
def n_sk
|
322
|
+
32
|
323
|
+
end
|
324
|
+
|
325
|
+
def asn1_oid
|
326
|
+
'1.3.101.110'
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
class HPKE::DHKEM::X448 < HPKE::DHKEM
|
331
|
+
def kem_id
|
332
|
+
0x0021
|
333
|
+
end
|
334
|
+
|
335
|
+
def create_key_pair_from_secret(secret)
|
336
|
+
asn1_seq = OpenSSL::ASN1.Sequence([
|
337
|
+
OpenSSL::ASN1.Integer(0),
|
338
|
+
OpenSSL::ASN1.Sequence([
|
339
|
+
OpenSSL::ASN1.ObjectId(asn1_oid)
|
340
|
+
]),
|
341
|
+
OpenSSL::ASN1.OctetString("\x04\x38" + secret)
|
342
|
+
])
|
343
|
+
|
344
|
+
OpenSSL::PKey.read(asn1_seq.to_der)
|
345
|
+
end
|
346
|
+
|
347
|
+
private
|
348
|
+
|
349
|
+
def n_secret
|
350
|
+
64
|
351
|
+
end
|
352
|
+
|
353
|
+
def n_enc
|
354
|
+
56
|
355
|
+
end
|
356
|
+
|
357
|
+
def n_pk
|
358
|
+
56
|
359
|
+
end
|
360
|
+
|
361
|
+
def n_sk
|
362
|
+
56
|
363
|
+
end
|
364
|
+
|
365
|
+
def asn1_oid
|
366
|
+
'1.3.101.111'
|
367
|
+
end
|
368
|
+
end
|
data/lib/hpke/hkdf.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require_relative 'util'
|
3
|
+
|
4
|
+
class HPKE::HKDF
|
5
|
+
include HPKE::Util
|
6
|
+
|
7
|
+
attr_reader :kdf_id
|
8
|
+
|
9
|
+
ALGORITHMS = {
|
10
|
+
sha256: {
|
11
|
+
name: 'SHA256',
|
12
|
+
kdf_id: 1
|
13
|
+
},
|
14
|
+
sha384: {
|
15
|
+
name: 'SHA384',
|
16
|
+
kdf_id: 2
|
17
|
+
},
|
18
|
+
sha512: {
|
19
|
+
name: 'SHA512',
|
20
|
+
kdf_id: 3
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
def n_h
|
25
|
+
@digest.digest_length
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(alg_name)
|
29
|
+
if algorithm = ALGORITHMS[alg_name]
|
30
|
+
@digest = OpenSSL::Digest.new(algorithm[:name])
|
31
|
+
@kdf_id = algorithm[:kdf_id]
|
32
|
+
else
|
33
|
+
raise Exception.new('Unknown hash algorithm')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def hmac(key, data)
|
38
|
+
OpenSSL::HMAC.digest(@digest, key, data)
|
39
|
+
end
|
40
|
+
|
41
|
+
def extract(salt, ikm)
|
42
|
+
hmac(salt, ikm)
|
43
|
+
end
|
44
|
+
|
45
|
+
def expand(prk, info, len)
|
46
|
+
n = (len.to_f / @digest.digest_length).ceil
|
47
|
+
t = ['']
|
48
|
+
for i in 0..n do
|
49
|
+
t << hmac(prk, t[i] + info + (i + 1).chr)
|
50
|
+
end
|
51
|
+
t_concat = t.join
|
52
|
+
t_concat[0..(len - 1)]
|
53
|
+
end
|
54
|
+
|
55
|
+
def labeled_extract(salt, label, ikm, suite_id)
|
56
|
+
labeled_ikm = 'HPKE-v1' + suite_id + label + ikm
|
57
|
+
extract(salt, labeled_ikm)
|
58
|
+
end
|
59
|
+
|
60
|
+
def labeled_expand(prk, label, info, l, suite_id)
|
61
|
+
labeled_info = i2osp(l, 2) + 'HPKE-v1' + suite_id + label + info
|
62
|
+
expand(prk, labeled_info, l)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class HPKE::HKDF::HMAC_SHA256 < HPKE::HKDF
|
67
|
+
private
|
68
|
+
|
69
|
+
def digest_algorithm
|
70
|
+
'SHA256'
|
71
|
+
end
|
72
|
+
|
73
|
+
def kdf_id
|
74
|
+
1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class HPKE::HKDF::HMAC_SHA384 < HPKE::HKDF
|
79
|
+
private
|
80
|
+
|
81
|
+
def digest_algorithm
|
82
|
+
'SHA384'
|
83
|
+
end
|
84
|
+
|
85
|
+
def kdf_id
|
86
|
+
2
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class HPKE::HKDF::HMAC_SHA512 < HPKE::HKDF
|
91
|
+
private
|
92
|
+
|
93
|
+
def digest_algorithm
|
94
|
+
'SHA512'
|
95
|
+
end
|
96
|
+
|
97
|
+
def kdf_id
|
98
|
+
3
|
99
|
+
end
|
100
|
+
end
|
data/lib/hpke/util.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module HPKE::Util
|
2
|
+
def i2osp(n, w)
|
3
|
+
# check n > 0 and n < 256 ** w
|
4
|
+
ret = []
|
5
|
+
for i in 0..(w - 1)
|
6
|
+
ret[w - (i + 1)] = n % 256
|
7
|
+
n = n >> 8
|
8
|
+
end
|
9
|
+
ret.map(&:chr).join
|
10
|
+
end
|
11
|
+
|
12
|
+
def os2ip(x)
|
13
|
+
x.bytes.reduce { |a, b| a * 256 + b }
|
14
|
+
end
|
15
|
+
|
16
|
+
def xor(a, b)
|
17
|
+
if a.bytesize != b.bytesize
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
c = ""
|
21
|
+
for i in 0 .. (a.bytesize - 1)
|
22
|
+
c += (a.bytes[i] ^ b.bytes[i]).chr
|
23
|
+
end
|
24
|
+
c
|
25
|
+
end
|
26
|
+
end
|
data/lib/hpke/version.rb
ADDED
data/lib/hpke.rb
ADDED
@@ -0,0 +1,300 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "hpke/version"
|
4
|
+
|
5
|
+
require 'openssl'
|
6
|
+
require_relative './hpke/dhkem'
|
7
|
+
require_relative './hpke/util'
|
8
|
+
|
9
|
+
class HPKE
|
10
|
+
include HPKE::Util
|
11
|
+
|
12
|
+
attr_reader :kem, :hkdf, :aead_name, :n_k, :n_n, :n_t
|
13
|
+
|
14
|
+
MODES = {
|
15
|
+
base: 0x00,
|
16
|
+
psk: 0x01,
|
17
|
+
auth: 0x02,
|
18
|
+
auth_psk: 0x03
|
19
|
+
}
|
20
|
+
CIPHERS = {
|
21
|
+
aes_128_gcm: {
|
22
|
+
name: 'aes-128-gcm',
|
23
|
+
aead_id: 0x0001,
|
24
|
+
n_k: 16,
|
25
|
+
n_n: 12,
|
26
|
+
n_t: 16
|
27
|
+
},
|
28
|
+
aes_256_gcm: {
|
29
|
+
name: 'aes-256-gcm',
|
30
|
+
aead_id: 0x0002,
|
31
|
+
n_k: 32,
|
32
|
+
n_n: 12,
|
33
|
+
n_t: 16
|
34
|
+
},
|
35
|
+
chacha20_poly1305: {
|
36
|
+
name: 'chacha20-poly1305',
|
37
|
+
aead_id: 0x0003,
|
38
|
+
n_k: 32,
|
39
|
+
n_n: 12,
|
40
|
+
n_t: 16
|
41
|
+
},
|
42
|
+
export_only: {
|
43
|
+
aead_id: 0xffff
|
44
|
+
}
|
45
|
+
}
|
46
|
+
HASHES = {
|
47
|
+
sha256: {
|
48
|
+
name: 'SHA256',
|
49
|
+
kdf_id: 1
|
50
|
+
},
|
51
|
+
sha384: {
|
52
|
+
name: 'SHA384',
|
53
|
+
kdf_id: 2
|
54
|
+
},
|
55
|
+
sha512: {
|
56
|
+
name: 'SHA512',
|
57
|
+
kdf_id: 3
|
58
|
+
}
|
59
|
+
}
|
60
|
+
KEM_CURVES = {
|
61
|
+
p_256: DHKEM::EC::P_256,
|
62
|
+
p_384: DHKEM::EC::P_384,
|
63
|
+
p_521: DHKEM::EC::P_521,
|
64
|
+
x25519: DHKEM::X25519,
|
65
|
+
x448: DHKEM::X448
|
66
|
+
}
|
67
|
+
|
68
|
+
def initialize(kem_curve_name, kem_hash, kdf_hash, aead_cipher)
|
69
|
+
raise Exception.new('Unsupported KEM curve name') if KEM_CURVES[kem_curve_name].nil?
|
70
|
+
raise Exception.new('Unsupported AEAD cipher name') if CIPHERS[aead_cipher].nil?
|
71
|
+
|
72
|
+
@kem = KEM_CURVES[kem_curve_name].new(kem_hash)
|
73
|
+
@hkdf = HKDF.new(kdf_hash)
|
74
|
+
@aead_name = CIPHERS[aead_cipher][:name]
|
75
|
+
@aead_id = CIPHERS[aead_cipher][:aead_id]
|
76
|
+
@n_k = CIPHERS[aead_cipher][:n_k]
|
77
|
+
@n_n = CIPHERS[aead_cipher][:n_n]
|
78
|
+
@n_t = CIPHERS[aead_cipher][:n_t]
|
79
|
+
end
|
80
|
+
|
81
|
+
# public facing APIs
|
82
|
+
def setup_base_s(pk_r, info)
|
83
|
+
encap_result = @kem.encap(pk_r)
|
84
|
+
{
|
85
|
+
enc: encap_result[:enc],
|
86
|
+
context_s: key_schedule_s(MODES[:base], encap_result[:shared_secret], info, DEFAULT_PSK, DEFAULT_PSK_ID)
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def setup_base_r(enc, sk_r, info)
|
91
|
+
shared_secret = @kem.decap(enc, sk_r)
|
92
|
+
key_schedule_r(MODES[:base], shared_secret, info, DEFAULT_PSK, DEFAULT_PSK_ID)
|
93
|
+
end
|
94
|
+
|
95
|
+
def setup_psk_s(pk_r, info, psk, psk_id)
|
96
|
+
encap_result = @kem.encap(pk_r)
|
97
|
+
{
|
98
|
+
enc: encap_result[:enc],
|
99
|
+
context_s: key_schedule_s(MODES[:psk], encap_result[:shared_secret], info, psk, psk_id)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def setup_psk_r(enc, sk_r, info, psk, psk_id)
|
104
|
+
shared_secret = @kem.decap(enc, sk_r)
|
105
|
+
key_schedule_r(MODES[:psk], shared_secret, info, psk, psk_id)
|
106
|
+
end
|
107
|
+
|
108
|
+
def setup_auth_s(pk_r, info, sk_s)
|
109
|
+
encap_result = @kem.auth_encap(pk_r, sk_s)
|
110
|
+
{
|
111
|
+
enc: encap_result[:enc],
|
112
|
+
context_s: key_schedule_s(MODES[:auth], encap_result[:shared_secret], info, DEFAULT_PSK, DEFAULT_PSK_ID)
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def setup_auth_r(enc, sk_r, info, pk_s)
|
117
|
+
shared_secret = @kem.auth_decap(enc, sk_r, pk_s)
|
118
|
+
key_schedule_r(MODES[:auth], shared_secret, info, DEFAULT_PSK, DEFAULT_PSK_ID)
|
119
|
+
end
|
120
|
+
|
121
|
+
def setup_auth_psk_s(pk_r, info, psk, psk_id, sk_s)
|
122
|
+
encap_result = @kem.auth_encap(pk_r, sk_s)
|
123
|
+
{
|
124
|
+
enc: encap_result[:enc],
|
125
|
+
context_s: key_schedule_s(MODES[:auth_psk], encap_result[:shared_secret], info, psk, psk_id)
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def setup_auth_psk_r(enc, sk_r, info, psk, psk_id, pk_s)
|
130
|
+
shared_secret = @kem.auth_decap(enc, sk_r, pk_s)
|
131
|
+
key_schedule_r(MODES[:auth_psk], shared_secret, info, psk, psk_id)
|
132
|
+
end
|
133
|
+
|
134
|
+
# for testing purposes
|
135
|
+
def setup_base_s_fixed(pk_r, info, ikm_e)
|
136
|
+
encap_result = @kem.encap_fixed(pk_r, ikm_e)
|
137
|
+
{
|
138
|
+
enc: encap_result[:enc],
|
139
|
+
context_s: key_schedule_s(MODES[:base], encap_result[:shared_secret], info, DEFAULT_PSK, DEFAULT_PSK_ID)
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def setup_psk_s_fixed(pk_r, info, psk, psk_id, ikm_e)
|
144
|
+
encap_result = @kem.encap_fixed(pk_r, ikm_e)
|
145
|
+
{
|
146
|
+
enc: encap_result[:enc],
|
147
|
+
context_s: key_schedule_s(MODES[:psk], encap_result[:shared_secret], info, psk, psk_id)
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def setup_auth_s_fixed(pk_r, info, sk_s, ikm_e)
|
152
|
+
encap_result = @kem.auth_encap_fixed(pk_r, sk_s, ikm_e)
|
153
|
+
{
|
154
|
+
enc: encap_result[:enc],
|
155
|
+
context_s: key_schedule_s(MODES[:auth], encap_result[:shared_secret], info, DEFAULT_PSK, DEFAULT_PSK_ID)
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
def setup_auth_psk_s_fixed(pk_r, info, psk, psk_id, sk_s, ikm_e)
|
160
|
+
encap_result = @kem.auth_encap_fixed(pk_r, sk_s, ikm_e)
|
161
|
+
{
|
162
|
+
enc: encap_result[:enc],
|
163
|
+
context_s: key_schedule_s(MODES[:auth_psk], encap_result[:shared_secret], info, psk, psk_id)
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
def export(exporter_secret, exporter_context, len)
|
168
|
+
@hkdf.labeled_expand(exporter_secret, 'sec', exporter_context, len, suite_id)
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def suite_id
|
174
|
+
'HPKE' + i2osp(@kem.kem_id, 2) + i2osp(@hkdf.kdf_id, 2) + i2osp(@aead_id, 2)
|
175
|
+
end
|
176
|
+
|
177
|
+
DEFAULT_PSK = ''
|
178
|
+
DEFAULT_PSK_ID = ''
|
179
|
+
|
180
|
+
def verify_psk_inputs(mode, psk, psk_id)
|
181
|
+
got_psk = (psk != DEFAULT_PSK)
|
182
|
+
got_psk_id = (psk_id != DEFAULT_PSK_ID)
|
183
|
+
|
184
|
+
raise Exception.new('Inconsistent PSK inputs') if got_psk != got_psk_id
|
185
|
+
raise Exception.new('PSK input provided when not needed') if got_psk && [MODES[:base], MODES[:auth]].include?(mode)
|
186
|
+
raise Exception.new('Missing required PSK input') if !got_psk && [MODES[:psk], MODES[:auth_psk]].include?(mode)
|
187
|
+
|
188
|
+
true
|
189
|
+
end
|
190
|
+
|
191
|
+
def key_schedule(mode, shared_secret, info, psk = '', psk_id = '')
|
192
|
+
verify_psk_inputs(mode, psk, psk_id)
|
193
|
+
|
194
|
+
psk_id_hash = @hkdf.labeled_extract('', 'psk_id_hash', psk_id, suite_id)
|
195
|
+
info_hash = @hkdf.labeled_extract('', 'info_hash', info, suite_id)
|
196
|
+
key_schedule_context = mode.chr + psk_id_hash + info_hash
|
197
|
+
|
198
|
+
secret = @hkdf.labeled_extract(shared_secret, 'secret', psk, suite_id)
|
199
|
+
|
200
|
+
unless @aead_id == CIPHERS[:export_only][:aead_id]
|
201
|
+
key = @hkdf.labeled_expand(secret, 'key', key_schedule_context, @n_k, suite_id)
|
202
|
+
base_nonce = @hkdf.labeled_expand(secret, 'base_nonce', key_schedule_context, @n_n, suite_id)
|
203
|
+
end
|
204
|
+
exporter_secret = @hkdf.labeled_expand(secret, 'exp', key_schedule_context, @hkdf.n_h, suite_id)
|
205
|
+
|
206
|
+
{
|
207
|
+
key: key,
|
208
|
+
base_nonce: base_nonce,
|
209
|
+
sequence_number: 0,
|
210
|
+
exporter_secret: exporter_secret
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
def key_schedule_s(mode, shared_secret, info, psk = '', psk_id = '')
|
215
|
+
ks = key_schedule(mode, shared_secret, info, psk, psk_id)
|
216
|
+
HPKE::ContextS.new(ks, self)
|
217
|
+
end
|
218
|
+
|
219
|
+
def key_schedule_r(mode, shared_secret, info, psk = '', psk_id = '')
|
220
|
+
ks = key_schedule(mode, shared_secret, info, psk, psk_id)
|
221
|
+
HPKE::ContextR.new(ks, self)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
class HPKE::Context
|
226
|
+
include HPKE::Util
|
227
|
+
attr_reader :key, :base_nonce, :sequence_number, :exporter_secret
|
228
|
+
|
229
|
+
def initialize(initializer_hash, hpke)
|
230
|
+
@hpke = hpke
|
231
|
+
@key = initializer_hash[:key]
|
232
|
+
@base_nonce = initializer_hash[:base_nonce]
|
233
|
+
@sequence_number = initializer_hash[:sequence_number]
|
234
|
+
@exporter_secret = initializer_hash[:exporter_secret]
|
235
|
+
end
|
236
|
+
|
237
|
+
def compute_nonce(seq)
|
238
|
+
seq_bytes = i2osp(seq, @hpke.n_n)
|
239
|
+
xor(@base_nonce, seq_bytes)
|
240
|
+
end
|
241
|
+
|
242
|
+
def increment_seq
|
243
|
+
raise Exception.new('MessageLimitReachedError') if @sequence_number >= (1 << (8 * @hpke.n_n)) - 1
|
244
|
+
|
245
|
+
@sequence_number += 1
|
246
|
+
end
|
247
|
+
|
248
|
+
def export(exporter_context, len)
|
249
|
+
@hpke.export(@exporter_secret, exporter_context, len)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
class HPKE::ContextS < HPKE::Context
|
254
|
+
def seal(aad, pt)
|
255
|
+
raise Exception.new('AEAD is export only') if @hpke.aead_name == :export_only
|
256
|
+
|
257
|
+
ct = cipher_seal(@key, compute_nonce(@sequence_number), aad, pt)
|
258
|
+
increment_seq
|
259
|
+
ct
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
def cipher_seal(key, nonce, aad, pt)
|
265
|
+
cipher = OpenSSL::Cipher.new(@hpke.aead_name)
|
266
|
+
cipher.encrypt
|
267
|
+
cipher.key = key
|
268
|
+
cipher.iv = nonce
|
269
|
+
cipher.auth_data = aad
|
270
|
+
cipher.padding = 0
|
271
|
+
s = cipher.update(pt) << cipher.final
|
272
|
+
s + cipher.auth_tag
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
class HPKE::ContextR < HPKE::Context
|
277
|
+
def open(aad, ct)
|
278
|
+
raise Exception.new('AEAD is export only') if @hpke.aead_name == :export_only
|
279
|
+
|
280
|
+
pt = cipher_open(@key, compute_nonce(@sequence_number), aad, ct)
|
281
|
+
# TODO: catch openerror then send out own openerror
|
282
|
+
increment_seq
|
283
|
+
pt
|
284
|
+
end
|
285
|
+
|
286
|
+
private
|
287
|
+
|
288
|
+
def cipher_open(key, nonce, aad, ct)
|
289
|
+
ct_body = ct[0, ct.length - @hpke.n_t]
|
290
|
+
tag = ct[-@hpke.n_t, @hpke.n_t]
|
291
|
+
cipher = OpenSSL::Cipher.new(@hpke.aead_name)
|
292
|
+
cipher.decrypt
|
293
|
+
cipher.key = key
|
294
|
+
cipher.iv = nonce
|
295
|
+
cipher.auth_tag = tag
|
296
|
+
cipher.auth_data = aad
|
297
|
+
cipher.padding = 0
|
298
|
+
cipher.update(ct_body) << cipher.final
|
299
|
+
end
|
300
|
+
end
|
data/sig/hpke.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hpke
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryo Kajiwara
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-07-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: openssl
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.0.0
|
27
|
+
description: Hybrid Public Key Encryption (HPKE; RFC 9180) on Ruby
|
28
|
+
email:
|
29
|
+
- sylph01@s01.ninja
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".rspec"
|
35
|
+
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- LICENSE.txt
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- lib/hpke.rb
|
41
|
+
- lib/hpke/dhkem.rb
|
42
|
+
- lib/hpke/hkdf.rb
|
43
|
+
- lib/hpke/util.rb
|
44
|
+
- lib/hpke/version.rb
|
45
|
+
- sig/hpke.rbs
|
46
|
+
homepage: https://github.com/sylph01/hpke-rb
|
47
|
+
licenses:
|
48
|
+
- MIT
|
49
|
+
metadata:
|
50
|
+
homepage_uri: https://github.com/sylph01/hpke-rb
|
51
|
+
source_code_uri: https://github.com/sylph01/hpke-rb
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 3.1.0
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.4.10
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Hybrid Public Key Encryption (HPKE; RFC 9180) on Ruby
|
71
|
+
test_files: []
|