Τεχνικό Άρθρο

Ψηφιακές υπογραφές PAdES στο Delphi: Υπογραφή και επαλήθευση με το PDF Library for Delphi

Η επικύρωση μιας υπογραφής PAdES σημαίνει τον έλεγχο τριών ανεξάρτητων πραγμάτων, και ένα πράσινο σημάδι ελέγχου σε ένα πρόγραμμα προβολής σας λέει μόνο για το τρίτο. Πρώτον, ο πίνακας /ByteRange πρέπει να καλύπτει τα σωστά bytes: τα διαστήματα που ονομάζει πρέπει να ανασυνθέτουν ακριβώς την είσοδο πάνω στην οποία λήφθηκε το digest του CMS, χωρίς να μένουν υπογεγραμμένα bytes εκτός αυτών. Δεύτερον, το πιστοποιητικό μέσα στο CMS πρέπει να αλυσοδένεται σε μια ρίζα που εμπιστεύεστε και να φέρει το υπογεγραμμένο χαρακτηριστικό signing-certificate που απαιτεί το PAdES. Τρίτον, αν το προφίλ δηλώνει timestamp, ένα token RFC 3161 πρέπει να δένει την τιμή της υπογραφής με ένα χρονικό σημείο πριν από τη λήξη του πιστοποιητικού. Ο Acrobat συμπτύσσει και τα τρία σε ένα εικονίδιο· ένας ελεγκτής συμμόρφωσης τα κρατά χωριστά, και το ίδιο οφείλει να κάνει ο κώδικας που παράγει αυτά τα αρχεία. Η losLab PDF Library (PDF Library for Delphi) σας δίνει την πλευρά της υπογραφής, την επανενσωμάτωση του timestamp και τις κλήσεις ελέγχου για να επιθεωρήσετε ένα ByteRange προτού το εμπιστευτείτε

Μια διάκριση μπερδεύει σχεδόν κάθε πρώτη υλοποίηση PAdES, οπότε αξίζει να δηλωθεί πριν από οποιονδήποτε κώδικα. Μια υπογραφή γραμμένη με /SubFilter /adbe.pkcs7.detached είναι απολύτως στέρεη υπογραφή κατά ISO 32000-1 §12.8, την οποία ο Acrobat θα αναφέρει ως έγκυρη. Δεν είναι όμως υπογραφή PAdES, επειδή το ETSI EN 319 142-1 απαιτεί ETSI.CAdES.detached σε κάθε baseline επίπεδο. Ένας ελεγκτής συμμόρφωσης eIDAS απορρίπτει την πρώτη και δέχεται τη δεύτερη, παρότι η κρυπτογραφία είναι πανομοιότυπη. Το προφίλ είναι μια δήλωση που κάνει το έγγραφο για τον εαυτό του, και το να τη διατυπώσετε σωστά είναι μία κλήση στο PDF Library for Delphi

Τι μετατρέπει μια υπογραφή PDF σε υπογραφή PAdES

Το ETSI EN 319 142-1 ορίζει τέσσερα baseline επίπεδα στοιβαγμένα πάνω στη μορφή CMS. Το PAdES-B-B είναι το σημείο εισόδου: μια υπογραφή CAdES σε πεδίο υπογραφής PDF με SubFilter ETSI.CAdES.detached και υπογεγραμμένο χαρακτηριστικό signing-certificate. Το PAdES-B-T προσθέτει timestamp RFC 3161 πάνω στην τιμή της υπογραφής, αποδεικνύοντας ότι η υπογραφή υπήρχε πριν από ένα χρονικό σημείο που κανείς δεν μπορεί να προχρονολογήσει. Το PAdES-B-LT ενσωματώνει τα πιστοποιητικά, τις CRL και τις αποκρίσεις OCSP που χρειάζονται για την επικύρωση μέσα σε ένα Document Security Store, ώστε το αρχείο να παραμένει επαληθεύσιμο αφότου η εκδότρια CA αποσύρει την υποδομή της. Το PAdES-B-LTA στεφανώνει τη στοίβα με ένα document timestamp που ξαναπροστατεύει τα συσσωρευμένα τεκμήρια καθώς οι αλγόριθμοι εξασθενούν

