PDF Library for Delphi (PDFlibPas) извлича сертификатите вътре в PDF подписа с чисто DER обхождане на CMS SignedData, съхранен в /Contents, без никакво участие на CryptoAPI. От v3.539.10 насам всяко вложено четене е ограничено от родителския си елемент, нулевият padding след CMS-а се отрязва на дължината, която CMS-ът декларира, а object identifier-ите кодират своя обединен първи subidentifier в base-128. Правилото за граници и поправката на OID-а заменят код, който даваше грешни резултати, без да вдига грешка, а правилото за padding-а не позволява на по-строгия четец да отхвърля истински подписи
Страната на четенето има по-голямо значение, отколкото изглежда. Инструментите за long-term validation трябва да извадят сертификата на подписващия и неговите издатели от съществуващ подпис, преди да стигнат до данните за отмяна, одитен доклад трябва да каже кой е подписал, а Lazarus build под Linux няма Windows message функции, на които да се опре. Парсер в това положение рядко се срива от лош вход. Болезненият режим на отказ е брой сертификати, включващ чужди байтове от съсед, съвпадение на подписващия по грешното поле или OID, който тихо се превръща в друг OID. Подписов pipeline, издигнат върху такава основа, докладва уверени безсмислици
Извличане на сертификатите на подписващия от подписан PDF
Пет метода на TPDFlib покриват страната на четенето, и всичките приемат InputFile, Password, FieldName: всяко извикване отваря файла само за четене, отговаря и го затваря отново. GetSignatureEmbeddedCertificateCount и GetSignatureEmbeddedCertificateDER изброяват множеството сертификати в реда на кодиране, GetSignatureSignerCertificateDER връща сертификата, произвел даден SignerInfo, а GetSignatureCertificateChainLength / GetSignatureCertificateChainDER тръгват от този подписващ към най-далечния издател, който самият подпис носи. Индексите са от нулата. Пазете резултатите в AnsiString — затова библиотеката ги връща така: DER blob, минал през string или TStrings, преминава през конверсия на знаковия набор и се връща повреден
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;
Две неща в този изход изискват внимание. Брой 0 не е диагноза: липсващо поле, грешна парола, blob, който не е DER, и SignedData, който просто пропуска незадължителното множество сертификати, всички връщат 0 или празен низ, затова логвайте името на полето до числото. И верига, приключила преди самоиздаден сертификат, също не е грешка. Строителят на вериги ползва само сертификатите, вградени в подписа, така че останалите издатели трябва да се доставят чрез адресите, които GetCertificateIssuerURLs докладва
Колко от /Contents всъщност е CMS-ът?
Само префиксът, който външният SEQUENCE декларира, принадлежи на CMS-а, а PLTrimCMSPadding отрязва всичко след него. Подписващият резервира hex низа /Contents, преди CMS-ът да съществува, защото /ByteRange, описан в ISO 32000-1 §12.8.1, трябва да бъде фиксиран първо, така че слотът е оразмерен щедро, а неизползваната опашка са нули. PLTrimCMSPadding чете първия TLV, изисква таг $30 и връща байтовете до края на този елемент; всичко, което не започва с добре оформен SEQUENCE, се връща празно. Това най-високо ниво е единственото място, където байтове в края са легални, а разликата има значение за следващия раздел: строго правило „елементът трябва да изчерпи целия буфер" би отхвърлило всеки реален подпис, докато слабо правило, приложено на всяка дълбочина, позволява на вложените полета да четат байтове, които не са техни
Защо DER четец се нуждае от крайното отместване на родителя?
Вложен елемент е валиден само ако свършва вътре в родителя си, а проверката срещу края на буфера не доказва това. Нисконивовият DERReadTLV в PDFlibASN1 ограничава всеки елемент с целия низ, което е правилната проверка за най-външния обект и грешната за всичко под него. Представете си SignerInfo, чийто issuerAndSerialNumber декларира 40 байта, докато issuer Name вътре в него претендира 60. Всеки байт пак е в буфера, така че четец, ограничен от буфера, приема Name-а, чете серийния номер от следващия дайджест алгоритъм и после сравнява тази двойка срещу вградените сертификати. Преди v3.539.10 CMS обхождането четеше точно така. Поправката е малка обвивка, която носи крайната позиция на родителя във всяко четене
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// нищо не остава вътре в родителя: отказ да стартира четене
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset вече стои един зад елемента; не бива да прекрачи родителя
Result := Offset <= ParentEnd;
end;
// всяко ниво записва собствения си край и го предава надолу:
// OuterEnd := end of ContentInfo (RFC 5652 section 3)
// ExplicitEnd := end of content [0] EXPLICIT
// ContentEnd := end of SignedData (RFC 5652 section 5.1)
// SignerEnd / InnerEnd for SignerInfo and issuerAndSerialNumber
Юнитът PDFlibCMSRead вече пренася тези краища през ContentInfo, обвивката [0] EXPLICIT, полетата на SignedData чак до signerInfos, SignerIdentifier и в двете му форми issuerAndSerialNumber и [0] subjectKeyIdentifier (RFC 5652 §5.3), и полетата на tbsCertificate, четени от всеки вграден сертификат при съвпадение на подписващия. Вътре в множеството сертификати и множеството signerInfos елемент, който мине отвъд края на множеството, спира цикъла: PLExtractCMSCertificates връща сертификатите, които вече е приел, и никога не залепя следващите байтове на crls или signerInfos към последния. Съвпадението issuer-and-serial изисква и двете половини, защото серийният номер е уникален само в рамките на един издател
Защо 2.999.3 излезе като 1.15.3?
Първите два клона на OID се обединяват в един subidentifier, не в един байт, а този subidentifier е кодиран в base-128 като всеки друг клон. X.690 §8.19.4 го дефинира като 40 * arc1 + arc2; по-старият DER_OID записваше тази стойност с Byte(...), което е коректно само до 127 — стойността на 2.47. За 2.999 сборът е 1079, cast-ът към байт запазва 55, а 55 се декодира като 1.15, така че идентификаторът тихо наименува друг клон на дървото. Стойностите от 128 до 255 се провалят различно, излъчвайки един байт с вдигнат continuation бит, който поглъща следващия клон. Повечето PKI идентификатори (1.2.840..., 2.5.29..., 0.4.0...) никога не стигат границата, което обяснява оцеляването на бъга; клоновете joint-iso-itu-t от 2.48 нагоре стигат. DER_OID обслужва както енкодера за signed attributes, така и съвпадението в DERFindExtensionByOID и проверката на content-type на SignedData, така че грешното кодиране счупи едновременно записа и търсенето
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.
Обединената стойност е държана в UInt64 нарочно. DER_OID парсва клоновете в Int64, така че законен втори клон може да бъде колкото Int64.MaxValue, а добавянето на 80 за arc1 = 2 препълва знаково 64-битово цяло число. UInt64 носи Int64.MaxValue + 80 без пренос, а десетбайтовият работен буфер побира десетте 7-битови групи, нужни на 64-битова стойност. Тестовите вектори, които си заслужава да се пазят, са тези от двете страни на границата: 2.47 трябва да остане един байт, а 2.48 трябва да стане два
Какво гарантира CMS обхождането от страната на четенето?
PDFlibCMSRead гарантира структура и нищо друго: връща байтовете, които седят там, където RFC 5652 казва, че трябва, и не проверява нито подпис, нито дайджест, нито период на валидност. Обхождането приема само DER, така че DERReadTLV отхвърля неопределени дължини и многобайтови номера на тагове, а BER-кодиран CMS от неконформен подписващ докладва нула сертификати вместо да отгатне частично. Attribute сертификатите и останалите алтернативи на CertificateChoices се прескачат, защото нищо надолу по веригата не може да ги използва. Криптографската проверка остава при кода, който я притежава — започва с проверките за byte покритие, описани в PAdES подписване и ByteRange валидация в Delphi, и продължава с класифицирането на промените след подписването на PDF
По-широкият урок важи за всеки двоичен формат: „вътре в буфера" е свойство на memory safety, „вътре в родителя" е свойство на коректността, а парсер се нуждае и от двете. Същото мислене за враждебни дължини минава през закаляването на Pascal PDF парсер срещу зловредни файлове. Обсъдените тук API за извличане на сертификати, строене на вериги и long-term validation идват с losLab PDF Library for Delphi, за Delphi, C++Builder и Lazarus