use std::io::{Read, Seek};
use std::path::Path;
use crate::crypto::{base64_encode, sm3};
use crate::model::signature::{Signature, Signatures};
use crate::types::{StId, StLoc, parent_dir, resolve_path};
use crate::{OfdReader, Result};
pub const OID_SM3: &str = "1.2.156.10197.1.401";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckMethod {
Sm3,
Unsupported(String),
}
impl CheckMethod {
pub fn from_oid(oid: Option<&str>) -> Self {
match oid.map(str::trim) {
None | Some("") | Some(OID_SM3) => CheckMethod::Sm3,
Some(other) => CheckMethod::Unsupported(other.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefStatus {
Ok,
Mismatch {
expected: String,
actual: String,
},
Missing,
Unsupported,
}
#[derive(Debug, Clone)]
pub struct RefCheck {
pub file_ref: String,
pub status: RefStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigVerdict {
Valid,
Invalid,
Unverified,
}
#[derive(Debug, Clone)]
pub struct SignatureReport {
pub id: StId,
pub sig_type: String,
pub base_loc: String,
pub method: CheckMethod,
pub references: Vec<RefCheck>,
}
impl SignatureReport {
pub fn verdict(&self) -> SigVerdict {
if matches!(self.method, CheckMethod::Unsupported(_)) {
return SigVerdict::Unverified;
}
if self
.references
.iter()
.any(|r| !matches!(r.status, RefStatus::Ok))
{
SigVerdict::Invalid
} else {
SigVerdict::Valid
}
}
pub fn failures(&self) -> impl Iterator<Item = &RefCheck> {
self.references
.iter()
.filter(|r| matches!(r.status, RefStatus::Mismatch { .. } | RefStatus::Missing))
}
}
#[derive(Debug, Clone, Default)]
pub struct CheckReport {
pub problems: Vec<String>,
pub signatures: Vec<SignatureReport>,
}
impl CheckReport {
pub fn conforms(&self) -> bool {
self.problems.is_empty()
&& self
.signatures
.iter()
.all(|s| s.verdict() != SigVerdict::Invalid)
}
}
impl<R: Read + Seek> OfdReader<R> {
pub fn load_signatures(&mut self) -> Result<Vec<LoadedSignature>> {
let sig_locs: Vec<StLoc> = self
.ofd()
.doc_bodies
.iter()
.filter_map(|b| b.signatures.clone())
.collect();
let mut loaded = Vec::new();
for loc in &sig_locs {
let list_path = resolve_path("", loc);
let base = parent_dir(&list_path).to_string();
let list: Signatures = self.package_mut().parse(&list_path)?;
for sig_ref in &list.signatures {
let path = resolve_path(&base, &sig_ref.base_loc);
let signature: Signature = self.package_mut().parse(&path)?;
loaded.push(LoadedSignature {
id: sig_ref.id,
sig_type: sig_ref.type_or_default().to_string(),
base_loc: path,
signature,
});
}
}
Ok(loaded)
}
pub fn verify_signature(&mut self, loaded: &LoadedSignature) -> SignatureReport {
let refs = &loaded.signature.signed_info.references;
let method = CheckMethod::from_oid(refs.check_method.as_deref());
let mut checks = Vec::with_capacity(refs.references.len());
for reference in &refs.references {
let file_ref = reference.file_ref.to_string();
let status = if method != CheckMethod::Sm3 {
RefStatus::Unsupported
} else {
let path = resolve_path("", &reference.file_ref);
match self.package_mut().read(&path) {
Err(_) => RefStatus::Missing,
Ok(bytes) => {
let actual = base64_encode(&sm3(&bytes));
if actual == reference.check_value.trim() {
RefStatus::Ok
} else {
RefStatus::Mismatch {
expected: reference.check_value.trim().to_string(),
actual,
}
}
}
}
};
checks.push(RefCheck { file_ref, status });
}
SignatureReport {
id: loaded.id,
sig_type: loaded.sig_type.clone(),
base_loc: loaded.base_loc.clone(),
method,
references: checks,
}
}
pub fn verify_signatures(&mut self) -> Result<Vec<SignatureReport>> {
let loaded = self.load_signatures()?;
Ok(loaded.iter().map(|l| self.verify_signature(l)).collect())
}
}
#[derive(Debug, Clone)]
pub struct LoadedSignature {
pub id: StId,
pub sig_type: String,
pub base_loc: String,
pub signature: Signature,
}
pub fn check_reader<R: Read + Seek>(reader: &mut OfdReader<R>) -> CheckReport {
let mut report = CheckReport::default();
check_structure(reader, &mut report);
match reader.verify_signatures() {
Ok(sigs) => report.signatures = sigs,
Err(e) => report.problems.push(format!("签名装载失败: {e}")),
}
report
}
pub fn check_path<P: AsRef<Path>>(path: P) -> CheckReport {
let mut report = CheckReport::default();
let mut reader = match OfdReader::open(path) {
Ok(r) => r,
Err(e) => {
report
.problems
.push(format!("无法打开或解析 OFD 主入口: {e}"));
return report;
}
};
let sub = check_reader(&mut reader);
report.problems.extend(sub.problems);
report.signatures = sub.signatures;
report
}
fn check_structure<R: Read + Seek>(reader: &mut OfdReader<R>, report: &mut CheckReport) {
let ofd = reader.ofd();
if ofd.version.trim().is_empty() {
report
.problems
.push("OFD.xml 缺少 Version 属性".to_string());
}
if ofd.doc_type != "OFD" && ofd.doc_type != "OFD-A" {
report
.problems
.push(format!("OFD.xml 的 DocType 非法: {:?}", ofd.doc_type));
}
if ofd.doc_bodies.is_empty() {
report.problems.push("OFD.xml 不含任何 DocBody".to_string());
}
let bodies = ofd.doc_bodies.clone();
for (i, body) in bodies.iter().enumerate() {
let Some(root) = &body.doc_root else {
report
.problems
.push(format!("DocBody #{i} 缺少 DocRoot,无法定位文档根节点"));
continue;
};
let doc = match reader.load_document(body) {
Ok(d) => d,
Err(e) => {
report
.problems
.push(format!("DocBody #{i} 文档根节点 {root} 解析失败: {e}"));
continue;
}
};
let pages: Vec<_> = doc.pages().to_vec();
for page in &pages {
if let Err(e) = reader.load_page(&doc, page) {
report.problems.push(format!(
"DocBody #{i} 页 {} ({}) 解析失败: {e}",
page.id, page.base_loc
));
}
}
let templates: Vec<_> = doc.template_pages().to_vec();
for tpl in &templates {
if let Err(e) = reader.load_template(&doc, tpl) {
report.problems.push(format!(
"DocBody #{i} 模板页 {} 解析失败: {e}",
tpl.base_loc
));
}
}
let resources: Vec<_> = doc
.public_res()
.iter()
.chain(doc.document_res())
.cloned()
.collect();
for loc in &resources {
if let Err(e) = reader.load_resource(&doc, loc) {
report
.problems
.push(format!("DocBody #{i} 资源 {loc} 解析失败: {e}"));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ref_check(status: RefStatus) -> RefCheck {
RefCheck {
file_ref: "Doc_0/Document.xml".to_string(),
status,
}
}
fn report_with(method: CheckMethod, statuses: Vec<RefStatus>) -> SignatureReport {
SignatureReport {
id: StId(1),
sig_type: "Seal".to_string(),
base_loc: "Doc_0/Signs/Sign_0/Signature.xml".to_string(),
method,
references: statuses.into_iter().map(ref_check).collect(),
}
}
#[test]
fn check_method_from_oid_branches() {
assert_eq!(CheckMethod::from_oid(None), CheckMethod::Sm3);
assert_eq!(CheckMethod::from_oid(Some("")), CheckMethod::Sm3);
assert_eq!(CheckMethod::from_oid(Some(" ")), CheckMethod::Sm3);
assert_eq!(CheckMethod::from_oid(Some(OID_SM3)), CheckMethod::Sm3);
assert_eq!(
CheckMethod::from_oid(Some("1.2.840.113549")),
CheckMethod::Unsupported("1.2.840.113549".to_string())
);
}
#[test]
fn verdict_unverified_when_method_unsupported() {
let r = report_with(CheckMethod::Unsupported("x".into()), vec![RefStatus::Ok]);
assert_eq!(r.verdict(), SigVerdict::Unverified);
}
#[test]
fn verdict_valid_and_invalid() {
let valid = report_with(CheckMethod::Sm3, vec![RefStatus::Ok, RefStatus::Ok]);
assert_eq!(valid.verdict(), SigVerdict::Valid);
assert_eq!(valid.failures().count(), 0);
let invalid = report_with(
CheckMethod::Sm3,
vec![
RefStatus::Ok,
RefStatus::Mismatch {
expected: "a".into(),
actual: "b".into(),
},
RefStatus::Missing,
RefStatus::Unsupported,
],
);
assert_eq!(invalid.verdict(), SigVerdict::Invalid);
assert_eq!(invalid.failures().count(), 2);
}
#[test]
fn check_report_conforms() {
let mut report = CheckReport::default();
report
.signatures
.push(report_with(CheckMethod::Sm3, vec![RefStatus::Ok]));
report.signatures.push(report_with(
CheckMethod::Unsupported("x".into()),
vec![RefStatus::Ok],
));
assert!(report.conforms());
report.problems.push("boom".into());
assert!(!report.conforms());
let mut report2 = CheckReport::default();
report2
.signatures
.push(report_with(CheckMethod::Sm3, vec![RefStatus::Missing]));
assert!(!report2.conforms());
}
}