Το PDF Library for Delphi αντιστοιχίζει αυτές τις έννοιες στο API της διαδικασίας υπογραφής του. Ο δείκτης του προφίλ είναι η SetSignProcessCustomSubFilter. Αν η πολιτική σας χρειάζεται ένδειξη τύπου δέσμευσης (απόδειξη προέλευσης, απόδειξη έγκρισης ή κάποιο από τα άλλα αναγνωριστικά ETSI με αρίθμηση 1 έως 6), αυτό περνά από την SetSignProcessCommitmentType. Μια ρητή πολιτική υπογραφής επισυνάπτεται με την SetSignProcessSignaturePolicy, η οποία δέχεται το OID της πολιτικής και το digest του. Μία προεπιλογή αξίζει προσοχής: με τον αλγόριθμο digest αφημένο σε auto, η βιβλιοθήκη επιλέγει SHA-256 για υπογραφές ETSI και adbe.pkcs7.detached και υποχωρεί σε SHA-1 μόνο στην παλαιά διαδρομή adbe.pkcs7.sha1. Ορίστε τον ρητά ούτως ή άλλως. Οι ελεγκτές ρωτούν ποιο hash χρησιμοποιήσατε, και μια ρητή τιμή μέσα στον κώδικα υπερασπίζεται ευκολότερα από μια προεπιλογή που πρέπει να πάτε να διαβάσετε στο εγχειρίδιο για να την εξηγήσετε

Κλίμακα επιπέδων baseline PAdES B-B, B-T, B-LT και B-LTA που κατασκευάζεται με το PDF Library for Delphi, δείχνοντας πώς κάθε επίπεδο προσθέτει χρονοσφραγίδες, αποδείξεις DSS ή ανανεώσιμη χρονοσφραγίδα εγγράφου πάνω στον πυρήνα ETSI.CAdES.detached
Κάθε επίπεδο βάσης ETSI προσθέτει μία ακόμη εγγύηση πάνω στον ίδιο πυρήνα CAdES, από υπογεγραμμένα χαρακτηριστικά έως ανανεώσιμη χρονοσφραγίδα εγγράφου

Παραγωγή της baseline υπογραφής

Το επίπεδο API οδηγεί την υπογραφή ως μηχανή καταστάσεων μιας βολής: ανοίγετε μια διαδικασία στο αρχείο πηγής, τη ρυθμίζετε, την ολοκληρώνετε σε ένα αρχείο εξόδου, διαβάζετε τον κωδικό αποτελέσματος. Η ακολουθία που ακολουθεί παράγει υπογραφή PAdES-B-B με SHA-256. Η γραμμή που μετράει περισσότερο δεν έχει καμία σχέση με την ίδια την υπογραφή. Είναι η σκόπιμα υπερμεγέθης δέσμευση /Contents, επειδή αυτό είναι το μόνο πράγμα που δεν μπορείτε να αλλάξετε αργότερα αν χρειαστεί ποτέ να προστεθεί timestamp σε αυτή την υπογραφή

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);  // χώρος για timestamp αργότερα
    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 επιστρέφει 0 όταν η πηγή δεν μπορεί να ανοίξει καθόλου. Μετά από αυτό, η GetSignProcessResult ξεχωρίζει τους τρόπους αστοχίας που όντως συμβαίνουν στην παραγωγή: 4 σημαίνει λάθος συνθηματικό PDF, 7 λάθος συνθηματικό PFX, 9 αρχείο πιστοποιητικού χωρίς ιδιωτικό κλειδί, 10 μη εγγράψιμη διαδρομή εξόδου, 11 αποτυχία κατά την εφαρμογή των bytes της υπογραφής. Η καταγραφή του αριθμητικού κωδικού δίπλα στο όνομα του αρχείου εισόδου μετατρέπει ένα ασαφές δελτίο υποστήριξης σε διάγνωση ενός λεπτού

