From bbfe1e309fab37e9696571acd647df0c1a46ace6 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:03:27 +0530 Subject: [PATCH 1/6] pkc%refac(eddsa): implement `EddsaPkHash`, alias to `PlatformNodeId` --- maint/codeql/rust/lib/imports.qll | 7 +++ pkgs/pkc/corpus/eddsa_node_id.json5 | 16 +++++++ pkgs/pkc/src/eddsa/mod.rs | 11 +++++ pkgs/pkc/src/eddsa/public_hash.rs | 52 ++++++++++++++++++++++ pkgs/pkc/src/lib.rs | 2 + pkgs/primitives/src/payload/mod.rs | 22 ++++----- pkgs/primitives/src/payload/proregtx.rs | 9 +--- pkgs/primitives/src/payload/proupservtx.rs | 3 +- 8 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 pkgs/pkc/corpus/eddsa_node_id.json5 create mode 100644 pkgs/pkc/src/eddsa/mod.rs create mode 100644 pkgs/pkc/src/eddsa/public_hash.rs diff --git a/maint/codeql/rust/lib/imports.qll b/maint/codeql/rust/lib/imports.qll index 218c51d8..24490788 100644 --- a/maint/codeql/rust/lib/imports.qll +++ b/maint/codeql/rust/lib/imports.qll @@ -100,6 +100,13 @@ private predicate isAllowlistedReexport(Use u) { usePrefix(u) = "zeroize" ) or + fileOf(u).getAbsolutePath().matches("%pkgs/primitives/%") and + ( + // `dash-pkc` names its hashes after the curve, the alias is the public API. + usePrefix(u) = "dash_pkc" and + u.getUseTree().getPath().getSegment().getIdentifier().getText() = "__EddsaPkHash" + ) + or fileOf(u).getAbsolutePath().matches("%pkgs/script/%") and ( // Workaround for the orphan rule, not part of public API diff --git a/pkgs/pkc/corpus/eddsa_node_id.json5 b/pkgs/pkc/corpus/eddsa_node_id.json5 new file mode 100644 index 00000000..69ac6a03 --- /dev/null +++ b/pkgs/pkc/corpus/eddsa_node_id.json5 @@ -0,0 +1,16 @@ +{ + // The EDDSA hashed public key also serves as the Dash Platform Node ID, defined as + // `SHA256(pk)[0..20]`. There is an inconsistency in how the hashed result is displayed + // (see https://github.com/dashpay/rust-dashcore/issues/887) and this divergence needs + // to be pinned to avoid unexpected behavior. + "wire_order": [ + { + "wire": "8cb97997a418f4814a63d3564b9574a393437968", + "display": "68794393a374954b56d3634a81f418a49779b98c" + }, + { + "wire": "4cd2ca50b36e0a2bb1b6b29da140448b47eeb7a1", + "display": "a1b7ee478b4440a19db2b6b12b0a6eb350cad24c" + } + ] +} diff --git a/pkgs/pkc/src/eddsa/mod.rs b/pkgs/pkc/src/eddsa/mod.rs new file mode 100644 index 00000000..33bcfc39 --- /dev/null +++ b/pkgs/pkc/src/eddsa/mod.rs @@ -0,0 +1,11 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Ed25519 types for the edwards25519 curve. + +mod public_hash; + +pub use public_hash::EddsaPkHash; diff --git a/pkgs/pkc/src/eddsa/public_hash.rs b/pkgs/pkc/src/eddsa/public_hash.rs new file mode 100644 index 00000000..e3bc4771 --- /dev/null +++ b/pkgs/pkc/src/eddsa/public_hash.rs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Hashed representation of an Ed25519 public key. + +use dash_num::make_hash; + +make_hash! { + /// 20-byte public key hash. + EddsaPkHash, 20 +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::prelude::*; + + use dash_dev::{arr_from_hex, Corpus}; + use dash_types::codec::BaseCodec; + use hex_conservative::DisplayHex; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + struct OrderVector { + wire: String, + display: String, + } + + /// Wire order pinning to satisfy rust-dashcore#887 + #[rstest] + fn corpus_wire_order() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "eddsa_node_id"); + + for v in corpus.vectors::("wire_order") { + let wire = arr_from_hex::<20>(&v.wire); + let id = EddsaPkHash::decode(&mut wire.as_slice()).unwrap(); + + assert_eq!(id.to_string(), v.display); + assert_eq!(id.as_bytes().to_lower_hex_string(), v.wire); + + let mut buf = Vec::new(); + id.encode(&mut buf); + + assert_eq!(buf.as_slice().to_lower_hex_string(), v.wire); + } + } +} diff --git a/pkgs/pkc/src/lib.rs b/pkgs/pkc/src/lib.rs index eeae04c7..2ed48db5 100644 --- a/pkgs/pkc/src/lib.rs +++ b/pkgs/pkc/src/lib.rs @@ -19,6 +19,7 @@ mod prelude; pub mod bls; pub mod ecdsa; +pub mod eddsa; #[doc(hidden)] pub mod __deps { @@ -43,4 +44,5 @@ pub mod __deps { #[doc(hidden)] pub mod __private { pub use crate::ecdsa::EcdsaPkHash as __EcdsaPkHash; + pub use crate::eddsa::EddsaPkHash as __EddsaPkHash; } diff --git a/pkgs/primitives/src/payload/mod.rs b/pkgs/primitives/src/payload/mod.rs index e45830d9..0a968110 100644 --- a/pkgs/primitives/src/payload/mod.rs +++ b/pkgs/primitives/src/payload/mod.rs @@ -30,6 +30,18 @@ use dash_types::{enum_map, impl_num}; use core::fmt; +pub use self::assetlock::{AssetLock, AssetLockInvalid}; +pub use self::assetunlock::{AssetUnlock, AssetUnlockInvalid}; +pub use self::cbtx::{CbTxInvalid, CoinbaseCommitment}; +pub use self::mnhftx::{MnHardFork, MnHardForkInvalid, VERSIONBITS_NUM_BITS}; +pub use self::proregtx::ProRegTx; +pub use self::proupregtx::ProUpRegTx; +pub use self::prouprevtx::ProUpRevTx; +pub use self::proupservtx::ProUpServTx; +pub use self::quorum::{Commitment, CommitmentInvalid, FinalCommitment, QuorumVvecHash}; + +pub use dash_pkc::__private::__EddsaPkHash as PlatformNodeId; + /// Maximum operator reward in basis points. pub(crate) const MAX_OPERATOR_REWARD: u16 = 10_000; @@ -184,16 +196,6 @@ pub(crate) fn check_sptx_netinfo(ext: &NetInfoV2, version: u16, mn_type: MnType) None } -pub use assetlock::{AssetLock, AssetLockInvalid}; -pub use assetunlock::{AssetUnlock, AssetUnlockInvalid}; -pub use cbtx::{CbTxInvalid, CoinbaseCommitment}; -pub use mnhftx::{MnHardFork, MnHardForkInvalid, VERSIONBITS_NUM_BITS}; -pub use proregtx::{PlatformNodeId, ProRegTx}; -pub use proupregtx::ProUpRegTx; -pub use prouprevtx::ProUpRevTx; -pub use proupservtx::ProUpServTx; -pub use quorum::{Commitment, CommitmentInvalid, FinalCommitment, QuorumVvecHash}; - /// A decoded special transaction payload. /// /// Provides a unified dispatch over all Dash special transaction types. Unknown diff --git a/pkgs/primitives/src/payload/proregtx.rs b/pkgs/primitives/src/payload/proregtx.rs index a713db34..1c8b14ed 100644 --- a/pkgs/primitives/src/payload/proregtx.rs +++ b/pkgs/primitives/src/payload/proregtx.rs @@ -11,12 +11,12 @@ use super::{ PROTX_VERSION_EXT_ADDR, }; use crate::codec::impl_payload; +use crate::payload::PlatformNodeId; use crate::prelude::*; use crate::types::{NITrait, NetInfo, NetInfoV1, NetInfoV2, ServiceV1}; use crate::{hash_impl, TxHash}; use bitcoin_primitives::script::ScriptPubKeyBuf; -use dash_num::make_hash; use dash_pkc::bls::{BlsPkBytes, BlsScIetf}; use dash_script::{PubKeyHash, Recipient}; use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf}; @@ -265,13 +265,6 @@ impl fmt::Display for ProRegTx { } } -make_hash! { - /// Platform node identifier for Evo masternodes. - PlatformNodeId, 20 -} - -hash_impl!(PlatformNodeId); - #[cfg(all(test, feature = "serde"))] mod tests { use super::*; diff --git a/pkgs/primitives/src/payload/proupservtx.rs b/pkgs/primitives/src/payload/proupservtx.rs index 130cef18..2951abfb 100644 --- a/pkgs/primitives/src/payload/proupservtx.rs +++ b/pkgs/primitives/src/payload/proupservtx.rs @@ -6,9 +6,10 @@ //! ProUpServTx service-update payload (type 2). -use super::proregtx::{check_platform_fields, PlatformNodeId}; +use super::proregtx::check_platform_fields; use super::{check_sptx_netinfo, InputsHash, MnType, ProTxInvalid, PROTX_VERSION_BASIC_BLS, PROTX_VERSION_EXT_ADDR}; use crate::codec::impl_payload; +use crate::payload::PlatformNodeId; use crate::types::{NITrait, NetInfo, NetInfoV1, NetInfoV2, ServiceV1}; use crate::{hash_impl, TxHash}; From 6491c3fdbe8ce154823c299c235182224f71bdc3 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:09:43 +0530 Subject: [PATCH 2/6] pkc%lint(codeql): let an arm define operations it cannot carry --- maint/codeql/rust/lib/pkc.qll | 6 ++++++ maint/codeql/rust/pkc.model.yml | 4 ++++ maint/codeql/rust/pkc.ql | 7 ++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/maint/codeql/rust/lib/pkc.qll b/maint/codeql/rust/lib/pkc.qll index ba96bf1d..40129edb 100644 --- a/maint/codeql/rust/lib/pkc.qll +++ b/maint/codeql/rust/lib/pkc.qll @@ -12,6 +12,12 @@ */ extensible predicate armOnly(string arm, string role, string name); +/** + * Holds if `arm` cannot carry `name` at `role`, though the other arms offer + * it. Rows live in `pkc.model.yml`. + */ +extensible predicate armLacks(string arm, string role, string name); + /** * Holds if `arm` cannot carry `trait` at `role`, though the other arms do. * Rows live in `pkc.model.yml`. diff --git a/maint/codeql/rust/pkc.model.yml b/maint/codeql/rust/pkc.model.yml index 4c9342c0..65c95990 100644 --- a/maint/codeql/rust/pkc.model.yml +++ b/maint/codeql/rust/pkc.model.yml @@ -64,6 +64,10 @@ extensions: # Recoverable signatures - ["Ecdsa", "SecretKey", "sign_recoverable"] # Sign - ["Ecdsa", "PublicKey", "recover"] # Get public key from a signature + - addsTo: + pack: base-sdk/codeql-rust + extensible: armLacks + data: [] - addsTo: pack: base-sdk/codeql-rust extensible: armLacksTrait diff --git a/maint/codeql/rust/pkc.ql b/maint/codeql/rust/pkc.ql index fb91c227..499e1401 100644 --- a/maint/codeql/rust/pkc.ql +++ b/maint/codeql/rust/pkc.ql @@ -73,19 +73,24 @@ predicate shapeGap(TypeItem lacks, string role, string name, string arm) { lacking != arm and publicMethod(offers, name) and not publicMethod(lacks, name) and - not armOnly(arm, role, name) + not armOnly(arm, role, name) and + not armLacks(lacking, role, name) ) } /** * Holds if `lacks` is missing `trait`, which `arm` carries for the same role * inclusive of derives gated by `cfg_attr`. + * + * Double-underscore traits are skipped, considered private implementation + * concerns not part of the public API. */ predicate traitGap(TypeItem lacks, string role, string trait, string arm) { exists(TypeItem offers, string lacking | armRole(offers, arm, role) and armRole(lacks, lacking, role) and lacking != arm and + not trait.matches("\\_\\_%") and implementsPlainTrait(offers, trait) and not implementsPlainTrait(lacks, trait) and not hasDerive(lacks, trait) and From c53b2c46b66c79b09c281d754fc03ce00f8cbf1a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:29:11 +0530 Subject: [PATCH 3/6] pkc%feat(eddsa): implement `EddsaPkBytes` and `EddsaPublicKey` --- Cargo.lock | 134 +++++++++++++++++++++++-- maint/codeql/rust/lib/imports.qll | 2 + maint/codeql/rust/pkc.model.yml | 16 ++- pkgs/pkc/Cargo.toml | 8 +- pkgs/pkc/bench/eddsa.rs | 26 +++++ pkgs/pkc/bench/main.rs | 4 +- pkgs/pkc/src/bls/error.rs | 2 +- pkgs/pkc/src/ecdsa/error.rs | 2 +- pkgs/pkc/src/eddsa/error.rs | 28 ++++++ pkgs/pkc/src/eddsa/mod.rs | 17 ++++ pkgs/pkc/src/eddsa/public_bytes.rs | 38 +++++++ pkgs/pkc/src/eddsa/public_ops.rs | 156 +++++++++++++++++++++++++++++ pkgs/pkc/src/eddsa/tests.rs | 40 ++++++++ pkgs/pkc/src/lib.rs | 2 + 14 files changed, 462 insertions(+), 13 deletions(-) create mode 100644 pkgs/pkc/bench/eddsa.rs create mode 100644 pkgs/pkc/src/eddsa/error.rs create mode 100644 pkgs/pkc/src/eddsa/public_bytes.rs create mode 100644 pkgs/pkc/src/eddsa/public_ops.rs create mode 100644 pkgs/pkc/src/eddsa/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 9b39077e..90046ed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", "zeroize", ] @@ -161,6 +161,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "blst" version = "0.3.17" @@ -264,7 +273,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", ] @@ -339,6 +348,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -380,6 +398,42 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "dash-dev" version = "0.0.0" @@ -469,6 +523,7 @@ dependencies = [ "dash-num", "dash-types", "divan", + "ed25519-dalek", "ff", "getrandom", "group", @@ -477,7 +532,7 @@ dependencies = [ "rstest", "secp256k1", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -561,8 +616,18 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", ] [[package]] @@ -601,6 +666,29 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2 0.11.0", + "signature", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.17.0" @@ -639,6 +727,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -807,6 +901,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1453,8 +1556,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -1463,6 +1577,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" + [[package]] name = "slab" version = "0.4.12" diff --git a/maint/codeql/rust/lib/imports.qll b/maint/codeql/rust/lib/imports.qll index 24490788..97d008a4 100644 --- a/maint/codeql/rust/lib/imports.qll +++ b/maint/codeql/rust/lib/imports.qll @@ -87,6 +87,8 @@ private predicate isAllowlistedReexport(Use u) { or usePrefix(u) = "dash_types" or + usePrefix(u) = "ed25519_dalek" + or usePrefix(u) = "ff" or usePrefix(u) = "group" diff --git a/maint/codeql/rust/pkc.model.yml b/maint/codeql/rust/pkc.model.yml index 65c95990..e0195d83 100644 --- a/maint/codeql/rust/pkc.model.yml +++ b/maint/codeql/rust/pkc.model.yml @@ -67,8 +67,20 @@ extensions: - addsTo: pack: base-sdk/codeql-rust extensible: armLacks - data: [] + data: + # Ed25519 + # Misc + - ["Eddsa", "PublicKey", "verify"] # Check a signature + # Tweaking + - ["Eddsa", "PublicKey", "add_tweak"] # Curve properties prevent this, see SLIP-0010 + - ["Eddsa", "PublicKey", "mul_tweak"] # Curve properties prevent this, see SLIP-0010 - addsTo: pack: base-sdk/codeql-rust extensible: armLacksTrait - data: [] + data: + # Ed25519 + # Consensus wire format + - ["Eddsa", "PublicKey", "Decode"] # Not a wire type, see `Unencodable` + - ["Eddsa", "PublicKey", "Encode"] # Not a wire type, see `Unencodable` + - ["Eddsa", "PublicKey", "SerBound"] # Follows from `Decode` + - ["Eddsa", "PublicKey", "TypeId"] # Follows from `Decode` diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index 49d8096e..75e8b808 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -21,6 +21,11 @@ group = { version = "0.14", default-features = false, optional = true } cfg-if = "1" dash-num = { version = "0.1.0-beta", path = "../num", default-features = false } dash-types = { version = "0.1.0-beta", path = "../types", default-features = false } +ed25519-dalek = { version = "3", default-features = false, features = [ + "fast", + "signature", + "zeroize", +], optional = true } hex-conservative = { workspace = true, features = [ "alloc", ] } @@ -65,8 +70,9 @@ codec = [ "dash-types/codec", ] ecdsa = ["dep:rand_core", "dep:secp256k1"] +eddsa = ["dep:ed25519-dalek"] serde = ["codec", "dep:serde", "dash-num/serde", "dash-types/serde"] -full = ["bls", "codec", "ecdsa", "serde", "std", "tests"] +full = ["bls", "codec", "ecdsa", "eddsa", "serde", "std", "tests"] tests = ["std", "dep:rstest"] [lints] diff --git a/pkgs/pkc/bench/eddsa.rs b/pkgs/pkc/bench/eddsa.rs new file mode 100644 index 00000000..ba6bae07 --- /dev/null +++ b/pkgs/pkc/bench/eddsa.rs @@ -0,0 +1,26 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Benchmarks for the `eddsa` (ed25519) feature + +use dash_pkc::eddsa::tests::ALICE_PK; +use dash_pkc::eddsa::{EddsaPkBytes, EddsaPublicKey}; +use dash_types::Hashable; + +#[divan::bench] +fn deser_pk(bencher: divan::Bencher) { + bencher + .counter(divan::counter::ItemsCount::new(1u32)) + .bench(|| EddsaPublicKey::from_bytes(&ALICE_PK).unwrap()); +} + +#[divan::bench] +fn hash_pk(bencher: divan::Bencher) { + let bag = EddsaPkBytes::from(ALICE_PK); + bencher + .counter(divan::counter::ItemsCount::new(1u32)) + .bench(|| Hashable::hash(&bag)); +} diff --git a/pkgs/pkc/bench/main.rs b/pkgs/pkc/bench/main.rs index da06c8b9..2c4bdd96 100644 --- a/pkgs/pkc/bench/main.rs +++ b/pkgs/pkc/bench/main.rs @@ -5,7 +5,7 @@ // #![cfg_attr( - any(feature = "bls", feature = "ecdsa"), + any(feature = "bls", feature = "ecdsa", feature = "eddsa"), expect(clippy::unwrap_used, reason = "benchmarks rely on trusted test vectors") )] @@ -13,6 +13,8 @@ mod bls; #[cfg(feature = "ecdsa")] mod ecdsa; +#[cfg(feature = "eddsa")] +mod eddsa; fn main() { divan::main(); diff --git a/pkgs/pkc/src/bls/error.rs b/pkgs/pkc/src/bls/error.rs index 8d50159f..7a22277e 100644 --- a/pkgs/pkc/src/bls/error.rs +++ b/pkgs/pkc/src/bls/error.rs @@ -9,7 +9,7 @@ use core::fmt; /// Errors produced by BLS operations. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum BlsError { /// paired input counts do not match CountMismatch, diff --git a/pkgs/pkc/src/ecdsa/error.rs b/pkgs/pkc/src/ecdsa/error.rs index de543166..d516ee1a 100644 --- a/pkgs/pkc/src/ecdsa/error.rs +++ b/pkgs/pkc/src/ecdsa/error.rs @@ -9,7 +9,7 @@ use core::fmt; /// Errors produced by secp256k1 operations. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum EcdsaError { /// public key bytes are not a valid curve point InvalidPublicKey, diff --git a/pkgs/pkc/src/eddsa/error.rs b/pkgs/pkc/src/eddsa/error.rs new file mode 100644 index 00000000..8f3c3abb --- /dev/null +++ b/pkgs/pkc/src/eddsa/error.rs @@ -0,0 +1,28 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Errors types for Ed25519 operations. + +use core::fmt; + +/// Errors produced by Ed25519 operations. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum EddsaError { + /// public key bytes are not a usable curve point + InvalidPublicKey, +} + +impl fmt::Display for EddsaError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let text = match self { + EddsaError::InvalidPublicKey => "public key bytes are not a usable curve point", + }; + f.write_str(text) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for EddsaError {} diff --git a/pkgs/pkc/src/eddsa/mod.rs b/pkgs/pkc/src/eddsa/mod.rs index 33bcfc39..694e69f8 100644 --- a/pkgs/pkc/src/eddsa/mod.rs +++ b/pkgs/pkc/src/eddsa/mod.rs @@ -6,6 +6,23 @@ //! Ed25519 types for the edwards25519 curve. +mod error; +mod public_bytes; mod public_hash; +pub use error::EddsaError; +pub use public_bytes::{EddsaPkBytes, EDDSA_PK_LEN}; pub use public_hash::EddsaPkHash; + +cfg_if::cfg_if! { + if #[cfg(feature = "eddsa")] { + mod public_ops; + + #[cfg(any(test, feature = "tests"))] + #[expect(clippy::unwrap_used, reason = "test code")] + #[allow(dead_code, reason = "usage dependent on build flags")] + pub mod tests; + + pub use public_ops::EddsaPublicKey; + } +} diff --git a/pkgs/pkc/src/eddsa/public_bytes.rs b/pkgs/pkc/src/eddsa/public_bytes.rs new file mode 100644 index 00000000..01bddc5b --- /dev/null +++ b/pkgs/pkc/src/eddsa/public_bytes.rs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Ed25519 public key byte bag. + +use super::EddsaPkHash; + +use bitcoin_hashes::sha256::Hash as Sha256; +use dash_types::{make_bytes, type_cvrt, Hashable}; + +/// Raw Ed25519 public key length. +pub const EDDSA_PK_LEN: usize = 32; + +make_bytes! { + /// Ed25519 public key bytes (32 bytes, unvalidated). + EddsaPkBytes, EDDSA_PK_LEN, fwd, nocodec +} + +impl Hashable for EddsaPkBytes { + type Hash = EddsaPkHash; + + /// A single SHA-256 over the key, truncated to 20 bytes. + fn hash(&self) -> Self::Hash { + let digest = Sha256::hash(self.as_bytes()).to_byte_array(); + let mut id = [0u8; 20]; + for (out, byte) in id.iter_mut().zip(digest[..20].iter().rev()) { + *out = *byte; + } + EddsaPkHash::from(id) + } +} + +type_cvrt!(From<[u8; EDDSA_PK_LEN]> for EddsaPkBytes, |bytes| { + Self::from_bytes(*bytes) +}); diff --git a/pkgs/pkc/src/eddsa/public_ops.rs b/pkgs/pkc/src/eddsa/public_ops.rs new file mode 100644 index 00000000..4838b884 --- /dev/null +++ b/pkgs/pkc/src/eddsa/public_ops.rs @@ -0,0 +1,156 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Ed25519 public key. + +use super::error::EddsaError; +use super::public_bytes::{EddsaPkBytes, EDDSA_PK_LEN}; +use super::EddsaPkHash; + +use dash_types::type_cvrt; +#[cfg(feature = "codec")] +use dash_types::type_id::Unencodable; +use dash_types::Hashable; +use ed25519_dalek::VerifyingKey; + +use core::hash::{Hash, Hasher}; + +/// An Ed25519 public key. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "codec", derive(Unencodable))] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(into = "EddsaPkBytes", try_from = "EddsaPkBytes"))] +pub struct EddsaPublicKey(VerifyingKey); + +impl Hashable for EddsaPublicKey { + type Hash = EddsaPkHash; + + /// The 20-byte hash of the key, as `EddsaPkBytes` defines it. + fn hash(&self) -> EddsaPkHash { + Hashable::hash(&EddsaPkBytes::from_bytes(self.to_bytes())) + } +} + +impl EddsaPublicKey { + /// Parses a key from its 32-byte encoding. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the bytes are not a point on the curve, + /// or when the point has low order. A low-order key produces signatures + /// that verify under almost any message. + pub fn from_bytes(bytes: &[u8; EDDSA_PK_LEN]) -> Result { + let key = VerifyingKey::from_bytes(bytes).map_err(|_| EddsaError::InvalidPublicKey)?; + if key.is_weak() { + return Err(EddsaError::InvalidPublicKey); + } + Ok(Self(key)) + } + + /// Emits the 32-byte encoding. + pub fn to_bytes(&self) -> [u8; EDDSA_PK_LEN] { + self.0.to_bytes() + } +} + +impl Eq for EddsaPublicKey {} + +impl Hash for EddsaPublicKey { + fn hash(&self, state: &mut H) { + self.to_bytes().hash(state); + } +} + +impl PartialEq for EddsaPublicKey { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +type_cvrt!(From for EddsaPkBytes, |pk| { + Self::from_bytes(pk.to_bytes()) +}); + +type_cvrt!(TryFrom for EddsaPublicKey, EddsaError, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); + +type_cvrt!(From for VerifyingKey, |pk| { + pk.0 +}); + +type_cvrt!(TryFrom for EddsaPublicKey, EddsaError, |inner| { + if inner.is_weak() { + return Err(EddsaError::InvalidPublicKey); + } + Ok(Self(*inner)) +}); + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::eddsa::tests::*; + use crate::prelude::*; + + #[cfg(feature = "serde")] + use dash_dev::assert_json_rt; + use rstest::rstest; + + #[rstest] + fn pk_round_trip(alice_pk: EddsaPublicKey) { + assert_eq!(EddsaPublicKey::from_bytes(&ALICE_PK).unwrap(), alice_pk); + assert_eq!(alice_pk.to_bytes(), ALICE_PK); + assert_eq!( + EddsaPublicKey::try_from(EddsaPkBytes::from(alice_pk)).unwrap(), + alice_pk + ); + assert_eq!(Hashable::hash(&alice_pk).to_string(), ALICE_PK_HASH); + + #[cfg(feature = "serde")] + assert_json_rt(&alice_pk); + } + + /// A point off the curve is no key, and a low-order one produces signatures + /// that verify under almost any message. + #[rstest] + #[case::off_curve(OFF_CURVE_PK)] + #[case::identity(SMALL_ORDER_PKS[0])] + #[case::order_two(SMALL_ORDER_PKS[1])] + #[case::order_four(SMALL_ORDER_PKS[2])] + #[case::order_eight(SMALL_ORDER_PKS[3])] + #[case::non_canonical_identity(SMALL_ORDER_PKS[4])] + #[case::non_canonical_order_two(SMALL_ORDER_PKS[5])] + #[case::non_canonical_order_four(SMALL_ORDER_PKS[6])] + fn malformed_key_is_refused(#[case] bytes: [u8; EDDSA_PK_LEN]) { + assert!(EddsaPublicKey::from_bytes(&bytes).is_err()); + } + + #[rstest] + fn non_canonical_y_is_accepted() { + // The parse refuses low-order and undecompressable points, but dalek masks + // the high bit and reduces, so a y at or above the field prime still + // parses. + assert!(EddsaPublicKey::from_bytes(&[0xff; EDDSA_PK_LEN]).is_ok()); + } + + #[rstest] + fn backend_roundtrip(alice_pk: EddsaPublicKey) { + let inner = VerifyingKey::from(&alice_pk); + assert_eq!(inner.to_bytes(), alice_pk.to_bytes()); + assert_eq!(EddsaPublicKey::try_from(inner).unwrap(), alice_pk); + } + + #[rstest] + fn backend_conversion_refuses_weak_keys() { + for bytes in SMALL_ORDER_PKS { + let Ok(weak) = VerifyingKey::from_bytes(&bytes) else { + continue; + }; + assert_eq!(EddsaPublicKey::try_from(weak), Err(EddsaError::InvalidPublicKey)); + } + } +} diff --git a/pkgs/pkc/src/eddsa/tests.rs b/pkgs/pkc/src/eddsa/tests.rs new file mode 100644 index 00000000..6866f5d5 --- /dev/null +++ b/pkgs/pkc/src/eddsa/tests.rs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Common test definitions. + +use crate::eddsa::EddsaPublicKey; + +use hex_conservative::hex; +use rstest::fixture; + +/// The public half of the platform account key at DIP-9 `m/9'/5'/3'/4'` for +/// the "abandon ... about" mnemonic. +pub const ALICE_PK: [u8; 32] = hex!("c352476b459846a552263aef12d35ce05d03ae9c6cfa380747d3700cdbb5c75f"); + +/// The printed form of `ALICE_PK`'s hash. +pub const ALICE_PK_HASH: &str = "834b7cd3bba35f514f36704b5a553423c39a8df8"; + +/// A y-coordinate the curve equation has no solution for, so the point cannot +/// be decompressed. +pub const OFF_CURVE_PK: [u8; 32] = hex!("0200000000000000000000000000000000000000000000000000000000000000"); + +/// The low-order points in canonical encoding, the identity among them. A +/// signature under any of these verifies for almost every message. +pub const SMALL_ORDER_PKS: [[u8; 32]; 7] = [ + hex!("0000000000000000000000000000000000000000000000000000000000000000"), + hex!("0100000000000000000000000000000000000000000000000000000000000000"), + hex!("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05"), + hex!("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa"), + hex!("ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), + hex!("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), + hex!("eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), +]; + +#[fixture] +pub fn alice_pk() -> EddsaPublicKey { + EddsaPublicKey::from_bytes(&ALICE_PK).unwrap() +} diff --git a/pkgs/pkc/src/lib.rs b/pkgs/pkc/src/lib.rs index 2ed48db5..276ef66d 100644 --- a/pkgs/pkc/src/lib.rs +++ b/pkgs/pkc/src/lib.rs @@ -27,6 +27,8 @@ pub mod __deps { pub use blst; pub use dash_num; pub use dash_types; + #[cfg(feature = "eddsa")] + pub use ed25519_dalek; #[cfg(feature = "bls")] pub use ff; #[cfg(feature = "bls")] From c289a33d08e4acba9caea361c2b0229e6219138a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:30:50 +0530 Subject: [PATCH 4/6] pkc%feat(eddsa): implement `EddsaSkBytes` and `EddsaSecretKey` --- Cargo.lock | 6 + maint/codeql/rust/pkc.model.yml | 6 + pkgs/pkc/Cargo.toml | 3 +- pkgs/pkc/bench/eddsa.rs | 16 ++- pkgs/pkc/corpus/eddsa_node_id.json5 | 11 ++ pkgs/pkc/src/eddsa/mod.rs | 4 + pkgs/pkc/src/eddsa/public_ops.rs | 4 + pkgs/pkc/src/eddsa/secret_bytes.rs | 17 +++ pkgs/pkc/src/eddsa/secret_ops.rs | 179 ++++++++++++++++++++++++++++ pkgs/pkc/src/eddsa/tests.rs | 22 +++- pkgs/pkc/src/lib.rs | 2 +- 11 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 pkgs/pkc/src/eddsa/secret_bytes.rs create mode 100644 pkgs/pkc/src/eddsa/secret_ops.rs diff --git a/Cargo.lock b/Cargo.lock index 90046ed8..d03c25ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,6 +405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", + "rand_core", ] [[package]] @@ -418,6 +419,7 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto", + "rand_core", "rustc_version", "subtle", "zeroize", @@ -683,6 +685,7 @@ checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core", "sha2 0.11.0", "signature", "subtle", @@ -1582,6 +1585,9 @@ name = "signature" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "rand_core", +] [[package]] name = "slab" diff --git a/maint/codeql/rust/pkc.model.yml b/maint/codeql/rust/pkc.model.yml index e0195d83..5e7dc0f8 100644 --- a/maint/codeql/rust/pkc.model.yml +++ b/maint/codeql/rust/pkc.model.yml @@ -71,7 +71,9 @@ extensions: # Ed25519 # Misc - ["Eddsa", "PublicKey", "verify"] # Check a signature + - ["Eddsa", "SecretKey", "sign"] # Sign a message # Tweaking + - ["Eddsa", "SecretKey", "add_tweak"] # Curve properties prevent this, see SLIP-0010 - ["Eddsa", "PublicKey", "add_tweak"] # Curve properties prevent this, see SLIP-0010 - ["Eddsa", "PublicKey", "mul_tweak"] # Curve properties prevent this, see SLIP-0010 - addsTo: @@ -84,3 +86,7 @@ extensions: - ["Eddsa", "PublicKey", "Encode"] # Not a wire type, see `Unencodable` - ["Eddsa", "PublicKey", "SerBound"] # Follows from `Decode` - ["Eddsa", "PublicKey", "TypeId"] # Follows from `Decode` + - ["Eddsa", "SecretKey", "Decode"] # Not a wire type, see `Unencodable` + - ["Eddsa", "SecretKey", "Encode"] # Not a wire type, see `Unencodable` + - ["Eddsa", "SecretKey", "Hashable"] # Follows from `Decode` + - ["Eddsa", "SecretKey", "TypeId"] # Follows from `Decode` diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index 75e8b808..a128de4f 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -23,6 +23,7 @@ dash-num = { version = "0.1.0-beta", path = "../num", default-features = false } dash-types = { version = "0.1.0-beta", path = "../types", default-features = false } ed25519-dalek = { version = "3", default-features = false, features = [ "fast", + "rand_core", "signature", "zeroize", ], optional = true } @@ -70,7 +71,7 @@ codec = [ "dash-types/codec", ] ecdsa = ["dep:rand_core", "dep:secp256k1"] -eddsa = ["dep:ed25519-dalek"] +eddsa = ["dep:ed25519-dalek", "dep:rand_core"] serde = ["codec", "dep:serde", "dash-num/serde", "dash-types/serde"] full = ["bls", "codec", "ecdsa", "eddsa", "serde", "std", "tests"] tests = ["std", "dep:rstest"] diff --git a/pkgs/pkc/bench/eddsa.rs b/pkgs/pkc/bench/eddsa.rs index ba6bae07..7facca82 100644 --- a/pkgs/pkc/bench/eddsa.rs +++ b/pkgs/pkc/bench/eddsa.rs @@ -6,10 +6,22 @@ //! Benchmarks for the `eddsa` (ed25519) feature -use dash_pkc::eddsa::tests::ALICE_PK; -use dash_pkc::eddsa::{EddsaPkBytes, EddsaPublicKey}; +use dash_pkc::eddsa::tests::{ALICE_PK, ALICE_SK}; +use dash_pkc::eddsa::{EddsaPkBytes, EddsaPublicKey, EddsaSecretKey}; use dash_types::Hashable; +fn test_key() -> EddsaSecretKey { + EddsaSecretKey::from_bytes(&ALICE_SK) +} + +#[divan::bench] +fn derive_pk(bencher: divan::Bencher) { + let sk = test_key(); + bencher + .counter(divan::counter::ItemsCount::new(1u32)) + .bench(|| sk.public_key()); +} + #[divan::bench] fn deser_pk(bencher: divan::Bencher) { bencher diff --git a/pkgs/pkc/corpus/eddsa_node_id.json5 b/pkgs/pkc/corpus/eddsa_node_id.json5 index 69ac6a03..76f15115 100644 --- a/pkgs/pkc/corpus/eddsa_node_id.json5 +++ b/pkgs/pkc/corpus/eddsa_node_id.json5 @@ -1,4 +1,15 @@ { + // The platform node key at DIP-9 `m/9'/5'/3'/4'/0'` for the "abandon ... about" + // mnemonic, as derived by dashwallet-ios and DashSync it. The secret is the seed, + // not the signing scalar. + "derive_id": [ + { + "sk": "5fa238b12be77347abf9b5957bd902d16c6aaca28d25c4267ffacbd7458dceb1", + "pk": "3130c14339391cf26a68d86879e180ee9a16b660f5aa91f560f67c0abe8cf789", + "display": "302f2615e6955cce8ed3cff81e8011bfd3a2991f", + "wire": "1f99a2d3bf11801ef8cfd38ece5c95e615262f30" + } + ], // The EDDSA hashed public key also serves as the Dash Platform Node ID, defined as // `SHA256(pk)[0..20]`. There is an inconsistency in how the hashed result is displayed // (see https://github.com/dashpay/rust-dashcore/issues/887) and this divergence needs diff --git a/pkgs/pkc/src/eddsa/mod.rs b/pkgs/pkc/src/eddsa/mod.rs index 694e69f8..8ec4c78f 100644 --- a/pkgs/pkc/src/eddsa/mod.rs +++ b/pkgs/pkc/src/eddsa/mod.rs @@ -9,14 +9,17 @@ mod error; mod public_bytes; mod public_hash; +mod secret_bytes; pub use error::EddsaError; pub use public_bytes::{EddsaPkBytes, EDDSA_PK_LEN}; pub use public_hash::EddsaPkHash; +pub use secret_bytes::{EddsaSkBytes, EDDSA_SK_LEN}; cfg_if::cfg_if! { if #[cfg(feature = "eddsa")] { mod public_ops; + mod secret_ops; #[cfg(any(test, feature = "tests"))] #[expect(clippy::unwrap_used, reason = "test code")] @@ -24,5 +27,6 @@ cfg_if::cfg_if! { pub mod tests; pub use public_ops::EddsaPublicKey; + pub use secret_ops::EddsaSecretKey; } } diff --git a/pkgs/pkc/src/eddsa/public_ops.rs b/pkgs/pkc/src/eddsa/public_ops.rs index 4838b884..e735857e 100644 --- a/pkgs/pkc/src/eddsa/public_ops.rs +++ b/pkgs/pkc/src/eddsa/public_ops.rs @@ -35,6 +35,10 @@ impl Hashable for EddsaPublicKey { } impl EddsaPublicKey { + pub(super) fn from_inner(inner: VerifyingKey) -> Self { + Self(inner) + } + /// Parses a key from its 32-byte encoding. /// /// # Errors diff --git a/pkgs/pkc/src/eddsa/secret_bytes.rs b/pkgs/pkc/src/eddsa/secret_bytes.rs new file mode 100644 index 00000000..ef888bb3 --- /dev/null +++ b/pkgs/pkc/src/eddsa/secret_bytes.rs @@ -0,0 +1,17 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Ed25519 secret key byte bag. + +use dash_types::make_sbytes; + +/// Raw Ed25519 secret key (seed) length. +pub const EDDSA_SK_LEN: usize = 32; + +make_sbytes! { + /// Ed25519 secret key seed (32 bytes). + EddsaSkBytes, EDDSA_SK_LEN, nocodec +} diff --git a/pkgs/pkc/src/eddsa/secret_ops.rs b/pkgs/pkc/src/eddsa/secret_ops.rs new file mode 100644 index 00000000..59fab2f6 --- /dev/null +++ b/pkgs/pkc/src/eddsa/secret_ops.rs @@ -0,0 +1,179 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Ed25519 secret key. + +use super::public_ops::EddsaPublicKey; +use super::secret_bytes::{EddsaSkBytes, EDDSA_SK_LEN}; + +use dash_types::{qtypestr, type_cvrt}; +use ed25519_dalek::SigningKey; +use rand_core::CryptoRng; +use subtle::ConstantTimeEq; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +use core::fmt::{Debug, Formatter, Result as FmtResult}; + +/// An Ed25519 secret key (32-byte seed). +pub struct EddsaSecretKey(SigningKey); + +impl EddsaSecretKey { + /// Builds a key from a 32-byte seed. + pub fn from_bytes(seed: &[u8; EDDSA_SK_LEN]) -> Self { + Self(SigningKey::from_bytes(seed)) + } + + /// Generates a new random secret key. + pub fn generate(rng: &mut impl CryptoRng) -> Self { + Self(SigningKey::generate(rng)) + } + + /// Emits the seed used to derive public key. + pub fn to_bytes(&self) -> Zeroizing<[u8; EDDSA_SK_LEN]> { + Zeroizing::new(self.0.to_bytes()) + } + + /// Derives the corresponding public key. + pub fn public_key(&self) -> EddsaPublicKey { + EddsaPublicKey::from_inner(self.0.verifying_key()) + } + + /// Verify that a public key matches this secret key. + pub fn verify_pubkey(&self, pubkey: &EddsaPublicKey) -> bool { + self.public_key() == *pubkey + } +} + +impl Clone for EddsaSecretKey { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Debug for EddsaSecretKey { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, core::any::type_name::())?; + f.write_str("(..)") + } +} + +impl Eq for EddsaSecretKey {} + +impl PartialEq for EddsaSecretKey { + fn eq(&self, other: &Self) -> bool { + (*self.to_bytes()).ct_eq(&*other.to_bytes()).into() + } +} + +impl Zeroize for EddsaSecretKey { + /// Overwrites the seed the key expands from. + fn zeroize(&mut self) { + let mut seed = self.0.to_bytes(); + seed.zeroize(); + self.0 = SigningKey::from_bytes(&seed); + } +} + +impl ZeroizeOnDrop for EddsaSecretKey {} + +impl Drop for EddsaSecretKey { + fn drop(&mut self) { + self.zeroize(); + } +} + +type_cvrt!(From for EddsaSkBytes, |sk| { + Self::from_bytes(*sk.to_bytes()) +}); + +type_cvrt!(From for EddsaSecretKey, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); + +type_cvrt!(From for SigningKey, |sk| { + sk.0.clone() +}); + +type_cvrt!(From for EddsaSecretKey, |inner| { + Self(inner.clone()) +}); + +#[cfg(test)] +mod tests { + use super::*; + use crate::eddsa::tests::*; + use crate::prelude::*; + + use dash_dev::{arr_from_hex, Corpus}; + use dash_types::Hashable; + use getrandom::SysRng; + use hex_conservative::DisplayHex; + use rand_core::UnwrapErr; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + struct NodeIdVector { + sk: String, + pk: String, + display: String, + wire: String, + } + + #[rstest] + fn corpus_derive_id() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "eddsa_node_id"); + + for v in corpus.vectors::("derive_id") { + let sk = EddsaSecretKey::from_bytes(&arr_from_hex(&v.sk)); + let pk = sk.public_key(); + let id = Hashable::hash(&pk); + + assert_eq!(pk.to_bytes(), arr_from_hex::<32>(&v.pk)); + assert_eq!(id.to_string(), v.display); + assert_eq!(id.as_bytes().to_lower_hex_string(), v.wire); + } + } + + #[rstest] + fn reference_key_round_trips(alice_sk: EddsaSecretKey, alice_pk: EddsaPublicKey) { + assert_eq!(*alice_sk.to_bytes(), ALICE_SK); + assert_eq!(alice_sk.public_key(), alice_pk); + assert_eq!(alice_sk.public_key().to_bytes(), ALICE_PK); + assert_eq!(Hashable::hash(&alice_sk.public_key()).to_string(), ALICE_PK_HASH); + assert_eq!(EddsaSecretKey::from(EddsaSkBytes::from(alice_sk.clone())), alice_sk); + } + + #[rstest] + #[case::zeros([0u8; EDDSA_SK_LEN])] + #[case::ones([0xff; EDDSA_SK_LEN])] + #[case::reference(ALICE_SK)] + fn every_seed_is_usable(#[case] seed: [u8; EDDSA_SK_LEN]) { + // No range to reject, unlike a secp256k1 or BLS12-381 scalar. + assert_eq!(*EddsaSecretKey::from_bytes(&seed).to_bytes(), seed); + } + + #[rstest] + fn generate_yields_a_working_key() { + let sk = EddsaSecretKey::generate(&mut UnwrapErr(SysRng)); + + assert!(sk.verify_pubkey(&sk.public_key())); + assert_ne!(sk, EddsaSecretKey::generate(&mut UnwrapErr(SysRng))); + } + + #[rstest] + fn verify_pubkey_matches(alice_sk: EddsaSecretKey, bob_sk: EddsaSecretKey) { + assert!(alice_sk.verify_pubkey(&alice_sk.public_key())); + assert!(!alice_sk.verify_pubkey(&bob_sk.public_key())); + } + + #[rstest] + fn backend_roundtrip(alice_sk: EddsaSecretKey) { + let inner = SigningKey::from(&alice_sk); + assert_eq!(inner.to_bytes(), *alice_sk.to_bytes()); + assert_eq!(EddsaSecretKey::from(inner), alice_sk); + } +} diff --git a/pkgs/pkc/src/eddsa/tests.rs b/pkgs/pkc/src/eddsa/tests.rs index 6866f5d5..930cae54 100644 --- a/pkgs/pkc/src/eddsa/tests.rs +++ b/pkgs/pkc/src/eddsa/tests.rs @@ -6,18 +6,20 @@ //! Common test definitions. -use crate::eddsa::EddsaPublicKey; +use crate::eddsa::{EddsaPublicKey, EddsaSecretKey}; use hex_conservative::hex; use rstest::fixture; -/// The public half of the platform account key at DIP-9 `m/9'/5'/3'/4'` for -/// the "abandon ... about" mnemonic. +/// The platform account secret at DIP-9 `m/9'/5'/3'/4'` for the +/// "abandon ... about" mnemonic. +pub const ALICE_SK: [u8; 32] = hex!("80035d9c2f89971a9c9fad826bba8be9328f1686ae555e912949c2c32800c379"); pub const ALICE_PK: [u8; 32] = hex!("c352476b459846a552263aef12d35ce05d03ae9c6cfa380747d3700cdbb5c75f"); - -/// The printed form of `ALICE_PK`'s hash. pub const ALICE_PK_HASH: &str = "834b7cd3bba35f514f36704b5a553423c39a8df8"; +/// An unrelated secret, for tests that need two distinct keys. +pub const BOB_SK: [u8; 32] = hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + /// A y-coordinate the curve equation has no solution for, so the point cannot /// be decompressed. pub const OFF_CURVE_PK: [u8; 32] = hex!("0200000000000000000000000000000000000000000000000000000000000000"); @@ -34,7 +36,17 @@ pub const SMALL_ORDER_PKS: [[u8; 32]; 7] = [ hex!("eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), ]; +#[fixture] +pub fn alice_sk() -> EddsaSecretKey { + EddsaSecretKey::from_bytes(&ALICE_SK) +} + #[fixture] pub fn alice_pk() -> EddsaPublicKey { EddsaPublicKey::from_bytes(&ALICE_PK).unwrap() } + +#[fixture] +pub fn bob_sk() -> EddsaSecretKey { + EddsaSecretKey::from_bytes(&BOB_SK) +} diff --git a/pkgs/pkc/src/lib.rs b/pkgs/pkc/src/lib.rs index 276ef66d..3d4929fc 100644 --- a/pkgs/pkc/src/lib.rs +++ b/pkgs/pkc/src/lib.rs @@ -33,7 +33,7 @@ pub mod __deps { pub use ff; #[cfg(feature = "bls")] pub use group; - #[cfg(any(feature = "bls", feature = "ecdsa"))] + #[cfg(any(feature = "bls", feature = "ecdsa", feature = "eddsa"))] pub use rand_core; #[cfg(feature = "ecdsa")] pub use secp256k1; From 3010f54e3677216e91062bbc93f3df729d23e820 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:31:36 +0530 Subject: [PATCH 5/6] pkc%feat(eddsa): implement `EddsaSigBytes` and `EddsaSignature` --- maint/codeql/rust/pkc.model.yml | 8 ++- pkgs/pkc/bench/eddsa.rs | 20 +++++- pkgs/pkc/src/eddsa/error.rs | 3 + pkgs/pkc/src/eddsa/mod.rs | 4 ++ pkgs/pkc/src/eddsa/public_ops.rs | 13 ++++ pkgs/pkc/src/eddsa/secret_ops.rs | 18 ++++- pkgs/pkc/src/eddsa/sig_bytes.rs | 21 ++++++ pkgs/pkc/src/eddsa/sig_ops.rs | 110 +++++++++++++++++++++++++++++++ pkgs/pkc/src/eddsa/tests.rs | 10 ++- 9 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 pkgs/pkc/src/eddsa/sig_bytes.rs create mode 100644 pkgs/pkc/src/eddsa/sig_ops.rs diff --git a/maint/codeql/rust/pkc.model.yml b/maint/codeql/rust/pkc.model.yml index 5e7dc0f8..4d1452c9 100644 --- a/maint/codeql/rust/pkc.model.yml +++ b/maint/codeql/rust/pkc.model.yml @@ -69,9 +69,6 @@ extensions: extensible: armLacks data: # Ed25519 - # Misc - - ["Eddsa", "PublicKey", "verify"] # Check a signature - - ["Eddsa", "SecretKey", "sign"] # Sign a message # Tweaking - ["Eddsa", "SecretKey", "add_tweak"] # Curve properties prevent this, see SLIP-0010 - ["Eddsa", "PublicKey", "add_tweak"] # Curve properties prevent this, see SLIP-0010 @@ -90,3 +87,8 @@ extensions: - ["Eddsa", "SecretKey", "Encode"] # Not a wire type, see `Unencodable` - ["Eddsa", "SecretKey", "Hashable"] # Follows from `Decode` - ["Eddsa", "SecretKey", "TypeId"] # Follows from `Decode` + - ["Eddsa", "Signature", "Decode"] # Not a wire type, see `Unencodable` + - ["Eddsa", "Signature", "Encode"] # Not a wire type, see `Unencodable` + - ["Eddsa", "Signature", "Hashable"] # Follows from `Decode` + - ["Eddsa", "Signature", "SerBound"] # Follows from `Decode` + - ["Eddsa", "Signature", "TypeId"] # Follows from `Decode` diff --git a/pkgs/pkc/bench/eddsa.rs b/pkgs/pkc/bench/eddsa.rs index 7facca82..6557f7c1 100644 --- a/pkgs/pkc/bench/eddsa.rs +++ b/pkgs/pkc/bench/eddsa.rs @@ -6,7 +6,7 @@ //! Benchmarks for the `eddsa` (ed25519) feature -use dash_pkc::eddsa::tests::{ALICE_PK, ALICE_SK}; +use dash_pkc::eddsa::tests::{ALICE_PK, ALICE_SK, MSG}; use dash_pkc::eddsa::{EddsaPkBytes, EddsaPublicKey, EddsaSecretKey}; use dash_types::Hashable; @@ -14,6 +14,24 @@ fn test_key() -> EddsaSecretKey { EddsaSecretKey::from_bytes(&ALICE_SK) } +#[divan::bench] +fn sign(bencher: divan::Bencher) { + let sk = test_key(); + bencher + .counter(divan::counter::ItemsCount::new(1u32)) + .bench(|| sk.sign(MSG)); +} + +#[divan::bench] +fn verify(bencher: divan::Bencher) { + let sk = test_key(); + let sig = sk.sign(MSG); + let pk = sk.public_key(); + bencher + .counter(divan::counter::ItemsCount::new(1u32)) + .bench(|| pk.verify(MSG, &sig)); +} + #[divan::bench] fn derive_pk(bencher: divan::Bencher) { let sk = test_key(); diff --git a/pkgs/pkc/src/eddsa/error.rs b/pkgs/pkc/src/eddsa/error.rs index 8f3c3abb..2b340add 100644 --- a/pkgs/pkc/src/eddsa/error.rs +++ b/pkgs/pkc/src/eddsa/error.rs @@ -13,12 +13,15 @@ use core::fmt; pub enum EddsaError { /// public key bytes are not a usable curve point InvalidPublicKey, + /// signature verification failed + VerifyFailed, } impl fmt::Display for EddsaError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let text = match self { EddsaError::InvalidPublicKey => "public key bytes are not a usable curve point", + EddsaError::VerifyFailed => "signature verification failed", }; f.write_str(text) } diff --git a/pkgs/pkc/src/eddsa/mod.rs b/pkgs/pkc/src/eddsa/mod.rs index 8ec4c78f..95831011 100644 --- a/pkgs/pkc/src/eddsa/mod.rs +++ b/pkgs/pkc/src/eddsa/mod.rs @@ -10,16 +10,19 @@ mod error; mod public_bytes; mod public_hash; mod secret_bytes; +mod sig_bytes; pub use error::EddsaError; pub use public_bytes::{EddsaPkBytes, EDDSA_PK_LEN}; pub use public_hash::EddsaPkHash; pub use secret_bytes::{EddsaSkBytes, EDDSA_SK_LEN}; +pub use sig_bytes::{EddsaSigBytes, EDDSA_SIG_LEN}; cfg_if::cfg_if! { if #[cfg(feature = "eddsa")] { mod public_ops; mod secret_ops; + mod sig_ops; #[cfg(any(test, feature = "tests"))] #[expect(clippy::unwrap_used, reason = "test code")] @@ -28,5 +31,6 @@ cfg_if::cfg_if! { pub use public_ops::EddsaPublicKey; pub use secret_ops::EddsaSecretKey; + pub use sig_ops::EddsaSignature; } } diff --git a/pkgs/pkc/src/eddsa/public_ops.rs b/pkgs/pkc/src/eddsa/public_ops.rs index e735857e..ce758ed6 100644 --- a/pkgs/pkc/src/eddsa/public_ops.rs +++ b/pkgs/pkc/src/eddsa/public_ops.rs @@ -8,6 +8,7 @@ use super::error::EddsaError; use super::public_bytes::{EddsaPkBytes, EDDSA_PK_LEN}; +use super::sig_ops::EddsaSignature; use super::EddsaPkHash; use dash_types::type_cvrt; @@ -58,6 +59,18 @@ impl EddsaPublicKey { pub fn to_bytes(&self) -> [u8; EDDSA_PK_LEN] { self.0.to_bytes() } + + /// Verifies a signature over a message. + /// + /// # Errors + /// + /// Returns `VerifyFailed` when the signature does not match. + pub fn verify(&self, msg: &[u8], sig: &EddsaSignature) -> Result<(), EddsaError> { + self + .0 + .verify_strict(msg, sig.as_inner()) + .map_err(|_| EddsaError::VerifyFailed) + } } impl Eq for EddsaPublicKey {} diff --git a/pkgs/pkc/src/eddsa/secret_ops.rs b/pkgs/pkc/src/eddsa/secret_ops.rs index 59fab2f6..3a4215e3 100644 --- a/pkgs/pkc/src/eddsa/secret_ops.rs +++ b/pkgs/pkc/src/eddsa/secret_ops.rs @@ -8,9 +8,10 @@ use super::public_ops::EddsaPublicKey; use super::secret_bytes::{EddsaSkBytes, EDDSA_SK_LEN}; +use super::sig_ops::EddsaSignature; use dash_types::{qtypestr, type_cvrt}; -use ed25519_dalek::SigningKey; +use ed25519_dalek::{Signer, SigningKey}; use rand_core::CryptoRng; use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; @@ -45,6 +46,11 @@ impl EddsaSecretKey { pub fn verify_pubkey(&self, pubkey: &EddsaPublicKey) -> bool { self.public_key() == *pubkey } + + /// Signs a message. + pub fn sign(&self, msg: &[u8]) -> EddsaSignature { + EddsaSignature::from_inner(self.0.sign(msg)) + } } impl Clone for EddsaSecretKey { @@ -161,6 +167,7 @@ mod tests { let sk = EddsaSecretKey::generate(&mut UnwrapErr(SysRng)); assert!(sk.verify_pubkey(&sk.public_key())); + assert!(sk.public_key().verify(MSG, &sk.sign(MSG)).is_ok()); assert_ne!(sk, EddsaSecretKey::generate(&mut UnwrapErr(SysRng))); } @@ -170,6 +177,15 @@ mod tests { assert!(!alice_sk.verify_pubkey(&bob_sk.public_key())); } + #[rstest] + fn signature_verifies_under_its_own_key(alice_sk: EddsaSecretKey, bob_sk: EddsaSecretKey) { + let sig = alice_sk.sign(MSG); + + assert!(alice_sk.public_key().verify(MSG, &sig).is_ok()); + assert!(alice_sk.public_key().verify(b"another message", &sig).is_err()); + assert!(bob_sk.public_key().verify(MSG, &sig).is_err()); + } + #[rstest] fn backend_roundtrip(alice_sk: EddsaSecretKey) { let inner = SigningKey::from(&alice_sk); diff --git a/pkgs/pkc/src/eddsa/sig_bytes.rs b/pkgs/pkc/src/eddsa/sig_bytes.rs new file mode 100644 index 00000000..9db4911a --- /dev/null +++ b/pkgs/pkc/src/eddsa/sig_bytes.rs @@ -0,0 +1,21 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Ed25519 signature byte bag. + +use dash_types::{make_bytes, type_cvrt}; + +/// Raw Ed25519 signature length. +pub const EDDSA_SIG_LEN: usize = 64; + +make_bytes! { + /// Ed25519 signature bytes (64 bytes, unvalidated). + EddsaSigBytes, EDDSA_SIG_LEN, fwd, nocodec +} + +type_cvrt!(From<[u8; EDDSA_SIG_LEN]> for EddsaSigBytes, |bytes| { + Self::from_bytes(*bytes) +}); diff --git a/pkgs/pkc/src/eddsa/sig_ops.rs b/pkgs/pkc/src/eddsa/sig_ops.rs new file mode 100644 index 00000000..86b3e241 --- /dev/null +++ b/pkgs/pkc/src/eddsa/sig_ops.rs @@ -0,0 +1,110 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Ed25519 signature. + +use super::sig_bytes::{EddsaSigBytes, EDDSA_SIG_LEN}; + +use dash_types::type_cvrt; +#[cfg(feature = "codec")] +use dash_types::type_id::Unencodable; +use ed25519_dalek::Signature; + +use core::hash::{Hash, Hasher}; + +/// An Ed25519 signature (64 bytes). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "codec", derive(Unencodable))] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(into = "EddsaSigBytes", from = "EddsaSigBytes"))] +pub struct EddsaSignature(Signature); + +impl EddsaSignature { + pub(super) fn from_inner(inner: Signature) -> Self { + Self(inner) + } + + pub(super) fn as_inner(&self) -> &Signature { + &self.0 + } + + /// Wraps a 64-byte encoding. + pub fn from_bytes(bytes: &[u8; EDDSA_SIG_LEN]) -> Self { + Self(Signature::from_bytes(bytes)) + } + + /// Emits the 64-byte encoding. + pub fn to_bytes(&self) -> [u8; EDDSA_SIG_LEN] { + self.0.to_bytes() + } +} + +impl Hash for EddsaSignature { + fn hash(&self, state: &mut H) { + self.to_bytes().hash(state); + } +} + +type_cvrt!(From for EddsaSigBytes, |sig| { + Self::from_bytes(sig.to_bytes()) +}); + +type_cvrt!(From for EddsaSignature, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); + +type_cvrt!(From for Signature, |sig| { + sig.0 +}); + +type_cvrt!(From for EddsaSignature, |inner| { + Self(*inner) +}); + +#[cfg(test)] +mod tests { + use super::*; + use crate::eddsa::tests::*; + use crate::eddsa::EddsaPublicKey; + + #[cfg(feature = "serde")] + use dash_dev::assert_json_rt; + use rstest::rstest; + + #[rstest] + fn the_signature_round_trips(alice_sig: EddsaSignature) { + assert_eq!(EddsaSignature::from_bytes(&alice_sig.to_bytes()), alice_sig); + assert_eq!(EddsaSignature::from(EddsaSigBytes::from(alice_sig)), alice_sig); + + #[cfg(feature = "serde")] + assert_json_rt(&alice_sig); + } + + /// The parse takes any 64 bytes, so every refusal below happens at + /// verification: `non_canonical_scalar` sets the high bits of the scalar + /// half to push `s` past 2^253, `tampered_commitment` flips a bit of `R`. + #[rstest] + #[case::non_canonical_scalar(EDDSA_SIG_LEN - 1, 0xe0)] + #[case::tampered_commitment(0, 0x01)] + fn a_mutated_signature_is_refused( + alice_pk: EddsaPublicKey, + alice_sig: EddsaSignature, + #[case] index: usize, + #[case] mask: u8, + ) { + let mut bytes = alice_sig.to_bytes(); + bytes[index] ^= mask; + + assert!(alice_pk.verify(MSG, &EddsaSignature::from_bytes(&bytes)).is_err()); + } + + #[rstest] + fn backend_roundtrip(alice_sig: EddsaSignature) { + let inner = Signature::from(&alice_sig); + assert_eq!(inner.to_bytes(), alice_sig.to_bytes()); + assert_eq!(EddsaSignature::from(inner), alice_sig); + } +} diff --git a/pkgs/pkc/src/eddsa/tests.rs b/pkgs/pkc/src/eddsa/tests.rs index 930cae54..cb2ed10c 100644 --- a/pkgs/pkc/src/eddsa/tests.rs +++ b/pkgs/pkc/src/eddsa/tests.rs @@ -6,11 +6,14 @@ //! Common test definitions. -use crate::eddsa::{EddsaPublicKey, EddsaSecretKey}; +use crate::eddsa::{EddsaPublicKey, EddsaSecretKey, EddsaSignature}; use hex_conservative::hex; use rstest::fixture; +/// The message the signature fixtures cover. +pub const MSG: &[u8] = b"dash platform node"; + /// The platform account secret at DIP-9 `m/9'/5'/3'/4'` for the /// "abandon ... about" mnemonic. pub const ALICE_SK: [u8; 32] = hex!("80035d9c2f89971a9c9fad826bba8be9328f1686ae555e912949c2c32800c379"); @@ -50,3 +53,8 @@ pub fn alice_pk() -> EddsaPublicKey { pub fn bob_sk() -> EddsaSecretKey { EddsaSecretKey::from_bytes(&BOB_SK) } + +#[fixture] +pub fn alice_sig() -> EddsaSignature { + alice_sk().sign(MSG) +} From c04f167f2b6a2bf0c9e04065ec5d66d72998a22c Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:21:49 +0530 Subject: [PATCH 6/6] pkc%test(eddsa): pin `eddsa` to RFC 8032 and SLIP-0010 vectors --- pkgs/pkc/corpus/eddsa_keygen.json5 | 68 ++++++++++++++++++++++++++++++ pkgs/pkc/corpus/eddsa_sign.json5 | 41 ++++++++++++++++++ pkgs/pkc/src/eddsa/secret_ops.rs | 17 ++++++++ pkgs/pkc/src/eddsa/sig_ops.rs | 29 ++++++++++++- 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 pkgs/pkc/corpus/eddsa_keygen.json5 create mode 100644 pkgs/pkc/corpus/eddsa_sign.json5 diff --git a/pkgs/pkc/corpus/eddsa_keygen.json5 b/pkgs/pkc/corpus/eddsa_keygen.json5 new file mode 100644 index 00000000..d0e5c3c2 --- /dev/null +++ b/pkgs/pkc/corpus/eddsa_keygen.json5 @@ -0,0 +1,68 @@ +{ + // Sourced from SLIP-0010's ed25519 test vectors. + // + // https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-ed25519 + // https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-2-for-ed25519 + "derive_pk": [ + { + // vector 1, chain m + "sk": "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7", + "pk": "a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed" + }, + { + // vector 1, chain m/0' + "sk": "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3", + "pk": "8c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c" + }, + { + // vector 1, chain m/0'/1' + "sk": "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2", + "pk": "1932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187" + }, + { + // vector 1, chain m/0'/1'/2' + "sk": "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9", + "pk": "ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1" + }, + { + // vector 1, chain m/0'/1'/2'/2' + "sk": "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662", + "pk": "8abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c" + }, + { + // vector 1, chain m/0'/1'/2'/2'/1000000000' + "sk": "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793", + "pk": "3c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a" + }, + { + // vector 2, chain m + "sk": "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012", + "pk": "8fe9693f8fa62a4305a140b9764c5ee01e455963744fe18204b4fb948249308a" + }, + { + // vector 2, chain m/0' + "sk": "1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635", + "pk": "86fab68dcb57aa196c77c5f264f215a112c22a912c10d123b0d03c3c28ef1037" + }, + { + // vector 2, chain m/0'/2147483647' + "sk": "ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4", + "pk": "5ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d" + }, + { + // vector 2, chain m/0'/2147483647'/1' + "sk": "3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c", + "pk": "2e66aa57069c86cc18249aecf5cb5a9cebbfd6fadeab056254763874a9352b45" + }, + { + // vector 2, chain m/0'/2147483647'/1'/2147483646' + "sk": "5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72", + "pk": "e33c0f7d81d843c572275f287498e8d408654fdf0d1e065b84e2e6f157aab09b" + }, + { + // vector 2, chain m/0'/2147483647'/1'/2147483646'/2' + "sk": "551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d", + "pk": "47150c75db263559a70d5778bf36abbab30fb061ad69f69ece61a72b0cfa4fc0" + } + ] +} diff --git a/pkgs/pkc/corpus/eddsa_sign.json5 b/pkgs/pkc/corpus/eddsa_sign.json5 new file mode 100644 index 00000000..9c8178c9 --- /dev/null +++ b/pkgs/pkc/corpus/eddsa_sign.json5 @@ -0,0 +1,41 @@ +{ + // Ed25519 test vectors from RFC 8032 section 7.1. + // https://www.rfc-editor.org/info/rfc8032/#section-7.1 + "sign": [ + { + // RFC 8032 TEST 1, message 0 bytes + "sk": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "pk": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "msg": "", + "sig": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + }, + { + // RFC 8032 TEST 2, message 1 bytes + "sk": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + "pk": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", + "msg": "72", + "sig": "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00" + }, + { + // RFC 8032 TEST 3, message 2 bytes + "sk": "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7", + "pk": "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", + "msg": "af82", + "sig": "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a" + }, + { + // RFC 8032 TEST 1024, message 1023 bytes + "sk": "f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5", + "pk": "278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e", + "msg": "08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0", + "sig": "0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03" + }, + { + // RFC 8032 TEST SHA(abc), message 64 bytes + "sk": "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42", + "pk": "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf", + "msg": "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + "sig": "dc2a4459e7369633a52b1bf277839a00201009a3efbf3ecb69bea2186c26b58909351fc9ac90b3ecfdfbc7c66431e0303dca179c138ac17ad9bef1177331a704" + } + ] +} diff --git a/pkgs/pkc/src/eddsa/secret_ops.rs b/pkgs/pkc/src/eddsa/secret_ops.rs index 3a4215e3..266a04ea 100644 --- a/pkgs/pkc/src/eddsa/secret_ops.rs +++ b/pkgs/pkc/src/eddsa/secret_ops.rs @@ -144,6 +144,23 @@ mod tests { } } + #[derive(Deserialize)] + struct KeygenVector { + sk: String, + pk: String, + } + + #[rstest] + fn corpus_derive_pk() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "eddsa_keygen"); + + for v in corpus.vectors::("derive_pk") { + let sk = EddsaSecretKey::from_bytes(&arr_from_hex(&v.sk)); + + assert_eq!(sk.public_key().to_bytes(), arr_from_hex::<32>(&v.pk)); + } + } + #[rstest] fn reference_key_round_trips(alice_sk: EddsaSecretKey, alice_pk: EddsaPublicKey) { assert_eq!(*alice_sk.to_bytes(), ALICE_SK); diff --git a/pkgs/pkc/src/eddsa/sig_ops.rs b/pkgs/pkc/src/eddsa/sig_ops.rs index 86b3e241..d659e414 100644 --- a/pkgs/pkc/src/eddsa/sig_ops.rs +++ b/pkgs/pkc/src/eddsa/sig_ops.rs @@ -68,11 +68,38 @@ type_cvrt!(From for EddsaSignature, |inner| { mod tests { use super::*; use crate::eddsa::tests::*; - use crate::eddsa::EddsaPublicKey; + use crate::eddsa::{EddsaPublicKey, EddsaSecretKey}; + use crate::prelude::*; #[cfg(feature = "serde")] use dash_dev::assert_json_rt; + use dash_dev::{arr_from_hex, vec_from_hex, Corpus}; use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + struct SignVector { + sk: String, + pk: String, + msg: String, + sig: String, + } + + #[rstest] + fn corpus_sign() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "eddsa_sign"); + + for v in corpus.vectors::("sign") { + let sk = EddsaSecretKey::from_bytes(&arr_from_hex(&v.sk)); + let pk = sk.public_key(); + let msg = vec_from_hex(&v.msg); + let sig = sk.sign(&msg); + + assert_eq!(pk.to_bytes(), arr_from_hex::<32>(&v.pk)); + assert_eq!(sig.to_bytes(), arr_from_hex::(&v.sig)); + assert!(pk.verify(&msg, &sig).is_ok()); + } + } #[rstest] fn the_signature_round_trips(alice_sig: EddsaSignature) {