Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use dpp::document::{Document, DocumentV0};
use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
use dpp::identity::signer::Signer;
use dpp::identity::{IdentityPublicKey, KeyType, SecurityLevel};
use dpp::identity::{Identity, IdentityPublicKey, KeyType, SecurityLevel};
use dpp::platform_value::Value;
use dpp::prelude::Identifier;

Expand All @@ -30,6 +30,26 @@ use crate::wallet::identity::crypto::contact_info::{
ContactInfoPrivateData,
};

// contactInfo documents require HIGH or CRITICAL authentication; MASTER is
// reserved for identity operations and cannot authorize an ordinary document
// write. Key selection follows the profile writer: full or HASH160 ECDSA keys,
// skipping keys bound elsewhere, disabled or expired; `Err` when eligible keys
// exist but `signer` can reach none of them.
fn contact_info_signing_key<'a>(
identity: &'a Identity,
signer: &impl Signer<IdentityPublicKey>,
dashpay_contract_id: Identifier,
) -> Result<Option<&'a IdentityPublicKey>, dash_sdk::Error> {
super::usable_authentication_key(
identity,
signer,
dashpay_contract_id,
"contactInfo",
&[SecurityLevel::HIGH, SecurityLevel::CRITICAL],
&[KeyType::ECDSA_SECP256K1, KeyType::ECDSA_HASH160],
)
}

/// One decrypted `contactInfo` document — the fields the sweep applies. (The
/// publish path's update-vs-create needs `doc_id`/`revision`/`derivation_index`
/// too; it reads those from [`RawContactInfoDoc`] before decrypting.)
Expand Down Expand Up @@ -597,22 +617,15 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
);
return Ok(ContactInfoPublishOutcome::SkippedWatchOnly);
};
let signing_key = super::usable_authentication_key(
&identity,
signer,
dashpay_contract.id(),
"contactInfo",
&[SecurityLevel::HIGH, SecurityLevel::CRITICAL],
&[KeyType::ECDSA_SECP256K1],
)?
.cloned()
.ok_or_else(|| {
PlatformWalletError::InvalidIdentityData(
"No HIGH or CRITICAL authentication key found on identity \
let signing_key = contact_info_signing_key(&identity, signer, dashpay_contract.id())?
.cloned()
.ok_or_else(|| {
PlatformWalletError::InvalidIdentityData(
"No HIGH or CRITICAL authentication key found on identity \
(required for document state transitions)"
.to_string(),
)
})?;
.to_string(),
)
})?;
let root_key_id = root_key_id.ok_or_else(|| {
PlatformWalletError::InvalidIdentityData(
"Identity has no ECDSA_SECP256K1 encryption key (required for contactInfo)"
Expand Down Expand Up @@ -795,3 +808,70 @@ mod pending_enqueue_tests {
assert_eq!(backend.writes.load(std::sync::atomic::Ordering::SeqCst), 1);
}
}

#[cfg(test)]
mod signing_key_tests {
use super::super::signing_key::tests::KeyFilter;
use super::*;
use dpp::identity::accessors::IdentityGettersV0;
use dpp::identity::identity_public_key::v0::IdentityPublicKeyV0;
use dpp::identity::identity_public_key::Purpose;
use dpp::version::PlatformVersion;

const DASHPAY: [u8; 32] = [0xDA; 32];

fn auth_key(id: u32, key_type: KeyType, security_level: SecurityLevel) -> IdentityPublicKey {
IdentityPublicKeyV0 {
id,
purpose: Purpose::AUTHENTICATION,
security_level,
key_type,
data: vec![1; key_type.default_size()].into(),
..Default::default()
}
.into()
}

fn identity_with(keys: Vec<IdentityPublicKey>) -> Identity {
let mut identity = Identity::default_versioned(PlatformVersion::latest()).unwrap();
identity.add_public_keys(keys);
identity
}

fn pick(identity: &Identity) -> Option<&IdentityPublicKey> {
contact_info_signing_key(identity, &KeyFilter(|_| true), Identifier::from(DASHPAY)).unwrap()
}

#[test]
fn should_select_high_and_critical_ecdsa_contact_info_keys() {
for key_type in [KeyType::ECDSA_SECP256K1, KeyType::ECDSA_HASH160] {
for level in [SecurityLevel::HIGH, SecurityLevel::CRITICAL] {
let identity = identity_with(vec![auth_key(1, key_type, level)]);
assert!(pick(&identity).is_some(), "{key_type:?}/{level:?}");
}
}
}

#[test]
fn should_reject_ineligible_contact_info_keys() {
for key_type in [KeyType::ECDSA_SECP256K1, KeyType::ECDSA_HASH160] {
for level in [SecurityLevel::MASTER, SecurityLevel::MEDIUM] {
assert!(pick(&identity_with(vec![auth_key(1, key_type, level)])).is_none());
}
}
for key_type in [KeyType::BLS12_381, KeyType::EDDSA_25519_HASH160] {
let identity = identity_with(vec![auth_key(1, key_type, SecurityLevel::HIGH)]);
assert!(pick(&identity).is_none(), "{key_type:?}");
}
}

#[test]
fn should_select_high_hash160_from_all_hash160_identity_layout() {
let identity = identity_with(vec![
auth_key(0, KeyType::ECDSA_HASH160, SecurityLevel::MASTER),
auth_key(1, KeyType::ECDSA_HASH160, SecurityLevel::HIGH),
auth_key(2, KeyType::ECDSA_HASH160, SecurityLevel::CRITICAL),
]);
assert_eq!(pick(&identity), identity.public_keys().get(&1));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -616,17 +616,7 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
};

// 5. Build the signing key reference for document signing.
let identity_public_key = sender_identity
// Contact-request send writes a document state transition,
// which DPP requires to be signed by a HIGH-or-stricter
// authentication key. MASTER is rejected on document writes.
.available_signing_key(
signer,
Purpose::AUTHENTICATION,
&[SecurityLevel::HIGH, SecurityLevel::CRITICAL],
&[KeyType::ECDSA_SECP256K1],
false,
)?
let identity_public_key = contact_request_signing_key(&sender_identity, signer)?
.cloned()
.ok_or_else(|| {
PlatformWalletError::InvalidIdentityData(
Expand Down Expand Up @@ -1230,6 +1220,23 @@ fn select_recipient_key_index(recipient_identity: &Identity) -> Result<u32, Plat
})
}

/// The key that signs an outgoing `contactRequest` document: the first enabled
/// HIGH or CRITICAL AUTHENTICATION key, full or HASH160 ECDSA, that `signer`
/// can sign with. DPP requires a HIGH-or-stricter key on document writes;
/// MASTER is rejected there.
fn contact_request_signing_key<'a>(
identity: &'a Identity,
signer: &impl Signer<IdentityPublicKey>,
) -> Result<Option<&'a IdentityPublicKey>, dash_sdk::Error> {
identity.available_signing_key(
signer,
Purpose::AUTHENTICATION,
&[SecurityLevel::HIGH, SecurityLevel::CRITICAL],
&[KeyType::ECDSA_SECP256K1, KeyType::ECDSA_HASH160],
false,
)
}

/// Select our OWN ECDH root key: the first **enabled** `ECDSA_SECP256K1`
/// ENCRYPTION key on the identity (`BTreeMap` order → lowest key id).
///
Expand Down Expand Up @@ -6597,3 +6604,83 @@ mod pending_enqueue_tests {
assert_eq!(backend.writes.load(Ordering::SeqCst), 1);
}
}