Προσθήκη του timestamp RFC 3161 που η βιβλιοθήκη δεν θα φέρει για εσάς

Το PDF Library for Delphi δεν περιλαμβάνει πελάτη TSA, και αυτό είναι σκόπιμο όριο και όχι κενό. Η βιβλιοθήκη υπολογίζει το hash που πρέπει να προσυπογράψει η αρχή χρονοσήμανσης και επανενσωματώνει έπειτα το εμπλουτισμένο CMS· η ανταλλαγή HTTP και η χειρουργική επέμβαση στο CMS ενδιάμεσα ανήκουν στον καλούντα. Υπάρχει σκληρός τεχνικός λόγος για τον διαχωρισμό. Το στοιχείο ελέγχου του Windows CryptoAPI που ονομαστικά προσθέτει μη υπογεγραμμένα χαρακτηριστικά, το CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, αποτυγχάνει με CRYPT_E_INVALID_INDEX στη διάταξη detached SignedData που χρησιμοποιεί το PAdES. Έτσι το εμπλουτισμένο CMS πρέπει να προέλθει από κωδικοποιητή CMS υπό τον δικό σας έλεγχο. Καμία βιβλιοθήκη δεν μπορεί να διπλώσει αθόρυβα το token μέσα με μία κλήση συστήματος, και όποια το ισχυρίζεται κάνει τη χειρουργική επέμβαση κάπου όπου δεν μπορείτε να τη δείτε

Ροή για προσθήκη χρονοσφραγίδας RFC 3161 σε υπογραφή PAdES σε Delphi, διαχωρίζοντας το hashing και την ενσωμάτωση του PDF Library for Delphi από το αίτημα TSA του καλούντα και την επανακωδικοποίηση CMS μέσα στον δεσμευμένο χώρο /Contents
Η βιβλιοθήκη υπολογίζει hashes και επανενσωματώνει ενώ ο κώδικάς σας ανακτά το token και επιχειρεί τη χειρουργική CMS, και το αποτέλεσμα πρέπει να καταλήξει μέσα στην κράτηση 8192 byte του /Contents
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);
    // οι δύο κλήσεις παρακάτω είναι κώδικας εφαρμογής: ένα HTTP POST προς την TSA σας,
    // και μια επανακωδικοποίηση CMS που επισυνάπτει το token ως μη υπογεγραμμένο χαρακτηριστικό
    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;

Προσέξτε τους κωδικούς αποτελέσματος εδώ: 12 σημαίνει ότι το κατονομαζόμενο πεδίο υπογραφής δεν υπάρχει, 11 ότι το υπάρχον CMS δεν μπόρεσε να αναλυθεί και 13 ότι το εμπλουτισμένο CMS δεν χωρά πια στη δεσμευμένη θέση /Contents. Ο κωδικός 13 είναι εκείνος που πονά, επειδή η μόνη λύση είναι η εκ νέου υπογραφή: ένα τυπικό token χρονοσήμανσης με την αλυσίδα πιστοποιητικών του πιάνει 4 έως 6 KB, και η δέσμευση των 8192 bytes που έγινε στο βήμα B-B υπάρχει ακριβώς για να έχει χώρο να προσγειωθεί αυτό το βήμα

Η επικύρωση ξεκινά από το ByteRange, όχι από την αλυσίδα πιστοποιητικών

Ένα πράσινο σημάδι ελέγχου σε ένα πρόγραμμα προβολής είναι απόφαση εμπιστοσύνης απέναντι στο αποθετήριο πιστοποιητικών εκείνου του μηχανήματος, όχι δομική ετυμηγορία για το αρχείο. Η προγραμματιστική επικύρωση θα πρέπει να ξεκινά χαμηλότερα, με το ερώτημα που τα incremental updates κάνουν λεπτό: ποια bytes καλύπτει πραγματικά κάθε υπογραφή; Κάθε βελτίωση που συζητείται εδώ, είτε πρόκειται για δεύτερη υπογραφή, είτε για λεξικό DSS, είτε για document timestamp, φτάνει μέσω incremental update, και κάθε ενημέρωση προσαρτά bytes εκτός του /ByteRange της προηγούμενης υπογραφής. Αυτά τα προσαρτημένα bytes είναι νόμιμα. Ένας επικυρωτής οφείλει παρ' όλα αυτά να τα κατατάξει σε σχέση με την πολιτική τροποποιήσεων του εγγράφου, και το επίπεδο DocMDP ανά πεδίο μέσα στο οποίο ζει αυτή η πολιτική διαβάζεται με την GetSignatureDocMDPLevelByName

