Teknik Makale

Delphi'de SM2, SM3 ve SM4 ile PDF İmzalama ve Şifreleme

PDFlibPas, Delphi ve C++Builder için PDF kütüphanesi, Çin ticari kriptografi setini saf Object Pascal içinde uygular: SM3 özeti (GB/T 32905), SM4 blok şifrelemesi (GB/T 32907) ve SM2 eliptik eğri imzaları (GB/T 32918). Bunlarla PDF'leri SetSignProcessSM2Identity üzerinden SM2-with-SM3 CMS imzalarıyla imzalar ve strength 6 ile Encrypt üzerinden belgeleri bir SM4 crypt filtresiyle şifreler; işin içinde ne bir OS sağlayıcısı ne de harici bir DLL vardır

Bunu istemenin olağan nedeni merak değil, satın alma şartıdır. Çin devletine, finansına ve sağlık sektörüne satılan sistemlerden ulusal algoritmaları kullanmaları giderek beklenir ve Windows CryptoAPI ile CNG, SM2 ile imzalamaz ya da SM3 ile özetlemez. İmzalama yolu platformdan geçiyorsa yanıt kısaca hayırdır. Saf Pascal bir uygulama aynı yanıtı Win32, Win64 ve FPC hedeflerinde de verir ve bunu belge hattının altında gezinen bir yerel bağımlılık olmadan verir

GB/T seti neden saf Object Pascal ile kuruldu?

PDFlibPas, SM2, SM3 ve SM4'ü Pascal'da kurar çünkü zaten hedeflediği hiçbir platform sağlayıcısı bu işi yapamaz. CryptoAPI ve CNG'nin SM3 imza desteği yoktur; SM2 kimliği var olmadan önce kütüphane, özet SM3'e ayarlandığında imzalamayı bilinçli olarak reddediyordu. Alternatif, bir özet OID bildirip imzayı başka bir şeyin üzerinden taşıyan bir SignerInfo'ydu; bu da görünür bir hata değil sessiz bir yanlış imzadır

Üç algoritmanın doğal bir kuruluş sırası vardır ve kütüphane onu izledi. Önce SM3 geldi, çünkü SM2 hem ZA kimlik özeti hem ileti özeti için ona ihtiyaç duyar. SM4 bağımsız bir ünite olarak ikinci geldi. SM2 en son geldi: önce spesifikasyon ekindeki yayımlanmış örnek eğriye karşı doğrulanmış ham ilkel fonksiyonlar olarak, sonra da Ed25519, Ed448 ve ML-DSA'yı hâlihazırda yöneten aynı CMS kurucusuna bağlanmış bir imzalama kimliği olarak. Aynı yaklaşım saf Pascal'da Ed448 ve Brainpool ECDSA eğrileri ile FIPS 204 ML-DSA post-quantum imzalayıcısı için anlatılır; SM2 de bir anahtar türü daha olarak o çerçeveye takılır

İlkel fonksiyonlar genel amaçlı işlevlerdir; PDF imzalamasının dışında da kullanabilirsiniz. PDFlibDigest içindeki SM3String ve SM3ByteArray ham 32 baytlık özeti döndürür (hex değil, karakter başına bir bayt) ve PDFlibSM4, iki biçimde ECB ve CBC yardımcıları dışa aktarır: yanlış uzunlukta boş dizge döndüren blok hizalı sürümler ve SM4EncryptCBCPKCS7 gibi, değişken uzunluklu veri için PKCS#7 sürümleri

uses
  SysUtils, PDFlibDigest, PDFlibSM4;

function RawToHex(const S: AnsiString): string;
var
  I: Integer;
begin
  Result := '';
  for I := 1 to Length(S) do
    Result := Result + IntToHex(Byte(S[I]), 2);
end;

var
  Digest, Key, IV, Cipher, Plain: AnsiString;
