Skip to main content
TAPLE uses robust cryptographic primitives to ensure security, authenticity, and integrity across the distributed ledger. The system supports multiple cryptographic schemes with a flexible architecture.

Key Pair Types

TAPLE supports multiple asymmetric cryptographic schemes:
pub enum KeyPair {
    Ed25519(Ed25519KeyPair),
    #[cfg(feature = "secp256k1")]
    Secp256k1(Secp256k1KeyPair),
}
Source: commons/crypto/mod.rs:24-29
Ed25519 is the default and recommended scheme. Secp256k1 support is available as an optional feature.

Key Derivators

Key derivators determine which cryptographic scheme to use:
pub enum KeyDerivator {
    Ed25519,
    Secp256k1,
}
Source: commons/identifier/derive/mod.rs (referenced)

Getting the Derivator

pub fn get_key_derivator(&self) -> KeyDerivator {
    match self {
        KeyPair::Ed25519(_) => KeyDerivator::Ed25519,
        KeyPair::Secp256k1(_) => KeyDerivator::Secp256k1,
    }
}
Source: commons/crypto/mod.rs:32-37

Key Generation

Keys can be generated in multiple ways:

Random Generation

let key_pair = crypto::KeyPair::Ed25519(Ed25519KeyPair::from_seed(&[]));
Source: lib.rs:32

From Seed

pub trait KeyGenerator: KeyMaterial {
    fn new() -> Self
    where
        Self: Sized,
    {
        Self::from_seed(vec![].as_slice())
    }

    fn from_seed(seed: &[u8]) -> Self
    where
        Self: Sized;

    fn from_public_key(public_key: &[u8]) -> Self
    where
        Self: Sized;

    fn from_secret_key(private_key: &[u8]) -> Self
    where
        Self: Sized;
}
Source: commons/crypto/mod.rs:90-113

Seed Creation

Seeds are validated and padded to 32 bytes:
pub fn create_seed(initial_seed: &[u8]) -> Result<[u8; 32], Error> {
    let mut seed = [0u8; 32];
    if initial_seed.is_empty() {
        getrandom::getrandom(&mut seed)
            .map_err(|_| Error::SeedError("couldn't generate random seed".to_owned()))?;
    } else if initial_seed.len() <= 32 {
        seed[..initial_seed.len()].copy_from_slice(initial_seed);
    } else {
        return Err(Error::SeedError("seed is greater than 32".to_owned()));
    }
    Ok(seed)
}
Source: commons/crypto/mod.rs:266-277
Seeds longer than 32 bytes will be rejected. Empty seeds trigger secure random generation.

From Hex String

pub fn from_hex(derivator: &KeyDerivator, hex_key: &str) -> Result<KeyPair, Error> {
    match derivator {
        KeyDerivator::Ed25519 => Ok(KeyPair::Ed25519(Ed25519KeyPair::from_secret_key(
            &hex::decode(hex_key).unwrap(),
        ))),
        KeyDerivator::Secp256k1 => Ok(KeyPair::Secp256k1(Secp256k1KeyPair::from_secret_key(
            &hex::decode(hex_key).unwrap(),
        ))),
    }
}
Source: commons/crypto/mod.rs:39-48

Ed25519 Implementation

Ed25519 is the primary cryptographic scheme:
pub type Ed25519KeyPair = BaseKeyPair<PublicKey, SecretKey>;

impl KeyGenerator for Ed25519KeyPair {
    fn from_seed(seed: &[u8]) -> Self {
        let secret_seed = create_seed(seed).expect("invalid seed");
        let sk: SecretKey =
            SecretKey::from_bytes(&secret_seed).expect("cannot generate secret key");
        let pk: PublicKey = (&sk).try_into().expect("cannot generate public key");
        Self {
            public_key: pk,
            secret_key: Some(sk),
        }
    }

    fn from_public_key(public_key: &[u8]) -> Self {
        Self {
            public_key: PublicKey::from_bytes(public_key).expect("cannot generate public key"),
            secret_key: None,
        }
    }

    fn from_secret_key(secret_key: &[u8]) -> Ed25519KeyPair {
        let sk: SecretKey = SecretKey::from_bytes(secret_key).expect("cannot generate secret key");
        let pk: PublicKey = (&sk).try_into().expect("cannot generate public key");

        Ed25519KeyPair {
            secret_key: Some(sk),
            public_key: pk,
        }
    }
}
Source: commons/crypto/ed25519.rs:16-46

Ed25519 Signing

impl DSA for Ed25519KeyPair {
    fn sign(&self, payload: Payload) -> Result<Vec<u8>, Error> {
        let esk: ExpandedSecretKey = match &self.secret_key {
            Some(x) => x,
            None => return Err(Error::SignError("Secret key not found".to_owned())),
        }
        .into();
        match payload {
            Payload::Buffer(msg) => Ok(esk
                .sign(msg.as_slice(), &self.public_key)
                .to_bytes()
                .to_vec()),
            _ => Err(Error::SignError(
                "Payload type not supported for this key".into(),
            )),
        }
    }