Έλεγχος διάταξης bytes ενός υπογεγραμμένου PDF σε Delphi που δείχνει τα καλυμμένα διαστήματα του ByteRange, τα εξαιρεθέντα bytes του /Contents, τις προσαρτημένες επαυξητικές ενημερώσεις εκτός εύρους και την ετυμηγορία κάλυψης σε σχέση με το μέγεθος αρχείου
Δύο καλυμμένα διαστήματα με εξαιρεμένα τα ίδια τα byte της υπογραφής λένε την αληθινή ιστορία κάλυψης, και οι προσαρτημένες ενημερώσεις ταξινομούνται απέναντι στην πολιτική DocMDP αντί να τρομάζουν
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // πριν από το Open: το SignDoc κρατά κλείδωμα κοινής χρήσης
  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 σημαίνει ότι έχει όντως υπογραφεί
        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;

Δύο παγίδες ζουν σε αυτή τη διαδρομή ελέγχου. Η TPDFlibSignDoc.Open κρατά το αρχείο με αποκλειστικό κλείδωμα κοινής χρήσης, οπότε ένας επικυρωτής που θέλει επίσης να υπολογίσει το hash των ακατέργαστων bytes του αρχείου για την επαλήθευση CMS πρέπει να διαβάσει το αρχείο στη μνήμη προτού το ανοίξει για έλεγχο. Αντιστρέψτε αυτή τη σειρά και η ανάγνωση αποτυγχάνει σε ένα κλείδωμα που βάλατε μόνοι σας. Η δεύτερη παγίδα είναι σιωπηλή αντί για θορυβώδης: το αντίστοιχο του επίπεδου API, η GetSignProcessByteRange, επιστρέφει Integer ενώ οι υποκείμενες μετατοπίσεις είναι Int64, οπότε πέρα από τα 2 GB η επίπεδη κλήση περικόπτει χωρίς παράπονο, και γι' αυτό το παράδειγμα εδώ τραβά τις μετατοπίσεις μέσω της κλάσης ελέγχου. Αξίζει επίσης να κατονομαστεί μια απουσία. Το επίπεδο των επίπεδων κλήσεων δεν έχει καθόλου wrapper VerifySignature. Οι κρυπτογραφικές ετυμηγορίες προέρχονται από την TPDFlibSignatureVerifier σε επίπεδο κλάσης, η οποία επιστρέφει vsValid, vsInvalid ή vsUnknown, ή από κάποιον εξωτερικό επικυρωτή που η πολιτική συμμόρφωσής σας ήδη εμπιστεύεται

Μακροχρόνια επικύρωση: DSS, VRI και το document timestamp

Το PAdES-B-LT υπάρχει επειδή η υποδομή ανάκλησης είναι θνητή. Το ETSI EN 319 142-1 §5.4.2.2 προδιαγράφει το Document Security Store: ένα λεξικό σε επίπεδο εγγράφου που φέρει πιστοποιητικά, CRL και αποκρίσεις OCSP, προαιρετικά ευρετηριασμένα ανά υπογραφή μέσω καταχωρίσεων VRI με κλειδί το hash του /Contents κάθε υπογραφής. Η ροή του PDF Library for Delphi αντικατοπτρίζει τον σχεδιασμό της χρονοσήμανσης. Η NewPAdESDSSProcessFromFile ανοίγει τη διαδικασία· οι AddPAdESDSSCertificate, AddPAdESDSSCRL και AddPAdESDSSOCSP δέχονται blobs DER· η AddPAdESDSSVRI δένει επιλεγμένο υλικό σε μία υπογραφή· η EndPAdESDSSProcessToFile γράφει τα πάντα ως incremental update. Το δύσκολο μέρος παραμένει στη δική σας πλευρά. Η ανάκτηση του υλικού ανάκλησης, και η κρίση για το αν είναι αρκετά πρόσφατο ώστε να αξίζει να ενσωματωθεί, είναι δουλειά του καλούντος. Η βιβλιοθήκη εγγυάται ότι τα λεξικά είναι δομικά συμμορφούμενα· δεν μπορεί να εγγυηθεί ότι ο OCSP responder σας είπε την αλήθεια

