บทความเทคนิค

DocMDP ของ PDFium Component: วง /P ของ widget ซ่อนการแก้หน้า

ในบิลด์ PDFium Component ก่อน v3.126.2 TPdf.AnalyzeSignatureRevisions เคยให้คะแนนการแก้เนื้อหาหน้าจริง ๆ เป็นการเปลี่ยน annotation ที่ได้รับอนุญาตใต้ DocMDP P=3 เพราะกราฟ role ของ revision มันถือว่า /P ที่ signature widget อ้างกลับไปหาหน้าของมันคือความเป็นเจ้าของ ตั้งแต่ v3.126.2 PDFium Component แยก navigation edge ออกจาก edge ของ payload ที่ถูกเป็นเจ้าของ เนื้อหาหน้าจึงยังเป็นเนื้อหาหน้า รายงานบั๊กหลังการแก้นี้บนกระดาษดูไร้พิษภัย สัญญาที่ผ่านการรับรองอนุญาตให้แก้ annotation คู่สัญญาบวกการเซฟแบบ incremental เข้ามาหนึ่งรอบ ตัววิเคราะห์ก็บอกว่าการเปลี่ยนทั้งหมดที่ตามมาได้รับอนุญาต แล้วใครสักคนดิฟหน้าที่ render ออกมา จำนวนเงินในหน้า 2 กลับไม่เหมือนเดิม

บทความนี้เป็นภาคต่อสายตาฝั่งผู้โจมตีของภาพรวมการวิเคราะห์การเปลี่ยนแปลง revision หลังลายเซ็น จึงข้ามพื้นฐานเรื่องการสร้าง revision ซ้ำกับการให้เกรด DocMDP ไปลงมือที่กราฟออบเจกต์เลย คือการเป็นเจ้าของถูกจำลองไว้อย่างไร ทำไมทิศทางของ edge ตัดสินคำตัดสินด้านความปลอดภัย อะไรเปลี่ยนไปใน v3.126.2 และจะตรวจ logic การรับไฟล์ของตัวเองอย่างไร

ทำไมการแก้หน้าถึงผ่านไปเป็นการเปลี่ยน annotation ใต้ DocMDP P=3

การแก้หน้าผ่านไปได้เพราะกราฟ role ตัวเก่าเดินตาม indirect reference ทุกตัวใน dictionary ราวกับว่าออบเจกต์ที่ถูกอ้างถึงเป็นของตัวอ้าง และ signature widget ชี้กลับไปหาหน้าของมัน dictionary ของ annotation แบก /P ซึ่งเป็น indirect reference ชี้ไปยังออบเจกต์หน้าที่มันนั่งอยู่ (ISO 32000-1 §12.5.2) entry นี้เป็นแค่ hint การเดินทาง widget ไม่ได้เป็นเจ้าของหน้า หน้าต่างหากเป็นเจ้าของ widget ผ่าน array /Annots ของมัน

ตัววิเคราะห์ assign ชุด role bits ให้แต่ละออบเจกต์ก่อนให้เกรดการเปลี่ยนที่ตามมา ได้แก่ role หน้า, annotation, form กับวัสดุตรวจสอบ ออบเจกต์ root ได้ role จาก dictionary ของตัวเอง แล้ว role จะแพร่ไปทุกสิ่งที่มันอ้างถึง ในการแพร่แบบเก่า โซ่เดินแบบนี้:

  1. signature widget เป็น /Subtype /Widget ที่มี /FT /Sig จึงได้ role annotation
  2. /P ของ widget ผลัก role annotation ขึ้นไปบน dictionary ของหน้า ซึ่งมี role หน้าอยู่แล้ว
  3. หน้าผลักทั้งสอง role เข้า /Contents, /Resources และผ่าน /Parent ไต่ขึ้นต้นไม้ Pages กับกระจายไปทุกหน้าพี่น้อง
  4. dictionary ของ content stream อย่าง << /Length 812 >> ไม่มี /Type ตัวจำแนกจึงตกกลับไปใช้ role bits และเช็ก role annotation ก่อน role หน้า
