O PDFium Delphi Component verifica assinaturas PDF no macOS através de TPdfKeychainCmsVerifier, um backend de verificação CMS construído sobre o CMSDecoder e o SecTrust da Apple, em vez de fazer parsing manual de CMS. ConfigureKeychainCmsVerifier instala-o e uma única chamada a CMSDecoderCopySignerStatus devolve o veredicto da assinatura, um handle SecTrust e um código de resultado do certificado, exatamente o par de colunas que TPdfCmsVerifyResult já transportava no Windows
O cenário que obrigou a este trabalho é banal e comum. Um build Lazarus de um arquivo documental corre num Mac, abre um contrato assinado e todas as assinaturas regressam como pcsUnsupported. Nada está errado com o ficheiro. A verificação de assinaturas simplesmente não tinha backend fora do Windows e o validador PAdES recusava-se a adivinhar na ausência de um. A versão 3.111.0 do PDFiumPas abriu a costura com IPdfCmsVerifier e ConfigurePadesCmsVerifier; a versão 3.113.0 preencheu-a no macOS. A parte interessante desse port não é a canalização, mas os três locais em que a API Apple não tem a mesma forma que a do Windows
Porque é que uma assinatura PDF cobre dois intervalos de bytes?
Porque uma assinatura não pode cobrir os bytes que a contêm. 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 cobrem tudo dos dois lados desse buraco. Dois segmentos, uma lacuna no meio, em todas as plataformas
As plataformas discordam sobre como esses segmentos chegam à camada criptográfica e a discordância custa memória. No Windows, CryptVerifyDetachedMessageSignature aceita um array de ponteiros e comprimentos, pelo que ambos os spans entram tal como estão no buffer e nada é duplicado. O CMSDecoderSetDetachedContent da Apple aceita um único CFData e não tem uma forma multissegmento, pelo que o backend macOS concatena os dois intervalos num buffer contíguo antes de descodificar. Isso é uma segunda cópia completa dos bytes assinados. Num arquivo digitalizado de 400 MB, é um pico de memória real, escala com o documento e não com a assinatura e não há uma API alternativa a usar. Dimensione o worker de batch de acordo, em vez de descobrir isto na máquina de um cliente
Uma chamada preenche duas colunas de TPdfCmsVerifyResult
CMSDecoderCopySignerStatus é invulgarmente generosa para um ponto de entrada do Security.framework: uma chamada devolve o estado do signatário, um SecTrustRef para a cadeia que construiu e um OSStatus para a avaliação do certificado. Estes valores entram diretamente no record que o validador PAdES já consome, com o estado do signatário a tornar-se SignatureStatus, o resultado do certificado a tornar-se TrustStatus e os valores crus 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 diretamente em IPdfCmsVerifier — ValidatePadesCompliance e ValidatePadesTrust encaminham todas as verificações através do backend que estiver instalado, pelo que o código que lê TPadesSignatureValidation é byte a byte igual nas duas plataformas, como descrito no guia sobre inspecionar dicionários de assinaturas PDF e níveis PAdES em Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// A assinatura e a verificação resolvem símbolos de framework diferentes, por isso um
// pode existir enquanto o outro não existe
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName responde agora 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Porque é que kCMSSignerInvalidCert comunica 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 apenas não foi possível estabelecer a cadeia de certificados. Por isso TPdfKeychainCmsVerifier mapeia kCMSSignerInvalidCert para pcvsValid na coluna SignatureStatus e deixa o problema do certificado surgir através de TrustStatus, onde pertence um problema de cadeia. Fundi-lo no veredicto da assinatura faria o componente dizer a um operador que um documento não adulterado tinha sido modificado, que é o pior falso alarme que um validador de assinaturas pode produzir
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// A assinatura foi verificada e apenas a cadeia falhou, algo que o estado
// de confiança comunica por si próprio
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Leia os dois estados como um par ordenado e a lógica de comunicação escreve-se sozinha. SignatureStatus = pcvsValid juntamente com TrustStatus = pcvsInvalid descreve um documento cujos bytes estão intactos e cujo emissor este Mac específico não confia: uma âncora ausente no Keychain, um intermediate expirado ou uma cadeia que não pode ser completada offline. É uma questão de política operacional, não de integridade do documento, e a distinção é precisamente a que está por trás da maioria dos casos na nota sobre porque os validadores rejeitam assinaturas PAdES criptograficamente sólidas
Onde verifica realmente o macOS a revogação?
Dentro da avaliação de confiança, razão pela qual TPdfCmsVerifyResult.RevocationStatus segue TrustStatus em vez de transportar um veredicto próprio. SecPolicyCreateRevocation produz uma policy, essa policy junta-se a SecPolicyCreateBasicX509 no array passado a CMSDecoderCopySignerStatus e o trabalho de OCSP ou CRL acontece no local onde a cadeia é construída. Não regressa nenhuma resposta separada, pelo que comunicá-la seria inventá-la. O próprio array transporta uma pequena regra de ownership que merece ser nomeada: CFArrayCreate retém ambas as policies, pelo que as duas referências locais são libertadas imediatamente depois, enquanto o caso de uma única policy ignora o array e passa diretamente a policy, uma forma que a API também aceita
A operação offline é um sinalizador explícito e não um acidente de conectividade. Quando TPdfCmsVerifyOptions.OnlineRetrieval é False, o backend acrescenta kSecRevocationNetworkAccessDisabled, confinando a avaliação às respostas já colocadas em cache na máquina, e o callback de checkpoint continua a disparar pcvstCryptographicSignature, pcvstChainBuild e pcvstRevocationCheck pela mesma ordem em que o backend Windows os comunica. O código da aplicação define tudo isto através do record de opções de nível superior
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // apenas 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 noutro lugar
SecTrustGetCertificateAtIndex tem semântica get e a referência que devolve nunca deve ser libertada, enquanto CMSDecoderCopySignerCert e SecCertificateCopyData, algumas linhas abaixo na mesma rotina, têm semântica copy e têm de ser libertadas. O Core Foundation codifica a regra inteira num verbo do nome da função e o sistema de tipos não impõe nada disso. Liberte a referência emprestada e nada correrá mal no ponto da chamada: o objeto trust torna-se simplesmente inconsistente e o crash chega mais tarde, num local sem ligação visível às cadeias de certificados
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Semântica get: esta referência é emprestada e não é libertada aqui
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Semântica copy: esta é própria e tem de ser devolvida
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Que garantia dá o verificador quando nenhum backend responde?
Que a resposta não é suportada, nunca uma aprovação silenciosa. Quando ConfigurePadesCmsVerifier não instalou nada e o default da plataforma não pode ajudar, TPdfCmsVerifyResult regressa com todas as colunas definidas como indisponíveis e o validador PAdES mapeia isso para pcsUnsupported, pelo que um build sem backend criptográfico comunica honestamente em vez de afirmar qualquer coisa sobre a assinatura. A binding macOS é deliberadamente conservadora na mesma direção: Security.framework e CoreFoundation são alcançados através de dlopen e dlsym, pelo que um framework ausente ou um nome de símbolo que esta binding tenha escrito mal aparece como KeychainVerificationAvailable a devolver False com KeychainMissingSymbols a nomear o culpado, não como uma falha de link e não como um veredicto errado. É a mesma postura fail-closed que o componente adota quando procura a biblioteca nativa, descrita no artigo sobre carregar a biblioteca nativa PDFium em qualquer alvo
A verificação de assinaturas é 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 suficiente para tornar os dois resultados fáceis de alcançar. Concatene os intervalos de bytes e aceite a cópia, mantenha o veredicto da assinatura e o veredicto da cadeia em colunas separadas, respeite os verbos get e copy e deixe um backend ausente dizê-lo. Se está a mover 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 do Windows por trás de uma interface única