Műszaki cikk

HotPDF CSC távoli aláírás: PDF-aláírás felhővel Delphiben

A HotPDF PDF dokumentumokat ír alá olyan privát kulccsal, amit egy távoli Cloud Signature Consortium (CSC) szolgálat tart a kezében, a THPDFCSCSignatureProvider-en keresztül, egy signature provider, ami a CSC API-t vezeti — credential info, hitelesítés, signatures/signHash és polling —, miközben a Delphi alkalmazásod szállítja a HTTP transportot és az OAuth access tokent. A kulcs soha nem hagyja el a szolgálat HSM-jét

Ez egyre inkább az egyetlen módja annak, hogy egyáltalán qualified aláírókulcshoz juss. A trust szolgáltatók CSC végpontot és OAuth klienst osztogatnak, nem PFX fájlt vagy USB tokent, így nincs mit betölteni egy helyi tanúsítványtárba úgy, mint ahogy a Windows cert store aláírás a CNG-n és CAPI-n keresztül teszi. A naiv integráció kiszámítható módon bukik meg: egy signHash hívás kifut az időből, és az újrapróbálás kétszer írja alá ugyanazt a szerződést, vagy negyven számlás tétel negyven egyszer használatos jelszót vált ki, mert minden hash külön lett hitelesítve. A provider tevékenységének nagy része annak a két hibának a kivédése

Miért hagyja a HotPDF a HTTP-t az alkalmazásodra?

Mert a transport pontosan az a hely, ahol minden telepítés eltér. A proxyk, a TLS pinning, a kliens tanúsítványok, a céges OAuth széfbe zárt kulcsok és a naplózási szabályzat mind a HTTP rétegben laknak, ezért a THPDFCSCSignatureProvider a protokollállapotot irányítja, és minden kérésre meghív egy THPDFCSCTransport függvényt. A provider átad neked egy THPDFCSCTransportRequest-et Method-dal (mindig POST), a ServiceBaseURL-ből és a végponti útvonalból összerakott teljes URL-lel, kész Authorization bearer fejléccel, ContentType-tal, a JSON Body-val, IdempotencyKey-vel, az Attempt számmal és MaxResponseBytes-szal. Te egy THPDFCSCTransportResponse-t töltesz meg StatusCode-val, Body-val és RetryAfterMS-szel, és visszaadsz egyet a ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure vagy ctsCancelled közül

HotPDF CSC transport-határ ábra: a THPDFCSCSignatureProvider irányítja a protokollt, és átad a kódodnak egy THPDFCSCTransportRequest-et POST módszerrel, a teljes URL-lel, kész Authorization bearer fejléccel, a JSON body-val, egy IdempotencyKey-vel és a próbálkozás számával, te pedig visszaadsz StatusCode-ot, Body-t, RetryAfterMS-t a négy cts státuszérték egyikével, miközben a kulcs soha nem hagyja el a HSM-et
A provider maga osztályozza a státuszkódokat, így egy transport, ami egy megválaszolt 503-at állandó hibává fordít, csendben letiltja az újrapróbálási logikát, miközben a proxyk és a TLS szabályzat a te kódodban marad
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  // fejlécnév, ahogy a te szolgálatod dokumentálja
          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- vagy DNS-gond: újrapróbálható
        end;
        Response.StatusCode := HttpResp.StatusCode;   // jelentsd a 503-at úgy, ahogy van, ne osztályozd
        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;

Az egyetlen szabály, amit érdemes megjegyezni: add vissza a ctsSuccess-ot, valahányszor a szerver ténylegesen válaszolt, még 503-mal is. A provider maga osztályozza a státuszkódokat, és egy transport, ami egy 429-et ctsPermanentFailure-ré tesz, csendben letiltja az alább leírt újrapróbálási logikát. A konstruktor a másik irányban szigorú — EHPDFCSCSignatureProviderError-t dob, ha a transport hiányzik, a CredentialID üres, se AccessToken se token callback nincs megadva, egy budget a tartományon kívül esik, vagy a ServiceBaseURL nem HTTPS. A sima http:// csak AllowInsecureHTTP-val fogadható el, ami egy tesztkörnyezetbe való, és sehova máshova

Mi az a SAD, és miért dobja ki a HotPDF egyetlen használat után?

