script_sign/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
mod keymap;
mod sign;
mod signature;
mod util;

pub use crate::keymap::KeyMap;
use crate::sign::{ecdsaverify, EcdsaAlgorithm};
use crate::signature::{
    CardEcSignResult, ScriptSignature, ScriptSignatureAlgorithm, SIGNATURE_PREFIX,
};
use crate::util::current_time;
use digest::Digest;
use rust_util::{debugging, opt_result, simple_error, util_cmd, XResult};
use sha2::Sha256;
use std::fs;

#[derive(Debug)]
pub struct Script {
    pub content_lines: Vec<String>,
    pub signature: Option<ScriptSignature>,
}

impl Script {
    pub fn verify_script_file_with_system_key_map(script_file: &str) -> XResult<bool> {
        Self::verify_script_file(script_file, &KeyMap::system()?)
    }

    pub fn verify_script_file(script_file: &str, key_map: &KeyMap) -> XResult<bool> {
        let script_content = opt_result!(
            fs::read_to_string(script_file),
            "Read script file: {script_file} failed: {}"
        );
        let script = opt_result!(
            Script::parse(&script_content),
            "Parse script file: {script_file} failed: {}"
        );
        match &script.signature {
            None => Ok(false),
            Some(_) => script.verify(key_map),
        }
    }

    pub fn parse(script: &str) -> XResult<Script> {
        let lines = script.lines().collect::<Vec<_>>();
        let last_non_empty_line = lines.iter().rev().find(|ln| !ln.is_empty());
        match last_non_empty_line {
            Some(last_non_empty_line) if last_non_empty_line.starts_with(SIGNATURE_PREFIX) => {
                let script_signature = ScriptSignature::parse(last_non_empty_line)?;
                let final_lines = lines
                    .iter()
                    .rev()
                    .skip_while(|ln| ln.is_empty())
                    .skip(1)
                    .collect::<Vec<_>>()
                    .into_iter()
                    .rev()
                    .collect::<Vec<_>>();
                Ok(Script {
                    content_lines: final_lines.iter().map(ToString::to_string).collect(),
                    signature: Some(script_signature),
                })
            }
            _ => Ok(Script {
                content_lines: lines.iter().map(ToString::to_string).collect(),
                signature: None,
            }),
        }
    }

    pub fn as_string(&self) -> String {
        let mut joined_content_liens = self.content_lines.join("\n");
        match &self.signature {
            None => joined_content_liens,
            Some(signature) => {
                if joined_content_liens.ends_with("\n\n") {
                    // SKIP add \n
                } else if joined_content_liens.ends_with("\n") {
                    joined_content_liens.push('\n');
                } else {
                    joined_content_liens.push_str("\n\n");
                }
                joined_content_liens.push_str(&signature.as_string());
                joined_content_liens.push('\n');
                joined_content_liens
            }
        }
    }

    pub fn has_signature(&self) -> bool {
        self.signature.is_some()
    }

    pub fn verify(&self, key_map: &KeyMap) -> XResult<bool> {
        let signature = match &self.signature {
            None => return simple_error!("Script is not signed."),
            Some(signature) => signature,
        };
        let key = match key_map.find(&signature.key_id) {
            None => return simple_error!("Sign key id: {} not found", &signature.key_id),
            Some(key) => key,
        };
        let key_bytes = hex::decode(key)?;
        let digest_sha256 = self.normalize_content_lines_and_sha256(&signature.time);
        match signature.algorithm {
            ScriptSignatureAlgorithm::ES256 => {
                match ecdsaverify(
                    EcdsaAlgorithm::P256,
                    &key_bytes,
                    &digest_sha256,
                    &signature.signature,
                ) {
                    Ok(_) => Ok(true),
                    Err(e) => {
                        debugging!("Verify ecdsa signature failed: {}", e);
                        Ok(false)
                    }
                }
            }
            _ => simple_error!("Not supported algorithm: {:?}", signature.algorithm),
        }
    }

    pub fn sign(&mut self) -> XResult<()> {
        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],
        );
        let ecsign_result: CardEcSignResult = opt_result!(
            serde_json::from_slice(&output.stdout),
            "Parse card piv-ecsign failed: {}"
        );
        if ecsign_result.algorithm == "ecdsa_p256_with_sha256" {
            self.signature = Some(ScriptSignature {
                key_id: "yk-r1".to_string(),
                algorithm: ScriptSignatureAlgorithm::ES256,
                time,
                signature: hex::decode(&ecsign_result.signed_data_hex)?,
            });
        } else {
            return simple_error!("Not supported algorithm: {}", ecsign_result.algorithm);
        }
        Ok(())
    }

    fn normalize_content_lines(&self) -> Vec<String> {
        let mut normalized_content_lines = Vec::with_capacity(self.content_lines.len());
        for ln in &self.content_lines {
            let trimed_ln = ln.trim();
            if !trimed_ln.is_empty() {
                normalized_content_lines.push(trimed_ln.to_string());
            }
        }
        normalized_content_lines
    }

    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(current_time.as_bytes());
        hasher.update(joined_normalized_content_lines.as_bytes());
        hasher.finalize().to_vec()
    }
}

#[test]
fn test_script_parse_01() {
    let script = Script::parse("test script").unwrap();
    assert_eq!(1, script.content_lines.len());
    assert!(script.signature.is_none());
}

#[test]
fn test_script_parse_02() {
    use base64::engine::general_purpose::STANDARD as standard_base64;
    use base64::Engine;
    let script =
        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!(
        "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);
}