Teknik Makale

HotPDF CSC uzaktan imzalama: Delphi'de bulut PDF imzaları

Uzak bir imzalama servisine giden bir signatures/signHash isteği, HSM imzayı çoktan üretmişken zaman aşımına uğrayabilir ve client, çizginin hangi tarafına düştüğünü bilemez. Körce yeniden deneyin; bir onay aynı sözleşmeye iki imza verebilir. Yeniden denemeyi reddedin; gece yarısı bir toplu iş tek bir 503'te ölür. Kırk faturayı birer hash hâlinde imzalayın; imzacı kırk tek kullanımlık parola yazar

Herhangi bir Cloud Signature Consortium (CSC) entegrasyonu ikisini de çözmek zorundadır. Nitelikli anahtar dağıtan trust service sağlayıcıları gittikçe bir PFX dosyası ya da USB token yerine bir CSC API ucu ile bir OAuth client veriyor; böylece CNG ile CAPI üzerinden Windows cert store imzalamasının çalıştığı gibi yerel bir depoya aktarılacak hiçbir şey kalmaz. Anahtar, sağlayıcının HSM'inde kalır ve her imza HTTPS üzerinden bir gidiş dönüşür

HotPDF HTTP'yi neden uygulamanıza bırakır?

HotPDF, uzak bir CSC servisi üzerinden THPDFCSCSignatureProvider ile imzalar; o signature provider, CSC v2 protokolünü (credential info, authorization, signatures/signHash, polling) koştururken HTTP transport ile OAuth token sizin Delphi kodunuzda kalır. Proxy'ler, TLS pinning, client sertifikaları, token kasaları ve istek loglaması hepsi HTTP katmanında yaşar ve her deployment'ta farklıdırlar

Her istek için provider bir THPDFCSCTransportRequest kurar ve sizin THPDFCSCTransport fonksiyonunuzu çağırır. Record, tam URL'yi, hazır bir Authorization bearer başlığını, JSON Body'yi, bir IdempotencyKey'i, OperationIdentifier'ı ve Attempt numarasını taşır. Fonksiyonunuz bir THPDFCSCTransportResponse'u StatusCode, Body ile RetryAfterMS doldurur ve sonra ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ya da ctsCancelled döndürür

HotPDF CSC transport sınırı şeması: THPDFCSCSignatureProvider protokolü orkestra eder ve kodunuza POST metodu, tam URL, hazır Authorization bearer başlığı, JSON body, IdempotencyKey ve deneme numarası taşıyan bir THPDFCSCTransportRequest verir; siz StatusCode, Body, RetryAfterMS artı dört cts durum değerinden birini döndürürsünüz ve anahtar HSMden asla çıkmaz
Sağlayıcı durum kodlarını kendisi sınıflandırır; cevaplanmış bir 503'ü kalıcı başarısızlığa çeviren bir transport, yeniden deneme mantığını sessizce etkisiz bırakır, oysa proxy'ler ve TLS politikası size ait kodda kalır

RTL'in THTTPClient'ı üzerine kurulu bir transport kabaca kırk satır ister. Body ham bayt olarak çıkar; JSON hiçbir code-page dönüşümünden geçmez:

uses
  System.SysUtils, System.Classes, 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, Reply: TMemoryStream;
      Headers: TNetHeaders;
      HttpResp: IHTTPResponse;
    begin
      Response := Default(THPDFCSCTransportResponse);
      Body := TMemoryStream.Create;
      Reply := TMemoryStream.Create;
      try
        if Request.Body <> '' then
          Body.WriteBuffer(Request.Body[1], Length(Request.Body));
        Body.Position := 0;
        Headers := [TNameValuePair.Create('Authorization', string(Request.Authorization)),
                    TNameValuePair.Create('Content-Type', string(Request.ContentType))];
        if Request.IdempotencyKey <> '' then   // başlık adı, servisinizin belgelediği gibi
          Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
            string(Request.IdempotencyKey))];
        try
          HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
        except
          on ENetHTTPClientException do
            Exit(ctsTemporaryFailure);            // DNS, TLS ya da socket derdi: yeniden denenebilir
        end;
        Response.StatusCode := HttpResp.StatusCode; // 429 ile 503'ü dokunulmadan geçir
        SetLength(Response.Body, Reply.Size);
        if Reply.Size > 0 then
          Move(Reply.Memory^, Response.Body[1], Reply.Size);
        // Saniye cinsinden Retry-After; HTTP-date değeri burada 0'a döner
        Response.RetryAfterMS := StrToIntDef(HttpResp.HeaderValue['Retry-After'], 0) * 1000;
        Result := ctsSuccess;
      finally
        Reply.Free;
        Body.Free;
      end;
    end;
