Teknik Makale

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

HotPDF, PDF belgelerini özel anahtarı uzak bir Cloud Signature Consortium (CSC) servisinde tutulan anahtarla imzalar; THPDFCSCSignatureProvider, CSC API'sini süren — credential info, yetkilendirme, signatures/signHash ve polling — bir imza sağlayıcısıdır ve HTTP transport ile OAuth access tokenini sizin Delphi uygulamanız sağlar. Anahtar, servisin HSMinden asla çıkmaz

Bu, nitelikli bir imzalama anahtarına hiç ulaşmanın giderek tek yolu. Güven hizmeti sağlayıcıları bir CSC endpointi ve bir OAuth client verir; PFX dosyası ya da USB token değil, dolayısıyla CNG ve CAPI ile Windows cert store imzalamasının yaptığı gibi yerel bir sertifika deposuna yüklenecek bir şey yoktur. Naif entegrasyon öngörülebilir biçimlerde başarısız olur: bir signHash çağrısı zaman aşımına uğrar ve yeniden deneme aynı sözleşmeyi iki kez imzalar, ya da kırk faturalık bir parti her hash ayrı yetkilendirildiği için kırk tek kullanımlık parola tetikler. Sağlayıcının yaptığı şeyin çoğu bu iki başarısızlığa karşı savunmadır

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

Çünkü transport, tam olarak her dağıtımın farklılaştığı yerdir. Proxy'ler, TLS pinning, client sertifikaları, kurumsal OAuth kasaları ve loglama politikası hep HTTP katmanında yaşar; THPDFCSCSignatureProvider bu yüzden protokol durumunu orkestra eder ve her istek için bir THPDFCSCTransport fonksiyonu çağırır. Sağlayıcı size Methodu (daima POST), ServiceBaseURL artı endpoint yoluyla kurulan tam URL, hazır bir Authorization bearer başlığı, ContentType, JSON Body, bir IdempotencyKey, Attempt numarası ve MaxResponseBytes taşıyan bir THPDFCSCTransportRequest verir. Siz bir THPDFCSCTransportResponseu StatusCode, Body ve RetryAfterMS ile doldurur ve ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ya da ctsCancelledden birini döndürürsünüz

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
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  // başlık adı, servisinizin belgelediği hâliyle
          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 ya da DNS sorunu: yeniden denenebilir
        end;
        Response.StatusCode := HttpResp.StatusCode;   // 503'ü olduğu gibi bildir, sınıflandırma
        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;

Ezberlenmeye değer tek kural: bir sunucu gerçekten cevap verdiyse, 503 ile bile ctsSuccess döndürün. Sağlayıcı durum kodlarını kendisi sınıflandırır ve 429'u ctsPermanentFailuree çeviren bir transport, aşağıda anlatılan yeniden deneme mantığını sessizce etkisiz bırakır. Constructor öbür yönde katıdır — transport eksikse, CredentialID boşsa, ne bir AccessToken ne bir token callback verilmişse, bütçe aralık dışındaysa ya da ServiceBaseURL HTTPS değilse EHPDFCSCSignatureProviderError fırlatır. Düz http:// yalnızca AllowInsecureHTTP ile kabul edilir; o da bir test düzeneğine aittir, başka hiçbir yere

SAD nedir ve HotPDF onu tek kullanımdan sonra neden çöpe atar?

THPDFCSCSignatureProvider, Signature Activation Data'yı (SAD) tek kullanımlık sayar: signatures/signHash kabul edildiği anda sağlayıcı durumundan temizlenir, imzanın kendisi sonradan asenkron polling ile gelse bile. SAD, imzalayanın tam olarak bu hashleri onayladığının servise kanıtıdır ve bellekte asılı kalan bir SAD, yanlış belgeye harcanmayı bekleyen bir yetkilendirmedir

THPDFCSCOptions.Defaultin varsayılanlarıyla — RequireSAD ile AutoAuthorize ikisi de True — sağlayıcı credentials/infoyu bir kez yükler, authData değerlerini (bir OTP, bir PIN, credentialın auth bloğunun istediği her neyse) THPDFCSCAuthenticationCallbackinizden ister ve credentials/authorize gönderir. 200 SAD'yi doğrudan taşır; 202, credentials/authorizeCheck üzerinden en çok MaxPollAttempts (60) kez, PollIntervalMS (250 ms) arayla yoklanan bir tutamaç taşır. Callback en çok 32 değer döndürebilir; her biri en çok 256 baytlık boş olmayan bir ID ve en çok 4.096 baytlık bir değerle. Ağ, signHash kabul edilmeden düşerse otomatik elde edilmiş bir SAD saklanır, böylece aynı parti imzalayana yeniden sormadan tekrar denenebilir

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
      // sizin OAuth clientınız; ForceRefresh, servis 401 cevapladıktan sonra True
      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   // sizin arayüzünüz
        Exit(spsCancelled);
      SetLength(Values, 1);
      Values[0].ID := 'otp';
      Values[0].Value := AnsiString(Otp);
      Result := spsValid;
    end);

