Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 56 additions & 5 deletions src/core/keypairs/algorithms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,13 @@ impl CryptoImplementation for Ed25519 {
/// assert_eq!(Some(signature), signing);
/// ```
fn sign(&self, message: &[u8], private_key: &str) -> XRPLCoreResult<Vec<u8>> {
let raw_private = hex::decode(&private_key[ED25519_PREFIX.len()..])?;
if private_key.len() < ED25519_PREFIX.len() {
return Err(XRPLKeypairsException::InvalidSecret.into());
}
let key_suffix = private_key
.get(ED25519_PREFIX.len()..)
.ok_or(XRPLKeypairsException::InvalidSecret)?;
let raw_private = hex::decode(key_suffix)?;
let raw_private_slice: &[u8; SECRET_KEY_LENGTH] = raw_private
.as_slice()
.try_into()
Expand Down Expand Up @@ -454,24 +460,36 @@ impl CryptoImplementation for Ed25519 {
/// ));
/// ```
fn is_valid_message(&self, message: &[u8], signature: &str, public_key: &str) -> bool {
let raw_public = hex::decode(&public_key[ED25519_PREFIX.len()..]);
if public_key.len() < ED25519_PREFIX.len() {
return false;
}
let key_suffix = match public_key.get(ED25519_PREFIX.len()..) {
Some(s) => s,
None => return false,
};
let raw_public = hex::decode(key_suffix);
let decoded_sig = hex::decode(signature);

if raw_public.is_err() || decoded_sig.is_err() {
return false;
};

if let (Ok(rpub), Ok(dsig)) = (raw_public, decoded_sig) {
let rpub = rpub.as_slice().try_into().unwrap();
let rpub: &[u8; 32] = match rpub.as_slice().try_into() {
Ok(b) => b,
Err(_) => return false,
};
let public = ed25519_dalek::VerifyingKey::from_bytes(rpub);

if dsig.len() != ED25519_SIGNATURE_LENGTH {
return false;
};

if let Ok(value) = public {
let sig: [u8; ED25519_SIGNATURE_LENGTH] =
dsig.try_into().expect("is_valid_message");
let sig: [u8; ED25519_SIGNATURE_LENGTH] = match dsig.try_into() {
Ok(s) => s,
Err(_) => return false,
};
let converted = &ed25519_dalek::Signature::from(sig);

value.verify(message, converted).is_ok()
Expand Down Expand Up @@ -544,4 +562,37 @@ mod test {

assert!(Ed25519.is_valid_message(message, signature, PUBLIC_ED25519));
}

#[test]
fn test_ed25519_sign_trait_short_key_guard() {
// Guard in Ed25519::sign (algorithms.rs) must catch keys shorter than 2 bytes
// even if called directly, bypassing the dispatch guard in mod.rs.
assert!(
Ed25519.sign(&[], "E").is_err(),
"single-byte key must return Err"
);
assert!(Ed25519.sign(&[], "").is_err(), "empty key must return Err");
}

#[test]
fn test_ed25519_is_valid_message_trait_wrong_length_key() {
// "EDAB" has the valid "ED" prefix; suffix "AB" hex-decodes to 1 byte ≠ 32.
// Previously this path called .unwrap() and panicked; after the fix it must
// return false without panicking.
assert!(
!Ed25519.is_valid_message(b"", "AB", "EDAB"),
"1-byte decoded key must return false"
);
// Single-char public key — too short to contain the "ED" prefix at a char boundary.
assert!(
!Ed25519.is_valid_message(b"", "", "E"),
"single-byte public key must return false"
);
// Multi-byte char boundary guard: "E£" is 3 bytes, so slicing at byte 2
// falls inside the £ codepoint and must return false rather than panic.
assert!(
!Ed25519.is_valid_message(b"", "", "E\u{00A3}"),
"non-char-boundary public key must return false"
);
}
}
51 changes: 39 additions & 12 deletions src/core/keypairs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ const fn _get_algorithm_sig_length(algo: CryptoAlgorithm) -> usize {
}

/// Return the CryptoAlgorithm from a key.
fn _get_algorithm_from_key(key: &str) -> CryptoAlgorithm {
match &key[..2] {
ED25519_PREFIX => CryptoAlgorithm::ED25519,
_ => CryptoAlgorithm::SECP256K1,
fn _get_algorithm_from_key(key: &str) -> XRPLCoreResult<CryptoAlgorithm> {
let prefix = key.get(..2).ok_or(XRPLKeypairsException::InvalidSecret)?;
if prefix == ED25519_PREFIX {
Ok(CryptoAlgorithm::ED25519)
} else {
Ok(CryptoAlgorithm::SECP256K1)
}
}

Expand All @@ -48,11 +50,8 @@ fn _get_algorithm_engine(algo: CryptoAlgorithm) -> Box<dyn CryptoImplementation>

/// Return the trait implementation based on the
/// provided key.
fn _get_algorithm_engine_from_key(key: &str) -> Box<dyn CryptoImplementation> {
match &key[..2] {
ED25519_PREFIX => _get_algorithm_engine(CryptoAlgorithm::ED25519),
_ => _get_algorithm_engine(CryptoAlgorithm::SECP256K1),
}
fn _get_algorithm_engine_from_key(key: &str) -> XRPLCoreResult<Box<dyn CryptoImplementation>> {
_get_algorithm_from_key(key).map(_get_algorithm_engine)
}

/// Generate a seed value that cryptographic keys
Expand Down Expand Up @@ -228,7 +227,7 @@ pub fn derive_classic_address(public_key: &str) -> XRPLCoreResult<String> {
/// assert_eq!(Some(signature), signing);
/// ```
pub fn sign(message: &[u8], private_key: &str) -> XRPLCoreResult<String> {
let module = _get_algorithm_engine_from_key(private_key);
let module = _get_algorithm_engine_from_key(private_key)?;
Ok(hex::encode_upper(module.sign(message, private_key)?))
}

Expand All @@ -255,8 +254,8 @@ pub fn sign(message: &[u8], private_key: &str) -> XRPLCoreResult<String> {
/// ));
/// ```
pub fn is_valid_message(message: &[u8], signature: &str, public_key: &str) -> bool {
let module = _get_algorithm_engine_from_key(public_key);
module.is_valid_message(message, signature, public_key)
_get_algorithm_engine_from_key(public_key)
.is_ok_and(|module| module.is_valid_message(message, signature, public_key))
}

/// Trait for cryptographic algorithms in the XRP Ledger.
Expand Down Expand Up @@ -347,4 +346,32 @@ mod test {
assert!(is_valid_message(message, sig_ed25519, PUBLIC_ED25519));
assert!(is_valid_message(message, sig_secp256k1, PUBLIC_SECP256K1));
}

#[test]
fn test_sign_empty_key_returns_error() {
assert!(sign(&[], "").is_err());
}

#[test]
fn test_sign_short_key_returns_error() {
assert!(sign(&[], "e").is_err());
}

#[test]
fn test_is_valid_message_empty_pubkey_returns_false() {
assert!(!is_valid_message(&[], "", ""));
}

#[test]
fn test_is_valid_message_short_pubkey_returns_false() {
// 1-byte key: exercises the <2-byte guard (the original panic case)
assert!(!is_valid_message(&[], "", "a"));
}

#[test]
fn test_is_valid_message_multibyte_pubkey_returns_false() {
// "E£" is 3 bytes (E + 2-byte UTF-8 £); key.get(..2) crosses a char
// boundary and returns None, verifying the non-char-boundary guard.
assert!(!is_valid_message(&[], "", "E\u{00A3}"));
}
}
Loading