neo3/neo_wallets/
yubi.rs

1//! Helpers for creating wallets for YubiHSM2
2#[cfg(feature = "yubi")]
3use elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
4#[cfg(feature = "yubi")]
5use p256::{NistP256, PublicKey};
6#[cfg(feature = "yubi")]
7use signature::Verifier;
8#[cfg(feature = "yubi")]
9use yubihsm::{
10	asymmetric::Algorithm::EcP256, ecdsa::Signer as YubiSigner, object, object::Label, Capability,
11	Client, Connector, Credentials, Domain,
12};
13
14use crate::{
15	neo_clients::public_key_to_address,
16	neo_crypto::{HashableForVec, Secp256r1PublicKey},
17	neo_types::Address,
18	neo_wallets::{WalletError, WalletSigner},
19};
20
21#[cfg(feature = "yubi")]
22impl WalletSigner<YubiSigner<NistP256>> {
23	/// Connects to a yubi key's ECDSA account at the provided id
24	pub fn connect(
25		connector: Connector,
26		credentials: Credentials,
27		id: object::Id,
28	) -> Result<Self, WalletError> {
29		let client = Client::open(connector, credentials, true).map_err(|e| {
30			WalletError::YubiHsmError(format!("Failed to open YubiHSM client: {e}"))
31		})?;
32
33		let signer = YubiSigner::create(client, id).map_err(|e| {
34			WalletError::YubiHsmError(format!("Failed to create YubiHSM signer: {e}"))
35		})?;
36
37		Ok(signer.into())
38	}
39
40	/// Creates a new random ECDSA keypair on the yubi at the provided id
41	pub fn new(
42		connector: Connector,
43		credentials: Credentials,
44		id: object::Id,
45		label: Label,
46		domain: Domain,
47	) -> Result<Self, WalletError> {
48		let client = Client::open(connector, credentials, true).map_err(|e| {
49			WalletError::YubiHsmError(format!("Failed to open YubiHSM client: {e}"))
50		})?;
51
52		let id = client
53			.generate_asymmetric_key(id, label, domain, Capability::SIGN_ECDSA, EcP256)
54			.map_err(|e| {
55				WalletError::YubiHsmError(format!("Failed to generate asymmetric key: {e}"))
56			})?;
57
58		let signer = YubiSigner::create(client, id).map_err(|e| {
59			WalletError::YubiHsmError(format!("Failed to create YubiHSM signer: {e}"))
60		})?;
61
62		Ok(signer.into())
63	}
64
65	/// Uploads the provided keypair on the yubi at the provided id
66	pub fn from_key(
67		connector: Connector,
68		credentials: Credentials,
69		id: object::Id,
70		label: Label,
71		domain: Domain,
72		key: impl Into<Vec<u8>>,
73	) -> Result<Self, WalletError> {
74		let client = Client::open(connector, credentials, true).map_err(|e| {
75			WalletError::YubiHsmError(format!("Failed to open YubiHSM client: {e}"))
76		})?;
77
78		let id = client
79			.put_asymmetric_key(id, label, domain, Capability::SIGN_ECDSA, EcP256, key)
80			.map_err(|e| WalletError::YubiHsmError(format!("Failed to put asymmetric key: {e}")))?;
81
82		let signer = YubiSigner::create(client, id).map_err(|e| {
83			WalletError::YubiHsmError(format!("Failed to create YubiHSM signer: {e}"))
84		})?;
85
86		Ok(signer.into())
87	}
88
89	// /// Verifies a given message and signature.
90	// /// The message will be hashed using the `Sha256` algorithm before being verified.
91	// /// If the signature is valid, the method will return `Ok(())`, otherwise it will return a `WalletError`.
92	// /// # Arguments
93	// /// * `message` - The message to be verified.
94	// /// * `signature` - The signature to be verified.
95	// /// # Returns
96	// /// A `Result` containing `Ok(())` if the signature is valid, or a `WalletError` on failure.
97	// pub async fn verify_message(
98	// 	&self,
99	// 	message: &[u8],
100	// 	signature: &Signature<NistP256>,
101	// ) -> Result<(), WalletError> {
102	// 	let hash = message.hash256();
103	// 	// let hash = H256::from_slice(hash.as_slice());
104	// 	let verify_key = p256::ecdsa::VerifyingKey::from_encoded_point(self.signer.public_key()).unwrap();
105	// 	match verify_key.verify(hash, &signature) {
106	// 		Ok(_) => Ok(()),
107	// 		Err(_) => Err(WalletError::VerifyError),
108	// 	}
109	// 	// let signature: ecdsa::Signature<NistP256> = signer.sign(TEST_MESSAGE);
110	// }
111}
112
113#[cfg(feature = "yubi")]
114impl From<YubiSigner<NistP256>> for WalletSigner<YubiSigner<NistP256>> {
115	fn from(signer: YubiSigner<NistP256>) -> Self {
116		// this should never fail for a valid YubiSigner
117		let public_key = PublicKey::from_encoded_point(signer.public_key());
118		if !bool::from(public_key.is_some()) {
119			// Log the error and create a fallback address instead of panicking
120			eprintln!(
121				"Warning: YubiSigner provided invalid public key, using zero address as fallback"
122			);
123			return Self { signer, address: Address::default(), network: None };
124		}
125
126		let public_key = public_key.unwrap();
127		let public_key = public_key.to_encoded_point(true);
128		let public_key = public_key.as_bytes();
129
130		// The first byte can be either 0x02 or 0x03 for compressed public keys
131		debug_assert!(public_key[0] == 0x02 || public_key[0] == 0x03);
132
133		let secp_public_key = match Secp256r1PublicKey::from_bytes(&public_key) {
134			Ok(key) => key,
135			Err(_) => {
136				eprintln!("Warning: Failed to convert YubiSigner public key to Secp256r1PublicKey, using zero address as fallback");
137				return Self { signer, address: Address::default(), network: None };
138			},
139		};
140
141		let address = public_key_to_address(&secp_public_key);
142
143		Self { signer, address, network: None }
144	}
145}
146
147#[cfg(test)]
148pub mod tests {
149	use std::str::FromStr;
150
151	use super::*;
152
153	#[cfg(feature = "mock-hsm")]
154	#[tokio::test]
155	async fn from_key() {
156		let key = hex::decode("2d8c44dc2dd2f0bea410e342885379192381e82d855b1b112f9b55544f1e0900")
157			.expect("Should be able to decode valid hex");
158
159		let connector = yubihsm::Connector::mockhsm();
160		let wallet = WalletSigner::from_key(
161			connector,
162			Credentials::default(),
163			0,
164			Label::from_bytes(&[]).expect("Empty label should be valid"),
165			Domain::at(1).expect("Domain 1 should be valid"),
166			key,
167		)
168		.expect("Should be able to create wallet from key");
169
170		let msg = "Some data";
171		let sig = wallet
172			.sign_message(msg.as_bytes())
173			.await
174			.expect("Should be able to sign message");
175
176		let verify_key = p256::ecdsa::VerifyingKey::from_encoded_point(wallet.signer.public_key())
177			.expect("Should be able to create verifying key from public key");
178
179		assert!(verify_key.verify(msg.as_bytes(), &sig).is_ok());
180
181		assert_eq!(
182			wallet.address(),
183			Address::from_str("NPZyWCdSCWghLM7hcxT5kgc7cC2V2RGeHZ")
184				.expect("Should be able to parse valid address")
185		);
186	}
187
188	#[cfg(feature = "mock-hsm")]
189	#[tokio::test]
190	async fn new_key() {
191		let connector = yubihsm::Connector::mockhsm();
192		let wallet = WalletSigner::<YubiSigner<NistP256>>::new(
193			connector,
194			Credentials::default(),
195			0,
196			Label::from_bytes(&[]).expect("Empty label should be valid"),
197			Domain::at(1).expect("Domain 1 should be valid"),
198		)
199		.expect("Should be able to create new wallet");
200
201		let msg = "Some data";
202		let sig = wallet
203			.sign_message(msg.as_bytes())
204			.await
205			.expect("Should be able to sign message");
206
207		let verify_key = p256::ecdsa::VerifyingKey::from_encoded_point(wallet.signer.public_key())
208			.expect("Should be able to create verifying key from public key");
209
210		assert!(verify_key.verify(msg.as_bytes(), &sig).is_ok());
211	}
212
213	// Non-mock tests can be added here for production hardware testing
214	#[test]
215	fn test_wallet_signer_creation() {
216		// This test doesn't require mockhsm and can run in production builds
217		// Add tests that don't require actual hardware here
218
219		// Test that the WalletSigner type exists and has correct type parameters
220		#[cfg(feature = "yubi")]
221		{
222			use std::marker::PhantomData;
223			let _phantom: PhantomData<WalletSigner<YubiSigner<NistP256>>> = PhantomData;
224		}
225
226		// Test that required error types exist
227		let _error = WalletError::YubiHsmError("test".to_string());
228
229		// Professional test validates type system without hardware dependency
230		assert!(true);
231	}
232}