end;

Ezberlenecek tek kural: bir sunucu cevap verdiyse — 503 bile olsa — ctsSuccess döndürün. Provider durum kodlarını kendisi sınıflandırır ve bir 429'u ctsPermanentFailure'a eşleyen bir transport, aşağıda anlatılan yeniden deneme mantığını kapatır. MaxResponseBytes'i (varsayılanı 2 MB) aşan body'ler, transport ne döndürürse döndürsün spsMalformed ile reddedilir

Konstrüktör öteki yönde katıdır. Transport eksikse, ServiceBaseURL ya da CredentialID boşsa, ne bir AccessToken ne bir token callback verilmişse, bir bütçe aralık dışındaysa ya da URL HTTPS değilse EHPDFCSCSignatureProviderError raise eder. Düz http:// yalnızca AllowInsecureHTTP set edildiğinde geçer ve o bayrak, yerel bir mock service'e yöneltilmiş test düzeneğine aittir

SAD nedir ve neden tek kullanımlıktır?

THPDFCSCSignatureProvider, Signature Activation Data'yı (SAD) tek kullanımlık sayar: signatures/signHash kabul edildiği anda provider durumundan temizlenir; imzanın kendisi sonradan asenkron polling ile gelse bile. SAD, imzacının tam da o hash'leri onayladığının servise kanıtıdır. Bellekte asılı kalan bir SAD, yanlış belgeye harcanmayı bekleyen bir yetkilendirmedir

THPDFCSCOptions.Default'un varsayılanlarıyla (RequireSAD ile AutoAuthorize ikisi de True) provider, credentials/info'u bir kez yükler, credential'ın auth bloğunun istediği authData değerlerini — genelde bir OTP ya da PIN — sizin THPDFCSCAuthenticationCallback'inizden ister ve credentials/authorize yollar. Bir 200 cevabı SAD'ı doğrudan taşır. Bir 202 bir handle taşır; provider onu credentials/authorizeCheck üzerinden, servis Retry-After göndermedikçe MaxPollAttempts (60) kez, PollIntervalMS (250 ms) arayla yoklar. Callback en fazla 32 değer döndürebilir; her biri 256 bayta dek boş olmayan bir ID ve 4.096 bayta dek bir değerle

Temizleme kabulden sonra olur. Ağ, signHash kabul edilmeden düşerse otomatik alınmış bir SAD yerinde kalır, hâlâ aynı toplu işe bağlıdır; yeniden deneme ikinci bir OTP sorusu olmadan çıkar. İmzacı bir kez onaylamıştır; provider o onayı bir kez harcar

HotPDF SAD yaşam döngüsü şeması: RequireSAD ve AutoAuthorize ile sağlayıcı credentials/infoyu bir kez yükler, kimlik doğrulama callbackine OTP ya da PIN değerlerini sorar, credentials/authorize gönderir, cevap 202 olduğunda credentials/authorizeCheck'i 250 ms arayla 60 kez yoklar ve Signature Activation Data'yı signatures/signHash kabul edildiği anda temizler; kabulden önce ağ düştüyse elde edilen SAD saklanır
Bellekte asılı kalan bir SAD, yanlış belgeye harcanmayı bekleyen bir yetkilendirmedir ve seçeneklerle geçilen hazır bir SAD yalnızca tek hashli istek için kullanılır
var
  Options: THPDFCSCOptions;
  Provider: THPDFCSCSignatureProvider;
begin
  Options := THPDFCSCOptions.Default;   // RequireSAD, AutoAuthorize, async mod, 2 yeniden deneme
  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
      // TokenVault sizin kendi OAuth client'ınız; 401'den sonra ForceRefresh True'dur
      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   // kendi UI'niz
        Exit(spsCancelled);
      SetLength(Values, 1);
      Values[0].ID := 'otp';
      Values[0].Value := AnsiString(Otp);
      Result := spsValid;
    end);