    fn verify(&self, payload: Payload, signature: &[u8]) -> Result<(), Error> {
        let sig = Signature::try_from(signature)
            .map_err(|_| Error::SignError("Invalid signature data".into()))?;
        match payload {
            Payload::Buffer(payload) => match self.public_key.verify(payload.as_slice(), &sig) {
                Ok(_) => Ok(()),
                _ => Err(Error::SignError("Signature verify failed".into())),
            },
            _ => Err(Error::SignError(
                "Payload type not supported for this key".into(),
            )),
        }
    }
}
Source: commons/crypto/ed25519.rs:67-98

Digital Signature Algorithm (DSA)

All key pairs implement the DSA trait:
pub trait DSA {
    /// Performs sign operation
    fn sign(&self, payload: Payload) -> Result<Vec<u8>, Error>;

    /// Performs verify operation
    fn verify(&self, payload: Payload, signature: &[u8]) -> Result<(), Error>;
}
Source: commons/crypto/mod.rs:116-122

Payload Types

pub enum Payload {
    Buffer(Vec<u8>),
    #[allow(dead_code)]
    BufferArray(Vec<Vec<u8>>),
}
Source: commons/crypto/mod.rs:258-263

KeyPair DSA Implementation

impl DSA for KeyPair {
    fn sign(&self, payload: Payload) -> Result<Vec<u8>, Error> {
        match self {
            KeyPair::Ed25519(x) => x.sign(payload),
            #[cfg(feature = "secp256k1")]
            KeyPair::Secp256k1(x) => x.sign(payload),
        }
    }

    fn verify(&self, payload: Payload, signature: &[u8]) -> Result<(), Error> {
        match self {
            KeyPair::Ed25519(x) => x.verify(payload, signature),
            #[cfg(feature = "secp256k1")]
            KeyPair::Secp256k1(x) => x.verify(payload, signature),
        }
    }
}
Source: commons/crypto/mod.rs:179-202

Key Material

Extracting key bytes:
pub trait KeyMaterial {
    /// Returns the public key bytes as slice
    fn public_key_bytes(&self) -> Vec<u8>;

    /// Returns the secret key bytes as slice
    fn secret_key_bytes(&self) -> Vec<u8>;

    /// Returns bytes from key pair
    fn to_bytes(&self) -> Vec<u8>;

    /// Returns String from key pair encoded in base64
    fn to_str(&self) -> String {
        encode_config(self.to_bytes(), base64::URL_SAFE_NO_PAD)
    }
}
Source: commons/crypto/mod.rs:72-86

KeyPair Material Implementation

impl KeyMaterial for KeyPair {
    fn public_key_bytes(&self) -> Vec<u8> {
        match self {
            KeyPair::Ed25519(x) => x.public_key_bytes(),
            #[cfg(feature = "secp256k1")]
            KeyPair::Secp256k1(x) => x.public_key_bytes(),
        }
    }

    fn secret_key_bytes(&self) -> Vec<u8> {
        match self {
            KeyPair::Ed25519(x) => x.secret_key_bytes(),
            #[cfg(feature = "secp256k1")]
            KeyPair::Secp256k1(x) => x.secret_key_bytes(),
        }
    }

    fn to_bytes(&self) -> Vec<u8> {
        match self {
            KeyPair::Ed25519(x) => x.to_bytes(),
            #[cfg(feature = "secp256k1")]
            KeyPair::Secp256k1(x) => x.to_bytes(),
        }
    }
}
Source: commons/crypto/mod.rs:147-177

Identifiers

TAPLE uses three types of cryptographic identifiers:

Key Identifier

Derived from public keys:
pub struct KeyIdentifier {
    // Internal structure
}

impl KeyIdentifier {
    pub fn new(derivator: KeyDerivator, public_key: &[u8]) -> Self {
        // Creates identifier from public key
    }
}
Source: commons/identifier/key_identifier.rs (referenced)

Digest Identifier

Cryptographic hash of data:
pub struct DigestIdentifier {
    // Internal structure
}
Source: commons/identifier/digest_identifier.rs (referenced)

Signature Identifier

Represents a cryptographic signature:
pub struct SignatureIdentifier {
    // Internal structure
}
Source: commons/identifier/signature_identifier.rs (referenced)

Derivable Trait

All identifiers implement the Derivable trait:
pub trait Derivable: FromStr<Err = Error> {
    fn derivative(&self) -> Vec<u8>;

    fn derivation_code(&self) -> String;

    fn to_str(&self) -> String {
        match self.derivative().len() {
            0 => "".to_string(),
            _ => [
                self.derivation_code(),
                encode_config(self.derivative(), base64::URL_SAFE_NO_PAD),
            ]
            .join(""),
        }
    }
}
Source: commons/identifier/mod.rs:46-61
Identifiers are base64-encoded with a derivation code prefix, making them self-describing and URL-safe.

Digest Derivators