แผนภาพ PDFium Component ของกราฟ role DocMDP ก่อน v3.126.2 ที่ /P อ้างกลับของ signature widget ผลัก role annotation ขึ้นไปบน dictionary ของหน้า หน้าแพร่มันผ่าน /Contents ลง content stream ที่ไม่มี entry /Type ตัวจำแนกจึงปล่อย prckAnnotation ออกมาและการให้เกรด P=3 คืน prdAllowed
ก่อน v3.126.2 กราฟ role ถือว่า indirect reference ทุกตัวคือความเป็นเจ้าของ entry /P ของ widget จึงผลัก role annotation ขึ้นไปบนหน้า และการแก้หน้าจริงออกจากตัววิเคราะห์ไปเป็นการเปลี่ยน annotation ที่ได้รับอนุญาต

content stream ที่ถูกแก้จึงหลุดออกมาเป็น prckAnnotation ใต้ ISO 32000-1 §12.8.2.2 DocMDP P=3 อนุญาตให้แก้ annotation คำตัดสินจึงเป็น prdAllowed และรายงานรวมขึ้นไปเป็น prasAllowed ไฟล์เดียวกันใต้ P=2 ถูกปฏิเสธ แต่ปฏิเสธโดยบังเอิญเท่านั้น P=2 ห้ามแก้ annotation stream ที่ถูกติดป้ายผิดจึงโดนปัดเพราะเหตุผลผิด ๆ ลูปการแพร่ที่รันนิ่งสี่รอบยังเติมจุดอ่อนอีกหนึ่งจุด payload ที่มาถึงผ่าน indirect array หรือผ่านโซ่ยาว ๆ ที่เลขออบเจกต์วิ่งย้อนหลัง อาจไม่เคยได้รับ role อะไรเลยก็ได้

ทำไมตัวตรวจลายเซ็นต้องถามว่าใครเป็นเจ้าของออบเจกต์

ตัวตรวจลายเซ็นต้องถามว่าใครเป็นเจ้าของออบเจกต์ เพราะ incremental update ของ PDF (ISO 32000-1 §7.5.6) ให้ใครก็ได้ต่อท้าย revision ที่แก้นิยามเลขออบเจกต์เดิม และ body ที่ถูกแก้นิยามใหม่ไม่เคยประกาศว่าตัวเองคืออะไร ลายเซ็นก็ยัง verify ผ่าน เพราะมันคลุมแค่ไบต์ของ revision ตัวเอง การป้องกันทุกชั้นต่อการแก้หลังลงลายเซ็นจึงพึ่งพาการแมปออบเจกต์ที่เปลี่ยนแต่ละตัวเข้ากับโครงสร้างที่ใช้มัน แล้วถามว่าผู้ลงลายเซ็นอนุญาตให้โครงสร้างนั้นเปลี่ยนได้หรือไม่

ชั้นการโจมตีที่เผยแพร่แล้วหลายชั้นทำงานพอดีในช่องว่างนี้ incremental saving attack ต่อท้าย revision ที่สลับเนื้อหาหน้าแล้วเดาว่าตัว verify เช็กแค่ช่วงไบต์ที่ลงลายเซ็น shadow attack ฝังเนื้อหาซ่อนไว้ก่อนลงลายเซ็นแล้วปลุกมันภายหลังด้วยการแก้เล็ก ๆ หน้าตาซื่อสัตย์ การโจมตีเอกสารที่ผ่านการรับรองเล่นกับความจริงที่ว่า P=2 กับ P=3 อนุญาตการแก้บางอย่างไว้ชัดเจน แล้วแต่งตัวการแก้ที่ห้ามให้ดูเหมือนแบบที่อนุญาต ตัว verify ที่จำแนกออบเจกต์ด้วยป้ายอย่าง /Type /Annot หรือด้วยเส้นทาง reference อะไรก็ตามที่บังเอิญไปถึงมัน จึงโดนชั้นที่สามเล่นงานได้ ผู้โจมตีต้องมีโครงสร้างที่อนุญาตแค่หนึ่งอันที่ไปถึงโครงสร้างที่ห้ามก็พอ

นี่แหละคือเหตุที่คำถามไม่ใช่ว่าออบเจกต์ไหนเปลี่ยน แต่คือใครเป็นเจ้าของมัน content stream ที่หน้าเรียกถึงผ่าน /Contents เป็นเนื้อหาหน้าไม่ว่าจะมีใครชี้มาที่มันอีก annotation ที่ชี้กลับไปหาหน้าผ่าน /P บอกว่า annotation อาศัยอยู่ตรงไหน ไม่ใช่บอกว่ามันเป็นเจ้าของอะไร

PDFium Component v3.126.2 จำลองความเป็นเจ้าของอย่างไร