begin
  // GB/T 32905 örneği: SM3("abc")
  Digest := SM3String('abc');
  Assert(LowerCase(RawToHex(Digest)) =
    '66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');

  // SM4 16 baytlık anahtar ister; CBC 16 baytlık IV ister
  Key := 'SixteenByteKey!!';
  IV  := 'InitVector16byte';
  Cipher := SM4EncryptCBCPKCS7(Key, IV, 'Invoice 2026-0042 approved');
  Plain  := SM4DecryptCBCPKCS7(Key, IV, Cipher);
  Assert(Plain = 'Invoice 2026-0042 approved');
end;

O 16 baytlık dizgeleri elle saymak riskinize olur. SM4 test setinde 16 bayt olması tasarlanan iki test dizgesi 17 bayt çıktı ve blok hizalı yardımcılar tasarım gereği yanlış uzunlukta boş dizge döndürdüğü için hata, bir istisna yerine kafa karıştırıcı bir assertion olarak ortaya çıktı

SM2 kimliğiyle bir PDF imzalamak

Bir PDFlibPas imzalama süreci, SetSignProcessSM2Identity ya da SetSignProcessSM2IdentityFromFile ona bir sertifika ve bir özel anahtar eklediğinde SM2 anahtarı kullanır. Sertifika DER'dir; özel anahtar ham bir 32 baytlık skaler, şifresiz bir PKCS#8 kapsayıcısı ya da bir SEC 1 ECPrivateKey olabilir. Bir SM2 kimliği eklemek, aynı süreçteki herhangi bir Ed25519, Ed448, ML-DSA ya da Brainpool kimliğini temizler ve sertifika deposu ile CNG imzalamayı kapatır; böylece tek bir imzalama süreci asla birbirine rakip iki anahtar taşımaz

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('contract.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Signature1');
    if Pdf.SetSignProcessSM2IdentityFromFile(SignId,
         'signer-sm2.cer', 'signer-sm2.key') <> 1 then
      raise Exception.Create('SM2 certificate or key file missing');
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Shanghai', '[email protected]');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 8);   // 8 = SM3
    Pdf.EndSignProcessToFile(SignId, 'contract-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('SM2 signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

SetSignProcessSM2IdentityFromFile dönüş değerinin ne söylediğine ve ne söylemediğine dikkat edin. 1 döndürmesi, iki dosyanın da okunup imzalama sürecine saklandığı anlamına gelir. Gerçek kontrol — özel skalerden genel anahtarı türetmek ve onu sertifikayla karşılaştırmak — imza uygulandığında çalışır; uyumsuz bir anahtar çifti bu yüzden kimlik çağrısında değil, 1 olmayan bir GetSignProcessResult olarak ortaya çıkar

SetSignProcessDigestAlgorithm içindeki 8 kodu SM3'ü seçer ve bir SM2 kimliğiyle fiilen zorunludur: CMS kurucusu, süreç ne istemiş olursa olsun bir SM2 anahtarı için SM3'ü zorlar; tıpkı bir Ed25519 anahtarının SHA-512'ye zorlanması gibi. Ters kombinasyon da engellenir. adbe.pkcs7.detached ya da ETSI.CAdES.detached ile SM3'e ayarlı ama SM2 kimliği taşımayan bir imzalama süreci, imzalamadan çıkar; çünkü kütüphanedeki başka hiçbir imzalayıcı, bildirilen OID'e uyan SM3 tabanlı bir imza üretemez

Bir SM2-with-SM3 SignerInfo'ya ne girer?

PDFlibPas'taki bir SM2 imzası, DER kodlu imzalı özniteliklerin — standart CMS kurulumunun — üzerinden hesaplanır, ama SM2, ECDSA'nın olmayan bir adım ekler. GB/T 32918.2, iletiyi ZA ile birlikte özetler; ZA, ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA) olarak tanımlı bir kimlik özetidir; burada eğrinin her parçası 32 baytlık big-endian bir değerdir ve ID, imzalayanın ayırt edici kimliğidir. İmza ardından r = (e + x1) mod n ve s = ((1 + d)-1(k - r·d)) mod n olur; k, kütüphane CSPRNG'sinden taze bir nonce alır ve bozuk turlarda yeniden deneme yapılır

