script_sign/
lib.rs

1mod keymap;
2mod sign;
3mod signature;
4mod util;
5
6pub use crate::keymap::KeyMap;
7use crate::sign::{ecdsaverify, EcdsaAlgorithm};
8use crate::signature::{
9    CardEcSignResult, ScriptSignature, ScriptSignatureAlgorithm, SIGNATURE_PREFIX,
10};
11use crate::util::current_time;
12use digest::Digest;
13use rust_util::{debugging, opt_result, simple_error, util_cmd, XResult};
14use sha2::Sha256;
15use std::fs;
16
17#[derive(Debug)]
18pub struct Script {
19    pub content_lines: Vec<String>,
20    pub signature: Option<ScriptSignature>,
21}
22
23impl Script {
24    pub fn verify_script_file_with_system_key_map(script_file: &str) -> XResult<bool> {
25        Self::verify_script_file(script_file, &KeyMap::system())
26    }
27
28    pub fn verify_script_file(script_file: &str, key_map: &KeyMap) -> XResult<bool> {
29        let script_content = opt_result!(
30            fs::read_to_string(script_file),
31            "Read script file: {script_file} failed: {}"
32        );
33        let script = opt_result!(
34            Script::parse(&script_content),
35            "Parse script file: {script_file} failed: {}"
36        );
37        match &script.signature {
38            None => Ok(false),
39            Some(_) => script.verify(key_map),
40        }
41    }
42
43    pub fn parse(script: &str) -> XResult<Script> {
44        let lines = script.lines().collect::<Vec<_>>();
45
46        let mut in_signature_section = false;
47        let mut signature_lines = vec![];
48        let mut content_lines = vec![];
49        let mut signature_line = String::new();
50
51        let mut push_signature_line = |signature_line: &mut String| {
52            if !signature_line.is_empty() {
53                signature_lines.push(signature_line.clone());
54                signature_line.clear();
55            }
56        };
57
58        for line in &lines {
59            if in_signature_section {
60                if line.starts_with(SIGNATURE_PREFIX) {
61                    push_signature_line(&mut signature_line);
62                    signature_line.push_str(line);
63                } else if line.starts_with("//") {
64                    signature_line.push_str(line.chars().skip(2).collect::<String>().trim());
65                } else if !line.trim().is_empty() {
66                    return simple_error!("Bad signature section line, find: '{line}'");
67                }
68            } else {
69                if line.starts_with(SIGNATURE_PREFIX) {
70                    in_signature_section = true;
71                    push_signature_line(&mut signature_line);
72                    signature_line.push_str(line);
73                } else {
74                    content_lines.push(line.to_string());
75                }
76            }
77        }
78        push_signature_line(&mut signature_line);
79
80        if signature_lines.len() > 1 {
81            return simple_error!(
82                "Found {} signatures, only supports one signature.",
83                signature_lines.len()
84            );
85        }
86
87        if signature_lines.is_empty() {
88            Ok(Script {
89                content_lines,
90                signature: None,
91            })
92        } else {
93            let script_signature = ScriptSignature::parse(&signature_lines[0])?;
94            Ok(Script {
95                content_lines,
96                signature: Some(script_signature),
97            })
98        }
99    }
100
101    pub fn as_string(&self) -> String {
102        let mut joined_content_liens = self.content_lines.join("\n");
103        match &self.signature {
104            None => joined_content_liens,
105            Some(signature) => {
106                if joined_content_liens.ends_with("\n\n") {
107                    // SKIP add \n
108                } else if joined_content_liens.ends_with("\n") {
109                    joined_content_liens.push('\n');
110                } else {
111                    joined_content_liens.push_str("\n\n");
112                }
113                let signature_lines = signature.as_string_lines_default_width();
114                for signature_line in &signature_lines {
115                    joined_content_liens.push_str(&signature_line);
116                    joined_content_liens.push('\n');
117                }
118                // joined_content_liens.push_str(&signature.as_string());
119                // joined_content_liens.push('\n');
120                joined_content_liens
121            }
122        }
123    }
124
125    pub fn has_signature(&self) -> bool {
126        self.signature.is_some()
127    }
128
129    pub fn verify(&self, key_map: &KeyMap) -> XResult<bool> {
130        let signature = match &self.signature {
131            None => return simple_error!("Script is not signed."),
132            Some(signature) => signature,
133        };
134        let key = match key_map.find(&signature.key_id) {
135            None => return simple_error!("Sign key id: {} not found", &signature.key_id),
136            Some(key) => key,
137        };
138        let key_bytes = hex::decode(&key.public_key_point_hex)?;
139        let digest_sha256 = self.normalize_content_lines_and_sha256(&signature.time);
140        match signature.algorithm {
141            ScriptSignatureAlgorithm::ES256 => {
142                match ecdsaverify(
143                    EcdsaAlgorithm::P256,
144                    &key_bytes,
145                    &digest_sha256,
146                    &signature.signature,
147                ) {
148                    Ok(_) => Ok(true),
149                    Err(e) => {
150                        debugging!("Verify ecdsa signature failed: {}", e);
151                        Ok(false)
152                    }
153                }
154            }
155            _ => simple_error!("Not supported algorithm: {:?}", signature.algorithm),
156        }
157    }
158
159    pub fn sign(&mut self) -> XResult<()> {
160        let (time, digest_sha256) = self.normalize_content_lines_and_sha256_with_current_time();
161        let digest_sha256_hex = hex::encode(&digest_sha256);
162        let output = util_cmd::run_command_or_exit(
163            "card-cli",
164            &["piv-ecsign", "--json", "-s", "r1", "-x", &digest_sha256_hex],
165        );
166        let ecsign_result: CardEcSignResult = opt_result!(
167            serde_json::from_slice(&output.stdout),
168            "Parse card piv-ecsign failed: {}"
169        );
170        if ecsign_result.algorithm == "ecdsa_p256_with_sha256" {
171            self.signature = Some(ScriptSignature {
172                key_id: "yk-r1".to_string(),
173                algorithm: ScriptSignatureAlgorithm::ES256,
174                time,
175                signature: hex::decode(&ecsign_result.signed_data_hex)?,
176            });
177        } else {
178            return simple_error!("Not supported algorithm: {}", ecsign_result.algorithm);
179        }
180        Ok(())
181    }
182
183    fn normalize_content_lines(&self) -> Vec<String> {
184        let mut normalized_content_lines = Vec::with_capacity(self.content_lines.len());
185        for ln in &self.content_lines {
186            let trimed_ln = ln.trim();
187            if !trimed_ln.is_empty() {
188                normalized_content_lines.push(trimed_ln.to_string());
189            }
190        }
191        normalized_content_lines
192    }
193
194    fn normalize_content_lines_and_sha256_with_current_time(&self) -> (String, Vec<u8>) {
195        let current_time = current_time();
196        (
197            current_time.clone(),
198            self.normalize_content_lines_and_sha256(&current_time),
199        )
200    }
201
202    fn normalize_content_lines_and_sha256(&self, current_time: &str) -> Vec<u8> {
203        let normalized_content_lines = self.normalize_content_lines();
204        let joined_normalized_content_lines = normalized_content_lines.join("\n");
205        let mut hasher = Sha256::new();
206        hasher.update(current_time.as_bytes());
207        hasher.update(joined_normalized_content_lines.as_bytes());
208        hasher.finalize().to_vec()
209    }
210}
211
212#[test]
213fn test_script_parse_01() {
214    let script = Script::parse("test script").unwrap();
215    assert_eq!(1, script.content_lines.len());
216    assert!(script.signature.is_none());
217}
218
219#[test]
220fn test_script_parse_02() {
221    use base64::engine::general_purpose::STANDARD as standard_base64;
222    use base64::Engine;
223    let script =
224        Script::parse("test script\n\n// @SCRIPT-SIGNATURE-V1: key-id.RS256.2025-01-05T20:57:14+08:00.aGVsbG93b3JsZA==\n")
225            .unwrap();
226    assert_eq!(2, script.content_lines.len());
227    assert_eq!("test script", script.content_lines[0]);
228    assert_eq!("", script.content_lines[1]);
229    let current_time = "2025-01-05T20:57:14+08:00";
230    let digest_sha256 = script.normalize_content_lines_and_sha256(&current_time);
231    assert_eq!(
232        "sybQ8O5TgRlkQ0i8pNIA6huHvAd5XbVZF+U60WMrdco=",
233        standard_base64.encode(&digest_sha256)
234    );
235    assert!(script.signature.is_some());
236    let s = script.signature.unwrap();
237    assert_eq!("key-id", s.key_id);
238    assert_eq!(ScriptSignatureAlgorithm::RS256, s.algorithm);
239    assert_eq!("2025-01-05T20:57:14+08:00", s.time);
240    assert_eq!(b"helloworld".to_vec(), s.signature);
241}
242
243#[test]
244fn test_script_parse_03() {
245    let script =
246        Script::parse(r##"#!/usr/bin/env -S deno run --allow-env
247
248console.log("Hello world.");
249
250// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20250122T233410+08:00.MEQCIGogDudoVpCVfGiNPu8Wn6YPDtFX5OXC4bKtsN1nw414AiAq+5EVdvOuKAlXdVeeE1d91mKX9TaSTR25jliUx0km6A=="##)
251            .unwrap();
252    assert!(script.verify(&KeyMap::system()).unwrap());
253}
254
255#[test]
256fn test_script_parse_04() {
257    let script =
258        Script::parse(r##"#!/usr/bin/env -S deno run --allow-env
259
260console.log("Hello world.");
261
262// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20250122T233410+08:00.MEQCIGogDudoVpCVfGiNPu8Wn6YPDtFX5OXC4bKtsN1nw414AiAq+5EVdvOuKAlXdVeeE1d91mKX9TaSTR25jliUx0km6A=="##)
263            .unwrap();
264    let script_str = script.as_string();
265    assert_eq!(r##"#!/usr/bin/env -S deno run --allow-env
266
267console.log("Hello world.");
268
269// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20250122T233410+08:00.MEQCIGogDudoVpCVfGiNPu8W
270// n6YPDtFX5OXC4bKtsN1nw414AiAq+5EVdvOuKAlXdVeeE1d91mKX9TaSTR25jliUx0km6A==
271"##, script_str);
272}
273
274#[test]
275fn test_script_parse_05() {
276    let script = Script::parse(
277        r##"#!/usr/bin/env -S deno run --allow-env
278
279console.log("Hello world.");
280
281// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20250122T233410+08:00.MEQCIGogDudoVpCVfGiNPu
282// 8Wn6YPDtFX5OXC4bKtsN1nw414AiAq+5EVdvOuKAlXdVeeE1d91mKX9TaSTR25jliUx0km6A=="##,
283    )
284    .unwrap();
285    assert!(script.verify(&KeyMap::system()).unwrap());
286}