Options.SAD üzerinden kendiniz geçtiğiniz bir SAD farklı davranır ve bilinçli olarak öyle. HotPDF hangi hashler için verildiğini bilemez; sağlayıcı bu yüzden hazır bir SAD'yi yalnızca tek hashli istekte kullanır. AutoAuthorize kapatılmış bir parti için sağlayıcı tahmin yürütmek yerine "CSC SAD is not pinned to the requested hash batch" ile başarısız olur

SignHashBatch tek yetkilendirmeyle çok belgeyi nasıl imzalar?

SignHashBatch, en çok MaxBatchSignatures (64) özet için tek bir credentials/authorize ve tek bir signatures/signHash gönderir ve her iki bodyi de aynı diziden kurar; böylece numSignatures, hashesin sırası ve hashAlgorithmOID iki çağrıda özdeş olur. O eşleşme, CSC multisign modelinin istediği şeydir. Tek hashli Sign metodunu kırk kez döngüye alırsanız kırk yetkilendirme alırsınız; anlaşmayan bir authorize ile signHash gönderirseniz servis SAD'yi yanlış partiye harcayabilir

Herhangi bir ağ trafiğinden önce sağlayıcı partiyi doğrular. Her istek, 1 ile 1.024 bayt arası, digest OIDli bir özet (sikDigest) olmalı ve tüm istekler bir imza algoritması OID, bir digest OID ve RSASSA-PSS için bir salt uzunluğu paylaşmalıdır. Çok hashli bir parti ayrıca credentials/infoyu yükler ve credentialın multisign değeri partiden küçükse spsUnsupported döndürür. SAD ardından bir parti parmak izine sabitlenir — bir sürüm etiketi, sayı ve istek başına algoritma OID, digest OID, algoritma, salt uzunluğu ve özet baytları üzerinde, her biri uzunluk önekli, bir SHA-256. İki hashi değiştirirseniz artık taze bir yetkilendirme isteyen başka bir partidir

var
  Requests: THPDFCSCSignatureRequests;
  Signatures: THPDFCSCSignatures;
  Status: THPDFSignatureProviderStatus;
  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;
  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]ye aittir; sayı istekle karşılaştırılarak sınandı
end;

RSASSA-PSS için sağlayıcı ayrıca signAlgoParams gönderir: hash algoritması, MGF1 ve salt uzunluğu taşıyan base64 DER bir RSASSA-PSS-params yapısı. Onu kurmak OID kodlamak demektir ve 2.748.5 sürümü bunun bir köşesini düzeltti: X.690 §8.19.4 ilk iki yayı tek değere katlar (40 × birinci + ikinci) ve 2 kökü altında 39un üzerinde bir ikinci yay, o değeri 127yi aşırır; erken derlemelerin uygulamadığı taban-128 çok baytlı biçim burada gerekir. Hiçbir SHA-2 OID etkilenmez — 2.16, 96ya katlanır — ama bozuk bir OID artık EConvertError yerine sağlayıcının kendi hatasını fırlatır

Yeniden denenen bir istek neden ikinci bir imza üretmez?

THPDFCSCSignatureProvider, yeniden denenebilir her çağrıya deterministik bir idempotency anahtarı taşıtır ve tamamlanmış sonuçları önbellekler; kaybolmuş bir cevaptan sonraki yeniden deneme, HSMden yeni imzalar istemek yerine özgün imzaları döndürür. Anahtar, csc- ve ardından işlem tanımlayıcısı ile fazın hex SHA-256sıdır ve faz, hem yetkilendirme hem signHash için parti parmak izini gömer. Kırpma yerine hash almak önemlidir: önek paylaşan iki uzun işlem ID kırpma altında çakışırdı; sabit uzunluklu, içeriğe adresli bir anahtar denemeler boyunca özgün ve kararlı kalır

