Het valideren van één PAdES-handtekening betekent drie onafhankelijke zaken controleren, en een groen vinkje in een viewer vertelt u alleen iets over de derde. Ten eerste moet de array /ByteRange de juiste bytes omvatten: de bereiken die zij noemt, moeten de exacte invoer reconstrueren waarover de CMS-digest is berekend, zonder ondertekende bytes erbuiten. Ten tweede moet het certificaat in de CMS naar een root ketenen die u vertrouwt en het ondertekende signing-certificate-attribuut bevatten dat PAdES vereist. Ten derde moet, als het profiel een tijdstempel claimt, een RFC 3161-token de handtekeningwaarde binden aan een tijdstip voordat het certificaat verliep. Acrobat vouwt alle drie samen tot één pictogram; een conformiteitscontrole houdt ze gescheiden, en de code die deze bestanden produceert moet dat ook doen. losLab PDF Library (PDF Library for Delphi) biedt u de ondertekeningskant daarvan, het opnieuw inbedden van tijdstempels en de audit-aanroepen om een ByteRange te inspecteren voordat u die vertrouwt
Eén onderscheid brengt bijna elke eerste PAdES-implementatie in verwarring, dus het verdient vermelding vóór enige code. Een handtekening geschreven met /SubFilter /adbe.pkcs7.detached is een volkomen geldige ISO 32000-1 §12.8-handtekening die Acrobat als geldig rapporteert. Het is ook geen PAdES-handtekening, omdat ETSI EN 319 142-1 ETSI.CAdES.detached vereist op elk baseline-niveau. Een eIDAS-conformiteitscontrole verwerpt de eerste en accepteert de tweede, ook al is de cryptografie identiek. Het profiel is een claim die het document over zichzelf doet, en die claim goed krijgen is één aanroep in PDF Library for Delphi
Wat een PDF-handtekening tot een PAdES-handtekening maakt
ETSI EN 319 142-1 definieert vier baselineniveaus die op het CMS-formaat zijn gestapeld. PAdES-B-B is het beginpunt: een CAdES-handtekening in een PDF-handtekeningveld met de SubFilter ETSI.CAdES.detached en een ondertekend signing-certificate-attribuut. PAdES-B-T voegt een RFC 3161-tijdstempel toe over de handtekeningwaarde, waarmee wordt bewezen dat de handtekening bestond vóór een tijdstip dat niemand kan antedateren. PAdES-B-LT sluit de certificaten, CRL's en OCSP-antwoorden die nodig zijn voor validatie in een Document Security Store in, zodat het bestand verifieerbaar blijft nadat de uitgevende CA zijn infrastructuur heeft opgeheven. PAdES-B-LTA sluit de stapel af met een documenttijdstempel dat het verzamelde bewijs opnieuw beschermt wanneer algoritmen verzwakken
PDF Library for Delphi koppelt deze concepten aan zijn sign-process-API. De profielmarkering is SetSignProcessCustomSubFilter. Als uw beleid een commitment-type-aanduiding nodig heeft (bewijs van oorsprong, bewijs van goedkeuring of een van de andere ETSI-identificatoren genummerd 1 tot en met 6), gaat dat via SetSignProcessCommitmentType. Een expliciet handtekeningbeleid koppelt u met SetSignProcessSignaturePolicy, dat de beleids-OID en zijn digest neemt. Eén standaard verdient aandacht: wanneer het digestalgoritme op automatisch blijft staan, kiest de bibliotheek SHA-256 voor ETSI- en adbe.pkcs7.detached-handtekeningen en valt alleen op het oude pad adbe.pkcs7.sha1 terug op SHA-1. Stel het toch expliciet in. Auditors vragen welke hash u hebt gebruikt, en een expliciete waarde in de code is eenvoudiger te verdedigen dan een standaard waarvoor u de handleiding moet raadplegen om hem uit te leggen
De baseline-handtekening produceren
De platte API stuurt ondertekening aan als een one-shot-toestandsmachine: open een proces op het bronbestand, configureer het, voltooi het naar een uitvoerbestand en lees de resultaatcode. De onderstaande volgorde produceert een PAdES-B-B-handtekening met SHA-256. De belangrijkste regel heeft niets met de handtekening zelf te maken. Het is de opzettelijk ruime reservering voor /Contents, want dat is het enige dat u later niet kunt veranderen als ooit een tijdstempel aan deze handtekening moet worden toegevoegd
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open source PDF');
Pdf.SetSignProcessField(SignId, 'Sig1');
Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
Pdf.SetSignProcessDigestAlgorithm(SignId, 2); // SHA-256
Pdf.SetSignProcessReserveContentsBytes(SignId, 8192); // room for a timestamp later
Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
Pdf.ReleaseSignProcess(SignId);
finally
Pdf.Free;
end;
end;
NewSignProcessFromFile retourneert 0 wanneer de bron helemaal niet kan worden geopend. Daarna scheidt GetSignProcessResult de foutmodi die in productie werkelijk optreden: 4 betekent een verkeerd PDF-wachtwoord, 7 een verkeerd PFX-wachtwoord, 9 een certificaatbestand zonder privésleutel, 10 een niet-beschrijfbaar uitvoerpad, 11 een fout bij het toepassen van de handtekeningbytes. Het loggen van de numerieke code naast de naam van het invoerbestand verandert een vaag supportticket in een diagnose van één minuut
De RFC 3161-tijdstempel toevoegen die de bibliotheek niet voor u ophaalt
PDF Library for Delphi levert geen TSA-client mee, en dat is een doelbewuste grens in plaats van een lacune. De bibliotheek berekent de hash die de tijdstempelautoriteit moet medeondertekenen en sluit daarna de aangevulde CMS opnieuw in; de HTTP-uitwisseling en de CMS-chirurgie daartussen behoren tot de aanroeper. Er is een harde technische reden voor die opsplitsing. De Windows CryptoAPI-control die nominaal niet-geauthenticeerde attributen toevoegt, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, faalt met CRYPT_E_INVALID_INDEX op de losgekoppelde SignedData-layout die PAdES gebruikt. De verbeterde CMS moet dus afkomstig zijn van een CMS-encoder die u zelf beheert. Geen bibliotheek kan het token stilletjes met één systeemaanroep toevoegen, en elke bibliotheek die beweert dat wel te doen, voert de operatie ergens uit waar u die niet kunt zien
var
Pdf: TPDFlib;
StsId: Integer;
HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
Pdf := TPDFlib.Create;
try
StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
// beide calls hieronder zijn applicatiecode: een HTTP POST naar je TSA,
// en een CMS re-encode die de token als unsigned attribute meestuurt
TstDer := RequestTimeStampToken(HashHex);
TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
raise Exception.Create('timestamp embedding failed');
Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
finally
Pdf.Free;
end;
end;
Let hier op de resultaatcodes: 12 betekent dat het genoemde handtekeningveld niet bestaat, 11 dat de bestaande CMS niet kon worden geparseerd en 13 dat de aangevulde CMS niet langer in de gereserveerde plaatsaanduiding /Contents past. Code 13 is de pijnlijke, omdat de enige oplossing opnieuw ondertekenen is: een typisch tijdstempeltoken met zijn certificaatketen neemt 4 tot 6 KB in, en de reservering van 8192 bytes tijdens de B-B-stap bestaat precies zodat deze stap ruimte heeft om te landen
Validatie begint bij de ByteRange, niet bij de certificaatketen
Een groen vinkje in een viewer is een vertrouwensbeslissing tegen de certificatenopslag van die machine, geen structureel oordeel over het bestand. Programmatische validatie moet lager beginnen, bij de vraag die incrementele updates subtiel maken: welke bytes omvat elke handtekening werkelijk? Elke hier besproken verbetering, of het nu een tweede handtekening, een DSS-dictionary of een documenttijdstempel betreft, arriveert via een incrementele update en elke update voegt bytes toe buiten de eerdere /ByteRange van de handtekening. Die toegevoegde bytes zijn legitiem. Een validator moet ze nog altijd classificeren tegen het wijzigingsbeleid van het document, en het DocMDP-niveau per veld waarin dat beleid staat, is leesbaar met GetSignatureDocMDPLevelByName
var
Doc: TPDFlibSignDoc;
Names: TStringList;
I: Integer;
B0, B1, B2, B3, FileSize: Int64;
begin
FileSize := TFile.GetSize('invoice-bt.pdf'); // before Open: SignDoc holds a share lock
Doc := TPDFlibSignDoc.Create;
try
if not Doc.Open('invoice-bt.pdf', '', False) then
raise Exception.Create('cannot open for audit');
Names := TStringList.Create;
try
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // >0 betekent daadwerkelijk ondertekend
begin
B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (B0 = 0) and (B2 + B3 = FileSize) then
Writeln(Names[I], ': covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
end;
finally
Names.Free;
end;
Doc.Close;
finally
Doc.Free;
end;
end;
Twee valkuilen zitten in dit auditpad. TPDFlibSignDoc.Open houdt het bestand met een exclusieve share lock vast, dus een validator die ook de ruwe bestandsbytes voor CMS-verificatie wil hashen, moet het bestand in het geheugen lezen voordat het voor audit wordt geopend. Keert u die volgorde om, dan mislukt het lezen op een vergrendeling die u zelf hebt ingesteld. De tweede valkuil is stil in plaats van luid: de platte API-tegenhanger GetSignProcessByteRange retourneert Integer terwijl de onderliggende offsets Int64 zijn, zodat de platte aanroep boven 2 GB zonder klacht afkapt. Daarom haalt dit voorbeeld de offsets via de auditklasse op. Ook één afwezigheid verdient vermelding. De platte laag heeft helemaal geen VerifySignature-wrapper. Cryptografische oordelen komen van de klasse TPDFlibSignatureVerifier, die vsValid, vsInvalid of vsUnknown retourneert, of van een externe validator die uw compliancebeleid al vertrouwt
Langetermijnvalidatie: DSS, VRI en het documenttijdstempel
PAdES-B-LT bestaat omdat intrekkingsinfrastructuur sterfelijk is. ETSI EN 319 142-1 §5.4.2.2 specificeert de Document Security Store: een dictionary op documentniveau die certificaten, CRL's en OCSP-antwoorden bevat, optioneel per handtekening geïndexeerd via VRI-items die zijn gekoppeld aan de hash van de /Contents van elke handtekening. De PDF Library for Delphi-stroom weerspiegelt het tijdstempelontwerp. NewPAdESDSSProcessFromFile opent het proces; AddPAdESDSSCertificate, AddPAdESDSSCRL en AddPAdESDSSOCSP accepteren DER-blobs; AddPAdESDSSVRI bindt geselecteerd materiaal aan één handtekening; EndPAdESDSSProcessToFile schrijft alles als een incrementele update. Het lastige deel blijft aan uw kant. Het ophalen van het intrekkingsmateriaal en beoordelen of het vers genoeg is om in te sluiten, is de taak van de aanroeper. De bibliotheek garandeert dat de dictionaries structureel conform zijn; zij kan niet garanderen dat uw OCSP-responder de waarheid sprak
Het archief-eindpunt, B-LTA, voegt een documenttijdstempel toe: een afzonderlijk handtekeningveld waarvan het type DocTimeStamp is in plaats van Sig, geproduceerd via SetSignProcessDocTimeStamp met een gereserveerde handtekeninglengte. Het vervangt niet het handtekeningtijdstempel uit de B-T-stap. Het handtekeningtijdstempel bewijst wanneer één specifieke handtekening bestond; het documenttijdstempel beschermt het hele bestand, inclusief DSS-bewijs, en is het element dat een langetermijnarchief elke paar jaar vernieuwt wanneer algoritmen verzwakken. Een volwassen archiefprofiel bevat beide. Voor lezers die ouder zijn dan deze structuren, legt TPDFlibSignDoc.EnsurePAdESExtensions de ESIC-ontwikkelaarsextensie vast in de documentcatalogus en meldt daarmee dat het bestand door ETSI gedefinieerde functies gebruikt
Eén reactie hierop verdient afwending, omdat hij op een fout lijkt maar dat niet is. Een viewer meldt vaak "validity unknown" voor een bestand waarvan de PAdES-structuur volledig correct is. Vertrouwen en structuur zijn onafhankelijke assen. De viewer kan de ondertekenaar eenvoudigweg niet ketenen aan een root die hij op die machine vertrouwt, wat gebruikelijk is bij private CA's en testcertificaten, zelfs als de ByteRange-audit en CMS-verificatie beide slagen. De oplossing is het rootcertificaat correct distribueren, of tegen de EU-trusted lists evalueren wanneer gekwalificeerde eIDAS-status het werkelijke doel is, in plaats van de ondertekeningscode aan te raken
Zie voor het perspectief aan de auditkant, dus het opsommen van handtekeningvelden in een corpus, het dumpen van ByteRange-layouts en het in bulk lezen van DocMDP-niveaus, het begeleidende artikel over de compliance- en ondertekeningswerkbank. Ondertekende documenten die ook aan archiefbeleid moeten voldoen, horen thuis in de workflow beschreven in PDF/A- en PDF/UA-preflight in Delphi. Volledige API-documentatie en evaluatiedownloads staan op de productpagina van losLab PDF Library voor Delphi