Technický článek

Podpisování a šifrování PDF pomocí SM2, SM3 a SM4 v Delphi

PDFlibPas, PDF knihovna pro Delphi a C++Builder, implementuje čínský komerční kryptografický soubor v čistém Object Pascalu: SM3 hashing (GB/T 32905), SM4 blokové šifrování (GB/T 32907) a SM2 podpisy na eliptické křivce (GB/T 32918). S nimi podepisuje PDF CMS podpisy SM2-with-SM3 přes SetSignProcessSM2Identity a šifruje dokumenty crypt filtrem SM4 přes Encrypt se strength 6, bez zapojení jakéhokoli OS providera nebo externí DLL

Obvyklý důvod to chtít je požadavek z veřejných zakázek, ne zvědavost. Od systémů prodávaných do čínské státní správy, financí a zdravotnictví se čím dál častěji čeká používání národních algoritmů a Windows CryptoAPI a CNG nepodepisují SM2 ani nehashují SM3. Pokud podpisová cesta vede přes platformu, odpověď je prostě ne. Čistě Pascalovská implementace dává tutéž odpověď na Win32, Win64 i na FPC cílech, a dává ji bez pohyblivé nativní závislosti pod dokumentovou pipeline

Proč postavit soubor GB/T v čistém Object Pascalu?

PDFlibPas staví SM2, SM3 a SM4 v Pascalu, protože žádný platformový provider, na který už cílí, tu práci neudělá. CryptoAPI a CNG nepodporují SM3 podpis, takže než existovala SM2 identita, knihovna záměrně odmítala podepsat, když byl digest nastaven na SM3: alternativa byla SignerInfo, které deklaruje jeden digest OID a nese podpis něčeho jiného, což je tichý špatný podpis místo viditelné chyby

Tři algoritmy mají i přirozené pořadí stavby a knihovna ho následovala. SM3 přišel první, protože SM2 ho potřebuje pro hash identity ZA i pro message digest. SM4 přišel druhý jako nezávislý unit. SM2 přišel poslední, nejdřív jako syrové primitivy ověřené proti publikované ukázkové křivce v příloze specifikace, pak jako podpisová identita zapojená do téhož CMS builderu, který už obsluhuje Ed25519, Ed448 a ML-DSA. Tentýž přístup je popsán u křivek Ed448 a Brainpool ECDSA v čistém Pascalu a u post-kvantového podepisovače FIPS 204 ML-DSA a SM2 se do toho frameworku zapojí jako jeden klíčový typ navíc

Primitivy jsou veřejné funkce, takže je můžete použít mimo PDF podpis. SM3String a SM3ByteArray v PDFlibDigest vrací syrový 32bajtový digest (jeden bajt na znak, ne hex) a PDFlibSM4 exportuje ECB a CBC helpery ve dvou podobách: block-aligned verze, které vrací prázdný řetězec při špatné délce, a PKCS#7 verze jako SM4EncryptCBCPKCS7 pro data libovolné délky

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
  // ukázka GB/T 32905: SM3("abc")
  Digest := SM3String('abc');
  Assert(LowerCase(RawToHex(Digest)) =
    '66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');

  // SM4 potřebuje 16bajtový klíč; CBC potřebuje 16bajtové IV
  Key := 'SixteenByteKey!!';
  IV  := 'InitVector16byte';
  Cipher := SM4EncryptCBCPKCS7(Key, IV, 'Invoice 2026-0042 approved');
  Plain  := SM4DecryptCBCPKCS7(Key, IV, Cipher);
  Assert(Plain = 'Invoice 2026-0042 approved');
end;

Kdo ty 16bajtové řetězce počítá ručně, dělá to na vlastní riziko. Dva testovací řetězce v SM4 test suite, které měly být 16 bajtů, jich měly nakonec 17, a protože block-aligned helpery vrací po designu prázdný řetězec při špatné délce, selhání se projevilo jako matoucí assertion místo výjimky

Podepsání PDF SM2 identitou

Sign proces PDFlibPas používá SM2 klíč, jakmile mu SetSignProcessSM2Identity nebo SetSignProcessSM2IdentityFromFile připojí certifikát a privátní klíč. Certifikát je DER a privátní klíč může být syrový 32bajtový skalár, nešifrovaný kontejner PKCS#8 nebo SEC 1 ECPrivateKey. Připojení SM2 identity smaže jakoukoli identitu Ed25519, Ed448, ML-DSA nebo Brainpool na témže procesu a vypne podpisování z certifikátového úložiště a CNG, takže jeden sign proces nikdy nenese dva soutěžící klíče

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;

