From cbcc25a49fa6bbba414af19340639f7bd2b57e48 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sun, 5 Jan 2025 18:49:34 +0800 Subject: [PATCH] feat: poc works --- script-sign-rs/Cargo.lock | 284 ++++++++++++++++++++++++++++++++ script-sign-rs/Cargo.toml | 5 + script-sign-rs/src/ecdsautil.rs | 35 ++++ script-sign-rs/src/main.rs | 114 +++++++++++-- script-sign-rs/test.js | 4 +- 5 files changed, 428 insertions(+), 14 deletions(-) create mode 100644 script-sign-rs/src/ecdsautil.rs diff --git a/script-sign-rs/Cargo.lock b/script-sign-rs/Cargo.lock index 801ed21..832afeb 100644 --- a/script-sign-rs/Cargo.lock +++ b/script-sign-rs/Cargo.lock @@ -60,12 +60,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "2.6.0" @@ -120,6 +132,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cpufeatures" version = "0.2.16" @@ -129,6 +147,18 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -139,6 +169,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -146,7 +187,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -170,6 +213,51 @@ dependencies = [ "winapi", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -178,6 +266,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -191,18 +280,53 @@ dependencies = [ "wasi", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + [[package]] name = "lazy_static" version = "1.5.0" @@ -231,6 +355,58 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -249,6 +425,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -289,6 +474,16 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rust_util" version = "0.6.47" @@ -307,6 +502,12 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "script-sign-rs" version = "0.1.0" @@ -314,12 +515,63 @@ dependencies = [ "base64", "clap", "digest", + "ecdsa", "hex", + "p256", + "p384", "regex", "rust_util", + "serde", + "serde_json", "sha2", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -331,12 +583,38 @@ dependencies = [ "digest", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.95" @@ -513,3 +791,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/script-sign-rs/Cargo.toml b/script-sign-rs/Cargo.toml index 8e92a1f..67fb649 100644 --- a/script-sign-rs/Cargo.toml +++ b/script-sign-rs/Cargo.toml @@ -7,7 +7,12 @@ edition = "2021" base64 = "0.22.1" clap = "4.5.23" digest = "0.10.7" +ecdsa = "0.16.9" hex = "0.4.3" +p256 = "0.13.2" +p384 = "0.13.0" regex = "1.11.1" rust_util = "0.6.47" +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.134" sha2 = "0.10.8" diff --git a/script-sign-rs/src/ecdsautil.rs b/script-sign-rs/src/ecdsautil.rs new file mode 100644 index 0000000..030dfef --- /dev/null +++ b/script-sign-rs/src/ecdsautil.rs @@ -0,0 +1,35 @@ +use ecdsa::VerifyingKey; +use p256::NistP256; +use p256::ecdsa::signature::hazmat::PrehashVerifier; +use p384::NistP384; +use ecdsa::Signature; +use rust_util::{opt_result, simple_error, XResult}; + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub enum EcdsaAlgorithm { + P256, + P384, +} + +macro_rules! ecdsa_verify_signature { + ($algo: tt, $pk_point: tt, $prehash: tt, $signature: tt) => ({ + let verifying_key: VerifyingKey<$algo> = opt_result!(VerifyingKey::<$algo>::from_sec1_bytes($pk_point), "Parse public key failed: {}"); + let sign = if let Ok(signature) = Signature::from_der($signature) { + signature + } else if let Ok(signature) = Signature::from_slice($signature) { + signature + } else { + return simple_error!("Parse signature failed: {}", hex::encode($signature)); + }; + opt_result!(verifying_key.verify_prehash($prehash, &sign), "Verify signature failed: {}"); + }) +} + +pub fn ecdsaverify(algo: EcdsaAlgorithm, pk_point: &[u8], prehash: &[u8], signature: &[u8]) -> XResult<()> { + match algo { + EcdsaAlgorithm::P256 => ecdsa_verify_signature!(NistP256, pk_point, prehash, signature), + EcdsaAlgorithm::P384 => ecdsa_verify_signature!(NistP384, pk_point, prehash, signature), + } + Ok(()) +} \ No newline at end of file diff --git a/script-sign-rs/src/main.rs b/script-sign-rs/src/main.rs index e7c6453..bedb01c 100644 --- a/script-sign-rs/src/main.rs +++ b/script-sign-rs/src/main.rs @@ -1,11 +1,18 @@ -use base64::engine::general_purpose as general_base64; +use crate::ecdsautil::EcdsaAlgorithm; +use base64::engine::general_purpose::STANDARD as standard_base64; use base64::Engine; use digest::Digest; use regex::Regex; -use rust_util::{opt_result, opt_value_result, simple_error, XResult}; +use rust_util::{ + debugging, opt_result, opt_value_result, simple_error, util_cmd, XResult, +}; +use serde::{Deserialize, Serialize}; use sha2::Sha256; +use std::collections::HashMap; use std::fs; +mod ecdsautil; + const SIGNATURE_PREFIX: &str = "// @SCRIPT-SIGNATURE-"; fn main() { @@ -15,19 +22,16 @@ fn main() { let mut script = Script::parse(&content).unwrap(); let digest_sha256 = script.normalize_content_lines_and_sha256(); - println!("{}", general_base64::STANDARD.encode(&digest_sha256)); + println!("{}", standard_base64.encode(&digest_sha256)); println!("{}", hex::encode(&digest_sha256)); if script.signature.is_some() { println!("File is signed."); + println!("Verify: {:?}", script.verify(&KeyMap::default().unwrap())); return; } - script.signature = Some(ScriptSignature{ - key_id: "yk-r1".to_string(), - algorithm: ScriptSignatureAlgorithm::ES256, - signature: general_base64::STANDARD.decode("MEUCIQCRbIPTC7jTMiRYSLF+u4gt7JDcDO4m4Y5pqbk3NjTvogIgKYgbkeGXl6XTiChN7dWJZ04GmLeEnKF9xnnPN3o7W8w=").unwrap(), - }); + script.sign().unwrap(); fs::write(test_file, script.as_string()).unwrap(); // println!("{}", script.as_string()); @@ -54,6 +58,36 @@ struct Script { signature: Option, } +#[derive(Debug, Serialize, Deserialize)] +struct KeyMap { + key_map: HashMap, +} + +#[derive(Debug, Deserialize)] +struct CardEcSignResult { + pub algorithm: String, + pub hash_hex: String, + pub signed_data_base64: String, + pub signed_data_hex: String, + pub slot: String, +} + +impl KeyMap { + fn default() -> XResult { + let mut key_map = HashMap::new(); + key_map.insert( + "yk-r1".to_string(), + "04dd3eebd906c9cf00b08ec29f7ed61804d1cc1d1352d9257b628191e08fc3717c4fae3298cd5c4829cec8bf3a946e7db60b7857e1287f6a0bae6b3f2342f007d0".to_string() + ); + + Ok(KeyMap { key_map }) + } + + fn find(&self, key_id: &str) -> Option<&String> { + self.key_map.get(key_id) + } +} + impl ScriptSignatureAlgorithm { fn try_from(algo: &str) -> XResult { let upper_algo = algo.to_uppercase(); @@ -79,7 +113,7 @@ impl ScriptSignatureAlgorithm { impl ScriptSignature { fn parse(script_signature_line: &str) -> XResult { // e.g. // @SCRIPT-SIGNATURE-V1: .. - let script_signature_v1_regex = Regex::new(r##"^//\s*@SCRIPT-SIGNATURE-V1:\s*([a-zA-Z0-9-_]+)\.([a-zA-Z0-9]+)\.([a-zA-Z0-9-+_/=]+)$"##).unwrap(); + let script_signature_v1_regex = Regex::new(r##"^\s*//\s*@SCRIPT-SIGNATURE-V1:\s*([a-zA-Z0-9-_]+)\.([a-zA-Z0-9]+)\.([a-zA-Z0-9-+_/=]+)\s*$"##).unwrap(); let script_signature_v1_captures = opt_value_result!( script_signature_v1_regex.captures(script_signature_line), "Parse script signature failed: {}", @@ -88,7 +122,7 @@ impl ScriptSignature { let (_, [key_id, algorithm, signature]) = script_signature_v1_captures.extract(); let signature = opt_result!( - general_base64::STANDARD.decode(signature), + standard_base64.decode(signature), "Parse script signature failed, decode signature failed: {}" ); Ok(ScriptSignature { @@ -106,7 +140,7 @@ impl ScriptSignature { s.push('.'); s.push_str(self.algorithm.as_str()); s.push('.'); - s.push_str(&general_base64::STANDARD.encode(&self.signature)); + s.push_str(&standard_base64.encode(&self.signature)); s } } @@ -142,10 +176,64 @@ impl Script { fn as_string(&self) -> String { match &self.signature { None => self.content_lines.join("\n"), - Some(signature) => self.content_lines.join("\n") + "\n" + &signature.as_string(), + Some(signature) => { + self.content_lines.join("\n") + "\n\n" + &signature.as_string() + "\n" + } } } + fn verify(&self, key_map: &KeyMap) -> XResult { + let signature = match &self.signature { + None => return simple_error!("Script is not signed."), + Some(signature) => signature, + }; + let key = match key_map.find(&signature.key_id) { + None => return simple_error!("Sign key id: {} not found", &signature.key_id), + Some(key) => key, + }; + let key_bytes = hex::decode(key)?; + let digest_sha256 = self.normalize_content_lines_and_sha256(); + match signature.algorithm { + ScriptSignatureAlgorithm::ES256 => { + match ecdsautil::ecdsaverify( + EcdsaAlgorithm::P256, + &key_bytes, + &digest_sha256, + &signature.signature, + ) { + Ok(_) => Ok(true), + Err(e) => { + debugging!("Verify ecdsa signature failed: {}", e); + Ok(false) + } + } + } + _ => simple_error!("Not supported algorithm: {:?}", signature.algorithm), + } + } + + fn sign(&mut self) -> XResult<()> { + let digest_sha256_hex = hex::encode(&self.normalize_content_lines_and_sha256()); + let output = util_cmd::run_command_or_exit( + "card-cli", + &["piv-ecsign", "--json", "-s", "r1", "-x", &digest_sha256_hex], + ); + let ecsign_result: CardEcSignResult = opt_result!( + serde_json::from_slice(&output.stdout), + "Parse card piv-ecsign failed: {}" + ); + if ecsign_result.algorithm == "ecdsa_p256_with_sha256" { + self.signature = Some(ScriptSignature { + key_id: "yk-r1".to_string(), + algorithm: ScriptSignatureAlgorithm::ES256, + signature: hex::decode(&ecsign_result.signed_data_hex)?, + }); + } else { + return simple_error!("Not supported algorithm: {}", ecsign_result.algorithm); + } + Ok(()) + } + fn normalize_content_lines(&self) -> Vec { let mut normalized_content_lines = Vec::with_capacity(self.content_lines.len()); for ln in &self.content_lines { @@ -194,7 +282,7 @@ fn test_script_parse_02() { assert_eq!("", script.content_lines[1]); assert_eq!( "Gfvd4W/64/WaSMaztA1nlq81vBLam8gcJokoOlGuGpc=", - general_base64::STANDARD.encode(&script.normalize_content_lines_and_sha256()) + standard_base64.encode(&script.normalize_content_lines_and_sha256()) ); assert!(script.signature.is_some()); let s = script.signature.unwrap(); diff --git a/script-sign-rs/test.js b/script-sign-rs/test.js index aa424fc..6b69a81 100644 --- a/script-sign-rs/test.js +++ b/script-sign-rs/test.js @@ -1,2 +1,4 @@ console.log('hello world'); -// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.MEUCIQCRbIPTC7jTMiRYSLF+u4gt7JDcDO4m4Y5pqbk3NjTvogIgKYgbkeGXl6XTiChN7dWJZ04GmLeEnKF9xnnPN3o7W8w= \ No newline at end of file + + +// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.MEYCIQDA5+Em6mgx8F/D0Mu1JxhXtIs3nAQ/PCl7QZwO+6v+gwIhAK+mDCjGXqpKfhrZ/qvDKeHUgSgixpEKvkXPqLqpEtiQ