PDFlibPas, spesifikasyonun varsayılan ayırt edici kimliğini — 16 karakterlik 1234567812345678 dizgesini — hem imzalama hem doğrulama tarafında kullanır. Onu değiştirecek bir API yoktur. Bu, sahendeki SM2 sertifikalarının ezici çoğunluğuyla uyuşur; ama bir karşı taraf özel bir ID ile imzalıyorsa ZA değerleri farklılaşır ve başka her bayt doğru olsa bile imza doğrulanmaz

(r, s) çifti, her DSA ailesi SignerInfo'nun kullandığı düzende, iki INTEGER değerinden oluşan bir DER SEQUENCE olarak CMS'e girer. PDFlibPas, GM/T 0006'da kayıtlı algoritma tanımlayıcılarını yazar: SM2 anahtarı için 1.2.156.10197.1.301, SM3 için 1.2.156.10197.1.401 ve SM2-with-SM3 için 1.2.156.10197.1.501. SM3 ile oluşturulan RFC 3161 zaman damgası istekleri (SetTimeStampOptions özeti 8) aynı SM3 OID'ini taşır. 3.589.1 öncesi sürümler kayıt dışı bir 1.2.156.101.1 kolu yazıyordu; doğrulama onu hâlâ kabul eder, yani o sürümlerle imzalanan belgeler PDFlibPas içinde geçerliliğini korur, ama üçüncü taraf doğrulayıcılar onları tanımaz. Kendi hattınızdan çıkacak her şeyi yeniden imzalamak için iyi bir neden

PDFlibPas'ta Delphi için SM2-with-SM3 SignerInfo kurulumu: özet, ENTL, ID ve eğri parametrelerinin varsayılan ID 1234567812345678 ile SM3ünü kapsar; r ve s, taze bir CSPRNG nonce ile GB/T 32918.2 izler ve CMS, GM/T 0006 OIDleri 1.2.156.10197.1.301, 1.2.156.10197.1.401 ve 1.2.156.10197.1.501 taşır
İmza değeri, imzalı öznitelikler üzerinden iki INTEGER'dan oluşan bir DER SEQUENCE'dir; 3.589.1 öncesi sürümler, üçüncü taraf doğrulayıcıların reddettiği kayıt dışı bir OID kolu yazıyordu, o yüzden hattınızdan çıkacak her şeyi yeniden imzalayın

SM2 doğrulamasının kırıldığı yer: DER imza baytı ve kaybolan elde

SM2 kodundaki mühendislik emeğinin çoğu, inandırıcı görünen çıktı üreten iki kusura gitti. İkisi de elle yazılmış büyük tamsayılı kriptografinin tipik kusurlarıdır ve bir kez tanıdığınızda ikisinden de ucuza kaçılır

İlki bir DER imza baytıydı. ASN.1 INTEGER değerleri işaretlidir; kodlayıcı, üst biti ayarlı her r ya da s değerinin başına doğru bir şekilde 0x00 ekler ve 33 baytlık bir değer ortaya çıkar. Doğrulayıcı ise her tamsayıyı, o baytı önce söküp atmadan sabit 32 bayt genişliğe dönüştürüyordu. r ve s değerlerinin yaklaşık yarısının üst biti ayarlıdır; yeni imzalanan belgelerin büyük bir kesimi bu yüzden doğrulamadan geçemiyordu. İlk deneme turu tesadüfen kısa değerler çekti ve geçti; rastgele nonce ile imzalayan Delphi Win32 seti sorunu anında ortaya çıkardı. Doğrulayıcı artık herhangi bir 33 baytlık tamsayıyı 32 bayta doldurmadan önce baştaki sıfırı siler ve daha uzun olanı reddeder