Všimněte si, co návratová hodnota SetSignProcessSM2IdentityFromFile říká a neříká. Návrat 1 znamená, že oba soubory se přečetly a uložily na sign proces. Skutečná kontrola — odvození veřejného klíče z privátního skalaru a srovnání s certifikátem — běží, když se aplikuje podpis, takže neshodný pár klíčů se projeví jako GetSignProcessResult jiný než 1, ne u volání identity

Kód 8 v SetSignProcessDigestAlgorithm vybírá SM3 a se SM2 identitou je fakticky povinný: CMS builder vynucuje SM3 pro SM2 klíč, bez ohledu na to, o co proces žádal, stejně jako se Ed25519 klíč nutí na SHA-512. Opačná kombinace je blokovaná také. S adbe.pkcs7.detached nebo ETSI.CAdES.detached sign proces nastavený na SM3, který nedrží SM2 identitu, skončí bez podpisu, protože žádný jiný podepisovač v knihovně nedokáže vytvořit SM3 podpis, který odpovídá deklarovanému OID

Co vstupuje do SignerInfo SM2-with-SM3?

SM2 podpis v PDFlibPas se počítá přes DER-kódované podepsané atributy, standardní CMS konstrukci, ale SM2 přidává krok, který ECDSA nemá. GB/T 32918.2 hashuje zprávu spolu s ZA, digestem identity definovaným jako ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA), kde každá část křivky je 32bajtová hodnota big-endian a ID je rozlišovací identifikátor podepisujícího. Podpis je pak r = (e + x1) mod n a s = ((1 + d)-1(k - r·d)) mod n, s čerstvým nonce k z CSPRNG knihovny a opakováním u degenerovaných kol

PDFlibPas používá výchozí rozlišovací identifikátor ze specifikace, 16znakový řetězec 1234567812345678, na straně podpisu i ověření. Neexistuje žádné API, jak ho změnit. Odpovídá to naprosté většině nasazených SM2 certifikátů, ale pokud protistrana podepisuje s vlastním ID, hodnoty ZA se liší a podpis se neověří, i když je každý ostatní bajt správný

Pár (r, s) jde do CMS jako DER SEQUENCE ze dvou hodnot INTEGER, layout, který používá každé SignerInfo rodiny DSA. PDFlibPas zapisuje identifikátory algoritmů registrované v GM/T 0006: 1.2.156.10197.1.301 pro SM2 klíč, 1.2.156.10197.1.401 pro SM3 a 1.2.156.10197.1.501 pro SM2-with-SM3. RFC 3161 požadavky na časová razítka vytvořené se SM3 (SetTimeStampOptions digest 8) nesou totéž SM3 OID. Vydání před 3.589.1 zapisovala neregistrovanou větev 1.2.156.101.1; ověření ji pořád přijímá, takže dokumenty podepsané těmi buildy dál validují v PDFlibPas, ale third-party validátory je nepoznají, což je dobrý důvod znovu podepsat cokoli, co opouští vaši vlastní pipeline

Konstrukce SignerInfo SM2-with-SM3 v PDFlibPas pro Delphi: digest pokrývá ZA = SM3 z ENTL, ID a parametrů křivky s výchozím ID 1234567812345678, r a s následují GB/T 32918.2 s čerstvým CSPRNG nonce a CMS nese OIDs GM/T 0006 1.2.156.10197.1.301, 1.2.156.10197.1.401 a 1.2.156.10197.1.501
Hodnota podpisu je DER SEQUENCE ze dvou INTEGER přes podepsané atributy; buildy před 3.589.1 zapisovaly neregistrovanou OID větev, kterou third-party validátory odmítají, takže znovu podepisujte vše, co opouští vaši pipeline

Kde se rozbilo ověřování SM2: DER podpisové bajty a ztracený přenos

Většina inženýrského úsilí v SM2 kódu šla do dvou defektů, které produkovaly věrohodný výstup. Oba jsou typické pro ručně psanou big-integer kryptografii a oba se dají levně vyhnout, jakmile je znáte

Prvním byl DER podpisový bajt. Hodnoty ASN.1 INTEGER jsou znaménkové, takže enkodér správně předřadí 0x00 ke každému r nebo s s nastaveným horním bitem, čímž vznikne 33bajtová hodnota. Verifikátor převáděl každý integer na pevnou šířku 32 bajtů, aniž by ten bajt nejdřív odstranil. Zhruba polovina všech hodnot r a s má nastavený horní bit, takže velká část čerstvě podepsaných dokumentů neprošla ověřením. První průzkumný běh náhodou losoval krátké hodnoty a prošel; Delphi Win32 suite, podepisující s náhodnými nonce, problém odhalil okamžitě. Verifikátor teď maže úvodní nulu z jakéhokoli 33bajtového integeru před doplněním na 32 bajtů a cokoli delšího odmítá

