Skip to main content
This guide covers cryptographic key management for Tashi Vertex, including key generation, parsing, storage, and security best practices.

Key types

Tashi Vertex uses Ed25519 cryptography with two key types:

Secret key (KeySecret)

Private key used for signing transactions. Must be kept secure.
use tashi_vertex::KeySecret;

let secret = KeySecret::generate();

Public key (KeyPublic)

Public key derived from the secret key, used for signature verification.
let public = secret.public();

Generating keys

Generate a new secret key

use tashi_vertex::KeySecret;

let secret = KeySecret::generate();
This generates a cryptographically secure Ed25519 secret key using the system’s random number generator.

Derive the public key

let public = secret.public();
The public key is deterministically derived from the secret key using Ed25519 mathematics.

Complete key pair generation

use tashi_vertex::KeySecret;

fn generate_keypair() -> (KeySecret, String) {
    let secret = KeySecret::generate();
    let public = secret.public().to_string();
    (secret, public)
}

let (secret_key, public_key) = generate_keypair();
println!("Public key: {}", public_key);
Never log or print secret keys in production. The example above should only be used for initial key generation.

Key formats

DER encoding

Keys are internally stored in DER (Distinguished Encoding Rules) format:
  • Secret key DER: 51 bytes
  • Public key DER: 91 bytes
use tashi_vertex::{KeySecret, KeyPublic};

// Secret key to DER
let secret = KeySecret::generate();
let der_bytes = secret.to_der_vec()?;
assert_eq!(der_bytes.len(), 51);

// Public key to DER
let public = secret.public();
let der_bytes = public.to_der_vec()?;
assert_eq!(der_bytes.len(), 91);

Base58 encoding

For human-readable representation, keys are encoded in Base58:
  • Secret key Base58: 69 characters (approximately)
  • Public key Base58: 123 characters (approximately)
// Convert to Base58 string
let secret_b58 = secret.to_string();
let public_b58 = public.to_string();

println!("Secret (Base58): {}", secret_b58);
println!("Public (Base58): {}", public_b58);
Base58 encoding is used because it avoids visually similar characters (0/O, l/1) and is more compact than Base64 while remaining human-readable.

Parsing keys

From Base58 strings

The most common way to load keys:
use tashi_vertex::{KeySecret, KeyPublic};

// Parse secret key
let secret: KeySecret = "base58_secret_key_here".parse()?;

// Parse public key
let public: KeyPublic = "base58_public_key_here".parse()?;
Using FromStr trait:
use std::str::FromStr;

let secret = KeySecret::from_str("base58_secret_key_here")?;
let public = KeyPublic::from_str("base58_public_key_here")?;

From DER bytes

For advanced use cases:
// Parse secret key from DER
let der_bytes: &[u8] = &[/* 51 bytes */];
let secret = KeySecret::from_der(der_bytes)?;

// Parse public key from DER
let der_bytes: &[u8] = &[/* 91 bytes */];
let public = KeyPublic::from_der(der_bytes)?;

From environment variables

use std::env;
use tashi_vertex::KeySecret;

let secret: KeySecret = env::var("NODE_SECRET_KEY")?.parse()?;

From configuration files

use serde::Deserialize;
use tashi_vertex::KeySecret;

#[derive(Deserialize)]
struct Config {
    #[serde(deserialize_with = "deserialize_key_secret")]
    secret_key: KeySecret,
}

fn deserialize_key_secret<'de, D>(deserializer: D) -> Result<KeySecret, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    s.parse().map_err(serde::de::Error::custom)
}

let config: Config = toml::from_str(include_str!("config.toml"))?;

From command-line arguments

use clap::Parser;
use tashi_vertex::KeySecret;

#[derive(Parser)]
struct Args {
    #[clap(short = 'K', long)]
    key: KeySecret,
}

fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    println!("Loaded key: {:?}", args.key);
    Ok(())
}
Run with:
cargo run -- --key "base58_secret_key_here"

Secure storage

Store keys in environment variables for development and deployment:
# .env file (DO NOT commit to git)
NODE_SECRET_KEY=your_base58_secret_key_here
use dotenv::dotenv;
use tashi_vertex::KeySecret;

dotenv().ok();
let secret: KeySecret = std::env::var("NODE_SECRET_KEY")?.parse()?;
Add .env to .gitignore to prevent accidentally committing secrets.

File system with restricted permissions

Store keys in files with strict permissions:
# Create key file with restricted permissions
echo "your_base58_secret_key" > /etc/tashi/secret.key
chmod 600 /etc/tashi/secret.key
chown tashi:tashi /etc/tashi/secret.key
use std::fs;
use tashi_vertex::KeySecret;

let key_str = fs::read_to_string("/etc/tashi/secret.key")?
    .trim()
    .to_string();
let secret: KeySecret = key_str.parse()?;

Hardware security modules (HSM)

For production systems, consider using HSMs or key management services:
  • AWS KMS
  • Azure Key Vault
  • Google Cloud KMS
  • HashiCorp Vault
  • YubiHSM

Encrypted storage

Encrypt keys at rest:
use aes_gcm::{
    aead::{Aead, KeyInit},
    Aes256Gcm, Nonce
};
use tashi_vertex::KeySecret;

fn encrypt_key(secret: &KeySecret, password: &str) -> anyhow::Result<Vec<u8>> {
    // Derive encryption key from password (use proper KDF in production)
    let key = derive_key_from_password(password)?;
    let cipher = Aes256Gcm::new(&key);
    let nonce = Nonce::from_slice(b"unique nonce");
    
    let der = secret.to_der_vec()?;
    let ciphertext = cipher.encrypt(nonce, der.as_ref())?;
    
    Ok(ciphertext)
}