Token callback, önbellekteki token eksikse ya da bildirdiğiniz sona ermeye TokenRefreshSkewSeconds (30) kala tetiklenir; sıfır bir ExpiresAtUTC, token'in 401 yenilemeyi dayatana dek yeniden kullanıldığı demektir

Kendiniz Options.SAD üzerinden verdiğiniz bir SAD başka davranır, bilerek. Hiçbir şey provider'a o SAD'ın hangi hash'leri kapsadığını söylemez; o yüzden önceden set edilmiş bir SAD yalnızca tek hash'li bir istek için kullanılır. AutoAuthorize açıkken çok hash'li bir toplu işte ön set değerini atar ve toplu işi yetkilendirir; AutoAuthorize kapalıyken tahmin etmek yerine "CSC SAD is not pinned to the requested hash batch" ile başarısız olur. Yetkilendirmeyi yalnızca OAuth scope ile yapan ve hiç SAD üretmeyen servisler RequireSAD False set edilerek çalışır; o hâlde SAD üyesi signHash body'sinden çıkarılır

Tek bir OTP sorusuyla kırk fatura imzalamak

SignHashBatch, 64'e kadar (MaxBatchSignatures) özet için bir credentials/authorize ve bir signatures/signHash yollar ve her iki body'yi aynı diziden kurar; numSignatures, hashes sırası ile hashAlgorithmOID iki çağrı arasında uyuşur. Tek hash'li Sign metodunu kırk kez döndürün; kırk yetkilendirme alırsınız. Birbirini dinlemeyen bir authorize ile bir signHash yollarsanız servis, SAD'ı yanlış toplu işe harcayabilir

Doğrulama, herhangi bir ağ trafiğinden önce koşar. Her istek, bir özet OID'iyle 1 ile 1.024 bayt arası bir özet (sikDigest) olmalı ve bütün istekler tek bir imza algoritması, tek bir özet OID'i ve RSASSA-PSS için tek bir salt uzunluğu paylaşmalı. Çok hash'li bir toplu iş ayrıca credentials/info'u yükler ve credential'ın multisign değeri toplu iş boyutundan küçükse spsUnsupported döndürür. Yetkilendirme, istek listesinin sıra duyarlı bir SHA-256 parmak izine bağlanır; iki hash'i takas etmek, taze bir onay isteyen başka bir toplu iş kurar

var
  Requests: THPDFCSCSignatureRequests;
  Signatures: THPDFCSCSignatures;
  Status: THPDFSignatureProviderStatus;
  OpId: AnsiString;
  I: Integer;
begin
  SetLength(Requests, Length(Digests));      // Digests: sizin hesapladığınız SHA-256 değerleri
  for I := 0 to High(Digests) do
  begin
    Requests[I] := Default(THPDFSignatureProviderRequest);
    Requests[I].Algorithm := hsaRSAPKCS1v15;   // AlgorithmOID boşken signAlgo türetilir
    Requests[I].DigestAlgorithmOID := '2.16.840.1.101.3.4.2.1';
    Requests[I].InputKind := sikDigest;
    Requests[I].Input := Digests[I];
  end;

  // ID'yi kendi koşu kaydınızdan türetin, asla çağrı başına taze bir GUID'den değil
  OpId := AnsiString('invoices-' + RunRecord.BatchNumber);
  Status := Provider.SignHashBatch(Requests, OpId, Signatures);
  if Status = spsProviderError then
    // Aynı OpId: tamamlanmış toplu iş önbellekten döner, bekleyen async
    // iş polling'ü sürdürür, gönderilmemiş istek aynı anahtarı yeniden kullanır
    Status := Provider.SignHashBatch(Requests, OpId, Signatures);
  if Status <> spsValid then
    raise Exception.CreateFmt('CSC batch failed (HTTP %d): %s',
      [Provider.LastHTTPStatus, Provider.LastError]);
  // Signatures[I], Digests[I]'ye aittir; sayı istekle karşı kontrol edildi
end;

RSASSA-PSS için provider, hash algoritmasını, MGF1 ile salt uzunluğunu taşıyan base64 DER bir RSASSA-PSS-params yapısı olan signAlgoParams'i ekler. X.690 §8.19.4 ilk iki OID yayını tek değere katlar (40 × birinci + ikinci) ve 2 kökü altında 39'u aşan ikinci bir yayın değeri 127'yi aşar; base-128 çok baytlı biçim gerekir. Hiçbir SHA-2 OID'i o yolu kullanmaz, çünkü 2.16 96'ya katlanır; bir encoder her SHA-2 testini geçip yine de yanlış olabilir. Provider çok baytlı biçimi yazar ve bozuk bir OID'i bir EConvertError yerine kendi EHPDFCSCSignatureProviderError'ı olarak bildirir

