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