v2 / vlib / x / crypto / curve25519 / curve25519.v
412 lines · 369 sloc · 12.12 KB · 226359ca71c6169f72c91b5457fe0004b084ff85
Raw
1// Copyright © 2025 blackshirt.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4//
5// This module implements building block for elliptic-curve diffie-helman
6// key exchange (ECDH) mechanism through curve25519 curve.
7module curve25519
8
9import crypto.rand
10import crypto.internal.subtle
11import crypto.ed25519.internal.edwards25519
12
13// scalar_size is the size of the Curve25519 key
14const scalar_size = 32
15
16// point_size is the size of the Curve25519 point
17const point_size = 32
18
19// zero_point is point with 32 bytes length of zeros bytes
20const zero_point = []u8{len: 32, init: u8(0x00)}
21
22// base_point is the canonical Curve25519 generator, encoded as a byte with value 9,
23// followed by 31 zero bytes
24const base_point = [u8(9), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
25 0, 0, 0, 0, 0, 0, 0, 0]
26
27// PrivateKey represents Curve25519 private key
28@[noinit]
29pub struct PrivateKey {
30mut:
31 // boolean flag that tells this key should not be
32 // used again when its true. its set after .free call
33 done bool
34 // clamped key, 32 bytes length
35 key []u8
36}
37
38// new creates a new Curve25519 key using randomly generated bytes from `crypto.rand`.
39@[direct_array_access]
40pub fn PrivateKey.new() !&PrivateKey {
41 mut bytes := rand.read(scalar_size)!
42 // do bytes clamping
43 clamp(mut bytes)!
44
45 if is_zero_point(bytes) || is_base_point(bytes) {
46 return error('PrivateKey.new: bytes zeros or base point')
47 }
48
49 return &PrivateKey{
50 key: bytes
51 }
52}
53
54// new_from_seed creates a new Curve25519 key from provided seed bytes.
55@[direct_array_access]
56pub fn PrivateKey.new_from_seed(seed []u8) !&PrivateKey {
57 if seed.len != scalar_size {
58 return error('invalid scalar size')
59 }
60 mut bytes := seed.clone()
61 clamp(mut bytes)!
62
63 if is_zero_point(bytes) || is_base_point(bytes) {
64 return error('PrivateKey.new_from_seed: bytes was zeros or base point')
65 }
66 return &PrivateKey{
67 key: bytes
68 }
69}
70
71// public_key returns the associated public key part of the PrivateKey.
72@[direct_array_access]
73pub fn (mut pv PrivateKey) public_key() !&PublicKey {
74 if pv.done {
75 return error('your key has been marked as freed')
76 }
77 out := x25519(mut pv.key, base_point)!
78 return &PublicKey{
79 key: out
80 }
81}
82
83// equal returns whether two private keys are equal.
84@[direct_array_access]
85pub fn (pv PrivateKey) equal(oth PrivateKey) bool {
86 if pv.done || oth.done {
87 panic('the key has been marked as freed')
88 }
89 if pv.key.len != scalar_size || oth.key.len != scalar_size {
90 return false
91 }
92 return subtle.constant_time_compare(pv.key, oth.key) == 1
93}
94
95// x25519 performs scalar multiplication between key and point and return another bytes (point).
96@[direct_array_access]
97pub fn (mut pv PrivateKey) x25519(point []u8) ![]u8 {
98 if pv.done {
99 return error('PrivateKey has been marked as freed')
100 }
101 if point.len != point_size {
102 return error('bad point size, should be 32')
103 }
104 // We reject and disallow zero-bytes point to be passed
105 // and check it here as a quick exit before heavy math
106 // calculation on `x25519` call
107 if is_zero(point) {
108 return error('x25519: get zeros point')
109 }
110 // even technically its possible, but we limit to unallow it
111 if subtle.constant_time_compare(pv.key, point) == 1 {
112 return error('pv.key identical with point')
113 }
114 out := x25519(mut pv.key, point)!
115
116 return out
117}
118
119// bytes return a clone of the bytes of the underlying PrivateKey
120pub fn (pv PrivateKey) bytes() ![]u8 {
121 if pv.done {
122 return error('PrivateKey has been marked as freed')
123 }
124 return pv.key.clone()
125}
126
127// free releases underlying key. Dont use the key after calling .free
128@[unsafe]
129pub fn (mut pv PrivateKey) free() {
130 // when private key has been marked as done (freed),
131 // calling free on already freed key would lead to undefined behavior.
132 // so, we check it
133 if pv.done {
134 return
135 }
136 unsafe { pv.key.free() }
137 // sets flag to finish
138 pv.done = true
139}
140
141// PublicKey represent Curve25519 key.
142@[noinit]
143pub struct PublicKey {
144mut:
145 // 32 bytes length of scalar * point
146 key []u8
147}
148
149// new_from_bytes creates a new Curve25519 public key from provided bytes.
150pub fn PublicKey.new_from_bytes(bytes []u8) !&PublicKey {
151 if bytes.len != point_size {
152 return error('PublicKey.new: bad bytes length')
153 }
154 // Refers to the D.J. Bernstein, the designer of the curve25519, public key validation
155 // in curve25519 is generally not needed for Diffie-Hellman key exchange.
156 // See https://cr.yp.to/ecdh.html#validate
157 // But there are availables suggestion to do validation on them spreads on the internet, likes
158 // - blacklisting the known bad public keys
159 // - check the shared value and to raise exception if it is zero.
160 // - You can also bind the exchanged public keys to the shared keys, i.e.,
161 // instead of using H(abG) as the shared keys, you should use H(aG || bG || abG)
162 //
163 // We only, check for zeros public key
164 if is_zero(bytes) {
165 return error('PublicKey.new: get zeros bytes')
166 }
167
168 // otherwise, we can return it
169 return &PublicKey{
170 key: bytes
171 }
172}
173
174// equal tells whether two public keys are equal
175pub fn (pb PublicKey) equal(other PublicKey) bool {
176 // different length, should not happen
177 if pb.key.len != point_size || other.key.len != point_size {
178 return false
179 }
180 return subtle.constant_time_compare(pb.key, other.key) == 1
181}
182
183// bytes return the clone of the bytes of the underlying PublicKey
184pub fn (pb PublicKey) bytes() ![]u8 {
185 if pb.key.len != point_size {
186 return error('bad public key size')
187 }
188 return pb.key.clone()
189}
190
191// SharedOpts is the configuration options to `derive_shared_secret` routine
192@[params]
193pub struct SharedOpts {
194pub mut:
195 should_derive bool
196 derivator Derivator = RawDerivator{}
197 drv_opts DeriveOpts
198}
199
200// DeriveOpts is config to drive the Derivator's derive operation.
201@[params]
202pub struct DeriveOpts {}
203
204// Derivator represent key derivation function
205pub interface Derivator {
206 // derive transforms bytes in sec into another form of bytes.
207 derive(sec []u8, opt DeriveOpts) ![]u8
208}
209
210// RawDerivator was a simple derivator with no derivation behaviour.
211struct RawDerivator {}
212
213fn (rd RawDerivator) derive(sec []u8, opt DeriveOpts) ![]u8 {
214 return sec
215}
216
217// derive_shared_secret derives a shared secret between two peer's
218// between first private key's peer and the second PublicKey's peer.
219// Its accepts SharedOpts options to advance supports for other key derivation mechanism.
220//
221// 6. Diffie-Hellman with Curve25519
222// See https://datatracker.ietf.org/doc/html/rfc7748#section-6
223pub fn derive_shared_secret(mut local PrivateKey, remote PublicKey, opt SharedOpts) ![]u8 {
224 // TODO: should this check be relaxed ?
225 // check for safety
226 local_pubkey := local.public_key()!
227 if local_pubkey.equal(remote) {
228 return error('unallowed equal public key between peer')
229 }
230 // The local peer generates local private key, local_privkey, generates local public key, local_pubkey.
231 // and remote peer generates remote private key, ie, remote_privkey,
232 // with generated remote public key, remote_pubkey.
233 // Both now share shared = X25519(local_privkey, remote_pubkey) = X25519(remote_privkey, local_pubkey)
234 // as a shared secret, which is then used as a key or input to a key derivation function.
235 sec := local.x25519(remote.key)!
236 // Internally, x25519 has builtin check for zeros result
237 // but only for non base point branch on x25519_generic routine
238 if is_zero(sec) {
239 return error('zeroes shared secret')
240 }
241 // you can choose this sec as an input into other key derivator, and pass this sec
242 // into provided derivator
243 if opt.should_derive {
244 // While the shared secret can be used directly, it's often recommended to apply
245 // a key derivation function (KDF), like HKDF to derive a more robust key for cryptographic operations.
246 new_sec := opt.derivator.derive(sec, opt.drv_opts)!
247 if is_zero(new_sec) {
248 return error('zeroes shared secret after derivation')
249 }
250 return new_sec
251 }
252 // otherwise, just return the sec as is.
253 return sec
254}
255
256// x25519 returns the result of the scalar multiplication (`scalar` * `point`),
257// according to RFC 7748, Section 5. scalar, point and the return value are slices of 32 bytes.
258// The functions take a scalar and a `u-coordinate` as inputs and produce a `u-coordinate` as output.
259// Although the functions work internally with integers, the inputs and
260// outputs are 32-bytes length (for X25519).
261// scalar can be generated at random, for example with `crypto.rand` and point should
262// be either `base_point` or the output of another `x25519` call.
263@[direct_array_access]
264pub fn x25519(mut scalar []u8, point []u8) ![]u8 {
265 // likes the previous comment, we add zeroes point check here
266 // and reject if it happen.
267 if is_zero(point) || is_zero(scalar) {
268 return error('x25519: unallowed zeros/scalar point')
269 }
270 mut dst := []u8{len: 32}
271 // we do bytes clamping here, to make sure scalar was ready to use
272 clamp(mut scalar)!
273 return x25519_generic(mut dst, mut scalar, point)
274}
275
276@[direct_array_access; inline]
277fn x25519_generic(mut dst []u8, mut scalar []u8, point []u8) ![]u8 {
278 // we dont check arrays length here, its has been checked
279 // on the underlying scalar_mult routine
280 if is_base_point(point) {
281 scalar_base_mult(mut dst, mut scalar)!
282 // check for base_point result
283 if is_base_point(dst) {
284 return error('dst: get base_point')
285 }
286 } else {
287 scalar_mult(mut dst, mut scalar, point)!
288 // check for zeros point result
289 if is_zero_point(dst) {
290 return error('bad input point: low order point')
291 }
292 }
293 return dst
294}
295
296// scalar_base_mult performs scalar * base_point
297@[direct_array_access]
298fn scalar_base_mult(mut dst []u8, mut scalar []u8) ! {
299 scalar_mult(mut dst, mut scalar, base_point)!
300}
301
302// scalar_mult performs scalar multiplicatipn (scalar * point) on the curve25519 curve.
303// for performance reason, scalar marked as mutable.
304// scalar_mult is the main routine to perform scalar multiplication
305// between scalar key and point on the curve25519 curve.
306@[direct_array_access; inline]
307fn scalar_mult(mut dst []u8, mut scalar []u8, point []u8) ! {
308 if dst.len != point_size {
309 return error('bad dst length')
310 }
311 if scalar.len != scalar_size {
312 return error('scalar.lenght != 32')
313 }
314 if point.len != point_size {
315 return error('point.lenght != 32')
316 }
317 // Note: we dont clamping scalar here, and responsible to the caller
318 // to do the clamping. Its assumed scalar has been clamped.
319 mut x1 := edwards25519.Element{}
320 mut x2 := edwards25519.Element{}
321 mut z2 := edwards25519.Element{}
322 mut x3 := edwards25519.Element{}
323 mut z3 := edwards25519.Element{}
324 mut tmp0 := edwards25519.Element{}
325 mut tmp1 := edwards25519.Element{}
326
327 x1.set_bytes(point[..])!
328 x2.one()
329 x3.set(x1)
330 z3.one()
331
332 mut swap := 0
333 for pos := 254; pos >= 0; pos-- {
334 mut b := scalar[pos / 8] >> u32(pos & 7)
335 b &= 1
336 swap = swap ^ int(b)
337 x2.swap(mut x3, swap)
338 z2.swap(mut z3, swap)
339 swap = int(b)
340
341 tmp0.subtract(x3, z3)
342 tmp1.subtract(x2, z2)
343 x2.add(x2, z2)
344 z2.add(x3, z3)
345 z3.multiply(tmp0, x2)
346 z2.multiply(z2, tmp1)
347 tmp0.square(tmp1)
348 tmp1.square(x2)
349 x3.add(z3, z2)
350 z2.subtract(z3, z2)
351 x2.multiply(tmp1, tmp0)
352 tmp1.subtract(tmp1, tmp0)
353 z2.square(z2)
354
355 z3.mult_32(tmp1, 121666)
356 x3.square(x3)
357 tmp0.add(tmp0, z3)
358 z3.multiply(x1, z2)
359 z2.multiply(tmp1, tmp0)
360 }
361
362 x2.swap(mut x3, swap)
363 z2.swap(mut z3, swap)
364
365 z2.invert(z2)
366 x2.multiply(x2, z2)
367 copy(mut dst, x2.bytes())
368}
369
370// Utility helpers
371//
372
373@[direct_array_access; inline]
374fn is_zero_point(point []u8) bool {
375 if point.len != point_size {
376 return false
377 }
378 return subtle.constant_time_compare(point, zero_point) == 1
379}
380
381@[direct_array_access; inline]
382fn is_base_point(point []u8) bool {
383 if point.len != point_size {
384 return false
385 }
386 return subtle.constant_time_compare(point, base_point) == 1
387}
388
389// clamp clears out some bits of seed bytes
390@[direct_array_access; inline]
391fn clamp(mut seed []u8) ! {
392 if seed.len != scalar_size {
393 return error('bad seed sizes for clamp')
394 }
395 // According to RFC 7748, for x25519, in order to decode 32 random bytes
396 // as an integer scalar, set the three least significant bits of the first byte
397 // and the most significant bit of the last to zero,
398 // set the second most significant bit of the last byte to 1
399 //
400 seed[0] &= 248
401 seed[31] &= 127
402 seed[31] |= 64
403}
404
405// is_zero returns whether seed is all zeroes in constant time.
406fn is_zero(seed []u8) bool {
407 mut acc := u8(0)
408 for b in seed {
409 acc |= b
410 }
411 return acc == 0
412}
413