PDFium VCL подписва PAdES документи с частен ключ, държан в macOS Keychain, чрез backend, разрешаващ всеки Security и CoreFoundation символ по време на изпълнение с dlopen и dlsym. Нищо не е привързано по време на link, което значи, че сбъркано изписано име на символ се проявява като KeychainAvailable, връщащ False, и KeychainMissingSymbols, назоваващ виновника, а не като linker грешка или краш
Този избор беше наложен от некомфортно ограничение, а начинът, по който е обработен, се обобщава. Unit-ът е писан на машина без macOS SDK, така че всяко име на framework символ и всяка константа идват от документация и нищо не може да бъде проверено срещу header. Грешният отговор на тази ситуация е да напишете кода внимателно и да се надявате. Правилният – да уредите неизбежните грешки сами да обявят себе си в най-локализуемата възможна форма
Защо динамичното свързване е правилният ход дори на целевата платформа
Защото превръща клас провали, спиращи програмата, в клас провали, докладващи себе си. Статично свързана framework референция, която е грешна, се проваля по време на link на целевата платформа и никога не се свързва другаде. Динамично свързана, която е грешна, произвежда недостъпен backend и списък с неразрешени имена, а първият пуск на Mac превръща въпроса защо това е недостъпно в един ред, назоваващ типо
Има втора изгода, изплащаща се ежедневно, а не веднъж. Тъй като unit-ът не свързва framework-ове, той се компилира на всяка платформа, така че обикновеният Windows build продължава да проверява синтаксиса му, типовете му и uses клаузата му. Unit, компилиращ се само на платформа, която никой в екипа няма, е unit без никакъв компилатор, гледащ го, и тлее тихо при всеки refactor на споделен тип
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // инсталира се като PAdES signer backend
ConfigureKeychainCmsVerifier; // и като verification backend
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, всякакъв регистър
Options.PaddingScheme := psRsaPss;
end;
Два вида експортирани символи, два начина да ги прочетеш
Това е най-объркващият детайл в цялото свързване, а разминаването му се компилира чисто и се проваля по време на изпълнение. CoreFoundation и Security експортират две категорично различни неща през същото dlsym извикване, а кодът трябва да знае кое е кое
Именуваните константи като ключовете за item клас на keychain и CoreFoundation булевите singleton-и са експортирани променливи, чието съдържание е CFStringRef или CFBooleanRef, който искате. dlsym връща адреса на тази променлива, така че трябва да разрешите указателя веднъж, за да получите стойността. Callback-table структурите като dictionary key и value callback-ите са експортирани структури, и dlsym връща адреса на структурата, което е точно указателят, който функцията за създаване на dictionary очаква. Разрешите ли и този, подавате първата машинна дума на структурата, сякаш е указател
Нито една от грешките не произвежда компилаторна грешка, и нито една – ясна runtime грешка. Получавате боклук указател, провалящ се някъде надолу по веригата. Начинът разликата да стане невъзможна за объркване е да спрете да разчитате на помнене: две helper функции, една, която свързва и разрешава, и една, която свързва без да разрешава, така че мястото на извикване обявява кой вид символ иска, а helper-ът налага останалото
// Експортирана променлива: dlsym дава адреса на променлива, държаща
// CFTypeRef, така че разрешете веднъж
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Експортирана структура: dlsym дава адреса НА структурата, което е
// точно това, което API-то иска. Не разрешавайте
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Защо RSA-PSS подпис се нуждае от два отделни fallback-а?
Защото алгоритъмът може да липсва по два независими начина, и само единият е въпрос на версия. PSS digest-signing алгоритмичната константа се появи в macOS 10.13, така че на по-стара система символът просто го няма и binding-ът получава nil. Това е проверката за версия. Отделно, на система, където константата съществува, конкретен ключ може пак да я откаже, а framework-ът отговаря на този въпрос чрез SecKeyIsAlgorithmSupported за този ключ. Хардуерно-базиран ключ или ключ с рестриктивни атрибути може да откаже PSS, докато софтуерен ключ на същата машина го приема
И двата пътя трябва да водят към един и същ fallback: преминаване към PKCS#1 v1.5. И критичната част е, че fallback-ът трябва да мени и алгоритмичния идентификатор, записван в CMS структурата, не само подписващото извикване. Излъчване на PSS алгоритмичен идентификатор, докато реално произвеждате v1.5 подпис, ражда документ, който всеки verifier отхвърля изцяло – строго по-лошо от докладване, че PSS не е поддържан. Понижение е приемливо, разминаване между това, което обявявате, и това, което сте свършили – не е, а това е общо правило за подписен код, а не macOS причудност. Последиците на ниво подпис са изложени в подписване на PDF-и с PAdES B-B
ECDSA подписно кодиране и обръщане, заслужаващо бележка
Elliptic-curve пътят не се нуждае изобщо от конверсия на macOS, а това е обратното на това, което PKCS#11 binding иска. Security framework-ът с digest-signing алгоритъма за ECDSA връща подписа още в X9.62 DER форма, която е точно това, което CMS иска. PKCS#11 токен вместо това връща суровата фиксирана-широчина P1363 двойка, която трябва да бъде прекодирана, преди да влезе в подписна структура
Така два backend-а, имплементиращи един и същ интерфейс, се нуждаят от противоположно третиране за един и същ алгоритъм, и нито единият не е грешен. Това е точно видът разлика, която една абстракция трябва да поглъща, а не да излага: PAdES слойт моли provider да подпише, а конвенциите за кодиране остават в provider-а. Изтечат ли нагоре, всеки викащ се озова носещ per-backend conditional. Същата форма се появява в историята за дистанционно подписване, описана в дистанционни PAdES подписване сесии срещу HSM
// Provider интерфейсът е същият на всяка платформа, така че изборът е
// решение при стартиране, а не при всяко извикване
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Windows CNG provider се инсталира от платформения unit
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Оттам нататък подписващият код е платформено-неутрален
Signer := ResolvePadesSigner(Options);
Правила за reference counting, седящи на три реда разстояние
Core Foundation управлението на памет следва именувани конвенции, а капанът тук е, че функции с различни конвенции се появяват една до друга в един и същ къс блок. Функция, която взима (get) сертификат от trust обект, връща назаем дадена референция, която не бива да бъде освобождавана. Функции, които копират (copy) сертификата на подписвача или данните му, връщат притежавани референции, които трябва да бъдат освобождавани. Три извиквания подред, два правила за собственост, и освобождаването на назаем дадената не се проваля на онзи ред. Тя развързва retain брояч и събаря нещо несвързано после
Облекчението е да прочитате глагола във всяко име на framework функция, преди да пишете изчистването, всеки път, без изключение. Това е CoreFoundation еквивалентът на проверката дали API връща копие или изглед, а цената на грешката е intermittent краш, а не грешка
Какво този backend не твърди
Той никога не е бил пускан на macOS към момента на писане, и казано съвсем прямо това е по-полезно от намекнато уверение. Демонстративно истинското е по-тясно и пак ценно: unit-ът се компилира на Windows като част от дневния build, всеки framework символ е свързан по име по време на изпълнение с изброени провали, а логиката за избор на алгоритъм, включително и двата PSS fallback-а, е обикновен Pascal, който може да бъде рецензиран и обсъден. Първият пуск на Mac или ще работи, или ще произведе списък с имена за поправка
Verification двойникът, ползващ високо-нивовия CMS декодер вместо ръчно сглобяване на CMS структурата, е обхванат в верифициране на PDF подписи на macOS със SecTrust и споделя същата binding инфраструктура и същия диагностичен подход
Преносимата идея тук е за разположение на риска, а не за macOS. Когато трябва да пишете код срещу интерфейс, който не можете да верифицирате, изберете конструкцията, в която грешките са най-евтини за локализиране. Динамично свързване с изричен списък с неразрешени имена превръща двадесет неверифицируеми допускания в един диагностичен ред. И двата backend-а ship-ват като source с PDFium Delphi компонента, така че ако някое име на символ все пак се нуждае от поправка, това е едноредова промяна във вашето дърво, а не support тикет