├── .gitignore ├── Cargo.toml ├── README.md └── src ├── dup.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hd-ed25519" 3 | version = "0.0.1" 4 | authors = ["Jeff Burdges "] 5 | edition = "2018" 6 | readme = "README.md" 7 | license = "BSD-3-Clause" 8 | repository = "https://github.com/w3f/hd-ed25519" 9 | categories = ["cryptography"] # "no-std" 10 | keywords = ["cryptography", "curve25519", "ECC"] 11 | description = "Hierachical key derivation on ed255190-dalek" 12 | 13 | 14 | [dependencies.ed25519-dalek] 15 | version = "1" 16 | default-features = false 17 | 18 | [dependencies.curve25519-dalek] 19 | version = "3" 20 | default-features = false 21 | 22 | [dependencies.rand] 23 | version = "0.7" 24 | default-features = false 25 | 26 | [dependencies.serde] 27 | version = "^1.0" 28 | optional = true 29 | 30 | [dependencies.clear_on_drop] 31 | version = "0.2" 32 | 33 | [dev-dependencies] 34 | sha2 = "^0.9" 35 | 36 | [features] 37 | default = ["dalek-rand", "std", "u64_backend"] 38 | dalek-rand = ["ed25519-dalek/rand"] 39 | # We don't add "rand/std" here because it would enable a bunch of Fuchsia dependencies. 40 | std = ["curve25519-dalek/std", "rand/std"] 41 | alloc = ["curve25519-dalek/alloc"] 42 | # nightly = ["curve25519-dalek/nightly", "rand/nightly", "clear_on_drop/nightly"] 43 | u64_backend = ["curve25519-dalek/u64_backend","ed25519-dalek/u64_backend"] 44 | u32_backend = ["curve25519-dalek/u32_backend","ed25519-dalek/u32_backend"] 45 | simd_backend = ["curve25519-dalek/simd_backend","ed25519-dalek/simd_backend"] 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Hierachical key derivation on ed255190-dalek 3 | 4 | Almost all hierachical key derivation schemes for ed25519 have 5 | vulnerabilities due to the "bit clamping" used in ed25519. 6 | Instead of hierachical key derivation on ed255190, we recommend 7 | using either a curve of cofactor 1 like secp256k1 or else 8 | a cofactor avoiding representation of Ed25519 like 9 | [ristretto](https://ristretto.group/) instead. That said.. 10 | 11 | [BIP32-Ed25519](https://cardanolaunch.com/assets/Ed25519_BIP.pdf) 12 | avoids the clamping by deriving new keys using only 224 bit scalars. 13 | There are straightforward full key recovery attack if one permits 14 | long key derivation paths along with either clamping by an Ed25519 15 | library or does addition mod another besides 8*l. Addition 16 | implementaitons are normally either mod l or mod 256. 17 | 18 | In this crate, we implement addition mod 8*l of numbers congruent 19 | to 0 mod 8 using the method indicated by Mike Hamburg in 20 | https://moderncrypto.org/mail-archive/curves/2017/000869.html 21 | 22 | We divide the secret scalars by the cofactor 8 as integers, 23 | add them mod l, and multiply them by 8 again as integers. 24 | We observe that this will not yield scalars whose high bit set to 1, 25 | and thus is not compatable with most Ed25519 libraries. 26 | It works with ed25519-dalek because the underlying implementation is 27 | constant-time and set the high bit when creating expanded secrety keys. 28 | 29 | An alternative appraopch is detailed in https://moderncrypto.org/mail-archive/curves/2017/000866.html and https://github.com/hdevalence/curve25519-dalek/commit/2ae0bdb6df26a74ef46d4332b635c9f6290126c7 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/dup.rs: -------------------------------------------------------------------------------- 1 | //! Access fields of the `ExpandedSecretKey` type. Ideally 2 | //! [ed25519-dalek](https://github.com/dalek-cryptography/ed25519-dalek) 3 | //! might expose these fields directly, but this might tempt missuse. 4 | //! 5 | //! All code here is copied form ed25519-dalek and therefore copyright 6 | //! Isis Agora Lovecruft. 7 | 8 | use clear_on_drop::clear::Clear; 9 | use curve25519_dalek::scalar::Scalar; 10 | // use curve25519_dalek::edwards::{CompressedEdwardsY,EdwardsPoint}, 11 | use ed25519_dalek::EXPANDED_SECRET_KEY_LENGTH; 12 | 13 | #[repr(C)] 14 | #[derive(Default)] // we derive Default in order to use the clear() method in Drop 15 | pub struct ExpandedSecretKey { 16 | pub key: Scalar, 17 | pub nonce: [u8; 32], 18 | } 19 | 20 | /// Overwrite secret key material with null bytes when it goes out of scope. 21 | impl Drop for ExpandedSecretKey { 22 | fn drop(&mut self) { 23 | self.key.clear(); 24 | self.nonce.clear(); 25 | } 26 | } 27 | 28 | impl ExpandedSecretKey { 29 | #[inline] 30 | pub fn to_bytes(&self) -> [u8; EXPANDED_SECRET_KEY_LENGTH] { 31 | let mut bytes: [u8; 64] = [0u8; 64]; 32 | 33 | bytes[..32].copy_from_slice(self.key.as_bytes()); 34 | bytes[32..].copy_from_slice(&self.nonce[..]); 35 | bytes 36 | } 37 | 38 | #[inline] 39 | pub fn from_bytes(bytes: &[u8; EXPANDED_SECRET_KEY_LENGTH]) -> ExpandedSecretKey { 40 | let mut lower: [u8; 32] = [0u8; 32]; 41 | let mut upper: [u8; 32] = [0u8; 32]; 42 | 43 | lower.copy_from_slice(&bytes[00..32]); 44 | upper.copy_from_slice(&bytes[32..64]); 45 | 46 | ExpandedSecretKey { 47 | key: Scalar::from_bits(lower), 48 | nonce: upper, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Hierarchical key derivation on curve25519 with cleaner structure than BIP32 2 | //! 3 | //! As in BIP32, we produce a chain code that can be incorporated into 4 | //! subsequence key derivations, and is similarly known by seret 5 | //! key holders. We improve on BIP32 in several respects though: 6 | //! 7 | //! We replace the 32 bit integer paramater in BIP32 with a free form 8 | //! byte array, optionally presented as a hash state. We expect this 9 | //! simpifies user code by avoiding any allocation requirements for 10 | //! integer paramaters. 11 | //! 12 | //! We do not conflate the "soft" derivation path that supports public 13 | //! key derivation with the "hard" derivation path that simply prodiuces 14 | //! some new private key. 15 | 16 | extern crate clear_on_drop; 17 | extern crate curve25519_dalek; 18 | extern crate ed25519_dalek; 19 | 20 | #[cfg(any(test))] 21 | extern crate sha2; 22 | 23 | use curve25519_dalek::digest::generic_array::typenum::U64; 24 | use curve25519_dalek::digest::Digest; 25 | // TODO use clear_on_drop::clear::Clear; 26 | 27 | use curve25519_dalek::constants; 28 | use curve25519_dalek::edwards::CompressedEdwardsY; // EdwardsPoint 29 | use curve25519_dalek::scalar::Scalar; 30 | use ed25519_dalek::{ExpandedSecretKey, PublicKey}; 31 | 32 | mod dup; 33 | 34 | /// Generate an `ExpandedSecretKey` key directly from another 35 | /// `ExpandedSecretKey` and extra secret data. 36 | /// 37 | /// Anyone who requires a 32 byte `SecretKey` should generate 38 | /// one manually using either `ed25519_dalek::SecretKey::generate` 39 | /// or else by applying `ed25519_dalek::SecretKey::from_bytes` to 40 | /// hash fucntion output with 256 bits of entropy. 41 | /// 42 | /// We recommend generating a `SecretKey` manually over using this 43 | /// function, as both methods provide analogs of the "hard" code 44 | /// path in BIP32 when i >= 2^31. In particular, all three require 45 | /// the source private key to prove any relationship between a source 46 | /// public key and any derivative keys produced by this function, or 47 | /// between two derivative keys. 48 | /// 49 | /// We deem the chain code from BIP32 supurfluous here because 50 | /// `ExpandedSecretKey::nonce` carries sufficent information. 51 | /// We replace the 32 bit integer paramater in BIP32 with an iterator 52 | /// of byte slices, which simpifies user code. 53 | /// We remove the silly scalar addion and nonce addition modulo 256 54 | /// from BIP32-Ed25519's hard code path because simply replacing 55 | /// the key and nonce suffice in this code path of BIP32. 56 | pub fn expanded_secret_key_prehashed(esk: ExpandedSecretKey, mut h: D) -> ExpandedSecretKey 57 | where 58 | D: Digest, 59 | { 60 | h.update(&esk.to_bytes() as &[u8]); 61 | let r = h.finalize(); 62 | 63 | let mut lower: [u8; 32] = [0u8; 32]; 64 | let mut upper: [u8; 32] = [0u8; 32]; 65 | 66 | lower.copy_from_slice(&r.as_slice()[00..32]); 67 | upper.copy_from_slice(&r.as_slice()[32..64]); 68 | 69 | lower[0] &= 248; 70 | lower[31] &= 63; 71 | lower[31] |= 64; 72 | 73 | let esk = dup::ExpandedSecretKey { 74 | // Ugly lack of fields hack 75 | key: Scalar::from_bits(lower), 76 | nonce: upper, 77 | }; 78 | ExpandedSecretKey::from_bytes(&esk.to_bytes()).unwrap() // Ugly lack of fields hack 79 | } 80 | 81 | // Length in bytes of the chain codes we produce 82 | // 83 | // In fact, only 16 bytes sounds safe, but this never appears on chain, 84 | // so no downsides to using 32 bytes. 85 | pub const CHAIN_CODE_LENGTH: usize = 32; 86 | 87 | /// Key pair using an `ExpandedSecretKey` instead of `SecretKey` to permit key derivation. 88 | #[repr(C)] 89 | pub struct ExpandedKeypair { 90 | /// The secret half of this keypair. 91 | pub secret: ExpandedSecretKey, 92 | /// The public half of this keypair. 93 | pub public: PublicKey, 94 | } 95 | 96 | impl ExpandedKeypair { 97 | pub fn sign_prehashed( 98 | &self, 99 | h: D, 100 | context: Option<&'static [u8]>, 101 | ) -> Result 102 | where 103 | D: Digest + Default, 104 | { 105 | self.secret.sign_prehashed::(h, &self.public, context) 106 | } 107 | } 108 | 109 | /* 110 | impl From for ExpandedKeypair { 111 | fn from(secret: ExpandedSecretKey) -> ExpandedKeypair { 112 | let public = ExpandedSecretKey::from_bytes(&esk).unwrap().into(); // hack for secret.clone().into(); 113 | ExpandedKeypair { secret, public } 114 | } 115 | } 116 | */ 117 | 118 | pub struct ExtendedKey { 119 | pub key: K, 120 | 121 | /// We cannot assume the original public key is secret and additional 122 | /// inputs might have low entropy, like `i` in BIP32. As in BIP32, 123 | /// chain code fill this gap by being a high entropy secret shared 124 | /// between public and private key holders. These are produced by 125 | /// key derivations and can be incorporated into subsequence key 126 | /// derivations. 127 | pub chaincode: [u8; CHAIN_CODE_LENGTH], 128 | } 129 | 130 | impl ExtendedKey { 131 | /// Derive an expanded secret key 132 | /// 133 | /// We derive a full 255 bit scalar mod 8*l with zero low bits 134 | 135 | /// We employ the 224 bit scalar trick from [BIP32-Ed25519](https://cardanolaunch.com/assets/Ed25519_BIP.pdf) 136 | /// so that addition with normal Ed25519 scalars 137 | /// 138 | /// We replace the 32 bit integer paramater in BIP32 with a free 139 | /// form byte array, which simpifies user code dramatically. 140 | /// Anyone who knows the source public key and the same byte array can 141 | /// derive the matching secret key, even if they lack the private keys. 142 | /// 143 | /// We produce a chain code that can be incorporated into subsequence 144 | /// key derivations' mask byte array, and is similarly known by public 145 | /// key holders. 146 | /// 147 | /// We continue deriving the nonce as in `unrecognisably_derive_secret_key` 148 | /// above bec ause the nonce must always remain secret. 149 | pub fn derive_secret_key_prehashed(&self, mut h: D) -> ExtendedKey 150 | where 151 | D: Digest + Clone, 152 | { 153 | let esk = self.key.secret.to_bytes(); // Ugly lack of fields hack 154 | 155 | h.update(self.key.public.as_bytes()); 156 | h.update(&self.chaincode); 157 | let mut h_secret = h.clone(); 158 | let r = h.finalize(); 159 | 160 | let mut chaincode = [0u8; CHAIN_CODE_LENGTH]; 161 | chaincode.copy_from_slice(&r.as_slice()[32..32 + CHAIN_CODE_LENGTH]); 162 | 163 | h_secret.update(&esk as &[u8]); // We compute the nonce from the private key instead of the public key. 164 | let r_secret = h_secret.finalize(); 165 | 166 | let mut esk = dup::ExpandedSecretKey::from_bytes(&esk); // Ugly lack of fields hack 167 | esk.nonce.copy_from_slice(&r_secret.as_slice()[32..64]); 168 | 169 | let mut lower = [0u8; 32]; 170 | lower.copy_from_slice(&r.as_slice()[0..32]); 171 | // lower[31] &= 0b00001111; 172 | lower[0] &= 248; 173 | lower[31] &= 64 + 63; 174 | divide_scalar_by_cofactor(&mut lower); 175 | let mut secret_scalar = esk.key.to_bytes(); 176 | divide_scalar_by_cofactor(&mut secret_scalar); 177 | let ss = Scalar::from_bits(lower) + Scalar::from_bits(secret_scalar); 178 | secret_scalar.copy_from_slice(&ss.to_bytes()); 179 | multiply_scalar_by_cofactor(&mut secret_scalar); 180 | esk.key = Scalar::from_bits(secret_scalar); 181 | 182 | ExtendedKey { 183 | key: ExpandedSecretKey::from_bytes(&esk.to_bytes()).unwrap(), // Ugly lack of fields hack 184 | chaincode, 185 | } 186 | } 187 | 188 | /// Derive an expanded key pair 189 | pub fn derive_keypair_prehashed(&self, h: D) -> ExtendedKey 190 | where 191 | D: Digest + Clone, 192 | { 193 | let ExtendedKey { key, chaincode } = self.derive_secret_key_prehashed(h); 194 | let esk = dup::ExpandedSecretKey::from_bytes(&key.to_bytes()); // Ugly lack of fields hack 195 | let pk = (&esk.key * &constants::ED25519_BASEPOINT_TABLE) 196 | .compress() 197 | .to_bytes(); 198 | ExtendedKey { 199 | key: ExpandedKeypair { 200 | secret: key, 201 | public: PublicKey::from_bytes(&pk).unwrap(), 202 | }, 203 | chaincode, 204 | } 205 | } 206 | } 207 | 208 | impl ExtendedKey { 209 | pub fn derive_secret_key(&self, h: D) -> ExtendedKey 210 | where 211 | D: Digest + Clone, 212 | { 213 | let esk = self.key.to_bytes(); 214 | let secret = ExpandedSecretKey::from_bytes(&esk).unwrap(); // hack: self.key.clone(); 215 | let public = (&ExpandedSecretKey::from_bytes(&esk).unwrap()).into(); // hack: secret.clone().into(); 216 | ExtendedKey { 217 | key: ExpandedKeypair { secret, public }, 218 | chaincode: self.chaincode, 219 | } 220 | .derive_secret_key_prehashed(h) 221 | } 222 | } 223 | 224 | impl ExtendedKey { 225 | /// Derivative public key 226 | /// 227 | /// Anyone who knows the source secret key, chain code, and byte array can 228 | /// can derive the matching secret key. 229 | /// 230 | /// We derive a 32 but integer 231 | /// 232 | /// We replace the 32 bit integer paramater in BIP32 with a free 233 | /// form byte array, which simpifies user code dramatically. 234 | /// 235 | /// We produce a chain code that can be incorporated into subsequence 236 | /// key derivations' mask byte array, and is similarly known by seret 237 | /// key holders. 238 | pub fn derive_public_key_prehashed(&self, mut h: D) -> ExtendedKey 239 | where 240 | D: Digest + Clone, 241 | { 242 | h.update(self.key.as_bytes()); 243 | h.update(&self.chaincode); 244 | let r = h.finalize(); 245 | 246 | let mut chaincode = [0u8; CHAIN_CODE_LENGTH]; 247 | chaincode.copy_from_slice(&r.as_slice()[32..32 + CHAIN_CODE_LENGTH]); 248 | 249 | let mut pk = CompressedEdwardsY::from_slice(self.key.as_bytes()) 250 | .decompress() 251 | .unwrap(); 252 | let mut lower = [0u8; 32]; 253 | lower.copy_from_slice(&r.as_slice()[0..32]); 254 | // lower[31] &= 0b00001111; 255 | lower[0] &= 248; 256 | lower[31] &= 64 + 63; 257 | pk += &Scalar::from_bits(lower) * &constants::ED25519_BASEPOINT_TABLE; 258 | 259 | ExtendedKey { 260 | key: PublicKey::from_bytes(pk.compress().as_bytes()).unwrap(), // Ugly lack of fields hack 261 | chaincode, 262 | } 263 | } 264 | } 265 | 266 | fn divide_scalar_by_cofactor(scalar: &mut [u8; 32]) { 267 | let mut low = 0u8; 268 | for i in scalar.iter_mut().rev() { 269 | let r = *i & 0b00000111; // save remainder 270 | *i >>= 3; // divide by 8 271 | *i += low; 272 | low = r << 5; 273 | } 274 | } 275 | 276 | fn multiply_scalar_by_cofactor(scalar: &mut [u8; 32]) { 277 | let mut high = 0u8; 278 | for i in scalar.iter_mut() { 279 | let r = *i & 0b11100000; // carry bits 280 | *i <<= 3; // multiply by 8 281 | *i += high; 282 | high = r >> 5; 283 | } 284 | } 285 | 286 | #[cfg(test)] 287 | mod tests { 288 | use super::*; 289 | use ed25519_dalek::SecretKey; 290 | use rand::{thread_rng, Rng}; 291 | use sha2::{Digest, Sha512}; 292 | 293 | #[test] 294 | fn cofactor_adjustment() { 295 | let mut x: [u8; 32] = thread_rng().gen(); 296 | x[31] &= 0b00011111; 297 | let mut y = x; 298 | multiply_scalar_by_cofactor(&mut y); 299 | divide_scalar_by_cofactor(&mut y); 300 | assert_eq!(x, y); 301 | 302 | let mut x: [u8; 32] = thread_rng().gen(); 303 | x[0] &= 0b11111000; 304 | let mut y = x; 305 | divide_scalar_by_cofactor(&mut y); 306 | multiply_scalar_by_cofactor(&mut y); 307 | assert_eq!(x, y); 308 | } 309 | 310 | #[test] 311 | fn public_vs_private_paths() { 312 | let mut rng = thread_rng(); 313 | let chaincode = [0u8; CHAIN_CODE_LENGTH]; 314 | let mut h: Sha512 = Sha512::default(); 315 | h.update(b"Just some test message!"); 316 | 317 | let secret_key = SecretKey::generate(&mut rng); 318 | let mut extended_expanded_keypair = ExtendedKey { 319 | key: ExpandedKeypair { 320 | secret: (&secret_key).into(), 321 | public: (&secret_key).into(), 322 | }, 323 | chaincode, 324 | }; 325 | let mut extended_public_key = ExtendedKey { 326 | key: (&secret_key).into(), 327 | chaincode, 328 | }; 329 | 330 | let context = Some(b"testing testing 1 2 3" as &[u8]); 331 | 332 | for i in 0..30 { 333 | let extended_expanded_keypair1 = 334 | extended_expanded_keypair.derive_keypair_prehashed(h.clone()); 335 | let extended_public_key1 = extended_public_key.derive_public_key_prehashed(h.clone()); 336 | assert_eq!( 337 | extended_expanded_keypair1.chaincode, extended_public_key1.chaincode, 338 | "Chain code derivation failed!" 339 | ); 340 | assert_eq!( 341 | extended_expanded_keypair1.key.public, extended_public_key1.key, 342 | "Public and secret key derivation missmatch!" 343 | ); 344 | extended_expanded_keypair = extended_expanded_keypair1; 345 | extended_public_key = extended_public_key1; 346 | h.update(b"Another"); 347 | 348 | if i % 5 == 0 { 349 | let good_sig = extended_expanded_keypair 350 | .key 351 | .sign_prehashed::(h.clone(), context) 352 | .unwrap(); 353 | let h_bad = h.clone().chain(b"oops"); 354 | let bad_sig = extended_expanded_keypair 355 | .key 356 | .sign_prehashed::(h_bad.clone(), context) 357 | .unwrap(); 358 | 359 | assert!( 360 | extended_public_key 361 | .key 362 | .verify_prehashed::(h.clone(), context, &good_sig) 363 | .is_ok(), 364 | "Verification of a valid signature failed!" 365 | ); 366 | assert!( 367 | extended_public_key 368 | .key 369 | .verify_prehashed::(h.clone(), context, &bad_sig) 370 | .is_err(), 371 | "Verification of a signature on a different message passed!" 372 | ); 373 | assert!( 374 | extended_public_key 375 | .key 376 | .verify_prehashed::(h_bad, context, &good_sig) 377 | .is_err(), 378 | "Verification of a signature on a different message passed!" 379 | ); 380 | } 381 | } 382 | } 383 | } 384 | --------------------------------------------------------------------------------