Sr25519 Security-Focused API Documentation

Version: 1.0
Last Updated: 2025-07-05
**Security Classification:
PUBLIC
Author: Phantom (phantom@metamui.id)

Overview

Sr25519 is a Schnorr signature scheme over the Ristretto group, designed for use in Substrate-based blockchains. It provides additional functionality beyond Ed25519, including hierarchical deterministic key derivation, multi-signatures, and verifiable random functions (VRF). Sr25519 addresses several limitations of Ed25519 while maintaining high security and performance.

Security Level: 128 bits
Private Key Size: 64 bytes (32-byte scalar + 32-byte nonce)
Public Key Size: 32 bytes
Signature Size: 64 bytes

Security Warnings ⚠️

  1. Soft Derivation Risks: Soft-derived keys can leak parent key if compromised
  2. Hard Derivation Only: Use hard derivation for independent security
  3. VRF Output Bias: VRF output has slight bias, use appropriate post-processing
  4. Batch Verification: Must handle mixed valid/invalid signatures correctly
  5. Mini Secret Keys: Always expand mini keys before use

API Functions

generate_keypair() -> (PrivateKey[64], PublicKey[32])

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Weak RNG | ⚠️ | Depends on system entropy | | Timing Attack | ✅ | Constant-time operations | | Key Malleability | ✅ | Canonical encoding enforced | | Cofactor Attack | ✅ | Ristretto eliminates cofactor | | Related Key | ✅ | Nonce provides separation |

Secure Usage Example:

use schnorrkel::{Keypair, SecretKey, PublicKey};
use rand::rngs::OsRng;

/// Secure key generation with derivation path
pub struct Sr25519KeyManager {
    master_key: Keypair,
}

impl Sr25519KeyManager {
    pub fn new() -> Self {
        // Generate master keypair from system randomness
        let master_key = Keypair::generate_with(&mut OsRng);
        
        Self { master_key }
    }
    
    pub fn derive_keypair(&self, path: &str) -> Result<Keypair, Error> {
        // Parse derivation path
        let chain_codes = parse_derivation_path(path)?;
        
        let mut current_key = self.master_key.secret.clone();
        
        for (i, chain_code) in chain_codes.iter().enumerate() {
            if chain_code.is_hard {
                // Hard derivation - secure
                current_key = current_key.hard_derive_mini_secret_key(
                    Some(chain_code.bytes),
                    &[b"sr25519", &i.to_le_bytes()].concat()
                );
            } else {
                // Soft derivation - use with caution
                warn!("Soft derivation at index {} - security implications", i);
                current_key = current_key.soft_derive_mini_secret_key(
                    chain_code.bytes
                );
            }
        }
        
        // Convert to full keypair
        let public = current_key.to_public();
        Ok(Keypair { secret: current_key, public })
    }
    
    pub fn sign_with_path(
        &self, 
        message: &[u8], 
        path: &str,
        context: &[u8]
    ) -> Result<Signature, Error> {
        let derived = self.derive_keypair(path)?;
        
        // Sign with context for domain separation
        let signature = derived.sign_simple(context, message);
        
        // Clear derived key
        derived.secret.zeroize();
        
        Ok(signature)
    }
}

// SECURE: Account key hierarchy
fn substrate_key_hierarchy() -> KeyHierarchy {
    let master = Keypair::generate_with(&mut OsRng);
    
    KeyHierarchy {
        // Hard derivation for account keys
        stash: master.derive("//stash"),          // Holds funds
        controller: master.derive("//controller"), // Controls stash
        
        // Hard derivation for different purposes
        governance: master.derive("//governance"),
        identity: master.derive("//identity"),
        
        // Soft derivation for related keys (careful!)
        session_keys: (0..4).map(|i| {
            master.derive(&format!("/session/{}", i))
        }).collect(),
    }
}

Common Mistakes:

# INSECURE: Weak entropy source
import hashlib
seed = hashlib.sha256(b"my password").digest()
keypair = sr25519.keypair_from_seed(seed)  # WEAK!

# INSECURE: Soft derivation for security-critical keys
master = sr25519.generate_keypair()
# Soft derivation - if this key leaks, master is compromised!
payment_key = master.soft_derive("/payment")  