Το τερματικό σημείο της αρχειοθέτησης, το B-LTA, προσθέτει document timestamp: ένα ξεχωριστό πεδίο υπογραφής του οποίου ο τύπος είναι DocTimeStamp αντί για Sig, που παράγεται μέσω της SetSignProcessDocTimeStamp με δεσμευμένο μήκος υπογραφής. Δεν αντικαθιστά το timestamp υπογραφής από το βήμα B-T. Το timestamp υπογραφής αποδεικνύει πότε υπήρχε μια συγκεκριμένη υπογραφή· το document timestamp προστατεύει ολόκληρο το αρχείο, μαζί με τα τεκμήρια DSS, και είναι το στοιχείο που ένα μακροχρόνιο αρχείο ανανεώνει κάθε λίγα χρόνια καθώς οι αλγόριθμοι εξασθενούν. Ένα ώριμο προφίλ αρχειοθέτησης φέρει και τα δύο. Για προγράμματα ανάγνωσης που προηγούνται αυτών των δομών, η TPDFlibSignDoc.EnsurePAdESExtensions καταγράφει την επέκταση προγραμματιστή ESIC στον κατάλογο του εγγράφου, ανακοινώνοντας ότι το αρχείο χρησιμοποιεί δυνατότητες ορισμένες από το ETSI

Μια αντίδραση σε όλα αυτά αξίζει να προληφθεί, επειδή μοιάζει με σφάλμα ενώ δεν είναι. Ένα πρόγραμμα προβολής συχνά αναφέρει «άγνωστη εγκυρότητα» για αρχείο του οποίου η δομή PAdES είναι απολύτως σωστή. Η εμπιστοσύνη και η δομή είναι ανεξάρτητοι άξονες. Το πρόγραμμα προβολής απλώς δεν μπορεί να αλυσοδέσει τον υπογράφοντα σε μια ρίζα που εμπιστεύεται σε εκείνο το μηχάνημα, κάτι σύνηθες με ιδιωτικές CA και δοκιμαστικά πιστοποιητικά, ακόμη κι όταν ο έλεγχος του ByteRange και η επαλήθευση CMS περνούν και τα δύο. Η λύση είναι να διανείμετε σωστά το πιστοποιητικό ρίζας, ή να αξιολογείτε έναντι των έμπιστων καταλόγων της ΕΕ όταν πραγματικός στόχος είναι το καθεστώς εγκεκριμένης υπογραφής eIDAS, και όχι να πειράξετε τον κώδικα υπογραφής

Για την οπτική της πλευράς ελέγχου, δηλαδή την απαρίθμηση πεδίων υπογραφής σε ένα σώμα εγγράφων, την αποτύπωση διατάξεων ByteRange και τη μαζική ανάγνωση επιπέδων DocMDP, δείτε το συνοδευτικό κείμενο για τον πάγκο εργασίας συμμόρφωσης και υπογραφής. Τα υπογεγραμμένα έγγραφα που πρέπει επίσης να ικανοποιούν πολιτική αρχειοθέτησης ανήκουν στη ροή εργασίας που περιγράφεται στο preflight PDF/A και PDF/UA στη Delphi. Η πλήρης τεκμηρίωση του API και οι λήψεις αξιολόγησης βρίσκονται στη σελίδα προϊόντος losLab PDF Library for Delphi