| 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. |
| 7 | module curve25519 |
| 8 | |
| 9 | import crypto.rand |
| 10 | import crypto.internal.subtle |
| 11 | import crypto.ed25519.internal.edwards25519 |
| 12 | |
| 13 | // scalar_size is the size of the Curve25519 key |
| 14 | const scalar_size = 32 |
| 15 | |
| 16 | // point_size is the size of the Curve25519 point |
| 17 | const point_size = 32 |
| 18 | |
| 19 | // zero_point is point with 32 bytes length of zeros bytes |
| 20 | const 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 |
| 24 | const 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] |
| 29 | pub struct PrivateKey { |
| 30 | mut: |
| 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] |
| 40 | pub 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] |
| 56 | pub 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] |
| 73 | pub 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] |
| 85 | pub 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] |
| 97 | pub 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 |
| 120 | pub 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] |
| 129 | pub 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] |
| 143 | pub struct PublicKey { |
| 144 | mut: |
| 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. |
| 150 | pub 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 |
| 175 | pub 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 |
| 184 | pub 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] |
| 193 | pub struct SharedOpts { |
| 194 | pub 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] |
| 202 | pub struct DeriveOpts {} |
| 203 | |
| 204 | // Derivator represent key derivation function |
| 205 | pub 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. |
| 211 | struct RawDerivator {} |
| 212 | |
| 213 | fn (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 |
| 223 | pub 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] |
| 264 | pub 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] |
| 277 | fn 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] |
| 298 | fn 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] |
| 307 | fn 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] |
| 374 | fn 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] |
| 382 | fn 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] |
| 391 | fn 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. |
| 406 | fn 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 | |