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 { + debugging!("Run command: {:?}", cmd); + let output = cmd.output(); + match output { + Err(e) => simple_error!("Run command failed: {:?}", e), + Ok(output) => { + debugging!("Output: {:?}", output); + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + simple_error!( + "Run command not success: {:?}\n - stdout: {}\n - stderr: {}", + output.status.code(), + stdout, + stderr + ) + } else { + Ok(output) + } + } + } +} + +fn parse_sign_result(stdout: &str) -> XResult> { + if is_success(stdout)? { + let sign_result: ExternalSignResult = from_str(stdout)?; + Ok(STANDARD.decode(&sign_result.signature_base64)?) + } else { + let error_result: ErrorResult = from_str(stdout)?; + simple_error!("{}", error_result.error) + } +} + +fn parse_ecdh_result(stdout: &str) -> XResult> { + if is_success(stdout)? { + let dh_result: ExternalDhResult = from_str(stdout)?; + Ok(hex::decode(&dh_result.shared_secret_hex)?) + } else { + let error_result: ErrorResult = from_str(stdout)?; + simple_error!("{}", error_result.error) + } +} + +pub fn from_str<'a, T>(s: &'a str) -> XResult +where + T: de::Deserialize<'a>, +{ + match serde_json::from_str(s) { + Ok(result) => Ok(result), + Err(e) => simple_error!("Parse JSON: {}, error: {}", s, e), + } +} + +fn is_success(cmd_stdout: &str) -> XResult { + let val = opt_result!( + serde_json::from_str::(cmd_stdout), + "Parse result: {}, failed: {}", + cmd_stdout + ); + if let Value::Object(map) = val { + if let Some(success_value) = map.get("success") { + if let Value::Bool(result) = success_value { + return Ok(*result); + } + } + } + simple_error!("Bad result: {}", cmd_stdout) +}