Decidir que uma assinatura PDF é qualificada sob eIDAS significa responder a uma pergunta que não tem nada a ver com criptografia: o certificado foi emitido por um serviço de confiança que um estado-membro listou como qualificado, no momento em que a assinatura foi feita. A resposta vive em uma lista confiável, um documento XML publicado por território, e todo o valor desse documento depende de sua autenticidade. Então o componente PDFium se recusa a olhar dentro de uma até que alguém a ateste. O TPdfEuropeanTrustedList.ParseAuthenticated entrega os bytes brutos completos a um IPdfTrustedListAuthenticator fornecido pelo chamador antes de interpretar um único serviço, e cria um snapshot só se esse autenticador passar explicitamente
Essa ordenação é o design. Todo o resto deste recurso decorre dela, incluindo as partes que parecem inconvenientes
Interpretado não é confiável
Uma lista confiável que interpreta limpo diz que o XML está bem formado. Não diz nada sobre quem a escreveu. Como a lista é o que sustenta toda a sua decisão de status qualificado, aceitar uma porque interpretou tornaria a decisão sem sentido: um atacante que pode substituir a lista pode declarar sua própria autoridade certificadora qualificada
O mesmo raciocínio se aplica ao cache, e esta é a armadilha que vale nomear. O cache de snapshots guarda o XML original junto com um digest SHA-256, e seria fácil tratar um digest que bate no carregamento como prova de que a lista é genuína. Não é. Um digest calculado pelo mesmo processo que guardou o arquivo, sem chave envolvida, verifica apenas que os bytes não mudaram desde que você os escreveu; se a lista era fraudulenta quando foi colocada em cache, o digest confirma que é a mesma lista fraudulenta. Então carregar um snapshot em cache passa pelo mesmo autenticador que interpretar uma fresca. Integridade e autenticidade são propriedades diferentes e só uma delas precisa de uma chave
uses
FPdfTrustedList;
type
TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
public
function Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
end;
function TListAuthenticator.Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
begin
// Sua política mora aqui: verifique a assinatura enveloped XMLDSIG
// contra o certificado de assinatura de lista que você fixou fora de
// banda, e descreva o que conferiu para o rastro de auditoria
Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
if Result then
AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;
var
List: TPdfEuropeanTrustedList;
Cache: TFileStream;
begin
List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
TListAuthenticator.Create, TPdfTrustedListOptions.Default);
// O snapshot existe só porque o autenticador disse sim
Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
try
List.SaveCache(Cache);
finally
Cache.Free;
end;
end;
O validador não é dono da política de rede
Um validador PAdES não tem nada a ver com decidir como alcançar a lista de listas de confiança, se passar por um proxy, com que frequência repetir, ou o que fazer quando um território está inalcançável. Essas são decisões de aplicativo e implantação, e em ambientes regulados são frequentemente auditadas. Então atualizações chegam por IPdfTrustedListSource, que recebe uma URI e um teto de bytes e retorna bytes
O que o componente aplica são os invariantes que fazem de uma atualização uma atualização em vez de uma substituição. O Update exige que o território não mude, que o número de sequência estritamente aumente e que o tempo de emissão não recue. Essas três checagens derrotam os ataques de downgrade mais óbvios: reproduzir uma lista mais antiga que ainda lista um serviço desde então retirado, ou trocar pela lista de outro território cujos serviços você nunca teve a intenção de confiar
Limites do parser, e nenhum DTD de forma alguma
O TPdfTrustedListOptions limita o tamanho do XML, a contagem de tokens, a profundidade de aninhamento, o número de serviços, o número de certificados e o tamanho de um certificado individual, com uma função de classe Default fornecendo valores utilizáveis. Listas confiáveis são documentos publicados de tamanho previsível, então limites são baratos de definir e não há lista legítima que precise excedê-los
Separamente e incondicionalmente, o parser rejeita declarações DTD e entity. Isso fecha tanto a negação de serviço por expansão de entity quanto a rota de divulgação por entity externa em uma única recusa, e não custa nada porque listas confiáveis não usam entities. Qualquer parser XML alcançável de entrada não confiável deveria ser configurado assim; a diferença aqui é que a recusa não é configurável, então não pode ser desligada por uma mudança de opção bem-intencionada
O status qualificado é registrado ao lado da confiança de cadeia, não mesclado nela
O lado da avaliação é deliberadamente separado. O TPadesTrustValidationOptions.QualifiedTrustEvaluator recebe um IPdfQualifiedTrustEvaluator, que o snapshot de lista confiável implementa. Durante a validação o avaliador recebe o certificado folha, a cadeia e um tempo de validação, casa certificados de serviço por comparação DER exata contra o signatário e a cadeia, combina o status do serviço, o identificador de tipo de serviço e as URIs de qualificador naquele momento, e retorna um record de avaliação
O resultado pousa em dois lugares em cada assinatura: QualifiedTrustStatus como um status grosso, e QualifiedTrust como a avaliação completa com território, nome do provedor, nome do serviço, identificador de tipo, status e tempo inicial do status. O que não faz é mudar o CertificateTrustStatus. Confiança de cadeia do sistema e status qualificado respondem a perguntas diferentes, e um relatório que os colapsa não distingue "confiável mas não qualificado" de "qualificado mas a cadeia não valida", ambos reais e ambos precisando de tratamento diferente
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
I: Integer;
begin
Options := TPadesTrustValidationOptions.Default;
Options.CheckRevocation := True;
Options.QualifiedTrustEvaluator := List; // o snapshot autenticado
Options.QualifiedValidationTime := SigningTime; // não Now
Report := Pdf.ValidatePadesTrust(Options);
for I := 0 to High(Report.Signatures) do
if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
Writeln(Format('signature %d qualified by %s / %s (%s)',
[I, Report.Signatures[I].QualifiedTrust.Territory,
Report.Signatures[I].QualifiedTrust.ProviderName,
Report.Signatures[I].QualifiedTrust.ServiceName]))
else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
// Nenhum serviço correspondente, ou o snapshot não pode responder por este tempo
Writeln(Format('signature %d: qualified status undetermined', [I]));
end;
Por que o tempo de validação não é agora
Porque a qualificação é uma propriedade de um momento. Um serviço de confiança pode receber o status qualificado, depois tê-lo retirado e depois ainda ser reintegrado, e cada uma dessas transições carrega um tempo inicial na lista. Uma assinatura feita enquanto o serviço estava qualificado permanece qualificada depois; uma assinatura feita antes da concessão não se torna qualificada retroativamente. Avaliar contra o tempo atual portanto dá a resposta errada nas duas direções
A lista carrega o necessário para isso: cada record de serviço tem um tempo inicial de status e um flag distinguindo entradas históricas das atuais, e o avaliador os combina contra o tempo que você fornece. Na prática esse tempo vem de um timestamp confiável na assinatura em vez do tempo de assinatura alegado no CMS, que é por que o material de validação de longo prazo importa mesmo para uma pergunta que parece uma consulta de política; o lado de timestamp e DSS é coberto em o artigo de assinaturas de longo prazo
O que você ainda precisa construir
Três coisas, e nenhuma delas pertence a uma biblioteca PDF. O autenticador, significando verificação XMLDSIG real contra um certificado de assinatura de lista que você obteve por um canal em que confia. A política de busca, significando como e com que frequência você atualiza, e o que seu aplicativo faz quando uma atualização falha. E o escopo territorial, significando quais listas você carrega de fato, o que é uma decisão de negócio sobre em quais estados-membros suas contrapartes assinam
O que você obtém do componente é a parte que é fácil de errar sutilmente: a ordenação autenticar-antes-de-interpretar, parse de XML limitado e sem entities, invariantes de atualização monótonos, casamento de serviço por DER exato, avaliação de status histórico e um resultado que permanece separado da confiança de cadeia comum. Se seu problema imediato é mais básico, que um validador rejeita uma assinatura que você acredita estar boa, as causas usuais estão catalogadas em por que validadores rejeitam assinaturas PAdES, e a superfície de inspeção de assinaturas é descrita em inspecionar assinaturas e níveis PAdES. As capacidades do componente estão listadas na página de produto do PDFium Delphi component