Files
FoFreeXit/crates/ff-engine/tests/sign_roundtrip.rs
T
adminandClaude Fable 5 f62cded997 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
2026-07-15 09:14:51 +00:00

83 lines
2.9 KiB
Rust

//! Test chữ ký số (Phase 5 iteration 2): tạo Digital ID → ký → xác thực hợp
//! lệ; và phát hiện GIẢ MẠO (sửa 1 byte nội dung sau khi ký → chữ ký hỏng).
//! Cần binary `qpdf`.
use std::path::PathBuf;
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.expect("workspace root")
}
fn ensure_qpdf() {
if std::env::var("FOFREEXIT_QPDF_PATH").is_err() {
std::env::set_var("FOFREEXIT_QPDF_PATH", "/usr/bin");
}
}
fn sample() -> PathBuf {
workspace_root().join("corpus").join("sample-multipage.pdf")
}
fn tmp(name: &str) -> PathBuf {
let p = std::env::temp_dir().join(name);
let _ = std::fs::remove_file(&p);
p
}
#[test]
fn sign_then_verify_is_valid() {
ensure_qpdf();
let id = tmp("ff_sign_id.pem");
let out = tmp("ff_signed.pdf");
ff_engine::generate_self_signed_id("Nguyen Van Ky", &id).expect("tạo ID");
ff_engine::sign_pdf(&sample(), &id, "Tôi đồng ý nội dung", "Nguyen Van Ky", &out).expect("ký");
let checks = ff_engine::verify_signatures(&out).expect("verify");
assert_eq!(checks.len(), 1, "phải có đúng 1 chữ ký");
let c = &checks[0];
assert!(c.crypto_valid, "chữ ký RSA phải hợp lệ");
assert!(c.digest_matches, "messageDigest phải khớp digest file");
assert!(c.covers_document, "chữ ký phải phủ toàn bộ file");
assert!(c.is_valid(), "tổng thể phải VALID");
assert!(c.signer.contains("Nguyen Van Ky"), "tên người ký: {:?}", c.signer);
}
#[test]
fn tampering_after_signing_is_detected() {
ensure_qpdf();
let id = tmp("ff_sign_id2.pem");
let signed = tmp("ff_signed2.pdf");
let tampered = tmp("ff_tampered2.pdf");
ff_engine::generate_self_signed_id("Kiem Thu", &id).expect("tạo ID");
ff_engine::sign_pdf(&sample(), &id, "ký", "Kiem Thu", &signed).expect("ký");
// Sửa 1 byte trong VÙNG ĐƯỢC KÝ (đầu file — header/nội dung), không đụng
// vùng Contents. Chữ ký phải phát hiện.
let mut bytes = std::fs::read(&signed).expect("đọc signed");
// Tìm 1 byte chữ cái gần đầu (sau "%PDF-") để lật.
let pos = 8;
bytes[pos] ^= 0x01;
std::fs::write(&tampered, &bytes).expect("ghi tampered");
let checks = ff_engine::verify_signatures(&tampered).expect("verify tampered");
assert_eq!(checks.len(), 1);
assert!(
!checks[0].is_valid(),
"sửa nội dung sau khi ký phải làm chữ ký KHÔNG hợp lệ (digest_matches={}, crypto={}, covers={})",
checks[0].digest_matches,
checks[0].crypto_valid,
checks[0].covers_document
);
}
#[test]
fn verify_reports_none_for_unsigned() {
ensure_qpdf();
let checks = ff_engine::verify_signatures(&sample()).expect("verify unsigned");
assert!(checks.is_empty(), "file chưa ký không có chữ ký nào");
}