Phase 5 Iteration 2: chữ ký số PKCS#7/PAdES + redact tỉa ký tự + lưu tối ưu
Engine — chữ ký số (sign.rs MỚI): - generate_self_signed_id: Digital ID tự ký RSA-2048 + X.509 (rcgen), ghi PEM - sign_pdf: đúng cơ chế Adobe — lopdf dựng sig dict (/SubFilter adbe.pkcs7.detached) + Widget field + AcroForm/SigFlags, ByteRange/Contents placeholder; serialize → vá ByteRange theo offset thật → SHA-256 trên 2 đoạn → CMS SignedData detached (crate cms, signed attrs messageDigest+contentType, RSA-SHA256) nhét DER vào Contents - verify_signatures: parse ByteRange/Contents, băm lại, kiểm chữ ký RSA trên signed attrs + messageDigest khớp + phủ toàn bộ file → phát hiện GIẢ MẠO - Bài học: cắt DER về đúng độ dài (bỏ padding hex '0'); bỏ qua /Contents của trang (tham chiếu), chỉ nhận /Contents <hex> của chữ ký Engine — redact tỉa theo KÝ TỰ (redact.rs): - Text chạm một phần: page_char_boxes xác định ký tự trong vùng, GIỮ các đoạn ngoài vùng, dựng lại đúng vị trí/baseline/màu (font nhúng gốc → cùng họ → mặc định). Guard: text xoay/không đọc được font → xoá nguyên (an toàn) - qpdf::optimize_save: --object-streams=generate + nén, dọn object rác Tauri + UI: - Commands: sig_create_id, sig_sign, sig_verify, security_optimize, pick_any_file, pick_save_pem - Thanh Bảo mật thêm: Tạo Digital ID, Ký số (dialog + tự kiểm tra sau khi ký), Kiểm tra chữ ký (modal bảng trạng thái + lý do), Lưu tối ưu Test (7 mới → 62/62 engine xanh ngoài 2 fixture qpdf Linux): - sign_then_verify_is_valid; tampering_after_signing_is_detected (phát hiện sửa sau khi ký); verify_reports_none_for_unsigned - redact tỉa ký tự: cụm mật biến mất, phần cùng dòng ngoài vùng vẫn còn - optimize_save không phình + giữ nội dung Deps: rsa, sha2, cms, x509-cert, rcgen, pkcs8, der, spki, const-oid. Docs: 15 mục 6 (iter 2 + giới hạn), 16 (S13–S19), README, roadmap (Phase 5 HOÀN TẤT), 14 (kế tiếp: Phase 6 Form). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
This commit is contained in:
@@ -939,6 +939,84 @@ fn security_strip_metadata(input: String, output: String) -> Result<(), String>
|
||||
}
|
||||
}
|
||||
|
||||
/// Lưu tối ưu: nén object stream + dọn object rác.
|
||||
#[tauri::command]
|
||||
fn security_optimize(input: String, output: String) -> Result<(), String> {
|
||||
ff_engine::optimize_save(std::path::Path::new(&input), std::path::Path::new(&output))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Tạo Digital ID tự ký (RSA-2048) ghi ra file PEM.
|
||||
#[tauri::command]
|
||||
fn sig_create_id(common_name: String, output: String) -> Result<(), String> {
|
||||
ff_engine::generate_self_signed_id(&common_name, std::path::Path::new(&output))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Ký số `input` bằng identity PEM, ghi ra `output`.
|
||||
#[tauri::command]
|
||||
fn sig_sign(
|
||||
input: String,
|
||||
id_pem: String,
|
||||
reason: String,
|
||||
signer_name: String,
|
||||
output: String,
|
||||
) -> Result<(), String> {
|
||||
ff_engine::sign_pdf(
|
||||
std::path::Path::new(&input),
|
||||
std::path::Path::new(&id_pem),
|
||||
&reason,
|
||||
&signer_name,
|
||||
std::path::Path::new(&output),
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Kết quả xác thực 1 chữ ký cho UI.
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SignatureCheckDto {
|
||||
signer: String,
|
||||
crypto_valid: bool,
|
||||
digest_matches: bool,
|
||||
covers_document: bool,
|
||||
valid: bool,
|
||||
}
|
||||
|
||||
/// Xác thực mọi chữ ký trong `input`.
|
||||
#[tauri::command]
|
||||
fn sig_verify(input: String) -> Result<Vec<SignatureCheckDto>, String> {
|
||||
let checks =
|
||||
ff_engine::verify_signatures(std::path::Path::new(&input)).map_err(|e| e.to_string())?;
|
||||
Ok(checks
|
||||
.into_iter()
|
||||
.map(|c| SignatureCheckDto {
|
||||
signer: c.signer.clone(),
|
||||
crypto_valid: c.crypto_valid,
|
||||
digest_matches: c.digest_matches,
|
||||
covers_document: c.covers_document,
|
||||
valid: c.is_valid(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Hộp thoại chọn file bất kỳ (dùng cho chọn Digital ID .pem / PDF để xác thực).
|
||||
#[tauri::command]
|
||||
fn pick_any_file(app: tauri::AppHandle) -> Option<String> {
|
||||
app.dialog().file().blocking_pick_file().map(|fp| fp.to_string())
|
||||
}
|
||||
|
||||
/// Hộp thoại lưu file PEM (Digital ID).
|
||||
#[tauri::command]
|
||||
fn pick_save_pem(app: tauri::AppHandle) -> Option<String> {
|
||||
app.dialog()
|
||||
.file()
|
||||
.add_filter("Digital ID (PEM)", &["pem"])
|
||||
.set_file_name("my-digital-id.pem")
|
||||
.blocking_save_file()
|
||||
.map(|fp| fp.to_string())
|
||||
}
|
||||
|
||||
/// Dọn các file làm việc tạm của chế độ sửa (undo stack). Chỉ xoá file có tên
|
||||
/// `ff_edit_*` nằm đúng trong thư mục temp — không bao giờ đụng file người dùng.
|
||||
#[tauri::command]
|
||||
@@ -1003,6 +1081,12 @@ fn main() {
|
||||
security_encrypt,
|
||||
security_decrypt,
|
||||
security_strip_metadata,
|
||||
security_optimize,
|
||||
sig_create_id,
|
||||
sig_sign,
|
||||
sig_verify,
|
||||
pick_any_file,
|
||||
pick_save_pem,
|
||||
pick_image
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
Reference in New Issue
Block a user