A PDF Library for Delphi (PDFlibPas) extrai os certificados de uma assinatura PDF com um percurso DER puro pelo CMS SignedData guardado em /Contents, sem passar pela CryptoAPI. Desde a v3.539.10 que cada leitura aninhada é limitada pelo elemento pai, o padding de zeros a seguir ao CMS é cortado no comprimento que o próprio CMS declara, e os identificadores de objeto passaram a codificar o primeiro subidentificador combinado em base-128. A regra dos limites e a correção dos OIDs substituíram ambas código que produzia respostas erradas sem levantar erro nenhum, e a regra do padding impede que o leitor mais estrito rejeite assinaturas reais
O lado da leitura interessa mais do que parece. As ferramentas de validação a longo prazo têm de retirar o certificado do signatário e os emissores dele de uma assinatura existente antes de irem buscar dados de revogação, um relatório de auditoria tem de dizer quem assinou, e uma build Lazarus em Linux não tem funções de mensagem do Windows onde se agarrar. Um parser nessa posição raramente rebenta com um input mau. O modo de falhanço que dói é uma contagem de certificados que inclui bytes pertencentes a um vizinho, um emparelhamento do signatário feito contra o campo errado, ou um OID que se transforma em silêncio noutro OID. Uma pipeline de assinaturas construída por cima disso relata disparates com toda a confiança
Retirar os certificados do signatário de um PDF assinado
Cinco métodos do TPDFlib cobrem o lado da leitura, e todos recebem InputFile, Password, FieldName: cada chamada abre o ficheiro em modo de leitura, responde e volta a fechá-lo. GetSignatureEmbeddedCertificateCount e GetSignatureEmbeddedCertificateDER enumeram os certificados do conjunto em ordem de codificação, GetSignatureSignerCertificateDER devolve o certificado que produziu um dado SignerInfo, e GetSignatureCertificateChainLength / GetSignatureCertificateChainDER percorrem a cadeia desde esse signatário até ao emissor mais distante que a própria assinatura transporta. Os índices são base zero. Guarde os resultados em AnsiString, que é precisamente por isso que a biblioteca os devolve assim: um blob DER que passe por string ou por TStrings sofre uma conversão de conjunto de caracteres e volta corrompido
uses
SysUtils, Classes, PDFlibrary;
procedure SaveDer(const FileName: string; const Der: AnsiString);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if Der <> '' then
Fs.WriteBuffer(Der[1], Length(Der));
finally
Fs.Free;
end;
end;
const
Src = 'contract-signed.pdf';
Field = 'Signature1';
var
Pdf: TPDFlib;
ChainLen, I: Integer;
SignerDer, LastDer: AnsiString;
begin
Pdf := TPDFlib.Create;
try
WriteLn('Certificates in the CMS: ',
Pdf.GetSignatureEmbeddedCertificateCount(Src, '', Field));
SignerDer := Pdf.GetSignatureSignerCertificateDER(Src, '', Field, 0);
if SignerDer = '' then
raise Exception.Create('signer certificate missing or not matched');
SaveDer('signer.cer', SignerDer);
ChainLen := Pdf.GetSignatureCertificateChainLength(Src, '', Field, 0);
for I := 0 to ChainLen - 1 do
SaveDer(Format('chain-%d.cer', [I]),
Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, I));
if ChainLen > 0 then
begin
LastDer := Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, ChainLen - 1);
WriteLn('Next issuer, if any: ', Pdf.GetCertificateIssuerURLs(LastDer));
end;
finally
Pdf.Free;
end;
end;
Dois pormenores desse output pedem atenção. Uma contagem de 0 não é um diagnóstico: um campo inexistente, uma palavra-passe errada, um blob que não é DER e um SignedData que simplesmente omite o conjunto opcional de certificados devolvem todos 0 ou uma string vazia, por isso registe o nome do campo ao lado do número. E uma cadeia que acaba antes de um certificado auto-emitido também não é um erro. O construtor de cadeias só usa certificados incorporados na assinatura, pelo que os emissores restantes têm de ser obtidos através dos endereços que o GetCertificateIssuerURLs reporta
Quanto de /Contents é afinal o CMS?
Só o prefixo que o SEQUENCE exterior declara pertence ao CMS, e o PLTrimCMSPadding corta tudo o que vem a seguir. O signatário reserva a string hexadecimal de /Contents antes de o CMS existir, porque o /ByteRange descrito na ISO 32000-1 §12.8.1 tem de ficar fixo primeiro, por isso o espaço é dimensionado com folga e a cauda não usada são zeros. O PLTrimCMSPadding lê o primeiro TLV, exige a tag $30 e devolve os bytes até ao fim desse elemento; tudo o que não comece por um SEQUENCE bem formado volta vazio. Esse nível de topo é o único sítio onde bytes a mais no fim são legais, e a distinção interessa para a secção seguinte: uma regra estrita de «o elemento tem de consumir o buffer inteiro» rejeitaria todas as assinaturas reais, enquanto uma regra laxista aplicada a todas as profundezas deixa os campos aninhados ler bytes que não lhes pertencem
Porque é que um leitor DER precisa do offset de fim do pai?
Um elemento aninhado só é válido se acabar dentro do pai, e verificar contra o fim do buffer não prova isso. O DERReadTLV de baixo nível no PDFlibASN1 limita cada elemento à string inteira, o que é a verificação certa para o objeto mais exterior e a errada para tudo o que está por baixo. Imagine um SignerInfo cujo issuerAndSerialNumber declara 40 bytes enquanto o Name do emissor lá dentro reclama 60. Todos os bytes continuam no buffer, por isso um leitor limitado ao buffer aceita o Name, lê o número de série a partir do algoritmo de digest que se segue, e depois compara esse par com os certificados incorporados. Antes da v3.539.10 o percurso do CMS lia exatamente assim. A correção é um wrapper pequeno que transporta a posição de fim do pai para dentro de cada leitura
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// nada resta dentro do pai: recusar iniciar uma leitura
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// o Offset fica um byte a seguir ao elemento; não pode passar do pai
Result := Offset <= ParentEnd;
end;
// cada nível regista o seu próprio fim e passa-o abaixo:
// OuterEnd := fim do ContentInfo (RFC 5652 secção 3)
// ExplicitEnd := fim do conteúdo [0] EXPLICIT
// ContentEnd := fim do SignedData (RFC 5652 secção 5.1)
// SignerEnd / InnerEnd para SignerInfo e issuerAndSerialNumber
A unit PDFlibCMSRead transporta agora esses fins pelo ContentInfo, pelo wrapper [0] EXPLICIT, pelos campos do SignedData até signerInfos, pelo SignerIdentifier nas suas duas formas issuerAndSerialNumber e [0] subjectKeyIdentifier (RFC 5652 §5.3), e pelos campos do tbsCertificate lidos de cada certificado incorporado ao emparelhar o signatário. Dentro do conjunto de certificados e do conjunto de signerInfos, um elemento que passe do fim do conjunto pára o ciclo: o PLExtractCMSCertificates devolve os certificados que já tinha aceito e nunca cola os bytes seguintes de crls ou de signerInfos ao último. A correspondência issuer-and-serial também exige as duas metades, já que um número de série só é único dentro de um emissor
Porque é que 2.999.3 saía como 1.15.3?
Os dois primeiros arcos de um OID combinam num só subidentificador, não num só byte, e esse subidentificador é codificado em base-128 como todos os outros arcos. A X.690 §8.19.4 define-o como 40 * arc1 + arc2; o DER_OID anterior escrevia esse valor com Byte(...), o que só está correto até 127, o valor de 2.47. Para 2.999 a soma é 1079, o cast para byte guarda 55, e 55 descodifica como 1.15, por isso o identificador passa a nomear em silêncio outro ramo da árvore. Os valores de 128 a 255 falham de outra forma, emitindo um byte com o bit de continuação posto que engole o arco seguinte. A maioria dos identificadores PKI (1.2.840..., 2.5.29..., 0.4.0...) nunca chega à fronteira, e é por isso que o defeito sobreviveu; os arcos joint-iso-itu-t a partir de 2.48 para cima chegam. O DER_OID serve tanto o codificador dos signed attributes como o comparador no DERFindExtensionByOID e na verificação do content-type do SignedData, por isso uma codificação errada partia a escrita e a procura ao mesmo tempo
uses
SysUtils, PDFlibASN1;
function Hex(const S: AnsiString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + IntToHex(Byte(S[I]), 2) + ' ';
Result := Trim(Result);
end;
begin
WriteLn(Hex(DER_OID('2.999.3'))); // 06 03 88 37 03
WriteLn(Hex(DER_OID('2.47.1'))); // 06 02 7F 01
WriteLn(Hex(DER_OID('2.48.1'))); // 06 03 81 00 01
WriteLn(Hex(DER_OID('2.5.29.14'))); // 06 03 55 1D 0E
WriteLn(Hex(DER_OID('1.2.840.113549.1.7.2'))); // 06 09 2A 86 48 86 F7 0D 01 07 02
end.
O valor combinado é guardado num UInt64 de propósito. O DER_OID interpreta os arcos como Int64, por isso um segundo arco legal pode chegar a Int64.MaxValue, e somar 80 para arc1 = 2 faz overflow num inteiro de 64 bits com sinal. O UInt64 carrega Int64.MaxValue + 80 sem dar a volta, e o buffer de rascunho de dez bytes guarda os dez grupos de 7 bits de que um valor de 64 bits precisa. Os vetores de teste que valem a pena guardar são os dos dois lados da fronteira: 2.47 tem de continuar a ser um byte, e 2.48 tem de passar a dois
O que garante o percurso CMS do lado da leitura?
O PDFlibCMSRead garante estrutura e mais nada: devolve os bytes que estão onde a RFC 5652 diz que devem estar e não verifica assinatura, digest nem período de validade nenhum. O percurso aceita apenas DER, por isso o DERReadTLV recusa comprimentos indefinidos e números de tag multi-byte, e um CMS codificado em BER vindo de um signatário fora de conformidade reporta zero certificados em vez de um palpite parcial. Os attribute certificates e as restantes alternativas de CertificateChoices são saltados porque nada a jusante os consegue usar. A verificação criptográfica fica com o código que lhe pertence, que começa pelas verificações de cobertura de bytes descritas em assinatura PAdES e validação de ByteRange no Delphi e continua com classificar o que mudou depois de um PDF ser assinado
A lição mais ampla vale para qualquer formato binário: «dentro do buffer» é uma propriedade de segurança de memória, «dentro do pai» é uma propriedade de correção, e um parser precisa das duas. O mesmo pensamento sobre comprimentos hostis atravessa o endurecimento de um parser PDF em Pascal contra ficheiros maliciosos. As APIs de extração de certificados, construção de cadeias e validação a longo prazo aqui discutidas vêm com a losLab PDF Library for Delphi, para Delphi, C++Builder e Lazarus