PDFium Component v3.126.2 ถือว่า back-reference เป็นการเดินทาง กันมันออกจากการแพร่ role และตัดสินว่า key ไหนนับเป็น navigation จาก role เชิงโครงสร้างของ dictionary ที่ถือมัน ไม่ใช่จากชื่อ key เพียงอย่างเดียว ตารางสรุป navigation key ที่ไม่แบกความเป็นเจ้าของอีกต่อไป

dictionary เจ้าของkey ที่ถือเป็น navigationอ้างอิงสเปก
Page หรือ node Pages/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
annotation หรือ widget/PISO 32000-1 §12.5.2
dictionary ของ widget หรือ field/ParentISO 32000-1 §12.7.3
แผนภาพกราฟออบเจกต์ของ PDFium Component v3.126.2 แยก edge ของ payload ที่ถูกเป็นเจ้าของอย่าง /Contents กับ /Annots ซึ่งแพร่ role หน้ากับ annotation ออกจาก navigation edge อย่าง /P ของ widget ซึ่งไม่แบก role ใด พร้อม navigation key ราย dictionary เจ้าของ และ content stream คงเป็น prckPageContent แม้โดนปลอม /Type
v3.126.2 กัน back-reference ออกจากการแพร่ role role จึงเดินทางผ่านความเป็นเจ้าของจริงเท่านั้น content stream ยังเป็นเนื้อหาหน้าและ hint /P ตัดสินอะไรไม่ได้อีกต่อไป

ถ้ากรองด้วยชื่อ key ทั้งระบบจะเจาะรูใหม่ขึ้นมาแทน font หรือ XObject resource เรียกชื่อ /P, /Parent หรือ /Annots ได้อย่างชอบธรรม และ dictionary /Resources ที่ตัด entry /P ออกจากการแพร่ จะปล่อยให้ผู้โจมตีซ่อน XObject ที่หน้าเป็นเจ้าของไว้หลังชื่อ resource หน้าตาซื่อสัตย์ได้ ใน v3.126.2 ตัวกรอง navigation ทำงานเฉพาะเมื่อ dictionary เจ้าของเป็นหน้า, node Pages, annotation, widget หรือ field เท่านั้น ถ้า dictionary ตระกูลนี้แบก navigation key ซ้ำ เช่น /P สอง entry ใน widget เดียว ตัววิเคราะห์จะไม่เดาว่า viewer จะใช้สำเนาไหน การสร้าง role จะล้มและลายเซ็นกลายเป็น Indeterminate

กฎเพิ่มเติมอีกหลายข้อปิดเส้นทางการเปลี่ยนป้ายที่เหลือ:

  • node Pages เป็น root แบบ role หน้าด้วยตัวเอง resource ที่สืบทอดจากต้นไม้ Pages (ISO 32000-1 §7.7.3.4) จึงเข้าบริบทหน้าผ่านความเป็นเจ้าของจริง ไม่ใช่ผ่านการไต่ /Parent จากหน้าลูก
  • role แบบ annotation หรือ form ที่วิ่งไปถึง catalog, node Pages, หน้า, annotation หรือ field dictionary จะหยุดตรงนั้น เพราะออบเจกต์เชิงโครงสร้างพวกนี้ตั้ง role ของตัวเอง และ payload role ที่ไหลเข้ามาห้ามลบล้างมัน
  • role หน้าเป็นผู้ชี้ขาดระหว่างการจำแนก ออบเจกต์ที่หน้าเป็นเจ้าของเป็น prckPageContent แม้ revision หลังจะเขียนใหม่ด้วย /FT ที่ปลอมขึ้น ป้าย /Type /Annot หรือแชร์มันกับ appearance stream
  • Form XObject ที่ถูกใช้เป็น appearance ของ field หรือ annotation อย่างเดียวคงหมวด form หรือ annotation ของมัน การสร้าง appearance ใหม่ตามปกติหลังกรอกฟอร์มจึงยังถูกให้เกรดใต้กฎสิทธิ์ปกติ
  • widget ที่ไม่มี /FT ของตัวเอง resolve ชนิด field ที่สืบทอดมาผ่านโซ่ /Parent และโซ่ที่ resolve ไม่ได้จะทำให้การสร้าง role ล้ม ไม่ใช่ตกค่าเริ่มต้นเป็น annotation
  • role bits จากทุก revision หลังถูกรวมเข้า role ของ revision ที่ลายเซ็นคลุม update หลังจึงลบความสัมพันธ์ความเป็นเจ้าของหน้าที่เคยบันทึกไว้ไม่ได้ ด้วยการแกะ stream ออกก่อนแล้วค่อยแก้มันทีหลัง

