feat: script-sign.rs now works

This commit is contained in:
2025-01-05 21:54:26 +08:00
parent 7ed829e794
commit c1bc33ef2d
9 changed files with 463 additions and 80 deletions

View File

@@ -1,35 +0,0 @@
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(())
}

272
script-sign-rs/src/main.rs Normal file → Executable file
View File

@@ -1,38 +1,163 @@
use crate::ecdsautil::EcdsaAlgorithm;
#!/usr/bin/env runrs
//! ```cargo
//! [dependencies]
//! base64 = "0.22.1"
//! clap = { version = "4.5.23", features = ["derive"] }
//! 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"
//! simpledateformat = "0.1.4"
//! ```
use base64::engine::general_purpose::STANDARD as standard_base64;
use base64::Engine;
use clap::Parser;
use digest::Digest;
use ecdsa::Signature;
use ecdsa::VerifyingKey;
use p256::ecdsa::signature::hazmat::PrehashVerifier;
use p256::NistP256;
use p384::NistP384;
use regex::Regex;
use rust_util::{debugging, opt_result, opt_value_result, simple_error, util_cmd, XResult};
use rust_util::{
debugging, failure, failure_and_exit, information, opt_result, opt_value_result, simple_error,
success, util_cmd, warning, XResult,
};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::collections::HashMap;
use std::fs;
mod ecdsautil;
use std::path::PathBuf;
const SIGNATURE_PREFIX: &str = "// @SCRIPT-SIGNATURE-";
/// Script signing tool
#[derive(Parser, Debug)]
#[command(version, about, long_about = None, bin_name = "script-sign.rs")]
struct Args {
/// Sign script
#[arg(long)]
sign: bool,
/// Verify script
#[arg(long)]
verify: bool,
/// Force sign script
#[arg(long)]
force: bool,
/// Script file path
scripts: Vec<PathBuf>,
}
fn main() {
let test_file = "test.js";
let content = fs::read_to_string(test_file).unwrap();
let mut script = Script::parse(&content).unwrap();
let digest_sha256 = script.normalize_content_lines_and_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;
let args = Args::parse();
if !args.verify && !args.sign {
failure_and_exit!("Argument --verify or --sign must assigned.");
}
script.sign().unwrap();
fs::write(test_file, script.as_string()).unwrap();
let key_map = KeyMap::default().unwrap();
let total_scripts = args.scripts.len();
for (i, script_path) in args.scripts.iter().enumerate() {
information!(
"Processing {}/{}: {}",
(i + 1),
total_scripts,
script_path.display()
);
// println!("{}", script.as_string());
if !script_path.is_file() {
warning!("Not a file: {}", script_path.display());
continue;
}
let script_content = match fs::read_to_string(script_path) {
Ok(script_content) => script_content,
Err(e) => {
warning!("Read script: {} failed: {}", script_path.display(), e);
continue;
}
};
let mut script = match Script::parse(&script_content) {
Ok(script) => script,
Err(e) => {
warning!("Read script: {} failed: {}", script_path.display(), e);
continue;
}
};
if args.verify {
// VERIFY SCRIPT
if let Some(signature) = &script.signature {
match script.verify(&key_map) {
Ok(true) => {
success!(
"Verify script success: {}, key ID: {}, sign date: {}",
script_path.display(),
signature.key_id,
signature.time
);
}
Ok(false) => {
failure!("Verify script failed: {}", script_path.display());
}
Err(e) => {
warning!("Verify script: {} failed: {}", script_path.display(), e);
}
}
} else {
warning!("Script is not signed: {}", script_path.display());
}
} else if args.sign {
// SIGN SCRIPT
let mut continue_sign = false;
if script.signature.is_some() {
match script.verify(&key_map) {
Ok(true) => {
if args.force {
continue_sign = true;
} else {
warning!("Script is singed, force sign script need --force flag.");
}
}
Ok(false) => {
continue_sign = true;
}
Err(e) => {
warning!("Verify script: {} failed: {}", script_path.display(), e);
}
}
} else {
continue_sign = true;
}
if continue_sign {
information!("Prepare sign script: {}", script_path.display());
match script.sign() {
Ok(_) => match fs::write(script_path, &script.as_string()) {
Ok(_) => {
success!("Sign script success: {}", script_path.display());
}
Err(e) => {
failure!("Sign script {} failed: {}", script_path.display(), e);
}
},
Err(e) => {
failure!("Sign script {} failed: {}", script_path.display(), e);
}
}
}
} else {
// SHOULD REACH HERE
failure_and_exit!("Argument --verify or --sign flag must assigned.");
}
}
}
#[derive(Debug, Eq, PartialEq)]
@@ -47,6 +172,7 @@ enum ScriptSignatureAlgorithm {
struct ScriptSignature {
key_id: String,
algorithm: ScriptSignatureAlgorithm,
time: String,
signature: Vec<u8>,
}
@@ -61,6 +187,7 @@ struct KeyMap {
key_map: HashMap<String, String>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct CardEcSignResult {
pub algorithm: String,
@@ -72,12 +199,12 @@ struct CardEcSignResult {
impl KeyMap {
fn default() -> XResult<Self> {
let mut key_map = HashMap::new();
key_map.insert(
"yk-r1".to_string(),
"04dd3eebd906c9cf00b08ec29f7ed61804d1cc1d1352d9257b628191e08fc3717c4fae3298cd5c4829cec8bf3a946e7db60b7857e1287f6a0bae6b3f2342f007d0".to_string()
);
let signing_keys = r##"
{
"yk-r1": "04dd3eebd906c9cf00b08ec29f7ed61804d1cc1d1352d9257b628191e08fc3717c4fae3298cd5c4829cec8bf3a946e7db60b7857e1287f6a0bae6b3f2342f007d0"
}
"##;
let key_map: HashMap<String, String> = serde_json::from_str(signing_keys).unwrap();
Ok(KeyMap { key_map })
}
@@ -110,14 +237,15 @@ impl ScriptSignatureAlgorithm {
impl ScriptSignature {
fn parse(script_signature_line: &str) -> XResult<ScriptSignature> {
// e.g. // @SCRIPT-SIGNATURE-V1: <key-id>.<algotirhm>.<signature-value-in-base64>
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();
// e.g. // @SCRIPT-SIGNATURE-V1: <key-id>.<algotirhm>.<time>.<signature-value-in-base64>
let script_signature_v1_regex = Regex::new(
r##"^\s*//\s*@SCRIPT-SIGNATURE-V1:\s*([a-zA-Z0-9-_]+)\.([a-zA-Z0-9]+)\.([0-9a-zA-Z\-+:]+)\.([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: {}",
script_signature_line
);
let (_, [key_id, algorithm, signature]) = script_signature_v1_captures.extract();
let (_, [key_id, algorithm, time, signature]) = script_signature_v1_captures.extract();
let signature = opt_result!(
standard_base64.decode(signature),
@@ -126,6 +254,7 @@ impl ScriptSignature {
Ok(ScriptSignature {
key_id: key_id.to_string(),
algorithm: ScriptSignatureAlgorithm::try_from(algorithm)?,
time: time.to_string(),
signature,
})
}
@@ -138,6 +267,8 @@ impl ScriptSignature {
s.push('.');
s.push_str(self.algorithm.as_str());
s.push('.');
s.push_str(&self.time);
s.push('.');
s.push_str(&standard_base64.encode(&self.signature));
s
}
@@ -200,10 +331,10 @@ impl Script {
Some(key) => key,
};
let key_bytes = hex::decode(key)?;
let digest_sha256 = self.normalize_content_lines_and_sha256();
let digest_sha256 = self.normalize_content_lines_and_sha256(&signature.time);
match signature.algorithm {
ScriptSignatureAlgorithm::ES256 => {
match ecdsautil::ecdsaverify(
match ecdsaverify(
EcdsaAlgorithm::P256,
&key_bytes,
&digest_sha256,
@@ -221,7 +352,8 @@ impl Script {
}
fn sign(&mut self) -> XResult<()> {
let digest_sha256_hex = hex::encode(&self.normalize_content_lines_and_sha256());
let (time, digest_sha256) = self.normalize_content_lines_and_sha256_with_current_time();
let digest_sha256_hex = hex::encode(&digest_sha256);
let output = util_cmd::run_command_or_exit(
"card-cli",
&["piv-ecsign", "--json", "-s", "r1", "-x", &digest_sha256_hex],
@@ -234,6 +366,7 @@ impl Script {
self.signature = Some(ScriptSignature {
key_id: "yk-r1".to_string(),
algorithm: ScriptSignatureAlgorithm::ES256,
time,
signature: hex::decode(&ecsign_result.signed_data_hex)?,
});
} else {
@@ -253,21 +386,81 @@ impl Script {
normalized_content_lines
}
fn normalize_content_lines_and_sha256(&self) -> Vec<u8> {
fn normalize_content_lines_and_sha256_with_current_time(&self) -> (String, Vec<u8>) {
let current_time = current_time();
(
current_time.clone(),
self.normalize_content_lines_and_sha256(&current_time),
)
}
fn normalize_content_lines_and_sha256(&self, current_time: &str) -> Vec<u8> {
let normalized_content_lines = self.normalize_content_lines();
let joined_normalized_content_lines = normalized_content_lines.join("\n");
let mut hasher = Sha256::new();
hasher.update(&joined_normalized_content_lines);
hasher.update(current_time.as_bytes());
hasher.update(joined_normalized_content_lines.as_bytes());
hasher.finalize().to_vec()
}
}
fn current_time() -> String {
simpledateformat::fmt("yyyyMMdd'T'HHmmssz")
.unwrap()
.format_local_now()
}
#[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(())
}
#[test]
fn test_script_signature_parse() {
let v1 = "// @SCRIPT-SIGNATURE-V1: key-id.RS256.aGVsbG93b3JsZA==";
let v1 = "// @SCRIPT-SIGNATURE-V1: key-id.RS256.2025-01-05T20:57:14+08:00.aGVsbG93b3JsZA==";
let s = ScriptSignature::parse(v1).unwrap();
assert_eq!("key-id", s.key_id);
assert_eq!(ScriptSignatureAlgorithm::RS256, s.algorithm);
assert_eq!("2025-01-05T20:57:14+08:00", s.time);
assert_eq!(b"helloworld".to_vec(), s.signature);
assert_eq!(v1, s.as_string());
@@ -283,18 +476,21 @@ fn test_script_parse_01() {
#[test]
fn test_script_parse_02() {
let script =
Script::parse("test script\n\n// @SCRIPT-SIGNATURE-V1: key-id.RS256.aGVsbG93b3JsZA==\n")
Script::parse("test script\n\n// @SCRIPT-SIGNATURE-V1: key-id.RS256.2025-01-05T20:57:14+08:00.aGVsbG93b3JsZA==\n")
.unwrap();
assert_eq!(2, script.content_lines.len());
assert_eq!("test script", script.content_lines[0]);
assert_eq!("", script.content_lines[1]);
let current_time = "2025-01-05T20:57:14+08:00";
let digest_sha256 = script.normalize_content_lines_and_sha256(&current_time);
assert_eq!(
"Gfvd4W/64/WaSMaztA1nlq81vBLam8gcJokoOlGuGpc=",
standard_base64.encode(&script.normalize_content_lines_and_sha256())
"sybQ8O5TgRlkQ0i8pNIA6huHvAd5XbVZF+U60WMrdco=",
standard_base64.encode(&digest_sha256)
);
assert!(script.signature.is_some());
let s = script.signature.unwrap();
assert_eq!("key-id", s.key_id);
assert_eq!(ScriptSignatureAlgorithm::RS256, s.algorithm);
assert_eq!("2025-01-05T20:57:14+08:00", s.time);
assert_eq!(b"helloworld".to_vec(), s.signature);
}