Teknik Makale

Delphi'de PDF sertifika şifrelemesi: RSA-OAEP ve ECDH

Sertifika ile şifreli bir PDF, dosyanın kime yazıldığından bağımsız olarak şifreleme sözlüğünde tek bir /P değeri taşır; bu yazıda sonra kurulan dosyalarda o değer -64'tür. Üç farklı izin setine sahip üç alıcı aynı tek sayıyı paylaşır. "Bu kullanıcı yazdırabilir mi?" sorusunu /P'den yanıtlayan kod bir yer tutucuyu okuyor demektir; çünkü public-key security handler altında gerçek izin sözcüğü her alıcının şifreli zarfının içinde seyahat eder ve yalnızca o alıcının özel anahtarı zarfı açtığında görünür

O tasarım, sertifika şifrelemesinin zahmete değmesinin nedenidir. Üç aylık denetim paketi üç dış incelemeciye gider; hukuk her birinin onu okumasını ister, yalnızca biri yazdırabilsin ister ve hiç kimse ekin yanındaki e-posta zincirinde bir parola beklemesin ister. Parola bunu ifade edemez. Sertifikalar eder: her alıcı dosyayı hâlihazırda elinde tuttuğu bir anahtarla açar ve her zarf kendi izin sözcüğünü taşır

Sertifika temelli PDF şifrelemesi paroladan nasıl ayrılır?

Public-key ile şifreli bir PDF, dosya anahtarını rastgele bir seed artı her alıcı zarfının tam baytlarından türetir; bir insanın yazdığı hiçbir şey hesaba girmez. Handler ISO 32000-1 §7.6.4'te (ISO 32000-2'de §7.6.5) tanımlanır ve zarflar RFC 5652'den CMS EnvelopedData yapılarıdır. HotPDF, /SubFilter /adbe.pkcs7.s5 ile /Filter /Adobe.PubSec yazar. AES-256 için bu, /V 5, /CF altında /CFM /AESV3'lü bir /DefaultCryptFilter girdisi ve üst düzey sözlük yerine o crypt filter'ın içine yerleştirilen /Recipients dizisi demektir

Her zarf 24 baytı şifreler: 20 baytlık seed ve ardından o alıcının 32 bitlik izin sözcüğü. Yükleme sırasında okuyucu bir zarfı açar, seedi geri kazanır ve dosya anahtarını yeniden kurmak için seedi /Recipients sırasındaki her zarfla birlikte hash eder (AES-256 için SHA-256, eski cipherlar için SHA-1). İncelemeci B, incelemeci A'nın ne yapabildiğini asla öğrenmez; ama B'nin okuyucusu yine de A'nın zarf baytlarını hash'ler, yani tüm dizi baytı baytına hayatta kalmalıdır. Bu model ile sıradan parolalar arasında hâlâ karar veriyorsanız, AES-256 parola şifrelemesi ve izin bayrakları rehberi o takasın öteki yüzünü kapsar

HotPDF public-key şifreleme şeması: EnablePubKeyEncryption 20 baytlık bir seed sabitler, her CMS EnvelopedData zarfı o 20 baytı artı bir 32 bitlik izin sözcüğünü /Filter /Adobe.PubSec içinde /SubFilter /adbe.pkcs7.s5 ve /CFM /AESV3 ile şifreler; okuyucu bir zarfı açar, seedi geri kazanır ve dosya anahtarını yeniden kurmak için diziliş sırasındaki her /Recipients girdisiyle hash eder
Şifreleme sözlüğündeki /P değeri yalnızca bir yer tutucudur, çünkü gerçek izinler her zarfın içinde seyahat eder ve özetin üzerinden koştuğu diziyi sonraki hiçbir aşama yeniden sıralayamaz ya da yeniden kodlayamaz

EnablePubKeyEncryption ile RSA alıcıları yazmak