PDFlibPas'ta Delphi için SM2 imza doğrulama kusuru ve düzeltmesi: ASN.1 INTEGER kodlaması, r ya da s değerinin üst biti ayarlıyken 0x00 öne ekler; eski doğrulayıcı 33 baytlık değeri imza baytını söküp atmadan sabit 32 bayta dolduruyordu ve düzeltilen doğrulayıcı baştaki sıfırı silip daha uzun olanı reddediyor
r ve s değerlerinin yaklaşık yarısı 0x00 önekini taşır; inandırıcı görünen çıktı bu yüzden toplu biçimde doğrulamadan geçemiyordu. Kısa deneme değerleri, Delphi Win32 seti rastgele nonce ile imzalayana dek ilk turu geçiyordu

İkincisi 256 bitlik toplamada kaybolan bir eldeydi. Alan elemanları, sekiz küçük-endian UInt32 limb'idir ve modüler toplama "topla, sonuç p'nin altında değilse p'yi çıkar" biçiminde yazılmıştı. İndirgenmiş girdilerde toplam 2p'nin altında kalır ama 2256'yı aşabilir; aştığında atılan elde, değeri düşük 256 bite geri katlıyor ve p ile karşılaştırma her şeyin yolunda olduğunu söylüyordu. Büyük koordinatlı bir noktayı ikiye katlamak yanlış gidiyordu; küçük katlar gitmiyordu. Görünür belirti şu oldu: 1G ile 8G referansla uyuşurken 9G, 10G ve 11G uyuşmuyordu. Düzeltme, 1280 toplama girdisi ile çıktısını bağımsız hesaplanmış değerlerle karşılaştırıp ilk sapan çifte inerek bulundu

PDFlibSM2 içindeki düzeltilmiş toplama, ham limb toplamasının döndürdüğü eldeyi tutar ve elde ayarlıysa ya da sonuç p'nin altında değilse p'yi bir kez çıkarır; tek çıkarma yeterlidir, çünkü p 2255'in üzerindedir. Desen, ikinin gücünün hemen üzerindeki herhangi bir asal alana uyar. Toplama yardımcınız son eldeni atıyorsa, yalnızca 1G, 2G ve 3G değil, ikiye katlamanın sözcük sınırını ilk aştığı noktayı çok geçen katları da test edin

SM3 doğru olmadan önce neden iki kez yanlıştı?

İlk SM3 uygulaması her standart test vektöründe başarısız oldu; onun yanında yazılan bağımsız bir referans uygulaması da öyle. Birbiriyle uyuşup standardla uyuşmayan iki uygulama, yazım hatasını değil spesifikasyonun paylaşılan bir yanlış anlaşılmasını işaret eder. Formülleri yayımlanmış SM3 metniyle satır satır karşılaştırmak, kodun bellekten yazıldığı üç yer buldu

  • İleti genişletmesi 7 bitlik döndürmeyi W[j-13]'e uygular; W[j-6] düz bir XOR terimidir
  • SS1, rotl(A, 12), E ve döndürülmüş tur sabitini XOR ile değil 232 modunda toplamayla birleştirir
  • 16 ile 63 arası turlarda FF çoğunluk fonksiyonudur: (X AND Y) OR (X AND Z) OR (Y AND Z); SHA tarzı seçim fonksiyonu GG'ye aittir

O üç düzeltmenin ardından boş dizge, abc ve 64 baytlık iki bloklu vektörün hepsi eşleşti. SM4 ilk denemede yerini buldu, çünkü S-box ve test vektörleri hiç kod yazılmadan spesifikasyon metninden alınmıştı: tur anahtarı rk0 = F12186F9, tek bloklu vektör 681EDF34D206965E86B3E94F536E4246 ve bir milyon iterasyonlu vektör 595298C7C6FD271F0402F804C33D3F66. SM2 çalışması ilgili bir kural ekledi: uzun hex sabitleri elle yazılmaz, gruplanıp üretilir ve uzunluk kontrolünden geçirilir; çünkü elle kopyalanmış bir taban noktası koordinatında eksik bir basamak, var olmayan uygulama hatalarının peşine bir hata ayıklama turu sürükledi

