@@ -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+}