Defekt ověřování SM2 podpisu v PDFlibPas pro Delphi a jeho oprava: kódování ASN.1 INTEGER předřadí 0x00, když je nastaven horní bit r nebo s, starý verifikátor doplnil 33bajtovou hodnotu na pevných 32 bajtů bez odstranění podpisového bajtu a opravený verifikátor maže úvodní nulu a odmítá cokoli delšího
Zhruba polovina všech hodnot r a s nese prefix 0x00, takže věrohodně vypadající výstup masově propadl ověřením; krátké průzkumné hodnoty první běh přežily, dokud Delphi Win32 suite nepodepsal s náhodnými nonce

Druhým byl ztracený carry v sčítání 256 bitů. Prvky tělesa jsou osm little-endian UInt32 limbů a modulární sčítání bylo napsáno jako „sčítej, pak odčítej p, pokud výsledek není pod p". Pro redukované vstupy je součet pod 2p, ale může pořád překročit 2256, a když překročil, zahozený carry poskladal hodnotu zpět do dolních 256 bitů, kde srovnání s p říkalo, že je všechno v pořádku. Zdvojnásobení bodu s velkou souřadnicí selhalo; malé násobky ne. Viditelným symptomem bylo, že 1G až 8G odpovídalo referenci, zatímco 9G, 10G a 11G ne, a oprava se našla srovnáním 1280 vstupů a výstupů sčítání s nezávisle spočtenými hodnotami a zahnáním do první divergentní dvojice

Opravené sčítání v PDFlibSM2 drží carry vrácené syrovým sčítáním limbů a odečte p jednou, když je carry nastavené nebo výsledek není pod p; jediné odečtení stačí, protože p přesahuje 2255. Vzor platí pro jakékoli prvočíselné těleso těsně nad mocninou dvou. Pokud váš sčítací helper zahazuje finální carry, testujte násobky hluboko za bodem, kde zdvojnásobení poprvé překročí hranici slova, ne jen 1G, 2G a 3G

Proč byl SM3 dvakrát špatně, než začal být správně

První implementace SM3 propadla každému standardnímu testovacímu vektoru a totéž udělala nezávislá referenční implementace psaná vedle ní. Dvě implementace, které souhlasí navzájem a nesouhlasí se standardem, ukazují na sdílené nepochopení specifikace, ne na přepisovací chybu. Srovnání vzorců řádek po řádku s publikovaným textem SM3 našlo tři místa, kde byl kód psán z paměti

  • Rozšiřování zprávy aplikuje 7bitovou rotaci na W[j-13]; W[j-6] je prostý XOR člen
  • SS1 kombinuje rotl(A, 12), E a rotovanou konstantu kola sčítáním modulo 232, ne XOR
  • Pro kola 16 až 63 je FF většinová funkce (X AND Y) OR (X AND Z) OR (Y AND Z); volbová funkce ve stylu SHA patří k GG

Po těch třech opravách se rovnaly prázdný řetězec, abc i 64bajtový dvoublokový vektor. SM4 pak vešel na první pokus, protože S-box a testovací vektory se braly z textu specifikace dřív, než byl napsaný jakýkoli kód: kolo klíče rk0 = F12186F9, jednoblokový vektor 681EDF34D206965E86B3E94F536E4246 a vektor s milionem iterací 595298C7C6FD271F0402F804C33D3F66. Práce na SM2 přidala příbuzné pravidlo: dlouhé hex konstanty se generují po skupinách a kontrolují se na délku, nikdy se nepíší ručně, poté, co ručně opsaná souřadnice základního bodu postrádající jednu číslici poslala debugovací session za implementačními bugy, které neexistovaly

Šifrování PDF crypt filtrem SM4

TPDFlib.Encrypt se strength 6 šifruje vybraný dokument SM4-128 přes crypt filter framework ISO 32000-1 §7.6.5. Šifrovací slovník používá standardní security handler s /V 4, /R 4 a /Length 128 a crypt filtr /StdCF nese /CFM /SM4. Uložení se strength 6 zvedá soubor alespoň na PDF 1.6, GetEncryptionLevel hlásí u takového souboru 7 a šifrování veřejným klíčem dostane tutéž volbu přes EncryptPublicKey se strength 4

var
  Pdf: TPDFlib;
  Perm: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('cannot load PDF');
    // tisk, kopírování a vyplňování formulářů povoleny; žádné úpravy ani skládání
    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;

Každý řetězec a stream se šifruje jako 16bajtové náhodné IV následované SM4-CBC ciphertextem s PKCS#7 paddingem, tentýž layout, který používá AESV2. Klíč na objekt následuje Algorithm 1 z ISO 32000-1 §7.6.2: souborový klíč, tři nízké bajty čísla objektu a dva bajty čísla generace, plus přípona sAlT, hashované MD5 a zkrácené na 16 bajtů. Odvození klíče z hesla je to revizní 4, takže strength 6 dědí jeho limity: hesla neprocházejí SASLprep (jen síly AES-256 3, 4 a 5 ano) a odvození klíče není silnější než u AESV2

