Beslissen dat een PDF-handtekening onder eIDAS gekwalificeerd is, betekent een vraag beantwoorden die niets met cryptografie te maken heeft: is het certificaat uitgegeven door een trustdienst die een lidstaat als gekwalificeerd heeft opgelijst, op het moment dat de handtekening werd gezet. Het antwoord huist in een trusted list, een XML-document dat per grondgebied wordt gepubliceerd, en de hele waarde van dat document hangt af van zijn authenticiteit. Daarom weigert de PDFium-component erin te kijken totdat iemand ervoor heeft geborgd. TPdfEuropeanTrustedList.ParseAuthenticated geeft de volledige ruwe bytes aan een door de aanroeper geleverde IPdfTrustedListAuthenticator voordat die één dienst parseert, en die maakt pas een snapshot als die authenticator expliciet slaagt
Die volgorde is het ontwerp. Al het andere in deze functie volgt daaruit, inclusief de delen die onhandig ogen
Geparseerd is niet vertrouwd
Een trustlijst die schoon parseert vertelt u dat de XML goed gevormd is. Die vertelt u niets over wie die schreef. Omdat de lijst is waarop uw hele beslissing over gekwalificeerde status rust, zou het accepteren ervan omdat die parseerde de beslissing betekenisloos maken: een aanvaller die de lijst kan vervangen kan zijn eigen certificeringsinstantie als gekwalificeerd laten gelden
Dezelfde redenering geldt voor cachen, en dit is de valkuil die het benoemen waard is. De snapshotcache bewaart de oorspronkelijke XML samen met een SHA-256-digest, en het zou gemakkelijk zijn een matchende digest bij het laden te zien als bewijs dat de lijst echt is. Dat is het niet. Een digest berekend door hetzelfde proces dat het bestand opslaat, zonder dat er een sleutel aan te pas komt, verifieert alleen dat de bytes niet zijn veranderd sinds u ze schreef; als de lijst frauduleus was toen die werd gecacht, bevestigt de digest dat het dezelfde frauduleuze lijst is. Dus het laden van een gecachte snapshot verloopt via dezelfde authenticator als het parsen van een verse. Integriteit en authenticiteit zijn verschillende eigenschappen en slechts één ervan heeft een sleutel nodig
uses
FPdfTrustedList;
type
TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
public
function Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
end;
function TListAuthenticator.Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
begin
// Uw beleid huist hier: verifieer de XMLDSIG enveloped signature
// tegen het certificaat van de lijstondertekenaar dat u out-of-band
// heeft vastgepind, en omschrijf wat u heeft gecontroleerd voor de audittrail
Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
if Result then
AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;
var
List: TPdfEuropeanTrustedList;
Cache: TFileStream;
begin
List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
TListAuthenticator.Create, TPdfTrustedListOptions.Default);
// De snapshot bestaat alleen omdat de authenticator ja zei
Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
try
List.SaveCache(Cache);
finally
Cache.Free;
end;
end;
De validator is geen eigenaar van netwerkbeleid
Een PAdES-validator heeft niets te maken met beslissen hoe de lijst van trustlijsten wordt bereikt, of dat via een proxy gaat, hoe vaak opnieuw wordt geprobeerd of wat er gebeurt als een grondgebied onbereikbaar is. Dat zijn beslissingen van applicatie en uitrol, en in gereguleerde omgevingen worden ze geregeld geauditeerd. Updates komen daarom binnen via IPdfTrustedListSource, aan welke een URI en een bytecap worden meegegeven en die bytes teruggeeft
Wat de component wél afdwingt zijn de invarianten die van een update een update maken in plaats van een vervanging. Update vereist dat het grondgebied onveranderd is, dat het volgnummer streng toeneemt en dat het uitgiftemoment niet achteruit beweegt. Die drie controles verslaan de meest voor de hand liggende downgrade-aanvallen: het herafspelen van een oudere lijst die nog een inmiddels ingetrokken dienst noemt, of het inwisselen voor de lijst van een ander grondgebied waarvan u de diensten nooit wilde vertrouwen
Parserlimieten, en helemaal geen DTD
TPdfTrustedListOptions begrenst de XML-grootte, het tokenaantal, de nestdiepte, het aantal diensten, het aantal certificaten en de grootte van een individueel certificaat, waarbij een klassenfunctie Default bruikbare waarden levert. Trustlijsten zijn gepubliceerde documenten van voorspelbare grootte, dus grenzen zijn goedkoop te zetten en er is geen legitieme lijst die die hoeft te overschrijden
Apart en onvoorwaardelijk verwerpt de parser DTD- en entiteitdeclarities. Dat sluit zowel de denial of service via entiteitsuitbreiding als de openbaarmakingsroute via externe entiteiten af in één weigering, en het kost niets omdat trustlijsten geen entiteiten gebruiken. Elke XML-parser die vanuit onbetrouwbare input bereikbaar is zou zo moeten zijn ingesteld; het verschil hier is dat de weigering niet instelbaar is, dus die kan niet worden uitgezet door een goedbedoelde optiewijziging
Gekwalificeerde status wordt naast kettentrust vastgelegd, niet ermee versmolten
De evaluatiekant is bewust apart. TPadesTrustValidationOptions.QualifiedTrustEvaluator neemt een IPdfQualifiedTrustEvaluator, die de trustlijst-snapshot implementeert. Tijdens de validatie ontvangt de evaluator het bladcertificaat, de keten en een validatietijd, matcht dienstcertificaten door exacte DER-vergelijking tegen de ondertekenaar en de keten, combineert de dienststatus, de diensttype-identificator en de qualifier-URI's op dat moment, en geeft een evaluatierecord terug
Het resultaat landt op twee plaatsen bij elke handtekening: QualifiedTrustStatus als grove status en QualifiedTrust als de volledige evaluatie met grondgebied, providernaam, dienstnaam, type-identificator, status en statusbegintijd. Wat die niet doet is CertificateTrustStatus veranderen. Systeemkettentrust en gekwalificeerde status beantwoorden verschillende vragen, en een rapport dat ze samenvouwen kan "vertrouwd maar niet gekwalificeerd" niet onderscheiden van "gekwalificeerd maar de keten valideert niet", en beide zijn echt en beide vragen verschillende afhandeling
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
I: Integer;
begin
Options := TPadesTrustValidationOptions.Default;
Options.CheckRevocation := True;
Options.QualifiedTrustEvaluator := List; // de geauthenticeerde snapshot
Options.QualifiedValidationTime := SigningTime; // niet Now
Report := Pdf.ValidatePadesTrust(Options);
for I := 0 to High(Report.Signatures) do
if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
Writeln(Format('signature %d qualified by %s / %s (%s)',
[I, Report.Signatures[I].QualifiedTrust.Territory,
Report.Signatures[I].QualifiedTrust.ProviderName,
Report.Signatures[I].QualifiedTrust.ServiceName]))
else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
// Geen matchende dienst, of de snapshot kan niet voor dit moment antwoorden
Writeln(Format('signature %d: qualified status undetermined', [I]));
end;
Waarom de validatietijd niet nu is
Omdat kwalificatie een eigenschap van een moment is. Een trustdienst kan gekwalificeerde status krijgen, die later wordt ingetrokken en later nog wordt hersteld, en elk van die overgangen draagt een begintijd in de lijst. Een handtekening gezet terwijl de dienst gekwalificeerd was blijft daarna gekwalificeerd; een handtekening gezet vóór de toekenning wordt niet met terugwerkende kracht gekwalificeerd. Evalueren tegen de huidige tijd geeft dus in beide richtingen het verkeerde antwoord
De lijst draagt mee wat daarvoor nodig is: elk dienstrecord heeft een statusbegintijd en een vlag die historische ingangen van huidige onderscheidt, en de evaluator combineert die tegen de tijd die u aanlevert. In de praktijk komt die tijd van een vertrouwd tijdstempel op de handtekening in plaats van van de ondertekentijd die de CMS claimt, en daarom doet langetermijnvalidatiemateriaal ertoe zelfs voor een vraag die oogt als een beleidsopzoek; de kant van tijdstempel en DSS wordt behandeld in het artikel over langetermijnhandtekeningen
Wat u nog steeds zelf moet bouwen
Drie dingen, en geen enkel ervan hoort in een PDF-bibliotheek. De authenticator, dus echte XMLDSIG-verificatie tegen het certificaat van een lijstondertekenaar dat u via een kanaal hebt verkregen dat u vertrouwt. Het ophaalbeleid, dus hoe en hoe vaak u ververst en wat uw applicatie doet als een verversing faalt. En de territoriale reikwijdte, dus welke lijsten u überhaupt meedraagt, wat een zakelijke beslissing is over in welke lidstaten uw tegenpartijen ondertekenen
Wat u van de component krijgt is het deel dat subtiel verkeerd gaan gemakkelijk is: de authenticeer-voor-parsen-volgorde, begrensd en entiteitvrij XML-parsen, monotone update-invarianten, exacte DER-dienstmatching, historische statusevaluatie en een resultaat dat apart blijft van gewone kettentrust. Is uw directe probleem basisser, dat een validator een handtekening verwerpt waarvan u gelooft dat die klopt, dan zijn de gebruikelijke oorzaken gecatalogiseerd in waarom validators PAdES-handtekeningen verwerpen, en het inspectieoppervlak van handtekeningen wordt beschreven in handtekeningen en PAdES-niveaus inspecteren. Componentmogelijkheden staan op de productpagina van de PDFium Delphi component