RSA sertifikaları için sıra kısadır: aes256 ile EnablePubKeyEncryption, sonra BeginDoctan önce DER kodlu her sertifika için bir AddPubKeyRecipientCertificate çağrısı. Yardımcı RSAES-OAEP zarfını işlem içinde kurar. Son iki parametresi OAEP özeti ile MGF1 özetini THPDFRSAOAEPHash'den seçer (rohSHA256, rohSHA384 ya da rohSHA512); ikisi de varsayılan olarak rohSHA256'dır ve zarf içeriği AES-256-CBC ile şifrelenir

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFRSA;

procedure WriteAuditPack(const OutFile: string);
var
  Pdf: THotPDF;
  Seed: AnsiString;
begin
  SetLength(Seed, 20);                      // tam 20 bayt, AES-256 için bile
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // varsayılan anahtar türü aes128
    // İncelemeci A yazdırabilir; incelemeci B yalnızca okur ve çıkarır
    Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-a.cer'),
      [prPrint, prPrint12bit, prExtractContent], rohSHA256, rohSHA256);
    Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-b.cer'),
      [prExtractContent]);
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Q3 audit pack');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

O listelemede üç ayrıntı yük taşır. Seed her anahtar türü için, AES-256 dâhil, 20 bayttır ve EnablePubKeyEncryption başka her uzunlukta fırlatır. Anahtar türünün varsayılanı aes128 iken her iki sertifika yardımcısı aes256 üzerinde ısrar eder; ikinci argümanı düşürmek, ilk AddPubKeyRecipientCertificate çağrısını "certificate envelopes require aes256" mesajıyla fırlatır. Eski cipherlar (k40, k128, aes128), başka araç takımlarının kurduğu eksiksiz bir DER zarfı alan AddPubKeyRecipient üzerinden hâlâ erişilebilirdir

Üçüncü ayrıntı sürümdür. Public-key handler altında AES-256 bir PDF 2.0 özelliğidir ve HotPDF belge sürümünü kendi başına 2.0'a çeker. Daha düşük bir sürümde StrictVersionLock set edildiğinde o yükseltmeye izin verilmez: EnablePubKeyEncryption, hiçbir şeyi etkinleştirmeden döner ve hata bir satır sonra, alıcı yardımcısı "call EnablePubKeyEncryption first" raporladığında görünür. O mesaj insanları bir çağrı sırası hatası aramaya yollar; sürüm kilidini denetleyin. Artımlı bir güncelleme sırasında şifrelemeyi değiştirmek ise tümüyle reddedilir, EInvalidOpException ile

ECDH alıcıları eklemek: P-256, P-384, P-521, X25519 ve X448

Eliptik eğri sertifikaları AddPubKeyAgreementRecipientWithSecretten geçer; o çağrı bir CMS key-agreement alıcısı yazar (RFC 5753'ten KeyAgreeRecipientInfo, RFC 8418'in X25519 ve X448 profiliyle) ve ECDH paylaşılan sırrını işlem içinde hesaplar. Eğriyi bir THPDFPubKeyAgreementScheme değeri adlandırır: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ya da pkasX448. Şema, sertifikanın anahtar algoritmasıyla, eğrisiyle ve anahtar uzunluğuyla eşleşmek zorundadır; aksi hâlde çağrı "Certificate key does not match the requested agreement scheme" fırlatır

