O PDFium VCL assina documentos PAdES com uma chave privada guardada na Keychain do macOS através de um backend que resolve todos os símbolos Security e CoreFoundation em runtime com dlopen e dlsym. Nada é vinculado em tempo de ligação, o que significa que um nome de símbolo mal escrito aparece como KeychainAvailable a devolver False e KeychainMissingSymbols a nomear o culpado, em vez de um erro de linker ou uma falha
Essa escolha foi forçada por uma restrição desconfortável, e a forma como foi tratada generaliza. A unidade foi escrita numa máquina sem SDK macOS, por isso cada nome de símbolo de framework e cada constante veio da documentação e nada podia ser verificado contra um header. A resposta errada a essa situação é escrever o código com cuidado e torcer. A certa é arranjar as coisas para que os erros inevitáveis se anunciem na forma mais localizável possível
Porque a vinculação dinâmica é a decisão certa mesmo na plataforma de destino
Porque converte uma classe de falha que para o programa numa classe de falha que se reporta a si própria. Uma referência a framework estaticamente ligada que está errada falha em tempo de ligação no alvo e não liga em lado nenhum. Uma vinculada dinamicamente que está 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 porque está isto indisponível numa única linha que nomeia uma gralha
Há um segundo benefício que se paga diariamente e não uma vez. Como a unidade não liga frameworks, compila em todas as plataformas, por isso a build Windows ordinária continua a verificar a sua sintaxe, os seus tipos e a sua cláusula uses. Uma unidade que só compila numa plataforma que ninguém na equipa tem é uma unidade sem compilador nenhum a olhar para ela, e decai em silêncio a cada refactor de um tipo partilhado
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 capitalização
Options.PaddingScheme := psRsaPss;
end;
Dois tipos de símbolo exportado, duas formas de os ler
Este é o detalhe mais confuso de todo o binding, e errá-lo compila limpo e falha em runtime. CoreFoundation e Security exportam duas coisas categoricamente diferentes através da mesma chamada dlsym, e o código tem de saber qual é qual
Constantes nomeadas como as chaves de classe de itens da keychain e os singletons booleanos do CoreFoundation são variáveis exportadas cujo conteúdo é o CFStringRef ou CFBooleanRef que se quer. dlsym devolve o endereço dessa variável, por isso tem de 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 dlsym devolve o endereço da estrutura, que é precisamente o ponteiro que a função de criação de dicionários espera. Desreferencie essa e passa a primeira palavra de máquina da estrutura como se fosse um ponteiro
Nenhum dos erros produz um erro de compilação, e nenhum produz um erro de runtime claro. Recebe um ponteiro de lixo que falha algures a jusante. A forma de tornar a distinção impossível de errar é deixar de confiar em a lembrar: duas funções auxiliares, uma que vincula e desreferencia e outra que vincula e não desreferencia, para que o call site declare que tipo de símbolo está a pedir e o auxiliar imponha o resto
// Variável exportada: a dlsym dá o endereço de uma variável que contém o
// CFTypeRef, por isso desreferencie uma vez
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Estrutura exportada: a dlsym dá o endereço DA estrutura, que é o que
// a API quer. Não desreferencie
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Porque é que uma assinatura RSA-PSS precisa de dois fallbacks separados?
Porque o algoritmo pode estar em falta 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, por isso num sistema mais antigo o símbolo simplesmente não está lá e o binding recebe nil. Esse é o teste de versão. Separadamente, num sistema onde a constante existe, uma chave específica pode ainda recusá-lo, e o framework responde a essa pergunta através de SecKeyIsAlgorithmSupported para essa chave. Uma chave suportada por hardware ou uma chave com atributos restritivos pode declinar PSS enquanto uma chave de software na mesma máquina o aceita
Ambos os caminhos têm de levar ao mesmo fallback: mudar para PKCS#1 v1.5. E a parte crítica é que o fallback tem de mudar também o identificador de algoritmo escrito na estrutura CMS, e não só a chamada de assinatura. Emitir um identificador de algoritmo PSS enquanto produz de facto uma assinatura v1.5 produz um documento que qualquer verificador rejeita de imediato, o que é estritamente pior do que reportar que o PSS não é suportado. Um downgrade é aceitável, um desacordo entre o que declara e o que fez não é, e isso é uma regra geral para código de assinaturas e não uma excentricidade do macOS. As implicações ao nível da assinatura estão expostas em assinar PDFs com PAdES B-B
Codificação de assinaturas ECDSA, e uma inversão que vale a pena notar
O caminho de curvas elípticas 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 DER X9.62, que é exatamente o que o CMS quer. Um token PKCS#11 devolve em vez disso o par P1363 bruto de largura fixa, que tem de ser recodificado antes de entrar numa estrutura de assinatura
Por isso dois backends que implementam a mesma interface precisam de tratamento oposto para o mesmo algoritmo, e nenhum está errado. É precisamente o tipo de diferença que uma abstração tem de 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, todos os chamadores acabam por transportar uma 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 do provider é a mesma em todas as plataformas, por isso a seleção é
// uma decisão de arranque e não uma decisão por chamada
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// o provider CNG do Windows é instalado pela unidade de plataforma
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// A partir daqui o código de assinatura é neutral quanto a plataforma
Signer := ResolvePadesSigner(Options);
Regras de contagem de referências que distam três linhas
A gestão de memória do Core Foundation segue convenções de nomes, e a armadilha aqui é que funções com convenções diferentes aparecem umas ao lado das outras no mesmo bloco curto. Uma função que obtém (get) um certificado de um objeto de trust devolve uma referência emprestada que não deve ser libertada. Funções que copiam um certificado de assinante ou copiam os seus dados devolvem referências possuídas que têm de ser libertadas. Três chamadas em sequência, duas regras de posse, e libertar a emprestada não falha nessa linha. Corrompe uma contagem de retains e derruba algo não relacionado mais tarde
A mitigação é ler o verbo em todos os nomes de funções de framework antes de escrever a limpeza, todas as vezes, sem exceção. É o equivalente CoreFoundation de verificar se uma API devolve uma cópia ou uma vista, e o custo de errar é uma falha intermitente em vez de um erro
O que este backend não afirma
Nunca correu em macOS à data da escrita, e dizê-lo com clareza é mais útil do que uma garantia implícita. O que é demonstravelmente verdade é mais estreito e continua valioso: a unidade compila em Windows como parte da build diária, todos os símbolos de framework são vinculados por nome em runtime com as falhas enumeradas, e a lógica de seleção de algoritmos incluindo ambos os fallbacks PSS é Pascal ordinário que se pode rever e raciocinar. A primeira execução num Mac ou funciona ou produz uma lista de nomes a corrigir
A contraparte de verificação, que usa o descodificador CMS de nível mais alto em vez de montar a estrutura CMS à mão, está coberta em verificar assinaturas PDF em macOS com SecTrust, e partilha a mesma infraestrutura de binding e a mesma abordagem de diagnóstico
A ideia transferível aqui é sobre colocação de risco e não sobre macOS. Quando tem de 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. Vinculação dinâmica com uma lista explícita de nomes não resolvidos transforma vinte suposições não verificáveis numa linha de diagnóstico. Ambos os backends são distribuídos como código-fonte com o componente PDF Delphi PDFium, por isso se um nome de símbolo precisar mesmo de correção, é uma alteração de uma linha na sua própria árvore e não um bilhete de suporte