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
224 changes: 217 additions & 7 deletions key-wallet/src/bip32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ use secp256k1::{self, XOnlyPublicKey};
use serde;

use crate::dip9::{
ASSET_LOCK_ADDRESS_TOPUP_PATH_MAINNET, ASSET_LOCK_ADDRESS_TOPUP_PATH_TESTNET,
ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_MAINNET, ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_TESTNET,
COINJOIN_PATH_MAINNET, COINJOIN_PATH_TESTNET, DASH_BIP44_PATH_MAINNET, DASH_BIP44_PATH_TESTNET,
IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET,
IDENTITY_INVITATION_PATH_MAINNET, IDENTITY_INVITATION_PATH_TESTNET,
IDENTITY_REGISTRATION_PATH_MAINNET, IDENTITY_REGISTRATION_PATH_TESTNET,
IDENTITY_TOPUP_PATH_MAINNET, IDENTITY_TOPUP_PATH_TESTNET,
APPLICATION_ENCRYPTION_PATH_MAINNET, APPLICATION_ENCRYPTION_PATH_TESTNET,
APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET,
APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET, ASSET_LOCK_ADDRESS_TOPUP_PATH_MAINNET,
ASSET_LOCK_ADDRESS_TOPUP_PATH_TESTNET, ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_MAINNET,
ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_TESTNET, COINJOIN_PATH_MAINNET, COINJOIN_PATH_TESTNET,
DASH_BIP44_PATH_MAINNET, DASH_BIP44_PATH_TESTNET, IDENTITY_AUTHENTICATION_PATH_MAINNET,
IDENTITY_AUTHENTICATION_PATH_TESTNET, IDENTITY_INVITATION_PATH_MAINNET,
IDENTITY_INVITATION_PATH_TESTNET, IDENTITY_REGISTRATION_PATH_MAINNET,
IDENTITY_REGISTRATION_PATH_TESTNET, IDENTITY_TOPUP_PATH_MAINNET, IDENTITY_TOPUP_PATH_TESTNET,
};
use base58ck;
#[cfg(feature = "bincode")]
Expand Down Expand Up @@ -1025,6 +1027,24 @@ impl From<KeyDerivationType> for u32 {
}
}

/// The `key_purpose'` level of a DIP-13 application encryption path: the Platform identity key
/// purpose the derived key is registered with.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
#[repr(u32)]
pub enum ApplicationKeyPurpose {
Encryption = 1,
Decryption = 2,
}

impl From<ApplicationKeyPurpose> for u32 {
fn from(val: ApplicationKeyPurpose) -> Self {
match val {
ApplicationKeyPurpose::Encryption => 1,
ApplicationKeyPurpose::Decryption => 2,
}
}
}

