O PDFium Delphi Component verifica assinaturas PDF no macOS por meio de TPdfKeychainCmsVerifier, um backend de verificação CMS construído sobre o Apple CMSDecoder e SecTrust, em vez de fazer parsing manual de CMS. ConfigureKeychainCmsVerifier o instala, e uma única chamada a CMSDecoderCopySignerStatus devolve o veredito da assinatura, um handle SecTrust e um código de resultado do certificado, exatamente o par de colunas que TPdfCmsVerifyResult já carregava no Windows
O cenário que forçou o trabalho é banal e comum. Um build Lazarus de um arquivo documental roda em um Mac, abre um contrato assinado e toda assinatura volta como pcsUnsupported. Nada está errado com o arquivo. A verificação de assinatura simplesmente não tinha backend fora do Windows, e o validator PAdES se recusava a adivinhar na ausência de um. A versão 3.111.0 do PDFiumPas abriu a seam com IPdfCmsVerifier e ConfigurePadesCmsVerifier; a versão 3.113.0 a preencheu no macOS. A parte interessante desse port não é o plumbing, mas os três pontos em que a API Apple não tem exatamente o mesmo formato da API Windows
Por que uma assinatura PDF cobre dois intervalos de bytes?
Porque uma assinatura não pode cobrir os bytes que a armazenam. A ISO 32000-1 §12.8.1 coloca o blob CMS SignedData na string /Contents do dicionário de assinatura e descreve a extensão assinada com /ByteRange, um conjunto de pares de offset e comprimento que cobre tudo dos dois lados desse buraco. Dois segmentos, um intervalo no meio, em qualquer plataforma
As plataformas discordam sobre como esses segmentos chegam à camada de criptografia, e a divergência custa memória. No Windows, CryptVerifyDetachedMessageSignature aceita um array de ponteiros e comprimentos, então os dois spans entram como estão no buffer e nada é duplicado. O Apple CMSDecoderSetDetachedContent aceita um único CFData e não tem uma forma multissegmento, então o backend macOS concatena os dois ranges em um buffer contíguo antes de decodificar. Isso é uma cópia completa adicional dos bytes assinados. Em um arquivo escaneado de 400 MB, é um pico real de memória, escala com o documento em vez de com a assinatura e não há uma API alternativa para usar. Dimensione o worker de batch de acordo, em vez de descobrir isso na máquina de um cliente
Uma chamada preenche duas colunas de TPdfCmsVerifyResult
CMSDecoderCopySignerStatus é excepcionalmente generosa para um entry point do Security.framework: uma chamada retorna o status do signer, um SecTrustRef para a cadeia que construiu e um OSStatus para a avaliação do certificado. Esses valores entram diretamente no record que o validator PAdES já consome, com o status do signer virando SignatureStatus, o resultado do certificado virando TrustStatus e os valores brutos preservados em SignatureError e TrustError, para que um ticket de suporte possa citar um número em vez de um adjetivo. Os chamadores nunca tocam em IPdfCmsVerifier; ValidatePadesCompliance e ValidatePadesTrust encaminham cada verificação pelo backend instalado, então o código que lê TPadesSignatureValidation é byte a byte o mesmo nas duas plataformas, como descrito no passo a passo sobre inspecionar dicionários de assinatura PDF e níveis PAdES no Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Assinatura e verificacao resolvem simbolos de framework diferentes, entao um
// pode existir enquanto o outro nao
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName agora responde 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Por que kCMSSignerInvalidCert reporta uma assinatura válida?
Porque a Apple atribui a esse valor um significado mais estreito do que o nome sugere: a assinatura foi verificada e somente a cadeia de certificados não pôde ser estabelecida. Por isso TPdfKeychainCmsVerifier mapeia kCMSSignerInvalidCert para pcvsValid na coluna SignatureStatus e deixa o problema do certificado aparecer por TrustStatus, onde um problema de cadeia pertence. Misturá-lo no veredito da assinatura faria o componente dizer a um operador que um documento não adulterado foi modificado, o pior falso alarme que um validator de assinaturas pode produzir
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// A assinatura foi verificada e somente a cadeia falhou, algo que o
// status de confianca informa por conta propria
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Leia os dois status como um par ordenado e a lógica de relatório se escreve sozinha. SignatureStatus = pcvsValid junto com TrustStatus = pcvsInvalid descreve um documento cujos bytes estão intactos e cujo emissor não é confiável para este Mac: uma âncora ausente no Keychain, um intermediário expirado ou uma cadeia que não pode ser completada offline. Essa é uma questão de política do operador, não de integridade do documento, e a distinção é exatamente a que está por trás da maioria dos casos na nota sobre por que validators rejeitam assinaturas PAdES criptograficamente sólidas
Onde o macOS verifica revogação de fato?
Dentro da avaliação de confiança, por isso TPdfCmsVerifyResult.RevocationStatus segue TrustStatus em vez de carregar um veredito próprio. SecPolicyCreateRevocation produz uma policy, essa policy se junta a SecPolicyCreateBasicX509 no array passado a CMSDecoderCopySignerStatus e o trabalho de OCSP ou CRL acontece onde a cadeia é construída. Nenhuma resposta separada volta, portanto reportar uma seria inventá-la. O array em si carrega uma regra de ownership que vale nomear: CFArrayCreate retém as duas policies, então as duas referências locais são liberadas imediatamente depois, enquanto o caso de uma única policy ignora o array e passa a policy diretamente, uma forma que a API também aceita
A operação offline é uma flag explícita, não um acidente de conectividade. Quando TPdfCmsVerifyOptions.OnlineRetrieval é False, o backend adiciona kSecRevocationNetworkAccessDisabled, confinando a avaliação a respostas já em cache na máquina, e o callback de checkpoint ainda dispara pcvstCryptographicSignature, pcvstChainBuild e pcvstRevocationCheck na mesma ordem informada pelo backend Windows. O código da aplicação configura tudo isso por meio do record de opções de nível mais alto
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // somente respostas em cache
Options.CheckTimeStamps:= True;
Stream:= TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
Report:= ValidatePadesTrust(Stream, Options);
finally
Stream.Free;
end;
if Report.SignatureCount= 0 then
Log('No signature dictionary in this document')
else if Report.Signatures[0].CmsSignatureStatus <> pcsValid then
Log('Document integrity failed')
else if Report.Signatures[0].CertificateTrustStatus <> pcsValid then
Log('Bytes intact, chain not trusted on this Mac');
end;
Get versus copy: a release que falha em outro lugar
SecTrustGetCertificateAtIndex tem semântica de get e a referência retornada nunca deve ser liberada, enquanto CMSDecoderCopySignerCert e SecCertificateCopyData, algumas linhas adiante na mesma rotina, têm semântica de copy e precisam ser liberadas. Core Foundation codifica toda a regra em um verbo do nome da função, e o sistema de tipos não impõe nada disso. Libere a referência emprestada e nada dá errado no ponto da chamada: o trust object simplesmente se torna inconsistente e o crash chega depois, em algum lugar sem relação visível com cadeias de certificados
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Semantica get: esta referencia e emprestada e nao e liberada aqui
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Semantica copy: esta referencia e propria e precisa ser devolvida
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
O que o verifier garante quando nenhum backend responde?
Que a resposta é unsupported, nunca um sucesso silencioso. Quando ConfigurePadesCmsVerifier não instalou nada e o default da plataforma não pode ajudar, TPdfCmsVerifyResult volta com todas as colunas como indisponíveis e o validator PAdES mapeia isso para pcsUnsupported, então um build sem backend de criptografia reporta honestamente, em vez de afirmar qualquer coisa sobre a assinatura. O binding macOS é deliberadamente conservador na mesma direção: Security.framework e CoreFoundation são acessados por dlopen e dlsym, então um framework ausente ou um nome de símbolo que esse binding tenha errado aparece como KeychainVerificationAvailable retornando False com KeychainMissingSymbols nomeando o culpado, não como falha de link e não como veredito errado. É a mesma postura fail-closed que o componente adota ao procurar a biblioteca nativa, descrita no artigo sobre carregar a biblioteca nativa PDFium em qualquer alvo
Verificação de assinatura é a parte de uma stack PDF em que estar errado silenciosamente é pior do que estar indisponível de forma explícita, e o macOS oferece uma API generosa o bastante para facilitar os dois resultados. Concatene os byte ranges e aceite a cópia, mantenha o veredito da assinatura e o da cadeia em colunas separadas, respeite os verbos get e copy e deixe um backend ausente dizer que está ausente. Se você está levando um workflow documental Delphi ou Free Pascal para o Mac e precisa de assinatura e validação PAdES nos dois lados, o PDFium Delphi Component distribui o backend Keychain ao lado do Windows por trás de uma única interface