Lấy một PDF hóa đơn đã mang sẵn mã hóa AES-256 và yêu cầu PDFium Component cho Delphi và C++Builder (PDFiumPas) đóng dấu nó PDF/A để lưu trữ, hoặc ký nó bằng PAdES, thông qua một cập nhật gia tăng thay vì một lượt ghi lại đầy đủ. Thư viện sẽ không làm được điều đó bằng cách vá trực tiếp các byte đã mã hóa: sáu bộ tiêm dấu hiệu tuân thủ chuẩn của nó phát hiện một entry /Encrypt đã tồn tại và chuyển nguồn qua nguyên vẹn từng byte, và bộ ký PAdES của nó ném ra một ngoại lệ thay vì phát ra một chữ ký mà không trình xác thực nào chấp nhận
Đó là một câu hỏi khác với việc kiểm toán một PDF bạn không tạo ra để tìm rủi ro ẩn, thứ là một bài tập chỉ-đọc riêng của nó. Bài viết này nói về phía ghi của cùng ranh giới tin cậy đó: code của chính bạn được phép làm gì với một file mà các byte của nó đã bị khóa sau mật khẩu của người khác, ngay khi code đó cố thêm bất cứ thứ gì vào nó sau này
ISO 32000-1 yêu cầu gì khi bạn cập nhật một PDF đã mã hóa?
ISO 32000-1 §7.5.6 yêu cầu trailer của một cập nhật gia tăng phải lặp lại mọi entry từ trailer trước đó ngoại trừ /Prev, và Bảng 15 liệt kê /Encrypt trong số các entry một trailer có thể mang. Bỏ nó khỏi trailer mới và một trình đọc tuân thủ chuẩn không có lý do gì để nghi ngờ việc thiếu sót đó: trailer mới nhất là có thẩm quyền, nên một trình đọc không tìm thấy /Encrypt ở đó quyết định toàn bộ file không được mã hóa và cố phân tích thân file cũ hơn, vẫn còn được mã hóa như các byte thuần túy. Giữ /Encrypt trong trailer mới nhưng viết các đối tượng riêng của cập nhật dưới dạng văn bản thuần túy, và lỗi chỉ chuyển sang một bước sau: trình đọc phát hiện đúng việc mã hóa, chạy mọi đối tượng nó chạm tới qua mật mã của file, kể cả các đối tượng mới chưa bao giờ được mã hóa ngay từ đầu, và nhận lại nhiễu cho nội dung từng hoàn toàn dễ đọc trước khi giải mã chạm vào nó. Cả hai lỗi đều tạo ra một file trông như một cập nhật gia tăng bình thường, được định dạng tốt ở cấp byte, cho đến khi một trình đọc tuân thủ chuẩn mở nó
Sáu bộ tiêm dấu hiệu, một cổng mã hóa v2.14.2
PDFiumPas gửi kèm sáu bộ tiêm dấu hiệu cấp byte, một cho mỗi tập con PDF theo ISO mà nó có thể gán nhãn: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1), và PDF/VT-1 (ISO 16612-2). Mỗi bộ nhận các byte mà FPDF_SaveAsCopy riêng của PDFium đã ghi và xếp lớp một cập nhật gia tăng thứ hai, nhỏ hơn lên trên chúng: một luồng metadata XMP mới, một chỉnh sửa từ điển catalog trỏ vào nó, và với các tập con hướng-in một OutputIntent và profile ICC. Kể từ v2.14.2, mỗi bộ trong InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, và InjectPdfVTMarkers đọc trailer nguồn trước, và nếu nó báo cáo một entry /Encrypt đã tồn tại, copy nguồn qua đích không sửa đổi và trả về ngay lập tức. Không có XMP, không có OutputIntent, không có chỉnh sửa catalog — caller nhận lại file gốc, nguyên từng byte
var
Src, Dst: TFileStream;
Opts: TPdfXSaveOptions;
begin
Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
try
Opts.Conformance := pxc4;
InjectPdfXMarkers(Src, Dst, Opts);
// pdfx-attempt.pdf is byte-identical to the source: still encrypted,
// no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
// nothing was corrupted either
finally
Dst.Free;
Src.Free;
end;
end;
Được phép mã hóa không giống với an toàn để tiêm vào
PDF/E-1 và PDF/R-1 đều tường minh cho phép tài liệu host của chúng được mã hóa ở cấp đặc tả, nghe như một miễn trừ cho đến khi bạn nhìn vào những gì thực sự phải xảy ra trên đĩa. ISO 24517-1 §6.3 cho phép mã hóa với PDF/E-1, và ISO 23504-1 §6.2.3 cho phép nó với PDF/R-1 miễn là header khai báo %PDF-2.0. Không điều khoản nào trong hai điều khoản đó nói gì về việc liệu một bộ hậu xử lý cấp byte có thể an toàn thêm một đối tượng văn bản thuần túy vào container đã mã hóa đó hay không, và nó không thể, vì cùng lý do §7.5.6 áp dụng cho mọi tập con khác. Các bộ xác thực tuân thủ riêng của PDFiumPas cho hai hồ sơ này, ValidatePdfECompliance và ValidatePdfRCompliance, ghi lại sự hiện diện của /Encrypt có chủ đích mà không đánh dấu nó là một khiếm khuyết, đúng đắn cho một bộ xác thực chỉ-đọc không bao giờ ghi một byte nào. Đó cũng là một khuôn mẫu dễ lướt qua và giả định bộ tiêm anh em không cần một bảo vệ riêng biệt, trong khi bộ tiêm mới là hàm duy nhất trong cặp đó thực sự phải từ chối
SaveAsPdfX có âm thầm giải mã tài liệu của bạn không?
Có, bất cứ khi nào bạn đi qua các phương thức tiện lợi công khai thay vì gọi trực tiếp một bộ tiêm. Mỗi phương thức trong TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, và SaveAsPdfVT render tài liệu hiện tại vào một luồng tạm bằng SaveAs(Tmp, saRemoveSecurity) trước khi giao các byte đó cho bộ tiêm tương ứng của nó. saRemoveSecurity ánh xạ đến cờ FPDF_REMOVE_SECURITY riêng của PDFium, nên bản sao tạm mà bộ tiêm nhận được chưa bao giờ được mã hóa ngay từ đầu, và bảo vệ /Encrypt của bộ tiêm không bao giờ có lý do để kích hoạt. Đầu ra mang các dấu hiệu PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, hay PDF/VT-1 của bạn, nhưng nó không còn được bảo vệ bởi bất cứ mật khẩu nào đã mở nguồn
Sự đánh đổi đó vô hình cho đến khi ai đó ở hạ nguồn mở bản sao lưu trữ "được bảo vệ" mà không cần mật khẩu và nhận ra nó cứ thế hoạt động. Cách sửa không phải một lệnh gọi phương thức khác; PDFiumPas không có một đối tác saAddSecurity nào để ghép với saRemoveSecurity, vì engine PDFium bên dưới chưa bao giờ được xây dựng để viết mã hóa mới, chỉ để loại bỏ nó. Nếu cả hai thuộc tính đều quan trọng cho một file, mã hóa phải là một bước riêng biệt bạn tự sở hữu, áp dụng sau các dấu hiệu tuân thủ chuẩn, không gộp vào cùng lệnh gọi SaveAsPdfA
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret'; // needed to open the source at all
Pdf.FileName := 'signed-encrypted-invoice.pdf';
Pdf.LoadDocument;
Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
// invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
// ran first inside SaveAsPdfA: the output opens without a password
finally
Pdf.Free;
end;
end;
Điều gì xảy ra khi bạn ký một PDF đã mã hóa bằng PAdES?
PDFiumPas từ chối thẳng thừng, thay vì âm thầm bỏ qua yêu cầu theo cách một bộ tiêm dấu hiệu làm. TPdf.SignPades và SignPadesToStream đều định tuyến qua một SignPadesBytes nội bộ, và điều đầu tiên nó làm sau khi phân tích trailer nguồn là kiểm tra /Encrypt. Nếu entry hiện diện, nó ném ra EPadesCrypto với thông điệp "SignPadesBytes: the source document is encrypted; remove encryption before signing" thay vì tiếp tục thêm nữa. InjectPadesDssMarkers, hàm nhúng chứng chỉ, phản hồi OCSP, và CRL cho việc xác thực dài hạn, áp dụng đúng kiểm tra giống hệt vì đúng lý do giống hệt, với thông điệp riêng của nó: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"
Lý lẽ ở đây nghiêm ngặt hơn việc chuyển-qua âm thầm của các bộ tiêm dấu hiệu, và có chủ đích như vậy. Một lượt chuyển-qua âm thầm an toàn cho một con dấu PDF/A vì bỏ qua nó để lại cho bạn cùng PDF hợp lệ bạn bắt đầu, chỉ là không được gán nhãn. Việc ký không thể thất bại êm ái như vậy: một chữ ký âm thầm chưa bao giờ được thêm vào trông, đối với bất kỳ code gọi nào chỉ kiểm tra một kết quả boolean, chính xác giống như một chữ ký đã được thêm thành công. EPadesCrypto kế thừa từ lớp Exception thông thường, nên bắt nó là xử lý ngoại lệ bình thường, không phải một quy ước điều khiển luồng đặc biệt bạn phải học
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret';
Pdf.FileName := 'encrypted-contract.pdf';
Pdf.LoadDocument;
try
Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
except
on E: EPadesCrypto do
// E.Message: 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
end;
finally
Pdf.Free;
end;
end;
Sắp xếp thứ tự con dấu tuân thủ chuẩn, chữ ký, và mã hóa
Cách sửa thực tế là sắp xếp thứ tự, không phải một thư viện khác. Áp dụng các dấu hiệu PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, hay PDF/VT-1 trước, thêm bất kỳ chữ ký PAdES nào tiếp theo, và chỉ khi đó mới chạy bất cứ bước nào trong pipeline của bạn thực sự sở hữu mã hóa, dù đó là một bộ ghi PDF chuyên dụng, một thiết bị ký, hay triển khai AES của riêng bạn. Lớp cập nhật gia tăng của PDFiumPas khớp tự nhiên vào giữa chuỗi đó, nối thêm các đối tượng nhỏ, có mục tiêu lên một file đã hoàn tất theo cách khác, và mã hóa thuộc về cuối chính vì nó là thao tác duy nhất trong chuỗi mà bản thân PDFiumPas không thể thực hiện hay đảo ngược
Không điều nào trong số này thay đổi cách PDFiumPas đọc dữ liệu trailer và cross-reference mà mọi cập nhật gia tăng phụ thuộc vào, một nguồn tinh tế riêng của nó một khi các luồng xref bước vào bức tranh; xác thực luồng object và xref của một PDF nói đến cách cùng đường đọc trailer đó xử lý các cấu trúc nén PDF 1.5+. Và một khi một tài liệu đã sẵn sàng cho thứ gì đó mạnh hơn một con dấu tuân thủ chuẩn, ký một PDF bằng chữ ký PAdES B-B trong Delphi là nơi SignPades tiếp quản đúng từ điểm bài viết này dừng lại
Các bộ tiêm dấu hiệu và các phương thức SignPades được mô tả ở đây đi kèm sẵn trong PDFium Component dành cho Delphi và C++Builder, cùng với việc render và kiểm tra chỉ-đọc mà PDFium cung cấp gốc