# INSECURE: Not using context in signatures
signature = keypair.sign(message)  # No domain separation!
# Should use: keypair.sign(message, context=b"MyApp-v1")

sign(private_key: PrivateKey, message: bytes, context: bytes = b"") -> Signature[64]

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Forgery | ✅ | 128-bit security | | Malleability | ✅ | Canonical signatures | | Timing Attack | ✅ | Constant-time scalar ops | | Nonce Reuse | ✅ | Additional randomness | | Cross-protocol | ✅ | Context separation |

Security Requirements:

Secure Usage Example:

import sr25519
import json
from typing import Dict, Any

class SecureMessageSigner:
    def __init__(self, keypair):
        self.keypair = keypair
        self.context = b"SecureMessage-v1"
        
    def sign_structured_data(self, data: Dict[str, Any]) -> bytes:
        """Sign structured data with canonical encoding"""
        
        # Canonical JSON encoding
        canonical = json.dumps(data, sort_keys=True, separators=(',', ':'))
        
        # Include type information
        typed_message = b"JSON:" + canonical.encode()
        
        # Sign with context
        signature = self.keypair.sign(
            message=typed_message,
            context=self.context
        )
        
        # Create signed envelope
        envelope = {
            'data': data,
            'signature': signature.hex(),
            'public_key': self.keypair.public_key.hex(),
            'algorithm': 'sr25519',
            'version': 1
        }
        
        return json.dumps(envelope).encode()
    
    def sign_transaction(self, tx: Transaction) -> SignedTransaction:
        """Sign blockchain transaction"""
        
        # Encode transaction deterministically
        encoded = tx.encode()
        
        # Include chain context
        signing_payload = {
            'method': tx.method,
            'encoded': encoded,
            'genesis': tx.genesis_hash,
            'era': tx.era,
            'nonce': tx.nonce,
            'tip': tx.tip
        }
        
        # Create signing message
        message = encode_compact(signing_payload)
        
        # Sign with chain-specific context
        context = b"substrate-transaction" + tx.genesis_hash[:8]
        signature = self.keypair.sign(message, context)
        
        return SignedTransaction(
            transaction=tx,
            signature=signature,
            signer=self.keypair.public_key
        )

# SECURE: Multi-signature aggregation
class MultiSigAggregator:
    def __init__(self, threshold: int, signers: List[PublicKey]):
        self.threshold = threshold
        self.signers = signers
        self.context = b"MultiSig-v1"
        
    def aggregate_signatures(
        self, 
        message: bytes, 
        signatures: List[Tuple[PublicKey, Signature]]
    ) -> AggregateSignature:
        """Aggregate signatures with threshold verification"""
        
        if len(signatures) < self.threshold:
            raise ValueError(f"Need {self.threshold} signatures")
        
        # Verify each signature
        valid_sigs = []
        for pubkey, sig in signatures:
            if pubkey not in self.signers:
                continue
                
            if sr25519.verify(sig, message, pubkey, self.context):
                valid_sigs.append((pubkey, sig))
        
        if len(valid_sigs) < self.threshold:
            raise ValueError("Insufficient valid signatures")
        
        # Create aggregate proof
        return AggregateSignature(
            signers=[pk for pk, _ in valid_sigs],
            signatures=[sig for _, sig in valid_sigs],
            threshold=self.threshold,
            message_hash=hashlib.sha256(message).digest()
        )

verify(signature: Signature, message: bytes, public_key: PublicKey, context: bytes = b"") -> bool

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Signature Forgery | ✅ | Computationally infeasible | | Key Substitution | ✅ | Proper key validation | | Timing Oracle | ✅ | Constant-time operations | | Batch Poisoning | ✅ | When using batch verify |

Secure Usage Example:

/// Secure batch verification with fallback
pub fn verify_batch_with_fallback(
    signatures: &[(Signature, Message, PublicKey)],
    context: &[u8],
) -> Vec<bool> {
    let mut results = vec![false; signatures.len()];
    
    // Try batch verification first (faster)
    let batch_result = sr25519::verify_batch(
        signatures.iter().map(|(s, m, p)| (*s, m.as_ref(), *p)),
        context,
    );
    
    if batch_result {
        // All signatures valid
        results.fill(true);
    } else {
        // Batch failed - fall back to individual verification
        // to identify which signatures are invalid
        for (i, (sig, msg, pubkey)) in signatures.iter().enumerate() {
            results[i] = sr25519::verify(sig, msg, pubkey, context);
        }
    }
    
    results
}

