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 } 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
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(¤t_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(¤t_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}