Teknisk artikel

SM2-, SM3- og SM4-PDF-signering og -kryptering i Delphi

PDFlibPas, PDF-biblioteket til Delphi og C++Builder, implementerer den kinesiske kommercielle kryptografisuite i ren Object Pascal: SM3-hashing (GB/T 32905), SM4-blokkryptering (GB/T 32907) og SM2-signaturer på elliptiske kurver (GB/T 32918). Med dem signerer den PDF-filer med SM2-with-SM3 CMS-signaturer via SetSignProcessSM2Identity og krypterer dokumenter med et SM4 crypt filter via Encrypt med strength 6, uden OS-provider eller ekstern DLL involveret

Den sædvanlige grund til at ville have dette er et udbudskrav, ikke nysgerrighed. Systemer solgt til kinesisk offentlig forvaltning, finans og sundhed forventes i stigende grad at bruge nationalalgoritmerne, og Windows CryptoAPI og CNG signerer ikke med SM2 og hasher ikke med SM3. Går signeringsstien gennem platformen, er svaret simpelthen nej. En ren Pascal-implementering giver samme svar på Win32, Win64 og FPC-målene, og den giver det uden en rørig native-afhængighed under dokumentpipelinen

Hvorfor bygge GB/T-suiten i ren Object Pascal?

PDFlibPas bygger SM2, SM3 og SM4 i Pascal, fordi ingen platform-provider, den allerede retter sig imod, kan klare opgaven. CryptoAPI og CNG har ingen SM3-signaturunderstøttelse, så før SM2-identiteten eksisterede, nægtede biblioteket bevidst at signere, når digesten var sat til SM3: alternativet var en SignerInfo, der erklærer én digest-OID og bærer en signatur over noget andet, hvilket er en lydløs forkert signatur frem for en synlig fejl

De tre algoritmer har også en naturlig byggeorden, og biblioteket fulgte den. SM3 kom først, fordi SM2 har brug for den til ZA-identitetshashen og meddelelsesdigesten. SM4 kom nummer to som en uafhængig unit. SM2 kom sidst, først som rå primitiver verificeret mod den publicerede eksempelkurve i specifikationstillægget, derefter som en signeringsidentitet koblet på den samme CMS-builder, der allerede håndterer Ed25519, Ed448 og ML-DSA. Samme tilgang er beskrevet til Ed448- og Brainpool-ECDSA-kurverne i ren Pascal og FIPS 204 ML-DSA post-quantum-signeren, og SM2 sættes ind i det framework som endnu en nøgletype

Primitiverne er offentlige funktioner, så du kan bruge dem uden for PDF-signering. SM3String og SM3ByteArray i PDFlibDigest returnerer den rå 32-byte digest (én byte pr. tegn, ikke hex), og PDFlibSM4 eksporterer ECB- og CBC-hjælpere i to varianter: blok-justerede versioner, der returnerer en tom streng ved forkert længde, og PKCS#7-versioner som SM4EncryptCBCPKCS7 til data af vilkårlig længde

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-eksempel: SM3("abc")
  Digest := SM3String('abc');
  Assert(LowerCase(RawToHex(Digest)) =
    '66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');

  // SM4 kræver en 16-byte-nøgle; CBC kræver en 16-byte-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;

Tæl de 16-byte-strenge i hånden på eget ansvar. To teststrenge i SM4-testsuiten, der skulle være 16 bytes, viste sig at være 17, og fordi de blok-justerede hjælpere efter design returnerer en tom streng ved forkert længde, viste fejlen sig som en forvirrende assertion frem for en exception

Signering af en PDF med en SM2-identitet

En PDFlibPas-signeringsproces bruger en SM2-nøgle, så snart SetSignProcessSM2Identity eller SetSignProcessSM2IdentityFromFile knytter et certifikat og en privat nøgle til den. Certifikatet er DER, og den private nøgle kan være en rå 32-byte-scalar, en ukrypteret PKCS#8-container eller en SEC 1 ECPrivateKey. At knytte en SM2-identitet rydder enhver Ed25519-, Ed448-, ML-DSA- eller Brainpool-identitet på samme proces og slår certifikatlager- og CNG-signering fra, så én signeringsproces aldrig bærer to konkurrerende nøgler

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;

Bemærk, hvad returværdien af SetSignProcessSM2IdentityFromFile fortæller dig, og hvad den ikke gør. En retur på 1 betyder, at begge filer blev læst og gemt på signeringsprocessen. Det rigtige tjek — at udlede den offentlige nøgle fra den private scalar og sammenligne den med certifikatet — kører, når signaturen anvendes, så et uparret nøglepar viser sig som en anden værdi end 1 fra GetSignProcessResult, ikke ved identitetskaldet