A THPDFCSCSignatureProvider a Signature Activation Data-t (SAD) egyszer használatosként kezeli: törlődik a provider állapotából abban a pillanatban, amikor a signatures/signHash elfogadásra kerül, akkor is, ha maga az aláírás később, aszinkron pollingon át érkezik. A SAD a szolgálat bizonyítéka arra, hogy az aláíró jóváhagyta pontosan ezeket a hasheket, és egy memóriában lógó SAD egy jogosultság, ami arra vár, hogy a rossz dokumentumra költsék el

A THPDFCSCOptions.Default alapértékeivel — RequireSAD és AutoAuthorize egyaránt True — a provider egyszer betölti a credentials/info-t, a THPDFCSCAuthenticationCallback-től kéri az authData értékeket (egy OTP, egy PIN, bármi, amit a credential auth blokkja kíván), és elküldi a credentials/authorize-t. A 200 közvetlenül hordozza a SAD-ot; a 202 egy handlet hordoz, amit a credentials/authorizeCheck-en át polloznak legfeljebb MaxPollAttempts (60) alkalommal PollIntervalMS (250 ms) időközzel. A callback legfeljebb 32 értéket adhat vissza, mindegyik legfeljebb 256 bájtos nem üres ID-vel és legfeljebb 4 096 bájtos értékkel. Ha a hálózat megszakad, mielőtt a signHash elfogadásra kerülne, egy automatikusan beszerzett SAD megmarad, hogy ugyanaz a tétel újrapróbálható legyen az aláíró újbóli megkérdezése nélkül

HotPDF SAD-életciklus ábra: RequireSAD-del és AutoAuthorize-ral a provider egyszer betölti a credentials/info-t, a hitelesítési callbacktől kéri az OTP- vagy PIN-értékeket, elküldi a credentials/authorize-t, 202-es válasz esetén legfeljebb 60-szor pollozza a credentials/authorizeCheck-et 250 ms-onként, és törli a Signature Activation Data-t abban a pillanatban, amikor a signatures/signHash elfogadásra kerül, miközben megtart egy beszerzett SAD-ot, ha a hálózat az elfogadás előtt szakadt meg
Egy memóriában lógó SAD egy jogosultság, ami arra vár, hogy a rossz dokumentumra költsék el, és az opciókon átadott előre beállított SAD csak egyetlen hashes kérésre használatos
var
  Options: THPDFCSCOptions;
  Provider: THPDFCSCSignatureProvider;
begin
  Options := THPDFCSCOptions.Default;   // RequireSAD, AutoAuthorize, aszinkron mód, 2 újrapróbálás
  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
      // a te OAuth kliensed; a ForceRefresh True a szolgálat 401-es válasza után
      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   // a te UI-d
        Exit(spsCancelled);
      SetLength(Values, 1);
      Values[0].ID := 'otp';
      Values[0].Value := AnsiString(Otp);
      Result := spsValid;
    end);

Egy SAD, amit magad adsz át az Options.SAD-on, másképp viselkedik, és szándékosan. A HotPDF nem tudhatja, mely hashekre bocsátották ki, ezért a provider az előre beállított SAD-ot csak egyetlen hashes kérésre használja. Egy tételnél, amelynél az AutoAuthorize kikapcsolt, a provider „CSC SAD is not pinned to the requested hash batch" üzenettel bukik meg a találgatás helyett

Hogyan ír alá a SignHashBatch sok dokumentumot egy hitelesítéssel?

A SignHashBatch egy credentials/authorize-t és egy signatures/signHash-t küld legfeljebb MaxBatchSignatures (64) digestre, és mindkét testet ugyanabból a tömbből építi, hogy a numSignatures, a hashes sorrendje és a hashAlgorithmOID azonos legyen a két hívásban. Ez az egyezés az, amit a CSC multisign modell megkövetel. Negyvenszer ciklusozd a single-hash Sign metódust, és negyven hitelesítést kapsz; küldj egy egymással ellentmondó authorize-t és signHash-t, és a szolgálat a rossz tételre égetheti el a SAD-ot

Mielőtt bármilyen hálózati forgalom elindulna, a provider validálja a tételt. Minden kérésnek 1-től 1 024 bájtig terjedő digestnek (sikDigest) kell lennie digest OID-del, és minden kérésnek egyetlen aláírási algoritmus OID-et, egyetlen digest OID-et, RSASSA-PSS esetén pedig egyetlen sóhosszt kell megosztania. Egy multi-hash tétel betölti a credentials/info-t is, és spsUnsupported-ot ad vissza, amikor a credential multisign értéke kisebb, mint a tétel. A SAD ezután egy tételujjlenyomathoz rögzül — egy SHA-256 egy verziócímkén, a számon és, kérésenként, az algoritmus OID-en, a digest OID-en, az algoritmuson, a sóhosszon és a digest bájtokon, mindegyik hosszelőtaggal. Cserélj fel két hash-t, és máris egy másik tétel, ami friss hitelesítést igényel

