Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
570 changes: 185 additions & 385 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion maint/codeql/rust/lib/imports.qll
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private predicate isAllowlistedReexport(Use u) {
(
// Workaround for the orphan rule, not part of public API
usePrefix(u) = "dash_pkc" and
u.getUseTree().getPath().getSegment().getIdentifier().getText() = "__PubKeyHash"
u.getUseTree().getPath().getSegment().getIdentifier().getText() = "__EcdsaPkHash"
or
// Workaround for the orphan rule, not part of public API
usePrefix(u) = "dash_types" and
Expand Down
17 changes: 17 additions & 0 deletions maint/codeql/rust/lib/pkc.qll
Original file line number Diff line number Diff line change
@@ -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
*
* @description Rules for dash-pkc.
*/

/**
* Holds if `name` belongs to `arm` alone for `role`, exempting the other arms
* from offering it. Rows live in `pkc.model.yml`.
*
* The rows name what an arm lacks rather than what the arms share, so a
* method added to one arm and forgotten in another is reported with no list
* to maintain.
*/
extensible predicate armOnly(string arm, string role, string name);
66 changes: 66 additions & 0 deletions maint/codeql/rust/pkc.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# List of known asymmetries in the APIs exposed for curves offered by dash-pkc
extensions:
- addsTo:
pack: base-sdk/codeql-rust
extensible: armOnly
data:
# BLS
# Aggregation
- ["Bls", "SecretKey", "aggregate"] # Add secret keys
- ["Bls", "PublicKey", "aggregate"] # Add
- ["Bls", "PublicKey", "secure_aggregate"] # Add w/ binding against rogue keys
- ["Bls", "Signature", "aggregate"] # Add
- ["Bls", "Signature", "secure_aggregate"] # Add w/ binding against rogue keys
- ["Bls", "Signature", "sub_insecure"] # Subtract
- ["Bls", "Signature", "verify_aggregates"] # Verify multi-message, multiple signers
- ["Bls", "Signature", "fast_verify_aggregates"] # Verify single-message, multiple signers
- ["Bls", "Signature", "secure_verify_aggregates"] # Verify w/ binding against rogue keys
# Integrated Encryption Scheme
- ["Bls", "SecretKey", "dh_exchange"] # Derive shared secret
- ["Bls", "SecretKey", "ies_decrypt"] # Decrypt blob
- ["Bls", "SecretKey", "ies_decrypt_multi"] # Decrypt multi
- ["Bls", "PublicKey", "ies_encrypt"] # Encrypt blob
- ["Bls", "PublicKey", "ies_encrypt_multi"] # Encrypt multi
# Key derivation
- ["Bls", "SecretKey", "from_ikm"] # Run key material through a KDF
# Scheme transform
- ["Bls", "SecretKey", "to_scheme"]
- ["Bls", "PublicKey", "to_scheme"]
- ["Bls", "Signature", "to_scheme"]
- ["Bls", "SecretKey", "sign_with"] # Specify non-type-native scheme
- ["Bls", "PublicKey", "verify_with"] # Specify non-type-native scheme
# Proof of possession
- ["Bls", "SecretKey", "prove_possession"] # Prove
- ["Bls", "PublicKey", "verify_possession"] # Verify
# Threshold signatures
- ["Bls", "SecretKey", "split"] # Issue shares
- ["Bls", "SecretKey", "derive_share"] # Get share from master keys
- ["Bls", "PublicKey", "derive_share"] # Get share from master keys
- ["Bls", "Signature", "recover_shares"] # Get signature
- ["Bls", "PublicKey", "recover_shares"] # Get master public key
# ECDSA
# Compression
- ["Ecdsa", "SecretKey", "is_compressed"] # Form the derived key will take
- ["Ecdsa", "PublicKey", "is_compressed"] # Query
- ["Ecdsa", "PublicKey", "is_hybrid"] # Query legacy parity hint
- ["Ecdsa", "PublicKey", "decompress"] # Switch to uncompressed
- ["Ecdsa", "PublicKey", "to_compressed"] # Emit 33-byte SEC1
- ["Ecdsa", "PublicKey", "to_uncompressed"] # Emit 65-byte SEC1
- ["Ecdsa", "PublicKey", "to_hybrid"] # Emit 65-byte SEC1 w/ parity hint
- ["Ecdsa", "SkBytes", "is_compressed"] # Query
- ["Ecdsa", "PkBytes", "is_compressed"] # Query
- ["Ecdsa", "PkBytes", "size"] # Live length, since SEC1 is not fixed width
# Encoding
- ["Ecdsa", "Signature", "from_der"] # Read DER
- ["Ecdsa", "Signature", "to_der"] # Emit DER
- ["Ecdsa", "PkHash", "to_base58c"] # Emit Base58c address
- ["Ecdsa", "SkBytes", "from_wif"] # Read Base58c wallet import format
- ["Ecdsa", "SkBytes", "to_wif"] # Emit Base58c wallet import format
# Malleability
- ["Ecdsa", "Signature", "is_low_s"] # Query whether S is in the lower half
- ["Ecdsa", "Signature", "normalize_s"] # Move S into the lower half
# Misc.
- ["Ecdsa", "SecretKey", "negate"] # Flips the scalar
# Recoverable signatures
- ["Ecdsa", "SecretKey", "sign_recoverable"] # Sign
- ["Ecdsa", "PublicKey", "recover"] # Get public key from a signature
82 changes: 82 additions & 0 deletions maint/codeql/rust/pkc.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2026-present, The Dash Core developers
* SPDX-License-Identifier: MIT
* See the accompanying file LICENSE or https://opensource.org/license/MIT
*
* @id base-sdk/pkc-rules
* @name Rules for dash-pkc
* @description The arms must offer the same operations under the same names.
* @kind problem
* @precision high
* @problem.severity warning
* @tags maintainability
*/

import lib.filters
import lib.fmt
import lib.policy
import lib.pkc
import lib.traits
import rust

/**
* Holds if `t` is `arm`'s type for `role`, split off the name rather than the
* module so that every arm naming a role holds the same one. A role only one
* arm holds never pairs, and reports nothing.
*/
predicate armRole(TypeItem t, string arm, string role) {
isSourceType(t) and
isEnforcedCrate(fileOf(t)) and
exists(string name |
name = t.getName().getText() and
arm = name.regexpCapture("^(Bls|Ecdsa|Eddsa)([A-Z].*)$", 1) and
role = name.regexpCapture("^(Bls|Ecdsa|Eddsa)([A-Z].*)$", 2)
)
}

/** Holds if `f` is `pub`, rather than restricted to a scope. */
predicate isBarePub(Function f) {
exists(f.getVisibility()) and
not exists(f.getVisibility().getPath())
}

/**
* Holds if `t` offers `name` as a public inherent method.
*
* Matched wherever the impl sits, not through `inherentImpl`, which the
* declaration order rule needs to be file-local; an arm spreads a type's
* methods over several modules.
*
* Macro-written impls are skipped, since what a macro grants a type follows
* from which macro it expands rather than from the arm.
*/
predicate publicMethod(TypeItem t, string name) {
exists(Impl i, Function f |
not exists(MacroItems m | i = m.getItem(_)) and
implSelfName(i) = t.getName().getText() and
not exists(implTraitName(i)) and
isEnforcedCrate(fileOf(i)) and
f = i.getAssocItemList().getAnAssocItem() and
isBarePub(f) and
not isTestCode(f) and
name = f.getName().getText()
)
}

/**
* Holds if `lacks` is missing `name`, which `arm` offers for the same role.
*/
predicate shapeGap(TypeItem lacks, string role, string name, string arm) {
exists(TypeItem offers, string lacking |
armRole(offers, arm, role) and
armRole(lacks, lacking, role) and
lacking != arm and
publicMethod(offers, name) and
not publicMethod(lacks, name) and
not armOnly(arm, role, name)
)
}

from TypeItem t, string role, string name, string arm
where shapeGap(t, role, name, arm)
select t, fmt("{0} offers {1}, {2} does not", arm + role, fmt("{0}()", name), t.getName().getText())
2 changes: 2 additions & 0 deletions maint/codeql/rust/qlpack.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: base-sdk/codeql-rust
version: 0.0.0
dataExtensions:
- "**/*.model.yml"
dependencies:
codeql/rust-all: ~0.2.17
codeql/rust-queries: ~0.1.38
4 changes: 0 additions & 4 deletions maint/codeql/rust/zeroize.ql
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ predicate wipesSelf(TypeItem t) {
predicate externalWiper(TypeItem t) {
not isWorkspaceFile(fileOf(t)) and
(
// `k256::ecdsa::SigningKey` derives `ZeroizeOnDrop`.
t.getName().getText() = "SigningKey" and
fileOf(t).getAbsolutePath().matches("%/ecdsa-%/src/signing.rs")
or
// `blst::{min_pk,min_sig}::SecretKey` are declared `#[zeroize(drop)]`.
t.getName().getText() = "SecretKey" and
fileOf(t).getAbsolutePath().matches("%/blst-%/src/lib.rs")
Expand Down
9 changes: 4 additions & 5 deletions pkgs/pkc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ dash-types = { version = "0.1.0-beta", path = "../types", default-features = fal
hex-conservative = { workspace = true, features = [
"alloc",
] }
k256 = { version = "0.14", default-features = false, features = [
"arithmetic",
"ecdsa",
"sha256",
secp256k1 = { version = "0.33.1", default-features = false, features = [
"alloc",
"recovery",
], optional = true }
rand_core = { workspace = true, optional = true }
rstest = { version = "0.25", optional = true }
Expand Down Expand Up @@ -65,7 +64,7 @@ codec = [
"dash-num/codec",
"dash-types/codec",
]
ecdsa = ["codec", "dep:k256", "dep:rand_core"]
ecdsa = ["dep:rand_core", "dep:secp256k1"]
serde = ["codec", "dep:serde", "dash-num/serde", "dash-types/serde"]
full = ["bls", "codec", "ecdsa", "serde", "std", "tests"]
tests = ["std", "dep:rstest"]
Expand Down
8 changes: 4 additions & 4 deletions pkgs/pkc/bench/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ fn sign(bencher: divan::Bencher) {
let sk = test_key();
bencher.counter(divan::counter::ItemsCount::new(1u32)).bench(|| {
let msg = message_hash(42);
sk.sign(&msg).unwrap()
sk.sign(&msg)
});
}

#[divan::bench]
fn verify(bencher: divan::Bencher) {
let sk = test_key();
let msg = message_hash(99);
let sig = sk.sign(&msg).unwrap();
let sig = sk.sign(&msg);
let pk = sk.public_key();
bencher
.counter(divan::counter::ItemsCount::new(1u32))
Expand All @@ -38,14 +38,14 @@ fn sign_recoverable(bencher: divan::Bencher) {
let sk = test_key();
bencher
.counter(divan::counter::ItemsCount::new(1u32))
.bench(|| sk.sign_recoverable(&message_hash(7)).unwrap());
.bench(|| sk.sign_recoverable(&message_hash(7)));
}

#[divan::bench]
fn recover(bencher: divan::Bencher) {
let sk = test_key();
let msg = message_hash(55);
let sig = sk.sign_recoverable(&msg).unwrap();
let sig = sk.sign_recoverable(&msg);
bencher
.counter(divan::counter::ItemsCount::new(1u32))
.bench(|| EcdsaPublicKey::recover(&msg, &sig).unwrap());
Expand Down
34 changes: 34 additions & 0 deletions pkgs/pkc/src/ecdsa/curve_consts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Copyright (c) 2026-present, The Dash Core developers
// SPDX-License-Identifier: MIT
// See the accompanying file LICENSE or https://opensource.org/license/MIT
//

//! secp256k1 constants.

use super::secret_bytes::ECDSA_SK_LEN;

use hex_conservative::hex;

/// DER lengths of a private key with a compressed and an uncompressed public
/// key respectively.
pub(super) const DER_SIZES: &[usize] = &[214, 279];

/// ASN.1 object identifier for a prime-field curve.
pub(super) const OID_PRIME_FIELD: &[u8] = &hex!("2a8648ce3d0101");

/// The field prime.
pub(super) const PRIME: &[u8; ECDSA_SK_LEN] = &hex!("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f");

/// The group order.
pub(super) const ORDER: &[u8; ECDSA_SK_LEN] = &hex!("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");

/// The generator point in SEC1 uncompressed form.
pub(super) const GENERATOR: &[u8; 65] = &hex!(
"0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\
483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"
);

/// The generator point in SEC1 compressed form.
pub(super) const GENERATOR_COMPRESSED: [u8; 33] =
hex!("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798");
34 changes: 10 additions & 24 deletions pkgs/pkc/src/ecdsa/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +19,27 @@ pub enum EcdsaError {
InvalidSecretKey,
/// signature bytes are malformed
InvalidSignature,
/// tweak is not below the order, or the result is zero or infinity
InvalidTweak,
/// DER-encoded private key has invalid structure
MalformedDer,
/// recovery failed; no valid public key for this signature and message
RecoveryFailed,
/// signing operation failed
SigningFailed,
/// signature verification failed
VerifyFailed,
}

impl fmt::Display for EcdsaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidPublicKey => {
write!(f, "public key bytes are not a valid curve point")
}
Self::InvalidRecoveryId => {
write!(f, "recovery id out of range (must be 0..4)")
}
Self::InvalidSecretKey => {
write!(f, "secret key bytes are not a valid scalar")
}
Self::InvalidSignature => {
write!(f, "signature bytes are malformed")
}
Self::MalformedDer => {
write!(f, "DER-encoded private key has invalid structure")
}
Self::RecoveryFailed => {
write!(f, "recovery failed; no valid public key")
}
Self::SigningFailed => write!(f, "signing failed"),
Self::VerifyFailed => {
write!(f, "signature verification failed")
}
Self::InvalidPublicKey => write!(f, "public key bytes are not a valid curve point"),
Self::InvalidRecoveryId => write!(f, "recovery id out of range (must be 0..4)"),
Self::InvalidSecretKey => write!(f, "secret key bytes are not a valid scalar"),
Self::InvalidSignature => write!(f, "signature bytes are malformed"),
Self::InvalidTweak => write!(f, "tweak is not below the order, or the result is zero or infinity"),
Self::MalformedDer => write!(f, "DER-encoded private key has invalid structure"),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Self::RecoveryFailed => write!(f, "recovery failed; no valid public key"),
Self::VerifyFailed => write!(f, "signature verification failed"),
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions pkgs/pkc/src/ecdsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ mod secret_bytes;
mod sig_bytes;
mod sig_rec_bytes;

#[cfg(feature = "codec")]
use dash_types::type_id::Unencodable;

pub use error::EcdsaError;
pub use public_bytes::{EcdsaPkBytes, ECDSA_PK_LEN};
pub use public_hash::PubKeyHash;
pub use public_hash::EcdsaPkHash;
pub use secret_bytes::{EcdsaSkBytes, ECDSA_SK_LEN};
pub use sig_bytes::{EcdsaSigBytes, ECDSA_SIG_LEN};
pub use sig_rec_bytes::EcdsaRecSigBytes;

/// Whether a key's public counterpart serializes in compressed (33-byte) or
/// uncompressed (65-byte) SEC1 form.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Unencodable)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "codec", derive(Unencodable))]
pub enum Compression {
/// The public key serializes compressed.
Compressed,
Expand All @@ -51,6 +53,8 @@ impl From<bool> for Compression {

cfg_if::cfg_if! {
if #[cfg(feature = "ecdsa")] {
#[allow(dead_code, reason = "curve constants")]
mod curve_consts;
mod public_ops;
mod secret_ops;
mod sig_ops;
Expand Down
Loading