Yeniden denenen bir istek neden ikinci bir imza üretmez?

THPDFCSCSignatureProvider her yeniden denenebilir çağrıya deterministik bir idempotency anahtarı verir ve tamamlanmış sonuçları önbellekler; kaybolmuş bir cevaptan sonraki yeniden deneme, HSM'den yenilerini istemek yerine orijinal imzaları döndürür. Anahtar, csc- ile işlem tanımlayıcısının ve protokol fazının hex SHA-256'sıdır ve authorization ile signHash için faz, toplu iş parmak izini içerir. Kırpmak yerine hash'lemek anahtarı sabit uzunlukta tutar ve önek paylaşan iki uzun işlem ID'sinin çarpışmasını önler

Paylaşılan istek yolundaki yeniden deneme politikası dardır:

  • HTTP 401, access-token callback'i üzerinden tam olarak bir token yenilemesi dayatır ve istek bir kez yinelenir; ikinci bir 401 kesindir, token callback yoksa ilki kesindir
  • Öteki 4xx cevapları ile ctsPermanentFailure çağrıyı spsProviderError ile bitirir ve servisin error_description'ı (ya da error'ı) LastError'a düşer
  • 408, 429, 5xx ile ctsTemporaryFailure, RetryLimit'e (varsayılan 2) dek yeniden denenir; Retry-After beklenir ya da her denemede ikilenen RetryBaseDelayMS (100 ms) beklenir, MaxRetryAfterMS (5.000 ms) ile sınırlı
  • Bekleyişler Cancel'i sınayan 25 ms dilimlerinde işler; vazgeçen bir kullanıcı beş saniyelik bir geri çekilmede oturmaz
  • signHash yalnızca EnableIdempotency açıkken yeniden denenir; kapatın, gönderimden sonraki bir timeout kesindir; çünkü hash'in çoktan imzalanıp imzalanmadığını kimse söyleyemez
HotPDF yeniden deneme politikası şeması: yeniden denenebilir her çağrı, işlem tanımlayıcısı ve fazdan hashlenen deterministik bir csc- idempotency anahtarı taşır; HTTP 401 tam olarak bir token yenilemesi dayatır, diğer 4xx cevapları spsProviderError ile biter ve 408, 429, 5xx ya da geçici transport başarısızlığı, Retry-After ya da 5.000 ms ile sınırlandırılmış üstel backoff bekleyerek RetryLimit 2'ye kadar yeniden denenir
Tamamlanan partiler işlem tanımlayıcısı, credential ve parmak iziyle önbelleklenir ve asenkron modda saklanan responseID, yinelenen bir çağrının hashi yeniden göndermek yerine yoklamayı sürdürmesini sağlar

