O PDFium VCL assina documentos PAdES com uma chave privada guardada no Keychain do macOS por meio de um backend que resolve todo símbolo do Security e do CoreFoundation em runtime com dlopen e dlsym. Nada é ligado em tempo de link, o que significa que um nome de símbolo digitado errado aparece como KeychainAvailable devolvendo False e KeychainMissingSymbols apontando o culpado, em vez de um erro de linker ou um crash
Essa escolha foi imposta por uma restrição desconfortável, e o jeito como foi tratada se generaliza. A unit foi escrita numa máquina sem SDK do macOS, então todo nome de símbolo de framework e toda constante vieram de documentação, e nada podia ser conferido contra um header. A resposta errada a essa situação é escrever o código com cuidado e torcer. A certa é arranjar para que os erros inevitáveis se anunciem na forma mais localizável possível
Por que binding dinâmico é a decisão certa mesmo na plataforma de destino
Porque ele converte uma classe de falha que para o programa numa classe de falha que se reporta. Uma referência de framework ligada estaticamente que esteja errada falha em tempo de link no target e não linka em lugar nenhum outro. Uma ligada dinamicamente que esteja errada produz um backend indisponível e uma lista de nomes não resolvidos, e a primeira execução num Mac transforma a pergunta de por que isso está indisponível numa única linha apontando um typo
Há um segundo benefício que se paga diariamente, e não uma vez só. Como a unit não linka frameworks nenhum, ela compila em toda plataforma, então o build Windows comum continua conferindo a sintaxe, os tipos e a uses clause dela. Uma unit que só compila numa plataforma que ninguém do time tem é uma unit sem compilador nenhum olhando para ela, e apodrece em silêncio a cada refactor de um tipo compartilhado
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // instala como backend de assinatura PAdES
ConfigureKeychainCmsVerifier; // e como backend de verificação
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, qualquer caixa
Options.PaddingScheme := psRsaPss;
end;
Dois tipos de símbolo exportado, duas formas de ler
Esse é o detalhe mais confuso de todo o binding, e acertá-lo ao contrário compila limpo e falha em runtime. CoreFoundation e Security exportam duas coisas categoricamente diferentes pela mesma chamada dlsym, e o código precisa saber qual é qual
Constantes nomeadas como as chaves de classe de item do keychain e os singletons booleanos do CoreFoundation são variáveis exportadas cujo conteúdo é o CFStringRef ou CFBooleanRef que você quer. O dlsym devolve o endereço dessa variável, então você precisa desreferenciar uma vez para obter o valor. Estruturas de tabela de callbacks como os callbacks de chave e valor do dicionário são estruturas exportadas, e o dlsym devolve o endereço da estrutura, que é precisamente o ponteiro que a função de criação do dicionário espera. Desreferencie essa e você passa a primeira palavra de máquina da estrutura como se fosse um ponteiro
Nenhum dos dois erros produz erro de compilação, e nenhum produz erro claro de runtime. Você recebe um ponteiro de lixo que falha em algum lugar downstream. O jeito de tornar a distinção impossível de errar é parar de confiar na memória: duas funções helper, uma que faz bind e desreferencia e outra que faz bind e não desreferencia, para o call site declarar qual tipo de símbolo está pedindo e o helper impor o resto
// Variável exportada: o dlsym dá o endereço de uma variável que guarda
// o CFTypeRef, então desreferencie uma vez
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Estrutura exportada: o dlsym dá o endereço DA estrutura, que é o que
// a API quer. Não desreferencie
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Por que uma assinatura RSA-PSS precisa de dois fallbacks separados?
Porque o algoritmo pode estar ausente de duas formas independentes, e só uma delas é uma questão de versão. A constante do algoritmo de assinatura de digest PSS apareceu no macOS 10.13, então num sistema mais velho o símbolo simplesmente não está lá e o binding recebe nil. Essa é a checagem de versão. Separadamente, num sistema em que a constante existe, uma chave específica ainda pode recusá-la, e o framework responde a essa pergunta via SecKeyIsAlgorithmSupported para aquela chave. Uma chave com suporte de hardware ou uma chave com atributos restritivos pode declinar PSS enquanto uma chave de software na mesma máquina aceita
Os dois caminhos precisam levar ao mesmo fallback: trocar para PKCS#1 v1.5. E a parte crítica é que o fallback precisa mudar o identificador de algoritmo escrito na estrutura CMS também, e não só a chamada de assinatura. Emitir um identificador de algoritmo PSS enquanto de fato se produz uma assinatura v1.5 gera um documento que todo verificador rejeita de cara, o que é estritamente pior que reportar que PSS não é suportado. Um downgrade é aceitável, uma divergência entre o que você declara e o que fez não é, e essa é uma regra geral de código de assinatura, não uma peculiaridade do macOS. As implicações no nível de assinatura estão postas em assinatura de PDFs com PAdES B-B
Codificação de assinatura ECDSA, e uma inversão que vale notar
O caminho de curva elíptica não precisa de conversão nenhuma no macOS, e isso é o oposto do que um binding PKCS#11 exige. O algoritmo de assinatura de digest para ECDSA do framework Security devolve a assinatura já em forma X9.62 DER, que é exatamente o que o CMS quer. Um token PKCS#11 devolve em vez disso o par P1363 bruto de largura fixa, que precisa ser re-encodado antes de entrar numa estrutura de assinatura
Então dois backends implementando a mesma interface precisam de tratamento oposto para o mesmo algoritmo, e nenhum dos dois está errado. Esse é precisamente o tipo de diferença que uma abstração precisa absorver, e não expor: a camada PAdES pede a um provider que assine, e as convenções de codificação ficam dentro do provider. Se vazarem para cima, todo caller acaba carregando um condicional por backend. A mesma forma aparece na história de assinatura remota descrita em sessões de assinatura PAdES remota contra um HSM
// A interface de provider é a mesma em toda plataforma, então a
// seleção é uma decisão de startup, e não uma decisão por chamada
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// O provider CNG do Windows é instalado pela unit de plataforma
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Daqui em diante o código de assinatura é neutro de plataforma
Signer := ResolvePadesSigner(Options);
Regras de contagem de referências a três linhas de distância
O gerenciamento de memória do Core Foundation segue convenções de nomenclatura, e a armadilha aqui é que funções com convenções diferentes aparecem lado a lado no mesmo bloco curto. Uma função que obtém um certificado de um objeto de trust devolve uma referência emprestada que não deve ser liberada. Funções que copiam um certificado de assinante ou copiam os dados dele devolvem referências possuídas que devem ser liberadas. Três chamadas em sequência, duas regras de posse, e liberar a emprestada não falha naquela linha. Ela corrompe um retain count e derruba algo sem relação mais tarde
A mitigação é ler o verbo em todo nome de função de framework antes de escrever o cleanup, toda vez, sem exceção. É o equivalente, no CoreFoundation, de conferir se uma API devolve uma cópia ou uma view, e o custo de errar é um crash intermitente em vez de um erro
O que este backend não afirma
Ele nunca rodou no macOS até o momento da escrita, e dizer isso com franqueza é mais útil que uma garantia implícita. O que é demonstravelmente verdade é mais estreito e ainda valioso: a unit compila no Windows como parte do build diário, todo símbolo de framework é ligado pelo nome em runtime com as falhas enumeradas, e a lógica de seleção de algoritmos, incluindo os dois fallbacks de PSS, é Pascal comum que pode ser revisada e raciocinada. A primeira execução num Mac vai ou funcionar ou produzir uma lista de nomes a consertar
A contraparte de verificação, que usa o decoder CMS de nível mais alto em vez de montar a estrutura CMS à mão, está coberta em verificação de assinaturas PDF no macOS com SecTrust, e compartilha a mesma infraestrutura de binding e a mesma abordagem de diagnóstico
A ideia transferível aqui é sobre posicionamento de risco, e não sobre macOS. Quando você precisa escrever código contra uma interface que não pode verificar, escolha a construção em que os erros são mais baratos de localizar. Binding dinâmico com uma lista explícita de nomes não resolvidos transforma vinte suposições inverificáveis numa linha de diagnóstico. Os dois backends vêm como fonte com o componente PDFium para Delphi, então se um nome de símbolo precisar de correção, é uma mudança de uma linha na sua própria árvore, e não um ticket de suporte