TAPLE supports multiple hash algorithms:
pub enum DigestDerivator {
    Blake3_256,
    Blake3_512,
    SHA2_256,
    SHA2_512,
    SHA3_256,
    SHA3_512,
}
Source: commons/identifier/derive/digest.rs (referenced)

Creating Digest Identifiers

impl DigestIdentifier {
    pub fn from_serializable_borsh<T: BorshSerialize>(
        content: &T,
        derivator: DigestDerivator
    ) -> Result<Self, Error> {
        // Serializes content and hashes it
    }

    pub fn generate_with_blake3<T: BorshSerialize>(content: &T) -> Result<Self, Error> {
        Self::from_serializable_borsh(content, DigestDerivator::Blake3_256)
    }
}
Source: commons/identifier/digest_identifier.rs (referenced)

Signatures

Creating and verifying signatures:
pub struct Signature {
    pub signer: KeyIdentifier,
    pub timestamp: TimeStamp,
    pub content_hash: DigestIdentifier,
    pub value: SignatureIdentifier,
}

impl Signature {
    pub fn new<T: HashId>(
        content: &T,
        key_pair: &KeyPair,
        derivator: DigestDerivator,
    ) -> Result<Self, Error> {
        // Creates signature
    }

    pub fn verify<T: HashId>(&self, content: &T) -> Result<(), Error> {
        // Verifies signature
    }
}
Source: commons/models/signature.rs (referenced)

Node Key Registration

Nodes register their cryptographic identity:
fn register_node_key(
    key_derivator: &KeyDerivator,
    secret_key: &str,
    db: DB<C>,
) -> Result<KeyPair, Error> {
    let key = KeyPair::from_hex(key_derivator, secret_key)
        .map_err(|_| Error::InvalidHexString)
        .unwrap();
    let identifier =
        KeyIdentifier::new(key.get_key_derivator(), &key.public_key_bytes()).to_str();
    let stored_identifier = db.get_controller_id().ok();
    if let Some(stored_identifier) = stored_identifier {
        if identifier != stored_identifier {
            error!("Invalid key. There is a differente key stored");
            return Err(Error::InvalidKeyPairSpecified(stored_identifier));
        }
    } else {
        db.set_controller_id(identifier)
            .map_err(|e| Error::DatabaseError(e.to_string()))?;
    }
    Ok(key)
}
Source: node.rs:418-439
Once a node key is registered, attempting to use a different key will result in an error. This prevents accidental identity changes.

Serialization

Key pairs are serializable for storage:
impl BorshSerialize for KeyPair {
    #[inline]
    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
        match &self {
            KeyPair::Ed25519(x) => {
                BorshSerialize::serialize(&0u8, writer)?;
                let a: [u8; 32] = x.secret_key_bytes().try_into().unwrap();
                BorshSerialize::serialize(&a, writer)
            }
            KeyPair::Secp256k1(x) => {
                BorshSerialize::serialize(&1u8, writer)?;
                let a: [u8; 32] = x.secret_key_bytes().try_into().unwrap();
                BorshSerialize::serialize(&a, writer)
            }
        }
    }
}

impl BorshDeserialize for KeyPair {
    #[inline]
    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
        let order: u8 = BorshDeserialize::deserialize_reader(reader)?;
        match order {
            0 => {
                let data: [u8; 32] = BorshDeserialize::deserialize_reader(reader)?;
                Ok(KeyPair::Ed25519(Ed25519KeyPair::from_secret_key(&data)))
            }
            1 => {
                let data: [u8; 32] = BorshDeserialize::deserialize_reader(reader)?;
                Ok(KeyPair::Secp256k1(Secp256k1KeyPair::from_secret_key(&data)))
            }
            _ => Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("Invalid Value representation: {}", order),
            )),
        }
    }
}
Source: commons/crypto/mod.rs:204-241

Best Practices

Key Storage

Store private keys securely, never in plain text

Ed25519 Default

Use Ed25519 unless you have specific Secp256k1 requirements

Seed Management

Use cryptographically secure random seeds in production

Signature Verification

Always verify signatures before trusting data

Example: Creating and Using Keys

use taple_core::crypto::*;
use taple_core::identifier::derive::KeyDerivator;

// Generate a new Ed25519 key pair
let key_pair = KeyPair::Ed25519(Ed25519KeyPair::new());

// Get public key bytes
let public_key = key_pair.public_key_bytes();

// Sign data
let message = b"Important data";
let signature = key_pair.sign(Payload::Buffer(message.to_vec())).unwrap();

// Verify signature
let result = key_pair.verify(Payload::Buffer(message.to_vec()), &signature);
assert!(result.is_ok());

// Create key identifier
let key_id = KeyIdentifier::new(
    key_pair.get_key_derivator(),
    &key_pair.public_key_bytes()
);
println!("Key ID: {}", key_id.to_str());

Next Steps

Architecture

Understand how cryptography fits in the architecture

Events

Learn how events are signed and verified

Build docs developers (and LLMs) love