var
  Requests: THPDFCSCSignatureRequests;
  Signatures: THPDFCSCSignatures;
  Status: THPDFSignatureProviderStatus;
  I: Integer;
begin
  SetLength(Requests, Length(Digests));      // Digests: a te számolt SHA-256 értékeid
  for I := 0 to High(Digests) do
  begin
    Requests[I] := Default(THPDFSignatureProviderRequest);
    Requests[I].Algorithm := hsaRSAPKCS1v15;           // a signAlgo származtatódik, ha az AlgorithmOID üres
    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]);
  // A Signatures[I] a Digests[I]-hez tartozik; a számot a kérés ellenőrizte
end;

RSASSA-PSS-hez a provider signAlgoParams-t is küld, egy base64 DER RSASSA-PSS-params struktúrát a hash algoritmussal, az MGF1-gyel és a sóhosszal. Felépíteni azt OID-ek kódolását jelenti, és a 2.748.5-ös verzió ennek egy zugát javította: az X.690 §8.19.4-e az első két ívet egyetlen értékbe hajtja (40 × első + második), és a 2 gyökér alatt egy 39 fölötti második ív azt az értéket 127 fölé nyomja, ahol a base-128 több bájtos formára van szükség, amit a korábbi buildek nem alkalmaztak. Egyetlen SHA-2 OID-et sem érint — a 2.16 96-ra hajlik — de egy rosszul formált OID mostantól a provider saját hibáját dobja egy EConvertError helyett

Miért nem gyárt második aláírást egy újrapróbált kérés?

A THPDFCSCSignatureProvider minden újrapróbálható hívást determinisztikus idempotencia kulccsal lát el, és cache-eli a befejezett eredményeket, így egy elveszett válasz utáni újrapróbálás az eredeti aláírásokat adja vissza ahelyett, hogy újakat kérne a HSM-től. A kulcs a csc- prefix, majd az operációazonosító és a fázis hex SHA-256-a, és a fázis mind a hitelesítéshez, mind a signHash-hoz beágyazza a tételujjlenyomatot. A hashelés számít a csonkolás helyett: két hosszú operációazonosító, amik megosztanak egy prefixet, csonkolás alatt ütköznének, míg egy fix hosszú tartalomalapú kulcs egyedi és stabil marad a próbálkozások között

Az újrapróbálási szabályzat a megosztott kérésútvonalban szándékosan szűk:

  • A HTTP 401 pontosan egy tokenfrissítést kényszerít az access-token callbacken át, majd a kérés egyszer megismétlődik, ha van hozzárendelve access-token callback; a második 401 végleges
  • A többi 4xx válasz és a ctsPermanentFailure spsProviderError-ral fejezi be a hívást, és a szolgálat error_description-ja a LastError-be kerül
  • A 408, a 429, az 5xx és a ctsTemporaryFailure legfeljebb RetryLimit (alapból 2) alkalommal próbálódik újra, Retry-After-ra vagy RetryBaseDelayMS × 2attempt (100 ms alap) értékre várva, MaxRetryAfterMS-re (5 000 ms) korlátozva
  • A várakozások 25 ms-os szeletekben futnak, amik ellenőrzik a Cancel-t, így egy megszakító felhasználónak nem kell végigülnie egy öt másodperces back-offot
  • A signHash csak addig próbálódik újra, amíg az EnableIdempotency be van kapcsolva; kapcsold ki, és egy beküldés utáni timeout végleges, mert senki nem tudja megmondani, használta-e már valaki a kulcsot
HotPDF újrapróbálási szabályzat ábra: minden újrapróbálható hívás determinisztikus csc- idempotencia kulcsot hordoz, hashelve az operációazonosítóból és a fázisból, a HTTP 401 pontosan egy tokenfrissítést kényszerít, a többi 4xx válasz spsProviderError-ral ér véget, a 408, a 429, az 5xx vagy az átmeneti transporthiba pedig RetryLimit 2-ig próbálódik újra, Retry-After-ra vagy legfeljebb 5 000 ms-re korlátozott exponenciális backoffra várva
A befejezett tételek operációazonosító, credential és ujjlenyomat szerint cache-elődnek, és aszinkron módban a tárolt responseID lehetővé teszi, hogy egy ismételt hívás a pollingot folytassa a hash újbóli beküldése helyett