fixed point แทนการนับรอบแบบตายตัว

ความสามารถถึงของ role ใน v3.126.2 รันเป็น work queue ที่วนจนกว่าจะไม่มีออบเจกต์ได้ role bit ใหม่ ซึ่งเป็น fixed point แท้ ๆ ไม่ว่าโซ่จะลึกหรือเลขออบเจกต์จะเรียงยังไง indirect array เช่น array /Contents ที่ถูกเก็บเป็นออบเจกต์ของตัวเองก็ถูกเดินด้วย ออบเจกต์หนึ่งได้ role bits ที่แตกต่างกันได้มากที่สุดสี่ตัว คิวจึงถูกจำกัดที่สี่ entry ต่อเลขออบเจกต์ เกินงบนี้ขึ้น prrResourceLimitExceeded reference ชี้ไปออบเจกต์อิสระ, generation ไม่ตรง หรือ header ออบเจกต์พัง ขึ้น prrMalformedRevisionChain และ payload ข้างใน object stream แบบบีบอัดขึ้น prrCompressedObjectUnresolved ทุกความล้มเหลวแบบนี้จบที่ prasIndeterminate ไม่มีวันจบที่คำตัดสินอนุญาต และเมื่อความล้มเหลวเกิดขณะสร้าง role ของ revision ที่คลุม ลายเซ็นจะไม่รายงาน Changes เลยด้วย

routine ต่อไปนี้แสดงรายการการแก้เนื้อหาหน้าที่รอดผ่านการวิเคราะห์แบบนี้ การเปลี่ยนแบบ prckPageContent ไม่มีวันถูกให้เกรด prdAllowed: DocMDP P=1, 2 หรือ 3 ทำให้มันเป็น prdDisallowed และลายเซ็นที่ไม่มี DocMDP ให้เกรดมัน prdSuspicious

uses
  SysUtils, TypInfo, PDFium, FPdfPades;

procedure ListPageContentEdits(const FileName: string);
const
  ShadowTag: array[Boolean] of string = (' (unreferenced shadow)', '');
var
  Pdf: TPdf;
  Report: TPadesRevisionAnalysisReport;
  Sig: TPadesSignatureRevisionAnalysis;
  Change: TPadesRevisionObjectChange;
  i, j: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    Report := Pdf.AnalyzeSignatureRevisions;   // เป็น record ไม่ต้อง free อะไร
    for i := 0 to High(Report.Signatures) do
    begin
      Sig := Report.Signatures[i];
      if not (prrPageContentChanged in Sig.Risks) then
        Continue;
      Writeln(Format('Signature %d (DocMDP P=%d): page content changed',
        [Sig.SignatureIndex, Sig.DocMdpPermission]));
      for j := 0 to High(Sig.Changes) do
      begin
        Change := Sig.Changes[j];
        if Change.Kind <> prckPageContent then
          Continue;
        Writeln(Format('  revision %d  object %d %d R  %s%s',
          [Change.RevisionIndex, Change.ObjectNumber, Change.Generation,
           GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Change.Decision)),
           ShadowTag[Change.IsAuthoritative]]));
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

เกิดอะไรขึ้นเมื่อ FieldMDP กับ annotation แชร์ออบเจกต์เดียวกัน

เมื่อ /V ของ field กับ /Contents ของ annotation ชี้ไปที่ indirect object ตัวเดียวกัน v3.126.2 คงกุญแจ FieldMDP ไว้เต็มรูป แม้การเปลี่ยนจะถูกจัดเป็นการแก้ annotation สถานการณ์นี้ประกอบด้วยมือได้ง่าย ๆ ผู้ลงลายเซ็นล็อก field Total ด้วย FieldMDP (ISO 32000-1 §12.8.2.4) แล้วผู้โจมตีทำให้ /Contents ของ annotation ข้อความหนึ่งชี้ไปยัง string object ตัวเดียวกับที่ถือค่าของ field ใต้ P=3 การแก้ annotation ได้รับอนุญาต ก่อนการแก้จึงเกิดเรื่องเขียนทับ string ที่แชร์กันนี้เปลี่ยนค่า field ที่ถูกล็อกโดยมีคำตัดสินเป็นอนุญาต

