Teknik Makale

Delphi'de CMS imza ayrıştırma: DER sınırları ve OID kolları

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

PDFlibPas PLTrimCMSPadding, rezerve edilmiş /Contents hex dizesinin ilk TLVsini okur, $30 etiketini şart koşar ve sıfır dolgusunu dış SEQUENCEnin bildirdiği uzunlukta keser; tampon düzgün kurulu bir SEQUENCE ile başlamadığında boş sonuç döndürür
Kuyruk baytları yalnızca en üst düzeyde yasaldır; rezerve slot /ByteRange için sabit kalmak zorundadır — daha derin okumalar üst öğeyle sınırlı kuralı alır

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ı

PDFlibPas her iç içe DER okumasını üst öğeyle sınırlar: 40 baytlık issuerAndSerialNumber içindeki 60 baytlık issuer Name, eski tampon sınırlı DERReadTLV tarafından kabul edilir ve seri numarası digestAlgorithmdan okunur; ReadTLVWithin ise ParentEndi aşan bitişe sahip hiçbir öğeyi kabul etmez
Tamponun içinde olmak bellek güvenliği, üst öğenin içinde olmak doğruluktur — PDFlibPas üst bitiş ofsetini her CMS düzeyine taşır ki düşmanca bir uzunluk komşusunun baytlarını ödünç alamasın
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.
PDFlibPas DER_OID, ilk iki OID kolunu 40 * arc1 + arc2 olarak birleştirip toplamı UInt64te base-128 kodlar; 2.999.3 böylece 06 03 88 37 03 olur, eski Byte cast ise 55i tutup tanımlayıcıyı sessizce 1.15.3 olarak çözüyordu
PKI kollarının çoğu sınıra hiç ulaşmaz, bugün hayatta olmasının sebebi bu — 2.48 ve üzeri joint-iso-itu-t kolları iki bayt ister ve test 2.47 ile 2.48i iki yanda tutar

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