diff --git a/Cargo.toml b/Cargo.toml index ae49f7d..a2fa79b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quorum-vault-client" -version = "0.2.1" +version = "0.3.0" edition = "2021" repository = "https://github.com/MaximFischuk/quorum-vault-client" documentation = "https://docs.rs/quorum-vault-client" @@ -25,3 +25,5 @@ base64 = "0.21.0" [dev-dependencies] tokio = { version = "1.20.1", features = ["full"] } wiremock = "0.5.17" +secp256k1 = "0.26.0" +hex = "0.4.3" diff --git a/examples/ethereum_wallet.rs b/examples/ethereum_wallet.rs index c73af55..7a9788b 100644 --- a/examples/ethereum_wallet.rs +++ b/examples/ethereum_wallet.rs @@ -6,7 +6,7 @@ async fn main() { let client = VaultClient::new( VaultClientSettingsBuilder::default() .address("http://127.0.0.1:8200") - .token("s.NgpQWnkfYEAxVPC83Bxfa7cy") + .token("root") .build() .unwrap(), ) diff --git a/examples/keys.rs b/examples/keys.rs index fab7b39..bc0b632 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -7,7 +7,7 @@ async fn main() { let client = VaultClient::new( VaultClientSettingsBuilder::default() .address("http://127.0.0.1:8200") - .token("s.NgpQWnkfYEAxVPC83Bxfa7cy") + .token("root") .build() .unwrap(), ) diff --git a/examples/zksnarks.rs b/examples/zksnarks.rs new file mode 100644 index 0000000..4292ac6 --- /dev/null +++ b/examples/zksnarks.rs @@ -0,0 +1,44 @@ +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; + +#[tokio::main] +async fn main() { + // Create a client + let client = VaultClient::new( + VaultClientSettingsBuilder::default() + .address("http://localhost:8200") + .token("root") + .build() + .unwrap(), + ) + .unwrap(); + + // Create a new account + let account = quorum_vault_client::api::create_zksnarks_account(&client, "quorum") + .await + .unwrap(); + println!("account: {:?}", account); + + // Read the account + let account = + quorum_vault_client::api::read_zksnarks_account(&client, "quorum", &account.public_key) + .await + .unwrap(); + println!("account: {:?}", account); + + // List the accounts + let accounts = quorum_vault_client::api::list_zksnarks_accounts(&client, "quorum") + .await + .unwrap(); + println!("accounts: {:?}", accounts); + + // Sign a message + let signature = quorum_vault_client::api::zksnarks_sign( + &client, + "quorum", + &account.public_key, + "some-data".as_bytes(), + ) + .await + .unwrap(); + println!("signature: {:?}", signature); +} diff --git a/src/api.rs b/src/api.rs index 2fe327b..4bb5d6a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -10,7 +10,15 @@ use crate::api::keys::requests::{ SignRequest, UpdateKeyTagsRequest, }; use crate::api::keys::responses::{KeyResponse, KeysResponse, SignResponse}; +use crate::api::zksnarks::requests::{ + CreateZkSnarksAccountRequest, ListZkSnarksAccountsRequest, ReadZkSnarksAccountRequest, + ZkSnarksSignRequest, +}; +use crate::api::zksnarks::responses::{ + ZkSnarksAccountResponse, ZkSnarksAccountsResponse, ZkSnarksSignResponse, +}; use crate::error::ClientError; +use crate::H256; use base64::Engine; use std::collections::HashMap; use vaultrs::client::Client; @@ -19,6 +27,7 @@ use web3::types::{Address, TransactionRequest}; pub mod ethereum; pub mod keys; +pub mod zksnarks; /// Key crypto algorithm. pub enum KeyCryptoAlgorithm { @@ -286,3 +295,94 @@ impl KeyCryptoAlgorithm { } } } + +/// Create a zk-SNARKs account (eddsa) +/// See [CreateZkSnarksAccountRequest] +pub async fn create_zksnarks_account( + client: &impl Client, + mount: &str, +) -> Result { + let request = CreateZkSnarksAccountRequest::builder() + .mount(mount) + .build() + .unwrap(); + vaultrs::api::exec_with_result(client, request) + .await + .map_err(Into::into) +} + +/// Read a zk-SNARKs account +/// See [ReadZkSnarksAccountRequest] +pub async fn read_zksnarks_account( + client: &impl Client, + mount: &str, + id: &str, +) -> Result { + let request = ReadZkSnarksAccountRequest::builder() + .mount(mount) + .id(id) + .build() + .unwrap(); + vaultrs::api::exec_with_result(client, request) + .await + .map_err(Into::into) +} + +/// List zk-SNARKs accounts +/// See [ListZkSnarksAccountsRequest] +pub async fn list_zksnarks_accounts( + client: &impl Client, + mount: &str, +) -> Result { + let request = ListZkSnarksAccountsRequest::builder() + .mount(mount) + .build() + .unwrap(); + vaultrs::api::exec_with_result(client, request) + .await + .map_err(Into::into) +} + +/// Sign a message with a zk-SNARKs account (eddsa) +/// See [ZkSnarksSignResponse] +pub async fn zksnarks_sign( + client: &impl Client, + mount: &str, + id: &str, + data: &[u8], +) -> Result { + let hash = keccak256(data); + let hex = H256::from(hash); + let encoded = format!("{:?}", hex); + let request = ZkSnarksSignRequest::builder() + .mount(mount) + .id(id) + .data(encoded) + .build() + .unwrap(); + vaultrs::api::exec_with_result(client, request) + .await + .map_err(Into::into) +} + +/// Sign a message with a zk-SNARKs account (eddsa) +/// Data must be a 32 byte hash +/// See [ZkSnarksSignResponse] +pub async fn zksnarks_sign_hash( + client: &impl Client, + mount: &str, + id: &str, + data: [u8; 32], +) -> Result { + let hex = H256::from(data); + let encoded = format!("{:?}", hex); + let request = ZkSnarksSignRequest::builder() + .mount(mount) + .id(id) + .data(encoded) + .build() + .unwrap(); + vaultrs::api::exec_with_result(client, request) + .await + .map_err(Into::into) +} diff --git a/src/api/zksnarks.rs b/src/api/zksnarks.rs new file mode 100644 index 0000000..116da0f --- /dev/null +++ b/src/api/zksnarks.rs @@ -0,0 +1,2 @@ +pub mod requests; +pub mod responses; diff --git a/src/api/zksnarks/requests.rs b/src/api/zksnarks/requests.rs new file mode 100644 index 0000000..2349d40 --- /dev/null +++ b/src/api/zksnarks/requests.rs @@ -0,0 +1,83 @@ +use crate::api::zksnarks::responses::*; +use rustify_derive::Endpoint; + +/// ## Create Zk-Snarks Account +/// This endpoint creates a new Zk-Snarks account. +/// +/// * Path: /zk-snarks/accounts +/// * Method: POST +/// * Response: [ZkSnarksAccountResponse] +#[derive(Builder, Debug, Endpoint)] +#[endpoint( + path = "{self.mount}/zk-snarks/accounts", + method = "POST", + response = "ZkSnarksAccountResponse", + builder = "true" +)] +#[builder(setter(into))] +pub struct CreateZkSnarksAccountRequest { + #[endpoint(skip)] + pub mount: String, +} + +/// ## Read Zk-Snarks Account +/// This endpoint gets a Zk-Snarks account by ID. +/// +/// * Path: /zk-snarks/accounts/{self.id} +/// * Method: GET +/// * Response: [ZkSnarksAccountResponse] +#[derive(Builder, Debug, Endpoint)] +#[endpoint( + path = "{self.mount}/zk-snarks/accounts/{self.id}", + method = "GET", + response = "ZkSnarksAccountResponse", + builder = "true" +)] +#[builder(setter(into))] +pub struct ReadZkSnarksAccountRequest { + #[endpoint(skip)] + pub mount: String, + pub id: String, +} + +/// ## List Zk-Snarks Accounts +/// This endpoint gets all Zk-Snarks accounts. +/// +/// * Path: /zk-snarks/accounts +/// * Method: GET +/// * Response: [ZkSnarksAccountsResponse] +#[derive(Builder, Debug, Endpoint)] +#[endpoint( + path = "{self.mount}/zk-snarks/accounts", + method = "GET", + response = "ZkSnarksAccountsResponse", + builder = "true" +)] +#[builder(setter(into))] +pub struct ListZkSnarksAccountsRequest { + #[endpoint(skip)] + pub mount: String, +} + +/// ## Sign data with Zk-Snarks Account +/// This endpoint signs data with a Zk-Snarks account. +/// +/// * Path: /zk-snarks/accounts/{self.id}/sign +/// * Method: POST +/// * Response: [ZkSnarksSignResponse] +#[derive(Builder, Debug, Endpoint)] +#[endpoint( + path = "{self.mount}/zk-snarks/accounts/{self.id}/sign", + method = "POST", + response = "ZkSnarksSignResponse", + builder = "true" +)] +#[builder(setter(into))] +pub struct ZkSnarksSignRequest { + #[endpoint(skip)] + pub mount: String, + #[endpoint(skip)] + pub id: String, + #[endpoint(body)] + pub data: String, +} diff --git a/src/api/zksnarks/responses.rs b/src/api/zksnarks/responses.rs new file mode 100644 index 0000000..eaf2bf2 --- /dev/null +++ b/src/api/zksnarks/responses.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +/// Response from executing [CreateZkSnarksAccountRequest][crate::api::zksnarks::requests::CreateZkSnarksAccountRequest] +#[derive(Deserialize, Debug, Serialize)] +pub struct ZkSnarksAccountResponse { + pub curve: String, + pub namespace: String, + pub public_key: String, + pub signing_algorithm: String, +} + +/// Response from executing [ListZkSnarksAccountsRequest][crate::api::zksnarks::requests::ListZkSnarksAccountsRequest] +#[derive(Deserialize, Debug, Serialize)] +pub struct ZkSnarksAccountsResponse { + pub keys: Vec, +} + +/// Response from executing [ZkSnarksSignRequest][crate::api::zksnarks::requests::ZkSnarksSignRequest] +#[derive(Deserialize, Debug, Serialize)] +pub struct ZkSnarksSignResponse { + pub signature: String, +} diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 6024f57..e66132d 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -1,2 +1,3 @@ mod ethereum; mod keys; +mod zksnarks; diff --git a/tests/api/zksnarks.rs b/tests/api/zksnarks.rs new file mode 100644 index 0000000..8d29ff6 --- /dev/null +++ b/tests/api/zksnarks.rs @@ -0,0 +1,248 @@ +use quorum_vault_client::api; +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; +use web3::signing::keccak256; +use wiremock::matchers::{body_json, method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +#[tokio::test] +async fn test_create_zksnarks_account() { + let mock = MockServer::start().await; + let vault_client = VaultClient::new( + VaultClientSettingsBuilder::default() + .address(mock.uri()) + .token("s.1234567890abcdef") + .build() + .unwrap(), + ) + .unwrap(); + + let response = serde_json::json!({ + "request_id": "e81af2c4-4e4c-a640-0f8f-99ce3f7d486a", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "curve": "babyjubjub", + "namespace": "", + "public_key": "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626", + "signing_algorithm": "eddsa" + }, + "wrap_info": null, + "warnings": null, + "auth": null + }); + + Mock::given(method("POST")) + .and(path("/v1/quorum/zk-snarks/accounts")) + .respond_with(ResponseTemplate::new(200).set_body_json(&response)) + .mount(&mock) + .await; + + let account = api::create_zksnarks_account(&vault_client, "quorum") + .await + .unwrap(); + + assert_eq!(account.curve, "babyjubjub"); + assert_eq!(account.namespace, ""); + assert_eq!( + account.public_key, + "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626" + ); + assert_eq!(account.signing_algorithm, "eddsa"); +} + +#[tokio::test] +async fn test_list_zksnarks_account() { + let mock = MockServer::start().await; + let vault_client = VaultClient::new( + VaultClientSettingsBuilder::default() + .address(mock.uri()) + .token("s.1234567890abcdef") + .build() + .unwrap(), + ) + .unwrap(); + + let response = serde_json::json!({ + "request_id": "2bd76aaf-405e-0330-2202-ea8361dae53a", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "keys": [ + "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626" + ] + }, + "wrap_info": null, + "warnings": null, + "auth": null + }); + + Mock::given(method("GET")) + .and(path("/v1/quorum/zk-snarks/accounts")) + .respond_with(ResponseTemplate::new(200).set_body_json(&response)) + .mount(&mock) + .await; + + let accounts = api::list_zksnarks_accounts(&vault_client, "quorum") + .await + .unwrap(); + + assert_eq!(accounts.keys.len(), 1); + assert_eq!( + accounts.keys[0], + "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626" + ); +} + +#[tokio::test] +async fn test_read_zksnarks_account() { + let mock = MockServer::start().await; + let vault_client = VaultClient::new( + VaultClientSettingsBuilder::default() + .address(mock.uri()) + .token("s.1234567890abcdef") + .build() + .unwrap(), + ) + .unwrap(); + + let response = serde_json::json!({ + "request_id": "e81af2c4-4e4c-a640-0f8f-99ce3f7d486a", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "curve": "babyjubjub", + "namespace": "", + "public_key": "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626", + "signing_algorithm": "eddsa" + }, + "wrap_info": null, + "warnings": null, + "auth": null + }); + + Mock::given(method("GET")) + .and(path("/v1/quorum/zk-snarks/accounts/0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626")) + .respond_with(ResponseTemplate::new(200).set_body_json(&response)) + .mount(&mock) + .await; + + let account = api::read_zksnarks_account( + &vault_client, + "quorum", + "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626", + ) + .await + .unwrap(); + + assert_eq!(account.curve, "babyjubjub"); + assert_eq!(account.namespace, ""); + assert_eq!( + account.public_key, + "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626" + ); + assert_eq!(account.signing_algorithm, "eddsa"); +} + +#[tokio::test] +async fn test_sign_message() { + let mock = MockServer::start().await; + let vault_client = VaultClient::new( + VaultClientSettingsBuilder::default() + .address(mock.uri()) + .token("s.1234567890abcdef") + .build() + .unwrap(), + ) + .unwrap(); + + let data = "Hello, world!"; + + let expected_request = serde_json::json!({ + "data": "0xb6e16d27ac5ab427a7f68900ac5559ce272dc6c37c82b3e052246c82244c50e4" + }); + + let response = serde_json::json!({ + "request_id": "e81af2c4-4e4c-a640-0f8f-99ce3f7d486a", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "signature": "0xac34541ff103beac043f2525d756c9a5f4288be4910c33f49c4fcea69b766ca6011b28e6ad62a1a3eddf2cc08ca7265553c175ffa60982616fa4facaf5f87d4a" + }, + "wrap_info": null, + "warnings": null, + "auth": null + }); + + Mock::given(method("POST")) + .and(path("/v1/quorum/zk-snarks/accounts/0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626/sign")) + .and(body_json(&expected_request)) + .respond_with(ResponseTemplate::new(200).set_body_json(&response)) + .mount(&mock) + .await; + + let signature = api::zksnarks_sign( + &vault_client, + "quorum", + "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626", + data.as_bytes(), + ) + .await + .unwrap(); + + assert_eq!(signature.signature, "0xac34541ff103beac043f2525d756c9a5f4288be4910c33f49c4fcea69b766ca6011b28e6ad62a1a3eddf2cc08ca7265553c175ffa60982616fa4facaf5f87d4a"); +} + +#[tokio::test] +async fn test_sign_hash() { + let mock = MockServer::start().await; + let vault_client = VaultClient::new( + VaultClientSettingsBuilder::default() + .address(mock.uri()) + .token("s.1234567890abcdef") + .build() + .unwrap(), + ) + .unwrap(); + + let data = "Hello, world!"; + let hash = keccak256(data.as_bytes()); + + let expected_request = serde_json::json!({ + "data": "0xb6e16d27ac5ab427a7f68900ac5559ce272dc6c37c82b3e052246c82244c50e4" + }); + + let response = serde_json::json!({ + "request_id": "e81af2c4-4e4c-a640-0f8f-99ce3f7d486a", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "signature": "0xac34541ff103beac043f2525d756c9a5f4288be4910c33f49c4fcea69b766ca6011b28e6ad62a1a3eddf2cc08ca7265553c175ffa60982616fa4facaf5f87d4a" + }, + "wrap_info": null, + "warnings": null, + "auth": null + }); + + Mock::given(method("POST")) + .and(path("/v1/quorum/zk-snarks/accounts/0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626/sign")) + .and(body_json(&expected_request)) + .respond_with(ResponseTemplate::new(200).set_body_json(&response)) + .mount(&mock) + .await; + + let signature = api::zksnarks_sign_hash( + &vault_client, + "quorum", + "0x7e8249b895434a1b02aade22033b887620ab5e756aa106d415ff33ace9048626", + hash, + ) + .await + .unwrap(); + + assert_eq!(signature.signature, "0xac34541ff103beac043f2525d756c9a5f4288be4910c33f49c4fcea69b766ca6011b28e6ad62a1a3eddf2cc08ca7265553c175ffa60982616fa4facaf5f87d4a"); +}