ออบเจกต์ตอนนี้แบกทั้ง role annotation กับ role form และคำตัดสินฝั่ง annotation จะเช็กฝั่ง form ซ้ำทุกครั้งที่ลายเซ็นมี transform แบบ FieldMDP:

  • ใต้ P=2 การเปลี่ยน annotation ถูกปฏิเสธตรง ๆ เหมือนเดิมทุกอย่าง
  • เมื่อ FieldMDP All ทุก field ถูกล็อก การเปลี่ยนที่แชร์กันจึงเป็น prdDisallowed
  • เมื่อ FieldMDP Include หรือ Exclude ตัววิเคราะห์ไล่ scalar ที่แชร์กลับไปหาชื่อ field เดียวไม่ได้ คำตัดสินจึงเป็น prdIndeterminate แทนการเดา
  • ไม่มี FieldMDP กฎ annotation ของ P=3 ใช้ได้และการเปลี่ยนยังเป็นอนุญาต
แผนภาพคำตัดสิน FieldMDP ของ PDFium Component ที่ /V ของ field Total ที่ถูกล็อก กับ /Contents ของ annotation ชี้ไปที่ indirect object เดียวกัน แตกกิ่งบน DocMDP P=2, FieldMDP All, FieldMDP Include หรือ Exclude กับไม่มี FieldMDP ไปสู่คำตัดสิน prdDisallowed, prdIndeterminate หรือ prdAllowed สำหรับการแก้ที่แชร์กันชุดเดียวกัน
เมื่อ indirect object เดียวแบกทั้ง role annotation กับ role form คำตัดสินฝั่ง annotation จะเช็กกุญแจ FieldMDP ซ้ำ การแก้ชุดเดียวกันจึงกางตั้งแต่อนุญาตไปจนถึงห้ามและระบุไม่ได้

รายละเอียดการรายงานหนึ่งข้อสำคัญกับโค้ดประตูรับไฟล์ กรณีที่แชร์กันถูกรายงานเป็น Kind = prckAnnotation พร้อม Decision = prdIndeterminate และ prrFieldMdpUnresolved ถูกเติมเข้าชุด risk เฉพาะการเปลี่ยนที่ถูกจัดเป็น form field ประตูที่ไล่หา prrFieldMdpUnresolved แล้วเมิน Status จะพลาดกรณีนี้ทั้งหมด

โค้ด Delphi ควร fail closed ต่อผลวิเคราะห์ revision อย่างไร

โค้ด Delphi ควรรับเอกสารที่ลงลายเซ็นเมื่อสถานะการวิเคราะห์เป็น prasNoLaterChanges หรือ prasAllowed และไม่มี structural risk เท่านั้น และควรถือ prasIndeterminate กับ prasSuspicious เป็นสิ่งที่เชื่อไม่ได้ ไม่ใช่คำเตือนให้ log แล้วผ่านต่อ Indeterminate แปลว่าตัววิเคราะห์พิสูจน์ไม่ได้ว่า revision ที่ตามมาได้รับอนุญาต สำหรับผู้โจมตี input ที่ผลิต Indeterminate ได้แน่นอนมีค่าเท่ากับ input ที่ผลิต Allowed ถ้าโค้ดของคุณปล่อยมันผ่าน ฟังก์ชันระดับ global AnalyzePadesSignatureRevisions รับ TStream อะไรก็ได้และอ่านจากตำแหน่ง 0 เหมาะกับ upload handler ที่ไม่จำเป็นต้อง render เอกสาร

uses
  Classes, SysUtils, TypInfo, FPdfPades;

const
  // definition ซ้ำถูกจดไว้โดยไม่ลดระดับ Status
  BlockingRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
    prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
    prrSignatureObjectRedefined, prrCompressedObjectUnresolved,
    prrResourceLimitExceeded];

function SignedRevisionsAcceptable(const FileName: string;
  out Reason: string): Boolean;
var
  Source: TFileStream;
  Report: TPadesRevisionAnalysisReport;
begin
  Result := False;
  Reason := '';
  Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Report := AnalyzePadesSignatureRevisions(Source);
  finally
    Source.Free;
  end;
  if Report.SignatureCount = 0 then
  begin
    Reason := 'no signature anchors the analysis';
    Exit;
  end;
  if Report.Risks * BlockingRisks <> [] then
  begin
    Reason := 'structural risk in the revision chain';
    Exit;
  end;
  case Report.Status of
    prasNoLaterChanges, prasAllowed:
      Result := True;
  else
    // prasIndeterminate กับ prasSuspicious คือการปฏิเสธ ไม่ใช่คำเตือน
    Reason := 'revision status ' +
      GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
  end;
