HotPDF menandatangani dokumen PDF dengan private key yang dipegang layanan Cloud Signature Consortium (CSC) jarak jauh lewat THPDFCSCSignatureProvider, signature provider yang menggerakkan CSC API — credential info, otorisasi, signatures/signHash, dan polling — sementara aplikasi Delphi Anda menyediakan transport HTTP dan OAuth access token. Key-nya tak pernah meninggalkan HSM milik layanan
Itu makin hari makin menjadi satu-satunya cara mendapat signing key yang qualified. Trust service provider menyerahkan endpoint CSC dan OAuth client, bukan file PFX atau token USB, jadi tak ada yang bisa dimuat ke certificate store lokal sebagaimana dilakukan penandatanganan cert store Windows lewat CNG dan CAPI. Integrasi yang naif gagal dengan cara yang bisa diduga: panggilan signHash timeout dan retry-nya menandatangani kontrak yang sama dua kali, atau satu batch empat puluh faktur memicu empat puluh one-time password karena setiap hash diotorisasi terpisah. Sebagian besar apa yang dilakukan provider ini adalah bertahan dari dua kegagalan itu
Kenapa HotPDF menyerahkan HTTP ke aplikasi Anda?
Karena transport justru tempat setiap deployment berbeda. Proxy, TLS pinning, client certificate, vault OAuth korporat, dan kebijakan logging semuanya tinggal di lapisan HTTP, jadi THPDFCSCSignatureProvider mengorkestrasi state protokol dan memanggil fungsi THPDFCSCTransport untuk setiap request. Provider menyerahkan THPDFCSCTransportRequest ke Anda dengan Method (selalu POST), URL lengkap yang dibangun dari ServiceBaseURL plus path endpoint-nya, header bearer Authorization yang siap pakai, ContentType, Body JSON, IdempotencyKey, nomor Attempt, dan MaxResponseBytes. Anda mengisi THPDFCSCTransportResponse dengan StatusCode, Body, dan RetryAfterMS, lalu mengembalikan salah satu dari ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure, atau 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 // nama header sebagaimana didokumentasikan layanan Anda
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // masalah socket atau DNS: bisa di-retry
end;
Response.StatusCode := HttpResp.StatusCode; // laporkan 503 apa adanya, jangan diklasifikasi
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;
Satu aturan yang layak dihafal: kembalikan ctsSuccess setiap kali server benar-benar menjawab, bahkan dengan 503. Provider mengklasifikasikan status code sendiri, dan transport yang mengubah 429 menjadi ctsPermanentFailure diam-diam mematikan logika retry yang dijelaskan di bawah. Constructor-nya ketat ke arah sebaliknya — ia melempar EHPDFCSCSignatureProviderError ketika transport hilang, CredentialID kosong, tak ada AccessToken maupun callback token yang diberikan, budget di luar rentang, atau ServiceBaseURL bukan HTTPS. http:// polos diterima hanya dengan AllowInsecureHTTP, yang tempatnya di test rig dan tak di mana pun selain itu
Apa itu SAD, dan kenapa HotPDF membuangnya setelah sekali pakai?
THPDFCSCSignatureProvider memperlakukan Signature Activation Data (SAD) sebagai sekali pakai: ia dibersihkan dari state provider begitu signatures/signHash diterima, bahkan ketika signature-nya sendiri datang belakangan lewat polling asinkron. SAD adalah bukti dari layanan bahwa signer menyetujui hash-hash khusus ini, dan SAD yang menganggur di memori adalah otorisasi yang menunggu dibelanjakan untuk dokumen yang salah
Dengan bawaan dari THPDFCSCOptions.Default — RequireSAD dan AutoAuthorize keduanya True — provider memuat credentials/info sekali, meminta nilai authData dari THPDFCSCAuthenticationCallback Anda (OTP, PIN, apa pun yang diminta blok auth milik credential), dan mengirim POST credentials/authorize. Jawaban 200 membawa SAD langsung; jawaban 202 membawa handle yang di-polling lewat credentials/authorizeCheck sampai MaxPollAttempts (60) kali pada PollIntervalMS (250 ms). Callback boleh mengembalikan paling banyak 32 nilai, masing-masing dengan ID tak kosong sampai 256 byte dan nilai sampai 4.096 byte. Kalau jaringan putus sebelum signHash diterima, SAD yang diperoleh otomatis dipertahankan supaya batch yang sama bisa di-retry tanpa menanyakan signer lagi
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, mode asinkron, 2 retry
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 Anda; ForceRefresh bernilai True setelah layanan menjawab 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 Anda
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
SAD yang Anda kirim sendiri lewat Options.SAD berperilaku berbeda, dan memang sengaja. HotPDF tak mungkin tahu SAD itu diterbitkan untuk hash mana, jadi provider memakai SAD preset hanya untuk request satu hash. Untuk batch dengan AutoAuthorize dimatikan, provider gagal dengan "CSC SAD is not pinned to the requested hash batch" alih-alih menduga-duga
Bagaimana SignHashBatch menandatangani banyak dokumen dengan satu otorisasi?
SignHashBatch mengirim satu credentials/authorize dan satu signatures/signHash untuk sampai MaxBatchSignatures (64) digest, dan membangun kedua body dari array yang sama sehingga numSignatures, urutan hashes, dan hashAlgorithmOID identik di kedua panggilan. Kecocokan itulah yang disyaratkan model multisign CSC. Loop method Sign satu hash empat puluh kali dan Anda kebagian empat puluh otorisasi; kirim authorize dan signHash yang tak cocok dan layanannya bisa menghabiskan SAD untuk batch yang salah
Sebelum trafik jaringan apa pun, provider memvalidasi batch-nya. Setiap request harus berupa digest (sikDigest) berukuran 1 sampai 1.024 byte dengan OID digest, dan semua request harus berbagi satu OID algoritma signature, satu OID digest, dan, untuk RSASSA-PSS, satu panjang salt. Batch multi-hash juga memuat credentials/info dan mengembalikan spsUnsupported ketika nilai multisign milik credential lebih kecil dari batchnya. SAD lalu dipatok ke fingerprint batch — SHA-256 atas label versi, hitungannya, dan per request: OID algoritma, OID digest, algoritma, panjang salt, dan byte digest, masing-masing berawalan panjang. Tukar dua hash dan itu batch berbeda yang butuh otorisasi segar
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: nilai SHA-256 yang Anda hitung
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo diturunkan saat AlgorithmOID kosong
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] milik Digests[I]; hitungannya sudah dicek terhadap request
end;
Untuk RSASSA-PSS provider juga mengirim signAlgoParams, struktur DER RSASSA-PSS-params ber-base64 dengan algoritma hash, MGF1, dan panjang salt. Membangunnya berarti mengenkode OID, dan versi 2.748.5 memperbaiki satu sudut dari itu: X.690 §8.19.4 melipat dua arc pertama menjadi satu nilai (40 × arc pertama + arc kedua), dan di bawah akar 2, arc kedua di atas 39 mendorong nilai itu melewati 127, tempat ia butuh bentuk multi-byte basis-128 yang build sebelumnya tak terapkan. Tak ada OID SHA-2 yang terdampak — 2.16 melipat menjadi 96 — tapi OID yang malformed kini melempar error milik provider sendiri alih-alih EConvertError
Kenapa request yang di-retry tidak menghasilkan signature kedua?
THPDFCSCSignatureProvider membuat setiap panggilan yang bisa di-retry membawa idempotency key deterministik dan meng-cache hasil yang selesai, jadi retry setelah respons yang hilang mengembalikan signature aslinya alih-alih meminta yang baru dari HSM. Key-nya adalah csc- disusul hex SHA-256 dari identifier operasi dan phase-nya, dan phase itu menyematkan fingerprint batch untuk otorisasi maupun signHash. Meng-hash alih-alih memotong itu penting: dua ID operasi panjang yang berbagi prefiks akan bertabrakan di bawah pemotongan, sementara key beralamat konten berpanjang tetap unik dan stabil lintas percobaan
Kebijakan retry di jalur request bersama sengaja dibuat sempit:
- HTTP 401 memaksa tepat satu refresh token lewat callback access-token, lalu request diulang sekali ketika callback access-token di-assign; 401 kedua bersifat final
- Respons 4xx lainnya dan
ctsPermanentFailuremengakhiri panggilan denganspsProviderError, danerror_descriptionmilik layanan mendarat diLastError - 408, 429, 5xx, dan
ctsTemporaryFailuredi-retry sampaiRetryLimit(bawaan 2), menungguRetry-AfteratauRetryBaseDelayMS× 2attempt (basis 100 ms), dipatokMaxRetryAfterMS(5.000 ms) - Penungguan berjalan dalam irisan 25 ms yang memeriksa
Cancel, jadi pengguna yang membatalkan tak perlu menunggu back-off lima detik signHashhanya di-retry selamaEnableIdempotencyaktif; matikan itu dan timeout setelah pengiriman bersifat final, karena tak ada yang bisa memastikan key-nya sudah terpakai atau belum
Penandatanganan asinkron (operationMode "A", bawaannya) menambah satu penjaga lagi: responseID disimpan sebelum mem-polling signatures/signPolling, jadi panggilan ulang dengan identifier operasi yang sama melanjutkan polling alih-alih mengirim ulang. Batch yang selesai menetap di cache berkunci identifier operasi, credential, dan fingerprint, dipatok MaxOperationCacheEntries (128) dan dikembalikan sebagai salinan dalam. Cache itu tinggal di instance provider dan tak selamat dari restart. Idempotency key-nya selamat, karena diturunkan alih-alih diacak, jadi proses yang di-restart dan memakai ulang identifier operasinya mengirim key yang sama — apakah layanannya mendeduplikasi atasnya adalah janji si layanan, bukan janji HotPDF
Bagaimana memasukkan signature CSC ke dalam PDF?
Kirimkan provider ke HPDFCMSSignPDFStreamWithProvider bersama sertifikat end-entity dari GetCertificateChain; HotPDF membangun CMS SignedData dan provider menandatangani digest dari signed attributes. PDF input-nya butuh placeholder /ByteRange dan /Contents yang ditulis THPDFPage.AddSignedSignatureField, persis seperti di alur kerja penandatanganan PAdES di HotPDF, dan model provider-nya adalah yang sama dengan yang dibahas di signature provider pluggable HotPDF untuk ML-DSA dan EdDSA
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSC mendaftar sertifikat end-entity lebih dulu
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;
Menentukan ukuran placeholder /Contents lewat EstimateSignatureSize, yang mengembalikan EstimatedSignatureBytes ketika Anda menyetelnya dan bila tidak berupa ukuran modulus RSA dari panjang key milik credential. Untuk ECDSA, setel EstimatedSignatureBytes sendiri, atau estimasinya melaporkan spsUnsupported. Varian penandatanganan auto-size menandatangani ulang ketika placeholder ternyata terlalu kecil, dan itu hanya ia lakukan untuk provider yang mengiklankan spcSafeSignRetry — yang dilakukan THPDFCSCSignatureProvider hanya selama EnableIdempotency aktif. Untuk alur kerja PAdES-B-T, TimestampDigest meminta timestamp token dari layanan yang sama lewat signatures/timestamp, dipatok MaxTimestampBytes (1 MB)
Apa yang tidak dilakukan provider CSC ini?
Ia tidak menandatangani pesan, hanya digest. Ed25519 dan Ed448 dalam mode pure menyerahkan seluruh pesan signed-attributes ke provider (sikMessage), dan validator batch menolaknya sebagai malformed, karena signHash memang berbasis hash sejak definisinya. Unit provider ter-compile di Free Pascal dengan tipe fungsi biasa menggantikan anonymous method, tapi builder CMS penggerak provider melempar exception di FPC saat ini, jadi menyematkan signature CSC ke PDF adalah jalur Delphi
Ia juga tak menentukan kebijakan. CredentialInfo melaporkan status key, status sertifikat, mode otorisasi, level SCAL, dan batas multisign, tapi provider tak akan menolak key yang dinonaktifkan atau credential SCAL1 dengan sendirinya — periksa itu sebelum Anda menampilkan prompt OTP kepada signer. Dan satu instance provider menandatangani satu batch pada satu waktu: SignHashBatch diserialisasi secara internal supaya dua thread tak bisa berebut satu SAD, artinya throughput datang dari batching, bukan dari berbagi provider lintas worker thread. Apakah signature hasilnya qualified bergantung pada trust service dan credential-nya, bukan pada library yang mengantarkan hashnya ke sana
Provider CSC, builder CMS dan PAdES, serta provider lokal dan PKCS#11 semuanya ikut terkirim di HotPDF Delphi PDF component