| name | mvx-constant-time |
| description | Verify cryptographic operations execute in constant time to prevent timing attacks. Use when auditing custom crypto implementations, secret comparisons, or security-sensitive algorithms in smart contracts. |
Constant Time Analysis
Verify that cryptographic secrets are handled in constant time to prevent timing attacks. This skill is essential when reviewing any code that processes sensitive data where execution time could leak information.
When to Use
- Auditing custom cryptographic implementations
- Reviewing secret comparison logic (hashes, signatures, keys)
- Analyzing authentication or verification code
- Checking password/PIN handling
- Reviewing any code where timing could leak secrets
1. Understanding Timing Attacks
The Threat Model
An attacker measures how long operations take to infer secret values:
Comparison: secret[i] == input[i]
- If mismatch at i=0: ~100ns (returns immediately)
- If mismatch at i=5: ~150ns (checked 5 bytes first)
- If all match: ~200ns (checked all bytes)
Attack: Try all values for byte 0, find fastest rejection = wrong guess
Repeat for each byte position
Why It Matters on MultiversX
- Gas metering can leak execution path information
- Cross-shard timing differences observable
- VM-level optimizations may vary execution time
2. Patterns to Avoid (Variable Time)
Early Exit Comparisons
fn compare_secrets(secret: &[u8], input: &[u8]) -> bool {
if secret.len() != input.len() {
return false;
}
for i in 0..secret.len() {
if secret[i] != input[i] {
return false;
}
}
true
}
Short-Circuit Boolean Operators
fn verify_auth(token_valid: bool, signature_valid: bool) -> bool {
token_valid && signature_valid
}
Conditional Branching on Secrets
fn process_key(key: &[u8]) {
if key[0] == 0x00 {
} else {
}
}
Data-Dependent Memory Access
fn lookup(secret_index: usize, table: &[u8]) -> u8 {
table[secret_index]
}
3. MultiversX-Safe Solutions
Use VM Cryptographic Functions
BEST PRACTICE: Always prefer built-in VM crypto operations:
fn verify_signature(&self, message: &ManagedBuffer, signature: &ManagedBuffer) {
let signer = self.expected_signer().get();
self.crypto().verify_ed25519(
signer.as_managed_buffer(),
message,
signature
);
}
fn hash_data(&self, data: &ManagedBuffer) -> ManagedBuffer {
self.crypto().sha256(data)
}
ManagedBuffer Comparison
The MultiversX VM's ManagedBuffer comparison is typically constant-time:
fn verify_hash(&self, input_hash: &ManagedBuffer) -> bool {
let stored_hash = self.secret_hash().get();
stored_hash == *input_hash
}
Manual Constant-Time Comparison (When Necessary)
If you must compare raw bytes:
fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result: u8 = 0;
for i in 0..a.len() {
result |= a[i] ^ b[i];
}
result == 0
}
Using the subtle Crate
For Rust code that needs constant-time operations:
use subtle::ConstantTimeEq;
fn verify_secret(stored: &[u8; 32], provided: &[u8; 32]) -> bool {
stored.ct_eq(provided).into()
}
Note: Verify subtle crate is compatible with no_std and WASM.
4. Verification Techniques
Code Review Checklist
Static Analysis Patterns
Search for potentially vulnerable patterns:
grep -n "return false" src/*.rs | grep -i "compare\|verify\|check"
grep -n "&&\|\\|\\|" src/*.rs | grep -i "secret\|key\|hash\|signature"
grep -n "if.*secret\|if.*key\|if.*hash" src/*.rs
Gas Analysis
On MultiversX, gas consumption can indicate timing:
#[view]
fn gas_test(&self, input: ManagedBuffer) -> u64 {
let before = self.blockchain().get_gas_left();
let after = self.blockchain().get_gas_left();
before - after
}
Warning: This is approximate. True constant-time requires VM-level guarantees.
5. Common Vulnerable Scenarios
Authentication Token Verification
fn verify_token(&self, token: &ManagedBuffer) -> bool {
let valid_token = self.auth_token().get();
let mut token_bytes = [0u8; 64];
let mut valid_bytes = [0u8; 64];
token.load_slice(0, &mut token_bytes[..token.len()]);
valid_token.load_slice(0, &mut valid_bytes[..valid_token.len()]);
for i in 0..token.len() {
if token_bytes[i] != valid_bytes[i] {
return false;
}
}
true
}
fn verify_token(&self, token: &ManagedBuffer) -> bool {
let valid_token = self.auth_token().get();
valid_token == *token
}
HMAC Verification
fn verify_hmac(&self, message: &ManagedBuffer, provided_mac: &ManagedBuffer) -> bool {
let computed_mac = self.compute_hmac(message);
computed_mac == *provided_mac
}
fn verify_hmac(&self, message: &ManagedBuffer, provided_mac: &ManagedBuffer) -> bool {
let computed_mac = self.compute_hmac(message);
self.constant_time_eq(&computed_mac, provided_mac)
}
Password/PIN Comparison
fn check_pin(&self, entered_pin: u32) -> bool {
entered_pin == self.stored_pin().get()
}
fn check_pin(&self, entered_pin: u32) -> bool {
let stored = self.stored_pin().get();
(entered_pin ^ stored) == 0
}
6. Audit Report Template
## Constant-Time Analysis
### Scope
Files reviewed: [list]
Crypto operations found: [count]
### Findings
| Location | Operation | Status | Notes |
|----------|-----------|--------|-------|
| lib.rs:45 | Hash comparison | Safe | Uses ManagedBuffer == |
| auth.rs:23 | Token verify | VULNERABLE | Early return pattern |
| crypto.rs:89 | Signature | Safe | Uses self.crypto() |
### Recommendations
1. [Specific fix for each vulnerable location]
7. Key Principles
- Prefer VM Functions:
self.crypto().* methods are optimized and likely constant-time
- Avoid DIY Crypto: Custom implementations are rarely necessary and often wrong
- Assume Timing Leaks: Any branching on secrets is a potential vulnerability
- Test with Gas: Gas consumption can reveal timing variations
- Document Assumptions: Note which operations you assume are constant-time