/// Time-constant verification for critical operations
pub fn verify_critical(
    signature: &Signature,
    message: &[u8],
    public_key: &PublicKey,
    context: &[u8],
) -> Result<(), VerificationError> {
    // Pre-validate inputs
    if signature.is_zero() {
        return Err(VerificationError::InvalidSignature);
    }
    
    // Perform verification
    let start = Instant::now();
    let valid = sr25519::verify(signature, message, public_key, context);
    
    // Ensure constant time by adding random delay
    let elapsed = start.elapsed();
    let target = Duration::from_micros(100);
    if elapsed < target {
        std::thread::sleep(target - elapsed);
    }
    
    if valid {
        Ok(())
    } else {
        Err(VerificationError::InvalidSignature)
    }
}

vrf_sign(private_key: PrivateKey, message: bytes) -> (VrfOutput[32], VrfProof[64])

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Output Prediction | ✅ | Without private key | | Proof Forgery | ✅ | Computationally infeasible | | Malleability | ✅ | Unique proof per output | | Timing Attack | ✅ | Constant-time operations |

Security Requirements:

Secure Usage Example:

class VRFLottery:
    """Verifiable random lottery using Sr25519 VRF"""
    
    def __init__(self, operator_key: Sr25519Keypair):
        self.operator_key = operator_key
        self.epoch = 0
        
    def draw_winner(self, participants: List[str], seed: bytes) -> LotteryResult:
        """Select winner with verifiable randomness"""
        
        # Create deterministic input
        message = json.dumps({
            'epoch': self.epoch,
            'participants': sorted(participants),
            'seed': seed.hex(),
            'timestamp': int(time.time())
        }).encode()
        
        # Generate VRF output and proof
        vrf_out, vrf_proof = self.operator_key.vrf_sign(message)
        
        # Convert VRF output to uniform randomness
        uniform_random = self._make_uniform(vrf_out)
        
        # Select winner
        winner_index = int.from_bytes(uniform_random, 'big') % len(participants)
        winner = participants[winner_index]
        
        self.epoch += 1
        
        return LotteryResult(
            winner=winner,
            vrf_output=vrf_out,
            vrf_proof=vrf_proof,
            message=message,
            epoch=self.epoch
        )
    
    def _make_uniform(self, vrf_output: bytes) -> bytes:
        """Remove bias from VRF output"""
        # VRF output has ~2^-126 bias
        # Use rejection sampling for perfect uniformity
        
        # For most applications, hash is sufficient
        return hashlib.sha256(vrf_output).digest()
    
    @staticmethod
    def verify_draw(
        result: LotteryResult, 
        operator_pubkey: Sr25519PublicKey
    ) -> bool:
        """Verify lottery draw was fair"""
        
        # Verify VRF proof
        verified_output = sr25519.vrf_verify(
            public_key=operator_pubkey,
            message=result.message,
            proof=result.vrf_proof
        )
        
        if verified_output != result.vrf_output:
            return False
        
        # Recompute winner selection
        participants = json.loads(result.message)['participants']
        uniform = hashlib.sha256(verified_output).digest()
        expected_index = int.from_bytes(uniform, 'big') % len(participants)
        
        return participants[expected_index] == result.winner

# SECURE: VRF-based commit-reveal
class VRFCommitReveal:
    def __init__(self, keypair: Sr25519Keypair):
        self.keypair = keypair
        self.commitments = {}
        
    def commit(self, value: bytes, nonce: bytes) -> VrfCommitment:
        """Create VRF-based commitment"""
        
        # Include value and nonce in VRF input
        vrf_input = b"commit:" + value + b":" + nonce
        
        # Generate VRF output (commitment) and proof
        vrf_out, vrf_proof = self.keypair.vrf_sign(vrf_input)
        
        commitment = VrfCommitment(
            output=vrf_out,
            timestamp=time.time()
        )
        
        # Store for later reveal
        self.commitments[vrf_out] = (value, nonce, vrf_proof)
        
        return commitment
    
    def reveal(self, commitment: VrfCommitment) -> Optional[VrfReveal]:
        """Reveal commitment with proof"""
        
        if commitment.output not in self.commitments:
            return None
        
        value, nonce, proof = self.commitments[commitment.output]
        
        return VrfReveal(
            value=value,
            nonce=nonce,
            proof=proof,
            commitment=commitment
        )

