├── .github ├── CODEOWNERS └── workflows │ ├── test.yaml │ ├── format.yaml │ └── lint.yaml ├── .gitignore ├── src ├── collections │ ├── mod.rs │ └── typed_usize.rs ├── sdk │ ├── mod.rs │ ├── api.rs │ ├── key.rs │ └── wire_bytes.rs ├── crypto_tools │ ├── mod.rs │ ├── testdata │ │ ├── rng_seed_signing_key_known_vectors.golden │ │ └── rng_seed_ecdsa_ephemeral_scalar_known_vectors.golden │ ├── message_digest.rs │ ├── rng.rs │ └── k256_serde.rs ├── constants.rs ├── lib.rs ├── ed25519 │ ├── testdata │ │ └── keygen_sign_known_vectors.golden │ └── mod.rs └── ecdsa │ ├── testdata │ └── keygen_sign_known_vectors.golden │ └── mod.rs ├── LICENSE-MIT ├── README.md ├── Cargo.toml └── LICENSE-APACHE /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @axelarnetwork/protocol 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode -------------------------------------------------------------------------------- /src/collections/mod.rs: -------------------------------------------------------------------------------- 1 | mod typed_usize; 2 | pub use typed_usize::TypedUsize; 3 | -------------------------------------------------------------------------------- /src/sdk/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | 3 | pub(crate) mod key; 4 | 5 | pub(crate) mod wire_bytes; 6 | -------------------------------------------------------------------------------- /src/crypto_tools/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "secp256k1")] 2 | pub mod k256_serde; 3 | 4 | pub mod message_digest; 5 | 6 | #[cfg(any(feature = "secp256k1", feature = "ed25519"))] 7 | pub mod rng; 8 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | // Domain separation for protocols/schemes 2 | #[cfg(feature = "secp256k1")] 3 | pub const ECDSA_TAG: u8 = 0x00; 4 | 5 | #[cfg(feature = "ed25519")] 6 | pub const ED25519_TAG: u8 = 0x01; 7 | -------------------------------------------------------------------------------- /src/crypto_tools/testdata/rng_seed_signing_key_known_vectors.golden: -------------------------------------------------------------------------------- 1 | [ 2 | "b2c35aa8dae0f4f116d486ce3189398aafcc84167904afad1b7791ae6801cc79", 3 | "c3fb9c19fddb649911b5ae8ca72367d99ea4bdf18064390c6d9082d406ff5a38" 4 | ] -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod collections; 2 | 3 | mod constants; 4 | 5 | pub mod sdk; 6 | 7 | mod crypto_tools; 8 | 9 | #[cfg(feature = "secp256k1")] 10 | pub mod ecdsa; 11 | 12 | #[cfg(feature = "ed25519")] 13 | pub mod ed25519; 14 | -------------------------------------------------------------------------------- /src/crypto_tools/testdata/rng_seed_ecdsa_ephemeral_scalar_known_vectors.golden: -------------------------------------------------------------------------------- 1 | [ 2 | "2af4f23a13541f798225876f6bacfbd54a1eeb35a64c1522c5e8784e70e6e27b", 3 | "72c48bd09c63d23951cf8977bea3269de76bc906466dc121504a1121dca0d828", 4 | "aabdeede33057b70fd68436345efea0a70e1d5009fafba0ad59f2827e92e289c" 5 | ] -------------------------------------------------------------------------------- /src/sdk/api.rs: -------------------------------------------------------------------------------- 1 | //! API for tofn users 2 | pub type TofnResult = Result; 3 | pub type BytesVec = Vec; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq)] 6 | pub struct TofnFatal; 7 | 8 | /// Expose tofn's (de)serialization functions 9 | /// that use the appropriate bincode config options. 10 | pub use super::wire_bytes::{deserialize, serialize}; 11 | 12 | pub use super::key::SecretRecoveryKey; 13 | 14 | pub use crate::crypto_tools::message_digest::MessageDigest; 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Install stable toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: 1.78.0 22 | override: true 23 | 24 | - name: Run cargo test 25 | run: cargo test --release --all-features 26 | -------------------------------------------------------------------------------- /src/ed25519/testdata/keygen_sign_known_vectors.golden: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "2fa7606e48bdca1855ac5c5922d6b7d07bcf6c06cb986a1e9816d5f6c4e40fcb", 4 | "fc13a6dc64e0777c0c3ef81d55d243466a1e86177a66ca80a757a201a2ae2d57", 5 | "fb6d72c3ed348f66053abaa3eb5df71f5cfdb9cc55e41dff6d3ebcfed4461be328ce70f86b22ad9444f3b4042b51c7ca08cce3bf90427fa0949ff2be2776a602" 6 | ], 7 | [ 8 | "37ec7ada60889124172cc5c85b137b8813001081bdb7164b782e7b47841ba1e8", 9 | "2340c4b8632b99f1ec9184b9a82091ec92fa6f16090e5fa33464c010c93e623a", 10 | "288c26f1fe0a9fc1a7d1df1620372cd68d55c74fbf48feaaafe24728584bee81d399d332cfd8899292115c2a377a1bddc6ece54d4eded255b150068ba9df9a0a" 11 | ] 12 | ] -------------------------------------------------------------------------------- /src/ecdsa/testdata/keygen_sign_known_vectors.golden: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "b2c35aa8dae0f4f116d486ce3189398aafcc84167904afad1b7791ae6801cc79", 4 | "02f6b8430a2d705ddc8b1ae530052ea26183aa66723f2e35b3a7d7d213fdbcb641", 5 | "3044022037431e9941bc2fdb0b79bf506e61e03a21aae9f2ad576de3a71c9689313e57cd022077aabd03ea0f117416c324a3b7a55efaf5955d60e03d1d389d29bb95d8a9c47a" 6 | ], 7 | [ 8 | "1400c57b757dbe0ec38e06f46c338e31b7c09d685ea7b9e75b7f49c429229279", 9 | "035b8d97cecf9ef4828f77007f94eb746aa300f7dbee8833d46681133bf5765d3f", 10 | "304402203e2c224974b53452ff0fed865a1918d6d3a94dfd01f082c60a1042308d3b71980220396728b3b3bc78ac3db58a804bb4d19ce153baf79f71872c124a64e2883b8ec2" 11 | ] 12 | ] -------------------------------------------------------------------------------- /.github/workflows/format.yaml: -------------------------------------------------------------------------------- 1 | name: Formatting 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | format: 8 | strategy: 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Install stable toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: 1.78.0 22 | override: true 23 | components: rustfmt, clippy 24 | 25 | - name: Run cargo fmt 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: fmt 29 | args: --all -- --check 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | lint: 8 | strategy: 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Install stable toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: 1.78.0 22 | override: true 23 | components: rustfmt, clippy 24 | 25 | - name: Run cargo clippy with all features 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: clippy 29 | args: --all-features --all-targets -- -D warnings 30 | -------------------------------------------------------------------------------- /src/crypto_tools/message_digest.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{ 3 | array::TryFromSliceError, 4 | convert::{TryFrom, TryInto}, 5 | }; 6 | 7 | /// Sign only 32-byte hash digests 8 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 9 | pub struct MessageDigest(pub(super) [u8; 32]); 10 | 11 | impl TryFrom<&[u8]> for MessageDigest { 12 | type Error = TryFromSliceError; 13 | 14 | fn try_from(v: &[u8]) -> Result { 15 | Ok(Self(v.try_into()?)) 16 | } 17 | } 18 | 19 | impl From<[u8; 32]> for MessageDigest { 20 | fn from(v: [u8; 32]) -> Self { 21 | Self(v) 22 | } 23 | } 24 | 25 | impl AsRef<[u8]> for MessageDigest { 26 | fn as_ref(&self) -> &[u8] { 27 | &self.0 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/sdk/key.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | array::TryFromSliceError, 3 | convert::{TryFrom, TryInto}, 4 | }; 5 | use zeroize::Zeroize; 6 | 7 | #[derive(Debug, Clone, Zeroize)] 8 | #[zeroize(drop)] 9 | pub struct SecretRecoveryKey(pub(crate) [u8; 64]); 10 | 11 | impl TryFrom<&[u8]> for SecretRecoveryKey { 12 | type Error = TryFromSliceError; 13 | 14 | fn try_from(v: &[u8]) -> Result { 15 | Ok(Self(v.try_into()?)) 16 | } 17 | } 18 | 19 | #[cfg(test)] 20 | /// return the all-zero array with the first bytes set to the bytes of `index` 21 | pub fn dummy_secret_recovery_key(index: usize) -> SecretRecoveryKey { 22 | let index_bytes = index.to_be_bytes(); 23 | let mut result = [0; 64]; 24 | for (i, &b) in index_bytes.iter().enumerate() { 25 | result[i] = b; 26 | } 27 | SecretRecoveryKey(result) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Axelar Foundation 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tofn: a cryptography library in Rust 2 | 3 | Tofn provides the following: 4 | 5 | * An implementation of ECDSA SECP256k1 signing scheme. 6 | * An implementation of ED25519 signing scheme. 7 | 8 | tofn is primarily used as part of [tofnd](https://github.com/axelarnetwork/tofnd) for the [Axelar network](https://www.axelar.network). For an older version of the library that included a threshold ECDSA implementation, see the section below on Threshold cryptography. 9 | 10 | ## Setup 11 | 12 | * Get the latest version of Rust stable. 13 | * Clone this repo. 14 | * Run `cargo build` to build the library. 15 | * Run `cargo test` to run the tests. 16 | * Run `GOLDIE_UPDATE=1 cargo test` to generate golden files for relevant tests. 17 | 18 | ## Threshold cryptography 19 | 20 | For an implementation of the [GG20](https://eprint.iacr.org/2020/540.pdf) threshold-ECDSA protocol, 21 | see this version of [tofn](https://github.com/axelarnetwork/tofn/tree/0b441ed758ebed6726f7a2cf1ccce6a95c33152c). This GG20 protocol implementation should not be considered ready for production since it has *known vulnerabilities* against [recently discovered attacks](https://www.verichains.io/tsshock/) on the protocol implementation. This was removed from `tofn` as it is not being used in the Axelar protocol. 22 | 23 | ## License 24 | 25 | All crates licensed under either of 26 | 27 | * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 28 | * [MIT license](http://opensource.org/licenses/MIT) 29 | 30 | at your option. 31 | 32 | ## Contribution 33 | 34 | Unless you explicitly state otherwise, any contribution intentionally submitted 35 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 36 | dual licensed as above, without any additional terms or conditions. 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tofn" 3 | version = "1.1.0" 4 | authors = [ 5 | "Interoplabs Eng ", 6 | ] 7 | edition = "2021" 8 | license = "MIT OR Apache-2.0" 9 | rust-version = "1.78.0" 10 | description = "A cryptography library in Rust, used by the Axelar Network." 11 | keywords = ["cryptography", "axelar", "blockchain"] 12 | 13 | [lib] 14 | crate-type = ["lib"] 15 | 16 | [dependencies] 17 | serde = { version = "1.0", features = ["derive"] } 18 | bincode = "1.3.3" 19 | rand_chacha = "0.3" 20 | hmac = "0.12" 21 | zeroize = { version = "1.8", features = ["zeroize_derive"] } 22 | rand = "0.8" 23 | sha2 = { version = "0.10", features = [ 24 | "std", 25 | "asm", 26 | ], default-features = false } 27 | 28 | # Ecdsa deps 29 | k256 = { version = "0.13", features = [ 30 | "ecdsa", 31 | ], default-features = false, optional = true } 32 | ecdsa = { version = "0.16", features = [ 33 | "digest", 34 | ], default-features = false, optional = true } # needed only for FromDigest trait 35 | crypto-bigint = { version = "0.5", default-features = false, optional = true } 36 | 37 | # Ed25519 deps 38 | ed25519 = { version = "2.2", default-features = false, optional = true } 39 | ed25519-dalek = { version = "2.1", features = [ 40 | "rand_core", 41 | "zeroize", 42 | "asm", 43 | ], default_features = false, optional = true } 44 | 45 | # logging 46 | tracing = { version = "0.1", default-features = false } 47 | 48 | [dev-dependencies] 49 | tracing-test = "0.2" # enable logging for tests 50 | tracing-subscriber = { version = "0.3", features = [ 51 | "env-filter", 52 | "fmt", 53 | ], default-features = false } 54 | goldie = "0.5" 55 | hex = "0.4" 56 | 57 | # Don't abort in case there is a panic to clean up data 58 | [profile.dev] 59 | panic = "unwind" 60 | 61 | [profile.release] 62 | panic = "unwind" 63 | 64 | [features] 65 | default = ["secp256k1", "ed25519"] 66 | secp256k1 = ["dep:ecdsa", "dep:k256", "dep:crypto-bigint"] 67 | ed25519 = ["dep:ed25519", "dep:ed25519-dalek"] 68 | -------------------------------------------------------------------------------- /src/collections/typed_usize.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 2 | use std::marker::PhantomData; 3 | use zeroize::Zeroize; 4 | 5 | pub struct TypedUsize(usize, PhantomData); 6 | 7 | impl TypedUsize { 8 | pub fn from_usize(index: usize) -> Self { 9 | TypedUsize(index, PhantomData) 10 | } 11 | 12 | pub fn as_usize(&self) -> usize { 13 | self.0 14 | } 15 | 16 | // Platform-independent byte conversion 17 | pub fn to_bytes(&self) -> [u8; 8] { 18 | (self.0 as u64).to_be_bytes() 19 | } 20 | } 21 | 22 | impl Zeroize for TypedUsize { 23 | fn zeroize(&mut self) { 24 | self.0.zeroize() 25 | } 26 | } 27 | 28 | // Manual blanket impls for common traits. 29 | // `#[derive(...)]` doesn't work for all `K`: 30 | // * https://stackoverflow.com/a/31371094 31 | // * https://github.com/serde-rs/serde/issues/183#issuecomment-157348366 32 | 33 | impl Copy for TypedUsize {} 34 | 35 | impl Clone for TypedUsize { 36 | fn clone(&self) -> Self { 37 | *self 38 | } 39 | } 40 | 41 | impl std::fmt::Debug for TypedUsize { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | self.0.fmt(f) 44 | } 45 | } 46 | 47 | impl std::fmt::Display for TypedUsize { 48 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 49 | self.0.fmt(f) 50 | } 51 | } 52 | 53 | impl PartialEq for TypedUsize { 54 | fn eq(&self, other: &Self) -> bool { 55 | self.0 == other.0 56 | } 57 | } 58 | 59 | impl Serialize for TypedUsize { 60 | fn serialize(&self, serializer: S) -> Result 61 | where 62 | S: Serializer, 63 | { 64 | self.0.serialize(serializer) 65 | } 66 | } 67 | 68 | impl<'de, K> Deserialize<'de> for TypedUsize { 69 | fn deserialize(deserializer: D) -> Result 70 | where 71 | D: Deserializer<'de>, 72 | { 73 | Ok(Self::from_usize(usize::deserialize(deserializer)?)) 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::TypedUsize; 80 | use crate::sdk::wire_bytes::{deserialize, serialize}; 81 | 82 | struct TestMarker; 83 | 84 | #[test] 85 | fn serde_bincode() { 86 | // test: `TypedUsize` and `usize` serialize to the same bytes 87 | let untyped: usize = 12345678; 88 | let typed = TypedUsize::::from_usize(untyped); 89 | let untyped_bytes = serialize(&untyped).unwrap(); 90 | let typed_bytes = serialize(&typed).unwrap(); 91 | assert_eq!(typed_bytes, untyped_bytes); 92 | let typed_deserialized: TypedUsize = deserialize(&typed_bytes).unwrap(); 93 | assert_eq!(typed_deserialized, typed); 94 | assert_eq!(typed_deserialized.as_usize(), untyped); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/sdk/wire_bytes.rs: -------------------------------------------------------------------------------- 1 | use crate::sdk::api::TofnFatal; 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | use tracing::{error, warn}; 4 | 5 | use super::api::{BytesVec, TofnResult}; 6 | use bincode::{ 7 | config::{ 8 | BigEndian, Bounded, RejectTrailing, VarintEncoding, WithOtherEndian, WithOtherIntEncoding, 9 | WithOtherLimit, WithOtherTrailing, 10 | }, 11 | DefaultOptions, Options, 12 | }; 13 | 14 | /// Max message length allowed to be (de)serialized 15 | const MAX_MSG_LEN: u64 = 1000 * 1000; // 1 MB 16 | 17 | /// Serialize a value using bincode and log errors 18 | pub fn serialize(value: &T) -> TofnResult { 19 | let bincode = bincoder(); 20 | 21 | bincode.serialize(value).map_err(|err| { 22 | error!("serialization failure: {}", err.to_string()); 23 | TofnFatal 24 | }) 25 | } 26 | 27 | /// Deserialize bytes to a type using bincode and log errors. 28 | /// Return an Option type since deserialization isn't treated as a Fatal error 29 | /// in tofn (for the purposes of fault identification). 30 | pub fn deserialize(bytes: &[u8]) -> Option { 31 | let bincode = bincoder(); 32 | 33 | bincode 34 | .deserialize(bytes) 35 | .map_err(|err| { 36 | warn!("deserialization failure: {}", err.to_string()); 37 | }) 38 | .ok() 39 | } 40 | 41 | /// Prepare a `bincode` serde backend with our preferred config 42 | /// (wow, that return type is ugly) 43 | #[allow(clippy::type_complexity)] 44 | fn bincoder() -> WithOtherTrailing< 45 | WithOtherIntEncoding< 46 | WithOtherEndian, BigEndian>, 47 | VarintEncoding, 48 | >, 49 | RejectTrailing, 50 | > { 51 | DefaultOptions::new() 52 | .with_limit(MAX_MSG_LEN) 53 | .with_big_endian() // do not ignore extra bytes at the end of the buffer 54 | .with_varint_encoding() // saves a lot of space in smaller messages 55 | .reject_trailing_bytes() // do not ignore extra bytes at the end of the buffer 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use bincode::{DefaultOptions, Options}; 61 | 62 | use crate::sdk::wire_bytes::{deserialize, serialize, MAX_MSG_LEN}; 63 | 64 | #[test] 65 | fn basic_correctness() { 66 | let msg = 255u8; 67 | let encoded_msg = serialize(&msg).unwrap(); 68 | assert_eq!(msg, deserialize::(&encoded_msg).unwrap()); 69 | 70 | let msg = 0xFFFFFFFF_usize; 71 | let encoded_msg = serialize(&msg).unwrap(); 72 | assert_eq!(msg, deserialize::(&encoded_msg).unwrap()); 73 | 74 | let msg = vec![42u64; 10]; 75 | let encoded_msg = serialize(&msg).unwrap(); 76 | assert_eq!(msg, deserialize::>(&encoded_msg).unwrap()); 77 | } 78 | 79 | #[test] 80 | fn serialization_checks() { 81 | // Fail to serialize a large message 82 | let msg = vec![0; (MAX_MSG_LEN - 2) as usize]; // 2 bytes for length 83 | assert!(serialize(&msg).is_err()); 84 | 85 | // Fail to deserialize a buffer with extra bytes 86 | let mut encoded_msg = serialize(&2_u8).unwrap(); 87 | encoded_msg.extend_from_slice(&[42u8]); 88 | let res: Option = deserialize(&encoded_msg); 89 | assert!(res.is_none()); 90 | 91 | // Fail to deserialize a large buffer 92 | let options = DefaultOptions::new() 93 | .with_big_endian() 94 | .with_varint_encoding(); 95 | let encoded_msg = options 96 | .serialize(&[42; (MAX_MSG_LEN as usize) + 1][..]) 97 | .unwrap(); 98 | let res: Option = deserialize(&encoded_msg); 99 | assert!(res.is_none()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/ed25519/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | constants::ED25519_TAG, 3 | crypto_tools::{message_digest::MessageDigest, rng}, 4 | sdk::{ 5 | api::{BytesVec, TofnFatal, TofnResult}, 6 | key::SecretRecoveryKey, 7 | }, 8 | }; 9 | use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH}; 10 | 11 | #[derive(Debug)] 12 | pub struct KeyPair(SigningKey); 13 | 14 | impl KeyPair { 15 | pub fn encoded_verifying_key(&self) -> [u8; PUBLIC_KEY_LENGTH] { 16 | *self.0.verifying_key().as_bytes() 17 | } 18 | } 19 | 20 | pub fn keygen( 21 | secret_recovery_key: &SecretRecoveryKey, 22 | session_nonce: &[u8], 23 | ) -> TofnResult { 24 | let mut rng = 25 | rng::rng_seed_signing_key(ED25519_TAG, KEYGEN_TAG, secret_recovery_key, session_nonce)?; 26 | 27 | let signing_key = SigningKey::generate(&mut rng); 28 | 29 | Ok(KeyPair(signing_key)) 30 | } 31 | 32 | /// Returns a Ed25519 signature. 33 | /// The signature is encoded raw (R and S bytes) as a 64-byte array as per this [RFC](https://www.rfc-editor.org/rfc/rfc8032#section-3.3) 34 | pub fn sign(signing_key: &KeyPair, message_digest: &MessageDigest) -> TofnResult { 35 | Ok(signing_key 36 | .0 37 | .sign(message_digest.as_ref()) 38 | .to_bytes() 39 | .into()) 40 | } 41 | 42 | pub fn verify( 43 | encoded_verifying_key: &[u8; PUBLIC_KEY_LENGTH], 44 | message_digest: &MessageDigest, 45 | encoded_signature: &[u8], 46 | ) -> TofnResult { 47 | let verifying_key = VerifyingKey::from_bytes(encoded_verifying_key).map_err(|_| TofnFatal)?; 48 | 49 | let signature = Signature::from_slice(encoded_signature).map_err(|_| TofnFatal)?; 50 | 51 | Ok(verifying_key 52 | .verify_strict(message_digest.as_ref(), &signature) 53 | .is_ok()) 54 | } 55 | 56 | /// Domain separation for seeding the RNG 57 | const KEYGEN_TAG: u8 = 0x00; 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::{keygen, sign, verify}; 62 | use crate::sdk::key::{dummy_secret_recovery_key, SecretRecoveryKey}; 63 | 64 | #[test] 65 | fn keygen_sign_decode_verify() { 66 | let message_digest = [42; 32].into(); 67 | 68 | let key_pair = keygen(&dummy_secret_recovery_key(42), b"tofn nonce").unwrap(); 69 | let mut encoded_signature = sign(&key_pair, &message_digest).unwrap(); 70 | 71 | // Correct signature should verify 72 | let success = verify( 73 | &key_pair.encoded_verifying_key(), 74 | &message_digest, 75 | &encoded_signature, 76 | ) 77 | .unwrap(); 78 | 79 | assert!(success); 80 | 81 | // Tamper with the signature, it should no longer verify. 82 | *encoded_signature.last_mut().unwrap() += 1; 83 | 84 | let success = verify( 85 | &key_pair.encoded_verifying_key(), 86 | &message_digest, 87 | &encoded_signature, 88 | ) 89 | .unwrap(); 90 | 91 | assert!(!success); 92 | } 93 | 94 | /// Check keygen/signing outputs against golden files to catch regressions (such as on updating deps). 95 | /// Golden files were generated from tofn v0.2.0 release when ed25519 was added. 96 | #[test] 97 | fn keygen_sign_known_vectors() { 98 | struct TestCase { 99 | secret_recovery_key: SecretRecoveryKey, 100 | session_nonce: Vec, 101 | message_digest: [u8; 32], 102 | } 103 | 104 | let test_cases = vec![ 105 | TestCase { 106 | secret_recovery_key: SecretRecoveryKey([0; 64]), 107 | session_nonce: vec![0; 4], 108 | message_digest: [42; 32], 109 | }, 110 | TestCase { 111 | secret_recovery_key: SecretRecoveryKey([0xff; 64]), 112 | session_nonce: vec![0xff; 32], 113 | message_digest: [0xff; 32], 114 | }, 115 | ]; 116 | 117 | let expected_outputs: Vec> = test_cases 118 | .into_iter() 119 | .map(|test_case| { 120 | let keypair = 121 | keygen(&test_case.secret_recovery_key, &test_case.session_nonce).unwrap(); 122 | let encoded_signing_key = keypair.0.to_bytes().into(); 123 | let encoded_verifying_key = keypair.encoded_verifying_key().to_vec(); 124 | 125 | let signature = sign(&keypair, &test_case.message_digest.into()).unwrap(); 126 | 127 | let success = verify( 128 | &keypair.encoded_verifying_key(), 129 | &test_case.message_digest.into(), 130 | &signature, 131 | ) 132 | .unwrap(); 133 | assert!(success); 134 | 135 | [encoded_signing_key, encoded_verifying_key, signature] 136 | .into_iter() 137 | .map(hex::encode) 138 | .collect() 139 | }) 140 | .collect(); 141 | 142 | goldie::assert_json!(expected_outputs); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/ecdsa/mod.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use ecdsa::{ 4 | elliptic_curve::{sec1::ToEncodedPoint, Field}, 5 | hazmat::{SignPrimitive, VerifyPrimitive}, 6 | }; 7 | use message_digest::MessageDigest; 8 | use tracing::error; 9 | 10 | use crate::{ 11 | constants::ECDSA_TAG, 12 | crypto_tools::{k256_serde, message_digest, rng}, 13 | sdk::{ 14 | api::{BytesVec, TofnFatal, TofnResult}, 15 | key::SecretRecoveryKey, 16 | }, 17 | }; 18 | 19 | #[derive(Debug)] 20 | pub struct KeyPair { 21 | signing_key: k256_serde::SecretScalar, 22 | encoded_verifying_key: [u8; 33], // SEC1-encoded compressed curve point 23 | } 24 | 25 | impl KeyPair { 26 | /// SEC1-encoded compressed curve point. 27 | /// tofnd needs to return this to axelar-core. 28 | pub fn encoded_verifying_key(&self) -> &[u8; 33] { 29 | &self.encoded_verifying_key 30 | } 31 | 32 | /// tofnd needs to store this in the kv store. 33 | pub fn signing_key(&self) -> &k256_serde::SecretScalar { 34 | &self.signing_key 35 | } 36 | } 37 | 38 | pub fn keygen( 39 | secret_recovery_key: &SecretRecoveryKey, 40 | session_nonce: &[u8], 41 | ) -> TofnResult { 42 | let rng = rng::rng_seed_signing_key(ECDSA_TAG, KEYGEN_TAG, secret_recovery_key, session_nonce)?; 43 | 44 | let signing_key = k256_serde::SecretScalar::random(rng); 45 | 46 | // TODO make this work with k256_serde::ProjectivePoint::to_bytes 47 | let encoded_verifying_key = k256_serde::ProjectivePoint::from(&signing_key) 48 | .as_ref() 49 | .to_affine() 50 | .to_encoded_point(true) 51 | .as_bytes() 52 | .try_into() 53 | .map_err(|_| { 54 | error!("failure to convert ecdsa verifying key to 33-byte array"); 55 | TofnFatal 56 | })?; 57 | 58 | Ok(KeyPair { 59 | signing_key, 60 | encoded_verifying_key, 61 | }) 62 | } 63 | 64 | /// Returns a ASN.1 DER-encoded ECDSA signature. 65 | /// These signatures have variable byte length so we must return a [BytesVec] instead of a [u8] array. 66 | pub fn sign( 67 | signing_key: &k256_serde::SecretScalar, 68 | message_digest: &MessageDigest, 69 | ) -> TofnResult { 70 | let signing_key = signing_key.as_ref(); 71 | let message_digest_scalar = k256::Scalar::from(message_digest); 72 | 73 | let rng = rng::rng_seed_ecdsa_ephemeral_scalar( 74 | ECDSA_TAG, 75 | SIGN_TAG, 76 | signing_key, 77 | &message_digest_scalar, 78 | )?; 79 | let ephemeral_scalar = k256::Scalar::random(rng); 80 | 81 | let signature = k256_serde::Signature::from( 82 | signing_key 83 | .try_sign_prehashed(ephemeral_scalar, &message_digest_scalar.to_bytes()) 84 | .map_err(|_| { 85 | error!("failure to sign"); 86 | TofnFatal 87 | }) 88 | .map(|(r, _)| r)?, 89 | ); 90 | 91 | Ok(signature.to_bytes()) 92 | } 93 | 94 | pub fn verify( 95 | encoded_verifying_key: &[u8; 33], 96 | message_digest: &MessageDigest, 97 | encoded_signature: &[u8], 98 | ) -> TofnResult { 99 | let verifying_key = 100 | k256_serde::ProjectivePoint::from_bytes(encoded_verifying_key).ok_or(TofnFatal)?; 101 | let signature = k256::ecdsa::Signature::from_der(encoded_signature).map_err(|_| TofnFatal)?; 102 | 103 | Ok(verifying_key 104 | .as_ref() 105 | .to_affine() 106 | .verify_prehashed(&k256::FieldBytes::from(message_digest), &signature) 107 | .is_ok()) 108 | } 109 | 110 | /// Domain separation for seeding the RNG 111 | const KEYGEN_TAG: u8 = 0x00; 112 | const SIGN_TAG: u8 = 0x01; 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use super::{keygen, sign, verify}; 117 | use crate::sdk::key::{dummy_secret_recovery_key, SecretRecoveryKey}; 118 | 119 | #[test] 120 | fn keygen_sign_decode_verify() { 121 | let message_digest = [42; 32].into(); 122 | 123 | let key_pair = keygen(&dummy_secret_recovery_key(42), b"tofn nonce").unwrap(); 124 | let encoded_signature = sign(key_pair.signing_key(), &message_digest).unwrap(); 125 | let success = verify( 126 | key_pair.encoded_verifying_key(), 127 | &message_digest, 128 | &encoded_signature, 129 | ) 130 | .unwrap(); 131 | 132 | assert!(success); 133 | } 134 | 135 | /// Check keygen/signing outputs against golden files to catch regressions (such as on updating deps). 136 | /// Golden files were generated from tofn commit corresponding to tofnd v0.10.1 release 137 | #[test] 138 | fn keygen_sign_known_vectors() { 139 | struct TestCase { 140 | secret_recovery_key: SecretRecoveryKey, 141 | session_nonce: Vec, 142 | message_digest: [u8; 32], 143 | } 144 | 145 | let test_cases = vec![ 146 | TestCase { 147 | secret_recovery_key: SecretRecoveryKey([0; 64]), 148 | session_nonce: vec![0; 4], 149 | message_digest: [42; 32], 150 | }, 151 | TestCase { 152 | secret_recovery_key: SecretRecoveryKey([0xff; 64]), 153 | session_nonce: vec![0xff; 32], 154 | message_digest: [0xff; 32], 155 | }, 156 | ]; 157 | 158 | let expected_outputs: Vec> = test_cases 159 | .into_iter() 160 | .map(|test_case| { 161 | let keypair = 162 | keygen(&test_case.secret_recovery_key, &test_case.session_nonce).unwrap(); 163 | let encoded_signing_key = keypair.signing_key().as_ref().to_bytes().to_vec(); 164 | let encoded_verifying_key = keypair.encoded_verifying_key().to_vec(); 165 | 166 | let signature: Vec = 167 | sign(keypair.signing_key(), &test_case.message_digest.into()).unwrap(); 168 | 169 | let success = verify( 170 | keypair.encoded_verifying_key(), 171 | &test_case.message_digest.into(), 172 | &signature, 173 | ) 174 | .unwrap(); 175 | assert!(success); 176 | 177 | [encoded_signing_key, encoded_verifying_key, signature] 178 | .into_iter() 179 | .map(hex::encode) 180 | .collect() 181 | }) 182 | .collect(); 183 | 184 | goldie::assert_json!(expected_outputs); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/crypto_tools/rng.rs: -------------------------------------------------------------------------------- 1 | use hmac::{Mac, SimpleHmac}; 2 | use rand::{CryptoRng, RngCore, SeedableRng}; 3 | use rand_chacha::ChaCha20Rng; 4 | use sha2::Sha256; 5 | use tracing::error; 6 | use zeroize::Zeroize; 7 | 8 | use crate::sdk::{ 9 | api::{TofnFatal, TofnResult}, 10 | key::SecretRecoveryKey, 11 | }; 12 | 13 | const SESSION_NONCE_LENGTH_MIN: usize = 4; 14 | const SESSION_NONCE_LENGTH_MAX: usize = 256; 15 | 16 | /// Initialize a RNG by hashing the arguments. 17 | /// Intended for use generating a ECDSA signing key. 18 | pub(crate) fn rng_seed_signing_key( 19 | protocol_tag: u8, 20 | tag: u8, 21 | secret_recovery_key: &SecretRecoveryKey, 22 | session_nonce: &[u8], 23 | ) -> TofnResult { 24 | if session_nonce.len() < SESSION_NONCE_LENGTH_MIN 25 | || session_nonce.len() > SESSION_NONCE_LENGTH_MAX 26 | { 27 | error!( 28 | "invalid session_nonce length {} not in [{},{}]", 29 | session_nonce.len(), 30 | SESSION_NONCE_LENGTH_MIN, 31 | SESSION_NONCE_LENGTH_MAX 32 | ); 33 | return Err(TofnFatal); 34 | } 35 | 36 | let mut prf = 37 | SimpleHmac::::new_from_slice(&secret_recovery_key.0[..]).map_err(|_| { 38 | error!("failure to initialize hmac"); 39 | TofnFatal 40 | })?; 41 | 42 | prf.update(&protocol_tag.to_be_bytes()); 43 | prf.update(&tag.to_be_bytes()); 44 | prf.update(session_nonce); 45 | 46 | let seed = prf.finalize().into_bytes().into(); 47 | 48 | Ok(ChaCha20Rng::from_seed(seed)) 49 | } 50 | 51 | /// Initialize a RNG by hashing the arguments. 52 | /// Intended for use generating an ephemeral scalar for ECDSA signatures in the spirit of RFC 6979, 53 | /// except this implementation does not conform to RFC 6979. 54 | /// Compare with RustCrypto: 55 | #[cfg(feature = "secp256k1")] 56 | pub(crate) fn rng_seed_ecdsa_ephemeral_scalar( 57 | protocol_tag: u8, 58 | tag: u8, 59 | signing_key: &k256::Scalar, 60 | message_digest: &k256::Scalar, 61 | ) -> TofnResult { 62 | let mut signing_key_bytes = signing_key.to_bytes(); 63 | let msg_to_sign_bytes = message_digest.to_bytes(); 64 | 65 | let mut prf = SimpleHmac::::new(&Default::default()); 66 | 67 | prf.update(&protocol_tag.to_be_bytes()); 68 | prf.update(&tag.to_be_bytes()); 69 | prf.update(&signing_key_bytes); 70 | prf.update(&msg_to_sign_bytes); 71 | 72 | signing_key_bytes.zeroize(); 73 | 74 | let seed = prf.finalize().into_bytes().into(); 75 | 76 | Ok(ChaCha20Rng::from_seed(seed)) 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use crate::{ 82 | crypto_tools::{ 83 | message_digest::MessageDigest, 84 | rng::{rng_seed_ecdsa_ephemeral_scalar, rng_seed_signing_key}, 85 | }, 86 | sdk::key::SecretRecoveryKey, 87 | }; 88 | 89 | use crypto_bigint::ArrayEncoding; 90 | use ecdsa::elliptic_curve::ops::Reduce; 91 | use k256::U256; 92 | use rand::RngCore; 93 | 94 | /// Check rng outputs against golden files to catch regressions (such as on updating deps). 95 | /// Golden files were generated from tofn commit corresponding to tofnd v0.10.1 release 96 | #[test] 97 | fn rng_seed_signing_key_known_vectors() { 98 | struct TestCase { 99 | protocol_tag: u8, 100 | tag: u8, 101 | secret_recovery_key: SecretRecoveryKey, 102 | session_nonce: Vec, 103 | } 104 | 105 | let test_cases = vec![ 106 | TestCase { 107 | protocol_tag: 0, 108 | tag: 0, 109 | secret_recovery_key: SecretRecoveryKey([0; 64]), 110 | session_nonce: vec![0; 4], 111 | }, 112 | TestCase { 113 | protocol_tag: 0x01, 114 | tag: 0x02, 115 | secret_recovery_key: SecretRecoveryKey([0x11; 64]), 116 | session_nonce: vec![0xAA, 0xBB, 0xCC, 0xDD], 117 | }, 118 | ]; 119 | 120 | let expected_outputs: Vec<_> = test_cases 121 | .into_iter() 122 | .map(|test_case| { 123 | let mut rng = rng_seed_signing_key( 124 | test_case.protocol_tag, 125 | test_case.tag, 126 | &test_case.secret_recovery_key, 127 | &test_case.session_nonce, 128 | ) 129 | .expect("Failed to initialize RNG"); 130 | 131 | let mut output = [0u8; 32]; 132 | rng.fill_bytes(&mut output); 133 | 134 | hex::encode(output) 135 | }) 136 | .collect(); 137 | 138 | goldie::assert_json!(expected_outputs); 139 | } 140 | 141 | /// Check rng outputs against golden files to catch regressions (such as on updating deps). 142 | /// Golden files were generated from tofn commit corresponding to tofnd v0.10.1 release 143 | #[test] 144 | fn rng_seed_ecdsa_ephemeral_scalar_known_vectors() { 145 | struct TestCase { 146 | protocol_tag: u8, 147 | tag: u8, 148 | signing_key: [u8; 32], 149 | message_digest: MessageDigest, 150 | } 151 | 152 | let test_cases: Vec = vec![ 153 | TestCase { 154 | protocol_tag: 0, 155 | tag: 0, 156 | signing_key: [0; 32], 157 | message_digest: MessageDigest::from([0; 32]), 158 | }, 159 | TestCase { 160 | protocol_tag: 0, 161 | tag: 0, 162 | signing_key: [1; 32], 163 | message_digest: MessageDigest::from([2; 32]), 164 | }, 165 | TestCase { 166 | protocol_tag: 1, 167 | tag: 2, 168 | signing_key: [255; 32], 169 | message_digest: MessageDigest::from([255; 32]), 170 | }, 171 | ]; 172 | 173 | let expected_outputs: Vec<_> = test_cases 174 | .into_iter() 175 | .map(|test_case| { 176 | let signing_key = 177 | k256::Scalar::reduce(U256::from_be_byte_array(test_case.signing_key.into())); 178 | 179 | let mut rng = rng_seed_ecdsa_ephemeral_scalar( 180 | test_case.protocol_tag, 181 | test_case.tag, 182 | &signing_key, 183 | &k256::Scalar::from(&test_case.message_digest), 184 | ) 185 | .expect("Failed to initialize RNG"); 186 | 187 | let mut output = [0u8; 32]; 188 | rng.fill_bytes(&mut output); 189 | 190 | hex::encode(output) 191 | }) 192 | .collect(); 193 | 194 | goldie::assert_json!(expected_outputs); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Axelar Foundation 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/crypto_tools/k256_serde.rs: -------------------------------------------------------------------------------- 1 | //! serde support for k256 2 | //! 3 | //! ## References 4 | //! 5 | //! [Implementing Serialize · Serde](https://serde.rs/impl-serialize.html) 6 | //! [Implementing Deserialize · Serde](https://serde.rs/impl-deserialize.html) 7 | 8 | use crypto_bigint::ArrayEncoding; 9 | use ecdsa::elliptic_curve::ops::Reduce; 10 | use ecdsa::elliptic_curve::{ 11 | consts::U33, generic_array::GenericArray, group::GroupEncoding, Field, 12 | }; 13 | use k256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; 14 | use k256::U256; 15 | use rand::{CryptoRng, RngCore}; 16 | use serde::{de, de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; 17 | use zeroize::Zeroize; 18 | 19 | use crate::crypto_tools::message_digest::MessageDigest; 20 | use crate::sdk::api::BytesVec; 21 | 22 | /// Convert a 32-byte hash digest into a scalar as per SEC1: 23 | /// for k256::Scalar { 27 | fn from(v: &MessageDigest) -> Self { 28 | k256::Scalar::reduce(U256::from_be_byte_array(v.0.into())) 29 | } 30 | } 31 | 32 | impl From<&MessageDigest> for k256::FieldBytes { 33 | fn from(v: &MessageDigest) -> Self { 34 | k256::Scalar::from(v).to_bytes() 35 | } 36 | } 37 | 38 | /// A wrapper for a random scalar value that is zeroized on drop 39 | /// TODO why not just do this for Scalar below? 40 | #[derive(Debug, Serialize, Deserialize, PartialEq, Zeroize)] 41 | #[zeroize(drop)] 42 | pub struct SecretScalar(Scalar); 43 | 44 | impl AsRef for SecretScalar { 45 | fn as_ref(&self) -> &k256::Scalar { 46 | &self.0 .0 47 | } 48 | } 49 | 50 | impl SecretScalar { 51 | pub fn random_with_thread_rng() -> Self { 52 | Self(Scalar(k256::Scalar::random(rand::thread_rng()))) 53 | } 54 | 55 | pub fn random(rng: impl CryptoRng + RngCore) -> Self { 56 | Self(Scalar(k256::Scalar::random(rng))) 57 | } 58 | } 59 | 60 | #[derive(Clone, Debug, PartialEq, Zeroize)] 61 | pub struct Scalar(k256::Scalar); 62 | 63 | impl AsRef for Scalar { 64 | fn as_ref(&self) -> &k256::Scalar { 65 | &self.0 66 | } 67 | } 68 | 69 | #[cfg(feature = "malicious")] 70 | impl AsMut for Scalar { 71 | fn as_mut(&mut self) -> &mut k256::Scalar { 72 | &mut self.0 73 | } 74 | } 75 | 76 | impl From for Scalar { 77 | fn from(s: k256::Scalar) -> Self { 78 | Scalar(s) 79 | } 80 | } 81 | 82 | impl Serialize for Scalar { 83 | fn serialize(&self, serializer: S) -> Result 84 | where 85 | S: Serializer, 86 | { 87 | let bytes: [u8; 32] = self.0.to_bytes().into(); 88 | bytes.serialize(serializer) 89 | } 90 | } 91 | 92 | impl<'de> Deserialize<'de> for Scalar { 93 | fn deserialize(deserializer: D) -> Result 94 | where 95 | D: Deserializer<'de>, 96 | { 97 | let bytes: [u8; 32] = Deserialize::deserialize(deserializer)?; 98 | let field_bytes = k256::FieldBytes::from(bytes); 99 | let scalar = k256::Scalar::reduce(U256::from_be_byte_array(bytes.into())); 100 | 101 | // ensure bytes encodes an integer less than the secp256k1 modulus 102 | // if not then scalar.to_bytes() will differ from bytes 103 | if field_bytes != scalar.to_bytes() { 104 | return Err(D::Error::custom("integer exceeds secp256k1 modulus")); 105 | } 106 | 107 | Ok(Scalar(scalar)) 108 | } 109 | } 110 | 111 | #[derive(Clone, Debug, PartialEq)] 112 | pub struct Signature(k256::ecdsa::Signature); 113 | 114 | impl Signature { 115 | /// Returns a ASN.1 DER-encoded ECDSA signature. 116 | /// ASN.1 DER encodings have variable byte length so we can't return a `[u8]` array. 117 | /// Must return a `BytesVec` instead of `&[u8]` to avoid returning a reference to temporary data. 118 | pub fn to_bytes(&self) -> BytesVec { 119 | self.0.to_der().as_bytes().to_vec() 120 | } 121 | 122 | /// Decode from a ASN.1 DER-encoded ECDSA signature. 123 | pub fn from_bytes(bytes: &[u8]) -> Option { 124 | Some(Self(k256::ecdsa::Signature::from_der(bytes).ok()?)) 125 | } 126 | } 127 | 128 | impl AsRef for Signature { 129 | fn as_ref(&self) -> &k256::ecdsa::Signature { 130 | &self.0 131 | } 132 | } 133 | 134 | impl From for Signature { 135 | fn from(s: k256::ecdsa::Signature) -> Self { 136 | Signature(s) 137 | } 138 | } 139 | 140 | impl Serialize for Signature { 141 | fn serialize(&self, serializer: S) -> Result 142 | where 143 | S: Serializer, 144 | { 145 | self.to_bytes().serialize(serializer) 146 | } 147 | } 148 | 149 | impl<'de> Deserialize<'de> for Signature { 150 | fn deserialize(deserializer: D) -> Result 151 | where 152 | D: Deserializer<'de>, 153 | { 154 | Signature::from_bytes(Deserialize::deserialize(deserializer)?) 155 | .ok_or_else(|| D::Error::custom("signature DER decoding failure")) 156 | } 157 | } 158 | 159 | #[derive(Clone, Debug, PartialEq, Zeroize)] 160 | struct EncodedPoint(k256::EncodedPoint); 161 | 162 | impl Serialize for EncodedPoint { 163 | fn serialize(&self, serializer: S) -> Result 164 | where 165 | S: Serializer, 166 | { 167 | serializer.serialize_bytes(self.0.as_bytes()) 168 | } 169 | } 170 | 171 | impl<'de> Deserialize<'de> for EncodedPoint { 172 | fn deserialize(deserializer: D) -> Result 173 | where 174 | D: Deserializer<'de>, 175 | { 176 | deserializer.deserialize_bytes(EncodedPointVisitor) 177 | } 178 | } 179 | 180 | struct EncodedPointVisitor; 181 | 182 | impl<'de> Visitor<'de> for EncodedPointVisitor { 183 | type Value = EncodedPoint; 184 | 185 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 186 | formatter.write_str("SEC1-encoded secp256k1 (K-256) curve point") 187 | } 188 | 189 | fn visit_bytes(self, v: &[u8]) -> Result 190 | where 191 | E: de::Error, 192 | { 193 | Ok(EncodedPoint( 194 | k256::EncodedPoint::from_bytes(v).map_err(E::custom)?, 195 | )) 196 | } 197 | } 198 | 199 | #[derive(Clone, Debug, PartialEq)] 200 | pub struct ProjectivePoint(k256::ProjectivePoint); 201 | 202 | impl ProjectivePoint { 203 | #[allow(dead_code)] 204 | /// Returns a SEC1-encoded compressed curve point. 205 | pub fn to_bytes(&self) -> [u8; 33] { 206 | to_array33(self.0.to_affine().to_bytes()) 207 | } 208 | 209 | /// Decode from a SEC1-encoded curve point. 210 | pub fn from_bytes(bytes: &[u8]) -> Option { 211 | k256::ProjectivePoint::from_encoded_point(&k256::EncodedPoint::from_bytes(bytes).ok()?) 212 | .map(Self) 213 | .into() 214 | } 215 | } 216 | 217 | impl AsRef for ProjectivePoint { 218 | fn as_ref(&self) -> &k256::ProjectivePoint { 219 | &self.0 220 | } 221 | } 222 | 223 | #[cfg(feature = "malicious")] 224 | impl AsMut for ProjectivePoint { 225 | fn as_mut(&mut self) -> &mut k256::ProjectivePoint { 226 | &mut self.0 227 | } 228 | } 229 | 230 | impl From for ProjectivePoint { 231 | fn from(p: k256::ProjectivePoint) -> Self { 232 | ProjectivePoint(p) 233 | } 234 | } 235 | 236 | impl From<&k256::ProjectivePoint> for ProjectivePoint { 237 | fn from(p: &k256::ProjectivePoint) -> Self { 238 | ProjectivePoint(*p) 239 | } 240 | } 241 | 242 | impl From<&SecretScalar> for ProjectivePoint { 243 | fn from(s: &SecretScalar) -> Self { 244 | ProjectivePoint(k256::ProjectivePoint::GENERATOR * s.0 .0) 245 | } 246 | } 247 | 248 | impl Serialize for ProjectivePoint { 249 | fn serialize(&self, serializer: S) -> Result 250 | where 251 | S: Serializer, 252 | { 253 | EncodedPoint(self.0.to_encoded_point(true)).serialize(serializer) 254 | } 255 | } 256 | 257 | impl<'de> Deserialize<'de> for ProjectivePoint { 258 | fn deserialize(deserializer: D) -> Result 259 | where 260 | D: Deserializer<'de>, 261 | { 262 | Option::<_>::from( 263 | k256::ProjectivePoint::from_encoded_point(&EncodedPoint::deserialize(deserializer)?.0) 264 | .map(Self), 265 | ) 266 | .ok_or_else(|| D::Error::custom("SEC1-encoded point is not on curve secp256k1 (K-256)")) 267 | } 268 | } 269 | 270 | /// [GenericArray] does not impl `From` for arrays of length exceeding 32. 271 | /// Hence, this helper function. 272 | fn to_array33(g: GenericArray) -> [u8; 33] { 273 | [ 274 | g[0], g[1], g[2], g[3], g[4], g[5], g[6], g[7], g[8], g[9], g[10], g[11], g[12], g[13], 275 | g[14], g[15], g[16], g[17], g[18], g[19], g[20], g[21], g[22], g[23], g[24], g[25], g[26], 276 | g[27], g[28], g[29], g[30], g[31], g[32], 277 | ] 278 | } 279 | 280 | #[cfg(test)] 281 | mod tests { 282 | use super::*; 283 | use bincode::Options; 284 | use ecdsa::hazmat::{SignPrimitive, VerifyPrimitive}; 285 | use k256::elliptic_curve::Field; 286 | use serde::de::DeserializeOwned; 287 | use std::fmt::Debug; 288 | 289 | #[test] 290 | fn basic_round_trip() { 291 | let s = k256::Scalar::random(rand::thread_rng()); 292 | basic_round_trip_impl::<_, Scalar>(s, Some(32)); 293 | 294 | let p = k256::ProjectivePoint::GENERATOR * s; 295 | basic_round_trip_impl::<_, ProjectivePoint>(p, None); 296 | 297 | let hashed_msg = k256::Scalar::random(rand::thread_rng()); 298 | let ephemeral_scalar = k256::Scalar::random(rand::thread_rng()); 299 | let (signature, _) = s 300 | .try_sign_prehashed(ephemeral_scalar, &hashed_msg.to_bytes()) 301 | .unwrap(); 302 | p.to_affine() 303 | .verify_prehashed(&hashed_msg.to_bytes(), &signature) 304 | .unwrap(); 305 | basic_round_trip_impl::<_, Signature>(signature, None); 306 | 307 | let p_bytes = ProjectivePoint(p).to_bytes(); 308 | let p_decoded = ProjectivePoint::from_bytes(&p_bytes).unwrap(); 309 | assert_eq!(ProjectivePoint(p), p_decoded); 310 | } 311 | 312 | fn basic_round_trip_impl(val: T, size: Option) 313 | where 314 | U: From + Serialize + DeserializeOwned + PartialEq + Debug, 315 | { 316 | let bincode = bincode::DefaultOptions::new(); 317 | 318 | let v = U::from(val); 319 | let v_serialized = bincode.serialize(&v).unwrap(); 320 | if let Some(size) = size { 321 | assert_eq!(v_serialized.len(), size); 322 | } 323 | let v_deserialized = bincode.deserialize(&v_serialized).unwrap(); 324 | assert_eq!(v, v_deserialized); 325 | } 326 | 327 | #[test] 328 | fn scalar_deserialization_fail() { 329 | let s = Scalar(k256::Scalar::random(rand::thread_rng())); 330 | scalar_deserialization_fail_impl(s); 331 | } 332 | 333 | fn scalar_deserialization_fail_impl(scalar: S) 334 | where 335 | S: Serialize + DeserializeOwned + Debug, 336 | { 337 | let bincode = bincode::DefaultOptions::new(); 338 | 339 | // test too few bytes 340 | let mut too_few_bytes = bincode.serialize(&scalar).unwrap(); 341 | too_few_bytes.pop(); 342 | bincode.deserialize::(&too_few_bytes).unwrap_err(); 343 | 344 | // test too many bytes 345 | let mut too_many_bytes = bincode.serialize(&scalar).unwrap(); 346 | too_many_bytes.push(42); 347 | bincode.deserialize::(&too_many_bytes).unwrap_err(); 348 | 349 | let mut modulus: [u8; 32] = [ 350 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 351 | 0xff, 0xfe, 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, 0x5e, 0x8c, 352 | 0xd0, 0x36, 0x41, 0x41, 353 | ]; // secp256k1 modulus 354 | 355 | // test edge case: integer too large 356 | bincode.deserialize::(&modulus).unwrap_err(); 357 | 358 | // test edge case: integer not too large 359 | modulus[31] -= 1; 360 | bincode.deserialize::(&modulus).unwrap(); 361 | } 362 | } 363 | --------------------------------------------------------------------------------