Kode 8 i SetSignProcessDigestAlgorithm vælger SM3, og med en SM2-identitet er den effektivt obligatorisk: CMS-builderen tvinger SM3 igennem for en SM2-nøgle uanset, hvad processen bad om, på samme måde som en Ed25519-nøgle tvinges over på SHA-512. Den omvendte kombination er også blokeret. Med adbe.pkcs7.detached eller ETSI.CAdES.detached afslutter en signeringsproces sat til SM3 uden en SM2-identitet uden at signere, fordi ingen anden signerer i biblioteket kan producere en SM3-baseret signatur, der matcher den erklærede OID

Hvad går ind i en SM2-with-SM3 SignerInfo?

En SM2-signatur i PDFlibPas beregnes over de DER-encodelagte signede attributter, standard-CMS-konstruktionen, men SM2 tilføjer et trin, ECDSA ikke har. GB/T 32918.2 hasher meddelelsen sammen med ZA, en identitetsdigest defineret som ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA), hvor hvert kurveled er en 32-byte big-endian-værdi, og ID er signerens distinguishing identifier. Signaturen er derefter r = (e + x1) mod n og s = ((1 + d)-1(k - r·d)) mod n, med en frisk nonce k fra bibliotekets CSPRNG og et retry ved degenererede runder

PDFlibPas bruger standard distinguishing identifier fra specifikationen, 16-tegns-strengen 1234567812345678, på både signerings- og verifikationssiden. Der findes ingen API til at ændre den. Det matcher det overvældende flertal af udrullede SM2-certifikater, men signerer en modpart med et custom-ID, afviger ZA-værdierne, og signaturen verificerer ikke, selvom alle andre bytes er korrekte

(r, s)-parret går ind i CMS'en som en DER SEQUENCE af to INTEGER-værdier, layoutet alle DSA-familie-SignerInfoer bruger. PDFlibPas skriver de algoritmeidentifikatorer, der er registreret i GM/T 0006: 1.2.156.10197.1.301 til SM2-nøglen, 1.2.156.10197.1.401 til SM3 og 1.2.156.10197.1.501 til SM2-with-SM3. RFC 3161-tidsstempelanmodninger oprettet med SM3 (SetTimeStampOptions digest 8) bærer samme SM3-OID. Releases før 3.589.1 skrev en uregistreret 1.2.156.101.1-bue; verifikationen accepterer den stadig, så dokumenter signeret med de builds forbliver gyldige i PDFlibPas, men tredjepartsvalidatorer genkender dem ikke, hvilket er en god grund til at signere alt, der forlader din egen pipeline, igen

SM2-with-SM3 SignerInfo-konstruktion i PDFlibPas til Delphi: digesten dækker ZA = SM3 af ENTL, ID og kurveparametrene med default-ID 1234567812345678, r og s følger GB/T 32918.2 med en frisk CSPRNG-nonce, og CMS'en bærer GM/T 0006-OID-værdierne 1.2.156.10197.1.301, 1.2.156.10197.1.401 og 1.2.156.10197.1.501
Signaturværdien er en DER SEQUENCE af to INTEGER-værdier over de signede attributter; builds før 3.589.1 skrev en uregistreret OID-bue, som tredjepartsvalidatorer afviser, så re-signér alt, der forlader din pipeline

Hvor SM2-verifikationen brød sammen: DER-sign bytes og en tabt carry

Størstedelen af ingeniørarbejdet i SM2-koden gik til to defekter, der producerede plausibelt output. Begge er typiske for håndskrevet big-integer-kryptografi, og begge er billige at undgå, når du først kender dem

Den første var en DER-sign-byte. ASN.1 INTEGER-værdier er signede, så encoderen sætter korrekt 0x00 foran enhver r eller s, hvis topbit er sat, hvilket giver en 33-byte-værdi. Verifikatoren konverterede hvert heltal til en fast 32-byte-bredde uden først at strippe den byte. Omkring halvdelen af alle r- og s-værdier har topbitten sat, så en stor andel af frisk signerede dokumenter fejlede verifikationen. Det første proberun trak tilfældigvis korte værdier og bestod; Delphi Win32-suiten, der signerer med tilfældige noncer, udstillede problemet med det samme. Verifikatoren sletter nu en foranstillet nul fra ethvert 33-byte-heltal, før det paddes til 32 bytes, og afviser alt længere