Her zarf taze rastgele 32 baytlık bir UKM alır, stdDH KDF'ten bir anahtar şifreleme anahtarı (P-256 ile X25519 için SHA-256, P-384 için SHA-384, P-521 ile X448 için SHA-512) ve RFC 3394'te tanımlanan bir AES-256 key wrap alır. Eğri aritmetiği altında platform kripto sağlayıcısı olmayan düz Pascal'dır; saf Pascal NIST eğri aritmetiği yazısı o katmanın nasıl kurulduğunu gezdirir. Montgomery eğrilerinde tüm geçici anahtar çifti yerelde üretilebilir:

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
  HPDFKeyAgreement;

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Zarf başına taze geçici skaler; clamping merdivenin içinde olur
  SetLength(Scalar, 32);
  AESGenerateRandomBytes(@Scalar[0], Length(Scalar));
  try
    OriginatorPublic := HPDFX25519PublicFromScalar(Scalar);
    Pdf.AddPubKeyAgreementRecipientWithSecret(
      TFile.ReadAllBytes('legal-x25519.cer'),
      [prPrint, prExtractContent], pkasX25519,
      OriginatorPublic, Scalar,
      []);   // OwnPublicPoint: yalnızca NIST eğrileri için anlamlı
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST eğrileri çağırandan daha fazlasını ister. HotPDF başlatıcı public anahtarlarını yalnızca X25519 ve X448 için türetir (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar); P-256, P-384 ve P-521 için geçici çift kendi araçlarınızdan gelir. Tam olarak alan boyutunda (32, 48 ya da 66 bayt) big-endian bir skaler ve eşleşen sıkıştırılmamış 0x04||X||Y noktasını (65, 97 ya da 133 bayt) OriginatorPublicKey olarak geçirin ve aynı noktayı NIST şemaları için ayrılmış OwnPublicPoint parametresine verin. Yanlış uzunluklar anında fırlatır. Sertifikadan gelen alıcı noktası, herhangi bir çarpma koşmadan önce eğri denklemine karşı denetlenir

Hiçbir kütüphanenin denetleyemeyeceği şey, başlatıcı public anahtarınızın skalerinize ait olup olmadığıdır. Uyuşmayan yarımlar yine de iyi biçimli bir zarf üretir. Yazıcı, anahtarını skaler × alıcı noktasından türetir; alıcı, anahtarını kendi özel anahtarı × yanlış başlatıcı noktasından türetir ve alıcı tarafındaki RFC 3394 bütünlük denetimi fark eden ilk şey olur, o zamana kadar dosya yollanmıştır. Aynı eğri üzerindeki bir test anahtarıyla gidiş-dönüşlü bir yükleme derlemenize girsin; başarılı bir EndDoc ve makul bir dosya boyutu burada hiçbir şey kanıtlamaz

O eşleştirme adımı, eğri seçiminin pratikte neden önemli olduğunun ta kendisidir. Sertifika ihraçlarını siz yönetiyorsanız X25519, Delphi'den doğru kurması daha kolay eğridir: tek bir 32 baytlık rastgele skaler, tek bir yardımcı çağrısı, elle senkronda tutulacak bir nokta yok. NIST eğrileri, alıcı sertifikalarının yalnızca P-256 ya da P-384 ihraç eden bir PKI'dan geldiği zaman yerini hak eder; o durumda gidiş-dönüşlü yükleme isteğe bağlı olmaktan çıkar

HotPDF ECDH anlaşması şeması: AddPubKeyAgreementRecipientWithSecret paylaşılan sırrı saf Pascal eğri koduyla türetir, taze 32 baytlık UKM'yi stdDH KDF üzerinden karıştırır (P-256 ve X25519 için SHA-256, P-384 için SHA-384, P-521 ve X448 için SHA-512), sonra içerik anahtarını RFC 3394 AES-256 key wrap ile sararak KeyAgreeRecipientInfo zarfını kurar
pkasECDHP256'dan pkasX448'e şema değeri sertifika anahtarıyla eşleşmeli; uyuşmayan skaler ve public nokta yarımları yine de hiçbir alıcının açamayacağı biçimli bir zarf üretir

/Recipients sırası neden önemlidir?

