Skip to main content

seq_runtime/crypto/
ed25519.rs

1//! Ed25519 signature operations: keypair generation, sign, verify.
2
3use crate::seqstring::global_string;
4use crate::stack::{Stack, pop, push};
5use crate::value::Value;
6
7use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
8// Ed25519's `SigningKey::generate` requires a `CryptoRngCore` from
9// `rand_core` 0.6. The runtime already depends on `aes_gcm`, which
10// re-exports a 0.6-compatible `OsRng` — reuse it rather than pull a
11// duplicate `rand_core` version.
12use aes_gcm::aead::OsRng;
13
14// ============================================================================
15// Ed25519 Digital Signatures
16// ============================================================================
17
18/// Generate an Ed25519 keypair
19///
20/// Stack effect: ( -- public-key private-key )
21///
22/// Returns:
23/// - public-key: Hex-encoded 32-byte public key (64 hex characters)
24/// - private-key: Hex-encoded 32-byte private key (64 hex characters)
25///
26/// # Safety
27/// Stack must be valid
28#[unsafe(no_mangle)]
29pub unsafe extern "C" fn patch_seq_crypto_ed25519_keypair(stack: Stack) -> Stack {
30    assert!(!stack.is_null(), "crypto.ed25519-keypair: stack is null");
31
32    let signing_key = SigningKey::generate(&mut OsRng);
33    let verifying_key = signing_key.verifying_key();
34
35    let private_hex = hex::encode(signing_key.to_bytes());
36    let public_hex = hex::encode(verifying_key.to_bytes());
37
38    let stack = unsafe { push(stack, Value::String(global_string(public_hex))) };
39    unsafe { push(stack, Value::String(global_string(private_hex))) }
40}
41
42/// Sign a message with an Ed25519 private key
43///
44/// Stack effect: ( message private-key -- signature success )
45///
46/// Parameters:
47/// - message: The message to sign (any string)
48/// - private-key: Hex-encoded 32-byte private key (64 hex characters)
49///
50/// Returns:
51/// - signature: Hex-encoded 64-byte signature (128 hex characters)
52/// - success: Bool indicating success
53///
54/// # Safety
55/// Stack must have String, String values on top
56#[unsafe(no_mangle)]
57pub unsafe extern "C" fn patch_seq_crypto_ed25519_sign(stack: Stack) -> Stack {
58    assert!(!stack.is_null(), "crypto.ed25519-sign: stack is null");
59
60    let (stack, key_val) = unsafe { pop(stack) };
61    let (stack, msg_val) = unsafe { pop(stack) };
62
63    match (msg_val, key_val) {
64        (Value::String(message), Value::String(private_key_hex)) => {
65            // Message is byte-clean; the key is hex (text).
66            match ed25519_sign(message.as_bytes(), private_key_hex.as_str_or_empty()) {
67                Some(signature) => {
68                    let stack = unsafe { push(stack, Value::String(global_string(signature))) };
69                    unsafe { push(stack, Value::Bool(true)) }
70                }
71                None => {
72                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
73                    unsafe { push(stack, Value::Bool(false)) }
74                }
75            }
76        }
77        _ => panic!("crypto.ed25519-sign: expected String, String on stack"),
78    }
79}
80
81/// Verify an Ed25519 signature
82///
83/// Stack effect: ( message signature public-key -- valid )
84///
85/// Parameters:
86/// - message: The original message
87/// - signature: Hex-encoded 64-byte signature (128 hex characters)
88/// - public-key: Hex-encoded 32-byte public key (64 hex characters)
89///
90/// Returns:
91/// - valid: Bool indicating whether the signature is valid
92///
93/// # Safety
94/// Stack must have String, String, String values on top
95#[unsafe(no_mangle)]
96pub unsafe extern "C" fn patch_seq_crypto_ed25519_verify(stack: Stack) -> Stack {
97    assert!(!stack.is_null(), "crypto.ed25519-verify: stack is null");
98
99    let (stack, pubkey_val) = unsafe { pop(stack) };
100    let (stack, sig_val) = unsafe { pop(stack) };
101    let (stack, msg_val) = unsafe { pop(stack) };
102
103    match (msg_val, sig_val, pubkey_val) {
104        (Value::String(message), Value::String(signature_hex), Value::String(public_key_hex)) => {
105            // Message is byte-clean; signature and key are hex (text).
106            let valid = ed25519_verify(
107                message.as_bytes(),
108                signature_hex.as_str_or_empty(),
109                public_key_hex.as_str_or_empty(),
110            );
111            unsafe { push(stack, Value::Bool(valid)) }
112        }
113        _ => panic!("crypto.ed25519-verify: expected String, String, String on stack"),
114    }
115}
116
117// Helper functions for Ed25519
118
119/// Sign arbitrary bytes (binary or text). The key is hex-encoded; the
120/// returned signature is hex-encoded (always valid UTF-8).
121pub(super) fn ed25519_sign(message: &[u8], private_key_hex: &str) -> Option<String> {
122    let key_bytes = hex::decode(private_key_hex).ok()?;
123    if key_bytes.len() != 32 {
124        return None;
125    }
126
127    let key_array: [u8; 32] = key_bytes.try_into().ok()?;
128    let signing_key = SigningKey::from_bytes(&key_array);
129    let signature = signing_key.sign(message);
130
131    Some(hex::encode(signature.to_bytes()))
132}
133
134/// Verify a signature against arbitrary bytes (binary or text).
135pub(super) fn ed25519_verify(message: &[u8], signature_hex: &str, public_key_hex: &str) -> bool {
136    let verify_inner = || -> Option<bool> {
137        let sig_bytes = hex::decode(signature_hex).ok()?;
138        if sig_bytes.len() != 64 {
139            return Some(false);
140        }
141
142        let pubkey_bytes = hex::decode(public_key_hex).ok()?;
143        if pubkey_bytes.len() != 32 {
144            return Some(false);
145        }
146
147        let sig_array: [u8; 64] = sig_bytes.try_into().ok()?;
148        let pubkey_array: [u8; 32] = pubkey_bytes.try_into().ok()?;
149
150        let signature = Signature::from_bytes(&sig_array);
151        let verifying_key = VerifyingKey::from_bytes(&pubkey_array).ok()?;
152
153        Some(verifying_key.verify(message, &signature).is_ok())
154    };
155
156    verify_inner().unwrap_or(false)
157}