Asenkron imzalama (operationMode "A", varsayılan; validity_period, 60.000 ms'lik AsyncValidityPeriodMS) bir koruma daha ekler. responseID, provider signatures/signPolling'i yoklamaya başlamadan saklanır; aynı işlem tanımlayıcısıyla yinelenen bir çağrı, hash'i yeniden göndermek yerine bekleyen işi devralır. Tamamlanmış toplu işler provider örneği başına önbelleklenir, MaxOperationCacheEntries'e (128) dek, ve o önbellek yeniden başlatmadan sonra yoktur. İdempotency anahtarı yaşar, çünkü türetilmiştir, rastgele değil: işlem tanımlayıcısını yeniden kullanan yeniden başlatılmış bir process aynı anahtarı yollar; servisin onun üzerinde deduplikasyon yapıp yapmaması servisin sözleşmesinin parçasıdır

Bu hesap bütün işlem tanımlayıcısına bel bağlar ve parametreyi boş bırakmak kolaydır. SignHashBatch'e '' geçirin; provider isteklerin OperationIdentifier'ına, sonra taze bir GUID'e döner. Çağrı başına taze bir GUID, kendi yeniden deneme döngünüzün her seferinde yeni bir işlem başlatması demektir: yeni önbellek yuvası, yeni idempotency anahtarı, belki ikinci bir OTP sorusu. Ayrıca Provider.Cancel'in hedefleyeceği bir şey kalmaz; çünkü kodunuz GUID'i hiç görmedi. Tanımlayıcıyı çoktan sakladığınız bir şeyden — toplu iş numarası ya da belge ID'si gibi — türetin ve EnableIdempotency'yi varsayılanı True'da bırakın. Bir servis başlığı reddederse option'ı kapatmak yerine onu transport'ta düşürün

CSC imzasını bir PDF'e gömmek

Provider'ı, GetCertificateChain'den gelen uç varlık sertifikasıyla birlikte HPDFCMSSignPDFStreamWithProvider'a geçirin; HotPDF CMS SignedData'yı kurar ve provider signed attributes'ın özetini imzalar. Girdi PDF'i, THPDFPage.AddSignedSignatureField'in yazdığı /ByteRange ile /Contents yer tutucusunu ister; tam olarak HotPDF'de PAdES imzalama akışındaki gibi. Ve provider modeli, ML-DSA ile EdDSA için HotPDF takılabilir signature provider'larının arkasındaki aynı modeldir

var
  Chain: THPDFCSCCertificateChain;
  SignOpts: THPDFCMSSignOptions;
  Src, Dst: TFileStream;
begin
  if Provider.RefreshCredentialInfo <> spsValid then
    raise Exception.Create(Provider.LastError);
  Chain := Provider.GetCertificateChain;   // CSC uç varlık sertifikasını önce listeler
  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;

Yer tutucu boyutlandırma için EstimateSignatureSize, set ettiğinizde EstimatedSignatureBytes'i döndürür; yoksa credentials/info'daki anahtar uzunluğundan türetilen RSA modulus boyutunu. ECDSA anahtarları için spsUnsupported cevap verir ve yer tutucu tahmincisi sertifikaya döner; EstimatedSignatureBytes'i kendiniz set etmek öngörülebilir bir sonuç verir. HPDFCMSSignPDFStreamWithProviderAutoSize, bir yer tutucu küçük kalınca yeniden kurar ve yeniden imzalar; bunu yalnızca spcSafeSignRetry ilan eden provider'lar için yapar. THPDFCSCSignatureProvider, EnableIdempotency açıkken onu ilan eder; o varsayılanı bırakmanın bir nedeni daha

PAdES-B-T için TimestampDigest, aynı servisten signatures/timestamp üzerinden isteğe bağlı bir nonce ile MaxTimestampBytes (1 MB) boyut tavanlı bir RFC 3161 token ister

Kodunuzda kalan politika kararları

Provider özetleri imzalar. Saf moddaki Ed25519 ile Ed448, bir signature provider'a bütün signed-attributes mesajını (sikMessage) verir ve toplu iş validator'ı onu bozuk olarak reddeder; çünkü signHash tanım gereği hash tabanlıdır. Free Pascal altında provider unit'i, anonim metotların yerine düz fonksiyon tipleriyle derlenir; SignHashBatch ile TimestampDigest orada çağrılabilirdir. Sonucu HPDFCMSSignPDFStreamWithProvider üzerinden bir PDF'e gömmek Delphi yoludur

Provider politikayı da size bırakır. CredentialInfo, KeyStatus, CertificateStatus, AuthorizationMode, SCAL ile MultiSignatures bildirir; ama devre dışı bir anahtarı ya da bir SCAL1 credential'ı reddetmek sizin kararınızdır. O alanları RefreshCredentialInfo'dan hemen sonra, imzacı bir OTP sorusu görmeden önce kontrol edin; imzacı bir kod yazdıktan sonra servisten gelen bir red, baştan açık bir hatadan kötü deneyimdir

Tek bir provider örneği bir seferde bir toplu iş koşturur; çünkü SignHashBatch, RefreshCredentialInfo ile TimestampDigest bir kilidi paylaşır ve iki thread bir SAD için yarışamaz. Verim, credential'ın multisign sınırına dek daha büyük toplu işlerden gelir. Elde edilen imzanın nitelikli sayılıp sayılmamasına trust service ve onun dağıttığı credential karar verir

CSC provider, CMS ile PAdES kurucularının ve yerel ile PKCS#11 signature provider'larının yanında, HotPDF Delphi PDF component içinde gelir; uzak, token tabanlı ve dosya tabanlı imzalama aynı PDF imzalama çağrılarını besler