Teknik Makale

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

HotPDF, bir PDF'i belirli sertifika sahipleri için ISO 32000 public-key security handler üzerinden şifreler: EnablePubKeyEncryption 20 baytlık rastgele bir seed alır ve her alıcı, RSA anahtarları için AddPubKeyRecipientCertificatein (RSA-OAEP key transport) ya da eliptik eğri anahtarları için AddPubKeyAgreementRecipientWithSecretin (P-256, P-384, P-521, X25519 ya da X448 üzerinde ECDH) kurduğu kendi CMS zarfını alır. Hiç kimse parola paylaşmaz; eşleşen özel anahtarı elinde tutan dosyayı açar

Kullanım vakası her zaman aynı hikâyenin bir türlüdür. Üç 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 şifrelemesi bunu ifade edemez. Sertifika şifrelemesi eder; çünkü her alıcı belgeyi zaten elinde tuttuğu bir anahtarla açar ve her alıcı kendi zarfının içinde farklı bir izin seti taşıyabilir

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

Public-key ile şifrelenmiş bir PDF, dosya anahtarını rastgele bir seed artı her alıcı zarfının baytlarından türetir; bir insanın yazdığı hiçbir şeyden değil. Handler ISO 32000-1 §7.6.4'te (ISO 32000-2'de §7.6.5) tanımlanır ve zarflar RFC 5652'de tanımlanan CMS EnvelopedData yapılarıdır. HotPDF, /SubFilter /adbe.pkcs7.s5 ile /Filter /Adobe.PubSec yazar; AES-256 için bu, /V 5 ve /CF altında /CFM /AESV3lü bir /DefaultCryptFilter girdisi demektir ve /Recipients dizisi o crypt filter içinde yaşar. Her zarf 24 baytı şifreler: 20 baytlık seed ve ardından o alıcının 32 bitlik izin sözcüğü. Şifreleme sözlüğündeki /P değeri yalnızca bir yer tutucudur, çünkü gerçek izinler her zarfın içinde seyahat eder. Yükleme sırasında bir 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). 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 EnablePubKeyEncryptionı aes256 ile çağırın, sonra BeginDoctan önce DER kodlu her sertifika için bir kez AddPubKeyRecipientCertificate çağırın. Yardımcı, OAEP özeti ve MGF1 özeti için THPDFRSAOAEPHash değerleriyle (rohSHA256, rohSHA384 ya da rohSHA512) işlem içinde bir RSAES-OAEP zarfı kurar ve zarf içeriğini AES-256-CBC ile şifreler

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. Birincisi, seed uzunluğu her anahtar türü için, AES-256 dahil, 20 bayt sabittir; EnablePubKeyEncryption başka her uzunlukta istisna fırlatır. İkincisi, EnablePubKeyEncryption varsayılanı aes128dir ve her iki sertifika yardımcısı da anahtar türü aes256 olmadan çalışmayı reddeder; ikinci argümanı unutmak size "certificate envelopes require aes256" istisnasını getirir. Eski cipherlar (k40, k128, aes128) hâlâ çalışır ama yalnızca başka yerde kurduğunuz bir zarfla AddPubKeyRecipient üzerinden. Üçüncüsü, AES-256 public-key şifrelemesi bir PDF 2.0 özelliğidir, bu yüzden HotPDF belge sürümünü otomatik olarak 2.0'a çeker. Daha düşük bir sürümde StrictVersionLock set edildiğinde EnablePubKeyEncryption, hiçbir şeyi etkinleştirmeden döner ve hata sonraki satırda ancak "call EnablePubKeyEncryption first" olarak görünür. Artımlı bir güncelleme sırasında şifrelemeyi değiştirmek anında EInvalidOpException fırlatır

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