SM2-signaturverifikationsdefekt i PDFlibPas til Delphi og dens fix: ASN.1 INTEGER-encoding sætter 0x00 foran, når topbitten af r eller s er sat, den gamle verifikator paddede 33-byte-værdien til faste 32 bytes uden at strippe sign-byten, og den korrigerede verifikator sletter det foranstillede nul og afviser alt længere
Omkring halvdelen af alle r- og s-værdier bærer 0x00-præfikset, så plausibelt udseende output fejlede verifikationen i bulk; korte probeværdier bestod det første run, indtil Delphi Win32-suiten signede med tilfældige noncer

Den anden var en tabt carry i 256-bit addition. Fieldelementer er otte little-endian UInt32-limbs, og den modulære addition var skrevet som "addér, og træk p fra, hvis resultatet ikke er under p". For reducerede input er summen under 2p, men kan stadig overstige 2256, og når den gjorde det, foldede den tabte carry værdien tilbage ind i de lave 256 bits, hvor sammenligningen med p sagde, at alt var fint. At fordoble et punkt med en stor koordinat gik galt; små multipler gjorde ikke. Det synlige symptom var, at 1G gennem 8G matchede referencen, mens 9G, 10G og 11G ikke gjorde, og fixet blev fundet ved at sammenligne 1280 additionsinput og -output med uafhængigt beregnede værdier og bore ned i det første afvigende par

Den korrigerede addition i PDFlibSM2 beholder carryen returneret af den rå limb-addition og trækker p fra én gang, når carryen er sat, eller resultatet ikke er under p; en enkelt subtraktion er nok, fordi p overstiger 2255. Mønsteret gælder ethvert primfelt lige over en toerpotens. Kasserer din additionshjælper sin sidste carry, så test multipler langt forbi det punkt, hvor fordoblingen første gang krydser ordgrænsen, ikke kun 1G, 2G og 3G

Hvorfor SM3 var forkert to gange, før den var rigtig

Den første SM3-implementering fejlede hver eneste standardtestvektor, og det gjorde en uafhængig referenceimplementering, der blev skrevet sideløbende, også. To implementeringer, der er enige med hinanden og uenige med standarden, peger på en fælles misforståelse af specifikationen, ikke på en skrivefejl. En linje-for-linje-sammenligning af formlerne med den publicerede SM3-tekst fandt tre steder, hvor koden var skrevet ud fra hukommelsen

  • Meddelelsesekspansionen anvender 7-bit-rotationen på W[j-13]; W[j-6] er et rent XOR-led
  • SS1 kombinerer rotl(A, 12), E og den roterede rundekonstant med addition modulo 232, ikke XOR
  • For runderne 16 til 63 er FF majoritetsfunktionen (X AND Y) OR (X AND Z) OR (Y AND Z); SHA-agtig choice-funktion hører hjemme i GG

Efter de tre rettelser matchede den tomme streng, abc og 64-byte-tvobloksvektoren alle. SM4 gik så ind på første forsøg, fordi S-boksen og testvektorerne var taget fra specifikationsteksten, før nogen kode blev skrevet: rundenøglen rk0 = F12186F9, enkeltblokvektoren 681EDF34D206965E86B3E94F536E4246 og millioniterationsvektoren 595298C7C6FD271F0402F804C33D3F66. SM2-arbejdet tilføjede en relateret regel: lange hex-konstanter genereres ved gruppering og længdetjekkes, aldrig tastes i hånden, efter en håndkopieret basispunktkoordinat med ét manglende ciffer sendte en debug-session ud på jagt efter implementeringsfejl, der ikke eksisterede

Kryptering af en PDF med SM4 crypt filter

TPDFlib.Encrypt med strength 6 krypterer det valgte dokument med SM4-128 gennem crypt filter-ramværket i ISO 32000-1 §7.6.5. Encryption-dictionaryen bruger standard security handler med /V 4, /R 4 og /Length 128, og /StdCF-crypt filteret bærer /CFM /SM4. At gemme med strength 6 hæver filen til mindst PDF 1.6, GetEncryptionLevel rapporterer 7 for sådan en fil, og public-key-kryptering får samme mulighed gennem EncryptPublicKey med 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');
    // print, kopi og formularudfyldning tilladt; ingen redigeringer eller assembly
    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;

Hver streng og stream krypteres som en 16-byte tilfældig IV efterfulgt af SM4-CBC-ciphertext med PKCS#7-padding, samme layout som AESV2 bruger. Nøglen pr. objekt følger Algorithm 1 i ISO 32000-1 §7.6.2: filnøglen, tre lave bytes af objektnummeret og to bytes af generationsnummeret, plus sAlT-suffikset, hashet med MD5 og afkortet til 16 bytes. Password-til-nøgle-afledningen er revision 4-versionen, så strength 6 arver dens begrænsninger: adgangskoder SASLprep-processeres ikke (kun AES-256-strengths 3, 4 og 5 gør), og nøgleafledningen er ikke stærkere end AESV2's

