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