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 ⚠️
- Soft Derivation Risks: Soft-derived keys can leak parent key if compromised
- Hard Derivation Only: Use hard derivation for independent security
- VRF Output Bias: VRF output has slight bias, use appropriate post-processing
- Batch Verification: Must handle mixed valid/invalid signatures correctly
- Mini Secret Keys: Always expand mini keys before use
API Functions
generate_keypair() -> (PrivateKey[64], PublicKey[32])
Security Contract:
- Preconditions:
- System CSPRNG properly initialized
- At least 256 bits of entropy available
- Postconditions:
- Private key contains 32-byte scalar and 32-byte nonce
- Public key is 32-byte Ristretto point
- Keys suitable for Sr25519 operations
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:
- Preconditions:
private_keymust be valid Sr25519 private keycontextshould identify the signature domainmessagecan be any length
- Postconditions:
- Returns 64-byte Schnorr signature
- Signature includes randomness from nonce
- Context prevents cross-domain attacks
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:
- Always use meaningful context strings
- Never sign raw hashes (include structure)
- Clear private keys after use
- Verify your own signatures in critical paths
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:
- Preconditions:
- All parameters must match signing exactly
public_keyshould be from trusted source- Same
contextas used in signing
- Postconditions:
- Returns true only for valid signatures
- Constant-time verification
- No information about failure reason
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:
- Preconditions:
private_keymust be valid Sr25519 key- VRF domain separation from signatures
- Message can be any length
- Postconditions:
- Output is deterministic pseudorandom
- Proof allows public verification
- Output has slight bias (not uniform)
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:
- Post-process output for uniformity if needed
- Never use VRF output directly as key
- Include application context in message
- Verify proofs from untrusted sources
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:
- Preconditions:
parentmust be valid Sr25519 private keychain_codeprovides derivation contextharddetermines security properties
- Postconditions:
- Returns derived keypair
- Hard derivation: child compromise doesn’t affect parent
- Soft derivation: child compromise can reveal parent
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
- Hard derivation for security-critical keys
- Context strings in all signatures
- VRF output post-processed for uniformity
- Private keys cleared after use
- Batch verification handles failures correctly
- Key derivation paths validated
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:
- Algorithm-specific attack vectors
- Implementation vulnerabilities
- Side-channel considerations
- Quantum resistance analysis (where applicable)
- Deployment recommendations
For complete security analysis and risk assessment, see the dedicated threat model documentation.
References
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