HotPDF, component PDF cho Delphi, giờ chặn signature wrapping: kể từ v2.759.0, cả VerifyLoadedSignatureEx lẫn bộ validator theo lô đòi hỏi khoảng hở giữa hai phân đoạn /ByteRange phải chính xác là chuỗi hex /Contents, kể cả các delimiter, còn v2.761.0 thêm AddLoadedSignedSignatureField để một chữ ký thứ hai có thể được nối vào một PDF đã ký như một bản sửa tăng dần sạch sẽ. Hai thay đổi này thuộc về nhau, vì một chữ ký thứ hai đúng đắn chính xác là bố cục mà verifier khắt khe hơn kỳ vọng
Tình huống phơi bày vấn đề rất đời thường. Một hợp đồng được nhà cung cấp ký, rồi chuyển tới một người phê duyệt phải ký đè mà không làm phiền chữ ký đầu tiên. Bản sửa thứ hai được nối sau bản đầu, /ByteRange riêng của nó phủ trọn tệp đã lớn lên, và cả hai chữ ký lẽ ra phải verify. Tự thân tới đó nghĩa là tự viết một phân đoạn tăng dần, và cái fixture test đã làm đúng việc ấy lại hóa ra là một cấu trúc signature-wrapping mẫu giáo khoa mà verifier cũ vui vẻ chấp nhận. Nếu bạn chưa từng nhìn API verification trước đây, hướng dẫn xác thực chữ ký số PDF với HotPDF đã bàn nền tảng mà bài này xây trên đó
Chính xác điều gì thuộc về khoảng hở ByteRange?
Khoảng hở phải chứa trọn vẹn giá trị /Contents và không hơn: ISO 32000-1 §12.8.3.3 nói chuỗi thập lục phân, cùng các delimiter < và >, nằm vừa khít trong khoảng giữa hai dải byte, và ISO 32000-2 §12.8.1 mang cùng luật đó tiến về phía trước. Bảng 252 và các tài liệu PAdES chỉ nói digest loại trừ giá trị Contents, một câu dễ đọc nhầm thành chỉ loại trừ các chữ số hex. Các bản HotPDF trước đây đọc theo kiểu ấy: PreparePDFForSigning và khâu chuẩn bị CMS dạng stream đã hash luôn cả hai dấu ngoặc nhọn, kèm một comment trong source khăng khăng rằng các ngoặc phải được phủ. Các validator so khoảng hở với giá trị chữ ký gắn cờ bố cục đó là một byte range vô hiệu, nên v2.759.0 dời cả hai delimiter ra khỏi các dải được ký. Một phép kiểm độc lập nhanh trên bất kỳ tệp đã ký nào là nhìn hai byte: byte tại offset ByteRange[1] phải là < và byte tại offset ByteRange[2] - 1 phải là >
Vì sao phép kiểm khoảng hở khác rỗng lại trượt mất signature wrapping?
Một phép kiểm khoảng hở khác rỗng chỉ chứng minh rằng có thứ gì đó đã bị bỏ ra ngoài digest, chứ không chứng minh đó là gì, và đó là toàn bộ mặt tấn công. Placeholder /Contents được đặt chỗ với hàng nghìn chữ số zero, trong khi một container CMS thật hiếm khi lấp đầy nó. Kẻ tấn công có thể đóng chuỗi hex sớm bên trong phần đệm zero ấy bằng một >, ghi các object mới hay một bản sửa giả vào phần không gian đặt chỗ còn lại, và để nguyên các dải byte. Chữ ký CMS vẫn verify vì mọi byte được ký đều nguyên vẹn, các dải vẫn bắt đầu từ 0 và kết thúc ở kích thước tệp, còn verifier HotPDF cũ báo svValid với CoversWholeDocument đặt là True. Trong lúc đó, một PDF reader parse bất cứ thứ gì nằm trong cái hố chưa ký ấy
HotPDF giờ coi khoảng hở là dữ liệu phải được xác thực từng byte một. Verifier đọc khoảng hở, cắt các delimiter, chỉ chấp nhận chữ số hex cộng whitespace PDF (tab, line feed, form feed, carriage return, dấu cách), giải mã các chữ số và đòi kết quả phải bằng đúng /Contents của dictionary chữ ký. Bất cứ thứ gì khác đều hạ kết quả xuống svInvalidByteRange. Phép kiểm chạy ở cả đường một-chữ-ký lẫn ValidateLoadedSignatureBatch, thứ từng giữ logic phủ riêng và cần cùng bản sửa đó. Các tệp do HotPDF sinh trước v2.759.0, mà khoảng hở chỉ chứa chữ số với các ngoặc nằm ngay trong dải, vẫn verify bình thường, nên các tài liệu lưu trữ không bỗng dưng đỏ lửa
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('SignedTwice.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Status := Pdf.VerifyLoadedSignatureEx(I, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln(Info.FieldName, ': valid, covers the whole file')
else
Writeln(Info.FieldName, ': valid, ',
Info.UnsignedTrailingBytes, ' bytes appended later');
svInvalidByteRange:
Writeln(Info.FieldName, ': ByteRange gap rejected (wrapping?)');
else
Writeln(Info.FieldName, ': failed, status ', Ord(Status));
end;
end;
finally
Pdf.Free;
end;
end;
Thêm một chữ ký thứ hai vào PDF đã ký bằng cách nào?
Mở tệp đã ký bằng BeginIncrementalUpdate, gọi AddLoadedSignedSignatureField, lưu bằng SaveIncrementalUpdate, rồi ký tệp đã chuẩn bị bằng class function THotPDF.SignPDFWithPFX. Trước v2.761.0, công thức được tài liệu hướng dẫn là gọi THPDFPage.AddSignedSignatureField sau BeginIncrementalUpdate không thể chạy nổi, vì CurrentPage là nil trong chế độ tăng dần và chẳng gì gắn nổi một placeholder /V lên một field trên tài liệu đã nạp. Phương thức mới dựng widget trên trang đã nạp và treo cùng dictionary placeholder mà đường tài liệu mới dùng dưới /V, nên cả hai đường ký chia sẻ một serialization. Riêng chữ ký đầu tiên, bài viết về tạo chữ ký số PAdES trong Delphi đã đi qua pipeline PFX
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Trang 0, hình chữ nhật widget theo point, 8192 byte đặt chỗ cho CMS
FieldIndex := Pdf.AddLoadedSignedSignatureField(0, 320, 60, 520, 120,
'ApproverSignature', 8192);
if FieldIndex < 0 then
raise Exception.Create('Page index out of range');
Pdf.SaveIncrementalUpdate('Prepared.pdf');
finally
Pdf.Free;
end;
if not THotPDF.SignPDFWithPFX('Prepared.pdf', 'SignedTwice.pdf',
'approver.pfx', 'pfx-password') then
raise Exception.Create('Second signature failed');
end;
AddLoadedSignedSignatureField được cố tình làm dịu hơn các anh em của nó. Các bộ dựng field AddLoaded* khác đặt /NeedAppearances true lên AcroForm, thứ bảo viewer tái tạo appearance của field; trên một tài liệu đã ký, phép tái tạo ấy có thể viết lại content đã ký, nên phương thức mới gỡ cờ đó trở lại trừ khi nguồn đã sẵn mang nó. /SigFlags giữ giá trị gốc OR 3 (SignaturesExist cộng AppendOnly, ISO 32000-1 Bảng 219). Bạn cũng chẳng cần gọi MarkDirty trên trang: việc thêm vào /Annots và /Fields tự lan cờ dirty tới indirect object sở hữu, còn một lần đánh dấu trang tường minh chỉ kéo dictionary trang chưa đổi gì vào bản sửa mới, thứ phân tích revision rồi báo là một sửa đổi trang. Cuối cùng, placeholder ghi /ByteRange trước /Contents, vì bộ vá định vị sentinel /ByteRange trước rồi mới tìm tiếp chuỗi hex khớp phía sau
Chuyện gì đổi khi một external signer hay HSM sinh CMS?
Workflow chẳng đổi gì, nhưng các offset giờ mang đúng nghĩa mà đặc tả nói. PreparePDFForSigning trả về hai dải 0-based mà khoảng hở là trọn chuỗi /Contents, và ContentsHexStart là chỉ số 1-based của chữ số hex đầu tiên trong AnsiString. Một CMS ngắn hơn được đệm 0 ở đuôi, trước dấu > đóng. Vì PreparePDFForSigning vá sentinel chưa vá đầu tiên mà nó gặp, hãy chuẩn bị đúng một placeholder cho mỗi bản sửa, và ưu tiên InsertSignatureHexAt với các offset được trả về hơn là InsertSignatureHex dựa trên tìm kiếm khi các chữ ký trước đó đã có trong tệp
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // helper của bạn
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Khoảng hở là trọn chuỗi hex: '<' chấm dứt range 1, '>' đứng trước range 2
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // CMS signer của bạn, DER hex
if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
raise Exception.Create('CMS does not fit the reserved space');
SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf'); // helper của bạn
end;
Các phép kiểm mới có giới hạn ở đâu?
Phép kiểm khoảng hở bịt một cái hố cụ thể và không nên được thổi phồng. svValid vẫn nghĩa là toàn vẹn byte cộng một khóa khớp với chứng chỉ nhúng; niềm tin vào chứng chỉ đó là một quyết định riêng. Khoảng hở chỉ được xác thực khi verifier có byte nguồn, mà VerifyLoadedSignatureEx đọc từ tệp đã nạp và các overload TStream nhận từ bạn. Với chữ ký đầu tiên trong một tệp đã ký đè, CoversWholeDocument đúng là False, còn việc bản sửa nối thêm chỉ thêm một chữ ký hay còn đổi cả trang là câu hỏi của DocMDP, FieldMDP và phân tích revision trong HotPDF. Để ý thêm rằng phép kiểm PDF MAC đính kèm so các offset với vị trí < và >, nên nó chấp nhận cả bố cục cũ lẫn mới; bất kỳ tool riêng nào của bạn hard-code các offset trước v2.759.0 sẽ hỏng đầu tiên khi gặp một tệp vừa ký
Nếu ứng dụng Delphi hay C++Builder của bạn ký, ký đè hay audit PDF, lối an toàn nhất là để một thư viện sinh và xác thực cùng một bố cục. HotPDF, component PDF Delphi thuần bản đóng gói xác thực khoảng hở khắt khe hơn, chữ ký thứ hai tăng dần và các hook external-signer được trình bày ở trên trong đúng một component