PDFlibPas, de PDF-library voor Delphi en C++Builder, implementeert de Chinese commerciële cryptografiesuite in pure Object Pascal: SM3-hashing (GB/T 32905), SM4-blokversleuteling (GB/T 32907) en SM2-handtekeningen over elliptische krommen (GB/T 32918). Daarmee ondertekent hij PDFs met SM2-with-SM3 CMS-handtekeningen via SetSignProcessSM2Identity en versleutelt hij documenten met een SM4-crypt filter via Encrypt met strength 6, zonder OS-provider of externe DLL
De gebruikelijke reden om dit te willen is een inkoopeis, geen nieuwsgierigheid. Van systemen die aan de Chinese overheid, het financiële wezen of de gezondheidszorg worden verkocht wordt steeds vaker verwacht dat ze de nationale algoritmen gebruiken, en Windows CryptoAPI en CNG ondertekenen niet met SM2 of hashen niet met SM3. Loopt het ondertekenpad via het platform, dan is het antwoord simpelweg nee. Een pure Pascal-implementatie geeft op Win32, Win64 en de FPC-doelen hetzelfde antwoord, en geeft het zonder een meebewegende native dependency onder de documentpipeline
Waarom de GB/T-suite in pure Object Pascal bouwen?
PDFlibPas bouwt SM2, SM3 en SM4 in Pascal omdat geen enkele platformprovider die hij al ondersteunt de klus kan klaren. CryptoAPI en CNG hebben geen SM3-handtekensteun, dus vóór de SM2-identiteit bestond weigerde de library bewust te ondertekenen wanneer de digest op SM3 stond: het alternatief was een SignerInfo die één digest-OID verkondigt en een handtekening ergens anders over draagt, een stille verkeerde handtekening in plaats van een zichtbare fout
De drie algoritmen hebben ook een natuurlijke bouwvolgorde, en de library heeft die gevolgd. SM3 kwam eerst, want SM2 heeft hem nodig voor de ZA-identiteitshash en voor de message digest. SM4 kwam als tweede als een onafhankelijke unit. SM2 kwam laatst, eerst als rauwe primitieven geverifieerd tegen de gepubliceerde voorbeeldkromme in de bijlage van de specificatie, daarna als een ondertekenidentiteit bedraad in dezelfde CMS-builder die al Ed25519, Ed448 en ML-DSA aankan. Dezelfde aanpak wordt beschreven voor de Ed448- en Brainpool-ECDSA-krommen in pure Pascal en de FIPS 204 ML-DSA post-quantum signer, en SM2 plugt in dat kader als nog een sleuteltype
De primitieven zijn publieke functies, dus u kunt ze buiten PDF-ondertekening gebruiken. SM3String en SM3ByteArray in PDFlibDigest geven de rauwe digest van 32 bytes terug (één byte per teken, geen hex), en PDFlibSM4 exporteert ECB- en CBC-helpers in twee smaken: block-aligned versies die bij een verkeerde lengte een lege string teruggeven, en PKCS#7-versies zoals SM4EncryptCBCPKCS7 voor data van willekeurige lengte
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-voorbeeld: SM3("abc")
Digest := SM3String('abc');
Assert(LowerCase(RawToHex(Digest)) =
'66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');
// SM4 heeft een sleutel van 16 bytes nodig; CBC een IV van 16 bytes
Key := 'SixteenByteKey!!';
IV := 'InitVector16byte';
Cipher := SM4EncryptCBCPKCS7(Key, IV, 'Invoice 2026-0042 approved');
Plain := SM4DecryptCBCPKCS7(Key, IV, Cipher);
Assert(Plain = 'Invoice 2026-0042 approved');
end;
Tel die strings van 16 bytes met de hand op eigen risico na. Twee teststrings in de SM4-testsuite die 16 bytes moesten zijn bleken er 17 te hebben, en omdat de block-aligned helpers bij een verkeerde lengte vanontwerpswege een lege string teruggeven, kwam de misser naar boven als een verwarrende assertion in plaats van als een exception
Een PDF ondertekenen met een SM2-identiteit
Een PDFlibPas-ondertekenproces gebruikt een SM2-sleutel zodra SetSignProcessSM2Identity of SetSignProcessSM2IdentityFromFile er een certificaat en een privésleutel aan koppelt. Het certificaat is DER, en de privésleutel kan een rauwe scalar van 32 bytes, een ongeëncrypteerde PKCS#8-container of een SEC 1 ECPrivateKey zijn. Een SM2-identiteit koppelen wist elke Ed25519-, Ed448-, ML-DSA- of Brainpool-identiteit op hetzelfde proces en zet certificate store- en CNG-ondertekening uit, dus één ondertekenproces draagt nooit twee concurrerende sleutels
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;
Let op wat de retourwaarde van SetSignProcessSM2IdentityFromFile wel en niet vertelt. Een retour van 1 betekent dat beide bestanden zijn gelezen en op het ondertekenproces zijn opgeslagen. De echte controle, het afleiden van de publieke sleutel uit de private scalar en het vergelijken met het certificaat, draait pas wanneer de handtekening wordt aangebracht, dus een niet-matchend sleutelpaar komt naar boven als een GetSignProcessResult ongelijk 1, niet bij de identiteitsaanroep
Code 8 in SetSignProcessDigestAlgorithm kiest SM3, en met een SM2-identiteit is dat feitelijk verplicht: de CMS-builder dwingt SM3 af voor een SM2-sleutel, wat het proces ook vraagt, net zoals een Ed25519-sleutel op SHA-512 wordt gedwongen. De omgekeerde combinatie wordt ook geblokkeerd. Met adbe.pkcs7.detached of ETSI.CAdES.detached eindigt een op SM3 gezet ondertekenproces zonder een SM2-identiteit zonder te ondertekenen, want geen enkele andere ondertekenaar in de library kan een op SM3 gebaseerde handtekening produceren die bij de verkondigde OID past
Wat gaat er in een SM2-with-SM3 SignerInfo?
Een SM2-handtekening in PDFlibPas wordt berekend over de DER-gecodeerde signed attributes, de standaard-CMS-opzet, maar SM2 voegt een stap toe die ECDSA niet heeft. GB/T 32918.2 hasht de boodschap samen met ZA, een identiteitsdigest gedefinieerd als ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA), waarbij elk krommedeel een 32-byte big-endian waarde is en ID de distinguishing identifier van de ondertekenaar is. De handtekening is dan r = (e + x1) mod n en s = ((1 + d)-1(k - r·d)) mod n, met een verse nonce k uit de CSPRNG van de library en een herkansing bij degenerieke rondes
PDFlibPas gebruikt de default distinguishing identifier uit de specificatie, de string 1234567812345678 van 16 tekens, zowel bij het ondertekenen als bij het verifiëren. Er is geen API om hem te veranderen. Dat matcht de overgrote meerderheid van de ingezette SM2-certificaten, maar tekent een tegenpartij met een custom ID, dan verschillen de ZA-waarden en verifieert de handtekening niet, ook al is elke andere byte correct
Het paar (r, s) gaat de CMS in als een DER-SEQUENCE van twee INTEGER-waarden, de opzet die elke SignerInfo uit de DSA-familie gebruikt. PDFlibPas schrijft de in GM/T 0006 geregistreerde algoritme-identificatoren: 1.2.156.10197.1.301 voor de SM2-sleutel, 1.2.156.10197.1.401 voor SM3 en 1.2.156.10197.1.501 voor SM2-with-SM3. RFC 3161-timestampverzoeken aangemaakt met SM3 (SetTimeStampOptions digest 8) dragen dezelfde SM3-OID. Releases vóór 3.589.1 schreven een niet-geregistreerde 1.2.156.101.1-arc; verificatie accepteert hem nog steeds, dus documenten die met die builds zijn ondertekend blijven valideren in PDFlibPas, maar validators van derden herkennen ze niet, een goede reden om alles wat uw eigen pipeline verlaat opnieuw te ondertekenen
Waar SM2-verificatie brak: DER sign bytes en een verloren carry
Het grootste deel van de engineering in de SM2-code ging op aan twee defecten die geloofwaardige output opleverden. Beide zijn typisch voor handgeschreven big-integer-cryptografie, en beide zijn goedkoop te vermijden zodra u ze kent
De eerste was een DER sign byte. ASN.1-INTEGER-waarden zijn signed, dus de encoder zet correct 0x00 vóór elke r of s waarvan de topbit staat, wat een waarde van 33 bytes oplevert. De verifier zette elk integer om naar een vaste breedte van 32 bytes zonder die byte er eerst af te halen. Ongeveer de helft van alle r- en s-waarden heeft de topbit staan, dus een flink deel van vers ondertekende documenten viel door de verificatie. De eerste probe-run trok toevallig korte waarden en slaagde; de Delphi Win32-suite, ondertekenend met willekeurige nonces, legde het probleem direct bloot. De verifier wist nu een voorloopnul van elke 33-byte integer vóór het opvullen tot 32 bytes en wijst alles wat langer is af
De tweede was een verloren carry bij 256-bit optelling. Veldelementen bestaan uit acht little-endian UInt32-limbs, en de modulaire optelling was geschreven als "optellen, dan p aftrekken als het resultaat niet onder p zit". Voor gereduceerde invoer zit de som onder 2p maar kan hij toch boven 2256 uitkomen, en als dat gebeurde, vouwde de gedropte carry de waarde terug in de lage 256 bits, waar de vergelijking met p zei dat alles prima was. Het verdubbelen van een punt met een grote coördinaat ging mis; kleine veelvouden niet. Het zichtbare symptoom was dat 1G tot en met 8G de referentie matchten terwijl 9G, 10G en 11G dat niet deden, en de fix werd gevonden door 1280 optellingen van invoer en uitvoer te vergelijken met onafhankelijk berekende waarden en in het eerste afwijkende paar te duiken
De gecorrigeerde optelling in PDFlibSM2 houdt de carry vast die de rauwe limb-optelling teruggeeft en trekt p één keer af wanneer de carry staat of het resultaat niet onder p zit; één aftrekking is genoeg, want p overstijgt 2255. Het patroon geldt voor elk priemveld net boven een macht van twee. Gooit uw optellingshelper de laatste carry weg, test dan veelvouden ruim voorbij het punt waar verdubbelen voor het eerst de woordgrens oversteekt, niet alleen 1G, 2G en 3G
Waarom SM3 twee keer fout was voordat hij goed was
De eerste SM3-implementatie zakte voor elke standaard testvector, en een onafhankelijke referentie-implementatie die er naast was geschreven ook. Twee implementaties die het met elkaar eens zijn en oneens met de standaard wijzen op een gedeeld misverstand over de specificatie, niet op een overschrijffout. De formules regel voor regel naast de gepubliceerde SM3-tekst leggen leverde drie plekken op waar de code uit het hoofd was geschreven
- Berichtexpansie past de 7-bit rotatie toe op W[j-13]; W[j-6] is een kale XOR-term
- SS1 combineert rotl(A, 12), E en de geroteerde rondeconstante met modulo 232 optelling, niet XOR
- Voor rondes 16 tot 63 is FF de majority-functie (X AND Y) OR (X AND Z) OR (Y AND Z); de choice-functie in SHA-stijl hoort bij GG
Na die drie correcties matchten de lege string, abc en de 64-byte vector van twee blokken allemaal. SM4 ging er daarna in één keer in, want de S-box en de testvectors waren uit de specifikatietekst gehaald voordat er ook maar code was geschreven: rondsleutel rk0 = F12186F9, de single-block vector 681EDF34D206965E86B3E94F536E4246 en de vector met één miljoen iteraties 595298C7C6FD271F0402F804C33D3F66. Het SM2-werk voegde een verwante regel toe: lange hex-constanten worden gegroepeerd gegenereerd en op lengte gecontroleerd, nooit met de hand getypt, nadat een met de hand gekopieerde coördinaat van het basispunt met één cijfer te weinig een debugsessie achter implementatiebugs aan stuurde die niet bestonden
Een PDF versleutelen met het SM4-crypt filter
TPDFlib.Encrypt met strength 6 versleutelt het geselecteerde document met SM4-128 via het crypt filter-kader van ISO 32000-1 §7.6.5. De encryption dictionary gebruikt de standaard security handler met /V 4, /R 4 en /Length 128, en het /StdCF-crypt filter draagt /CFM /SM4. Opslaan met strength 6 tilt het bestand naar ten minste PDF 1.6, GetEncryptionLevel meldt 7 voor zo'n bestand, en public-key-versleuteling krijgt dezelfde optie via EncryptPublicKey met 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');
// printen, kopiëren en formulieren invullen toegestaan; geen bewerkingen of samenstellen
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;
Elke string en stream wordt versleuteld als een willekeurige IV van 16 bytes gevolgd door SM4-CBC-ciphertext met PKCS#7-padding, dezelfde opzet die AESV2 gebruikt. De sleutel per object volgt Algorithm 1 van ISO 32000-1 §7.6.2: de bestandssleutel, drie lage bytes van het objectnummer en twee bytes van het generation number, plus het sAlT-achtervoegsel, gehasht met MD5 en ingekort tot 16 bytes. De password-to-key-afleiding is die van revisie 4, dus strength 6 erft de grenzen ervan: wachtwoorden worden niet SASLprep-bewerkt (alleen de AES-256-strengths 3, 4 en 5 wel), en de sleutelafleiding is niet sterker dan die van AESV2
Interoperabiliteitsgrenzen die u moet kennen voordat u uitrolt
De eerlijke samenvatting is dat SM2-ondertekening binnen de grenzen van de hierboven beschreven OIDs en de default ID interoperabel is, terwijl SM4-versleuteling alleen interoperabel is met software die dezelfde conventie kent. /SM4 is geen van de /CFM-waarden gedefinieerd in ISO 32000-1 Table 25 of in ISO 32000-2, die None, V2, AESV2 en AESV3 definiëren. Een mainstream PDF-reader die /CFM /SM4 tegenkomt zal het bestand niet ontsleutelen. PDFlibPas zelf leest /SM4 en de variant /SM4V1 via zijn security handler, dus een gesloten pipeline waarin PDFlibPas schrijft en leest werkt; een bestand voor het grote publiek gebruikt beter AES-256
Bij handtekeningen ligt de grens bij de certificaatopzet en de validator aan de andere kant. PLValidateSM2Identity, de controle achter het ondertekenproces, accepteert de GM/T 0015-opzet, waarin de SubjectPublicKeyInfo id-ecPublicKey draagt met de SM2-kromme 1.2.156.10197.1.301 als parameter, en ook certificaten die de SM2-OID in het algoritmeveld zelf zetten, en hij wijst alles af waarvan het afgeleide publieke punt niet bij de private scalar past. Voordat u in productie op SM2-handtekeningen leunt, onderteken een document met een certificaat van de CA die u echt gaat gebruiken en verifieer het met de validator die uw tegenpartij draait. De identiteitscontrole kunt u zelf draaien voordat u een ondertekenproces start:
uses
SysUtils, PDFlibModernSign;
var
CertDER, KeyDER, Scalar: AnsiString;
begin
// CertDER en KeyDER geladen van schijf of een HSM-export
// Houd de Out-parameter in zijn eigen variabele: KeyDER als zowel
// de Const-invoer als de Out-uitvoer geven wist eerst de invoer
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;
Die commentaurregel houdt een echt defect uit het integratiewerk vast. Toen dezelfde AnsiString-variabele zowel als Const-invoer als als Out-uitvoer werd doorgegeven, finalizeerde de compiler de Out-variabele bij binnenkomst, werd de invoer leeg voordat de routine hem las, en faalde de identiteitscontrole op een volstrekt goede sleutel. Elke API die een sleutel als Const neemt en een afgeleide sleutel als Out teruggeeft heeft aan de kant van de aanroeper aparte variabelen nodig
FAQ
Heeft PDFlibPas Windows CNG of een externe DLL nodig voor SM2, SM3 en SM4?
Nee. PDFlibPas implementeert alle drie de algoritmen in Object Pascal in PDFlibDigest, PDFlibSM4 en PDFlibSM2, en SM2-handtekeningen worden door de CMS-code van de library gebouwd en geverifieerd. Windows CryptoAPI en CNG zijn niet betrokken bij het produceren van de SM2-handtekening
Kan ik SM2 met SHA-256 gebruiken in plaats van SM3?
Nee. Zodra een ondertekenproces een SM2-identiteit draagt, gebruikt de PDFlibPas CMS-builder altijd SM3, omdat GB/T 32918.2 SM2-handtekeningen definieert over een SM3-digest die de ZA-identiteitshash omvat. De digest op SHA-256 zetten bij een SM2-ondertekenproces verandert het resultaat niet
Opent Adobe Acrobat een door PDFlibPas met SM4 versleutelde PDF?
Verwacht het niet. Het /CFM /SM4-crypt filter valt buiten de waarden die ISO 32000 definieert, dus mainstream readers herkennen het niet. Gebruik SM4-versleuteling voor gesloten systemen die bestanden met PDFlibPas of een compatibele implementatie lezen, en AES-256 voor documenten bedoeld voor algemene distributie
SM2-ondertekening, SM3-hashing, SM4-versleuteling en de volledige set PAdES-onderteken-API's worden geleverd met PDFlibPas, de Delphi PDF Library, die compileert met Delphi, C++Builder en FPC/Lazarus op Windows- en Unix-doelen