derive_keypair(parent: PrivateKey, chain_code: bytes, index: int, hard: bool) -> (PrivateKey, PublicKey)

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Parent Recovery (hard) | ✅ | One-way derivation | | Parent Recovery (soft) | ❌ | Can recover with child | | Sibling Discovery | ✅ | Can’t find siblings | | Chain Code Guess | ✅ | With proper entropy |

Secure Usage Example:

/// Hierarchical deterministic wallet
pub struct HDWallet {
    master: Keypair,
    chain_code: [u8; 32],
}

impl HDWallet {
    pub fn new(seed: &[u8]) -> Result<Self, Error> {
        // Derive master key from seed
        let master = Keypair::from_seed(seed);
        
        // Derive chain code
        let chain_code = blake2b_256(&[b"Sr25519HDKD", seed].concat());
        
        Ok(Self { master, chain_code })
    }
    
    pub fn derive_account(&self, account: u32) -> Result<Account, Error> {
        // Always use hard derivation for accounts
        let account_key = self.master.secret.hard_derive_keypair(
            &self.chain_code,
            &account.to_le_bytes(),
        )?;
        
        // Derive sub-accounts with hard derivation
        let receive_key = account_key.secret.hard_derive_keypair(
            b"receive",
            &[0u8; 4],
        )?;
        
        let change_key = account_key.secret.hard_derive_keypair(
            b"change",
            &[1u8; 4],
        )?;
        
        Ok(Account {
            keypair: account_key,
            receive_addresses: self.derive_addresses(&receive_key, 20)?,
            change_addresses: self.derive_addresses(&change_key, 20)?,
        })
    }
    
    fn derive_addresses(&self, parent: &Keypair, count: u32) -> Result<Vec<Address>, Error> {
        let mut addresses = Vec::new();
        
        for i in 0..count {
            // Soft derivation is safe here - these are receive addresses
            let addr_key = parent.secret.soft_derive_keypair(&i.to_le_bytes())?;
            
            addresses.push(Address {
                public_key: addr_key.public,
                path: format!("m/44'/434'/{}'/{}", account, chain, i),
            });
        }
        
        Ok(addresses)
    }
}

Security Best Practices

Key Derivation Patterns

class SubstrateKeyDerivation:
    """Substrate-compatible key derivation"""
    
    @staticmethod
    def parse_path(path: str) -> List[DeriveJunction]:
        """Parse derivation path like //hard/soft"""
        junctions = []
        
        for part in path.split('/'):
            if not part:
                continue
                
            if part.startswith('/'):
                # Hard derivation
                junctions.append(DeriveJunction(
                    chain_code=part[1:].encode(),
                    is_hard=True
                ))
            else:
                # Soft derivation
                junctions.append(DeriveJunction(
                    chain_code=part.encode(),
                    is_hard=False
                ))
        
        return junctions
    
    @staticmethod
    def derive_address(seed: bytes, path: str) -> str:
        """Derive Substrate address from seed and path"""
        
        # Start with master key
        current = sr25519.keypair_from_seed(seed)
        
        # Apply derivation path
        for junction in SubstrateKeyDerivation.parse_path(path):
            if junction.is_hard:
                current = current.hard_derive(
                    junction.chain_code
                )
            else:
                current = current.soft_derive(
                    junction.chain_code
                )
        
        # Encode as Substrate address
        return encode_address(current.public_key, network_id=42)

Multi-Signature Schemes

/// Threshold signature scheme using Sr25519
pub struct ThresholdSigner {
    threshold: usize,
    participants: Vec<PublicKey>,
    aggregator: SignatureAggregator,
}