Struktura crypt filtru SM4 zapsaná PDFlibPas Encrypt se strength 6 v Delphi: /V 4, /R 4 a /Length 128 s filtrem /StdCF nastaveným na /CFM /SM4, MD5 klíč na objekt přes souborový klíč, číslo objektu a generace plus sAlT zkrácené na 16 bajtů a každý stream uložený jako 16bajtové IV následované SM4-CBC ciphertextem s PKCS#7 paddingem
Řetězce a streamy používají byte layout AESV2 s jádrem SM4, ale /SM4 leží mimo hodnoty /CFM, které ISO 32000 definuje, takže hlavní proud čteček nedokáže dešifrovat a obecná distribuce by měla zůstat na AES-256

Limity interoperability, které poznáte před nasazením

Poctivé shrnutí je, že SM2 podpisování interoperuje v mezích OIDs a výchozího ID popsaných výše, zatímco SM4 šifrování interoperuje jen se softwarem, který zná tutéž konvenci. /SM4 není jednou z hodnot /CFM definovaných v tabulce 25 ISO 32000-1 ani v ISO 32000-2, které definují None, V2, AESV2 a AESV3. Hlavní proud PDF čteček nedekšifruje soubor, na který narazí s /CFM /SM4. PDFlibPas samo čte /SM4 a variantu /SM4V1 přes svůj security handler, takže uzavřená pipeline, kde PDFlibPas zapisuje i čte, funguje; soubor pro širokou veřejnost by měl použít raději AES-256

U podpisů je hranicí layout certifikátu a validátor na druhé straně. PLValidateSM2Identity, kontrola za sign procesem, přijímá layout GM/T 0015, kde SubjectPublicKeyInfo nese id-ecPublicKey s SM2 křivkou 1.2.156.10197.1.301 jako parametrem, i certifikáty, které dávají SM2 OID přímo do pole algoritmu, a odmítá cokoli, jehož odvozený veřejný bod neodpovídá privátnímu skalaru. Než se v produkci spolehnete na SM2 podpisy, podepište dokument certifikátem od CA, kterou skutečně použijete, a ověřte ho validátorem, který běží u vaší protistrany. Kontrolu identity můžete pustit sami před spuštěním sign procesu:

uses
  SysUtils, PDFlibModernSign;

var
  CertDER, KeyDER, Scalar: AnsiString;
begin
  // CertDER a KeyDER načtené z disku nebo z HSM exportu
  // Nechávejte Out parametr ve vlastní proměnné: předání KeyDER jako
  // Const vstupu i Out výstupu by nejdřív vymazalo vstup
  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;

Ten komentář zaznamenává skutečný defekt z integrační práce. Když se tatáž proměnná AnsiString předávala jako Const vstup i Out výstup, kompilátor finalizoval proměnnou Out na vstupu do rutiny, vstup byl prázdný dřív, než ho rutina přečetla, a kontrola identity selhala na naprosto v pořádku klíči. Každé API, které bere klíč jako Const a vrací odvozený klíč jako Out, potřebuje na straně volajícího oddělené proměnné

FAQ

Potřebuje PDFlibPas Windows CNG nebo externí DLL pro SM2, SM3 a SM4?

Ne. PDFlibPas implementuje všechny tři algoritmy v Object Pascalu v PDFlibDigest, PDFlibSM4 a PDFlibSM2 a SM2 podpisy staví a ověřuje CMS kód knihovny. Windows CryptoAPI a CNG se na tvorbě SM2 podpisu nepodílejí

Lze použít SM2 se SHA-256 místo SM3?

Ne. Když sign proces drží SM2 identitu, CMS builder PDFlibPas vždy použije SM3, protože GB/T 32918.2 definuje SM2 podpisy přes SM3 digest, který zahrnuje hash identity ZA. Nastavení digestu na SHA-256 u SM2 sign procesu nezmění výsledek

Otevře Adobe Acrobat PDF zašifrovaný SM4 přes PDFlibPas?

Nevěřte v to. Crypt filtr /CFM /SM4 leží mimo hodnoty definované ISO 32000, takže hlavní proud čteček ho nepoznává. SM4 šifrování používejte pro uzavřené systémy, které čtou soubory s PDFlibPas nebo kompatibilní implementací, a AES-256 pro dokumenty určené k obecné distribuci

SM2 podepisování, SM3 hashing, SM4 šifrování a celou sadu PAdES podpisových API dodává PDFlibPas, Delphi PDF Library, která se kompiluje s Delphi, C++Builderem a FPC/Lazarus na Windows i Unix cílech