Skip to main content

ofd_core/
verify.rs

1//! OFD 规范符合性校验与数字签名完整性校验(见 GB/T 33190—2016 第 7、18 章)。
2//!
3//! 本模块在解析能力之上提供两类校验:
4//!
5//! 1. **结构符合性**:容器能否打开、`OFD.xml` 主入口是否完整、各版式文档的根
6//!    节点/页/模板/资源能否按声明的路径找到并解析;
7//! 2. **签名完整性**:依据 `Signature.xml` 的 [`References`] 重新计算每个被保护
8//!    文件的摘要(国密 SM3,见 [`crate::crypto`])并与 `CheckValue` 比对,判断
9//!    签名覆盖的内容是否被篡改。
10//!
11//! [`check_path`] 给出面向命令行的高层入口:对单个 OFD 文件执行上述全部校验并
12//! 汇总为 [`CheckReport`],任何失败都不会以 `Err` 形式中断,而是记入报告,便于
13//! 批量校验多个文件并列出不合规清单。
14//!
15//! 注意:本模块仅做摘要级完整性校验,**不**对签名值(SM2 签名)做密码学验签。
16//!
17//! # 示例
18//!
19//! ```no_run
20//! use ofd_core::verify::check_path;
21//!
22//! let report = check_path("sample.ofd");
23//! if report.conforms() {
24//!     println!("符合规范");
25//! } else {
26//!     for p in &report.problems {
27//!         eprintln!("问题: {p}");
28//!     }
29//! }
30//! ```
31
32use std::io::{Read, Seek};
33use std::path::Path;
34
35use crate::crypto::{base64_encode, sm3};
36use crate::model::signature::{Signature, Signatures};
37use crate::types::{StId, StLoc, parent_dir, resolve_path};
38use crate::{OfdReader, Result};
39
40/// 国密 SM3 杂凑算法的 OID(`References@CheckMethod` 取值)。
41pub const OID_SM3: &str = "1.2.156.10197.1.401";
42
43/// 摘要清单所用的校验(杂凑)方法。
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum CheckMethod {
46    /// 国密 SM3,可重新计算并比对。
47    Sm3,
48    /// 本库未实现的摘要算法,无法据此判定完整性(携带原始 OID)。
49    Unsupported(String),
50}
51
52impl CheckMethod {
53    /// 由 `References@CheckMethod` 的 OID 解析校验方法;缺省按 SM3 处理。
54    pub fn from_oid(oid: Option<&str>) -> Self {
55        match oid.map(str::trim) {
56            None | Some("") | Some(OID_SM3) => CheckMethod::Sm3,
57            Some(other) => CheckMethod::Unsupported(other.to_string()),
58        }
59    }
60}
61
62/// 单条 [`Reference`](crate::model::signature::Reference) 的校验状态。
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum RefStatus {
65    /// 重新计算的摘要与 `CheckValue` 一致。
66    Ok,
67    /// 摘要不一致,文件内容已被篡改。
68    Mismatch {
69        /// 签名中记录的期望摘要(Base64)。
70        expected: String,
71        /// 重新计算得到的实际摘要(Base64)。
72        actual: String,
73    },
74    /// 被引用的文件在包内不存在。
75    Missing,
76    /// 摘要算法不受支持,无法判定。
77    Unsupported,
78}
79
80/// 单个被保护文件的校验记录。
81#[derive(Debug, Clone)]
82pub struct RefCheck {
83    /// 被保护文件在包内的路径(`FileRef` 原文)。
84    pub file_ref: String,
85    /// 校验状态。
86    pub status: RefStatus,
87}
88
89/// 单个签名的整体判定。
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum SigVerdict {
92    /// 所有被保护文件摘要均一致。
93    Valid,
94    /// 存在被篡改或缺失的被保护文件。
95    Invalid,
96    /// 因摘要算法不受支持等原因无法判定。
97    Unverified,
98}
99
100/// 单个签名的完整性校验报告。
101#[derive(Debug, Clone)]
102pub struct SignatureReport {
103    /// 签名标识。
104    pub id: StId,
105    /// 签名类型(`Seal`/`Sign`)。
106    pub sig_type: String,
107    /// 签名描述文件(`Signature.xml`)在包内的路径。
108    pub base_loc: String,
109    /// 摘要校验方法。
110    pub method: CheckMethod,
111    /// 各被保护文件的校验记录。
112    pub references: Vec<RefCheck>,
113}
114
115impl SignatureReport {
116    /// 综合判定本签名的完整性。
117    pub fn verdict(&self) -> SigVerdict {
118        if matches!(self.method, CheckMethod::Unsupported(_)) {
119            return SigVerdict::Unverified;
120        }
121        if self
122            .references
123            .iter()
124            .any(|r| !matches!(r.status, RefStatus::Ok))
125        {
126            SigVerdict::Invalid
127        } else {
128            SigVerdict::Valid
129        }
130    }
131
132    /// 返回所有校验未通过(篡改或缺失)的被保护文件记录。
133    pub fn failures(&self) -> impl Iterator<Item = &RefCheck> {
134        self.references
135            .iter()
136            .filter(|r| matches!(r.status, RefStatus::Mismatch { .. } | RefStatus::Missing))
137    }
138}
139
140/// 一个 OFD 文件的完整校验报告。
141#[derive(Debug, Clone, Default)]
142pub struct CheckReport {
143    /// 结构符合性问题清单(每项为一条人类可读的描述)。
144    pub problems: Vec<String>,
145    /// 各签名的完整性校验报告。
146    pub signatures: Vec<SignatureReport>,
147}
148
149impl CheckReport {
150    /// 是否符合规范:既无结构问题,也没有任何签名被判定为 [`SigVerdict::Invalid`]。
151    ///
152    /// 因算法不受支持而 [`SigVerdict::Unverified`] 的签名不视为不合规。
153    pub fn conforms(&self) -> bool {
154        self.problems.is_empty()
155            && self
156                .signatures
157                .iter()
158                .all(|s| s.verdict() != SigVerdict::Invalid)
159    }
160}
161
162impl<R: Read + Seek> OfdReader<R> {
163    /// 装载文档登记的全部签名(`Signatures.xml` 及其引用的各 `Signature.xml`)。
164    ///
165    /// 遍历所有 `DocBody`,对声明了 `Signatures` 的文档解析其签名列表与每份签名
166    /// 描述。返回的 [`LoadedSignature`] 携带在包内的解析路径,供完整性校验定位。
167    pub fn load_signatures(&mut self) -> Result<Vec<LoadedSignature>> {
168        let sig_locs: Vec<StLoc> = self
169            .ofd()
170            .doc_bodies
171            .iter()
172            .filter_map(|b| b.signatures.clone())
173            .collect();
174
175        let mut loaded = Vec::new();
176        for loc in &sig_locs {
177            let list_path = resolve_path("", loc);
178            let base = parent_dir(&list_path).to_string();
179            let list: Signatures = self.package_mut().parse(&list_path)?;
180            for sig_ref in &list.signatures {
181                let path = resolve_path(&base, &sig_ref.base_loc);
182                let signature: Signature = self.package_mut().parse(&path)?;
183                loaded.push(LoadedSignature {
184                    id: sig_ref.id,
185                    sig_type: sig_ref.type_or_default().to_string(),
186                    base_loc: path,
187                    signature,
188                });
189            }
190        }
191        Ok(loaded)
192    }
193
194    /// 校验单个签名的完整性:逐个重算被保护文件的摘要并与 `CheckValue` 比对。
195    pub fn verify_signature(&mut self, loaded: &LoadedSignature) -> SignatureReport {
196        let refs = &loaded.signature.signed_info.references;
197        let method = CheckMethod::from_oid(refs.check_method.as_deref());
198
199        let mut checks = Vec::with_capacity(refs.references.len());
200        for reference in &refs.references {
201            let file_ref = reference.file_ref.to_string();
202            let status = if method != CheckMethod::Sm3 {
203                RefStatus::Unsupported
204            } else {
205                let path = resolve_path("", &reference.file_ref);
206                match self.package_mut().read(&path) {
207                    Err(_) => RefStatus::Missing,
208                    Ok(bytes) => {
209                        let actual = base64_encode(&sm3(&bytes));
210                        if actual == reference.check_value.trim() {
211                            RefStatus::Ok
212                        } else {
213                            RefStatus::Mismatch {
214                                expected: reference.check_value.trim().to_string(),
215                                actual,
216                            }
217                        }
218                    }
219                }
220            };
221            checks.push(RefCheck { file_ref, status });
222        }
223
224        SignatureReport {
225            id: loaded.id,
226            sig_type: loaded.sig_type.clone(),
227            base_loc: loaded.base_loc.clone(),
228            method,
229            references: checks,
230        }
231    }
232
233    /// 校验文档全部签名的完整性,返回逐个签名的报告。
234    pub fn verify_signatures(&mut self) -> Result<Vec<SignatureReport>> {
235        let loaded = self.load_signatures()?;
236        Ok(loaded.iter().map(|l| self.verify_signature(l)).collect())
237    }
238}
239
240/// 已装载的单个签名及其在包内的解析路径。
241#[derive(Debug, Clone)]
242pub struct LoadedSignature {
243    /// 签名标识。
244    pub id: StId,
245    /// 签名类型(`Seal`/`Sign`)。
246    pub sig_type: String,
247    /// `Signature.xml` 在包内的解析路径。
248    pub base_loc: String,
249    /// 解析后的签名描述。
250    pub signature: Signature,
251}
252
253/// 对已打开的 OFD 读取器执行结构符合性 + 签名完整性校验。
254///
255/// 与 [`check_path`] 的区别在于此函数针对已打开的 [`OfdReader`](如内存中的包),
256/// 因此不包含“容器能否打开/主入口能否解析”这一步。
257pub fn check_reader<R: Read + Seek>(reader: &mut OfdReader<R>) -> CheckReport {
258    let mut report = CheckReport::default();
259    check_structure(reader, &mut report);
260
261    match reader.verify_signatures() {
262        Ok(sigs) => report.signatures = sigs,
263        Err(e) => report.problems.push(format!("签名装载失败: {e}")),
264    }
265    report
266}
267
268/// 对文件系统中的一个 OFD 文件执行完整校验。
269///
270/// 任何失败(无法打开、主入口缺失、引用文件解析失败、签名被篡改等)都会记入
271/// 返回的 [`CheckReport`],本函数不会返回 `Err`,便于批量校验。
272pub fn check_path<P: AsRef<Path>>(path: P) -> CheckReport {
273    let mut report = CheckReport::default();
274    let mut reader = match OfdReader::open(path) {
275        Ok(r) => r,
276        Err(e) => {
277            report
278                .problems
279                .push(format!("无法打开或解析 OFD 主入口: {e}"));
280            return report;
281        }
282    };
283    let sub = check_reader(&mut reader);
284    report.problems.extend(sub.problems);
285    report.signatures = sub.signatures;
286    report
287}
288
289/// 检查主入口、各版式文档及其页/模板/资源能否按声明定位并解析。
290fn check_structure<R: Read + Seek>(reader: &mut OfdReader<R>, report: &mut CheckReport) {
291    let ofd = reader.ofd();
292    if ofd.version.trim().is_empty() {
293        report
294            .problems
295            .push("OFD.xml 缺少 Version 属性".to_string());
296    }
297    if ofd.doc_type != "OFD" && ofd.doc_type != "OFD-A" {
298        report
299            .problems
300            .push(format!("OFD.xml 的 DocType 非法: {:?}", ofd.doc_type));
301    }
302    if ofd.doc_bodies.is_empty() {
303        report.problems.push("OFD.xml 不含任何 DocBody".to_string());
304    }
305
306    let bodies = ofd.doc_bodies.clone();
307    for (i, body) in bodies.iter().enumerate() {
308        let Some(root) = &body.doc_root else {
309            report
310                .problems
311                .push(format!("DocBody #{i} 缺少 DocRoot,无法定位文档根节点"));
312            continue;
313        };
314
315        let doc = match reader.load_document(body) {
316            Ok(d) => d,
317            Err(e) => {
318                report
319                    .problems
320                    .push(format!("DocBody #{i} 文档根节点 {root} 解析失败: {e}"));
321                continue;
322            }
323        };
324
325        // 页对象逐页定位与解析。
326        let pages: Vec<_> = doc.pages().to_vec();
327        for page in &pages {
328            if let Err(e) = reader.load_page(&doc, page) {
329                report.problems.push(format!(
330                    "DocBody #{i} 页 {} ({}) 解析失败: {e}",
331                    page.id, page.base_loc
332                ));
333            }
334        }
335
336        // 模板页。
337        let templates: Vec<_> = doc.template_pages().to_vec();
338        for tpl in &templates {
339            if let Err(e) = reader.load_template(&doc, tpl) {
340                report.problems.push(format!(
341                    "DocBody #{i} 模板页 {} 解析失败: {e}",
342                    tpl.base_loc
343                ));
344            }
345        }
346
347        // 公共资源与文档资源。
348        let resources: Vec<_> = doc
349            .public_res()
350            .iter()
351            .chain(doc.document_res())
352            .cloned()
353            .collect();
354        for loc in &resources {
355            if let Err(e) = reader.load_resource(&doc, loc) {
356                report
357                    .problems
358                    .push(format!("DocBody #{i} 资源 {loc} 解析失败: {e}"));
359            }
360        }
361    }
362}