impl ThresholdSigner {
    pub fn new(threshold: usize, participants: Vec<PublicKey>) -> Self {
        Self {
            threshold,
            participants,
            aggregator: SignatureAggregator::new(),
        }
    }
    
    pub fn sign_with_threshold(
        &mut self,
        message: &[u8],
        signatures: Vec<(PublicKey, Signature)>,
    ) -> Result<ThresholdSignature, Error> {
        // Verify we have enough signatures
        if signatures.len() < self.threshold {
            return Err(Error::InsufficientSignatures);
        }
        
        // Verify all signatures
        let mut valid_sigs = Vec::new();
        for (pubkey, sig) in signatures {
            // Check participant
            if !self.participants.contains(&pubkey) {
                continue;
            }
            
            // Verify signature
            if sr25519::verify(&sig, message, &pubkey, b"threshold-v1") {
                valid_sigs.push((pubkey, sig));
            }
        }
        
        if valid_sigs.len() < self.threshold {
            return Err(Error::InvalidSignatures);
        }
        
        // Create threshold signature proof
        Ok(ThresholdSignature {
            signatures: valid_sigs,
            threshold: self.threshold,
            message_hash: blake2b_256(message),
        })
    }
}

Common Integration Patterns

Substrate Integration

use sp_core::{sr25519, Pair};
use sp_runtime::traits::Verify;

/// Substrate-compatible signing
pub fn sign_extrinsic<T: frame_system::Config>(
    signer: &sr25519::Pair,
    call: &Call<T>,
    nonce: T::Index,
    genesis_hash: T::Hash,
) -> SignedExtrinsic<T> {
    let extra = SignedExtra::<T>::new(nonce);
    
    let payload = SignedPayload::new(call, extra, genesis_hash);
    let signature = payload.using_encoded(|encoded| {
        signer.sign(encoded)
    });
    
    SignedExtrinsic {
        call: call.clone(),
        signature: MultiSignature::Sr25519(signature),
        signer: signer.public().into(),
        extra,
    }
}

Session Key Rotation

class SessionKeyManager:
    """Manage rotating session keys"""
    
    def __init__(self, master_key: Sr25519Keypair):
        self.master_key = master_key
        self.current_session = 0
        self.session_duration = 3600  # 1 hour
        self.last_rotation = time.time()
        
    def get_current_session_key(self) -> Sr25519Keypair:
        """Get or rotate session key"""
        
        # Check if rotation needed
        if time.time() - self.last_rotation > self.session_duration:
            self.rotate_session()
        
        # Derive current session key
        session_key = self.master_key.derive(
            f"//session/{self.current_session}"
        )
        
        return session_key
    
    def rotate_session(self):
        """Advance to next session"""
        self.current_session += 1
        self.last_rotation = time.time()
        
        # Notify session change
        self.broadcast_session_update()

Performance Considerations

Operation Time vs Ed25519 Notes
Key Generation ~150 μs 1.5x Additional nonce
Signing ~100 μs 1.4x Schnorr overhead
Verification ~200 μs 1.1x Similar
VRF Sign ~150 μs N/A Includes proof
VRF Verify ~250 μs N/A Proof verification
Batch Verify (n=100) ~15 ms 1.2x Good scaling

Security Auditing

Verification Checklist

Common Vulnerabilities

# AUDIT: Look for these patterns

# ❌ Soft derivation for critical keys
payment_key = master.soft_derive("/payment")  # Parent at risk!

# ❌ No context in signatures
sig = key.sign(message)  # No domain separation

# ❌ Direct VRF output use
random_key = vrf_output  # Has bias!

# ❌ Accepting any derivation path
key = derive(user_provided_path)  # Path injection

Security Analysis

Threat Model: Sr25519 Threat Model

The comprehensive threat analysis covers:

For complete security analysis and risk assessment, see the dedicated threat model documentation.

References

  1. Schnorrkel Documentation
  2. Substrate Key Derivation
  3. VRF Specification

Support

Security Issues: security@metamui.id
Documentation Updates: phantom@metamui.id
Vulnerability Disclosure: See SECURITY.md


Document Version: 1.0
Review Cycle: Quarterly
Next Review: 2025-04-05
Classification: PUBLIC