Skip to content

Commit 160d35f

Browse files
feat(key-wallet): DIP-13 application session authentication and encryption paths
Adds sub-features 6' (application session authentication) and 7' (application encryption) under m/9'/coin_type'/5'/ from the DIP-13 amendment in dashpay/dips#191: the sub-feature constants and mainnet / testnet roots in dip9.rs, two DerivationPathReference variants (declared after Root because the bincode derive encodes variants by position), and DerivationPath::application_session_authentication_path / application_encryption_path in bip32.rs, with ApplicationKeyPurpose next to KeyDerivationType for the trailing key purpose level. The identity, request and contract ids are DIP-14 256-bit hardened children, which only secp256k1 derivation defines, so the builders take no key type and always put ECDSA (0') at the key type level. A BLS key cannot be requested on these paths. Tests pin the path strings on both networks and the derived public keys for the all-zero-entropy mnemonic, with the uniform ids dashpay/platform already pins (so moving the derivation here moves no key) and with non-uniform ids so a byte-order or argument-order change fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 929a651 commit 160d35f

2 files changed

Lines changed: 304 additions & 7 deletions

File tree

‎key-wallet/src/bip32.rs‎

Lines changed: 217 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ use secp256k1::{self, XOnlyPublicKey};
3333
use serde;
3434

3535
use crate::dip9::{
36-
ASSET_LOCK_ADDRESS_TOPUP_PATH_MAINNET, ASSET_LOCK_ADDRESS_TOPUP_PATH_TESTNET,
37-
ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_MAINNET, ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_TESTNET,
38-
COINJOIN_PATH_MAINNET, COINJOIN_PATH_TESTNET, DASH_BIP44_PATH_MAINNET, DASH_BIP44_PATH_TESTNET,
39-
IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET,
40-
IDENTITY_INVITATION_PATH_MAINNET, IDENTITY_INVITATION_PATH_TESTNET,
41-
IDENTITY_REGISTRATION_PATH_MAINNET, IDENTITY_REGISTRATION_PATH_TESTNET,
42-
IDENTITY_TOPUP_PATH_MAINNET, IDENTITY_TOPUP_PATH_TESTNET,
36+
APPLICATION_ENCRYPTION_PATH_MAINNET, APPLICATION_ENCRYPTION_PATH_TESTNET,
37+
APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET,
38+
APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET, ASSET_LOCK_ADDRESS_TOPUP_PATH_MAINNET,
39+
ASSET_LOCK_ADDRESS_TOPUP_PATH_TESTNET, ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_MAINNET,
40+
ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_TESTNET, COINJOIN_PATH_MAINNET, COINJOIN_PATH_TESTNET,
41+
DASH_BIP44_PATH_MAINNET, DASH_BIP44_PATH_TESTNET, IDENTITY_AUTHENTICATION_PATH_MAINNET,
42+
IDENTITY_AUTHENTICATION_PATH_TESTNET, IDENTITY_INVITATION_PATH_MAINNET,
43+
IDENTITY_INVITATION_PATH_TESTNET, IDENTITY_REGISTRATION_PATH_MAINNET,
44+
IDENTITY_REGISTRATION_PATH_TESTNET, IDENTITY_TOPUP_PATH_MAINNET, IDENTITY_TOPUP_PATH_TESTNET,
4345
};
4446
use base58ck;
4547
#[cfg(feature = "bincode")]
@@ -1025,6 +1027,24 @@ impl From<KeyDerivationType> for u32 {
10251027
}
10261028
}
10271029