Eliptik eğri sertifikaları için AddPubKeyAgreementRecipientWithSecret, bir CMS key-agreement alıcısı yazar (RFC 5753'ten KARI yapısı KeyAgreeRecipientInfo, RFC 8418'den X25519 ve X448 profiliyle) ve ECDH paylaşılan sırrını işlem içinde hesaplar. Eğriyi bir THPDFPubKeyAgreementScheme değeriyle seçersiniz: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ya da pkasX448. Şema sertifikadaki anahtarla eşleşmek zorundadır, yoksa çağrı "Certificate key does not match the requested agreement scheme" fırlatır. Kaputun altında her zarf taze rastgele 32 baytlık bir UKM, stdDH KDF ile türetilmiş bir anahtar şifreleme anahtarı (P-256 ve X25519 için SHA-256, P-384 için SHA-384, P-521 ve X448 için SHA-512) ve RFC 3394'te tanımlanan bir AES-256 key wrap alır. Paylaşılan sırrın kendisi saf Pascal eğri kodundan gelir; platform crypto sağlayıcısı devrede değildir. Saf Pascal NIST eğri aritmetiği yazısı o katmanın nasıl kurulduğunu ve doğrulandığını anlatır. Montgomery eğrileri için 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. Kütüphane public-key yardımcılarını yalnızca X25519 ve X448 için getirir (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar); P-256, P-384 ve P-521 için geçici anahtar çiftini kendi araçlarınızla üretir ve tam olarak alan boyutunda (32, 48 ya da 66 bayt) big-endian bir skaler ile eşleşen sıkıştırılmamış 0x04||X||Y noktasını OriginatorPublicKey olarak geçirirsiniz. HotPDF alıcı noktasını eğri denklemine karşı doğrular ama başlatıcı public anahtarınızın gerçekten skalerinize ait olduğunu denetleyemez. Uyuşmayan yarımlar yine de hiçbir alıcının açamayacağı tümüyle biçimli bir zarf üretir; gidiş dönüşlü bir yüklemenin test takımınızda olmasının, yalnızca dosya boyutu kontrolünün değil, sebebi budur

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çeriği 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?

/Recipientsin sırası önemlidir, çünkü dosya anahtarı seed ile diziliş sırasındaki her zarfın özetidir; yazan ve okuyan aynı baytları aynı sırayla hash etmek zorundadır. HotPDF zarfları eklediğiniz sırayla tutar ve değiştirmeden yazar; alıcıları istediğiniz sıraya ekleyebilirsiniz ama o diziyi sonraki hiçbir aşama yeniden sıralayamaz, yeniden kodlayamaz ya da "temizleyemez". Bu alandaki gerçek hataların çoğu o temanın bir varyasyonuydu: iki taraf biraz farklı baytları hash ediyordu:

  • TListe Add ile dinamik dizi koymak yalnızca ham bir işaretçi tutar, referans sayısı yerel değişkende kalır. Sonraki SetLength tamponu serbest bırakır ve yeniden kullanabilir; her yuva son zarfla özdeşleşti ve çok alıcılı dosyalar yanlış anahtar türetti. Çözüm, sahiplenilmiş bir kopyayı List.Add(Pointer(System.Copy(Bytes))) ile saklamaktır
  • Zarf açma DER'i yerinde ayrıştırır ve anahtar geri kazanım geçişi başlangıçta o canlı dizileri hash ediyordu. Okuyucu artık herhangi bir açma onlara dokunmadan önce her zarfın tertemiz kopyasının anlık görüntüsünü alır ve özet anlık görüntülerin üzerinden koşar
  • Unicode bir TStringListten geçen ikili DER, $80 ve üzeri baytların kod sayfası tarafından yeniden kodlanmasına yol açar; HotPDF bu yüzden zarfları içeride hex metin olarak saklar
  • Şifreli ve ikili dizeler hex dize olarak yazılmalıdır. Literal bir dize satır sonu normalizasyonuna tabidir; CR, LF ve CRLF tek bir LF olur (ISO 32000-1 §7.3.4.2) ve bu, ciphertexti sessizce yeniden yazar. HotPDF her /Recipients girdisini hex dize olarak çıkarır ve onu dize şifrelemesinden muaf tutar; çünkü her okuyucu herhangi bir anahtara sahip olmadan önce zarflara ihtiyaç duyar
  • DER BIT STRINGin ilk baytı kullanılmayan bitleri sayar ve bayt hizalı anahtarlar için sıfır olmalıdır. SetLength sonrası onu başlatılmamış bırakmak yığındaki ne varsa yazdı ve katı bir açıcı başlatıcı anahtarı reddetti; dosya zaman zaman yazıldığı anahtarın kendisiyle açılamıyordu
  • Aynı anahtar hâlâ çözemiyorsa katman katman karşılaştırın: dosya anahtarı, sonra ciphertext öneki (IV), sonra nesne anahtarı, sonra düz metin. Hata, anlaşmazlığa düşen ilk katmanın hemen ardında yaşar