/Recipients'in sırası önemlidir, çünkü dosya anahtarı, seedin ardından diziliş sırasındaki her zarfın geldiği bir özettir; yazan ile okuyan özdeş baytları özdeş sırada hash etmek zorundadır. HotPDF zarfları eklediğiniz sırada tutar ve değiştirmeden yazar; yani sırayı seçmekte serbestsiniz. Riskli olan, sonrasında dizinin başına gelen şeydir. İşleme sonrası araçları, imzalama hatları ve ev yapımı zarf kurucuları kırılmanın yaşadığı yerlerdir ve başarısızlıklar tek biçimlidir — iki taraf biraz farklı baytları hash ediyor:

  • Delphi'nin tiplenmemiş TList'i ham bir işaretçi saklar. Zarfları List.Add(Pointer(Bytes)) ile toplamak, referans sayısını yerel değişkende bırakır; o değişkendeki sonraki SetLength tamponu serbest bırakır ya da yeniden kullanır ve her yuva son zarfla özdeşleşebilir. TList<TBytes> ya da düz bir TArray<TBytes> referansları sayılı tutar
  • Yerinde çalışan DER ayrıştırıcıları, ayrıştırdıkları tamponu değiştirir. Her zarfı, hiçbir açma denemesi dokunmadan önce alınmış tertemiz bir kopyasından hash edin
  • Bir Unicode TStringList ikili DER için hiçbir yer değildir: $80 ve üzeri baytlar, girişte kod sayfası tarafından yeniden kodlanır. Zarfları TBytes'te tutun ya da önce hex kodlayın
  • Ciphertext, hex dizgelere aittir. Bir literal dizge içinde CR, LF ve CRLF tek bir LF olarak geri okunur (ISO 32000-1 §7.3.4.2); tesadüfen $0D $0A taşıyan bir zarf bir bayt kaybeder. HotPDF her /Recipients girdisini hex dizge olarak yazar ve onu dizge şifrelemesinin dışında tutar; çünkü okuyucu herhangi bir anahtar elde etmeden önce zarflara ihtiyaç duyar
  • Bir DER BIT STRING'in ilk içerik baytı kullanılmayan bitleri sayar ve bayt hizalı anahtarlar için $00 olmak zorundadır. O baytı düşürüp 0x04||X||Y noktasını doğrudan içeriğe koyan elle yazılmış DER, tüm anahtarı bir bayt kaydırır ve katı bir ayrıştırıcı başlatıcı anahtarını tümüyle reddeder
  • Çalışması gereken bir anahtar hâlâ çözemyorsa katman katman karşılaştırın, bu sırada: dosya anahtarı, ciphertext'in önündeki IV, nesne başına anahtar, düz metin. Hata, anlaşmazlığa düşen ilk katmanın hemen ardındadır

Sertifika ile şifreli bir PDF'i özel anahtarlarla yüklemek

Özel anahtarlar LoadFromFile koşmadan önce kaydedilmiş olmalıdır; çünkü HotPDF dosya anahtarını yapısal geçiş sırasında geri kazanır. HPDFParsePFX ile ayrıştırılan RSA kimlikleri PubSecKeyMaterial'e gider, ek RSA anahtarları AddPubSecKeyMaterial'e; o yardımcı, modulus ve özel üs taşımayan malzemeyi reddeder. İki anahtar yuvası da RSA modulusu ve özel üssü üzerinden eşlenir; yani EC anahtarları, PFX tabanlılar dâhil, üçüncü bir yolu izler: AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint); HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ya da HPDFOIDECP521 sabitlerinden biriyle. NIST eğrileri orada alıcının kendi sıkıştırılmamış public noktasını ister; Montgomery eğrileri onu yok sayar

PFX dosyası olarak gelen bir EC kimliği aynı çağrıya oturur. HPDFParsePFX onu KeyAlgorithm = pfxkaECDSA ile rapor eder (X25519 ve X448 kimlikleri dâhil) ve CurveOID'yi, PrivateKey'teki özel skaleri ve PublicKey'teki sertifika noktasını doldurur; üçü de doğrudan üç parametreye eşlenir

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;

