Skip to content

Commit

Permalink
radix-crypto-utils
Browse files Browse the repository at this point in the history
  • Loading branch information
Lukasz2891 committed Nov 8, 2024
1 parent a5010d5 commit 2204d17
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 35 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/rust-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: Swatinem/rust-cache@v2
- name: Set up Rust PATH
run: echo "$HOME/.cargo/bin:$PATH" >> $GITHUB_PATH
Expand Down
12 changes: 8 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,23 @@ crypto_secp256k1 = ["secp256k1/recovery", "secp256k1/lowmemory", "secp256k1/allo
# A variant of decrypting the message-signers using k256 library. Cheaper during contract deployment.
crypto_k256 = ["k256/alloc", "k256/sha256", "k256/ecdsa"]

# A variant of decrypting the message-signers using Radix library.
crypto_radix = ["scrypto", "radix-common", "secp256k1/lowmemory", "getrandom/js"]

# A set of helpers for testing & offline usage.
helpers = ["hex/serde", "hex/alloc"]

[dependencies]
casper-contract = { version = "^4.0.0", default-features = false, features = [], optional = true }
casper-types = { version = "^4.0.1", default-features = false, features = [], optional = true }
radix-common = { version = "^1.2.0", default-features = false, features = [], optional = true }
scrypto = { version = "^1.2.0", optional = true }
casper-types = { version = "^4.0.2", default-features = false, features = [], optional = true }
radix-common = { workspace = true, default-features = false, features = [], optional = true }
scrypto = { workspace = true, default-features = false, features = ["alloc"], optional = true }
sha3 = { version = "^0.10.8", default-features = false, features = ["asm"] }
k256 = { version = "^0.13.3", default-features = false, features = [], optional = true }
secp256k1 = { version = "^0.29.0", default-features = false, features = [], optional = true }
secp256k1 = { version = "^0.28.0", default-features = false, features = [], optional = true }
hex = { version = "^0.4.3", default-features = false, features = [], optional = true }
primitive-types = { version = "^0.13.1", optional = true }
getrandom = { version = "0.2.15", optional = true, default-features = false }

[dev-dependencies]
itertools = { version = "^0.13.0" }
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
CLIPPY=cargo clippy --release --fix --allow-dirty --allow-staged
DOC=cargo doc --no-deps --document-private-items
TEST=RUST_BACKTRACE=full cargo test --features="helpers"
FEATURE_SETS="crypto_k256" "crypto_k256,network_casper" "crypto_secp256k1" "crypto_secp256k1,network_casper" "crypto_k256,network_radix" "crypto_secp256k1" "crypto_secp256k1,network_radix"
FEATURE_SETS="crypto_k256" "crypto_k256,network_casper" "crypto_secp256k1" "crypto_secp256k1,network_casper" "crypto_secp256k1" "crypto_secp256k1,network_radix"

prepare:
@rustup target add wasm32-unknown-unknown

test:
test: clippy
# $(TEST) --features="crypto_radix,network_radix"
# $(TEST) --features="crypto_radix"
@for features in $(FEATURE_SETS); do \
echo "Running tests with features: $$features"; \
($(TEST) --features=$$features); \
Expand Down
17 changes: 13 additions & 4 deletions src/crypto/keccak256.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
use sha3::{Digest, Keccak256};
use crate::crypto::Keccak256Hash;
#[cfg(not(all(feature = "crypto_radix", target_arch = "wasm32")))]
use sha3::Digest;

pub fn keccak256(data: &[u8]) -> Box<[u8]> {
Keccak256::new_with_prefix(data)
#[cfg(not(all(feature = "crypto_radix", target_arch = "wasm32")))]
pub fn keccak256(data: &[u8]) -> Keccak256Hash {
sha3::Keccak256::new_with_prefix(data)
.finalize()
.as_slice()
.into()
.try_into()
.unwrap()
}

#[cfg(all(feature = "crypto_radix", target_arch = "wasm32"))]
pub fn keccak256(data: &[u8]) -> Keccak256Hash {
scrypto::prelude::CryptoUtils::keccak256_hash(data).0
}

#[cfg(feature = "helpers")]
Expand Down
4 changes: 4 additions & 0 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
mod keccak256;
pub(crate) mod recover;

pub(crate) type Keccak256Hash = [u8; 32];
pub(crate) type Secp256SigRs = [u8; 64];
pub(crate) type EcdsaUncompressedPublicKey = [u8; 65];
93 changes: 68 additions & 25 deletions src/crypto/recover.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use crate::crypto::{keccak256, recover::crypto256::recover_public_key};
use crate::crypto::{
keccak256, recover::crypto256::recover_public_key, EcdsaUncompressedPublicKey, Keccak256Hash,
Secp256SigRs,
};

pub fn recover_address(message: Vec<u8>, signature: Vec<u8>) -> Vec<u8> {
//TODO: check malleability support by the libraries
let recovery_byte = signature[64]; // 65-byte representation
let msg_hash = keccak256::keccak256(message.as_slice());
let key = recover_public_key(
msg_hash,
&signature[..64],
signature[..64].try_into().unwrap(),
recovery_byte - (if recovery_byte >= 27 { 27 } else { 0 }),
);
let key_hash = keccak256::keccak256(&key[1..]); // skip first uncompressed-key byte
Expand All @@ -15,61 +19,100 @@ pub fn recover_address(message: Vec<u8>, signature: Vec<u8>) -> Vec<u8> {

#[cfg(feature = "crypto_secp256k1")]
pub(crate) mod crypto256 {
use super::{EcdsaUncompressedPublicKey, Keccak256Hash, Secp256SigRs};
use crate::network::{assert::Unwrap, error::Error};
use secp256k1::{ecdsa::RecoverableSignature, Message, Secp256k1 as Secp256k1Curve};

pub(crate) fn recover_public_key(
message_hash: Box<[u8]>,
signature_bytes: &[u8],
message_hash: Keccak256Hash,
signature_bytes: Secp256SigRs,
recovery_byte: u8,
) -> Box<[u8]> {
let msg = Message::from_digest_slice(message_hash.as_ref())
.unwrap_or_revert(|_| Error::CryptographicError(message_hash.len()));
) -> EcdsaUncompressedPublicKey {
let msg = Message::from_digest(message_hash);

let recovery_id = secp256k1::ecdsa::RecoveryId::from_i32(recovery_byte.into())
.unwrap_or_revert(|_| Error::CryptographicError(recovery_byte.into()));

let sig: RecoverableSignature =
RecoverableSignature::from_compact(signature_bytes, recovery_id)
RecoverableSignature::from_compact(signature_bytes.as_slice(), recovery_id)
.unwrap_or_revert(|_| Error::CryptographicError(signature_bytes.len()));

let public_key = Secp256k1Curve::new().recover_ecdsa(&msg, &sig);

public_key.unwrap().serialize_uncompressed().into()
public_key.unwrap().serialize_uncompressed()
}
}

#[cfg(feature = "crypto_k256")]
pub(crate) mod crypto256 {
use super::{EcdsaUncompressedPublicKey, Keccak256Hash, Secp256SigRs};
use crate::network::{assert::Unwrap, error::Error};
use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};

pub(crate) fn recover_public_key(
message_hash: Box<[u8]>,
signature_bytes: &[u8],
message_hash: Keccak256Hash,
signature_bytes: Secp256SigRs,
recovery_byte: u8,
) -> Box<[u8]> {
) -> EcdsaUncompressedPublicKey {
let recovery_id = RecoveryId::from_byte(recovery_byte)
.unwrap_or_revert(|_| Error::CryptographicError(recovery_byte.into()));

let signature = Signature::try_from(signature_bytes)
let signature = Signature::try_from(signature_bytes.as_slice())
.unwrap_or_revert(|_| Error::CryptographicError(signature_bytes.len()));

let recovered_key =
VerifyingKey::recover_from_prehash(message_hash.as_ref(), &signature, recovery_id)
.map(|key| key.to_encoded_point(false).to_bytes());
.map(|key| key.to_encoded_point(false).to_bytes())
.unwrap_or_revert(|_| Error::CryptographicError(0));

recovered_key.unwrap()
recovered_key.as_ref().try_into().unwrap()
}
}

#[cfg(all(not(feature = "crypto_k256"), not(feature = "crypto_secp256k1")))]
#[cfg(all(feature = "crypto_radix", target_arch = "wasm32"))]
pub(crate) mod crypto256 {
use super::{EcdsaUncompressedPublicKey, Keccak256Hash, Secp256SigRs};
use crate::network::assert::Unwrap;
use crate::network::error::Error;
use radix_common::crypto::{Hash, IsHash, Secp256k1Signature};
use scrypto::crypto_utils::CryptoUtils;
use secp256k1::PublicKey;

pub(crate) fn recover_public_key(
message_hash: Keccak256Hash,
signature_bytes: Secp256SigRs,
recovery_byte: u8,
) -> EcdsaUncompressedPublicKey {
let hash = Hash::from_bytes(message_hash);

let mut sig_vec = Vec::with_capacity(65);
sig_vec.push(recovery_byte);
sig_vec.extend(signature_bytes);
let signature = Secp256k1Signature::try_from(sig_vec.as_slice())
.unwrap_or_revert(|_| Error::CryptographicError(signature_bytes.len()));

let compressed_key: [u8; 33] =
CryptoUtils::secp256k1_ecdsa_verify_and_key_recover(hash, signature).0;
let pub_key = PublicKey::from_slice(&compressed_key)
.unwrap_or_revert(|_| Error::CryptographicError(compressed_key.len()));

pub_key.serialize_uncompressed()
}
}

#[cfg(all(
not(feature = "crypto_k256"),
not(feature = "crypto_secp256k1"),
not(all(feature = "crypto_radix", target_arch = "wasm32"))
))]
pub(crate) mod crypto256 {
use super::{EcdsaUncompressedPublicKey, Keccak256Hash, Secp256SigRs};

pub(crate) fn recover_public_key(
_message_hash: Box<[u8]>,
_signature_bytes: &[u8],
_message_hash: Keccak256Hash,
_signature_bytes: Secp256SigRs,
_recovery_byte: u8,
) -> Box<[u8]> {
) -> EcdsaUncompressedPublicKey {
panic!("Not implemented!")
}
}
Expand All @@ -95,16 +138,16 @@ mod tests {

#[test]
fn test_recover_public_key_v27() {
let public_key = recover_public_key(u8_box(MESSAGE_HASH), &u8_box(SIG_V27), 0);
let public_key = recover_public_key(u8_slice(MESSAGE_HASH), u8_slice(SIG_V27), 0);

assert_eq!(u8_box(PUBLIC_KEY_V27), public_key);
assert_eq!(u8_slice(PUBLIC_KEY_V27), public_key);
}

#[test]
fn test_recover_public_key_v28() {
let public_key = recover_public_key(u8_box(MESSAGE_HASH), &u8_box(SIG_V28), 1);
let public_key = recover_public_key(u8_slice(MESSAGE_HASH), u8_slice(SIG_V28), 1);

assert_eq!(u8_box(PUBLIC_KEY_V28), public_key);
assert_eq!(u8_slice(PUBLIC_KEY_V28), public_key);
}

#[test]
Expand All @@ -127,7 +170,7 @@ mod tests {
assert_eq!(hex_to_bytes(ADDRESS_V28.into()), address);
}

fn u8_box(str: &str) -> Box<[u8]> {
hex_to_bytes(str.into()).as_slice().into()
fn u8_slice<const N: usize>(str: &str) -> [u8; N] {
hex_to_bytes(str.into()).as_slice().try_into().unwrap()
}
}

0 comments on commit 2204d17

Please sign in to comment.