SM4 crypt filter-struktur skrevet af PDFlibPas Encrypt med strength 6 i Delphi: /V 4, /R 4 og /Length 128 med /StdCF-filteret sat til /CFM /SM4, en MD5-nøgle pr. objekt over filnøglen, objekt- og generationsnumre plus sAlT afkortet til 16 bytes, og hver stream gemt som en 16-byte IV efterfulgt af SM4-CBC-ciphertext med PKCS#7-padding
Strenge og streams bruger AESV2-byte-layoutet med en SM4-kerne, men /SM4 ligger uden for de /CFM-værdier, ISO 32000 definerer, så mainstream-læsere kan ikke dekryptere, og generel distribution bør blive på AES-256

Interop-grænser, du bør kende, før du udruller

Den ærlige opsummering er, at SM2-signering interopererer inden for grænserne af de ovenfor beskrevne OID-værdier og standard-ID, mens SM4-kryptering kun interopererer med software, der kender samme konvention. /SM4 er ikke én af de /CFM-værdier, der er defineret i ISO 32000-1 Tabel 25 eller i ISO 32000-2, som definerer None, V2, AESV2 og AESV3. En mainstream PDF-læser, der møder /CFM /SM4, dekrypterer ikke filen. PDFlibPas læser selv /SM4 og varianten /SM4V1 gennem sin security handler, så en lukket pipeline, hvor PDFlibPas skriver og læser, fungerer; en fil til den brede offentlighed bør bruge AES-256 i stedet

For signaturer er grænsen certifikatlayoutet og validatoren på den anden side. PLValidateSM2Identity, tjekket bag signeringsprocessen, accepterer GM/T 0015-layoutet, hvor SubjectPublicKeyInfo bærer id-ecPublicKey med SM2-kurven 1.2.156.10197.1.301 som parameter, samt certifikater, der lægger SM2-OID-værdien i algoritmefeltet selv, og den afviser alt, hvis udledte offentlige punkt ikke matcher den private scalar. Før du støtter dig til SM2-signaturer i produktion, så signér et dokument med et certifikat fra den CA, du reelt kommer til at bruge, og verificér det med validatoren, din modpart kører. Du kan selv køre identitetstjekket, før du starter en signeringsproces:

uses
  SysUtils, PDFlibModernSign;

var
  CertDER, KeyDER, Scalar: AnsiString;
begin
  // CertDER og KeyDER indlæst fra disk eller en HSM-eksport
  // Hold Out-parameteren i sin egen variabel: at give KeyDER med som både
  // Const-input og Out-output ville rydde inputtet først
  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;

Den kommentar registrerer en reel defekt fra integrationsarbejdet. Da samme AnsiString-variabel blev givet med som Const-input og Out-output, finaliserede compileren Out-variablen ved indgang, inputtet blev tomt, før rutinen læste det, og identitetstjekket fejlede på en fuldt ud gyldig nøgle. Enhver API, der tager en nøgle som Const og returnerer en udledt nøgle som Out, kræver separate variable hos kalderen

FAQ

Har PDFlibPas brug for Windows CNG eller en ekstern DLL til SM2, SM3 og SM4?

Nej. PDFlibPas implementerer alle tre algoritmer i Object Pascal i PDFlibDigest, PDFlibSM4 og PDFlibSM2, og SM2-signaturer bygges og verificeres af bibliotekets CMS-kode. Windows CryptoAPI og CNG er ikke involveret i at producere SM2-signaturen

Kan jeg bruge SM2 med SHA-256 i stedet for SM3?

Nej. Når en signeringsproces holder en SM2-identitet, bruger PDFlibPas' CMS-builder altid SM3, fordi GB/T 32918.2 definerer SM2-signaturer over en SM3-digest, der inkluderer ZA-identitetshashen. At sætte digesten til SHA-256 på en SM2-signeringsproces ændrer ikke resultatet

Vil Adobe Acrobat åbne en af PDFlibPas SM4-krypteret PDF?

Forvent det ikke. /CFM /SM4-crypt filteret ligger uden for de værdier, ISO 32000 definerer, så mainstream-læsere genkender det ikke. Brug SM4-kryptering til lukkede systemer, der læser filer med PDFlibPas eller en kompatibel implementering, og AES-256 til dokumenter beregnet til generel distribution

SM2-signering, SM3-hashing, SM4-kryptering og hele sættet af PAdES-signerings-API'er følger med PDFlibPas, Delphi PDF Library, som kompilerer med Delphi, C++Builder og FPC/Lazarus på Windows- og Unix-mål