#[cfg(test)]
mod signing_key_tests {
use super::super::signing_key::tests::KeyFilter;
use super::contact_request_signing_key;
use dpp::identity::accessors::IdentityGettersV0;
use dpp::identity::identity_public_key::v0::IdentityPublicKeyV0;
use dpp::identity::identity_public_key::Purpose;
use dpp::identity::{Identity, IdentityPublicKey, KeyType, SecurityLevel};
use dpp::version::PlatformVersion;

fn key(
id: u32,
purpose: Purpose,
key_type: KeyType,
security_level: SecurityLevel,
) -> IdentityPublicKey {
IdentityPublicKeyV0 {
id,
purpose,
security_level,
key_type,
data: vec![1; key_type.default_size()].into(),
..Default::default()
}
.into()
}

fn identity_with(keys: Vec<IdentityPublicKey>) -> Identity {
let mut identity = Identity::default_versioned(PlatformVersion::latest()).unwrap();
identity.add_public_keys(keys);
identity
}

fn pick(identity: &Identity) -> Option<&IdentityPublicKey> {
contact_request_signing_key(identity, &KeyFilter(|_| true)).unwrap()
}

#[test]
fn should_select_high_and_critical_ecdsa_contact_request_keys() {
for key_type in [KeyType::ECDSA_SECP256K1, KeyType::ECDSA_HASH160] {
for level in [SecurityLevel::HIGH, SecurityLevel::CRITICAL] {
let identity =
identity_with(vec![key(1, Purpose::AUTHENTICATION, key_type, level)]);
assert!(pick(&identity).is_some(), "{key_type:?}/{level:?}");
}
}
}

#[test]
fn should_select_high_hash160_over_master_transfer_and_encryption_keys() {
let identity = identity_with(vec![
key(
0,
Purpose::AUTHENTICATION,
KeyType::ECDSA_HASH160,
SecurityLevel::MASTER,
),
key(
1,
Purpose::TRANSFER,
KeyType::ECDSA_HASH160,
SecurityLevel::CRITICAL,
),
key(
2,
Purpose::ENCRYPTION,
KeyType::ECDSA_SECP256K1,
SecurityLevel::MEDIUM,
),
key(
3,
Purpose::AUTHENTICATION,
KeyType::ECDSA_HASH160,
SecurityLevel::HIGH,
),
]);
assert_eq!(pick(&identity), identity.public_keys().get(&3));
}
}
Loading