procedure OpenAuditPack(const LegalScalar: TBytes);
var
  Reader: THotPDF;
  EcKey: THPDFPFXKeyMaterial;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.AutoLaunch := False;
    // RSA kimliği birincil anahtar olur
    Reader.PubSecKeyMaterial :=
      HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
    // Bir PFX'ten EC kimliği: eğri, skaler ve nokta anlaşma listesine gider
    EcKey := HPDFParsePFX(TFile.ReadAllBytes('reviewer-c-p256.pfx'), 'pfx-password');
    if EcKey.KeyAlgorithm = pfxkaECDSA then
      Reader.AddPubSecAgreementKeyMaterial(EcKey.CurveOID,
        EcKey.PrivateKey, EcKey.PublicKey);
    // Hukuk ekibinin tuttuğu ham X25519 skaleri; nokta gerekmez
    Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Kendi haline bırakılırsa HotPDF her zarfı her kayıtlı anahtara karşı dener: birincil RSA anahtarı, sonra her ek RSA anahtarı, sonra EC malzemesi. PubSecRecipientQuery bunu tek bir zarfa daraltır. Callback zarf sayısını alır ve dizi içinde sıfır tabanlı bir indeks döndürmek zorundadır; öteki her değer, -1 dâhil, yükleme sırasında fırlatır. Her zarfı denemek istiyorsanız özelliği atamasız bırakın. Delphi altında tür, bir fonksiyona referanstır ve anonim bir metot iş görür; FPC altında ise düz bir fonksiyon işaretçisidir ve Context argümanı PubSecRecipientQueryContext'ten gelir

Bir davranış kendi testini hak eder. Hiçbir kayıtlı anahtar hiçbir zarfı açamadığında, anahtar geri kazanımı bir dosya anahtarı olmadan biter ve o yüzden fırlamaz. Belgenin taşıdığını bildiğiniz içeriğe, bir metin dizgesine ya da bir metadata değerine iddia kurun; normal dönen bir LoadFromFile çağrısı yalnızca dosya yapısının ayrıştığını gösterir

HotPDF özel anahtar yükleme şeması: PubSecKeyMaterial HPDFParsePFX'ten gelen birincil RSA anahtarını taşır, AddPubSecKeyMaterial yalnızca RSA anahtarı ekler, AddPubSecAgreementKeyMaterial ham ECDH skalerlerini HPDFOIDX25519'dan HPDFOIDECP521'e eğri OID'leri altına kaydeder; LoadFromFile'da sağlayıcı her zarfa karşı önce birincil anahtarı, sonra her ek RSA anahtarını, sonra EC malzemesini dener
Hiçbir anahtar hiçbir zarfı açamadığında geri kazanım adımı istisna fırlatmak yerine dosya anahtarsız döner; içeriğin gerçekten çözüldüğünü doğrulayın ya da zarfı PubSecRecipientQuery ile sabitleyin

Hedef görüntüleyicide neyi denetlemelisiniz?

HotPDF'in yazıcısı ile okuyucusu bayt bayt anlaşır ve zarflar yukarıda anılan CMS yapılarını izler. Üçüncü taraf görüntüleyiciler ayrı bir sorudur: RSA-OAEP anahtar taşıması için, üstelik X25519 ya da X448 alıcılar için, destek okuyucudan okuyucuya ve aynı okuyucunun sürümleri arasında değişir. Belge belirli bir görüntüleyicide açılmak zorundaysa, aynı anahtar türünden bir test sertifikası için bir test dosyası şifreleyin ve bir şemaya bağlanmadan önce orada açın. Görüntüleyici sizin kontrolünüzün dışındaysa destek listesi şemayı seçer ve o, daha önce söylenen X25519 kolaylık argümanını geçersiz kılar

Zarf izinleri, kurallara uyan yazılımın uyduğu politika olarak kalır; parola şifrelemesi altında oldukları gibi. Seed kalitesi size aittir: AESGenerateRandomBytes o iş için oradadır ve HotPDF, dosya anahtarı türetildikten sonra seedin kendi kopyasını siler. Bir dizgenin, akışın ya da ekin farklı bir crypt filter'a ihtiyacı varsa StmF, StrF ve EFF için crypt filter politika rehberi nasıl atandığını gösterir; public-key handler DefaultCryptFilter'ı ya da Identity'yi kabul eder

RSA ve eliptik eğri sertifikaları için alıcı başına zarflar, geri yolundaki anahtar yüklemesiyle birlikte, parola şifrelemesi ve dijital imzaların yanında, Delphi ve C++Builder için HotPDF PDF component'in parçasıdır