Az aszinkron aláírás (operationMode „A", az alapértelmezett) még egy őrt tesz hozzá: a responseID tárolódik, mielőtt a signatures/signPolling-t pollolnák, így egy ugyanazzal az operációazonosítóval érkező ismételt hívás folytatja a pollingot az újbóli beküldés helyett. A befejezett tételek egy, operációazonosító, credential és ujjlenyomat szerint kulcsolt cache-ben ülnek, MaxOperationCacheEntries-re (128) korlátozva, mély másolatként visszaadva. Az a cache a provider instanciájában lakik, és nem éli túl az újraindítást. Az idempotencia kulcs igen, mert származtatott, nem véletlen, így egy újraindított process, ami újrahasznosítja az operációazonosítóját, ugyanazt a kulcsot küldi — hogy a szolgálat deduplikál-e rá, az a szolgálat ígérete, nem a HotPDF-é

Hogyan kerül CSC aláírás egy PDF-be?

Add át a providert a HPDFCMSSignPDFStreamWithProvider-nek a GetCertificateChain-ből származó végentitás-tanúsítvánnyal együtt; a HotPDF felépíti a CMS SignedData-t, a provider pedig aláírja az aláírt attribútumok digestjét. A bemeneti PDF-nek szüksége van a /ByteRange-re és a /Contents helyőrzőre, amit a THPDFPage.AddSignedSignatureField ír, pontosanúgy, mint a PAdES aláírási munkafolyamat a HotPDF-ben, a provider modell pedig ugyanaz, amit a HotPDF cserélhető signature providerek ML-DSA-hoz és EdDSA-hoz tárgyal

var
  Chain: THPDFCSCCertificateChain;
  SignOpts: THPDFCMSSignOptions;
  Src, Dst: TFileStream;
begin
  if Provider.RefreshCredentialInfo <> spsValid then
    raise Exception.Create(Provider.LastError);
  Chain := Provider.GetCertificateChain;   // a CSC a végentitás tanúsítványt sorolja először
  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;

A /Contents helyőrző méretezése az EstimateSignatureSize-on át megy, ami a beállításod esetén az EstimatedSignatureBytes-t adja vissza, egyébként az RSA modulus méretét a credential kulcshosszából. ECDSA-nál állítsd be magad az EstimatedSignatureBytes-t, vagy a becslés spsUnsupported-ot jelent. Az automatikus méretű aláíró variáns újraaláír, amikor egy helyőrző kicsinek bizonyul, és csak olyan providereknek teszi ezt, amik hirdetik a spcSafeSignRetry-t — amit a THPDFCSCSignatureProvider csak akkor tesz, amíg az EnableIdempotency be van kapcsolva. PAdES-B-T munkafolyamatokhoz a TimestampDigest időbélyeg tokent kér ugyanattól a szolgálattól a signatures/timestamp-en át, MaxTimestampBytes-re (1 MB) korlátozva

Mit nem tesz a CSC provider?

Nem ír alá üzeneteket, csak digesteket. Az Ed25519 és az Ed448 tiszta módban a teljes aláírt-attribútum üzenetet adja a provider kezébe (sikMessage), és a tételvalidátor rosszul formáltként utasítja el, mert a signHash definíció szerint hash-alapú. A provider unit Free Pascal alatt is fordul sima függvénytípusokkal az anonim metódusok helyett, de a provider-vezérelt CMS építők ma FPC alatt kivételt dobnak, így egy CSC aláírás PDF-be ágyazása Delphi-útvonal

Szabályzatot sem dönt. A CredentialInfo jelenti a kulcs állapotát, a tanúsítvány állapotát, a hitelesítési módot, a SCAL szintet és a multisign limitet, de a provider önmagától nem utasít el letiltott kulcsot vagy SCAL1 credentiált — ellenőrizd ezeket, mielőtt egy aláírónak megmutatnád az OTP promptot. És egy provider instancia egyszer egy tételt ír alá: a SignHashBatch belül szerializált, így két szál nem versenyezhet egy SAD-ért, ami azt jelenti, hogy az áteresztőképesség a tételesítésből jön, nem abból, hogy providert osztanak meg worker szálak között. Hogy az eredményaláírás qualified-e, az a trust szolgálaton és a credentialján múlik, nem a libraryn, ami a hash-t oda szállította

A CSC provider, a CMS és PAdES építők, valamint a lokális és PKCS#11 providerek mind a HotPDF Delphi PDF komponensben érkeznek