Paylaşılan istek yolundaki yeniden deneme politikası bilinçli olarak dardır:

  • HTTP 401, access-token callbacki üzerinden tam olarak bir token yenilemesi dayatır ve access-token callback atanmışsa istek bir kez yinelenir; ikinci 401 kesindir
  • Diğer 4xx cevapları ve ctsPermanentFailure çağrıyı spsProviderError ile bitirir ve servisin error_descriptionı LastErrora düşer
  • 408, 429, 5xx ve ctsTemporaryFailure, RetryLimite (varsayılan 2) kadar yeniden denenir; Retry-After ya da RetryBaseDelayMS × 2attempt beklenir (100 ms taban), MaxRetryAfterMS ile (5.000 ms) sınırlandırılır
  • Beklemeler Canceli kontrol eden 25 ms dilimler hâlinde yürür; vazgeçen bir kullanıcı beş saniyelik geri çekilmeyi beklemek zorunda kalmaz
  • signHash, EnableIdempotency açıkken yeniden denenir; kapatırsanız gönderimden sonraki zaman aşımı kesindir, çünkü anahtarın zaten kullanılıp kullanılmadığını kim 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) bir koruma daha ekler: responseID, signatures/signPolling yoklanmadan önce saklanır, böylece aynı işlem tanımlayıcıyla yinelenen bir çağrı yeniden göndermek yerine yoklamayı sürdürür. Tamamlanan partiler, işlem tanımlayıcısı, credential ve parmak iziyle anahtarlanmış bir önbellekte durur; MaxOperationCacheEntries ile (128) sınırlıdır ve derin kopyalar olarak döndürülür. O önbellek sağlayıcı örneğinde yaşar ve bir yeniden başlatmayı atlatmaz. Idempotency anahtarı atlatır, çünkü rastgele değil türetilmiştir; işlem tanımlayıcısını yeniden kullanan yeniden başlatılmış bir süreç aynı anahtarı gönderir — servisin bunun üzerinde tekilleştirme yapıp yapmayacağı servisin sözüdür, HotPDF'in değil

Bir CSC imzasını PDF'e nasıl koyarsınız?

Sağlayıcıyı GetCertificateChainden gelen uç varlık sertifikasıyla birlikte HPDFCMSSignPDFStreamWithProvidere geçirin; HotPDF CMS SignedDatayı kurar ve sağlayıcı imzalı özniteliklerin özetini imzalar. Girdi PDF'i, THPDFPage.AddSignedSignatureFieldin yazdığı /ByteRange ve /Contents yer tutucusuna ihtiyaç duyar; tam olarak HotPDF'de PAdES imzalama iş akışındaki gibi ve sağlayıcı model, ML-DSA ve EdDSA için HotPDF takılabilir imza sağlayıcılarında kapsanan 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;

/Contents yer tutucusunu boyutlandırma EstimateSignatureSize üzerinden gider; set ettiğinizde EstimatedSignatureBytesi döndürür, aksi hâlde credentialın anahtar uzunluğundan RSA modulus boyutunu. ECDSA için EstimatedSignatureBytesi kendiniz set edin ya da kestirim spsUnsupported bildirir. Otomatik boyutlu imzalama varyantı, yer tutucu küçük çıkarsa yeniden imzalar ve bunu yalnızca spcSafeSignRetry ilan eden sağlayıcılar için yapar — THPDFCSCSignatureProvider bunu yalnızca EnableIdempotency açıkken yapar. PAdES-B-T iş akışları için TimestampDigest, aynı servisten signatures/timestamp üzerinden bir zaman damgası tokeni ister; MaxTimestampBytes ile (1 MB) sınırlıdır

CSC sağlayıcısı neyi yapmaz?

Mesaj imzalamaz, yalnızca özet imzalar. Saf moddaki Ed25519 ile Ed448, sağlayıcıya imzalı öznitelikler mesajının tamamını (sikMessage) verir ve parti doğrulayıcı bunu bozuk olarak reddeder; signHash tanımı gereği hash temellidir. Sağlayıcı birimi, anonim metotlar yerine düz fonksiyon türleriyle Free Pascal altında derlenir ama sağlayıcı güdümlü CMS kurucuları bugün FPC altında istisna fırlatır; dolayısıyla bir CSC imzasını PDF'e gömmek bir Delphi yoludur

Politika da belirlemez. CredentialInfo anahtar durumunu, sertifika durumunu, yetkilendirme modunu, SCAL seviyesini ve multisign sınırını bildirir ama sağlayıcı, devre dışı bir anahtarı ya da SCAL1 bir credentialı kendi başına reddetmez — imzalayana OTP istemini göstermeden önce bunları kontrol edin. Ve bir sağlayıcı örneği bir anda tek parti imzalar: SignHashBatch içeride serileştirilir, böylece iki iş parçacığı tek bir SAD için yarışamaz; bu da verimin partileşmeden geldiği anlamına gelir, bir sağlayıcının işçi iş parçacıkları arasında paylaşılmasından değil. Ortaya çıkan imzanın nitelikli olup olmayacağı güven hizmetine ve credentialına bağlıdır, hashi oraya taşıyan kütüphaneye değil

CSC sağlayıcısı, CMS ile PAdES kurucuları ve yerel ile PKCS#11 sağlayıcılarının hepsi HotPDF Delphi PDF component ile gelir