PDFlibPas, PDF-biblioteket för Delphi och C++Builder, implementerar den kinesiska kommersiella kryptosviten i ren Object Pascal: SM3-hashning (GB/T 32905), SM4-blockkryptering (GB/T 32907) och SM2-elliptiska kurvsignaturer (GB/T 32918). Med dem signerar den PDF-filer med SM2-med-SM3-CMS-signaturer via SetSignProcessSM2Identity och krypterar dokument med ett SM4 crypt filter via Encrypt med styrka 6, utan att någon OS-provider eller extern DLL är inblandad
Det vanliga skälet att vilja det här är ett upphandlingskrav, inte nyfikenhet. System som säljs in i kinesisk stat, finans och vård förväntas allt oftare använda nationalalgoritmerna, och Windows CryptoAPI och CNG signerar inte med SM2 eller hashar inte med SM3. Går signeringsvägen via plattformen är svaret helt enkelt nej. En ren Pascal-implementation ger samma svar på Win32, Win64 och FPC-målen, och den ger det utan ett rörligt nativt beroende under dokumentpipelinen
Varför bygga GB/T-sviten i ren Object Pascal?
PDFlibPas bygger SM2, SM3 och SM4 i Pascal eftersom ingen plattformsprovider den redan riktar sig mot kan göra jobbet. CryptoAPI och CNG saknar SM3-signaturstöd, så innan SM2-identiteten fanns vägrade biblioteket medvetet att signera när digesten sattes till SM3: alternativet var en SignerInfo som deklarerar en digest-OID men bär en signatur över något annat, vilket är en tyst felaktig signatur snarare än ett synligt fel
De tre algoritmerna har också en naturlig byggordning, och biblioteket följde den. SM3 kom först, eftersom SM2 behöver den för ZA-identitetshashen och för meddelandedigesten. SM4 kom tvåa som en oberoende unit. SM2 kom sist, först som råa primitiver verifierade mot den publicerade exempelkurvan i specifikationens appendix, sedan som signeringsidentitet kopplad in i samma CMS-byggare som redan hanterar Ed25519, Ed448 och ML-DSA. Samma tillvägagångssätt beskrivs för Ed448- och Brainpool-ECDSA-kurvor i ren Pascal och FIPS 204 ML-DSA post-quantum-signeraren, och SM2 pluggar in i det ramverket som ytterligare en nyckeltyp
Primitiverna är publika funktioner, så du kan använda dem utanför PDF-signeringen. SM3String och SM3ByteArray i PDFlibDigest returnerar den råa 32-byte-digesten (en byte per tecken, inte hex), och PDFlibSM4 exporterar ECB- och CBC-hjälpfunktioner i två smaker: blockjusterade varianter som returnerar en tom sträng vid fel längd, och PKCS#7-varianter som SM4EncryptCBCPKCS7 för data med godtycklig längd
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-exempel: SM3("abc")
Digest := SM3String('abc');
Assert(LowerCase(RawToHex(Digest)) =
'66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');
// SM4 behöver en 16-byte-nyckel; CBC behö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;
Räkna de där 16-bytesträngarna för hand på egen risk. Två teststrängar i SM4-testsviten som skulle vara 16 byte visade sig vara 17, och eftersom de blockjusterade hjälpfunktionerna medvetet returnerar en tom sträng vid fel längd dök felet upp som en förvirrande assertion i stället för ett undantag
Att signera en PDF med en SM2-identitet
En PDFlibPas-signeringsprocess använder en SM2-nyckel så snart SetSignProcessSM2Identity eller SetSignProcessSM2IdentityFromFile fäster ett certifikat och en privat nyckel vid den. Certifikatet är DER, och den privata nyckeln kan vara en rå 32-byte-skalar, en okrypterad PKCS#8-container eller en SEC 1 ECPrivateKey. Att fästa en SM2-identitet rensar varje Ed25519-, Ed448-, ML-DSA- eller Brainpool-identitet på samma process och stänger av certifikatbutiks- och CNG-signering, så en signeringsprocess bär aldrig två konkurrerande nycklar
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;
Notera vad returvärdet från SetSignProcessSM2IdentityFromFile säger och inte säger. Retur 1 betyder att båda filerna lästs och lagrats på signeringsprocessen. Den riktiga kontrollen, att härleda den publika nyckeln från den privata skalaren och jämföra med certifikatet, körs när signaturen appliceras, så ett felmatchat nyckelpar syns som ett icke-1 GetSignProcessResult, inte vid identitetsanropet
Kod 8 i SetSignProcessDigestAlgorithm väljer SM3, och med en SM2-identitet är det i praktiken obligatoriskt: CMS-byggaren tvingar SM3 för en SM2-nyckel oavsett vad processen bad om, på samma sätt som en Ed25519-nyckel tvingas till SHA-512. Den omvända kombinationen blockeras också. Med adbe.pkcs7.detached eller ETSI.CAdES.detached avslutas en signeringsprocess satt till SM3 som inte håller en SM2-identitet utan att signera, eftersom ingen annan signerare i biblioteket kan producera en SM3-baserad signatur som matchar den deklarerade OID:en
Vad går in i en SM2-med-SM3-SignerInfo?
En SM2-signatur i PDFlibPas beräknas över de DER-kodade signerade attributen, den vanliga CMS-konstruktionen, men SM2 lägger till ett steg som ECDSA inte har. GB/T 32918.2 hashar meddelandet tillsammans med ZA, en identitetsdigest definierad som ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA), där varje kurvdel är ett 32-byte big-endian-värde och ID är signerarens skiljeidentifikator. Signaturen blir sedan r = (e + x1) mod n och s = ((1 + d)-1(k - r·d)) mod n, med ett färskt nonce k från bibliotekets CSPRNG och ett nytt försök vid degenererade varv
PDFlibPas använder standardskiljeidentifikatorn från specifikationen, 16-teckenssträngen 1234567812345678, på både signerings- och verifieringssidan. Det finns inget API att ändra den. Det matchar den överväldigande majoriteten av utplacerade SM2-certifikat, men om en motpart signerar med ett anpassat ID skiljer ZA-värdena sig och signaturen verifieras inte, fast alla andra byte är korrekta
(r, s)-paret går in i CMS som en DER SEQUENCE av två INTEGER-värden, layouten som varje SignerInfo i DSA-familjen använder. PDFlibPas skriver algoritmidentifierarna registrerade i GM/T 0006: 1.2.156.10197.1.301 för SM2-nyckeln, 1.2.156.10197.1.401 för SM3 och 1.2.156.10197.1.501 för SM2-med-SM3. RFC 3161-tidsstämpelansökningar skapade med SM3 (SetTimeStampOptions digest 8) bär samma SM3-OID. Releases före 3.589.1 skrev en oregistrerad 1.2.156.101.1-båge; verifieringen accepterar den fortfarande, så dokument signerade med de byggen fortsätter validera i PDFlibPas, men tredjepartsvalidatorer känner inte igen dem, vilket är en god anledning att signera om allt som lämnar din egen pipeline
Där SM2-verifieringen gick sönder: DER-signeringsbyte och en tappad carry
Största delen av ingenjörsarbetet i SM2-koden gick åt till två defekter som producerade trovärdig utdata. Båda är typiska för handskriven big-integer-kryptografi, och båda är billiga att undvika när man väl känner till dem
Den första var en DER-signeringsbyte. ASN.1 INTEGER-värden är teckenhavande, så kodaren fogar korrekt 0x00 framför varje r eller s vars översta bit är satt, vilket ger ett 33-byte-värde. Verifieraren konverterade varje heltal till en fast bredd på 32 byte utan att först stryka den byten. Omkring hälften av alla r- och s-värden har översta biten satt, så en stor andel av nyss signerade dokument misslyckades i verifieringen. Det första probloppet råkade dra korta värden och gick igenom; Delphi Win32-sviten, signerande med slumpmässiga nonce, blottade problemet omedelbart. Verifieraren raderar nu en inledande nolla från varje 33-byte-heltal innan utfyllnad till 32 byte, och avvisar allt längre
Den andra var en tappad carry i 256-bitars addition. Fältelement är åtta little-endian UInt32-limbs, och den modulära additionen var skriven som "addera, subtrahera sedan p om resultatet inte är under p". För reducerade indata är summan under 2p men kan fortfarande överstiga 2256, och när den gjorde det fällde den tappade carryn tillbaka värdet in i de låga 256 bitarna, där jämförelsen mot p sade att allt var bra. Att dubblera en punkt med en stor koordinat gick fel; små multipler gjorde inte det. Det synliga symptomet var att 1G till och med 8G matchade referensen medan 9G, 10G och 11G inte gjorde det, och fixen hittades genom att jämföra 1280 additionsindata och utdata mot oberoende beräknade värden och borra ner i det första divergerande paret
Den korrigerade additionen i PDFlibSM2 behåller carryn som rå limb-additionen returnerar och subtraherar p en gång när carryn är satt eller resultatet inte är under p; en enda subtraktion räcker eftersom p överstiger 2255. Mönstret gäller varje primkropp precis ovanför en potens av två. Om din additionshjälpare slänger sin sista carry, testa multipler långt förbi punkten där dubbleringen först korsar ordgränsen, inte bara 1G, 2G och 3G
Varför SM3 var fel två gånger innan den blev rätt
Den första SM3-implementationen misslyckades med varje standardtestvektor, och samma sak gjorde en oberoende referensimplementation skriven vid sidan om. Två implementationer som håller med varandra men tvistar med standarden pekar på ett gemensamt missförstånd av specifikationen, inte på ett avskrivningsfel. Att jämföra formlerna rad för rad mot den publicerade SM3-texten fann tre ställen där koden skrivits ur minnet
- Meddelandeexpansionen tillämpar 7-bitsrotationen på W[j-13]; W[j-6] är en ren XOR-term
- SS1 kombinerar rotl(A, 12), E och den roterade rondkonstanten med addition modulo 232, inte XOR
- För varv 16 till 63 är FF majoritetsfunktionen (X AND Y) OR (X AND Z) OR (Y AND Z); urvalsfunktionen i SHA-stil tillhör GG
Efter de tre korrigeringarna matchade tomma strängen, abc och den 64-byte tvåblocksvektorn. SM4 gick sedan in på första försöket, eftersom S-boxen och testvektorerna hämtats från specifikationstexten innan någon kod skrevs: rundnyckeln rk0 = F12186F9, enblocksvektorn 681EDF34D206965E86B3E94F536E4246, och enmiljoniterationsvektorn 595298C7C6FD271F0402F804C33D3F66. SM2-arbetet lade till en relaterad regel: långa hexkonstanter genereras genom gruppering och längdkontrolleras, aldrig skrivna för hand, sedan en handkopierad baspunktskoordinat som saknade en siffra skickade en felsökningssession efter implementationsbuggar som inte fanns
Att kryptera en PDF med SM4 crypt filtret
TPDFlib.Encrypt med styrka 6 krypterar det valda dokumentet med SM4-128 via crypt filter-ramverket i ISO 32000-1 §7.6.5. Krypteringsordboken använder standardsecurityhandlern med /V 4, /R 4 och /Length 128, och crypt filtret /StdCF bär /CFM /SM4. Att spara med styrka 6 lyfter filen till minst PDF 1.6, GetEncryptionLevel rapporterar 7 för en sådan fil, och public-key-kryptering får samma alternativ via EncryptPublicKey med styrka 4
var
Pdf: TPDFlib;
Perm: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report.pdf', '') <> 1 then
raise Exception.Create('cannot load PDF');
// utskrift, kopiering och formulärfyllning tillåtna; inga redigeringar eller sammanställning
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;
Varje sträng och ström krypteras som en slumpmässig 16-byte IV följt av SM4-CBC-chiffertext med PKCS#7-utfyllnad, samma layout som AESV2 använder. Nyckeln per objekt följer Algoritm 1 i ISO 32000-1 §7.6.2: filnyckeln, tre låga byte av objektnumret och två byte av generationsnumret, plus sAlT-suffixet, hashad med MD5 och avkortad till 16 byte. Password-till-nyckel-härledningen är revision 4, så styrka 6 ärver dess gränser: lösenord bearbetas inte med SASLprep (bara AES-256-styrkorna 3, 4 och 5 gör det), och nyckelhärledningen är inte starkare än AESV2:s
Interop-gränser du bör känna till före utplacering
Den ärliga sammanfattningen är att SM2-signering samverkar inom gränserna för OID:erna och standard-ID:t ovan, medan SM4-kryptering bara samverkar med mjukvara som känner samma konvention. /SM4 är inte ett av de /CFM-värden som definieras i ISO 32000-1 tabell 25 eller i ISO 32000-2, som definierar None, V2, AESV2 och AESV3. En vanlig PDF-läsare som möter /CFM /SM4 dekrypterar inte filen. PDFlibPas själv läser /SM4 och varianten /SM4V1 via sin security handler, så ett slutet system där PDFlibPas skriver och läser fungerar; en fil för allmänheten bör använda AES-256 i stället
För signaturer är gränsen certifikatlayouten och validatorn på andra sidan. PLValidateSM2Identity, kontrollen bakom signeringsprocessen, accepterar GM/T 0015-layouten, där SubjectPublicKeyInfo bär id-ecPublicKey med SM2-kurvan 1.2.156.10197.1.301 som parameter, liksom certifikat som lägger SM2-OID:en i algoritmfältet själv, och den avvisar allt vars härledda publika punkt inte matchar den privata skalaren. Innan du förlitar dig på SM2-signaturer i produktion, signera ett dokument med ett certifikat från den CA du faktiskt ska använda och verifiera det med validatorn din motpart kör. Du kan köra identitetskontrollen själv innan en signeringsprocess startas:
uses
SysUtils, PDFlibModernSign;
var
CertDER, KeyDER, Scalar: AnsiString;
begin
// CertDER och KeyDER laddade från disk eller en HSM-export
// Håll Out-parametern i sin egen variabel: att skicka KeyDER som både
// Const-indata och Out-utdata skulle rensa indata 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 kommentaren registrerar en verklig defekt från integrationsarbetet. När samma AnsiString-variabel skickades som Const-indata och Out-utdata finaliserade kompilatorn Out-variabeln vid ingången, indata blev tomt innan rutinen hann läsa det, och identitetskontrollen misslyckades på en fullt bra nyckel. Varje API som tar en nyckel som Const och returnerar en härledd nyckel som Out behöver separata variabler på anroparsidan
FAQ
Behöver PDFlibPas Windows CNG eller en extern DLL för SM2, SM3 och SM4?
Nej. PDFlibPas implementerar alla tre algoritmerna i Object Pascal i PDFlibDigest, PDFlibSM4 och PDFlibSM2, och SM2-signaturer byggs och verifieras av bibliotekets CMS-kod. Windows CryptoAPI och CNG är inte inblandade i att producera SM2-signaturen
Kan jag använda SM2 med SHA-256 i stället för SM3?
Nej. När en signeringsprocess håller en SM2-identitet använder PDFlibPas CMS-byggare alltid SM3, eftersom GB/T 32918.2 definierar SM2-signaturer över en SM3-digest som innehåller ZA-identitetshashen. Att sätta digesten till SHA-256 på en SM2-signeringsprocess ändrar inte resultatet
Öppnar Adobe Acrobat en PDF krypterad med SM4 av PDFlibPas?
Räkna inte med det. Crypt filtret /CFM /SM4 ligger utanför de värden ISO 32000 definierar, så vanliga läsare känner inte igen det. Använd SM4-kryptering för slutna system som läser filer med PDFlibPas eller en kompatibel implementation, och AES-256 för dokument avsedda för allmän distribution
SM2-signering, SM3-hashning, SM4-kryptering och hela uppsättningen PAdES-signerings-API:er ingår i PDFlibPas, Delphi PDF Library, som kompilerar med Delphi, C++Builder och FPC/Lazarus på Windows- och Unix-mål