impl DerivationPath {
pub fn bip_44_account(network: Network, account: u32) -> Self {
let mut root_derivation_path: DerivationPath = match network {
Expand Down Expand Up @@ -1176,6 +1196,66 @@ impl DerivationPath {
root_derivation_path
}

/// DIP-13 application session authentication key path,
/// `m/9'/coin_type'/5'/6'/0'/identity_id'/request_id'`. The identity id and request id are
/// DIP-14 256-bit hardened children, which only secp256k1 derivation defines, so the key type
/// level is always ECDSA (`0'`).
pub fn application_session_authentication_path(
network: Network,
identity_id: [u8; 32],
request_id: [u8; 32],
) -> Self {
let mut root_derivation_path: DerivationPath = match network {
Network::Mainnet => APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET,
_ => APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET,
}
.into();
root_derivation_path.0.extend(&[
ChildNumber::Hardened {
index: KeyDerivationType::ECDSA.into(),
},
ChildNumber::Hardened256 {
index: identity_id,
},
ChildNumber::Hardened256 {
index: request_id,
},
]);
root_derivation_path
}

/// DIP-13 application encryption key path,
/// `m/9'/coin_type'/5'/7'/0'/identity_id'/contract_id'/key_purpose'`. The identity id and
/// contract id are DIP-14 256-bit hardened children, which only secp256k1 derivation defines,
/// so the key type level is always ECDSA (`0'`).
pub fn application_encryption_path(
network: Network,
identity_id: [u8; 32],
contract_id: [u8; 32],
key_purpose: ApplicationKeyPurpose,
) -> Self {
let mut root_derivation_path: DerivationPath = match network {
Network::Mainnet => APPLICATION_ENCRYPTION_PATH_MAINNET,
_ => APPLICATION_ENCRYPTION_PATH_TESTNET,
}
.into();
root_derivation_path.0.extend(&[
ChildNumber::Hardened {
index: KeyDerivationType::ECDSA.into(),
},
ChildNumber::Hardened256 {
index: identity_id,
},
ChildNumber::Hardened256 {
index: contract_id,
},
ChildNumber::Hardened {
index: key_purpose.into(),
},
]);
root_derivation_path
}

pub fn derive_priv_ecdsa_for_master_seed(
&self,
seed: &[u8],
Expand Down Expand Up @@ -2676,6 +2756,136 @@ mod tests {
assert_eq!(path.to_string(), "m/9'/1'/5'/0'/1'/2'/3'");
}

#[test]
fn test_application_session_authentication_path() {
let path = DerivationPath::application_session_authentication_path(
Network::Mainnet,
[0x01; 32],
[0x02; 32],
);
assert_eq!(
path.to_string(),
format!("m/9'/5'/5'/6'/0'/0x{}'/0x{}'", "01".repeat(32), "02".repeat(32))
);

let path = DerivationPath::application_session_authentication_path(
Network::Testnet,
[0x01; 32],
[0x02; 32],
);
assert_eq!(
path.to_string(),
format!("m/9'/1'/5'/6'/0'/0x{}'/0x{}'", "01".repeat(32), "02".repeat(32))
);
}

#[test]
fn test_application_encryption_path() {
let path = DerivationPath::application_encryption_path(
Network::Mainnet,
[0x01; 32],
[0x02; 32],
ApplicationKeyPurpose::Encryption,
);
assert_eq!(
path.to_string(),
format!("m/9'/5'/5'/7'/0'/0x{}'/0x{}'/1'", "01".repeat(32), "02".repeat(32))
);

let path = DerivationPath::application_encryption_path(
Network::Testnet,
[0x01; 32],
[0x02; 32],
ApplicationKeyPurpose::Decryption,
);
assert_eq!(
path.to_string(),
format!("m/9'/1'/5'/7'/0'/0x{}'/0x{}'/2'", "01".repeat(32), "02".repeat(32))
);
}

/// Pinned public keys for the all-zero-entropy test mnemonic. A change here orphans registered
/// encryption keys, which wallets re-derive from seed. The uniform ids match the vectors
/// dashpay/platform pins; the non-uniform ones catch a byte-order or argument-order change.
#[test]
fn test_application_key_vectors() {
let seed = crate::mnemonic::Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
)
.unwrap()
.to_seed("");
let ascending: [u8; 32] = core::array::from_fn(|i| i as u8);
let descending: [u8; 32] = core::array::from_fn(|i| 0xff - i as u8);
let public_key_hex = |path: DerivationPath, network| {
hex::encode(
path.derive_pub_ecdsa_for_master_seed(&seed, network)
.unwrap()
.public_key
.serialize(),
)
};

let session = |identity_id, request_id| {
public_key_hex(
DerivationPath::application_session_authentication_path(
Network::Testnet,
identity_id,
request_id,
),
Network::Testnet,
)
};
let encryption = |identity_id, contract_id, key_purpose| {
public_key_hex(
DerivationPath::application_encryption_path(
Network::Mainnet,
identity_id,
contract_id,
key_purpose,
),
Network::Mainnet,
)
};

assert_eq!(
session([0x35; 32], [0x6B; 32]),
"022c8b2e806244482374b1caf8306146dc03aad3b99a5954efd5e70a0eddd37a5d"
);
assert_eq!(
encryption([0x35; 32], [0x6B; 32], ApplicationKeyPurpose::Encryption),
"03e989de1b62f137231cc06659c810c17100becd1f5e23d5710faa7602769fe434"
);
assert_eq!(
session(ascending, descending),
"0278ad78c1a220924bf0e06e1111c61422efeace068f30efa825604b436970b230"
);
assert_eq!(
encryption(ascending, descending, ApplicationKeyPurpose::Encryption),
"03c4e462229f176595ec17e28e9096d3cfbc42496fb8ed4d48f8f58355d6105aa9"
);
assert_eq!(
encryption(ascending, descending, ApplicationKeyPurpose::Decryption),
"031fd0a3cfc01bebb89d43c2e12a6321c37ebdacb62c09495899e5b65a24fa5c99"
);

// The builder's path is the documented string, byte order included.
let parsed: DerivationPath = format!(
"m/9'/1'/5'/6'/0'/0x{}'/0x{}'",
hex::encode(ascending),
hex::encode(descending)
)
.parse()
.unwrap();
assert_eq!(
parsed,
DerivationPath::application_session_authentication_path(
Network::Testnet,
ascending,
descending,
)
);
}

#[test]
fn test_derive_priv_ecdsa_for_master_seed() {
let path = DerivationPath::bip_44_account(Network::Mainnet, 0);
Expand Down
87 changes: 87 additions & 0 deletions key-wallet/src/dip9.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub enum DerivationPathReference {
BlockchainAssetLockAddressTopupFunding = 17,
BlockchainAssetLockShieldedAddressTopupFunding = 18,
Root = 255,
// Declared after `Root` so the bincode variant index of every earlier variant is unchanged.
ApplicationSessionAuthentication = 19,
ApplicationEncryption = 20,
}

bitflags! {
Expand Down Expand Up @@ -131,6 +134,12 @@ pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_TOPUP: u32 = 2;
pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_INVITATIONS: u32 = 3;
pub const FEATURE_PURPOSE_ASSET_LOCK_SUBFEATURE_ADDRESS_TOPUP: u32 = 4;
pub const FEATURE_PURPOSE_ASSET_LOCK_SUBFEATURE_SHIELDED_ADDRESS_TOPUP: u32 = 5;
/// DIP-13 application session authentication sub-feature:
/// `m/9'/coin_type'/5'/6'/key_type'/identity_id'/request_id'`.
pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_SESSION_AUTHENTICATION: u32 = 6;
/// DIP-13 application encryption sub-feature:
/// `m/9'/coin_type'/5'/7'/key_type'/identity_id'/contract_id'/key_purpose'`.
pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_ENCRYPTION: u32 = 7;
pub const FEATURE_PURPOSE_DASHPAY: u32 = 15;
/// DIP-15 auto-accept feature index: the derivation family
/// `m/9'/coin_type'/16'/expiry'` holding the shareable, expiry-bounded
Expand Down Expand Up @@ -473,6 +482,84 @@ pub const IDENTITY_AUTHENTICATION_PATH_TESTNET: IndexConstPath<4> = IndexConstPa
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
};

// Application Session Authentication Keys Paths
pub const APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET: IndexConstPath<4> = IndexConstPath {
indexes: [
ChildNumber::Hardened {
index: FEATURE_PURPOSE,
},
ChildNumber::Hardened {
index: DASH_COIN_TYPE,
},
ChildNumber::Hardened {
index: FEATURE_PURPOSE_IDENTITIES,
},
ChildNumber::Hardened {
index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_SESSION_AUTHENTICATION,
},
],
reference: DerivationPathReference::ApplicationSessionAuthentication,
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
};

pub const APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET: IndexConstPath<4> = IndexConstPath {
indexes: [
ChildNumber::Hardened {
index: FEATURE_PURPOSE,
},
ChildNumber::Hardened {
index: DASH_TESTNET_COIN_TYPE,
},
ChildNumber::Hardened {
index: FEATURE_PURPOSE_IDENTITIES,
},
ChildNumber::Hardened {
index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_SESSION_AUTHENTICATION,
},
],
reference: DerivationPathReference::ApplicationSessionAuthentication,
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
};

// Application Encryption Keys Paths
pub const APPLICATION_ENCRYPTION_PATH_MAINNET: IndexConstPath<4> = IndexConstPath {
indexes: [
ChildNumber::Hardened {
index: FEATURE_PURPOSE,
},
ChildNumber::Hardened {
index: DASH_COIN_TYPE,
},
ChildNumber::Hardened {
index: FEATURE_PURPOSE_IDENTITIES,
},
ChildNumber::Hardened {
index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_ENCRYPTION,
},
],
reference: DerivationPathReference::ApplicationEncryption,
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
};

pub const APPLICATION_ENCRYPTION_PATH_TESTNET: IndexConstPath<4> = IndexConstPath {
indexes: [
ChildNumber::Hardened {
index: FEATURE_PURPOSE,
},
ChildNumber::Hardened {
index: DASH_TESTNET_COIN_TYPE,
},
ChildNumber::Hardened {
index: FEATURE_PURPOSE_IDENTITIES,
},
ChildNumber::Hardened {
index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_ENCRYPTION,
},
],
reference: DerivationPathReference::ApplicationEncryption,
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
};

// DIP-17: Platform Payment Address Paths
// Path: m/9'/coin_type'/17'/account'/key_class'/index
// Note: The full path includes account'/key_class'/index which is appended during derivation
Expand Down
Loading