Sertifika ile şifreli bir PDF'i özel anahtarla nasıl açarsınız?

Sertifika ile şifreli bir PDF'i açmak için LoadFromFile çağırmadan önce özel anahtar malzemesini kaydedin, çünkü HotPDF dosya anahtarını yapısal geçiş sırasında geri kazanır. HPDFParsePFX ile ayrıştırılmış bir RSA ya da EC anahtarını PubSecKeyMateriale atayın, ek RSA anahtarlarını AddPubSecKeyMaterial ile ekleyin ve ham ECDH skalerlerini AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint) ile kaydedin; HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ya da HPDFOIDECP521 sabitlerini kullanarak. NIST eğrileri alıcının kendi sıkıştırılmamış public noktasını ister; Montgomery eğrileri onu yok sayar

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

procedure OpenAuditPack(const LegalScalar: TBytes);
var
  Reader: THotPDF;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.AutoLaunch := False;
    Reader.PubSecKeyMaterial :=
      HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
    Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
    // İsteğe bağlı: hepsini denemek yerine zarfı doğrudan seç
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = her zarfı sırayla dene
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Callback olmadan HotPDF her zarfı kayıtlı her anahtara karşı dener: önce birincil anahtar, sonra her ek RSA anahtarı, sonra EC malzemesi. PubSecRecipientQuery zarf sayısını alır ve 0 tabanlı bir indeks ya da -1 döndürür; dizi dışı bir indeks kırpılmak yerine istisna fırlatır. AddPubSecKeyMaterialin yalnızca RSA malzemesi kabul ettiğine dikkat edin (bir modulus ve özel üs ister); EC anahtarlarının yeri PubSecKeyMaterial ya da AddPubSecAgreementKeyMaterialdir. Hiçbir anahtar hiçbir zarfı açamadığında geri kazanım adımı istisna fırlatmak yerine dosya anahtarsız döner; yük çağrısının dönmesine güvenmek yerine beklediğiniz içeriğin gerçekten çözüldüğünü doğrulayın

HotPDF özel anahtar yükleme şeması: PubSecKeyMaterial HPDFParsePFX'ten gelen birincil RSA ya da EC anahtarını taşır, AddPubSecKeyMaterial yalnızca RSA anahtarı ekler, AddPubSecAgreementKeyMaterial ham ECDH skalerlerini HPDFOIDX25519'dan HPDFOIDP521'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

HotPDF'in garanti etmediği şeyler

HotPDF, kendi yazıcısı ile okuyucusunun bayt bayt anlaştığını garanti eder ve yukarıda anılan CMS yapılarını izleyen zarflar kurar. Her PDF görüntüleyicinin her kombinasyonu açacağını garanti etmez. RSA-OAEP key transport ve X25519 ya da X448 alıcı desteği okuyucular ve sürümler arasında değişir ve o kombinasyonlar için uyumluluk sonuçları yayımlamadık. Bir belgenin belirli bir görüntüleyicide açılması gerekiyorsa, şemaya bağlanmadan önce aynı anahtar türünden bir test sertifikası için bir test dosyası şifreleyin ve orada açın. Zarfın taşıdığı izinler, kurallara uyan yazılımın uyduğu politika olarak kalır; parola şifrelemesi altındakiyle tam olarak aynı. Seed kalitesi de sizin sorumluluğunuzdadır: AESGenerateRandomBytes o iş için oradadır ve HotPDF, dosya anahtarı türetildikten sonra seedin kendi kopyasını siler. Bir dizenin, akışın ya da ekin farklı bir crypt filter kullanmasına da ihtiyacınız varsa, StmF, StrF ve EFF için crypt filter politika rehberi public-key handlerın hangi filtre adlarını kabul ettiğini gösterir

Sertifika şifrelemesi, RSA-OAEP ve ECDH alıcı zarfları ve özel anahtar yükleme, HotPDF Delphi PDF component ile gelir; parola şifrelemesi, dijital imzalar ve ISO 32000 araç setinin gerisi, Delphi ve C++Builder için yanında durur