diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/external-command-rs.iml b/.idea/external-command-rs.iml
new file mode 100644
index 0000000..7c12fe5
--- /dev/null
+++ b/.idea/external-command-rs.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..de2db02
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..9ea7fa5
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "external-command-rs"
+version = "0.1.0"
+edition = "2024"
+authors = ["Hatter Jiang"]
+repository = "https://git.hatter.ink/hatter/external-command-rs"
+description = "External tool in Rust"
+license = "MIT OR Apache-2.0"
+keywords = ["crypto"]
+categories = ["cryptography"]
+
+[dependencies]
+hex = "0.4.3"
+base64 = "0.22.1"
+rust_util = "0.6.47"
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = "1.0.140"
+
diff --git a/README.md b/README.md
index 2c88ca4..d22b87a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,4 @@
# external-command-rs
+Specification: https://openwebstandard.org/rfc1
+
diff --git a/examples/simple_test.rs b/examples/simple_test.rs
new file mode 100644
index 0000000..1810512
--- /dev/null
+++ b/examples/simple_test.rs
@@ -0,0 +1,17 @@
+use external_command_rs::{external_public_key, external_sign, external_spec};
+
+fn main() {
+ let cmd = "/Users/hatterjiang/Code/hattergit/external-signer-pkcs11/external-signer-pkcs11";
+ let spec = external_spec(cmd).unwrap();
+ println!("{:#?}", spec);
+
+ let parameter = "ewogICJsaWJyYXJ5IjogIi91c3IvbG9jYWwvbGliL2xpYnlrY3MxMS5keWxpYiIsCiAgInRva\
+2VuX2xhYmVsIjogIll1YmlLZXkgUElWICM1MDEwMjIwIiwKICAicGluIjogIiIsCiAgImtleV9sYWJlbCI6ICJQcml2YXRlIGtle\
+SBmb3IgUElWIEF1dGhlbnRpY2F0aW9uIgp9Cg==";
+
+ let public_key = external_public_key(cmd, parameter).unwrap();
+ println!("{}", hex::encode(public_key));
+
+ let signature = external_sign(cmd, parameter, "ES384", "hello world".as_bytes()).unwrap();
+ println!("{}", hex::encode(signature));
+}
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..fa5d40c
--- /dev/null
+++ b/justfile
@@ -0,0 +1,7 @@
+_:
+ @just --list
+
+# publish
+publish:
+ cargo publish --registry crates-io
+
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..5fa9347
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,183 @@
+use base64::Engine;
+use base64::engine::general_purpose::STANDARD;
+use rust_util::{XResult, debugging, opt_result, simple_error};
+use serde::{Deserialize, de};
+use serde_json::Value;
+use std::process::{Command, Output};
+
+#[derive(Debug, Deserialize)]
+struct ErrorResult {
+ #[allow(dead_code)]
+ pub success: bool,
+ pub error: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ExternalSpecResult {
+ #[allow(dead_code)]
+ pub success: bool,
+ pub agent: String,
+ pub specification: String,
+ pub commands: Vec,
+}
+
+#[derive(Debug, Deserialize)]
+struct ExternalPublicKeyResult {
+ #[allow(dead_code)]
+ pub success: bool,
+ pub public_key_base64: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct ExternalSignResult {
+ #[allow(dead_code)]
+ pub success: bool,
+ pub signature_base64: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct ExternalDhResult {
+ #[allow(dead_code)]
+ pub success: bool,
+ pub shared_secret_hex: String,
+}
+
+pub fn external_spec(external_command: &str) -> XResult {
+ let mut cmd = Command::new(external_command);
+ cmd.arg("external_spec");
+
+ let cmd_stdout = run_command_stdout(cmd)?;
+ if is_success(&cmd_stdout)? {
+ let external_spec: ExternalSpecResult = from_str(&cmd_stdout)?;
+ Ok(external_spec)
+ } else {
+ let error_result: ErrorResult = from_str(&cmd_stdout)?;
+ simple_error!("{}", error_result.error)
+ }
+}
+
+pub fn external_public_key(external_command: &str, parameter: &str) -> XResult> {
+ let mut cmd = Command::new(external_command);
+ cmd.arg("external_public_key");
+ cmd.arg("--parameter");
+ cmd.arg(parameter);
+
+ let cmd_stdout = run_command_stdout(cmd)?;
+ if is_success(&cmd_stdout)? {
+ let external_public_key: ExternalPublicKeyResult = from_str(&cmd_stdout)?;
+ Ok(STANDARD.decode(&external_public_key.public_key_base64)?)
+ } else {
+ let error_result: ErrorResult = from_str(&cmd_stdout)?;
+ simple_error!("{}", error_result.error)
+ }
+}
+
+pub fn external_sign(
+ external_command: &str,
+ parameter: &str,
+ alg: &str,
+ content: &[u8],
+) -> XResult> {
+ let mut cmd = Command::new(external_command);
+ cmd.arg("external_sign");
+ cmd.arg("--parameter");
+ cmd.arg(parameter);
+ cmd.arg("--alg");
+ cmd.arg(alg);
+ cmd.arg("--message-base64");
+ cmd.arg(STANDARD.encode(content));
+
+ let cmd_stdout = run_command_stdout(cmd)?;
+ parse_sign_result(&cmd_stdout)
+}
+
+pub fn external_ecdh(
+ external_command: &str,
+ parameter: &str,
+ ephemera_public_key: &[u8],
+) -> XResult> {
+ let mut cmd = Command::new(external_command);
+ cmd.arg("external_ecdh");
+ cmd.arg("--parameter");
+ cmd.arg(parameter);
+ cmd.arg("--epk");
+ cmd.arg(STANDARD.encode(ephemera_public_key));
+
+ let cmd_stdout = run_command_stdout(cmd)?;
+ parse_ecdh_result(&cmd_stdout)
+}
+
+fn run_command_stdout(cmd: Command) -> XResult {
+ let output = run_command(cmd)?;
+ let stdout_text = opt_result!(String::from_utf8(output.stdout), "Parse stdout failed:{}");
+ Ok(stdout_text.trim().to_string())
+}
+
+fn run_command(mut cmd: Command) -> XResult