SM4 crypt filtresiyle bir PDF şifrelemek

TPDFlib.Encrypt, strength 6 ile seçili belgeyi ISO 32000-1 §7.6.5'in crypt filtresi çerçevesi üzerinden SM4-128 ile şifreler. Şifreleme sözlüğü /V 4, /R 4 ve /Length 128 ile standart güvenlik işleyicisini kullanır ve /StdCF crypt filtresi /CFM /SM4 taşır. Strength 6 ile kaydetmek dosyayı en az PDF 1.6'ya yükseltir; GetEncryptionLevel böyle bir dosya için 7 raporlar ve genel anahtarlı şifreleme, strength 4 ile EncryptPublicKey üzerinden aynı seçeneği alır

var
  Pdf: TPDFlib;
  Perm: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('cannot load PDF');
    // yazdırma, kopyalama ve form doldurma serbest; düzenleme ve birleştirme yok
    Perm := Pdf.EncodePermissions(1, 1, 0, 0, 1, 1, 0, 1);
    if Pdf.Encrypt('owner-secret', 'reader-pass', 6, Perm) <> 1 then
      raise Exception.Create('SM4 encryption failed');
    Pdf.SaveToFile('report-sm4.pdf');
  finally
    Pdf.Free;
  end;
end;

Her dizge ve akış, 16 baytlık rastgele bir IV ve ardından PKCS#7 dolgulu SM4-CBC şifreli metni olarak şifrelenir; AESV2'nin kullandığı düzenin aynısıdır. Nesne başına anahtar, ISO 32000-1 §7.6.2'nin 1 numaralı algoritmasını izler: dosya anahtarı, nesne numarasının üç düşük baytı ve üretim numarasının iki baytı, artı sAlT eki; MD5 ile özetlenir ve 16 bayta kırpılır. Paroladan anahtara türetim revizyon 4 olanıdır; strength 6 bu yüzden onun sınırlarını miras alır: parolalar SASLprep işleminden geçirilmez (yalnızca AES-256 olan 3, 4 ve 5 geçirilir) ve anahtar türetimi AESV2'ninkinden daha güçlü değildir

Delphi'de PDFlibPas Encrypt strength 6'nın yazdığı SM4 crypt filtresi yapısı: /V 4, /R 4 ve /Length 128 ile /StdCF filtresi /CFM /SM4 olarak ayarlı; dosya anahtarı, nesne ve üretim numaraları artı sAlT üzerinden 16 bayta kırpılan nesne başına MD5 anahtarı; her akış, 16 baytlık IV ve ardından PKCS#7 dolgulu SM4-CBC şifreli metni olarak saklanır
Dizgeler ve akışlar SM4 çekirdeğiyle AESV2 bayt düzenini kullanır ama /SM4, ISO 32000'ün tanımladığı /CFM değerlerinin dışındadır; ana akış okuyucuları şifreyi çözemez ve genel dağıtım AES-256'da kalmalıdır

Dağıtmadan önce bilmeniz gereken birlikte çalışabilirlik sınırları

Dürüst özet şu: SM2 imzalaması yukarıda anlatılan OID'ler ve varsayılan ID sınırları içinde birlikte çalışır; SM4 şifrelemesi ise yalnızca aynı kuralı bilen yazılımlarla birlikte çalışır. /SM4, ISO 32000-1 Tablo 25'te ya da ISO 32000-2'de tanımlı /CFM değerlerinden biri değildir; o tablolar None, V2, AESV2 ve AESV3'ü tanımlar. /CFM /SM4 ile karşılaşan ana akış bir PDF okuyucusu dosyanın şifresini çözmez. PDFlibPas ise kendi güvenlik işleyicisi üzerinden /SM4 ile /SM4V1 varyantını okur; yani PDFlibPas'ın yazıp okuduğu kapalı bir hat çalışır. Geneli hedefleyen bir dosya AES-256 kullanmalıdır