fn decrypt_key(ciphertext: &[u8], password: &str) -> anyhow::Result<KeySecret> {
    let key = derive_key_from_password(password)?;
    let cipher = Aes256Gcm::new(&key);
    let nonce = Nonce::from_slice(b"unique nonce");
    
    let plaintext = cipher.decrypt(nonce, ciphertext)?;
    KeySecret::from_der(&plaintext)
}
This example is simplified. Use proper key derivation functions (KDF) like Argon2 in production.

Key rotation

Generate a new key

use tashi_vertex::KeySecret;

let old_key: KeySecret = std::env::var("NODE_SECRET_KEY")?.parse()?;
let new_key = KeySecret::generate();

println!("Old public key: {}", old_key.public());
println!("New public key: {}", new_key.public());
println!("New secret key: {}", new_key);

Migration process

1

Generate new key

Create a new secret key while keeping the old one active.
2

Update peer configurations

Distribute the new public key to all peers in the network.
3

Restart with new key

Update your node configuration to use the new secret key.
4

Verify connectivity

Ensure your node can still communicate with the network.
5

Decommission old key

Once verified, securely delete the old secret key.

Security best practices

Never commit secrets to version control

Add these to .gitignore:
# Secret keys
*.key
secret_key.txt
.env
.env.local

# Configuration with secrets
config.production.toml
secrets/

Use different keys per environment

use tashi_vertex::KeySecret;

let key = match std::env::var("ENV")?.as_str() {
    "production" => std::env::var("PROD_SECRET_KEY")?.parse()?,
    "staging" => std::env::var("STAGING_SECRET_KEY")?.parse()?,
    "development" => std::env::var("DEV_SECRET_KEY")?.parse()?,
    _ => return Err(anyhow::anyhow!("Unknown environment")),
};

Implement key lifecycle management

  • Generation: Use cryptographically secure RNG
  • Storage: Encrypt at rest, restrict file permissions
  • Access: Limit to necessary processes only
  • Rotation: Schedule regular key rotation
  • Revocation: Have a plan for compromised keys
  • Deletion: Securely wipe old keys from memory and disk

Validate keys on startup

use tashi_vertex::KeySecret;

fn load_and_validate_key() -> anyhow::Result<KeySecret> {
    let key_str = std::env::var("NODE_SECRET_KEY")?;
    
    // Parse the key
    let secret: KeySecret = key_str.parse()?;
    
    // Derive public key to verify key is valid
    let public = secret.public();
    
    println!("Loaded key with public key: {}", public);
    
    Ok(secret)
}

Memory safety

Zero out key material when dropping:
use zeroize::Zeroize;

struct SecureKeyWrapper {
    key_str: String,
}

impl Drop for SecureKeyWrapper {
    fn drop(&mut self) {
        self.key_str.zeroize();
    }
}
The KeySecret struct contains raw key material. Avoid cloning or copying unless necessary.

Debugging key issues

Verify key format

use tashi_vertex::KeySecret;

fn validate_key_string(s: &str) -> bool {
    s.parse::<KeySecret>().is_ok()
}

let is_valid = validate_key_string("base58_key_here");
println!("Key is valid: {}", is_valid);

Compare public keys

let secret: KeySecret = "secret_base58".parse()?;
let expected_public: String = "public_base58".to_string();
let actual_public = secret.public().to_string();

assert_eq!(actual_public, expected_public, "Public key mismatch!");

Debug key representation

use tashi_vertex::KeySecret;

let secret: KeySecret = std::env::var("NODE_SECRET_KEY")?.parse()?;

// Debug trait shows quoted string without revealing key
println!("Secret key: {:?}", secret);

// Display trait shows Base58 encoding (be careful!)
// println!("Secret key (Base58): {}", secret); // Don't use in production!

// Public key is safe to display
println!("Public key: {}", secret.public());

Example: Complete key management CLI

use clap::{Parser, Subcommand};
use tashi_vertex::KeySecret;

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Generate a new keypair
    Generate,
    
    /// Show public key for a secret key
    ShowPublic {
        #[clap(short, long)]
        secret: KeySecret,
    },
    
    /// Validate a secret key
    Validate {
        #[clap(short, long)]
        secret: String,
    },
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    
    match cli.command {
        Commands::Generate => {
            let secret = KeySecret::generate();
            let public = secret.public();
            
            println!("Generated new keypair:");
            println!("Secret key: {}", secret);
            println!("Public key: {}", public);
            println!();
            println!("⚠️  Store the secret key securely!");
        }
        
        Commands::ShowPublic { secret } => {
            let public = secret.public();
            println!("Public key: {}", public);
        }
        
        Commands::Validate { secret } => {
            match secret.parse::<KeySecret>() {
                Ok(key) => {
                    println!("✓ Valid secret key");
                    println!("Public key: {}", key.public());
                }
                Err(e) => {
                    println!("✗ Invalid secret key: {}", e);
                }
            }
        }
    }
    
    Ok(())
}
Run with:
# Generate new keypair
cargo run -- generate

# Show public key
cargo run -- show-public --secret "base58_secret_here"

# Validate key
cargo run -- validate --secret "base58_secret_here"

Next steps

Now that you understand key management, learn how to use keys when Running a node and setting up Network configuration.

Build docs developers (and LLMs) love