end;

สองขอบเขตควรพูดให้ชัด TPadesRevisionAnalysisReport ไม่ได้พูดอะไรเรื่องความสมบูรณ์ของ CMS หรือความน่าเชื่อถือของใบรับรอง ประตูนี้จึงนั่งเคียงกับการตรวจสอบเชิงเข้ารหัสและความน่าเชื่อถือ ไม่ใช่มาแทนที่พวกมัน และกราฟความเป็นเจ้าของที่ถูกต้องไม่ได้ทำให้ P=3 ปลอดภัยกับทุก workflow P=3 อนุญาต annotation จริง ๆ และ annotation ที่มี appearance อ่านไม่ออกสามารถคลุมข้อความที่ลงลายเซ็นไว้ได้โดยไม่แตะ content stream เลย ถ้าเอกสารที่ผ่านการรับรองของคุณคือสัญญา ไม่ใช่สำเนาสำหรับรีวิว จงรับรองด้วย P=2 หรือส่งการเปลี่ยน annotation ที่ได้รับอนุญาตไปให้คนตรวจ ตามตัวช่วยนี้:

function AllowedAnnotationEditsUnderP3(
  const Report: TPadesRevisionAnalysisReport): Integer;
var
  i, j: Integer;
begin
  Result := 0;
  for i := 0 to High(Report.Signatures) do
    if Report.Signatures[i].DocMdpPermission = 3 then
      for j := 0 to High(Report.Signatures[i].Changes) do
        if (Report.Signatures[i].Changes[j].Kind = prckAnnotation) and
           (Report.Signatures[i].Changes[j].Decision = prdAllowed) then
          Inc(Result);
end;

เช็กลิสต์ตรวจ revision ของลายเซ็น

ใช้ลิสต์นี้เช็กว่าเส้นทางตรวจสอบของคุณเคยโดนเปิดช่องไว้หรือเปล่า และตอนนี้ fail closed หรือยัง:

  • บิลด์ PDFium Component ก่อน v3.126.2 อาจรายงาน prasAllowed ให้การแก้เนื้อหาหน้าในเอกสาร DocMDP P=3 รัน TPdf.AnalyzeSignatureRevisions ซ้ำบนไฟล์ P=3 ที่ผ่านการรับรองซึ่งบิลด์เก่าเคยรับไว้
  • เช็กซ้ำเอกสาร P=3 ที่มีกุญแจ FieldMDP ในกรณีที่ค่า field กับ annotation อาจแชร์ indirect object กัน
  • รับเฉพาะ prasNoLaterChanges กับ prasAllowed ถือ prasIndeterminate กับ prasSuspicious เป็นสิ่งที่เชื่อไม่ได้
  • เทสต์ Report.Risks ควบคู่กับ Report.Status เพราะ prrDuplicateObjectDefinition เองไม่เปลี่ยนสถานะ
  • อย่าอ่าน array Changes ที่ว่างเปล่าเป็นผลสะอาดเมื่อสถานะลายเซ็นเป็น Indeterminate การสร้าง role ที่ล้มจะรายงานว่าไม่มีการเปลี่ยนเลย
  • อย่าพึ่ง prrFieldMdpUnresolved ตัวเดียวในการจับปัญหา FieldMDP เพราะกรณี annotation ที่แชร์กันโผล่ผ่านคำตัดสินกับสถานะเท่านั้น
  • ตัดสินใจว่าการเปลี่ยน annotation ที่ได้รับอนุญาตใต้ P=3 ต้องผ่านคนตรวจใน workflow ของคุณหรือไม่
  • วิเคราะห์จากไบต์ไฟล์ต้นฉบับ เอกสารที่ถูกเขียนใหม่ด้วย SaveAs ไม่มีโซ่ revision อยู่แล้ว

การวิเคราะห์ revision เป็นเพียงชั้นหนึ่งของการตรวจลายเซ็น จับคู่กับการสำรวจ digital signature กับระดับ PAdES ของ PDF สำหรับ dictionary กับระดับ baseline และกับการตรวจความเสี่ยงความปลอดภัยของ PDF แบบกว้างสำหรับ JavaScript, launch action กับไฟล์ฝังตัว TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions กับกราฟ role ที่รู้เรื่องความเป็นเจ้าของที่เล่าไว้ในนี้ มาพร้อมในPDFium Component สำหรับ Delphi, C++Builder และ Lazarus