Elliptic Curve Multiset Hash (ECMH) for Rust and WebAssembly
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feature: ecmh crate

Nick Gerakines ac4e9209

+1179
+1
.gitignore
··· 1 + /target
+468
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "base16ct" 7 + version = "0.2.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 10 + 11 + [[package]] 12 + name = "base64ct" 13 + version = "1.8.3" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 16 + 17 + [[package]] 18 + name = "block-buffer" 19 + version = "0.10.4" 20 + source = "registry+https://github.com/rust-lang/crates.io-index" 21 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 22 + dependencies = [ 23 + "generic-array", 24 + ] 25 + 26 + [[package]] 27 + name = "cfg-if" 28 + version = "1.0.4" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 31 + 32 + [[package]] 33 + name = "const-oid" 34 + version = "0.9.6" 35 + source = "registry+https://github.com/rust-lang/crates.io-index" 36 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 37 + 38 + [[package]] 39 + name = "cpufeatures" 40 + version = "0.2.17" 41 + source = "registry+https://github.com/rust-lang/crates.io-index" 42 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 43 + dependencies = [ 44 + "libc", 45 + ] 46 + 47 + [[package]] 48 + name = "crypto-bigint" 49 + version = "0.5.5" 50 + source = "registry+https://github.com/rust-lang/crates.io-index" 51 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 52 + dependencies = [ 53 + "generic-array", 54 + "rand_core", 55 + "subtle", 56 + "zeroize", 57 + ] 58 + 59 + [[package]] 60 + name = "crypto-common" 61 + version = "0.1.6" 62 + source = "registry+https://github.com/rust-lang/crates.io-index" 63 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 64 + dependencies = [ 65 + "generic-array", 66 + "typenum", 67 + ] 68 + 69 + [[package]] 70 + name = "curve25519-dalek" 71 + version = "4.1.3" 72 + source = "registry+https://github.com/rust-lang/crates.io-index" 73 + checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 74 + dependencies = [ 75 + "cfg-if", 76 + "cpufeatures", 77 + "curve25519-dalek-derive", 78 + "digest", 79 + "fiat-crypto", 80 + "rustc_version", 81 + "subtle", 82 + "zeroize", 83 + ] 84 + 85 + [[package]] 86 + name = "curve25519-dalek-derive" 87 + version = "0.1.1" 88 + source = "registry+https://github.com/rust-lang/crates.io-index" 89 + checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 90 + dependencies = [ 91 + "proc-macro2", 92 + "quote", 93 + "syn", 94 + ] 95 + 96 + [[package]] 97 + name = "der" 98 + version = "0.7.10" 99 + source = "registry+https://github.com/rust-lang/crates.io-index" 100 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 101 + dependencies = [ 102 + "const-oid", 103 + "pem-rfc7468", 104 + "zeroize", 105 + ] 106 + 107 + [[package]] 108 + name = "digest" 109 + version = "0.10.7" 110 + source = "registry+https://github.com/rust-lang/crates.io-index" 111 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 112 + dependencies = [ 113 + "block-buffer", 114 + "const-oid", 115 + "crypto-common", 116 + "subtle", 117 + ] 118 + 119 + [[package]] 120 + name = "ecdsa" 121 + version = "0.16.9" 122 + source = "registry+https://github.com/rust-lang/crates.io-index" 123 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 124 + dependencies = [ 125 + "der", 126 + "digest", 127 + "elliptic-curve", 128 + "rfc6979", 129 + "signature", 130 + "spki", 131 + ] 132 + 133 + [[package]] 134 + name = "ecmh" 135 + version = "0.1.0" 136 + dependencies = [ 137 + "curve25519-dalek", 138 + "elliptic-curve", 139 + "hex", 140 + "k256", 141 + "p256", 142 + "serde", 143 + "sha2", 144 + ] 145 + 146 + [[package]] 147 + name = "elliptic-curve" 148 + version = "0.13.8" 149 + source = "registry+https://github.com/rust-lang/crates.io-index" 150 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 151 + dependencies = [ 152 + "base16ct", 153 + "crypto-bigint", 154 + "digest", 155 + "ff", 156 + "generic-array", 157 + "group", 158 + "pem-rfc7468", 159 + "pkcs8", 160 + "rand_core", 161 + "sec1", 162 + "subtle", 163 + "zeroize", 164 + ] 165 + 166 + [[package]] 167 + name = "ff" 168 + version = "0.13.1" 169 + source = "registry+https://github.com/rust-lang/crates.io-index" 170 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 171 + dependencies = [ 172 + "rand_core", 173 + "subtle", 174 + ] 175 + 176 + [[package]] 177 + name = "fiat-crypto" 178 + version = "0.2.9" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 181 + 182 + [[package]] 183 + name = "generic-array" 184 + version = "0.14.9" 185 + source = "registry+https://github.com/rust-lang/crates.io-index" 186 + checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" 187 + dependencies = [ 188 + "typenum", 189 + "version_check", 190 + "zeroize", 191 + ] 192 + 193 + [[package]] 194 + name = "getrandom" 195 + version = "0.2.17" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 198 + dependencies = [ 199 + "cfg-if", 200 + "libc", 201 + "wasi", 202 + ] 203 + 204 + [[package]] 205 + name = "group" 206 + version = "0.13.0" 207 + source = "registry+https://github.com/rust-lang/crates.io-index" 208 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 209 + dependencies = [ 210 + "ff", 211 + "rand_core", 212 + "subtle", 213 + ] 214 + 215 + [[package]] 216 + name = "hex" 217 + version = "0.4.3" 218 + source = "registry+https://github.com/rust-lang/crates.io-index" 219 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 220 + 221 + [[package]] 222 + name = "hmac" 223 + version = "0.12.1" 224 + source = "registry+https://github.com/rust-lang/crates.io-index" 225 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 226 + dependencies = [ 227 + "digest", 228 + ] 229 + 230 + [[package]] 231 + name = "k256" 232 + version = "0.13.4" 233 + source = "registry+https://github.com/rust-lang/crates.io-index" 234 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 235 + dependencies = [ 236 + "cfg-if", 237 + "ecdsa", 238 + "elliptic-curve", 239 + "once_cell", 240 + "sha2", 241 + "signature", 242 + ] 243 + 244 + [[package]] 245 + name = "libc" 246 + version = "0.2.184" 247 + source = "registry+https://github.com/rust-lang/crates.io-index" 248 + checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" 249 + 250 + [[package]] 251 + name = "once_cell" 252 + version = "1.21.4" 253 + source = "registry+https://github.com/rust-lang/crates.io-index" 254 + checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" 255 + 256 + [[package]] 257 + name = "p256" 258 + version = "0.13.2" 259 + source = "registry+https://github.com/rust-lang/crates.io-index" 260 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 261 + dependencies = [ 262 + "ecdsa", 263 + "elliptic-curve", 264 + "primeorder", 265 + "sha2", 266 + ] 267 + 268 + [[package]] 269 + name = "pem-rfc7468" 270 + version = "0.7.0" 271 + source = "registry+https://github.com/rust-lang/crates.io-index" 272 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 273 + dependencies = [ 274 + "base64ct", 275 + ] 276 + 277 + [[package]] 278 + name = "pkcs8" 279 + version = "0.10.2" 280 + source = "registry+https://github.com/rust-lang/crates.io-index" 281 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 282 + dependencies = [ 283 + "der", 284 + "spki", 285 + ] 286 + 287 + [[package]] 288 + name = "primeorder" 289 + version = "0.13.6" 290 + source = "registry+https://github.com/rust-lang/crates.io-index" 291 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 292 + dependencies = [ 293 + "elliptic-curve", 294 + ] 295 + 296 + [[package]] 297 + name = "proc-macro2" 298 + version = "1.0.106" 299 + source = "registry+https://github.com/rust-lang/crates.io-index" 300 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 301 + dependencies = [ 302 + "unicode-ident", 303 + ] 304 + 305 + [[package]] 306 + name = "quote" 307 + version = "1.0.45" 308 + source = "registry+https://github.com/rust-lang/crates.io-index" 309 + checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" 310 + dependencies = [ 311 + "proc-macro2", 312 + ] 313 + 314 + [[package]] 315 + name = "rand_core" 316 + version = "0.6.4" 317 + source = "registry+https://github.com/rust-lang/crates.io-index" 318 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 319 + dependencies = [ 320 + "getrandom", 321 + ] 322 + 323 + [[package]] 324 + name = "rfc6979" 325 + version = "0.4.0" 326 + source = "registry+https://github.com/rust-lang/crates.io-index" 327 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 328 + dependencies = [ 329 + "hmac", 330 + "subtle", 331 + ] 332 + 333 + [[package]] 334 + name = "rustc_version" 335 + version = "0.4.1" 336 + source = "registry+https://github.com/rust-lang/crates.io-index" 337 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 338 + dependencies = [ 339 + "semver", 340 + ] 341 + 342 + [[package]] 343 + name = "sec1" 344 + version = "0.7.3" 345 + source = "registry+https://github.com/rust-lang/crates.io-index" 346 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 347 + dependencies = [ 348 + "base16ct", 349 + "der", 350 + "generic-array", 351 + "pkcs8", 352 + "subtle", 353 + "zeroize", 354 + ] 355 + 356 + [[package]] 357 + name = "semver" 358 + version = "1.0.27" 359 + source = "registry+https://github.com/rust-lang/crates.io-index" 360 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 361 + 362 + [[package]] 363 + name = "serde" 364 + version = "1.0.228" 365 + source = "registry+https://github.com/rust-lang/crates.io-index" 366 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 367 + dependencies = [ 368 + "serde_core", 369 + "serde_derive", 370 + ] 371 + 372 + [[package]] 373 + name = "serde_core" 374 + version = "1.0.228" 375 + source = "registry+https://github.com/rust-lang/crates.io-index" 376 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 377 + dependencies = [ 378 + "serde_derive", 379 + ] 380 + 381 + [[package]] 382 + name = "serde_derive" 383 + version = "1.0.228" 384 + source = "registry+https://github.com/rust-lang/crates.io-index" 385 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 386 + dependencies = [ 387 + "proc-macro2", 388 + "quote", 389 + "syn", 390 + ] 391 + 392 + [[package]] 393 + name = "sha2" 394 + version = "0.10.9" 395 + source = "registry+https://github.com/rust-lang/crates.io-index" 396 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 397 + dependencies = [ 398 + "cfg-if", 399 + "cpufeatures", 400 + "digest", 401 + ] 402 + 403 + [[package]] 404 + name = "signature" 405 + version = "2.2.0" 406 + source = "registry+https://github.com/rust-lang/crates.io-index" 407 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 408 + dependencies = [ 409 + "digest", 410 + "rand_core", 411 + ] 412 + 413 + [[package]] 414 + name = "spki" 415 + version = "0.7.3" 416 + source = "registry+https://github.com/rust-lang/crates.io-index" 417 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 418 + dependencies = [ 419 + "base64ct", 420 + "der", 421 + ] 422 + 423 + [[package]] 424 + name = "subtle" 425 + version = "2.6.1" 426 + source = "registry+https://github.com/rust-lang/crates.io-index" 427 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 428 + 429 + [[package]] 430 + name = "syn" 431 + version = "2.0.117" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" 434 + dependencies = [ 435 + "proc-macro2", 436 + "quote", 437 + "unicode-ident", 438 + ] 439 + 440 + [[package]] 441 + name = "typenum" 442 + version = "1.19.0" 443 + source = "registry+https://github.com/rust-lang/crates.io-index" 444 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 445 + 446 + [[package]] 447 + name = "unicode-ident" 448 + version = "1.0.24" 449 + source = "registry+https://github.com/rust-lang/crates.io-index" 450 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 451 + 452 + [[package]] 453 + name = "version_check" 454 + version = "0.9.5" 455 + source = "registry+https://github.com/rust-lang/crates.io-index" 456 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 457 + 458 + [[package]] 459 + name = "wasi" 460 + version = "0.11.1+wasi-snapshot-preview1" 461 + source = "registry+https://github.com/rust-lang/crates.io-index" 462 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 463 + 464 + [[package]] 465 + name = "zeroize" 466 + version = "1.8.2" 467 + source = "registry+https://github.com/rust-lang/crates.io-index" 468 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+26
Cargo.toml
··· 1 + [package] 2 + name = "ecmh" 3 + version = "0.1.0" 4 + edition = "2024" 5 + description = "Elliptic Curve Multiset Hash - a homomorphic hash function for multisets" 6 + license = "MIT OR Apache-2.0" 7 + 8 + [dependencies] 9 + curve25519-dalek = { version = "4", features = ["digest"], optional = true } 10 + sha2 = "0.10" 11 + p256 = { version = "0.13", features = ["hash2curve", "arithmetic"], optional = true } 12 + k256 = { version = "0.13", features = ["hash2curve", "arithmetic"], optional = true } 13 + elliptic-curve = { version = "0.13", features = ["hash2curve", "sec1"], optional = true } 14 + serde = { version = "1", features = ["derive"], optional = true } 15 + 16 + [dependencies.hex] 17 + version = "0.4" 18 + 19 + [dev-dependencies] 20 + 21 + [features] 22 + default = ["ristretto"] 23 + ristretto = ["dep:curve25519-dalek"] 24 + p256 = ["dep:p256", "dep:elliptic-curve"] 25 + k256 = ["dep:k256", "dep:elliptic-curve"] 26 + serde = ["dep:serde"]
+109
README.md
··· 1 + # ecmh 2 + 3 + Elliptic Curve Multiset Hash (ECMH) for Rust — a homomorphic hash function for multisets based on [Maitin-Shepard et al. (2016)](https://arxiv.org/abs/1601.06502). 4 + 5 + ECMH maps each set element to an elliptic curve point and sums them. The hash of the union of two multisets is simply the sum of their individual hashes, enabling efficient incremental updates without rehashing the entire set. 6 + 7 + ## Features 8 + 9 + - **Incremental**: insert and remove elements without recomputing from scratch 10 + - **Order-independent**: `H({a, b}) == H({b, a})` 11 + - **Homomorphic**: `H(A ∪ B) == H(A) + H(B)` 12 + - **Multiple curve backends** behind feature flags: 13 + 14 + | Type alias | Curve | Output size | Feature | 15 + |---|---|---|---| 16 + | `RistrettoEcmh` | Ristretto255 | 32 bytes | `ristretto` (default) | 17 + | `P256Ecmh` | NIST P-256 | 33 bytes | `p256` | 18 + | `K256Ecmh` | secp256k1 | 33 bytes | `k256` | 19 + 20 + - **Operator overloading**: `+`, `-`, `+=`, `-=` for combining hashes 21 + - **Custom curves**: implement the `CurveBackend` trait 22 + 23 + ## Usage 24 + 25 + ```toml 26 + [dependencies] 27 + ecmh = "0.1" 28 + ``` 29 + 30 + For a specific curve backend: 31 + 32 + ```toml 33 + [dependencies] 34 + ecmh = { version = "0.1", default-features = false, features = ["p256"] } 35 + ``` 36 + 37 + ## Example 38 + 39 + ```rust 40 + use ecmh::RistrettoEcmh; 41 + 42 + // Build a multiset hash incrementally 43 + let mut hash = RistrettoEcmh::new(); 44 + hash.insert(b"alice"); 45 + hash.insert(b"bob"); 46 + 47 + // Order doesn't matter 48 + let mut hash2 = RistrettoEcmh::new(); 49 + hash2.insert(b"bob"); 50 + hash2.insert(b"alice"); 51 + assert_eq!(hash, hash2); 52 + 53 + // Combine disjoint sets 54 + let a = RistrettoEcmh::from_element(b"alice"); 55 + let b = RistrettoEcmh::from_element(b"bob"); 56 + assert_eq!(hash, a + b); 57 + 58 + // Remove an element 59 + let mut h = hash.clone(); 60 + h.remove(b"bob"); 61 + assert_eq!(h, RistrettoEcmh::from_element(b"alice")); 62 + 63 + // Build from an iterator 64 + let from_iter = RistrettoEcmh::from_elements([ 65 + b"alice".as_slice(), 66 + b"bob".as_slice(), 67 + ]); 68 + assert_eq!(from_iter, hash); 69 + 70 + // Serialize / deserialize 71 + let bytes = hash.to_bytes(); 72 + let recovered = RistrettoEcmh::from_bytes(&bytes).unwrap(); 73 + assert_eq!(recovered, hash); 74 + ``` 75 + 76 + ## Custom curve backend 77 + 78 + Implement the `CurveBackend` trait to use any elliptic curve: 79 + 80 + ```rust 81 + use ecmh::CurveBackend; 82 + 83 + #[derive(Clone, Default, Debug)] 84 + struct MyCurve; 85 + 86 + impl CurveBackend for MyCurve { 87 + type Point = /* your point type */; 88 + const POINT_SIZE: usize = /* compressed size */; 89 + 90 + fn identity() -> Self::Point { /* ... */ } 91 + fn hash_to_point(data: &[u8]) -> Self::Point { /* ... */ } 92 + fn add(a: &Self::Point, b: &Self::Point) -> Self::Point { /* ... */ } 93 + fn sub(a: &Self::Point, b: &Self::Point) -> Self::Point { /* ... */ } 94 + fn is_identity(p: &Self::Point) -> bool { /* ... */ } 95 + fn to_bytes(p: &Self::Point) -> Vec<u8> { /* ... */ } 96 + fn from_bytes(bytes: &[u8]) -> Option<Self::Point> { /* ... */ } 97 + } 98 + ``` 99 + 100 + ## References 101 + 102 + - [Elliptic Curve Multiset Hash](https://arxiv.org/abs/1601.06502) — Maitin-Shepard, Tibouchi, Aranha (2016) 103 + - [jbms/ecmh](https://github.com/jbms/ecmh) — C++ reference implementation 104 + - [cronokirby/multiset-hash](https://github.com/cronokirby/multiset-hash) — earlier Rust crate (Ristretto only, outdated deps) 105 + - [ECMH BIP proposal](https://github.com/tomasvdw/bips/blob/master/ecmh.mediawiki) — Bitcoin specification using secp256k1 106 + 107 + ## License 108 + 109 + MIT OR Apache-2.0
+36
src/backend.rs
··· 1 + use core::fmt::Debug; 2 + 3 + /// Trait abstracting the elliptic curve operations needed for ECMH. 4 + /// 5 + /// Implementors provide a specific curve with hash-to-curve, point arithmetic, 6 + /// and serialization. The crate ships with backends for Ristretto255, NIST P-256, 7 + /// and secp256k1 behind feature flags. 8 + pub trait CurveBackend: Clone + Default + Debug { 9 + /// The elliptic curve point type. 10 + type Point: Clone + Debug + PartialEq + Eq; 11 + 12 + /// Size in bytes of the compressed/serialized point. 13 + const POINT_SIZE: usize; 14 + 15 + /// Return the identity (neutral) element of the curve group. 16 + fn identity() -> Self::Point; 17 + 18 + /// Deterministically hash arbitrary data to a curve point. 19 + fn hash_to_point(data: &[u8]) -> Self::Point; 20 + 21 + /// Elliptic curve point addition. 22 + fn add(a: &Self::Point, b: &Self::Point) -> Self::Point; 23 + 24 + /// Elliptic curve point subtraction. 25 + fn sub(a: &Self::Point, b: &Self::Point) -> Self::Point; 26 + 27 + /// Check whether a point is the identity element. 28 + fn is_identity(p: &Self::Point) -> bool; 29 + 30 + /// Serialize a point to its compressed byte representation. 31 + fn to_bytes(p: &Self::Point) -> Vec<u8>; 32 + 33 + /// Deserialize a point from its compressed byte representation. 34 + /// Returns `None` if the bytes do not represent a valid point. 35 + fn from_bytes(bytes: &[u8]) -> Option<Self::Point>; 36 + }
+14
src/curves/mod.rs
··· 1 + #[cfg(feature = "ristretto")] 2 + mod ristretto; 3 + #[cfg(feature = "ristretto")] 4 + pub use ristretto::Ristretto255; 5 + 6 + #[cfg(feature = "p256")] 7 + mod nist_p256; 8 + #[cfg(feature = "p256")] 9 + pub use nist_p256::NistP256Backend; 10 + 11 + #[cfg(feature = "k256")] 12 + mod secp256k1; 13 + #[cfg(feature = "k256")] 14 + pub use self::secp256k1::Secp256k1Backend;
+59
src/curves/nist_p256.rs
··· 1 + use elliptic_curve::Group; 2 + use elliptic_curve::hash2curve::{ExpandMsgXmd, GroupDigest}; 3 + use elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; 4 + use p256::ProjectivePoint; 5 + use sha2::Sha256; 6 + 7 + use crate::backend::CurveBackend; 8 + 9 + const DST: &[u8] = b"ECMH_P256_XMD:SHA-256_SSWU_RO_"; 10 + 11 + /// ECMH backend using NIST P-256. 12 + /// 13 + /// Hash-to-curve follows RFC 9380 using the SSWU map with `ExpandMsgXmd<SHA-256>`. 14 + #[derive(Clone, Default, Debug)] 15 + pub struct NistP256Backend; 16 + 17 + impl CurveBackend for NistP256Backend { 18 + type Point = ProjectivePoint; 19 + const POINT_SIZE: usize = 33; 20 + 21 + fn identity() -> ProjectivePoint { 22 + ProjectivePoint::IDENTITY 23 + } 24 + 25 + fn hash_to_point(data: &[u8]) -> ProjectivePoint { 26 + p256::NistP256::hash_from_bytes::<ExpandMsgXmd<Sha256>>(&[data], &[DST]) 27 + .expect("hash_from_bytes should not fail with valid DST") 28 + } 29 + 30 + fn add(a: &ProjectivePoint, b: &ProjectivePoint) -> ProjectivePoint { 31 + a + b 32 + } 33 + 34 + fn sub(a: &ProjectivePoint, b: &ProjectivePoint) -> ProjectivePoint { 35 + a - b 36 + } 37 + 38 + fn is_identity(p: &ProjectivePoint) -> bool { 39 + bool::from(p.is_identity()) 40 + } 41 + 42 + fn to_bytes(p: &ProjectivePoint) -> Vec<u8> { 43 + let affine = p256::AffinePoint::from(*p); 44 + affine.to_encoded_point(true).as_bytes().to_vec() 45 + } 46 + 47 + fn from_bytes(bytes: &[u8]) -> Option<ProjectivePoint> { 48 + if bytes.len() != Self::POINT_SIZE { 49 + return None; 50 + } 51 + let encoded = p256::EncodedPoint::from_bytes(bytes).ok()?; 52 + let affine = p256::AffinePoint::from_encoded_point(&encoded); 53 + if affine.is_some().into() { 54 + Some(ProjectivePoint::from(affine.unwrap())) 55 + } else { 56 + None 57 + } 58 + } 59 + }
+50
src/curves/ristretto.rs
··· 1 + use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint}; 2 + use curve25519_dalek::traits::Identity; 3 + use sha2::{Digest, Sha512}; 4 + 5 + use crate::backend::CurveBackend; 6 + 7 + /// ECMH backend using Ristretto255 over Curve25519. 8 + /// 9 + /// Hash-to-curve uses SHA-512 to produce 64 uniform bytes, then applies the 10 + /// Ristretto-flavored Elligator 2 map via `RistrettoPoint::from_uniform_bytes`. 11 + #[derive(Clone, Default, Debug)] 12 + pub struct Ristretto255; 13 + 14 + impl CurveBackend for Ristretto255 { 15 + type Point = RistrettoPoint; 16 + const POINT_SIZE: usize = 32; 17 + 18 + fn identity() -> RistrettoPoint { 19 + RistrettoPoint::identity() 20 + } 21 + 22 + fn hash_to_point(data: &[u8]) -> RistrettoPoint { 23 + let hash: [u8; 64] = Sha512::digest(data).into(); 24 + RistrettoPoint::from_uniform_bytes(&hash) 25 + } 26 + 27 + fn add(a: &RistrettoPoint, b: &RistrettoPoint) -> RistrettoPoint { 28 + a + b 29 + } 30 + 31 + fn sub(a: &RistrettoPoint, b: &RistrettoPoint) -> RistrettoPoint { 32 + a - b 33 + } 34 + 35 + fn is_identity(p: &RistrettoPoint) -> bool { 36 + *p == RistrettoPoint::identity() 37 + } 38 + 39 + fn to_bytes(p: &RistrettoPoint) -> Vec<u8> { 40 + p.compress().to_bytes().to_vec() 41 + } 42 + 43 + fn from_bytes(bytes: &[u8]) -> Option<RistrettoPoint> { 44 + if bytes.len() != 32 { 45 + return None; 46 + } 47 + let compressed = CompressedRistretto::from_slice(bytes).ok()?; 48 + compressed.decompress() 49 + } 50 + }
+59
src/curves/secp256k1.rs
··· 1 + use elliptic_curve::Group; 2 + use elliptic_curve::hash2curve::{ExpandMsgXmd, GroupDigest}; 3 + use elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; 4 + use k256::ProjectivePoint; 5 + use sha2::Sha256; 6 + 7 + use crate::backend::CurveBackend; 8 + 9 + const DST: &[u8] = b"ECMH_K256_XMD:SHA-256_SSWU_RO_"; 10 + 11 + /// ECMH backend using secp256k1 (K-256). 12 + /// 13 + /// Hash-to-curve follows RFC 9380 using the SSWU map with `ExpandMsgXmd<SHA-256>`. 14 + #[derive(Clone, Default, Debug)] 15 + pub struct Secp256k1Backend; 16 + 17 + impl CurveBackend for Secp256k1Backend { 18 + type Point = ProjectivePoint; 19 + const POINT_SIZE: usize = 33; 20 + 21 + fn identity() -> ProjectivePoint { 22 + ProjectivePoint::IDENTITY 23 + } 24 + 25 + fn hash_to_point(data: &[u8]) -> ProjectivePoint { 26 + k256::Secp256k1::hash_from_bytes::<ExpandMsgXmd<Sha256>>(&[data], &[DST]) 27 + .expect("hash_from_bytes should not fail with valid DST") 28 + } 29 + 30 + fn add(a: &ProjectivePoint, b: &ProjectivePoint) -> ProjectivePoint { 31 + a + b 32 + } 33 + 34 + fn sub(a: &ProjectivePoint, b: &ProjectivePoint) -> ProjectivePoint { 35 + a - b 36 + } 37 + 38 + fn is_identity(p: &ProjectivePoint) -> bool { 39 + bool::from(p.is_identity()) 40 + } 41 + 42 + fn to_bytes(p: &ProjectivePoint) -> Vec<u8> { 43 + let affine = k256::AffinePoint::from(*p); 44 + affine.to_encoded_point(true).as_bytes().to_vec() 45 + } 46 + 47 + fn from_bytes(bytes: &[u8]) -> Option<ProjectivePoint> { 48 + if bytes.len() != Self::POINT_SIZE { 49 + return None; 50 + } 51 + let encoded = k256::EncodedPoint::from_bytes(bytes).ok()?; 52 + let affine = k256::AffinePoint::from_encoded_point(&encoded); 53 + if affine.is_some().into() { 54 + Some(ProjectivePoint::from(affine.unwrap())) 55 + } else { 56 + None 57 + } 58 + } 59 + }
+357
src/lib.rs
··· 1 + //! # ECMH - Elliptic Curve Multiset Hash 2 + //! 3 + //! A homomorphic (incremental) multiset hash function based on elliptic curves, 4 + //! as described in [Maitin-Shepard et al. (2016)](https://arxiv.org/abs/1601.06502). 5 + //! 6 + //! ECMH hashes each set element to an elliptic curve point, then sums all points. 7 + //! This construction is homomorphic: the hash of the union of two multisets equals 8 + //! the sum of their individual hashes. Elements can be incrementally inserted or 9 + //! removed without rehashing the entire set. 10 + //! 11 + //! # Curve backends 12 + //! 13 + //! The crate supports multiple elliptic curves via the [`CurveBackend`] trait: 14 + //! 15 + //! | Type alias | Curve | Feature flag | 16 + //! |------------|-------|-------------| 17 + //! | [`RistrettoEcmh`] | Ristretto255 | `ristretto` (default) | 18 + //! | [`P256Ecmh`] | NIST P-256 | `p256` | 19 + //! | [`K256Ecmh`] | secp256k1 | `k256` | 20 + //! 21 + //! # Example 22 + //! 23 + //! ```rust 24 + //! use ecmh::RistrettoEcmh; 25 + //! 26 + //! let mut hash = RistrettoEcmh::new(); 27 + //! hash.insert(b"alice"); 28 + //! hash.insert(b"bob"); 29 + //! 30 + //! // Order doesn't matter 31 + //! let mut hash2 = RistrettoEcmh::new(); 32 + //! hash2.insert(b"bob"); 33 + //! hash2.insert(b"alice"); 34 + //! assert_eq!(hash, hash2); 35 + //! 36 + //! // Incremental: union of disjoint sets 37 + //! let a = RistrettoEcmh::from_element(b"alice"); 38 + //! let b = RistrettoEcmh::from_element(b"bob"); 39 + //! assert_eq!(hash, a + b); 40 + //! ``` 41 + 42 + pub mod backend; 43 + pub mod curves; 44 + 45 + pub use backend::CurveBackend; 46 + #[cfg(feature = "p256")] 47 + pub use curves::NistP256Backend; 48 + #[cfg(feature = "ristretto")] 49 + pub use curves::Ristretto255; 50 + #[cfg(feature = "k256")] 51 + pub use curves::Secp256k1Backend; 52 + 53 + use core::fmt; 54 + use core::marker::PhantomData; 55 + use std::ops::{Add, AddAssign, Sub, SubAssign}; 56 + 57 + /// A homomorphic multiset hash value parameterized by a curve backend. 58 + /// 59 + /// The hash of a multiset is the sum of the hash-to-curve images of its elements. 60 + /// The identity point represents the empty set. 61 + #[derive(Clone)] 62 + pub struct EcmhHash<C: CurveBackend> { 63 + point: C::Point, 64 + _curve: PhantomData<C>, 65 + } 66 + 67 + impl<C: CurveBackend> EcmhHash<C> { 68 + /// Create a hash representing the empty multiset. 69 + pub fn new() -> Self { 70 + Self { 71 + point: C::identity(), 72 + _curve: PhantomData, 73 + } 74 + } 75 + 76 + /// Create a hash from a single element. 77 + pub fn from_element(data: &[u8]) -> Self { 78 + Self { 79 + point: C::hash_to_point(data), 80 + _curve: PhantomData, 81 + } 82 + } 83 + 84 + /// Build a hash from an iterator of elements. 85 + pub fn from_elements<'a, I>(iter: I) -> Self 86 + where 87 + I: IntoIterator<Item = &'a [u8]>, 88 + { 89 + let mut hash = Self::new(); 90 + for element in iter { 91 + hash.insert(element); 92 + } 93 + hash 94 + } 95 + 96 + /// Insert an element into the multiset. 97 + pub fn insert(&mut self, data: &[u8]) { 98 + let point = C::hash_to_point(data); 99 + self.point = C::add(&self.point, &point); 100 + } 101 + 102 + /// Remove an element from the multiset. 103 + /// 104 + /// If the element was not present, this effectively adds its inverse, 105 + /// which will produce a different hash than any valid multiset. The caller 106 + /// is responsible for tracking membership. 107 + pub fn remove(&mut self, data: &[u8]) { 108 + let point = C::hash_to_point(data); 109 + self.point = C::sub(&self.point, &point); 110 + } 111 + 112 + /// Compute the union of two multiset hashes. 113 + pub fn union(&self, other: &Self) -> Self { 114 + Self { 115 + point: C::add(&self.point, &other.point), 116 + _curve: PhantomData, 117 + } 118 + } 119 + 120 + /// Compute the difference of two multiset hashes. 121 + pub fn difference(&self, other: &Self) -> Self { 122 + Self { 123 + point: C::sub(&self.point, &other.point), 124 + _curve: PhantomData, 125 + } 126 + } 127 + 128 + /// Returns `true` if this hash represents the empty multiset. 129 + pub fn is_empty(&self) -> bool { 130 + C::is_identity(&self.point) 131 + } 132 + 133 + /// Serialize the hash to its compressed byte representation. 134 + pub fn to_bytes(&self) -> Vec<u8> { 135 + C::to_bytes(&self.point) 136 + } 137 + 138 + /// Deserialize a hash from its compressed byte representation. 139 + /// Returns `None` if the bytes do not represent a valid curve point. 140 + pub fn from_bytes(bytes: &[u8]) -> Option<Self> { 141 + C::from_bytes(bytes).map(|point| Self { 142 + point, 143 + _curve: PhantomData, 144 + }) 145 + } 146 + } 147 + 148 + impl<C: CurveBackend> Default for EcmhHash<C> { 149 + fn default() -> Self { 150 + Self::new() 151 + } 152 + } 153 + 154 + impl<C: CurveBackend> PartialEq for EcmhHash<C> { 155 + fn eq(&self, other: &Self) -> bool { 156 + self.point == other.point 157 + } 158 + } 159 + 160 + impl<C: CurveBackend> Eq for EcmhHash<C> {} 161 + 162 + impl<C: CurveBackend> fmt::Debug for EcmhHash<C> { 163 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 164 + f.debug_struct("EcmhHash") 165 + .field("bytes", &hex::encode(self.to_bytes())) 166 + .finish() 167 + } 168 + } 169 + 170 + impl<C: CurveBackend> fmt::Display for EcmhHash<C> { 171 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 172 + write!(f, "{}", hex::encode(self.to_bytes())) 173 + } 174 + } 175 + 176 + impl<C: CurveBackend> Add for EcmhHash<C> { 177 + type Output = Self; 178 + 179 + fn add(self, rhs: Self) -> Self { 180 + self.union(&rhs) 181 + } 182 + } 183 + 184 + impl<C: CurveBackend> Add<&EcmhHash<C>> for EcmhHash<C> { 185 + type Output = Self; 186 + 187 + fn add(self, rhs: &Self) -> Self { 188 + self.union(rhs) 189 + } 190 + } 191 + 192 + impl<C: CurveBackend> AddAssign for EcmhHash<C> { 193 + fn add_assign(&mut self, rhs: Self) { 194 + self.point = C::add(&self.point, &rhs.point); 195 + } 196 + } 197 + 198 + impl<C: CurveBackend> AddAssign<&EcmhHash<C>> for EcmhHash<C> { 199 + fn add_assign(&mut self, rhs: &Self) { 200 + self.point = C::add(&self.point, &rhs.point); 201 + } 202 + } 203 + 204 + impl<C: CurveBackend> Sub for EcmhHash<C> { 205 + type Output = Self; 206 + 207 + fn sub(self, rhs: Self) -> Self { 208 + self.difference(&rhs) 209 + } 210 + } 211 + 212 + impl<C: CurveBackend> Sub<&EcmhHash<C>> for EcmhHash<C> { 213 + type Output = Self; 214 + 215 + fn sub(self, rhs: &Self) -> Self { 216 + self.difference(rhs) 217 + } 218 + } 219 + 220 + impl<C: CurveBackend> SubAssign for EcmhHash<C> { 221 + fn sub_assign(&mut self, rhs: Self) { 222 + self.point = C::sub(&self.point, &rhs.point); 223 + } 224 + } 225 + 226 + impl<C: CurveBackend> SubAssign<&EcmhHash<C>> for EcmhHash<C> { 227 + fn sub_assign(&mut self, rhs: &Self) { 228 + self.point = C::sub(&self.point, &rhs.point); 229 + } 230 + } 231 + 232 + // Type aliases for convenience 233 + 234 + /// ECMH using Ristretto255 over Curve25519 (32-byte output). 235 + #[cfg(feature = "ristretto")] 236 + pub type RistrettoEcmh = EcmhHash<Ristretto255>; 237 + 238 + /// ECMH using NIST P-256 (33-byte SEC1 compressed output). 239 + #[cfg(feature = "p256")] 240 + pub type P256Ecmh = EcmhHash<NistP256Backend>; 241 + 242 + /// ECMH using secp256k1 (33-byte SEC1 compressed output). 243 + #[cfg(feature = "k256")] 244 + pub type K256Ecmh = EcmhHash<Secp256k1Backend>; 245 + 246 + #[cfg(test)] 247 + mod tests { 248 + use super::*; 249 + 250 + /// Run all ECMH property tests for a given backend. 251 + fn test_ecmh_properties<C: CurveBackend>() { 252 + // Empty set is identity 253 + let empty = EcmhHash::<C>::new(); 254 + assert!(empty.is_empty()); 255 + assert_eq!(empty, EcmhHash::<C>::default()); 256 + 257 + // Single element 258 + let a = EcmhHash::<C>::from_element(b"alice"); 259 + assert!(!a.is_empty()); 260 + 261 + // Determinism 262 + let a2 = EcmhHash::<C>::from_element(b"alice"); 263 + assert_eq!(a, a2); 264 + 265 + // Different elements produce different hashes 266 + let b = EcmhHash::<C>::from_element(b"bob"); 267 + assert_ne!(a, b); 268 + 269 + // Commutativity: {alice, bob} == {bob, alice} 270 + let mut ab = EcmhHash::<C>::new(); 271 + ab.insert(b"alice"); 272 + ab.insert(b"bob"); 273 + 274 + let mut ba = EcmhHash::<C>::new(); 275 + ba.insert(b"bob"); 276 + ba.insert(b"alice"); 277 + 278 + assert_eq!(ab, ba); 279 + 280 + // Incremental: H({a,b,c}) == H({a,b}) + H({c}) 281 + let mut abc = EcmhHash::<C>::new(); 282 + abc.insert(b"alice"); 283 + abc.insert(b"bob"); 284 + abc.insert(b"charlie"); 285 + 286 + let c = EcmhHash::<C>::from_element(b"charlie"); 287 + let ab_plus_c = ab.clone() + c; 288 + assert_eq!(abc, ab_plus_c); 289 + 290 + // Union via operator 291 + let a_hash = EcmhHash::<C>::from_element(b"alice"); 292 + let b_hash = EcmhHash::<C>::from_element(b"bob"); 293 + assert_eq!(ab, a_hash.clone() + b_hash.clone()); 294 + 295 + // Removal: H({a,b}) - H({b}) == H({a}) 296 + let ab_minus_b = ab.clone() - b_hash.clone(); 297 + assert_eq!(ab_minus_b, a_hash); 298 + 299 + // Remove all elements -> empty 300 + let mut hash = ab.clone(); 301 + hash.remove(b"alice"); 302 + hash.remove(b"bob"); 303 + assert!(hash.is_empty()); 304 + 305 + // AddAssign / SubAssign 306 + let mut hash = a_hash.clone(); 307 + hash += &b_hash; 308 + assert_eq!(hash, ab); 309 + hash -= &b_hash; 310 + assert_eq!(hash, a_hash); 311 + 312 + // from_elements matches sequential inserts 313 + let from_iter = EcmhHash::<C>::from_elements([ 314 + b"alice".as_slice(), 315 + b"bob".as_slice(), 316 + b"charlie".as_slice(), 317 + ]); 318 + assert_eq!(from_iter, abc); 319 + 320 + // Serialization round-trip 321 + let bytes = ab.to_bytes(); 322 + assert_eq!(bytes.len(), C::POINT_SIZE); 323 + let deserialized = 324 + EcmhHash::<C>::from_bytes(&bytes).expect("valid bytes should deserialize"); 325 + assert_eq!(deserialized, ab); 326 + 327 + // Invalid bytes 328 + let bad_bytes = vec![0xff; C::POINT_SIZE]; 329 + // This may or may not be valid depending on the curve, so we just ensure it doesn't panic 330 + let _ = EcmhHash::<C>::from_bytes(&bad_bytes); 331 + 332 + // Wrong length always fails 333 + assert!(EcmhHash::<C>::from_bytes(&[0u8; 1]).is_none()); 334 + 335 + // Display / Debug don't panic 336 + let _ = format!("{}", ab); 337 + let _ = format!("{:?}", ab); 338 + } 339 + 340 + #[cfg(feature = "ristretto")] 341 + #[test] 342 + fn test_ristretto() { 343 + test_ecmh_properties::<Ristretto255>(); 344 + } 345 + 346 + #[cfg(feature = "p256")] 347 + #[test] 348 + fn test_p256() { 349 + test_ecmh_properties::<NistP256Backend>(); 350 + } 351 + 352 + #[cfg(feature = "k256")] 353 + #[test] 354 + fn test_k256() { 355 + test_ecmh_properties::<Secp256k1Backend>(); 356 + } 357 + }