Το HotPDF υπογράφει PDF έγγραφα με private key που κρατά απομακρυσμένη υπηρεσία Cloud Signature Consortium (CSC) μέσω του THPDFCSCSignatureProvider, ενός signature provider που οδηγεί το CSC API — credential info, εξουσιοδότηση, signatures/signHash και polling — ενώ η εφαρμογή Delphi σας προμηθεύει το HTTP transport και το access token OAuth. Το κλειδί δεν φεύγει ποτέ από το HSM της υπηρεσίας
Αυτό είναι όλο και περισσότερο ο μόνος τρόπος να αποκτήσετε καθόλου qualified κλειδί υπογραφής. Οι πάροχοι trust υπηρεσιών μοιράζουν CSC endpoint και OAuth client, όχι αρχείο PFX ή USB token, οπότε δεν υπάρχει τίποτα να φορτώσετε σε τοπικό certificate store όπως κάνει η υπογραφή Windows cert store μέσω CNG και CAPI. Η αφελής ενσωμάτωση αποτυγχάνει με προβλέψιμους τρόπους: μια κλήση signHash παίρνει timeout και το retry υπογράφει το ίδιο συμβόλαιο δύο φορές, ή μια παρτίδα σαράντα τιμολογίων πυροδοτεί σαράντα one-time κωδικούς επειδή κάθε hash εξουσιοδοτήθηκε ξεχωριστά. Τα περισσότερα από όσα κάνει ο provider είναι άμυνα απέναντι σε εκείνες τις δύο αποτυχίες
Γιατί αφήνει το HotPDF το HTTP στην εφαρμογή σας;
Επειδή το transport είναι ακριβώς εκεί όπου κάθε deployment διαφέρει. Proxies, TLS pinning, client πιστοποιητικά, εταιρικά OAuth vaults και πολιτική logging ζουν όλα στη στρώση HTTP, οπότε το THPDFCSCSignatureProvider ορχηστρώνει την κατάσταση πρωτοκόλλου και καλεί συνάρτηση THPDFCSCTransport για κάθε αίτημα. Ο provider σας παραδίδει THPDFCSCTransportRequest με Method (πάντα POST), το πλήρες URL χτισμένο από ServiceBaseURL συν το μονοπάτι endpoint, έτοιμο header Authorization bearer, ContentType, το JSON Body, IdempotencyKey, τον αριθμό Attempt και MaxResponseBytes. Γεμίζετε ένα THPDFCSCTransportResponse με StatusCode, Body και RetryAfterMS, και επιστρέφετε ένα από ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ή ctsCancelled
uses
System.Net.HttpClient, System.Net.URLClient, HPDFSignatureProvider,
HPDFCSCSignatureProvider;
function MakeCSCTransport(Client: THTTPClient): THPDFCSCTransport;
begin
Result :=
function(const Request: THPDFCSCTransportRequest;
out Response: THPDFCSCTransportResponse): THPDFCSCTransportStatus
var
Body: TStringStream;
Reply: TMemoryStream;
Headers: TNetHeaders;
HttpResp: IHTTPResponse;
begin
Response := Default(THPDFCSCTransportResponse);
Body := TStringStream.Create(string(Request.Body), TEncoding.UTF8);
Reply := TMemoryStream.Create;
try
Headers := [TNameValuePair.Create('Authorization', string(Request.Authorization)),
TNameValuePair.Create('Content-Type', string(Request.ContentType))];
if Request.IdempotencyKey <> '' then // όνομα header όπως το τεκμηριώνει η υπηρεσία σας
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // φασαρία socket ή DNS: επαναλήψιμο
end;
Response.StatusCode := HttpResp.StatusCode; // αναφέρετε το 503 ως έχει, μην το ταξινομήσετε
SetLength(Response.Body, Reply.Size);
if Reply.Size > 0 then
Move(Reply.Memory^, Response.Body[1], Reply.Size);
Response.RetryAfterMS := StrToIntDef(HttpResp.HeaderValue['Retry-After'], 0) * 1000;
Result := ctsSuccess;
finally
Reply.Free;
Body.Free;
end;
end;
end;
Ο ένας κανόνας που αξίζει να απομνημονεύσετε: επιστρέφετε ctsSuccess όποτε ένας server όντως απάντησε, ακόμα και με 503. Ο provider ταξινομεί μόνος του τους status codes, και transport που μετατρέπει 429 σε ctsPermanentFailure σιωπηλά απενεργοποιεί τη λογική retry που περιγράφεται παρακάτω. Ο constructor είναι αυστηρός προς την άλλη κατεύθυνση — πετάει EHPDFCSCSignatureProviderError όταν λείπει το transport, το CredentialID είναι κενό, δεν προμηθεύεται ούτε AccessToken ούτε token callback, ένα budget είναι εκτός ορίων, ή το ServiceBaseURL δεν είναι HTTPS. Σκέτο http:// γίνεται δεκτό μόνο με AllowInsecureHTTP, που ανήκει σε πειραματική διάταξη και πουθενά αλλού
Τι είναι το SAD και γιατί το πετάει το HotPDF μετά από μία χρήση;
Το THPDFCSCSignatureProvider μεταχειρίζεται τα Signature Activation Data (SAD) ως μίας χρήσης: καθαρίζονται από την κατάσταση του provider τη στιγμή που γίνεται δεκτό το signatures/signHash, ακόμα κι όταν η ίδια η υπογραφή φτάνει αργότερα μέσω ασύγχρονου polling. Το SAD είναι η απόδειξη της υπηρεσίας ότι ο υπογράφων ενέκρινε εκείνα τα συγκεκριμένα hashes, και ένα SAD που μένει στη μνήμη είναι εξουσιοδότηση που περιμένει να ξοδευτεί σε λάθος έγγραφο
Με τα defaults από THPDFCSCOptions.Default — RequireSAD και AutoAuthorize και τα δύο True — ο provider φορτώνει credentials/info μία φορά, ρωτά το THPDFCSCAuthenticationCallback σας για τις τιμές authData (ένα OTP, ένα PIN, ό,τι απαιτεί το block auth του credential), και στέλνει credentials/authorize. Ένα 200 κουβαλά το SAD ευθέως· ένα 202 κουβαλά handle που polled μέσω credentials/authorizeCheck έως MaxPollAttempts (60) φορές σε PollIntervalMS (250 ms). Το callback μπορεί να επιστρέψει το πολύ 32 τιμές, καθεμία με μη κενό ID έως 256 bytes και τιμή έως 4.096 bytes. Αν το δίκτυο πέσει πριν γίνει δεκτό το signHash, ένα αυτόματα αποκτηθέν SAD κρατιέται ώστε η ίδια παρτίδα να επαναληφθεί χωρίς να ρωτηθεί ξανά ο υπογράφων
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, async mode, 2 retries
Options.ServiceBaseURL := 'https://csc.example.com/csc/v2';
Options.CredentialID := 'contracts-signing-01';
Options.ClientData := 'invoice-run-2026-09';
Provider := THPDFCSCSignatureProvider.Create(Options, MakeCSCTransport(HttpClient),
function(ForceRefresh: Boolean; const OperationIdentifier: AnsiString;
out AccessToken: AnsiString; out ExpiresAtUTC: TDateTime): THPDFSignatureProviderStatus
begin
// ο OAuth client σας· το ForceRefresh είναι True αφού η υπηρεσία απάντησε 401
if not TokenVault.Acquire(ForceRefresh, AccessToken, ExpiresAtUTC) then
Exit(spsProviderError);
Result := spsValid;
end,
function(const CredentialID, CredentialInfoJSON, OperationIdentifier: AnsiString;
out Values: THPDFCSCAuthenticationValues): THPDFSignatureProviderStatus
var
Otp: string;
begin
if not AskSignerForOtp(Otp) then // το UI σας
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Ένα SAD που περνάτε οι ίδιοι μέσω Options.SAD συμπεριφέρεται διαφορετικά, και επίτηδες. Το HotPDF δεν μπορεί να ξέρει για ποια hashes εκδόθηκε, οπότε ο provider χρησιμοποιεί προεπιλεγμένο SAD μόνο για αίτημα ενός hash. Για παρτίδα με κλειστό AutoAuthorize, ο provider αποτυγχάνει με «CSC SAD is not pinned to the requested hash batch» αντί να μαντέψει
Πώς υπογράφει το SignHashBatch πολλά έγγραφα με μία εξουσιοδότηση;
Το SignHashBatch στέλνει ένα credentials/authorize και ένα signatures/signHash για έως MaxBatchSignatures (64) digests, και χτίζει και τα δύο bodies από τον ίδιο πίνακα ώστε numSignatures, η σειρά των hashes και το hashAlgorithmOID να είναι πανομοιότυπα στις δύο κλήσεις. Εκείνο το ταίριασμα είναι ό,τι απαιτεί το μοντέλο multisign του CSC. Κάντε λούπα τη μέθοδο Sign ενός hash σαράντα φορές και παίρνετε σαράντα εξουσιοδοτήσεις· στείλτε authorize και signHash που διαφωνούν και η υπηρεσία μπορεί να ξοδέψει το SAD απέναντι σε λάθος παρτίδα
Πριν από οποιαδήποτε κίνηση δικτύου, ο provider επικυρώνει την παρτίδα. Κάθε αίτημα πρέπει να είναι digest (sikDigest) 1 έως 1.024 bytes με digest OID, και όλα τα αιτήματα πρέπει να μοιράζονται ένα OID αλγορίθμου υπογραφής, ένα digest OID και, για RSASSA-PSS, ένα μήκος salt. Μια παρτίδα πολλών hashes φορτώνει επίσης credentials/info και επιστρέφει spsUnsupported όταν η τιμή multisign του credential είναι μικρότερη από την παρτίδα. Το SAD μετά καρφώνεται σε fingerprint παρτίδας — SHA-256 πάνω σε ετικέτα έκδοσης, το πλήθος και, ανά αίτημα, το OID αλγορίθμου, το digest OID, τον αλγόριθμο, το μήκος salt και τα bytes digest, το καθένα με πρόθεμα μήκους. Ανταλλάξτε δύο hashes και είναι διαφορετική παρτίδα που θέλει φρέσκια εξουσιοδότηση
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: τιμές SHA-256 που υπολογίσατε
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo παράγεται όταν το AlgorithmOID είναι κενό
Requests[I].DigestAlgorithmOID := '2.16.840.1.101.3.4.2.1';
Requests[I].InputKind := sikDigest;
Requests[I].Input := Digests[I];
end;
Status := Provider.SignHashBatch(Requests, 'invoices-2026-09-25-a', Signatures);
if Status <> spsValid then
raise Exception.CreateFmt('CSC batch failed (HTTP %d): %s',
[Provider.LastHTTPStatus, Provider.LastError]);
// Το Signatures[I] ανήκει στο Digests[I]· το πλήθος ελέγχθηκε απέναντι στο αίτημα
end;
Για RSASSA-PSS ο provider στέλνει επίσης signAlgoParams, μια δομή base64 DER RSASSA-PSS-params με τον αλγόριθμο hash, το MGF1 και το μήκος salt. Το χτίσιμο της σημαίνει κωδικοποίηση OIDs, και η έκδοση 2.748.5 διόρθωσε μια γωνιά εκεί: το X.690 §8.19.4 διπλώνει τα δύο πρώτα arcs σε μία τιμή (40 × πρώτο + δεύτερο), και κάτω από τη ρίζα 2 ένας δεύτερος arc πάνω από 39 σπρώχνει εκείνη την τιμή πέρα από 127, όπου θέλει τη μορφή πολλών bytes base-128 που προηγούμενα builds δεν εφάρμοζαν. Κανένα OID SHA-2 δεν επηρεάζεται — το 2.16 διπλώνει στο 96 — αλλά ένας παραμορφωμένος OID τώρα σηκώνει το δικό του error του provider αντί για EConvertError
Γιατί ένα επαναλαμβανόμενο αίτημα δεν παράγει δεύτερη υπογραφή;
Το THPDFCSCSignatureProvider κάνει κάθε επαναλήψιμη κλήση να κουβαλά ντετερμινιστικό idempotency key και cache-άρει ολοκληρωμένα αποτελέσματα, οπότε retry μετά από χαμένη απάντηση επιστρέφει τις πρωτότυπες υπογραφές αντί να ζητήσει νέες από το HSM. Το κλειδί είναι csc- ακολουθούμενο από το hex SHA-256 του αναγνωριστικού λειτουργίας και της φάσης, και η φάση ενσωματώνει το fingerprint της παρτίδας τόσο για εξουσιοδότηση όσο και για signHash. Το hashing αντί για περικοπή έχει σημασία: δύο μακρινά IDs λειτουργίας που μοιράζονται πρόθεμα θα συγκρούονταν κάτω από περικοπή, ενώ κλειδί σταθερού μήκους με βάση το περιεχόμενο μένει μοναδικό και σταθερό ανάμεσα σε προσπάθειες
Η πολιτική retry στο κοινό μονοπάτι αιτημάτων είναι στενή επίτηδες:
- Το HTTP 401 επιβάλλει ακριβώς ένα token refresh μέσω του callback access-token, μετά το αίτημα επαναλαμβάνεται μία φορά όταν έχει ανατεθεί callback access-token· δεύτερο 401 είναι τελικό
- Άλλες απαντήσεις 4xx και
ctsPermanentFailureτελειώνουν την κλήση μεspsProviderError, και τοerror_descriptionτης υπηρεσίας καταλήγει στοLastError - Τα 408, 429, 5xx και
ctsTemporaryFailureεπαναλαμβάνονται έωςRetryLimit(default 2), περιμένονταςRetry-AfterήRetryBaseDelayMS× 2προσπάθεια (βάση 100 ms), περικομμένα σταMaxRetryAfterMS(5.000 ms) - Οι αναμονές τρέχουν σε φέτες 25 ms που ελέγχουν το
Cancel, οπότε χρήστης που ματαιώνει δεν κάθεται σε πενταδευτερόλεπτο back-off - Το
signHashεπαναλαμβάνεται μόνο όσο τοEnableIdempotencyείναι ανοιχτό· κλείστε το και timeout μετά την υποβολή είναι τελικό, επειδή κανείς δεν μπορεί να πει αν το κλειδί είχε ήδη χρησιμοποιηθεί
Η ασύγχρονη υπογραφή (operationMode «A», το default) προσθέτει ακόμα έναν φρουρό: το responseID αποθηκεύεται πριν το polling στο signatures/signPolling, οπότε επαναλαμβανόμενη κλήση με το ίδιο αναγνωριστικό λειτουργίας συνεχίζει polling αντί να ξαναστείλει. Ολοκληρωμένες παρτίδες κάθονται σε cache κλειδωμένη κατά αναγνωριστικό λειτουργίας, credential και fingerprint, περικομμένη από MaxOperationCacheEntries (128) και επιστρεφόμενη ως deep copies. Εκείνη η cache ζει στο instance του provider και δεν επιζεί επανεκκίνησης. Το idempotency key επιζεί, επειδή παράγεται αντί να είναι τυχαίο, οπότε ξαναρχίσα διαδικασία που ξαναχρησιμοποιεί το αναγνωριστικό λειτουργίας της στέλνει το ίδιο κλειδί — αν η υπηρεσία κάνει deduplication πάνω του είναι υπόσχεση της υπηρεσίας, όχι του HotPDF
Πώς βάζετε υπογραφή CSC μέσα σε PDF;
Περάστε τον provider στο HPDFCMSSignPDFStreamWithProvider μαζί με το πιστοποιητικό end-entity από το GetCertificateChain· το HotPDF χτίζει το CMS SignedData και ο provider υπογράφει τον digest των signed attributes. Το PDF εισόδου θέλει το placeholder /ByteRange και /Contents που γράφει το THPDFPage.AddSignedSignatureField, ακριβώς όπως στο workflow υπογραφής PAdES στο HotPDF, και το μοντέλο provider είναι το ίδιο που καλύπτει το HotPDF pluggable signature providers για ML-DSA και EdDSA
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // Το CSC απαριθμεί πρώτο το πιστοποιητικό end-entity
if Length(Chain) = 0 then
raise Exception.Create('Credential returned no certificate');
SignOpts := HPDFCMSDefaultOptions(palBaseline_B_B);
SignOpts.DigestAlgorithm := cmsdaSHA256;
SignOpts.SignatureScheme := cmsRSAPKCS1v15;
Src := TFileStream.Create('contract-unsigned.pdf', fmOpenRead or fmShareDenyWrite);
Dst := TFileStream.Create('contract-signed.pdf', fmCreate);
try
if not HPDFCMSSignPDFStreamWithProvider(Src, Dst, Chain[0], Provider, '', SignOpts) then
raise Exception.Create('PDF signing failed');
finally
Dst.Free;
Src.Free;
end;
end;
Το μέγεθος του placeholder /Contents περνά από το EstimateSignatureSize, που επιστρέφει EstimatedSignatureBytes όταν το ορίσετε και αλλιώς το μέγεθος modulus RSA από το μήκος κλειδιού του credential. Για ECDSA ορίστε EstimatedSignatureBytes μόνοι σας, ή η εκτίμηση αναφέρει spsUnsupported. Η παραλλαγή υπογραφής αυτόματου μεγέθους ξανα-υπογράφει όταν αποδειχτεί ότι ένα placeholder είναι πολύ μικρό, και το κάνει μόνο για providers που διαφημίζουν spcSafeSignRetry — που το THPDFCSCSignatureProvider το κάνει μόνο όσο το EnableIdempotency είναι ανοιχτό. Για workflows PAdES-B-T, το TimestampDigest ζητά timestamp token από την ίδια υπηρεσία μέσω signatures/timestamp, περικομμένο στα MaxTimestampBytes (1 MB)
Τι δεν κάνει ο CSC provider;
Δεν υπογράφει μηνύματα, μόνο digests. Τα Ed25519 και Ed448 σε pure mode δίνουν στον provider ολόκληρο μήνυμα signed-attributes (sikMessage), και ο validator παρτίδας το απορρίπτει ως παραμορφωμένο, επειδή το signHash είναι εκ φύσεως βασισμένο σε hash. Η μονάδα provider μεταγλωττίζεται κάτω από Free Pascal με σκέτους τύπους συναρτήσεων στη θέση anonymous methods, αλλά οι CMS builders με provider σηκώνουν εξαίρεση κάτω από FPC σήμερα, οπότε η ενσωμάτωση υπογραφής CSC σε PDF είναι μονοπάτι Delphi
Δεν αποφασίζει ούτε πολιτική. Το CredentialInfo αναφέρει την κατάσταση κλειδιού, την κατάσταση πιστοποιητικού, τη λειτουργία εξουσιοδότησης, το επίπεδο SCAL και το όριο multisign, αλλά ο provider δεν θα αρνηθεί μόνος του απενεργοποιημένο κλειδί ή credential SCAL1 — ελέγξτε εκείνα πριν δείξετε στον υπογράφοντα το prompt OTP. Και ένα instance provider υπογράφει μία παρτίδα τη φορά: το SignHashBatch σειριοποιείται εσωτερικά ώστε δύο threads να μην τρέχουν για ένα SAD, που σημαίνει ότι η απόδοση έρχεται από παρτίδες, όχι από μοίρασμα provider ανάμεσα σε worker threads. Αν η προκύψασα υπογραφή είναι qualified εξαρτάται από την trust υπηρεσία και το credential της, όχι από τη βιβλιοθήκη που μετέφερε εκεί το hash
Ο CSC provider, οι builders CMS και PAdES και οι τοπικοί και PKCS#11 providers παραδίδονται όλα στο HotPDF Delphi PDF component