PDF Library for Delphi (PDFlibPas), bir PDF imzasının içindeki sertifikaları /Contentste saklanan CMS SignedData üzerinde saf bir DER yürüyüşüyle çıkarır; CryptoAPI devreye girmez. v3.539.10dan beri her iç içe okuma üst öğesiyle sınırlı, CMS sonrasındaki sıfır dolgusu CMSnin bildirdiği uzunlukta kesiliyor ve object identifierlar birleşik ilk alt tanımlayıcılarını base-128 ile kodluyor. Sınır kuralı ile OID düzeltmesi, hata fırlatmadan yanlış sonuçlar üreten kodun yerine geçti; dolgu kuralı ise daha katı okuyucunun gerçek imzaları reddetmesini engelliyor
Okuma tarafı göründüğünden daha ağır basar. Long-term validation araçları revocation verisi çekebilmek için önce mevcut imzadan signer sertifikasını ve onu verenleri dışarı çekmek zorunda; bir denetim raporu kimin imzaladığını söylemek zorundadır; Linux üzerindeki bir Lazarus derlemesinin ise yaslanacak hiçbir Windows mesaj fonksiyonu yoktur. Bu konumdaki bir ayrıştırıcı bozuk girdide nadiren çöker. Asıl acıtan başarısızlık modu, komşusuna ait baytları sayaçta toplayan bir sertifika sayısı, yanlış alana karşı yapılmış bir signer eşleşmesi ya da sessizce başka bir OIDe dönüşen bir OIDdir. Bunun üzerine kurulu imza hattı kendinden emin bir saçmalık raporlar
İmzalı bir PDFten signer sertifikalarını okumak
Okuma tarafını beş TPDFlib metodu karşılar ve hepsi InputFile, Password, FieldName alır: her çağrı dosyayı salt okunur açar, cevabı verir ve yeniden kapatır. GetSignatureEmbeddedCertificateCount ile GetSignatureEmbeddedCertificateDER sertifika setini kodlama sırasına göre numaralandırır, GetSignatureSignerCertificateDER verilen SignerInfo için imzalayıcı sertifikayı döndürür, GetSignatureCertificateChainLength / GetSignatureCertificateChainDER ise o signerdan imzanın kendisinin taşıdığı en uzak verene kadar yürür. İndeksler sıfır tabanlıdır. Sonuçları AnsiStringte tutun; kütüphane onları bu yüzden bu şekilde döndürüyor: string ya da TStrings üzerinden giden bir DER blobu karakter seti dönüşümünden geçer ve bozuk geri döner
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;
O çıktıda iki ayrıntıya dikkat gerekir. 0 sayısı bir teşhis değildir: eksik bir alan, yanlış bir parola, DER olmayan bir blob ve opsiyonel sertifika setini tamamen atlayan bir SignedData, tümü 0 ya da boş dize olarak döner; bu yüzden sayının yanına alan adını da loglayın. Ve bir self-issued sertifikaya ulaşamadan sona eren bir zincir de hata değildir. Zincir kurucu yalnızca imzanın içine gömülü sertifikaları kullanır; kalan verenler, GetCertificateIssuerURLsin bildirdiği adreslerden alınmak zorundadır
Gerçekte /Contentsin ne kadarı CMS?
Dış SEQUENCEnin bildirdiği önek CMSye aittir ve PLTrimCMSPadding sonrasındaki her şeyi keser. Bir signer, CMS daha var olmadan /Contents hex dizesini rezerve eder, çünkü ISO 32000-1 §12.8.1de anlatılan /ByteRangein önce sabitlenmesi gerekir; bu yüzden slot geniş boyutlanır ve kullanılmayan kuyruk sıfırlardır. PLTrimCMSPadding ilk TLVyi okur, $30 etiketini şart koşar ve o öğenin sonuna kadarki baytları döndürür; düzgün kurulu bir SEQUENCE ile başlamayan her şey boş döner. Kuyruk baytlarının yasal olduğu tek yer bu en üst düzeydir ve ayrım bir sonraki bölüm için önemlidir: öğenin tüm tamponu tüketmesini şart koşan katı bir kural, gerçek dünyadaki her imzayı reddederken her derinlikte uygulanan gevşek bir kural, iç içe alanlara kendilerine ait olmayan baytları okutturur
Bir DER okuyucusu üst öğenin bitiş ofsetine neden ihtiyaç duyar?
İç içe bir öğe yalnızca üst öğesinin içinde bitiyorsa geçerlidir ve tampon sonuna karşı kontrol bunu kanıtlamaz. PDFlibASN1deki alt düzey DERReadTLV her öğeyi tüm dizeyle sınırlar; bu, en dış nesne için doğru, altındaki her şey için yanlış kontroldur. issuerAndSerialNumberı 40 bayt bildiren ve içindeki issuer Name 60 iddia eden bir SignerInfo düşünün. Her bayt hâlâ tamponun içindedir; tamponla sınırlı okuyucu Namei kabul eder, ardından gelen digest algoritmasından seri numarasını okur ve o çifti gömülü sertifikalarla karşılaştırır. v3.539.10dan önce CMS yürüyüşçüsü tam olarak böyle okuyordu. Düzeltme, üst öğenin bitiş konumunu her okumaya taşıyan küçük bir sarmalayıcı
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// üst öğenin içinde hiçbir şey kalmadı: okuma başlatmayı reddet
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset artık öğenin bir ötesinde; üst öğeyi geçmemeli
Result := Offset <= ParentEnd;
end;
// her düzey kendi sonunu kaydeder ve aşağıya aktarır:
// 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 birimi artık o sonları ContentInfodan, [0] EXPLICIT sarmalayıcısından, signerInfosa kadarki SignedData alanlarından, hem issuerAndSerialNumber hem [0] subjectKeyIdentifier biçimindeki SignerIdentifierten (RFC 5652 §5.3) ve signer eşleştirilirken her gömülü sertifikadan okunan tbsCertificate alanlarından geçirir. Sertifika seti ile signerInfos seti içinde, setin sonunu aşan bir öğe döngüyü durdurur: PLExtractCMSCertificates o ana kadar kabul ettiği sertifikaları döndürür ve ardından gelen crls ya da signerInfos baytlarını sonuncusuna yapıştırmaz. Issuer-and-serial eşleşmesi iki yarımın ikisini de şart koşar; çünkü bir seri numarası yalnızca tek bir issuer içinde özgündir
2.999.3 neden 1.15.3 olarak çıktı?
Bir OIDnin ilk iki kolu tek alt tanımlayıcıda birleşir, tek baytta değil; ve o alt tanımlayıcı, diğer tüm kollar gibi base-128 ile kodlanır. X.690 §8.19.4 onu 40 * arc1 + arc2 olarak tanımlar; eski DER_OID bu değeri Byte(...) ile yazıyordu, bu da 2.47nin değeri olan 127ye kadar doğrudur. 2.999 için toplam 1079dur, byte cast 55i tutar ve 55, 1.15 olarak çözülür; yani tanımlayıcı sessizce ağacın başka bir dalını adlandırır. 128 ile 255 arasındaki değerler başka türlü başarısız olur: devam biti set edilmiş tek bayt yayıp bir sonraki kolu yutar. PKI tanımlayıcılarının çoğu (1.2.840..., 2.5.29..., 0.4.0...) bu sınıra hiç ulaşmaz; bugün hayatta olmasının sebebi budur. 2.48 ve üzeri joint-iso-itu-t kolları ulaşır. DER_OID hem signed attribute kodlayıcısına hem DERFindExtensionByOIDdeki eşleştiriciye hem SignedData content-type kontrolüne hizmet eder; yanlış bir kodlama yazmayı da aramayı da bozar
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.
Birleşik değer bilerek UInt64te tutulur. DER_OID kolları Int64e ayrıştırır; yani yasal bir ikinci kol Int64.MaxValue kadar büyük olabilir ve arc1 = 2 için 80 eklemek işaretli 64-bit integerı taşırır. UInt64 Int64.MaxValue + 80i sarmalanmadan taşır ve on baytlık scratch tamponu, 64-bit bir değerin gereksinim duyduğu on adet 7-bitlik grubu barındırır. Saklamaya değer test vektörleri sınırın iki yanındakilerdir: 2.47 tek bayt kalmalı, 2.48 iki bayta dönüşmeli
Okuma tarafındaki CMS yürüyüşçüsü neyi garanti eder?
PDFlibCMSRead yapıyı garanti eder, başka hiçbir şeyi değil: baytları RFC 5652nin söylediği yerde döndürür ve hiçbir imzayı, digesti ya da geçerlilik süresini doğrulamaz. Yürüyüşcü yalnızca DER kabul eder; DERReadTLV belirsiz uzunlukları ve çok baytlı etiket numaralarını reddeder ve standartlara uymayan bir signerın BER kodlu CMSi kısmi bir tahmin yerine sıfır sertifika bildirir. Attribute certificatelar ve diğer CertificateChoices alternatifleri atlanır, çünkü aşağı akışta onları kullanabilecek hiçbir şey yoktur. Kriptografik doğrulama, ona sahip olan kodda kalır; bu, Delphi'de PAdES imzalama ve ByteRange doğrulama yazısında anlatılan byte kapsama kontrolleriyle başlar ve PDF imzalandıktan sonra değişenlerin sınıflandırılması ile sürer
Daha geniş ders her ikili formata da taşınır: tamponun içinde olmak bir bellek güvenliği özelliği, üst öğenin içinde olmak bir doğruluk özelliğidir ve bir ayrıştırıcının ikisine de ihtiyacı vardır. Düşmanca uzunluklar hakkındaki aynı düşünce, saldırıya uğramış dosyalara karşı Pascal PDF ayrıştırıcısının sertleştirilmesi yazısında da akar. Burada konuşulan sertifika çıkarma, zincir kurma ve long-term validation APIleri, Delphi, C++Builder ve Lazarus için losLab PDF Library for Delphi ile birlikte gelir