Техническа статия

PAdES подписване с macOS Keychain в PDFium VCL

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-ът налага останалото

Диаграма на macOS Keychain backend-а в PDFium VCL, разрешаващ Security и CoreFoundation символи чрез dlsym: kSecClass е експортирана променлива, която BindConstant разрешава веднъж, за да получи CFStringRef стойността, докато kCFTypeDictionaryKeyCallBacks е експортирана структура, която BindStruct подава по адрес, а разместването на двете правила ражда боклук указатели надолу по веригата
Едно dlsym извикване връща две категорично различни неща: адреса на променлива, държаща CFTypeRef, и адреса на callback структура. Два 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

Верига от решения, показваща защо RSA-PSS подписването в Keychain backend-а на PDFium VCL се нуждае от два независими fallback-а: dlsym връща nil за digest-signing константата на macOS версии преди 10.13, SecKeyIsAlgorithmSupported може да откаже хардуерно-базиран ключ, а и двете порти се изливат в едно и също PKCS#1 v1.5 понижение, чийто CMS алгоритмичен идентификатор трябва да смени заедно с него
PSS може да е недостъпен два пъти – по веднъж на macOS версия и по веднъж на ключ, и само портата за версия е системен въпрос. И двете порти се изливат в едно и също v1.5 понижение, а CMS идентификаторът следва

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

Сравнение на ECDSA подписно кодиране през два backend-а на PAdES подписвача в PDFium VCL: macOS Keychain Security framework-ът връща X9.62 DER, който CMS приема с нула конверсии, докато PKCS#11 токен връща суровата фиксирана-широчина P1363 двойка, която трябва да бъде прекодирана, така че ResolvePadesSigner пази конвенциите за кодиране в provider-а
Един и същ ECDSA интерфейс иска противоположно третиране по backend: Security предава готов DER, докато PKCS#11 токен предава суров P1363, затова конверсията живее в provider-а, а викащите никога не виждат per-backend conditional
// 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 тикет