Η συνάρτηση PLCreateSelfSignedCertificate του PDFlibPas χτίζει ένα αυτο-υπογεγραμμένο πιστοποιητικό RSA/SHA-256 και το εξάγει, μαζί με το ιδιωτικό κλειδί, απευθείας σε ένα αρχείο PFX προστατευμένο με κωδικό πρόσβασης, χρησιμοποιώντας μόνο το Win32 CryptoAPI που είναι ήδη εγκατεστημένο σε κάθε μηχάνημα Windows. Χωρίς εξωτερικό εργαλείο, χωρίς αρχή πιστοποίησης, χωρίς χειροκίνητο βήμα makecert ή OpenSSL: μία κλήση συνάρτησης, ένα πιστοποιητικό αρκετά καλό για να οδηγήσει μια δοκιμή υπογραφής
Το σενάριο που κάνει αυτή τη συνάρτηση άξια ύπαρξης είναι σχεδόν πάντα ένα pipeline CI. Ένα smoke test υπογραφής χρειάζεται ένα πραγματικό PFX με ένα πραγματικό ιδιωτικό κλειδί από πίσω του, και το να προστεθεί ένα τέτοιο στο repository είναι δικό του πρόβλημα ασφάλειας, αφού ένα commit-αρισμένο ιδιωτικό κλειδί είναι διαρρευσμένο ιδιωτικό κλειδί από τη στιγμή που φτάνει εκείνο το commit. Η κλήση του makecert.exe ή μιας εκτέλεσης OpenSSL από ένα build script λειτουργεί επίσης, αλλά τότε το pipeline εξαρτάται από ένα εργαλείο που πρέπει να είναι εγκατεστημένο, να βρίσκεται στο PATH, και να διατηρείται συνεπές σε έκδοση σε κάθε build agent. Η δημιουργία του πιστοποιητικού μέσα στην ίδια διεργασία που τρέχει τη δοκιμή, με τις ίδιες κλήσεις Win32 CryptoAPI που τα Windows ήδη διαθέτουν, αφαιρεί εντελώς αυτή την εξάρτηση
Τι παράγει πραγματικά η PLCreateSelfSignedCertificate;
Η PLCreateSelfSignedCertificate παράγει ένα αρχείο PFX προστατευμένο με κωδικό πρόσβασης που κρατά ένα αυτο-υπογεγραμμένο πιστοποιητικό RSA και το ιδιωτικό του κλειδί, υπογεγραμμένο με sha256RSA, ελεγχόμενο από πέντε παραμέτρους: SubjectName, PFXFileName, PFXPassword, ValidDays και KeyBits, και επιστρέφει μια απλή σημαία επιτυχίας Boolean. Το SubjectName δέχεται μια πλήρη συμβολοσειρά X.500 όπως 'CN=Alice, O=Example', και ένα γυμνό όνομα χωρίς σύμβολο = προθεματοδοτείται αυτόματα με CN=. Το ValidDays κάτω από 1 επιστρέφει στο 365 ως προεπιλογή, και το KeyBits εκτός του εύρους 1024 έως 16384 επιστρέφει στο 2048 ως προεπιλογή. Το PDFlibPas διαθέτει αυτή τη συνάρτηση από την v3.224.0, προσβάσιμη όχι μόνο από τη μονάδα Delphi αλλά και μέσω των επιφανειών DLL και ActiveX, και το ίδιο το σχόλιο τεκμηρίωσής της είναι ξεκάθαρο για το πού σταματά να είναι χρήσιμη: κάθε δημοφιλής θεατής σημειώνει ένα αυτο-υπογεγραμμένο πιστοποιητικό ως μη έμπιστο εκτός αν κάποιος το εγκαταστήσει ρητά, οπότε αντιμετωπίστε ό,τι παράγει ως πιστοποιητικό για την άσκηση μιας διαδρομής κώδικα, όχι ως υπογραφή που κάποιος εκτός της ομάδας σας θα έπρεπε να εμπιστευτεί
var
Success: Boolean;
begin
Success := PLCreateSelfSignedCertificate(
'CN=PDFlibPas CI Test, O=Example Corp',
'ci-test-signer.pfx',
'a-strong-throwaway-password',
365, // ValidDays
2048); // KeyBits
if not Success then
raise Exception.Create('Self-signed certificate generation failed');
end;
Γιατί η CryptGenKey κωδικοποιεί το μήκος κλειδιού στην παράμετρο flags;
Η CryptGenKey συσκευάζει δύο άσχετες μεταξύ τους ρυθμίσεις σε μία μοναδική παράμετρο dwFlags. Η κατώτερη λέξη φέρει σημαίες συμπεριφοράς, μεταξύ αυτών το CRYPT_EXPORTABLE, ενώ η ανώτερη λέξη, για ένα κλειδί ανταλλαγής κλειδιών RSA, φέρει το ζητούμενο μήκος κλειδιού σε bits. Αν περάσετε το 2048 σαν να ήταν απλώς μια ακόμα σημαία, καταλήγει στην κατώτερη λέξη αντ' αυτού, όπου δεν ταιριάζει με καμία σημαία συμπεριφοράς που ορίζει το CryptoAPI, οπότε η κλήση παράγει ένα κλειδί στο όποιο προεπιλεγμένο μήκος επιστρέφει ο πάροχος αντί για το μήκος που νόμιζε ο καλών ότι ζητούσε. Η απόκτηση ενός πραγματικού κλειδιού RSA 2048-bit σημαίνει να μετακινήσετε πρώτα τον αριθμό στην ανώτερη λέξη
// Key length lives in the upper 16 bits of the CryptGenKey flags;
// the low word carries behavior flags such as CRYPT_EXPORTABLE.
if not CryptGenKey(hProv, AT_KEYEXCHANGE,
(Cardinal(KeyBits) shl 16) or CRYPT_EXPORTABLE, hKey) then
Exit;
Τι συμβαίνει αν ξεχάσετε το CRYPT_EXPORTABLE;
Αφαιρέστε το CRYPT_EXPORTABLE από την ίδια τιμή flags και η CryptGenKey πετυχαίνει και πάλι, αλλά σημαίνει το παραγόμενο ιδιωτικό κλειδί ως μη εξαγώγιμο σε επίπεδο CSP. Οτιδήποτε ακολουθεί συνεχίζει επίσης να αναφέρει επιτυχία: η CertCreateSelfSignCertificate επιστρέφει ένα έγκυρο πλαίσιο πιστοποιητικού, και η PFXExportCertStoreEx, ακόμα κι αν κληθεί με EXPORT_PRIVATE_KEYS, πετυχαίνει ούτως ή άλλως και γράφει ένα αρχείο PFX που ανοίγει, αναλύεται, και φαίνεται εντελώς φυσιολογικό. Αυτό που δεν περιέχει είναι το ιδιωτικό κλειδί, γιατί το CSP αρνήθηκε να το αφήσει να βγει από το container κλειδιών, και η PFXExportCertStoreEx ποτέ δεν αντιμετωπίζει αυτή την άρνηση ως λόγο αποτυχίας ολόκληρης της εξαγωγής
Η αποτυχία εμφανίζεται μόνο αργότερα, και εντελώς αλλού: μια κλήση υπογραφής ανοίγει εκείνο το PFX, βρίσκει ένα πιστοποιητικό χωρίς συνημμένο ιδιωτικό κλειδί, και αναφέρει ακριβώς το σφάλμα που θα παίρνατε από ένα κατεστραμμένο ή λάθος PFX, όχι από μια λείπουσα σημαία τρία επίπεδα πιο πάνω. Όποιος κάνει debugging μόνο από την πλευρά της υπογραφής μπορεί να καταναλώσει ένα ολόκληρο απόγευμα στο λάθος αρχείο πριν συνειδητοποιήσει ότι το πραγματικό σφάλμα είναι ένα μοναδικό λείπον bit κατά τη δημιουργία κλειδιού, σε μια εντελώς διαφορετική κλήση συνάρτησης, πιθανόν σε ένα εντελώς διαφορετικό build script
Γιατί το ProvType πρέπει να ταιριάζει μεταξύ CryptAcquireContextW και του πιστοποιητικού;
Το ProvType πρέπει να ταιριάζει γιατί η CertCreateSelfSignCertificate επιλύει το ιδιωτικό κλειδί του νέου πιστοποιητικού μέσω μιας εγγραφής CRYPT_KEY_PROV_INFO, και ένα πεδίο σε εκείνη την εγγραφή, το ProvType, πρέπει να ονομάζει ακριβώς την ίδια τιμή τύπου CSP που πέρασε στην CryptAcquireContextW όταν ανοίχτηκε το container κλειδιών, PROV_RSA_AES, αριθμητικά 24, στην υλοποίηση του PDFlibPas. Ορίστε το ProvType σε μηδέν, ή σε οποιαδήποτε άλλη σταθερά παρόχου εκτός από αυτή στην οποία πράγματι ανήκει το container, και το πιστοποιητικό μπορεί ακόμα να δημιουργηθεί, αλλά ο καταγεγραμμένος σύνδεσμός του πίσω στο ιδιωτικό κλειδί δεν επιλύεται πλέον στο container που το κρατά, κάτι που εμφανίζεται αργότερα ως αποτυχία υπογραφής ή εξαγωγής που δεν έχει καμία σχέση με το πραγματικό κρυπτογραφικό περιεχόμενο του πιστοποιητικού
// The provider type used to open the key container must match the
// provider type recorded in the certificate's key-provider info.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, build the subject name blob, then:
KeyProvInfo.ProvType := PROV_RSA_AES; // same constant, both call sites
Συνδυάζοντας τα Πάντα: Από Container GUID σε PFX Προστατευμένο με Κωδικό
Η αλυσίδα κλήσεων μέσα στην PLCreateSelfSignedCertificate ακολουθεί μια ευθεία γραμμή: ανοίγει ένα καινούριο container κλειδιών ονομασμένο βάσει ενός πρόσφατα παραγμένου GUID ώστε ταυτόχρονες εκτελέσεις CI να μην συγκρούονται ποτέ πάνω σε ονόματα containers, παράγει το ζεύγος κλειδιών RSA μέσα σε αυτό με τις δύο σημαίες που καλύφθηκαν παραπάνω, κωδικοποιεί το SubjectName σε ένα blob ονόματος X.500 μέσω της CertStrToNameW, και καλεί την CertCreateSelfSignCertificate με ένα παράθυρο εγκυρότητας υπολογισμένο από το ValidDays και παραδομένο ως μια απλή δομή σχήματος SYSTEMTIME. Το πλαίσιο πιστοποιητικού που προκύπτει πηγαίνει σε ένα in-memory κατάστημα πιστοποιητικών ανοιγμένο με CertOpenStore και CERT_STORE_PROV_MEMORY, αποκλειστικά ώστε η PFXExportCertStoreEx να έχει από πού να εξάγει, αφού εκείνο το API λειτουργεί πάνω σε λαβή καταστήματος αντί για γυμνό πλαίσιο πιστοποιητικού
// Each call opens a throwaway container named after a fresh GUID:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, self-sign the certificate, export the PFX ...
// then delete the container once the PFX holds its own copy of the key:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_DELETEKEYSET);
Η ίδια η PFXExportCertStoreEx ακολουθεί τη συνηθισμένη σύμβαση δύο περασμάτων του Win32: καλέστε τη μία φορά με buffer μηδενικού μήκους για να μάθετε πόσα bytes χρειάζεται το PFX, εκχωρήστε τόσα, και μετά καλέστε την ξανά για να γεμίσετε τον buffer. Μόλις τα bytes βρεθούν στον δίσκο, το PDFlibPas διαγράφει το προσωρινό container κλειδιών με CRYPT_DELETEKEYSET αντί να το αφήσει πίσω, γιατί το PFX ήδη φέρει το δικό του αντίγραφο κάθε byte υλικού κλειδιού που κρατούσε το container. Παραλείψτε αυτόν τον καθαρισμό και κάθε κλήση στην PLCreateSelfSignedCertificate αφήνει ένα ορφανό container κλειδιών με όνομα GUID να κάθεται στο προφίλ του καλούντος χρήστη, ακριβώς το είδος διαρροής που ένας πράκτορας CI που τρέχει αυτή τη συνάρτηση σε κάθε build θα συσσωρεύσει επί μήνες προτού κάποιος το προσέξει
Είναι ασφαλές να χρησιμοποιηθεί ένα αυτο-υπογεγραμμένο πιστοποιητικό για υπογραφή παραγωγής;
Όχι: ένα αυτο-υπογεγραμμένο πιστοποιητικό είναι ασφαλές για την άσκηση μιας διαδρομής κώδικα υπογραφής και μη ασφαλές για μια υπογραφή που αναμένεται να εμπιστευτεί κάποιος εκτός της ομάδας, γιατί τίποτα δεν το συνδέει πίσω σε μια ρίζα που το λογισμικό ενός εξαρτώμενου μέρους ήδη εμπιστεύεται. Το φυσικό επόμενο βήμα για ένα PFX σαν αυτό είναι μια πραγματική κλήση υπογραφής, που καλύπτεται στο χτίσιμο ενός πάγκου εργασίας συμμόρφωσης και υπογραφής σε Delphi με το PDFlibPas, όπου ένα PFX χτισμένο έτσι οδηγεί το κομμάτι υπογραφής ενός pipeline που επίσης τρέχει preflight PDF/A και ελέγχους ByteRange. Η υπογραφή είναι όμως μόνο το μισό απ' ό,τι περιβάλλει ένα πιστοποιητικό, και το άλλο μισό είναι ακριβώς εκεί όπου ένα αυτο-υπογεγραμμένο leaf υποτίθεται ότι θα αποτύχει: η υπογραφή και επικύρωση PAdES σε Delphi με το PDFlibPas καλύπτει τους ελέγχους αλυσίδας εμπιστοσύνης που εκτελεί ένας επικυρωτής συμμόρφωσης, και ένας επικυρωτής που ανατρέχει την αλυσίδα πίσω σε μια έμπιστη ρίζα δεν έχει κανέναν λόγο να εμπιστευτεί ένα πιστοποιητικό που αυτή η συνάρτηση επινόησε πριν από πέντε λεπτά από το τίποτα
Η PLCreateSelfSignedCertificate είναι μία συνάρτηση ανάμεσα στα API πιστοποιητικών και υπογραφής της βιβλιοθήκης PDF PDFlibPas για Delphi και C++Builder, και υπάρχει ακριβώς για το κενό που περιγράφεται εδώ: μια δοκιμή υπογραφής που χρειάζεται ένα πραγματικό ζεύγος κλειδιών από πίσω της και τίποτα εξωτερικό για να το παράγει