on master help sign in
Add elliptic curve cryptography to standard library
commitf0808f4e2511e85e5a7f25324a5f717437f54872 authoriko <mail@iko.soy> dateThu 19 Mar 2026, 11:17 (6 months ago) parent8fff4ab
7 files changed+605−2
Cargo.lock+297−1 crates/knot-compiler/src/codegen.rs+29 crates/knot-compiler/src/effects.rs+7−1 crates/knot-compiler/src/infer.rs+54 crates/knot-runtime/Cargo.toml+3 crates/knot-runtime/src/lib.rs+181 examples/crypto.knot+34
Cargo.lock+297−1
@@ -2,6 +2,16 @@
22# It is not intended for manual editing.
33version = 4
44
5+[[package]]
6+name = "aead"
7+version = "0.5.2"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
10+dependencies = [
11+ "crypto-common",
12+ "generic-array",
13+]
14+
515[[package]]
616name = "allocator-api2"
717version = "0.2.21"
@@ -38,6 +48,15 @@ version = "2.11.0"
3848source = "registry+https://github.com/rust-lang/crates.io-index"
3949checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
4050
51+[[package]]
52+name = "block-buffer"
53+version = "0.10.4"
54+source = "registry+https://github.com/rust-lang/crates.io-index"
55+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
56+dependencies = [
57+ "generic-array",
58+]
59+
4160[[package]]
4261name = "bumpalo"
4362version = "3.20.2"
@@ -63,12 +82,56 @@ version = "1.0.4"
6382source = "registry+https://github.com/rust-lang/crates.io-index"
6483checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
6584
85+[[package]]
86+name = "chacha20"
87+version = "0.9.1"
88+source = "registry+https://github.com/rust-lang/crates.io-index"
89+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
90+dependencies = [
91+ "cfg-if",
92+ "cipher",
93+ "cpufeatures",
94+]
95+
96+[[package]]
97+name = "chacha20poly1305"
98+version = "0.10.1"
99+source = "registry+https://github.com/rust-lang/crates.io-index"
100+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
101+dependencies = [
102+ "aead",
103+ "chacha20",
104+ "cipher",
105+ "poly1305",
106+ "zeroize",
107+]
108+
66109[[package]]
67110name = "chunked_transfer"
68111version = "1.5.0"
69112source = "registry+https://github.com/rust-lang/crates.io-index"
70113checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
71114
115+[[package]]
116+name = "cipher"
117+version = "0.4.4"
118+source = "registry+https://github.com/rust-lang/crates.io-index"
119+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
120+dependencies = [
121+ "crypto-common",
122+ "inout",
123+ "zeroize",
124+]
125+
126+[[package]]
127+name = "cpufeatures"
128+version = "0.2.17"
129+source = "registry+https://github.com/rust-lang/crates.io-index"
130+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
131+dependencies = [
132+ "libc",
133+]
134+
72135[[package]]
73136name = "cranelift-bforest"
74137version = "0.116.1"
@@ -205,6 +268,75 @@ dependencies = [
205268 "cfg-if",
206269]
207270
271+[[package]]
272+name = "crypto-common"
273+version = "0.1.7"
274+source = "registry+https://github.com/rust-lang/crates.io-index"
275+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
276+dependencies = [
277+ "generic-array",
278+ "rand_core",
279+ "typenum",
280+]
281+
282+[[package]]
283+name = "curve25519-dalek"
284+version = "4.1.3"
285+source = "registry+https://github.com/rust-lang/crates.io-index"
286+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
287+dependencies = [
288+ "cfg-if",
289+ "cpufeatures",
290+ "curve25519-dalek-derive",
291+ "digest",
292+ "fiat-crypto",
293+ "rustc_version",
294+ "subtle",
295+ "zeroize",
296+]
297+
298+[[package]]
299+name = "curve25519-dalek-derive"
300+version = "0.1.1"
301+source = "registry+https://github.com/rust-lang/crates.io-index"
302+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
303+dependencies = [
304+ "proc-macro2",
305+ "quote",
306+ "syn",
307+]
308+
309+[[package]]
310+name = "digest"
311+version = "0.10.7"
312+source = "registry+https://github.com/rust-lang/crates.io-index"
313+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
314+dependencies = [
315+ "block-buffer",
316+ "crypto-common",
317+]
318+
319+[[package]]
320+name = "ed25519"
321+version = "2.2.3"
322+source = "registry+https://github.com/rust-lang/crates.io-index"
323+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
324+dependencies = [
325+ "signature",
326+]
327+
328+[[package]]
329+name = "ed25519-dalek"
330+version = "2.2.0"
331+source = "registry+https://github.com/rust-lang/crates.io-index"
332+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
333+dependencies = [
334+ "curve25519-dalek",
335+ "ed25519",
336+ "sha2",
337+ "subtle",
338+]
339+
208340[[package]]
209341name = "equivalent"
210342version = "1.0.2"
@@ -223,6 +355,12 @@ version = "0.1.9"
223355source = "registry+https://github.com/rust-lang/crates.io-index"
224356checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
225357
358+[[package]]
359+name = "fiat-crypto"
360+version = "0.2.9"
361+source = "registry+https://github.com/rust-lang/crates.io-index"
362+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
363+
226364[[package]]
227365name = "find-msvc-tools"
228366version = "0.1.9"
@@ -235,6 +373,27 @@ version = "0.1.5"
235373source = "registry+https://github.com/rust-lang/crates.io-index"
236374checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
237375
376+[[package]]
377+name = "generic-array"
378+version = "0.14.7"
379+source = "registry+https://github.com/rust-lang/crates.io-index"
380+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
381+dependencies = [
382+ "typenum",
383+ "version_check",
384+]
385+
386+[[package]]
387+name = "getrandom"
388+version = "0.2.17"
389+source = "registry+https://github.com/rust-lang/crates.io-index"
390+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
391+dependencies = [
392+ "cfg-if",
393+ "libc",
394+ "wasi",
395+]
396+
238397[[package]]
239398name = "getrandom"
240399version = "0.3.4"
@@ -304,6 +463,15 @@ dependencies = [
304463 "hashbrown 0.16.1",
305464]
306465
466+[[package]]
467+name = "inout"
468+version = "0.1.4"
469+source = "registry+https://github.com/rust-lang/crates.io-index"
470+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
471+dependencies = [
472+ "generic-array",
473+]
474+
307475[[package]]
308476name = "knot"
309477version = "0.1.0"
@@ -326,12 +494,15 @@ dependencies = [
326494name = "knot-runtime"
327495version = "0.1.0"
328496dependencies = [
329− "getrandom",
497+ "chacha20poly1305",
498+ "ed25519-dalek",
499+ "getrandom 0.3.4",
330500 "num-bigint",
331501 "num-integer",
332502 "num-traits",
333503 "rusqlite",
334504 "tiny_http",
505+ "x25519-dalek",
335506]
336507
337508[[package]]
@@ -403,12 +574,29 @@ dependencies = [
403574 "memchr",
404575]
405576
577+[[package]]
578+name = "opaque-debug"
579+version = "0.3.1"
580+source = "registry+https://github.com/rust-lang/crates.io-index"
581+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
582+
406583[[package]]
407584name = "pkg-config"
408585version = "0.3.32"
409586source = "registry+https://github.com/rust-lang/crates.io-index"
410587checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
411588
589+[[package]]
590+name = "poly1305"
591+version = "0.8.0"
592+source = "registry+https://github.com/rust-lang/crates.io-index"
593+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
594+dependencies = [
595+ "cpufeatures",
596+ "opaque-debug",
597+ "universal-hash",
598+]
599+
412600[[package]]
413601name = "proc-macro2"
414602version = "1.0.106"
@@ -433,6 +621,15 @@ version = "5.3.0"
433621source = "registry+https://github.com/rust-lang/crates.io-index"
434622checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
435623
624+[[package]]
625+name = "rand_core"
626+version = "0.6.4"
627+source = "registry+https://github.com/rust-lang/crates.io-index"
628+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
629+dependencies = [
630+ "getrandom 0.2.17",
631+]
632+
436633[[package]]
437634name = "regalloc2"
438635version = "0.11.2"
@@ -467,6 +664,21 @@ version = "2.1.1"
467664source = "registry+https://github.com/rust-lang/crates.io-index"
468665checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
469666
667+[[package]]
668+name = "rustc_version"
669+version = "0.4.1"
670+source = "registry+https://github.com/rust-lang/crates.io-index"
671+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
672+dependencies = [
673+ "semver",
674+]
675+
676+[[package]]
677+name = "semver"
678+version = "1.0.27"
679+source = "registry+https://github.com/rust-lang/crates.io-index"
680+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
681+
470682[[package]]
471683name = "serde"
472684version = "1.0.228"
@@ -474,6 +686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
474686checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
475687dependencies = [
476688 "serde_core",
689+ "serde_derive",
477690]
478691
479692[[package]]
@@ -496,12 +709,29 @@ dependencies = [
496709 "syn",
497710]
498711
712+[[package]]
713+name = "sha2"
714+version = "0.10.9"
715+source = "registry+https://github.com/rust-lang/crates.io-index"
716+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
717+dependencies = [
718+ "cfg-if",
719+ "cpufeatures",
720+ "digest",
721+]
722+
499723[[package]]
500724name = "shlex"
501725version = "1.3.0"
502726source = "registry+https://github.com/rust-lang/crates.io-index"
503727checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
504728
729+[[package]]
730+name = "signature"
731+version = "2.2.0"
732+source = "registry+https://github.com/rust-lang/crates.io-index"
733+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
734+
505735[[package]]
506736name = "smallvec"
507737version = "1.15.1"
@@ -514,6 +744,12 @@ version = "1.2.1"
514744source = "registry+https://github.com/rust-lang/crates.io-index"
515745checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
516746
747+[[package]]
748+name = "subtle"
749+version = "2.6.1"
750+source = "registry+https://github.com/rust-lang/crates.io-index"
751+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
752+
517753[[package]]
518754name = "syn"
519755version = "2.0.117"
@@ -543,18 +779,46 @@ dependencies = [
543779 "log",
544780]
545781
782+[[package]]
783+name = "typenum"
784+version = "1.19.0"
785+source = "registry+https://github.com/rust-lang/crates.io-index"
786+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
787+
546788[[package]]
547789name = "unicode-ident"
548790version = "1.0.24"
549791source = "registry+https://github.com/rust-lang/crates.io-index"
550792checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
551793
794+[[package]]
795+name = "universal-hash"
796+version = "0.5.1"
797+source = "registry+https://github.com/rust-lang/crates.io-index"
798+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
799+dependencies = [
800+ "crypto-common",
801+ "subtle",
802+]
803+
552804[[package]]
553805name = "vcpkg"
554806version = "0.2.15"
555807source = "registry+https://github.com/rust-lang/crates.io-index"
556808checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
557809
810+[[package]]
811+name = "version_check"
812+version = "0.9.5"
813+source = "registry+https://github.com/rust-lang/crates.io-index"
814+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
815+
816+[[package]]
817+name = "wasi"
818+version = "0.11.1+wasi-snapshot-preview1"
819+source = "registry+https://github.com/rust-lang/crates.io-index"
820+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
821+
558822[[package]]
559823name = "wasip2"
560824version = "1.0.2+wasi-0.2.9"
@@ -569,3 +833,35 @@ name = "wit-bindgen"
569833version = "0.51.0"
570834source = "registry+https://github.com/rust-lang/crates.io-index"
571835checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
836+
837+[[package]]
838+name = "x25519-dalek"
839+version = "2.0.1"
840+source = "registry+https://github.com/rust-lang/crates.io-index"
841+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
842+dependencies = [
843+ "curve25519-dalek",
844+ "rand_core",
845+ "serde",
846+ "zeroize",
847+]
848+
849+[[package]]
850+name = "zeroize"
851+version = "1.8.2"
852+source = "registry+https://github.com/rust-lang/crates.io-index"
853+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
854+dependencies = [
855+ "zeroize_derive",
856+]
857+
858+[[package]]
859+name = "zeroize_derive"
860+version = "1.4.3"
861+source = "registry+https://github.com/rust-lang/crates.io-index"
862+checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
863+dependencies = [
864+ "proc-macro2",
865+ "quote",
866+ "syn",
867+]
crates/knot-compiler/src/codegen.rs+29
@@ -439,6 +439,14 @@ impl Codegen {
439439 self.declare_rt("knot_random_int", &[p], &[p]);
440440 self.declare_rt("knot_random_float", &[], &[p]);
441441
442+ // Elliptic curve cryptography
443+ self.declare_rt("knot_crypto_generate_key_pair", &[], &[p]);
444+ self.declare_rt("knot_crypto_generate_signing_key_pair", &[], &[p]);
445+ self.declare_rt("knot_crypto_encrypt", &[p, p], &[p]);
446+ self.declare_rt("knot_crypto_decrypt", &[p, p], &[p]);
447+ self.declare_rt("knot_crypto_sign", &[p, p], &[p]);
448+ self.declare_rt("knot_crypto_verify", &[p, p, p, p], &[p]);
449+
442450 // Temporal queries (history)
443451 self.declare_rt("knot_now", &[], &[p]);
444452 self.declare_rt("knot_history_init", &[p, p, p, p, p], &[]);
@@ -762,6 +770,7 @@ impl Codegen {
762770 "readFile", "writeFile", "appendFile",
763771 "fileExists", "removeFile", "listDir",
764772 "randomInt",
773+ "encrypt", "decrypt", "sign", "verify",
765774 ];
766775 for name in &stdlib_names {
767776 self.register_stdlib_fn(name);
@@ -1604,6 +1613,14 @@ impl Codegen {
16041613 // Random: 1-param
16051614 self.define_stdlib_fn_1("randomInt", "knot_random_int");
16061615
1616+ // Crypto: 2-param (curried)
1617+ self.define_stdlib_fn_2("encrypt", "knot_crypto_encrypt", false);
1618+ self.define_stdlib_fn_2("decrypt", "knot_crypto_decrypt", false);
1619+ self.define_stdlib_fn_2("sign", "knot_crypto_sign", false);
1620+
1621+ // Crypto: 3-param (double-curried)
1622+ self.define_stdlib_fn_3("verify", "knot_crypto_verify");
1623+
16071624 // File system: 1-param
16081625 self.define_stdlib_fn_1("readFile", "knot_fs_read_file");
16091626 self.define_stdlib_fn_1("fileExists", "knot_fs_file_exists");
@@ -2454,6 +2471,12 @@ impl Codegen {
24542471 if name == "randomFloat" {
24552472 return self.call_rt(builder, "knot_random_float", &[]);
24562473 }
2474+ if name == "generateKeyPair" {
2475+ return self.call_rt(builder, "knot_crypto_generate_key_pair", &[]);
2476+ }
2477+ if name == "generateSigningKeyPair" {
2478+ return self.call_rt(builder, "knot_crypto_generate_signing_key_pair", &[]);
2479+ }
24572480 if name == "readLine" {
24582481 return self.call_rt(builder, "knot_read_line", &[]);
24592482 }
@@ -6211,6 +6234,12 @@ fn is_builtin_name(name: &str) -> bool {
62116234 | "fileExists"
62126235 | "removeFile"
62136236 | "listDir"
6237+ | "generateKeyPair"
6238+ | "generateSigningKeyPair"
6239+ | "encrypt"
6240+ | "decrypt"
6241+ | "sign"
6242+ | "verify"
62146243 )
62156244}
62166245
crates/knot-compiler/src/effects.rs+7−1
@@ -179,7 +179,13 @@ impl EffectChecker {
179179 e
180180 };
181181 builtin_effects.insert("randomInt".into(), random_effect.clone());
182− builtin_effects.insert("randomFloat".into(), random_effect);
182+ builtin_effects.insert("randomFloat".into(), random_effect.clone());
183+ builtin_effects.insert("generateKeyPair".into(), random_effect.clone());
184+ builtin_effects.insert("generateSigningKeyPair".into(), random_effect.clone());
185+ builtin_effects.insert("encrypt".into(), random_effect.clone()); // ephemeral key + nonce
186+ builtin_effects.insert("decrypt".into(), EffectSet::empty());
187+ builtin_effects.insert("sign".into(), EffectSet::empty());
188+ builtin_effects.insert("verify".into(), EffectSet::empty());
183189
184190 // listen: network effect
185191 let network_effect = {
crates/knot-compiler/src/infer.rs+54
@@ -2619,6 +2619,60 @@ impl Infer {
26192619 Box::new(Ty::Fun(Box::new(Ty::Bytes), Box::new(Ty::Int))),
26202620 )),
26212621 );
2622+
2623+ // Elliptic curve cryptography
2624+
2625+ // generateKeyPair : {privateKey: Bytes, publicKey: Bytes}
2626+ let key_pair_record = Ty::Record(
2627+ BTreeMap::from([
2628+ ("privateKey".into(), Ty::Bytes),
2629+ ("publicKey".into(), Ty::Bytes),
2630+ ]),
2631+ None,
2632+ );
2633+ self.bind_top("generateKeyPair", Scheme::mono(key_pair_record.clone()));
2634+
2635+ // generateSigningKeyPair : {privateKey: Bytes, publicKey: Bytes}
2636+ self.bind_top("generateSigningKeyPair", Scheme::mono(key_pair_record));
2637+
2638+ // encrypt : Bytes -> Bytes -> Bytes
2639+ self.bind_top(
2640+ "encrypt",
2641+ Scheme::mono(Ty::Fun(
2642+ Box::new(Ty::Bytes),
2643+ Box::new(Ty::Fun(Box::new(Ty::Bytes), Box::new(Ty::Bytes))),
2644+ )),
2645+ );
2646+
2647+ // decrypt : Bytes -> Bytes -> Bytes
2648+ self.bind_top(
2649+ "decrypt",
2650+ Scheme::mono(Ty::Fun(
2651+ Box::new(Ty::Bytes),
2652+ Box::new(Ty::Fun(Box::new(Ty::Bytes), Box::new(Ty::Bytes))),
2653+ )),
2654+ );
2655+
2656+ // sign : Bytes -> Bytes -> Bytes
2657+ self.bind_top(
2658+ "sign",
2659+ Scheme::mono(Ty::Fun(
2660+ Box::new(Ty::Bytes),
2661+ Box::new(Ty::Fun(Box::new(Ty::Bytes), Box::new(Ty::Bytes))),
2662+ )),
2663+ );
2664+
2665+ // verify : Bytes -> Bytes -> Bytes -> Bool
2666+ self.bind_top(
2667+ "verify",
2668+ Scheme::mono(Ty::Fun(
2669+ Box::new(Ty::Bytes),
2670+ Box::new(Ty::Fun(
2671+ Box::new(Ty::Bytes),
2672+ Box::new(Ty::Fun(Box::new(Ty::Bytes), Box::new(Ty::Bool))),
2673+ )),
2674+ )),
2675+ );
26222676 }
26232677
26242678 fn register_trait_methods(
crates/knot-runtime/Cargo.toml+3
@@ -13,3 +13,6 @@ num-bigint = { workspace = true }
1313num-traits = { workspace = true }
1414num-integer = { workspace = true }
1515getrandom = "0.3"
16+ed25519-dalek = { version = "2", default-features = false }
17+x25519-dalek = { version = "2", features = ["static_secrets"] }
18+chacha20poly1305 = "0.10"
crates/knot-runtime/src/lib.rs+181
@@ -5802,3 +5802,184 @@ pub extern "C" fn knot_relation_index_free(index: *mut c_void) {
58025802 drop(Box::from_raw(index as *mut HashIndex));
58035803 }
58045804}
5805+
5806+// ── Elliptic curve cryptography ──────────────────────────────────
5807+
5808+/// Generate an X25519 key pair for encryption.
5809+/// Returns Record {privateKey: Bytes, publicKey: Bytes}.
5810+#[unsafe(no_mangle)]
5811+pub extern "C" fn knot_crypto_generate_key_pair() -> *mut Value {
5812+ let mut secret_bytes = [0u8; 32];
5813+ getrandom::fill(&mut secret_bytes).expect("knot runtime: failed to generate random bytes");
5814+ let secret = x25519_dalek::StaticSecret::from(secret_bytes);
5815+ let public = x25519_dalek::PublicKey::from(&secret);
5816+
5817+ let record = knot_record_empty(2);
5818+ let k = b"privateKey";
5819+ knot_record_set_field(record, k.as_ptr(), k.len(), alloc(Value::Bytes(secret_bytes.to_vec())));
5820+ let k = b"publicKey";
5821+ knot_record_set_field(record, k.as_ptr(), k.len(), alloc(Value::Bytes(public.as_bytes().to_vec())));
5822+ record
5823+}
5824+
5825+/// Generate an Ed25519 key pair for signing.
5826+/// Returns Record {privateKey: Bytes, publicKey: Bytes}.
5827+#[unsafe(no_mangle)]
5828+pub extern "C" fn knot_crypto_generate_signing_key_pair() -> *mut Value {
5829+ let mut secret_bytes = [0u8; 32];
5830+ getrandom::fill(&mut secret_bytes).expect("knot runtime: failed to generate random bytes");
5831+ let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret_bytes);
5832+ let verifying_key = signing_key.verifying_key();
5833+
5834+ let record = knot_record_empty(2);
5835+ let k = b"privateKey";
5836+ knot_record_set_field(record, k.as_ptr(), k.len(), alloc(Value::Bytes(signing_key.to_bytes().to_vec())));
5837+ let k = b"publicKey";
5838+ knot_record_set_field(record, k.as_ptr(), k.len(), alloc(Value::Bytes(verifying_key.to_bytes().to_vec())));
5839+ record
5840+}
5841+
5842+/// Sealed-box encryption: X25519 ECDH + ChaCha20-Poly1305.
5843+/// Takes (publicKey: Bytes, plaintext: Bytes), returns ciphertext Bytes.
5844+/// Format: [ephemeral_pub: 32][nonce: 12][encrypted + tag: len+16]
5845+#[unsafe(no_mangle)]
5846+pub extern "C" fn knot_crypto_encrypt(public_key: *mut Value, plaintext: *mut Value) -> *mut Value {
5847+ use chacha20poly1305::{ChaCha20Poly1305, KeyInit};
5848+ use chacha20poly1305::aead::Aead;
5849+
5850+ let pub_bytes = match unsafe { as_ref(public_key) } {
5851+ Value::Bytes(b) => b,
5852+ _ => panic!("knot runtime: encrypt expected Bytes for publicKey, got {}", type_name(public_key)),
5853+ };
5854+ let plain = match unsafe { as_ref(plaintext) } {
5855+ Value::Bytes(b) => b,
5856+ _ => panic!("knot runtime: encrypt expected Bytes for plaintext, got {}", type_name(plaintext)),
5857+ };
5858+
5859+ let recipient_pub: [u8; 32] = pub_bytes.as_slice().try_into()
5860+ .expect("knot runtime: encrypt publicKey must be 32 bytes");
5861+ let recipient_public = x25519_dalek::PublicKey::from(recipient_pub);
5862+
5863+ // Generate ephemeral key pair
5864+ let mut eph_secret_bytes = [0u8; 32];
5865+ getrandom::fill(&mut eph_secret_bytes).expect("knot runtime: failed to generate random bytes");
5866+ let eph_secret = x25519_dalek::StaticSecret::from(eph_secret_bytes);
5867+ let eph_public = x25519_dalek::PublicKey::from(&eph_secret);
5868+
5869+ // ECDH shared secret
5870+ let shared = eph_secret.diffie_hellman(&recipient_public);
5871+ let key = chacha20poly1305::Key::from_slice(shared.as_bytes());
5872+ let cipher = ChaCha20Poly1305::new(key);
5873+
5874+ // Random nonce
5875+ let mut nonce_bytes = [0u8; 12];
5876+ getrandom::fill(&mut nonce_bytes).expect("knot runtime: failed to generate nonce");
5877+ let nonce = chacha20poly1305::Nonce::from_slice(&nonce_bytes);
5878+
5879+ let ciphertext = cipher.encrypt(nonce, plain.as_slice())
5880+ .expect("knot runtime: encryption failed");
5881+
5882+ // Pack: ephemeral_public (32) + nonce (12) + ciphertext
5883+ let mut result = Vec::with_capacity(32 + 12 + ciphertext.len());
5884+ result.extend_from_slice(eph_public.as_bytes());
5885+ result.extend_from_slice(&nonce_bytes);
5886+ result.extend_from_slice(&ciphertext);
5887+ alloc(Value::Bytes(result))
5888+}
5889+
5890+/// Sealed-box decryption: reverse of encrypt.
5891+/// Takes (privateKey: Bytes, ciphertext: Bytes), returns plaintext Bytes.
5892+#[unsafe(no_mangle)]
5893+pub extern "C" fn knot_crypto_decrypt(private_key: *mut Value, ciphertext: *mut Value) -> *mut Value {
5894+ use chacha20poly1305::{ChaCha20Poly1305, KeyInit};
5895+ use chacha20poly1305::aead::Aead;
5896+
5897+ let priv_bytes = match unsafe { as_ref(private_key) } {
5898+ Value::Bytes(b) => b,
5899+ _ => panic!("knot runtime: decrypt expected Bytes for privateKey, got {}", type_name(private_key)),
5900+ };
5901+ let ct = match unsafe { as_ref(ciphertext) } {
5902+ Value::Bytes(b) => b,
5903+ _ => panic!("knot runtime: decrypt expected Bytes for ciphertext, got {}", type_name(ciphertext)),
5904+ };
5905+
5906+ if ct.len() < 32 + 12 + 16 {
5907+ panic!("knot runtime: decrypt ciphertext too short (need at least 60 bytes, got {})", ct.len());
5908+ }
5909+
5910+ let secret_bytes: [u8; 32] = priv_bytes.as_slice().try_into()
5911+ .expect("knot runtime: decrypt privateKey must be 32 bytes");
5912+ let secret = x25519_dalek::StaticSecret::from(secret_bytes);
5913+
5914+ // Unpack
5915+ let eph_pub_bytes: [u8; 32] = ct[..32].try_into().unwrap();
5916+ let nonce_bytes: [u8; 12] = ct[32..44].try_into().unwrap();
5917+ let encrypted = &ct[44..];
5918+
5919+ let eph_public = x25519_dalek::PublicKey::from(eph_pub_bytes);
5920+ let shared = secret.diffie_hellman(&eph_public);
5921+ let key = chacha20poly1305::Key::from_slice(shared.as_bytes());
5922+ let cipher = ChaCha20Poly1305::new(key);
5923+ let nonce = chacha20poly1305::Nonce::from_slice(&nonce_bytes);
5924+
5925+ let plaintext = cipher.decrypt(nonce, encrypted)
5926+ .expect("knot runtime: decryption failed (invalid key or corrupted ciphertext)");
5927+ alloc(Value::Bytes(plaintext))
5928+}
5929+
5930+/// Ed25519 signing. Takes (privateKey: Bytes, message: Bytes), returns signature Bytes.
5931+#[unsafe(no_mangle)]
5932+pub extern "C" fn knot_crypto_sign(private_key: *mut Value, message: *mut Value) -> *mut Value {
5933+ use ed25519_dalek::Signer;
5934+
5935+ let priv_bytes = match unsafe { as_ref(private_key) } {
5936+ Value::Bytes(b) => b,
5937+ _ => panic!("knot runtime: sign expected Bytes for privateKey, got {}", type_name(private_key)),
5938+ };
5939+ let msg = match unsafe { as_ref(message) } {
5940+ Value::Bytes(b) => b,
5941+ _ => panic!("knot runtime: sign expected Bytes for message, got {}", type_name(message)),
5942+ };
5943+
5944+ let secret_bytes: [u8; 32] = priv_bytes.as_slice().try_into()
5945+ .expect("knot runtime: sign privateKey must be 32 bytes");
5946+ let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret_bytes);
5947+ let signature = signing_key.sign(msg);
5948+ alloc(Value::Bytes(signature.to_bytes().to_vec()))
5949+}
5950+
5951+/// Ed25519 verification. Takes (db, publicKey: Bytes, message: Bytes, signature: Bytes), returns Bool.
5952+#[unsafe(no_mangle)]
5953+pub extern "C" fn knot_crypto_verify(
5954+ _db: *mut c_void,
5955+ public_key: *mut Value,
5956+ message: *mut Value,
5957+ signature: *mut Value,
5958+) -> *mut Value {
5959+ use ed25519_dalek::Verifier;
5960+
5961+ let pub_bytes = match unsafe { as_ref(public_key) } {
5962+ Value::Bytes(b) => b,
5963+ _ => panic!("knot runtime: verify expected Bytes for publicKey, got {}", type_name(public_key)),
5964+ };
5965+ let msg = match unsafe { as_ref(message) } {
5966+ Value::Bytes(b) => b,
5967+ _ => panic!("knot runtime: verify expected Bytes for message, got {}", type_name(message)),
5968+ };
5969+ let sig_bytes = match unsafe { as_ref(signature) } {
5970+ Value::Bytes(b) => b,
5971+ _ => panic!("knot runtime: verify expected Bytes for signature, got {}", type_name(signature)),
5972+ };
5973+
5974+ let pub_arr: [u8; 32] = pub_bytes.as_slice().try_into()
5975+ .expect("knot runtime: verify publicKey must be 32 bytes");
5976+ let sig_arr: [u8; 64] = sig_bytes.as_slice().try_into()
5977+ .expect("knot runtime: verify signature must be 64 bytes");
5978+
5979+ let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pub_arr)
5980+ .expect("knot runtime: verify invalid public key");
5981+ let signature = ed25519_dalek::Signature::from_bytes(&sig_arr);
5982+
5983+ let valid = verifying_key.verify(msg, &signature).is_ok();
5984+ alloc(Value::Bool(valid))
5985+}
examples/crypto.knotnew file+34
@@ -0,0 +1,34 @@
1+-- Elliptic curve cryptography demo
2+
3+main = do
4+ -- Key pair generation for encryption (X25519)
5+ let keys = generateKeyPair
6+ println "Encryption key pair generated"
7+ println (" Public key: " ++ bytesToHex keys.publicKey)
8+
9+ -- Encrypt and decrypt a message
10+ let message = textToBytes "Hello, cryptography!"
11+ let ciphertext = encrypt keys.publicKey message
12+ println (" Ciphertext: " ++ bytesToHex ciphertext)
13+
14+ let decrypted = decrypt keys.privateKey ciphertext
15+ println (" Decrypted: " ++ bytesToText decrypted)
16+
17+ -- Signing key pair generation (Ed25519)
18+ let sigKeys = generateSigningKeyPair
19+ println "Signing key pair generated"
20+ println (" Public key: " ++ bytesToHex sigKeys.publicKey)
21+
22+ -- Sign and verify a message
23+ let sig = sign sigKeys.privateKey message
24+ println (" Signature: " ++ bytesToHex sig)
25+
26+ let valid = verify sigKeys.publicKey message sig
27+ println (" Valid: " ++ show valid)
28+
29+ -- Verify with wrong message should fail
30+ let wrongMsg = textToBytes "Wrong message"
31+ let invalid = verify sigKeys.publicKey wrongMsg sig
32+ println (" Invalid (wrong msg): " ++ show invalid)
33+
34+ yield {}