İmzalarda sınır, sertifika düzeni ve karşı taraftaki doğrulayıcıdır. İmzalama sürecinin arkasındaki kontrol olan PLValidateSM2Identity, SubjectPublicKeyInfo'nun SM2 eğrisi 1.2.156.10197.1.301'i parametre olarak taşıyan id-ecPublicKey içerdiği GM/T 0015 düzenini kabul eder; SM2 OID'ini algoritma alanının kendisine koyan sertifikaları da kabul eder ve türetilen genel noktası özel skalerle uyuşmayan her şeyi reddeder. Üretimde SM2 imzalarına güvenmeden önce, gerçekten kullanacağınız CA'dan bir sertifikayla bir belge imzalayın ve karşı tarafın çalıştırdığı doğrulayıcıyla doğrulayın. Kimlik kontrolünü bir imzalama süreci başlatmadan önce kendiniz de çalıştırabilirsiniz:

uses
  SysUtils, PDFlibModernSign;

var
  CertDER, KeyDER, Scalar: AnsiString;
begin
  // CertDER ve KeyDER diskten ya da bir HSM dışa aktarımından yüklendi
  // Out parametresini kendi değişkeninde tutun: KeyDER'i hem Const girdi
  // hem Out çıktı olarak vermek girdiyi önce boşaltır
  if not PLValidateSM2Identity(CertDER, KeyDER, Scalar) then
    raise Exception.Create('certificate and SM2 private key do not match');
  FillChar(Pointer(Scalar)^, Length(Scalar), 0);
end;

O yorum, entegrasyon çalışmasından gerçek bir kusuru kaydeder. Aynı AnsiString değişkeni hem Const girdi hem Out çıktı olarak verildiğinde derleyici Out değişkenini girişte sonlandırdı, girdi rutin onu okumadan boşaldı ve tam sağlam bir anahtarda kimlik kontrolü başarısız oldu. Anahtarı Const olarak alıp türetilmiş bir anahtarı Out olarak döndüren her API, çağıran tarafında ayrı değişkenler ister

SSS

PDFlibPas, SM2, SM3 ve SM4 için Windows CNG ya da harici bir DLL gerektirir mi?

Hayır. PDFlibPas üç algoritmayı da Object Pascal içinde, PDFlibDigest, PDFlibSM4 ve PDFlibSM2'de uygular ve SM2 imzalarını kütüphanenin CMS kodu kurar ve doğrular. SM2 imzasının üretilmesinde Windows CryptoAPI ve CNG'nin payı yoktur

SM3 yerine SM2'yi SHA-256 ile kullanabilir miyim?

Hayır. Bir imzalama süreci SM2 kimliği taşıdığında PDFlibPas CMS kurucusu daima SM3 kullanır, çünkü GB/T 32918.2, ZA kimlik özetini içeren bir SM3 özeti üzerinden SM2 imzası tanımlar. SM2 imzalama sürecinde özeti SHA-256'ya ayarlamak sonucu değiştirmez

Adobe Acrobat, PDFlibPas ile SM4 ile şifrelenmiş bir PDF açar mı?

Beklemeyin. /CFM /SM4 crypt filtresi ISO 32000'ün tanımladığı değerlerin dışındadır; ana akış okuyucular onu tanımaz. SM4 şifrelemesini dosyaları PDFlibPas ile ya da uyumlu bir uygulamayla okuyan kapalı sistemler için kullanın; genel dağıtıma gidecek belgeler için AES-256 kullanın

SM2 imzalaması, SM3 özetlemesi, SM4 şifrelemesi ve PAdES imzalama API'lerinin tamamı PDFlibPas, the Delphi PDF Library ile gelir; kütüphane Windows ve Unix hedeflerinde Delphi, C++Builder ve FPC/Lazarus ile derlenir