PDFium VCL ký tài liệu PAdES với private key nằm trong macOS Keychain qua một backend resolve mọi symbol của Security và CoreFoundation tại runtime bằng dlopen và dlsym. Không thứ gì bị bind lúc link, nghĩa là một symbol gõ sai tên sẽ lộ diện dưới dạng KeychainAvailable trả False cùng KeychainMissingSymbols nêu đích danh thủ phạm, thay vì một linker error hay một cú crash
Lựa chọn đó bị ép ra từ một ràng buộc khó chịu, và cách xử lý nó thì dùng lại được khắp nơi. Unit được viết trên một máy không có macOS SDK, nên từng tên framework symbol và từng constant đều đến từ tài liệu, và không cái nào được đối chiếu với một header. Phản ứng sai trước tình thế đó là viết code cẩn thận rồi hy vọng. Phản ứng đúng là sắp xếp để những sai lầm không thể tránh tự công bố mình dưới dạng dễ định vị nhất có thể
Vì sao dynamic binding là lựa chọn đúng ngay cả trên chính platform đích
Vì nó biến một hạng failure chặn đứng chương trình thành một hạng failure tự báo cáo. Một reference framework link tĩnh bị sai sẽ fail lúc link trên platform đích và không bao giờ link được ở nơi khác. Một cái bind động bị sai cho ra một backend không khả dụng cùng một danh sách tên chưa resolve, và lần chạy đầu trên một máy Mac biến câu hỏi vì sao nó không khả dụng thành đúng một dòng nêu một lỗi gõ
Còn một lợi ích thứ hai trả cổng mỗi ngày chứ không một lần. Vì unit không link framework nào, nó compile trên mọi platform, nên bản build Windows thường nhật tiếp tục kiểm tra syntax, type và uses clause của nó. Một unit chỉ compile được trên platform mà cả team chẳng ai có là một unit không có compiler nào nhìn nó, và nó mục rữa âm thầm qua từng đợt refactor của một shared type
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // cài làm signer backend cho PAdES
ConfigureKeychainCmsVerifier; // đồng thời làm verification backend
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, hoa thường tùy ý
Options.PaddingScheme := psRsaPss;
end;
Hai loại exported symbol, hai cách đọc
Đây là chi tiết rối nhất trong toàn bộ phần binding, và làm ngược nó thì compile sạch mà fail lúc runtime. CoreFoundation và Security export hai loại thứ khác nhau một trời một vực qua cùng một lời gọi dlsym, và code phải biết cái nào là cái nào
Các named constant như các keychain item class key hay các CoreFoundation boolean singleton được export dưới dạng biến mà nội dung của biến chính là CFStringRef hay CFBooleanRef bạn cần. dlsym trả về địa chỉ của biến đó, nên bạn phải dereference một lần để lấy giá trị. Các cấu trúc bảng callback như dictionary key và value callback được export dưới dạng cấu trúc, và dlsym trả về địa chỉ của cấu trúc — đúng con trỏ mà hàm tạo dictionary kỳ vọng. Dereference cái đó thì bạn đang truyền machine word đầu tiên của cấu trúc đi như thể nó là một pointer
Cả hai lỗi đều không cho compile error, và cũng không cho runtime error rõ ràng. Bạn nhận một garbage pointer fail ở đâu đó phía sau. Cách khiến sự phân biệt này không thể làm sai là ngừng dựa vào trí nhớ: hai hàm helper, một cái bind rồi dereference và một cái bind thôi, để call site tuyên bố nó đang xin loại symbol nào và helper lo phần còn lại
// Exported variable: dlsym đưa ra địa chỉ của một biến đang giữ
// CFTypeRef, nên dereference một lần
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Exported structure: dlsym đưa ra địa chỉ CỦA structure, và đó
// chính là thứ API muốn. Đừng dereference
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Vì sao một chữ ký RSA-PSS cần hai đường fallback riêng?
Vì thuật toán có thể vắng mặt theo hai cách độc lập, và chỉ một trong hai là câu hỏi version. Constant của thuật toán digest-signing PSS xuất hiện từ macOS 10.13, nên trên hệ cũ hơn symbol đơn giản không có đó và binding nhận nil. Đó là phần check version. Riêng phần kia, trên một hệ mà constant có mặt, một key cụ thể vẫn có thể từ chối nó, và framework trả lời câu hỏi đó qua SecKeyIsAlgorithmSupported cho đúng key đó. Một key nằm trên phần cứng hay một key mang attribute gắt kèo có thể khước từ PSS trong khi một key software trên cùng máy chấp nhận
Cả hai đường phải hội tụ về cùng một fallback: chuyển sang PKCS#1 v1.5. Và phần sống còn là fallback phải đổi cả algorithm identifier được ghi vào cấu trúc CMS, chứ không chỉ lời gọi ký. Emit một PSS algorithm identifier trong khi thật sự sinh chữ ký v1.5 cho ra một tài liệu mọi verifier từ chối thẳng — tệ hơn hẳn việc báo PSS không được hỗ trợ. Một downgrade thì chấp nhận được, còn sự lệch nhau giữa cái bạn khai báo và cái bạn đã làm thì không, và đó là luật chung cho code chữ ký chứ không phải một quirk của macOS. Phần ý nghĩa cấp chữ ký được trình bày trong ký PDF với PAdES B-B
Encode chữ ký ECDSA, và một sự đảo ngược đáng chú ý
Đường đường cong elliptic không cần bất kỳ conversion nào trên macOS, và đó là điều ngược lại với những gì một binding PKCS#11 đòi hỏi. Thuật toán digest-signing cho ECDSA của Security framework trả về chữ ký sẵn ở dạng X9.62 DER — đúng cái CMS muốn. Một token PKCS#11 trả về cặp P1363 thô độ rộng cố định thay vào đó, và nó phải được encode lại trước khi vào một cấu trúc chữ ký
Vậy là hai backend triển khai cùng một interface lại cần cách xử lý ngược nhau cho cùng một thuật toán, và không bên nào sai. Đây đúng là loại khác biệt mà một tầng abstraction phải hấp thụ thay vì phơi ra: tầng PAdES nhờ một provider ký, và các quy ước encode ở lại trong provider. Nếu chúng rò rỉ lên trên, mọi caller đều ôm theo một conditional theo backend. Hình dáng tương tự xuất hiện trong câu chuyện ký từ xa được kể trong remote PAdES signing session với một HSM
// Provider interface giống hệt nhau trên mọi platform, nên việc chọn
// là một quyết định khởi động chứ không phải theo từng lời gọi
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Windows CNG provider được platform unit cài sẵn
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Từ đây trở đi, code ký số trung lập về platform
Signer := ResolvePadesSigner(Options);
Các luật reference counting cách nhau đúng ba dòng
Quản lý bộ nhớ Core Foundation đi theo quy ước đặt tên, và cái bẫy ở đây là những hàm với quy ước khác nhau đứng cạnh nhau trong cùng một đoạn code ngắn. Một hàm gets một certificate từ một trust object trả về một borrowed reference không được release. Các hàm copy một signer certificate hay copy dữ liệu của nó trả về owned reference bắt buộc phải release. Ba lời gọi liên tiếp, hai luật sở hữu, và release nhầm cái borrowed không fail ngay tại dòng đó. Nó làm hỏng một retain count rồi hạ gục một thứ không liên quan nào đó về sau
Cách giảm thiểu là đọc động từ trong tên của mọi framework function trước khi viết phần dọn dẹp, mỗi lần một, không ngoại lệ. Đó là bản tương đương CoreFoundation của việc check xem một API trả về bản copy hay một view, và cái giá của việc sai là một crash chớp giật thay vì một error
Backend này không tuyên bố điều gì
Nó chưa từng chạy trên macOS tại thời điểm viết bài, và nói thẳng như vậy hữu ích hơn một lời bảo đảm chìm. Những thứ chứng minh được thì hẹp hơn nhưng vẫn có giá trị: unit compile trên Windows như một phần của build thường nhật, mọi framework symbol được bind theo tên lúc runtime với các thất bại được liệt kê, và logic chọn thuật toán gồm cả hai fallback PSS là Pascal thuần có thể review và suy luận được. Lần chạy đầu trên một máy Mac sẽ hoặc chạy, hoặc trả về một danh sách tên cần sửa
Phần verify đối ứng, thứ dùng CMS decoder cấp cao hơn thay vì tự lắp cấu trúc CMS, được nói trong verify PDF signature trên macOS với SecTrust, và nó dùng chung hạ tầng binding lẫn cách tiếp cận chẩn đoán
Ý tưởng mang đi được ở đây nằm ở chỗ đặt rủi ro chứ không phải ở macOS. Khi bạn bắt buộc phải viết code chống lại một interface mà bạn không kiểm chứng được, hãy chọn kết cấu mà sai lầm ở đó rẻ nhất để định vị. Dynamic binding với một danh sách tường minh các tên chưa resolve biến hai mươi giả định không kiểm chứng được thành đúng một dòng chẩn đoán. Cả hai backend ship dạng source kèm PDFium Delphi component, nên nếu một symbol name quả thật cần sửa, đó là một dòng trong source tree của bạn chứ không phải một support ticket