diff --git a/Cargo.lock b/Cargo.lock index 11a3836c..49785270 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2674,6 +2674,15 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "inout" version = "0.1.4" @@ -3697,6 +3706,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -4316,6 +4334,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "potential_utf" version = "0.1.3" @@ -4457,6 +4481,95 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pyo3" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "serde", + "unindent", +] + +[[package]] +name = "pyo3-async-runtimes" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0b83dc42f9d41f50d38180dad65f0c99763b65a3ff2a81bf351dd35a1df8bf" +dependencies = [ + "futures", + "once_cell", + "pin-project-lite", + "pyo3", + "pyo3-async-runtimes-macros", + "tokio", +] + +[[package]] +name = "pyo3-async-runtimes-macros" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf103ba4062fbb1e8022d9ed9b9830fbab074b2db0a0496c78e45a62f4330bcd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pyo3-build-config" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.106", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -5404,6 +5517,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-pyobject" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c485853a65e1a5f2db72e818ec4c7548a39614fabdd988f5e3504071453b7d7" +dependencies = [ + "pyo3", + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -5996,6 +6119,12 @@ dependencies = [ "libc", ] +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + [[package]] name = "tempfile" version = "3.23.0" @@ -6418,6 +6547,8 @@ dependencies = [ "getrandom 0.2.16", "hpke", "once_cell", + "pyo3", + "pyo3-async-runtimes", "quinn", "rand 0.8.5", "rand_core 0.6.4", @@ -6427,6 +6558,7 @@ dependencies = [ "rustls-pemfile", "rustls-pki-types", "serde", + "serde-pyobject", "serde_json", "serde_with", "sha2 0.11.0-rc.3", @@ -6566,6 +6698,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index ac0db031..f67b9d2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ blurhash = { version = "0.2.3", default-features = false } ## Commit "f0bc4625dcd729e07e4a36257df2f1d94c81cef4" is the most recent one without the invalid change to pin serde to 1.0.219. ## See my issue here: . ## However, that commit doesn't build.... yikes. So we have to use a slightly older commit in the "rev" field below. -tsp_sdk = { git = "https://github.com/openwallet-foundation-labs/tsp.git", rev = "1cd0cc9442e144ad7c01ccd30daffbb3a52c0f20", optional = true, features = ["async", "resolve"] } +tsp_sdk = { git = "https://github.com/openwallet-foundation-labs/tsp.git", rev = "1cd0cc9442e144ad7c01ccd30daffbb3a52c0f20", optional = true, features = ["async", "resolve", "create-webvh"] } quinn = { version = "0.11", default-features = false, optional = true } ## We only include this such that we can specify the prebuilt-nasm features, ## which is required to build this on Windows x86_64 without having to install NASM separately. diff --git a/src/tsp/create_did_modal.rs b/src/tsp/create_did_modal.rs index 465fed95..828c6fec 100644 --- a/src/tsp/create_did_modal.rs +++ b/src/tsp/create_did_modal.rs @@ -94,7 +94,6 @@ live_design! { } did_webvh = { text: "WebVH" - animator: { disabled = { default: on } } } did_peer = { text: "Peer", @@ -359,13 +358,30 @@ impl WidgetMatchEvent for CreateDidModal { "" => did_server_input.empty_text(), non_empty => non_empty.to_string(), }; + + // Determine which DID type is selected from the radio buttons + // Since Web is set as default active in the UI, we check the others first + + let did_type = if let Some(widget_idex) = self.view.radio_button_set(ids_array!( + did_type_radio_buttons.did_webvh, + did_type_radio_buttons.did_peer, + )).selected(cx, actions) { + if widget_idex == 0 { + tsp::DidType::WebVh + } else { + tsp::DidType::Peer + } + } else { + tsp::DidType::Web + }; // Submit the identity creation request to the TSP async worker thread. tsp::submit_tsp_request(tsp::TspRequest::CreateDid { username: username.to_string(), alias, server, - did_server + did_server, + did_type, }); self.state = CreateDidModalState::WaitingForIdentityCreation; diff --git a/src/tsp/mod.rs b/src/tsp/mod.rs index 57ec160f..2f99de6a 100644 --- a/src/tsp/mod.rs +++ b/src/tsp/mod.rs @@ -33,6 +33,18 @@ pub fn live_design(cx: &mut Cx) { tsp_settings_screen::live_design(cx); } +/// Supported DID types for creation. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum DidType { + /// Standard DID Web type + #[default] + Web, + /// DID Web with Verifiable History + WebVh, + /// Peer DID type + Peer, +} + /// The sender used by [`submit_tsp_request()`] to send TSP requests to the async worker thread. /// Currently there is only one, but it can be cloned if we need more concurrent senders. static TSP_REQUEST_SENDER: OnceLock> = OnceLock::new(); @@ -576,6 +588,7 @@ pub enum TspRequest { alias: Option, server: String, did_server: String, + did_type: DidType, }, /// Request to re-publish/re-upload our own DID back up to the DID server. /// @@ -772,8 +785,8 @@ async fn async_tsp_worker( todo!("handle deleting a wallet"); } - TspRequest::CreateDid { username, alias, server, did_server } => { - log!("Received TspRequest::CreateDid(username: {username}, alias: {alias:?}, server: {server}, did_server: {did_server})"); + TspRequest::CreateDid { username, alias, server, did_server, did_type } => { + log!("Received TspRequest::CreateDid(username: {username}, alias: {alias:?}, server: {server}, did_server: {did_server}, did_type: {did_type:?})"); let client = get_reqwest_client(); Handle::current().spawn(async move { @@ -783,6 +796,7 @@ async fn async_tsp_worker( alias, server, did_server, + did_type, ).await; Cx::post_action(TspIdentityAction::DidCreationResult(result)); }); @@ -845,12 +859,17 @@ async fn create_did_and_add_to_wallet( alias: Option, server: String, did_server: String, + did_type: DidType, ) -> Result { let cw_db = tsp_state_ref().lock().unwrap() .current_wallet.as_ref() .map(|w| w.db.clone()) .ok_or_else(|| anyhow!("Please choose a default TSP wallet to hold the DID."))?; - let (did, private_vid, metadata) = create_did_web(&did_server, &server, &username, client).await?; + let (did, private_vid, metadata) = match did_type { + DidType::Web => create_did_web(&did_server, &server, &username, client).await?, + DidType::WebVh => create_did_webvh(&did_server, &server, &username, client).await?, + DidType::Peer => return Err(anyhow!("Peer DID type is not yet implemented")), + }; let new_vid = private_vid.identifier().to_string(); log!("Successfully created & published new DID: {did}.\n\ Adding private VID {new_vid} to current wallet...", @@ -954,6 +973,106 @@ async fn create_did_web( Ok((did, private_vid, metadata)) } +/// Creates a new WebVH DID on the given `did_server` and publishes it to the TSP `server`. +/// +/// This function does not modify or add anything to the current TSP wallet. +/// The caller must do that separately. +/// +/// Returns a tuple of the DID string, the private VID, and optional metadata. +async fn create_did_webvh( + did_server: &str, + server: &str, + username: &str, + client: &reqwest::Client, +) -> Result<(String, OwnedVid, Option), anyhow::Error> { + // The following code is based on the TSP SDK's CLI example for creating a WebVH DID. + let endpoint_url = format!("{}/endpoint/{}", did_server, username); + + let transport = Url::parse( + &format!("https://{}/endpoint/{}", server, username) + ).map_err(|e| anyhow!("Invalid transport URL: {e}"))?; + + // Create WebVH DID using TSP SDK + let (private_vid, history, update_kid, update_key) = + tsp_sdk::vid::did::webvh::create_webvh(&endpoint_url, transport).await + .map_err(|e| anyhow!("Failed to create WebVH DID: {e}"))?; + + // Store the update key in the current wallet + let cw_db = tsp_state_ref().lock().unwrap() + .current_wallet.as_ref() + .map(|w| w.db.clone()) + .ok_or_else(|| anyhow!("No current wallet available for storing update key"))?; + + cw_db.add_secret_key(update_kid, update_key) + .map_err(|e| anyhow!("Cannot store update key: {e}"))?; + + let did = private_vid.identifier().to_string(); + log!("created WebVH identity {}", did); + + // Publish the VID to the DID server + let response = client + .post(format!("https://{did_server}/add-vid")) + .json(&private_vid.vid()) + .send() + .await + .inspect(|r| log!("DID server responded with status code {}", r.status())) + .map_err(|e| anyhow!("Could not publish VID. The DID server responded with error: {e}"))?; + + let vid_result: Result = match response.status() { + r if r.is_success() => { + response.json().await + .map_err(|e| anyhow!("Could not decode response from DID server as a valid VID: {e}")) + } + r => { + let text = response.text().await.unwrap_or_else(|_| "[Unknown]".to_string()); + if r.as_u16() == 500 { + return Err(anyhow!( + "The DID server returned error code 500. The DID username may already exist, \ + or the server had another problem.\n\nResponse: \"{text}\"." + )); + } else { + return Err(anyhow!( + "The DID server returned error code {}.\n\nResponse: \"{text}\".", + r.as_u16() + )); + } + } + }; + + let _vid = vid_result?; + log!("published DID document at {}", + tsp_sdk::vid::did::get_resolve_url(&did)?.to_string() + ); + + // Publish the DID history to the DID server + let history_response = client + .post(format!("https://{did_server}/add-history/{}", did)) + .json(&history) + .send() + .await + .inspect(|r| log!("DID server responded with status code {}", r.status())) + .map_err(|e| anyhow!("Could not publish DID history. The DID server responded with error: {e}"))?; + + match history_response.status() { + r if r.is_success() => { + log!("published DID history"); + } + r => { + let text = history_response.text().await.unwrap_or_else(|_| "[Unknown]".to_string()); + return Err(anyhow!( + "Failed to publish DID history. The DID server returned error code {}.\n\nResponse: \"{text}\".", + r.as_u16() + )); + } + } + + let (_vid, metadata) = verify_vid(private_vid.identifier()) + .await + .map_err(|err| tsp_sdk::Error::Vid(VidError::InvalidVid(err.to_string())))?; + + Ok((did, private_vid, metadata)) +} + /// Stores the given private VID in the current default TSP wallet, /// and optionally establishes an alias for the given `did`.