1030+
/// The `key_purpose'` level of a DIP-13 application encryption path: the Platform identity key
1031+
/// purpose the derived key is registered with.
1032+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1033+
#[repr(u32)]
1034+
pub enum ApplicationKeyPurpose {
1035+
Encryption = 1,
1036+
Decryption = 2,
1037+
}
1038+
1039+
impl From<ApplicationKeyPurpose> for u32 {
1040+
fn from(val: ApplicationKeyPurpose) -> Self {
1041+
match val {
1042+
ApplicationKeyPurpose::Encryption => 1,
1043+
ApplicationKeyPurpose::Decryption => 2,
1044+
}
1045+
}
1046+
}
1047+
10281048
impl DerivationPath {
10291049
pub fn bip_44_account(network: Network, account: u32) -> Self {
10301050
let mut root_derivation_path: DerivationPath = match network {
@@ -1176,6 +1196,66 @@ impl DerivationPath {
11761196
root_derivation_path
11771197
}
11781198

1199+
/// DIP-13 application session authentication key path,
1200+
/// `m/9'/coin_type'/5'/6'/0'/identity_id'/request_id'`. The identity id and request id are
1201+
/// DIP-14 256-bit hardened children, which only secp256k1 derivation defines, so the key type
1202+
/// level is always ECDSA (`0'`).
1203+
pub fn application_session_authentication_path(
1204+
network: Network,
1205+
identity_id: [u8; 32],
1206+
request_id: [u8; 32],
1207+
) -> Self {
1208+
let mut root_derivation_path: DerivationPath = match network {
1209+
Network::Mainnet => APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET,
1210+
_ => APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET,
1211+
}
1212+
.into();
1213+
root_derivation_path.0.extend(&[
1214+
ChildNumber::Hardened {
1215+
index: KeyDerivationType::ECDSA.into(),
1216+
},
1217+
ChildNumber::Hardened256 {
1218+
index: identity_id,
1219+
},
1220+
ChildNumber::Hardened256 {
1221+
index: request_id,
1222+
},
1223+
]);
1224+
root_derivation_path
1225+
}
1226+
1227+
/// DIP-13 application encryption key path,
1228+
/// `m/9'/coin_type'/5'/7'/0'/identity_id'/contract_id'/key_purpose'`. The identity id and
1229+
/// contract id are DIP-14 256-bit hardened children, which only secp256k1 derivation defines,
1230+
/// so the key type level is always ECDSA (`0'`).
1231+
pub fn application_encryption_path(
1232+
network: Network,
1233+
identity_id: [u8; 32],
1234+
contract_id: [u8; 32],
1235+
key_purpose: ApplicationKeyPurpose,
1236+
) -> Self {
1237+
let mut root_derivation_path: DerivationPath = match network {
1238+
Network::Mainnet => APPLICATION_ENCRYPTION_PATH_MAINNET,
1239+
_ => APPLICATION_ENCRYPTION_PATH_TESTNET,
1240+
}
1241+
.into();
1242+
root_derivation_path.0.extend(&[
1243+
ChildNumber::Hardened {
1244+
index: KeyDerivationType::ECDSA.into(),
1245+
},
1246+
ChildNumber::Hardened256 {
1247+
index: identity_id,
1248+
},
1249+
ChildNumber::Hardened256 {
1250+
index: contract_id,
1251+
},
1252+
ChildNumber::Hardened {
1253+
index: key_purpose.into(),
1254+
},
1255+
]);
1256+
root_derivation_path
1257+
}
1258+
11791259
pub fn derive_priv_ecdsa_for_master_seed(
11801260
&self,
11811261
seed: &[u8],
@@ -2676,6 +2756,136 @@ mod tests {
26762756
assert_eq!(path.to_string(), "m/9'/1'/5'/0'/1'/2'/3'");
26772757
}
26782758

2759+
#[test]
2760+
fn test_application_session_authentication_path() {
2761+
let path = DerivationPath::application_session_authentication_path(
2762+
Network::Mainnet,
2763+
[0x01; 32],
2764+
[0x02; 32],
2765+
);
2766+
assert_eq!(
2767+
path.to_string(),
2768+
format!("m/9'/5'/5'/6'/0'/0x{}'/0x{}'", "01".repeat(32), "02".repeat(32))
2769+
);
2770+
2771+
let path = DerivationPath::application_session_authentication_path(
2772+
Network::Testnet,
2773+
[0x01; 32],
2774+
[0x02; 32],
2775+
);
2776+
assert_eq!(
2777+
path.to_string(),
2778+
format!("m/9'/1'/5'/6'/0'/0x{}'/0x{}'", "01".repeat(32), "02".repeat(32))
2779+
);
2780+
}
2781+
2782+
#[test]
2783+
fn test_application_encryption_path() {
2784+
let path = DerivationPath::application_encryption_path(
2785+
Network::Mainnet,
2786+
[0x01; 32],
2787+
[0x02; 32],
2788+
ApplicationKeyPurpose::Encryption,
2789+
);
2790+
assert_eq!(
2791+
path.to_string(),
2792+
format!("m/9'/5'/5'/7'/0'/0x{}'/0x{}'/1'", "01".repeat(32), "02".repeat(32))
2793+
);
2794+
2795+
let path = DerivationPath::application_encryption_path(
2796+
Network::Testnet,
2797+
[0x01; 32],
2798+
[0x02; 32],
2799+
ApplicationKeyPurpose::Decryption,
2800+
);
2801+
assert_eq!(
2802+
path.to_string(),
2803+
format!("m/9'/1'/5'/7'/0'/0x{}'/0x{}'/2'", "01".repeat(32), "02".repeat(32))
2804+
);
2805+
}
2806+
2807+
/// Pinned public keys for the all-zero-entropy test mnemonic. A change here orphans registered
2808+
/// encryption keys, which wallets re-derive from seed. The uniform ids match the vectors
2809+
/// dashpay/platform pins; the non-uniform ones catch a byte-order or argument-order change.
2810+
#[test]
2811+
fn test_application_key_vectors() {
2812+
let seed = crate::mnemonic::Mnemonic::from_phrase(
2813+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
2814+
)
2815+
.unwrap()
2816+
.to_seed("");
2817+
let ascending: [u8; 32] = core::array::from_fn(|i| i as u8);
2818+
let descending: [u8; 32] = core::array::from_fn(|i| 0xff - i as u8);
2819+
let public_key_hex = |path: DerivationPath, network| {
2820+
hex::encode(
2821+
path.derive_pub_ecdsa_for_master_seed(&seed, network)
2822+
.unwrap()
2823+
.public_key
2824+
.serialize(),
2825+
)
2826+
};
2827+
2828+
let session = |identity_id, request_id| {
2829+
public_key_hex(
2830+
DerivationPath::application_session_authentication_path(
2831+
Network::Testnet,
2832+
identity_id,
2833+
request_id,
2834+
),
2835+
Network::Testnet,
2836+
)
2837+
};
2838+
let encryption = |identity_id, contract_id, key_purpose| {
2839+
public_key_hex(
2840+
DerivationPath::application_encryption_path(
2841+
Network::Mainnet,
2842+
identity_id,
2843+
contract_id,
2844+
key_purpose,
2845+
),
2846+
Network::Mainnet,
2847+
)
2848+
};
2849+
2850+
assert_eq!(
2851+
session([0x35; 32], [0x6B; 32]),
2852+
"022c8b2e806244482374b1caf8306146dc03aad3b99a5954efd5e70a0eddd37a5d"
2853+
);
2854+
assert_eq!(
2855+
encryption([0x35; 32], [0x6B; 32], ApplicationKeyPurpose::Encryption),
2856+
"03e989de1b62f137231cc06659c810c17100becd1f5e23d5710faa7602769fe434"
2857+
);
2858+
assert_eq!(
2859+
session(ascending, descending),
2860+
"0278ad78c1a220924bf0e06e1111c61422efeace068f30efa825604b436970b230"
2861+
);
2862+
assert_eq!(
2863+
encryption(ascending, descending, ApplicationKeyPurpose::Encryption),
2864+
"03c4e462229f176595ec17e28e9096d3cfbc42496fb8ed4d48f8f58355d6105aa9"
2865+
);
2866+
assert_eq!(
2867+
encryption(ascending, descending, ApplicationKeyPurpose::Decryption),
2868+
"031fd0a3cfc01bebb89d43c2e12a6321c37ebdacb62c09495899e5b65a24fa5c99"
2869+
);
2870+
2871+
// The builder's path is the documented string, byte order included.
2872+
let parsed: DerivationPath = format!(
2873+
"m/9'/1'/5'/6'/0'/0x{}'/0x{}'",
2874+
hex::encode(ascending),
2875+
hex::encode(descending)
2876+
)
2877+
.parse()
2878+
.unwrap();
2879+
assert_eq!(
2880+
parsed,
2881+
DerivationPath::application_session_authentication_path(
2882+
Network::Testnet,
2883+
ascending,
2884+
descending,
2885+
)
2886+
);
2887+
}
2888+
26792889
#[test]
26802890
fn test_derive_priv_ecdsa_for_master_seed() {
26812891
let path = DerivationPath::bip_44_account(Network::Mainnet, 0);

‎key-wallet/src/dip9.rs‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ pub enum DerivationPathReference {
3030
BlockchainAssetLockAddressTopupFunding = 17,
3131
BlockchainAssetLockShieldedAddressTopupFunding = 18,
3232
Root = 255,
33+
// Declared after `Root` so the bincode variant index of every earlier variant is unchanged.
34+
ApplicationSessionAuthentication = 19,
35+
ApplicationEncryption = 20,
3336
}
3437

3538
bitflags! {
@@ -131,6 +134,12 @@ pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_TOPUP: u32 = 2;
131134
pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_INVITATIONS: u32 = 3;
132135
pub const FEATURE_PURPOSE_ASSET_LOCK_SUBFEATURE_ADDRESS_TOPUP: u32 = 4;
133136
pub const FEATURE_PURPOSE_ASSET_LOCK_SUBFEATURE_SHIELDED_ADDRESS_TOPUP: u32 = 5;
137+
/// DIP-13 application session authentication sub-feature:
138+
/// `m/9'/coin_type'/5'/6'/key_type'/identity_id'/request_id'`.
139+
pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_SESSION_AUTHENTICATION: u32 = 6;
140+
/// DIP-13 application encryption sub-feature:
141+
/// `m/9'/coin_type'/5'/7'/key_type'/identity_id'/contract_id'/key_purpose'`.
142+
pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_ENCRYPTION: u32 = 7;
134143
pub const FEATURE_PURPOSE_DASHPAY: u32 = 15;
135144
/// DIP-15 auto-accept feature index: the derivation family
136145
/// `m/9'/coin_type'/16'/expiry'` holding the shareable, expiry-bounded
@@ -473,6 +482,84 @@ pub const IDENTITY_AUTHENTICATION_PATH_TESTNET: IndexConstPath<4> = IndexConstPa
473482
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
474483
};
475484

485+
// Application Session Authentication Keys Paths
486+
pub const APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET: IndexConstPath<4> = IndexConstPath {
487+
indexes: [
488+
ChildNumber::Hardened {
489+
index: FEATURE_PURPOSE,
490+
},
491+
ChildNumber::Hardened {
492+
index: DASH_COIN_TYPE,
493+
},
494+
ChildNumber::Hardened {
495+
index: FEATURE_PURPOSE_IDENTITIES,
496+
},
497+
ChildNumber::Hardened {
498+
index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_SESSION_AUTHENTICATION,
499+
},
500+
],
501+
reference: DerivationPathReference::ApplicationSessionAuthentication,
502+
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
503+
};
504+
505+
pub const APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET: IndexConstPath<4> = IndexConstPath {
506+
indexes: [
507+
ChildNumber::Hardened {
508+
index: FEATURE_PURPOSE,
509+
},
510+
ChildNumber::Hardened {
511+
index: DASH_TESTNET_COIN_TYPE,
512+
},
513+
ChildNumber::Hardened {
514+
index: FEATURE_PURPOSE_IDENTITIES,
515+
},
516+
ChildNumber::Hardened {
517+
index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_SESSION_AUTHENTICATION,
518+
},
519+
],
520+
reference: DerivationPathReference::ApplicationSessionAuthentication,
521+
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
522+
};
523+
524+
// Application Encryption Keys Paths
525+
pub const APPLICATION_ENCRYPTION_PATH_MAINNET: IndexConstPath<4> = IndexConstPath {
526+
indexes: [
527+
ChildNumber::Hardened {
528+
index: FEATURE_PURPOSE,
529+
},
530+
ChildNumber::Hardened {
531+
index: DASH_COIN_TYPE,
532+
},
533+
ChildNumber::Hardened {
534+
index: FEATURE_PURPOSE_IDENTITIES,
535+
},
536+
ChildNumber::Hardened {
537+
index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_ENCRYPTION,
538+
},
539+
],
540+
reference: DerivationPathReference::ApplicationEncryption,
541+
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
542+
};
543+
544+
pub const APPLICATION_ENCRYPTION_PATH_TESTNET: IndexConstPath<4> = IndexConstPath {
545+
indexes: [
546+
ChildNumber::Hardened {
547+
index: FEATURE_PURPOSE,
548+
},
549+
ChildNumber::Hardened {
550+
index: DASH_TESTNET_COIN_TYPE,
551+
},
552+
ChildNumber::Hardened {
553+
index: FEATURE_PURPOSE_IDENTITIES,
554+
},
555+
ChildNumber::Hardened {
556+
index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_APPLICATION_ENCRYPTION,
557+
},
558+
],
559+
reference: DerivationPathReference::ApplicationEncryption,
560+
path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION,
561+
};
562+
476563
// DIP-17: Platform Payment Address Paths
477564
// Path: m/9'/coin_type'/17'/account'/key_class'/index
478565
// Note: The full path includes account'/key_class'/index which is appended during derivation

0 commit comments

Comments
 (0)