diff --git a/CODING.md b/CODING.md index 164b492..302dd0f 100644 --- a/CODING.md +++ b/CODING.md @@ -61,6 +61,7 @@ The `bc-ur` crate is also in this workspace and provides the UR serialization/de | `SymmetricKey`, `EncryptedMessage` | Types for symmetric encryption | | `ECPrivateKey`, `ECPublicKey` | Elliptic curve cryptography keys | | `Ed25519PrivateKey`, `Ed25519PublicKey` | Ed25519 cryptographic keys | +| `Sr25519PrivateKey`, `Sr25519PublicKey` | SR25519 (Schnorr-Ristretto) cryptographic keys | | `X25519PrivateKey`, `X25519PublicKey` | X25519 key agreement keys | | `SigningPrivateKey`, `SigningPublicKey` | Keys for digital signatures | | `MLDSAPrivateKey`, `MLDSAPublicKey` | Post-quantum digital signature keys | @@ -154,10 +155,11 @@ This section inventories all public API items that need documentation, ordered f 2. **✅ `AuthenticationTag`** (`symmetric/authentication_tag.rs`) - Authentication tag for authenticated encryption 3. **✅ `EncryptedMessage`** (`symmetric/encrypted_message.rs`) - A symmetrically-encrypted message -#### Ed25519 and X25519 +#### Ed25519, SR25519, and X25519 1. **✅ `Ed25519PrivateKey`**, **✅ `Ed25519PublicKey`** (`ed25519/ed25519_private_key.rs`, `ed25519/ed25519_public_key.rs`) - Ed25519 keys -2. **✅ `X25519PrivateKey`**, **✅ `X25519PublicKey`** (`x25519/x25519_private_key.rs`, `x25519/x25519_public_key.rs`) - X25519 keys +2. **✅ `Sr25519PrivateKey`**, **✅ `Sr25519PublicKey`** (`sr25519/sr25519_private_key.rs`, `sr25519/sr25519_public_key.rs`) - SR25519 keys +3. **✅ `X25519PrivateKey`**, **✅ `X25519PublicKey`** (`x25519/x25519_private_key.rs`, `x25519/x25519_public_key.rs`) - X25519 keys #### ECDSA diff --git a/Cargo.toml b/Cargo.toml index d46fec8..9081cfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ zeroize = { version = "1.8.1", default-features = false, features = [ "zeroize_derive", ] } rand_core = "^0.9.3" +schnorrkel = { version = "0.11.5", optional = true } +blake2 = { version = "0.10", optional = true } pqcrypto-mlkem = { version = "^0.1.0", optional = true } pqcrypto-mldsa = { version = "^0.1.1", optional = true } pqcrypto-traits = { version = "^0.3.5", optional = true } @@ -46,9 +48,10 @@ indoc = "^2.0.0" version-sync = "^0.9.0" [features] -default = ["secp256k1", "ed25519", "pqcrypto", "ssh"] +default = ["secp256k1", "ed25519", "sr25519", "pqcrypto", "ssh"] secp256k1 = ["bc-crypto/secp256k1"] ed25519 = ["bc-crypto/ed25519"] +sr25519 = ["dep:schnorrkel", "dep:blake2"] pqcrypto = ["dep:pqcrypto-mlkem", "dep:pqcrypto-mldsa", "dep:pqcrypto-traits"] ssh = ["dep:ssh-key"] ssh-agent = ["dep:ssh-agent-client-rs"] diff --git a/README.md b/README.md index afac82d..ffbcd77 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ The library is organized into several categories of cryptographic primitives and | `SchnorrPublicKey` | A Schnorr (x-only) elliptic curve public key (BIP-340) | | `Ed25519PrivateKey` | An Edwards curve (Ed25519) private key for signatures | | `Ed25519PublicKey` | An Edwards curve (Ed25519) public key | +| `Sr25519PrivateKey` | A Schnorr-Ristretto (SR25519) private key for signatures | +| `Sr25519PublicKey` | A Schnorr-Ristretto (SR25519) public key | | `X25519PrivateKey` | A Curve25519 private key used for key agreement | | `X25519PublicKey` | A Curve25519 public key used for key agreement | @@ -81,14 +83,14 @@ The library is organized into several categories of cryptographic primitives and ### Digital Signatures -| Name | Description | -| ------------------- | ----------------------------------------------------------------------------- | -| `SigningPrivateKey` | A private key for digital signatures (Schnorr, ECDSA, Ed25519, MLDSA, or SSH) | -| `SigningPublicKey` | A public key for signature verification | -| `Signature` | A digital signature supporting multiple algorithms | -| `SignatureScheme` | Enumeration of supported signature schemes | -| `Signer` | A trait for types that can create signatures | -| `Verifier` | A trait for types that can verify signatures | +| Name | Description | +| ------------------- | -------------------------------------------------------------------------------------- | +| `SigningPrivateKey` | A private key for digital signatures (Schnorr, ECDSA, Ed25519, Sr25519, MLDSA, or SSH) | +| `SigningPublicKey` | A public key for signature verification | +| `Signature` | A digital signature supporting multiple algorithms | +| `SignatureScheme` | Enumeration of supported signature schemes | +| `Signer` | A trait for types that can create signatures | +| `Verifier` | A trait for types that can verify signatures | ### Key Encapsulation and Encryption diff --git a/run_tests.sh b/run_tests.sh index 2f9b1f2..61421af 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -40,11 +40,14 @@ cargo test --lib --bins --tests --benches --no-default-features > /dev/null test_only_features "pqcrypto" test_only_features "secp256k1" test_only_features "ed25519" +test_only_features "sr25519" test_only_features "ssh" test_only_features "ssh,ed25519" test_only_features "secp256k1,ed25519,pqcrypto" test_only_features "secp256k1,pqcrypto,ssh" +test_only_features "sr25519,ed25519" +test_only_features "sr25519,secp256k1" test_additional_features "ssh-agent" test_additional_features "ssh-agent-tests" diff --git a/src/keypair.rs b/src/keypair.rs index f04db90..879bf3c 100644 --- a/src/keypair.rs +++ b/src/keypair.rs @@ -111,6 +111,7 @@ pub fn keypair_opt( #[cfg(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" ))] @@ -130,6 +131,7 @@ pub fn keypair_opt( #[cfg(not(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" )))] diff --git a/src/lib.rs b/src/lib.rs index 6a54234..6c182cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,11 @@ mod ed25519; #[cfg(feature = "ed25519")] pub use ed25519::{Ed25519PrivateKey, Ed25519PublicKey}; +#[cfg(feature = "sr25519")] +mod sr25519; +#[cfg(feature = "sr25519")] +pub use sr25519::{Sr25519PrivateKey, Sr25519PublicKey}; + mod seed; pub use seed::Seed; diff --git a/src/private_keys.rs b/src/private_keys.rs index c198348..d7010de 100644 --- a/src/private_keys.rs +++ b/src/private_keys.rs @@ -156,6 +156,7 @@ impl CBORTaggedEncodable for PrivateKeys { #[cfg(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" ))] @@ -169,6 +170,7 @@ impl CBORTaggedEncodable for PrivateKeys { #[cfg(not(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" )))] diff --git a/src/public_keys.rs b/src/public_keys.rs index fb247e6..57cd9a4 100644 --- a/src/public_keys.rs +++ b/src/public_keys.rs @@ -150,6 +150,7 @@ impl CBORTaggedEncodable for PublicKeys { #[cfg(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" ))] @@ -163,6 +164,7 @@ impl CBORTaggedEncodable for PublicKeys { #[cfg(not(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" )))] diff --git a/src/signing/mod.rs b/src/signing/mod.rs index 8dccca2..7c8fa77 100644 --- a/src/signing/mod.rs +++ b/src/signing/mod.rs @@ -90,7 +90,7 @@ mod tests { use bc_rand::make_fake_random_number_generator; #[cfg(any(feature = "secp256k1", feature = "pqcrypto"))] use dcbor::prelude::*; - #[cfg(any(feature = "secp256k1", feature = "ed25519"))] + #[cfg(any(feature = "secp256k1", feature = "ed25519", feature = "sr25519"))] use hex_literal::hex; #[cfg(feature = "secp256k1")] use indoc::indoc; @@ -100,6 +100,7 @@ mod tests { #[cfg(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh" ))] use super::SignatureScheme; @@ -110,17 +111,22 @@ mod tests { #[cfg(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh" ))] use crate::SigningOptions; #[cfg(all(feature = "secp256k1", not(feature = "ed25519")))] use crate::SigningPrivateKey; #[cfg(feature = "ed25519")] - use crate::{Ed25519PrivateKey, Signer, SigningPrivateKey, Verifier}; + use crate::Ed25519PrivateKey; + #[cfg(feature = "sr25519")] + use crate::Sr25519PrivateKey; + #[cfg(any(feature = "ed25519", feature = "sr25519"))] + use crate::{Signer, SigningPrivateKey, Verifier}; #[cfg(feature = "pqcrypto")] use crate::{MLDSA, MLDSASignature}; #[cfg(all( - not(feature = "ed25519"), + not(any(feature = "ed25519", feature = "sr25519")), any(feature = "secp256k1", feature = "ssh") ))] use crate::{Signer, Verifier}; @@ -142,9 +148,17 @@ mod tests { "322b5c1dd5a17c3481c2297990c85c232ed3c17b52ce9905c6ec5193ad132c36" ))); + #[cfg(feature = "sr25519")] + fn sr25519_signing_private_key() -> SigningPrivateKey { + SigningPrivateKey::new_sr25519(Sr25519PrivateKey::from_seed(hex!( + "322b5c1dd5a17c3481c2297990c85c232ed3c17b52ce9905c6ec5193ad132c36" + ))) + } + #[cfg(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "pqcrypto", feature = "ssh" ))] @@ -227,6 +241,22 @@ mod tests { assert_eq!(signature, received_signature); } + #[test] + #[cfg(feature = "sr25519")] + fn test_sr25519_cbor() { + let private_key = sr25519_signing_private_key(); + let signature = private_key.sign(MESSAGE).unwrap(); + let signature_cbor: CBOR = signature.clone().into(); + let tagged_cbor_data = signature_cbor.to_cbor_data(); + + let received_signature = + Signature::from_tagged_cbor_data(&tagged_cbor_data).unwrap(); + + let public_key = private_key.public_key().unwrap(); + assert!(public_key.verify(&signature, MESSAGE)); + assert!(public_key.verify(&received_signature, MESSAGE)); + } + #[test] #[cfg(feature = "ed25519")] fn test_ed25519_signing() { @@ -242,6 +272,22 @@ mod tests { assert!(public_key.verify(&another_signature, MESSAGE)); } + #[test] + #[cfg(feature = "sr25519")] + fn test_sr25519_signing() { + let private_key = sr25519_signing_private_key(); + let public_key = private_key.public_key().unwrap(); + let signature = private_key.sign(MESSAGE).unwrap(); + + assert!(public_key.verify(&signature, MESSAGE)); + assert!(!public_key.verify(&signature, b"Wolf Mcnally")); + + // SR25519 signatures include randomness, so they differ each time + let another_signature = private_key.sign(MESSAGE).unwrap(); + assert_ne!(signature, another_signature); + assert!(public_key.verify(&another_signature, MESSAGE)); + } + #[test] #[cfg(feature = "pqcrypto")] fn test_mldsa_signing() { @@ -268,7 +314,7 @@ mod tests { assert_eq!(signature, received_signature); } - #[cfg(any(feature = "secp256k1", feature = "ed25519", feature = "ssh"))] + #[cfg(any(feature = "secp256k1", feature = "ed25519", feature = "sr25519", feature = "ssh"))] fn test_keypair_signing( scheme: SignatureScheme, options: Option, @@ -297,10 +343,16 @@ mod tests { test_keypair_signing(SignatureScheme::Ed25519, None); } + #[test] + #[cfg(feature = "sr25519")] + fn test_sr25519_keypair() { + test_keypair_signing(SignatureScheme::Sr25519, None); + } + #[test] #[cfg(all( feature = "pqcrypto", - any(feature = "secp256k1", feature = "ed25519") + any(feature = "secp256k1", feature = "ed25519", feature = "sr25519") ))] fn test_mldsa44_keypair() { test_keypair_signing(SignatureScheme::MLDSA44, None); @@ -309,7 +361,7 @@ mod tests { #[test] #[cfg(all( feature = "pqcrypto", - any(feature = "secp256k1", feature = "ed25519") + any(feature = "secp256k1", feature = "ed25519", feature = "sr25519") ))] fn test_mldsa65_keypair() { test_keypair_signing(SignatureScheme::MLDSA65, None); @@ -318,7 +370,7 @@ mod tests { #[test] #[cfg(all( feature = "pqcrypto", - any(feature = "secp256k1", feature = "ed25519") + any(feature = "secp256k1", feature = "ed25519", feature = "sr25519") ))] fn test_mldsa87_keypair() { test_keypair_signing(SignatureScheme::MLDSA87, None); diff --git a/src/signing/signature.rs b/src/signing/signature.rs index 324e5ba..2775e6e 100644 --- a/src/signing/signature.rs +++ b/src/signing/signature.rs @@ -2,12 +2,14 @@ use bc_crypto::ED25519_SIGNATURE_SIZE; #[cfg(feature = "secp256k1")] use bc_crypto::{ECDSA_SIGNATURE_SIZE, SCHNORR_SIGNATURE_SIZE}; +#[cfg(feature = "sr25519")] +use crate::sr25519::SR25519_SIGNATURE_SIZE; use bc_ur::prelude::*; #[cfg(feature = "ssh")] use ssh_key::{LineEnding, SshSig}; use super::SignatureScheme; -#[cfg(any(feature = "secp256k1", feature = "ed25519", feature = "ssh"))] +#[cfg(any(feature = "secp256k1", feature = "ed25519", feature = "sr25519", feature = "ssh"))] use crate::Error; #[cfg(feature = "pqcrypto")] use crate::MLDSASignature; @@ -79,6 +81,10 @@ pub enum Signature { #[cfg(feature = "ed25519")] Ed25519([u8; ED25519_SIGNATURE_SIZE]), + /// An SR25519 (Schnorr-Ristretto) signature (64 bytes) + #[cfg(feature = "sr25519")] + Sr25519([u8; SR25519_SIGNATURE_SIZE]), + /// An SSH signature #[cfg(feature = "ssh")] SSH(SshSig), @@ -316,6 +322,69 @@ impl Signature { Ok(Self::Ed25519(arr)) } + /// Creates a new SR25519 signature from a 64-byte array. + /// + /// # Arguments + /// + /// * `data` - The 64-byte SR25519 signature data + /// + /// # Returns + /// + /// A new SR25519 signature + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "sr25519")] + /// # { + /// use bc_components::Signature; + /// + /// let data = [0u8; 64]; // In practice, this would be a real signature + /// let signature = Signature::sr25519_from_data(data); + /// # } + /// ``` + #[cfg(feature = "sr25519")] + pub fn sr25519_from_data(data: [u8; SR25519_SIGNATURE_SIZE]) -> Self { + Self::Sr25519(data) + } + + /// Creates an SR25519 signature from a byte slice. + /// + /// # Arguments + /// + /// * `data` - A byte slice containing the signature data + /// + /// # Returns + /// + /// A `Result` containing the signature or an error if the data is not + /// exactly 64 bytes in length. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "sr25519")] + /// # { + /// use bc_components::Signature; + /// + /// let data = vec![0u8; 64]; // In practice, this would be a real signature + /// let signature = Signature::sr25519_from_data_ref(&data).unwrap(); + /// # } + /// ``` + #[cfg(feature = "sr25519")] + pub fn sr25519_from_data_ref(data: impl AsRef<[u8]>) -> Result { + let data = data.as_ref(); + if data.len() != SR25519_SIGNATURE_SIZE { + return Err(Error::invalid_size( + "SR25519 signature", + SR25519_SIGNATURE_SIZE, + data.len(), + )); + } + let mut arr = [0u8; SR25519_SIGNATURE_SIZE]; + arr.copy_from_slice(data); + Ok(Self::Sr25519(arr)) + } + /// Creates an SSH signature from an `SshSig` object. /// /// # Arguments @@ -431,6 +500,8 @@ impl Signature { Self::ECDSA(_) => Ok(SignatureScheme::Ecdsa), #[cfg(feature = "ed25519")] Self::Ed25519(_) => Ok(SignatureScheme::Ed25519), + #[cfg(feature = "sr25519")] + Self::Sr25519(_) => Ok(SignatureScheme::Sr25519), #[cfg(feature = "ssh")] Self::SSH(sig) => match sig.algorithm() { ssh_key::Algorithm::Dsa => Ok(SignatureScheme::SshDsa), @@ -490,6 +561,11 @@ impl std::fmt::Debug for Signature { .debug_struct("Ed25519") .field("data", &hex::encode(data)) .finish(), + #[cfg(feature = "sr25519")] + Signature::Sr25519(data) => _f + .debug_struct("Sr25519") + .field("data", &hex::encode(data)) + .finish(), #[cfg(feature = "ssh")] Signature::SSH(sig) => { _f.debug_struct("SSH").field("sig", sig).finish() @@ -544,6 +620,8 @@ impl CBORTaggedEncodable for Signature { /// signature /// - SSH: A tagged text string containing the PEM-encoded signature /// - ML-DSA: Delegates to the MLDSASignature implementation + /// - Sr25519: An array containing the discriminator 3 and the 64-byte + /// signature #[allow(unreachable_patterns)] fn untagged_cbor(&self) -> CBOR { match self { @@ -557,6 +635,10 @@ impl CBORTaggedEncodable for Signature { Signature::Ed25519(data) => { vec![(2).into(), CBOR::to_byte_string(data)].into() } + #[cfg(feature = "sr25519")] + Signature::Sr25519(data) => { + vec![(3).into(), CBOR::to_byte_string(data)].into() + } #[cfg(feature = "ssh")] Signature::SSH(sig) => { let pem = sig.to_pem(LineEnding::LF).unwrap(); @@ -625,7 +707,7 @@ impl CBORTaggedDecodable for Signature { let mut drain = elements.drain(0..); let ele_0 = drain.next().unwrap().into_case(); #[cfg_attr( - not(any(feature = "secp256k1", feature = "ed25519")), + not(any(feature = "secp256k1", feature = "ed25519", feature = "sr25519")), allow(unused_variables) )] let ele_1 = drain.next().unwrap().into_case(); @@ -646,6 +728,12 @@ impl CBORTaggedDecodable for Signature { return Ok(Self::ed25519_from_data_ref(data)?); } } + #[cfg(feature = "sr25519")] + CBORCase::Unsigned(3) => { + if let CBORCase::ByteString(data) = ele_1 { + return Ok(Self::sr25519_from_data_ref(data)?); + } + } _ => (), } } diff --git a/src/signing/signature_scheme.rs b/src/signing/signature_scheme.rs index 1ae332f..45ef346 100644 --- a/src/signing/signature_scheme.rs +++ b/src/signing/signature_scheme.rs @@ -7,6 +7,8 @@ use super::{SigningPrivateKey, SigningPublicKey}; use crate::ECPrivateKey; #[cfg(feature = "ed25519")] use crate::Ed25519PrivateKey; +#[cfg(feature = "sr25519")] +use crate::Sr25519PrivateKey; #[cfg(feature = "ssh")] use crate::PrivateKeyBase; #[cfg_attr(not(feature = "pqcrypto"), allow(unused_imports))] @@ -54,6 +56,10 @@ pub enum SignatureScheme { #[cfg_attr(all(feature = "ed25519", not(feature = "secp256k1")), default)] Ed25519, + /// SR25519 (Schnorr-Ristretto) signature scheme + #[cfg(feature = "sr25519")] + Sr25519, + /// ML-DSA44 post-quantum signature scheme (NIST level 2) #[cfg(feature = "pqcrypto")] MLDSA44, @@ -172,6 +178,13 @@ impl SignatureScheme { let public_key = private_key.public_key().unwrap(); (private_key, public_key) } + #[cfg(feature = "sr25519")] + Self::Sr25519 => { + let private_key = + SigningPrivateKey::new_sr25519(Sr25519PrivateKey::new()); + let public_key = private_key.public_key().unwrap(); + (private_key, public_key) + } #[cfg(feature = "pqcrypto")] Self::MLDSA44 => { let (private_key, public_key) = crate::MLDSA::MLDSA44.keypair(); @@ -332,6 +345,14 @@ impl SignatureScheme { let public_key = private_key.public_key().unwrap(); Ok((private_key, public_key)) } + #[cfg(feature = "sr25519")] + Self::Sr25519 => { + let private_key = SigningPrivateKey::new_sr25519( + Sr25519PrivateKey::new_using(_rng), + ); + let public_key = private_key.public_key().unwrap(); + Ok((private_key, public_key)) + } #[cfg(feature = "ssh")] Self::SshEd25519 => { let private_key_base = PrivateKeyBase::new_using(_rng); diff --git a/src/signing/signing_private_key.rs b/src/signing/signing_private_key.rs index d279155..cdf9c4a 100644 --- a/src/signing/signing_private_key.rs +++ b/src/signing/signing_private_key.rs @@ -10,6 +10,8 @@ use ssh_key::{HashAlg, LineEnding, private::PrivateKey as SSHPrivateKey}; use super::Verifier; #[cfg(feature = "ed25519")] use crate::Ed25519PrivateKey; +#[cfg(feature = "sr25519")] +use crate::Sr25519PrivateKey; #[cfg(any(feature = "secp256k1", feature = "ssh", feature = "pqcrypto"))] use crate::Error; #[cfg(feature = "pqcrypto")] @@ -142,6 +144,10 @@ pub enum SigningPrivateKey { #[cfg(feature = "ed25519")] Ed25519(Ed25519PrivateKey), + /// An SR25519 (Schnorr-Ristretto) private key + #[cfg(feature = "sr25519")] + Sr25519(Sr25519PrivateKey), + /// An SSH private key #[cfg(feature = "ssh")] SSH(Box), @@ -165,6 +171,8 @@ impl std::hash::Hash for SigningPrivateKey { Self::ECDSA(key) => key.hash(_state), #[cfg(feature = "ed25519")] Self::Ed25519(key) => key.hash(_state), + #[cfg(feature = "sr25519")] + Self::Sr25519(key) => key.hash(_state), #[cfg(feature = "ssh")] Self::SSH(key) => key.to_bytes().unwrap().hash(_state), #[cfg(feature = "pqcrypto")] @@ -172,6 +180,7 @@ impl std::hash::Hash for SigningPrivateKey { #[cfg(not(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" )))] @@ -266,6 +275,35 @@ impl SigningPrivateKey { Self::Ed25519(key) } + /// Creates a new SR25519 signing private key from an `Sr25519PrivateKey`. + /// + /// # Arguments + /// + /// * `key` - The SR25519 private key to use + /// + /// # Returns + /// + /// A new SR25519 signing private key + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "sr25519")] + /// # { + /// use bc_components::{Sr25519PrivateKey, SigningPrivateKey}; + /// + /// // Create a new SR25519 private key + /// let sr_key = Sr25519PrivateKey::new(); + /// + /// // Create an SR25519 signing key from it + /// let signing_key = SigningPrivateKey::new_sr25519(sr_key); + /// # } + /// ``` + #[cfg(feature = "sr25519")] + pub const fn new_sr25519(key: Sr25519PrivateKey) -> Self { + Self::Sr25519(key) + } + /// Creates a new SSH signing private key from an `SSHPrivateKey`. /// /// # Arguments @@ -402,6 +440,10 @@ impl SigningPrivateKey { Self::Ed25519(key) => { Ok(SigningPublicKey::from_ed25519(key.public_key())) } + #[cfg(feature = "sr25519")] + Self::Sr25519(key) => { + Ok(SigningPublicKey::from_sr25519(key.public_key())) + } #[cfg(feature = "ssh")] Self::SSH(key) => { Ok(SigningPublicKey::from_ssh(key.public_key().clone())) @@ -413,6 +455,7 @@ impl SigningPrivateKey { #[cfg(not(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" )))] @@ -543,7 +586,8 @@ impl SigningPrivateKey { #[cfg(any( feature = "secp256k1", feature = "ssh", - feature = "pqcrypto" + feature = "pqcrypto", + feature = "sr25519" ))] if let Self::Ed25519(key) = self { let sig = key.sign(message.as_ref()); @@ -554,7 +598,8 @@ impl SigningPrivateKey { #[cfg(not(any( feature = "secp256k1", feature = "ssh", - feature = "pqcrypto" + feature = "pqcrypto", + feature = "sr25519" )))] { let Self::Ed25519(key) = self; @@ -563,6 +608,61 @@ impl SigningPrivateKey { } } + /// Signs a message using SR25519. + /// + /// This method is only valid for SR25519 keys. + /// + /// # Arguments + /// + /// * `message` - The message to sign + /// + /// # Returns + /// + /// A `Result` containing the SR25519 signature, or an error if the key is + /// not an SR25519 key. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "sr25519")] + /// # { + /// use bc_components::{Sr25519PrivateKey, Signer, SigningPrivateKey}; + /// + /// // Create an SR25519 key + /// let private_key = SigningPrivateKey::new_sr25519(Sr25519PrivateKey::new()); + /// + /// // Sign a message + /// let message = b"Hello, world!"; + /// let signature = private_key.sign(&message).unwrap(); + /// # } + /// ``` + #[cfg(feature = "sr25519")] + pub fn sr25519_sign(&self, message: impl AsRef<[u8]>) -> Result { + #[cfg(any( + feature = "secp256k1", + feature = "ed25519", + feature = "ssh", + feature = "pqcrypto" + ))] + if let Self::Sr25519(key) = self { + let sig = key.sign(message.as_ref()); + Ok(Signature::sr25519_from_data(sig)) + } else { + Err(Error::crypto("Invalid key type for SR25519 signing")) + } + #[cfg(not(any( + feature = "secp256k1", + feature = "ed25519", + feature = "ssh", + feature = "pqcrypto" + )))] + { + let Self::Sr25519(key) = self; + let sig = key.sign(message.as_ref()); + Ok(Signature::sr25519_from_data(sig)) + } + } + /// Signs a message using SSH. /// /// This method is only valid for SSH keys. @@ -683,6 +783,8 @@ impl Signer for SigningPrivateKey { Self::ECDSA(_) => self.ecdsa_sign(_message), #[cfg(feature = "ed25519")] Self::Ed25519(_) => self.ed25519_sign(_message), + #[cfg(feature = "sr25519")] + Self::Sr25519(_) => self.sr25519_sign(_message), #[cfg(feature = "ssh")] Self::SSH(_) => { if let Some(SigningOptions::Ssh { namespace, hash_alg }) = @@ -700,6 +802,7 @@ impl Signer for SigningPrivateKey { #[cfg(not(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" )))] @@ -784,6 +887,10 @@ impl CBORTaggedEncodable for SigningPrivateKey { SigningPrivateKey::Ed25519(key) => { vec![(2).into(), CBOR::to_byte_string(key.data())].into() } + #[cfg(feature = "sr25519")] + SigningPrivateKey::Sr25519(key) => { + vec![(3).into(), CBOR::to_byte_string(key.to_seed())].into() + } #[cfg(feature = "ssh")] SigningPrivateKey::SSH(key) => { let string = key.to_openssh(LineEnding::LF).unwrap(); @@ -944,6 +1051,7 @@ impl std::fmt::Display for SigningPrivateKey { #[cfg(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" ))] @@ -977,6 +1085,7 @@ impl std::fmt::Display for SigningPrivateKey { #[cfg(not(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" )))] diff --git a/src/signing/signing_public_key.rs b/src/signing/signing_public_key.rs index 8bf3d28..abcb59d 100644 --- a/src/signing/signing_public_key.rs +++ b/src/signing/signing_public_key.rs @@ -4,6 +4,8 @@ use ssh_key::public::PublicKey as SSHPublicKey; #[cfg(feature = "ed25519")] use crate::Ed25519PublicKey; +#[cfg(feature = "sr25519")] +use crate::Sr25519PublicKey; #[cfg(feature = "pqcrypto")] use crate::MLDSAPublicKey; use crate::{Digest, Reference, ReferenceProvider, Signature, Verifier, tags}; @@ -74,6 +76,10 @@ pub enum SigningPublicKey { #[cfg(feature = "ed25519")] Ed25519(Ed25519PublicKey), + /// An SR25519 (Schnorr-Ristretto) public key + #[cfg(feature = "sr25519")] + Sr25519(Sr25519PublicKey), + /// An SSH public key #[cfg(feature = "ssh")] SSH(SSHPublicKey), @@ -167,6 +173,34 @@ impl SigningPublicKey { #[cfg(feature = "ed25519")] pub fn from_ed25519(key: Ed25519PublicKey) -> Self { Self::Ed25519(key) } + /// Creates a new signing public key from an SR25519 public key. + /// + /// # Arguments + /// + /// * `key` - An SR25519 public key + /// + /// # Returns + /// + /// A new signing public key containing the SR25519 key + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "sr25519")] + /// # { + /// use bc_components::{Sr25519PrivateKey, SigningPublicKey}; + /// + /// // Create an SR25519 private key and get its public key + /// let private_key = Sr25519PrivateKey::new(); + /// let public_key = private_key.public_key(); + /// + /// // Create a signing public key from it + /// let signing_key = SigningPublicKey::from_sr25519(public_key); + /// # } + /// ``` + #[cfg(feature = "sr25519")] + pub fn from_sr25519(key: Sr25519PublicKey) -> Self { Self::Sr25519(key) } + /// Creates a new signing public key from an SSH public key. /// /// # Arguments @@ -306,6 +340,18 @@ impl Verifier for SigningPublicKey { Signature::Ed25519(sig) => key.verify(sig, _message), #[cfg(any( feature = "secp256k1", + feature = "sr25519", + feature = "ssh", + feature = "pqcrypto" + ))] + _ => false, + }, + #[cfg(feature = "sr25519")] + SigningPublicKey::Sr25519(key) => match _signature { + Signature::Sr25519(sig) => key.verify(sig, _message), + #[cfg(any( + feature = "secp256k1", + feature = "ed25519", feature = "ssh", feature = "pqcrypto" ))] @@ -372,6 +418,8 @@ impl CBORTaggedEncodable for SigningPublicKey { /// public key /// - SSH: A tagged text string containing the OpenSSH-encoded public key /// - ML-DSA: Delegates to the MLDSAPublicKey implementation + /// - Sr25519: An array containing the discriminator 3 and the 32-byte + /// public key #[allow(unreachable_patterns)] fn untagged_cbor(&self) -> CBOR { match self { @@ -385,6 +433,10 @@ impl CBORTaggedEncodable for SigningPublicKey { SigningPublicKey::Ed25519(key) => { vec![(2).into(), CBOR::to_byte_string(key.data())].into() } + #[cfg(feature = "sr25519")] + SigningPublicKey::Sr25519(key) => { + vec![(3).into(), CBOR::to_byte_string(key.data())].into() + } #[cfg(feature = "ssh")] SigningPublicKey::SSH(key) => { let string = key.to_openssh().unwrap(); @@ -524,6 +576,7 @@ impl std::fmt::Display for SigningPublicKey { #[cfg(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" ))] @@ -553,6 +606,7 @@ impl std::fmt::Display for SigningPublicKey { #[cfg(not(any( feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "ssh", feature = "pqcrypto" )))] diff --git a/src/sr25519/mod.rs b/src/sr25519/mod.rs new file mode 100644 index 0000000..6879149 --- /dev/null +++ b/src/sr25519/mod.rs @@ -0,0 +1,24 @@ +//! SR25519 digital signature algorithm types. +//! +//! This module provides types for working with the SR25519 digital signature +//! algorithm: +//! +//! - `Sr25519PrivateKey`: A 32-byte private key for signing data +//! - `Sr25519PublicKey`: A 32-byte public key for verifying signatures +//! +//! SR25519 (Schnorr-Ristretto) is a signature scheme based on Schnorr signatures +//! over the Ristretto group. It provides: +//! +//! - High security and performance +//! - Non-deterministic signatures +//! - Compatibility with Substrate and Polkadot ecosystems +//! - Support for hierarchical deterministic key derivation (HDKD) +//! +//! Unlike Ed25519, SR25519 uses Schnorr signatures which enable more advanced +//! cryptographic protocols and better batching capabilities. + +mod sr25519_private_key; +pub use sr25519_private_key::Sr25519PrivateKey; + +mod sr25519_public_key; +pub use sr25519_public_key::{Sr25519PublicKey, SR25519_SIGNATURE_SIZE}; diff --git a/src/sr25519/sr25519_private_key.rs b/src/sr25519/sr25519_private_key.rs new file mode 100644 index 0000000..76f4b06 --- /dev/null +++ b/src/sr25519/sr25519_private_key.rs @@ -0,0 +1,182 @@ +use bc_rand::{RandomNumberGenerator, SecureRandomNumberGenerator}; +use schnorrkel::{ + Keypair, MiniSecretKey, SecretKey, Signature as SchnorrkelSignature, + signing_context, +}; + +use crate::{ + Digest, Error, Reference, ReferenceProvider, Result, Sr25519PublicKey, +}; + +pub const SR25519_PRIVATE_KEY_SIZE: usize = 32; +pub const SR25519_SIGNATURE_SIZE: usize = 64; + +/// An SR25519 private key for creating digital signatures. +/// +/// SR25519 is a Schnorr signature scheme based on the Ristretto group, providing: +/// +/// - Fast signature creation and verification +/// - Batch verification capabilities +/// - Hierarchical deterministic key derivation +/// - High security level (equivalent to 128 bits of symmetric security) +/// - Compatibility with Substrate and Polkadot ecosystems +/// +/// This implementation allows: +/// - Creating random SR25519 private keys +/// - Deriving the corresponding public key +/// - Signing messages with context support +/// - Converting between various formats +#[derive(Clone, PartialEq, Eq)] +pub struct Sr25519PrivateKey { + secret: SecretKey, +} + +impl Sr25519PrivateKey { + /// Creates a new random SR25519 private key. + pub fn new() -> Self { + let mut rng = SecureRandomNumberGenerator; + Self::new_using(&mut rng) + } + + /// Creates a new random SR25519 private key using the given random number + /// generator. + pub fn new_using(rng: &mut impl RandomNumberGenerator) -> Self { + let mut seed = [0u8; SR25519_PRIVATE_KEY_SIZE]; + rng.fill_random_data(&mut seed); + Self::from_seed(seed) + } + + /// Creates an SR25519 private key from a 32-byte seed. + pub fn from_seed(seed: [u8; SR25519_PRIVATE_KEY_SIZE]) -> Self { + let mini_secret = MiniSecretKey::from_bytes(&seed) + .expect("32 bytes always valid for MiniSecretKey"); + let secret = mini_secret.expand(schnorrkel::ExpansionMode::Ed25519); + Self { secret } + } + + /// Restores an SR25519 private key from a seed reference. + pub fn from_seed_ref(data: impl AsRef<[u8]>) -> Result { + let data = data.as_ref(); + if data.len() != SR25519_PRIVATE_KEY_SIZE { + return Err(Error::invalid_size( + "SR25519 private key seed", + SR25519_PRIVATE_KEY_SIZE, + data.len(), + )); + } + let mut seed = [0u8; SR25519_PRIVATE_KEY_SIZE]; + seed.copy_from_slice(data); + Ok(Self::from_seed(seed)) + } + + /// Derives a new SR25519 private key from the given key material. + pub fn derive_from_key_material(key_material: impl AsRef<[u8]>) -> Self { + let mut seed = [0u8; SR25519_PRIVATE_KEY_SIZE]; + let material = key_material.as_ref(); + + // Use BLAKE2b to derive a seed from arbitrary length key material + use blake2::{Blake2b512, Digest as Blake2Digest}; + let mut hasher = Blake2b512::new(); + hasher.update(material); + let result = hasher.finalize(); + seed.copy_from_slice(&result[..32]); + + Self::from_seed(seed) + } + + /// Returns the seed bytes of this private key. + pub fn to_seed(&self) -> [u8; SR25519_PRIVATE_KEY_SIZE] { + self.secret.to_bytes()[..SR25519_PRIVATE_KEY_SIZE] + .try_into() + .expect("secret key is 32 bytes") + } + + /// Get the SR25519 private key seed as bytes. + pub fn as_bytes(&self) -> Vec { + self.to_seed().to_vec() + } + + /// Returns the private key seed as a hex string. + pub fn hex(&self) -> String { + hex::encode(self.to_seed()) + } + + /// Creates an SR25519 private key from a hex string. + pub fn from_hex(hex_str: impl AsRef) -> Result { + let data = hex::decode(hex_str.as_ref())?; + Self::from_seed_ref(data) + } + + /// Derives the public key from this SR25519 private key. + pub fn public_key(&self) -> Sr25519PublicKey { + let keypair = Keypair { + secret: self.secret.clone(), + public: self.secret.to_public(), + }; + Sr25519PublicKey::from_public_key(keypair.public) + } + + /// Signs a message with this SR25519 private key. + /// + /// # Arguments + /// + /// * `message` - The message to sign + /// * `context` - Optional signing context (defaults to "substrate" if None) + pub fn sign(&self, message: impl AsRef<[u8]>) -> [u8; SR25519_SIGNATURE_SIZE] { + self.sign_with_context(message, b"substrate") + } + + /// Signs a message with a specific context. + pub fn sign_with_context( + &self, + message: impl AsRef<[u8]>, + context: &[u8], + ) -> [u8; SR25519_SIGNATURE_SIZE] { + let keypair = Keypair { + secret: self.secret.clone(), + public: self.secret.to_public(), + }; + let ctx = signing_context(context); + let signature: SchnorrkelSignature = keypair.sign(ctx.bytes(message.as_ref())); + signature.to_bytes() + } +} + +impl From<[u8; SR25519_PRIVATE_KEY_SIZE]> for Sr25519PrivateKey { + fn from(seed: [u8; SR25519_PRIVATE_KEY_SIZE]) -> Self { + Self::from_seed(seed) + } +} + +// Note: AsRef<[u8]> is not implemented because we cannot return a reference +// to temporary data. Use to_seed() instead. + +impl std::fmt::Debug for Sr25519PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Sr25519PrivateKey({})", self.hex()) + } +} + +impl Default for Sr25519PrivateKey { + fn default() -> Self { + Self::new() + } +} + +impl ReferenceProvider for Sr25519PrivateKey { + fn reference(&self) -> Reference { + Reference::from_digest(Digest::from_image(self.to_seed())) + } +} + +impl std::fmt::Display for Sr25519PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Sr25519PrivateKey") + } +} + +impl std::hash::Hash for Sr25519PrivateKey { + fn hash(&self, state: &mut H) { + self.to_seed().hash(state); + } +} diff --git a/src/sr25519/sr25519_public_key.rs b/src/sr25519/sr25519_public_key.rs new file mode 100644 index 0000000..1ea1b79 --- /dev/null +++ b/src/sr25519/sr25519_public_key.rs @@ -0,0 +1,144 @@ +use schnorrkel::{ + PublicKey, Signature as SchnorrkelSignature, signing_context, +}; + +use crate::{Digest, Error, Reference, ReferenceProvider, Result}; + +pub const SR25519_PUBLIC_KEY_SIZE: usize = 32; +pub const SR25519_SIGNATURE_SIZE: usize = 64; + +/// An SR25519 public key for verifying digital signatures. +/// +/// SR25519 public keys are used to verify signatures created with the +/// corresponding private key. The SR25519 signature system provides: +/// +/// - Fast signature verification +/// - Batch verification capabilities +/// - Small public keys (32 bytes) +/// - High security with resistance to various attacks +/// - Compatibility with Substrate and Polkadot ecosystems +/// +/// This implementation allows: +/// - Creating SR25519 public keys from raw data +/// - Verifying signatures against messages +/// - Converting between various formats +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct Sr25519PublicKey([u8; SR25519_PUBLIC_KEY_SIZE]); + +impl Sr25519PublicKey { + /// Restores an SR25519 public key from an array of bytes. + pub const fn from_data(data: [u8; SR25519_PUBLIC_KEY_SIZE]) -> Self { + Self(data) + } + + /// Restores an SR25519 public key from a byte reference. + pub fn from_data_ref(data: impl AsRef<[u8]>) -> Result { + let data = data.as_ref(); + if data.len() != SR25519_PUBLIC_KEY_SIZE { + return Err(Error::invalid_size( + "SR25519 public key", + SR25519_PUBLIC_KEY_SIZE, + data.len(), + )); + } + let mut key = [0u8; SR25519_PUBLIC_KEY_SIZE]; + key.copy_from_slice(data); + Ok(Self(key)) + } + + /// Creates an SR25519PublicKey from a schnorrkel PublicKey. + pub(crate) fn from_public_key(public: PublicKey) -> Self { + Self(public.to_bytes()) + } + + /// Returns the SR25519 public key as an array of bytes. + pub fn data(&self) -> &[u8; SR25519_PUBLIC_KEY_SIZE] { + &self.0 + } + + /// Get the SR25519 public key as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + self.as_ref() + } + + /// Returns the public key as a hex string. + fn hex(&self) -> String { + hex::encode(self.data()) + } + + /// Creates an SR25519 public key from a hex string. + pub fn from_hex(hex_str: impl AsRef) -> Result { + let data = hex::decode(hex_str.as_ref())?; + Self::from_data_ref(data) + } + + /// Converts this public key to a schnorrkel PublicKey. + fn to_schnorrkel_public(self) -> Result { + PublicKey::from_bytes(&self.0).map_err(|e| { + Error::general(format!("Invalid SR25519 public key: {}", e)) + }) + } + + /// Verifies the given SR25519 signature for the given message using this + /// SR25519 public key with the default "substrate" context. + pub fn verify( + &self, + signature: &[u8; SR25519_SIGNATURE_SIZE], + message: impl AsRef<[u8]>, + ) -> bool { + self.verify_with_context(signature, message, b"substrate") + } + + /// Verifies a signature with a specific context. + pub fn verify_with_context( + &self, + signature: &[u8; SR25519_SIGNATURE_SIZE], + message: impl AsRef<[u8]>, + context: &[u8], + ) -> bool { + let public_key = match self.to_schnorrkel_public() { + Ok(pk) => pk, + Err(_) => return false, + }; + + let sig = match SchnorrkelSignature::from_bytes(signature) { + Ok(s) => s, + Err(_) => return false, + }; + + let ctx = signing_context(context); + public_key + .verify(ctx.bytes(message.as_ref()), &sig) + .is_ok() + } +} + +impl AsRef<[u8]> for Sr25519PublicKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl std::fmt::Debug for Sr25519PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Sr25519PublicKey({})", self.hex()) + } +} + +impl From<[u8; SR25519_PUBLIC_KEY_SIZE]> for Sr25519PublicKey { + fn from(value: [u8; SR25519_PUBLIC_KEY_SIZE]) -> Self { + Self::from_data(value) + } +} + +impl ReferenceProvider for Sr25519PublicKey { + fn reference(&self) -> Reference { + Reference::from_digest(Digest::from_image(self.data())) + } +} + +impl std::fmt::Display for Sr25519PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Sr25519PublicKey({})", self.ref_hex_short()) + } +}