Tekninen artikkeli

HotPDF CSC-etäallekirjoitus: pilviallekirjoitukset Delphissä

HotPDF allekirjoittaa PDF-asiakirjoja yksityisellä avaimella, jota pitää hallussaan etäinen Cloud Signature Consortium (CSC) -palvelu, THPDFCSCSignatureProviderin kautta — allekirjoituspalveluntarjoaja, joka ajaa CSC API:a: credential infoa, valtuutusta, signatures/signHashia ja pollausta — sillä aikaa kun Delphi-sovelluksesi toimittaa HTTP-kuljetuksen ja OAuth-pääsytokenin. Avain ei koskaan poistu palvelun HSM:stä

Se on yhä useammin ainoa tapa saada lainkaan kvalifioitu allekirjoitusavain. Luottamuspalveluntarjoajat jakavat CSC-päätepisteen ja OAuth-asiakkaan, ei PFX-tiedostoa eikä USB-tokenia, joten paikalliseen sertifikaattisäilöön ei ole mitään ladattavaa siinä missä Windowsin sertifikaattisäilön allekirjoitus CNG:n ja CAPI:n kautta tekee. Naiivi integraatio kaatuu ennustettavilla tavoilla: signHash-kutsu aikakatkeaa ja uusinta allekirjoittaa saman sopimuksen kahdesti, tai erä neljääkymmentä laskua laukoo neljäkymmentä kertakäyttösalasanaa, koska jokainen tiiviste valtuutettiin erikseen. Suurin osa siitä, mitä palveluntarjoaja tekee, on puolustautumista näitä kahta epäonnistumista vastaan

Miksi HotPDF jättää HTTP:n sovelluksellesi?

Koska kuljetus on täsmälleen se kohta, jossa jokainen käyttöönotto eroaa. Välityspalvelimet, TLS-pinnitys, asiakassertifikaatit, yrityksen OAuth-holvit ja lokituspolitiikka elävät kaikki HTTP-kerroksessa, joten THPDFCSCSignatureProvider orkestroi protokollan tilan ja kutsuu THPDFCSCTransport-funktiota jokaista pyyntöä kohden. Palveluntarjoaja antaa sinulle THPDFCSCTransportRequestin, jossa on Method (aina POST), täysi URL, joka on rakennettu ServiceBaseURLista plus päätepistepolku, valmis Authorization-bearer-otsikko, ContentType, JSON-Body, IdempotencyKey, Attempt-numero ja MaxResponseBytes. Sinä täytät THPDFCSCTransportResponsein kentillä StatusCode, Body ja RetryAfterMS ja palautat yhden arvoista ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure tai ctsCancelled

HotPDF:n CSC-kuljetusrajan kaavio: THPDFCSCSignatureProvider orkestroi protokollan ja antaa koodillesi THPDFCSCTransportRequestin, jossa on POST-metodi, täysi URL, valmis Authorization-bearer-otsikko, JSON-runko, IdempotencyKey ja yritysnumero, ja sinä palautat StatusCode-arvon, Bodyn ja RetryAfterMS:n plus yhden neljästä cts-tilaarvosta, sillä avain ei koskaan poistu HSM:stä
Palveluntarjoaja luokittelee tilakoodit itse, joten kuljetus, joka muuttaa vastatun 503:n pysyväksi epäonnistumiseksi, poistaa uusintalogiikan hiljaa käytöstä, kun taas välityspalvelimet ja TLS-politiikka pysyvät omassa koodissasi
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  // otsikon nimi niin kuin palvelusi dokumentoi sen
          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- tai DNS-häiriö: uusittava
        end;
        Response.StatusCode := HttpResp.StatusCode;   // raportoi 503 sellaisenaan, älä luokittele
        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;

Sääntö, joka kannattaa opetella ulkoa: palauta ctsSuccess aina, kun palvelin oikeasti vastasi, vaikka 503:lla. Palveluntarjoaja luokittelee tilakoodit itse, ja kuljetus, joka muuttaa 429:n ctsPermanentFailureiksi, poistaa alla kuvatun uusintalogiikan hiljaa käytöstä. Konstruktori on tiukka toiseen suuntaan — se nostaa EHPDFCSCSignatureProviderErrorin, kun kuljetus puuttuu, CredentialID on tyhjä, ei ole annettu AccessTokenia eikä token-takaisinkutsua, budjetti on alueen ulkopuolella tai ServiceBaseURL ei ole HTTPS. Pelkkä http:// hyväksytään vain AllowInsecureHTTPilla, joka kuuluu testiympäristöön eikä mihinkään muuhun

Mikä SAD on, ja miksi HotPDF heittää sen pois yhden käytön jälkeen?

THPDFCSCSignatureProvider käsittelee Signature Activation Dataa (SAD) kertakäyttöisenä: se pyyhitään palveluntarjoajan tilasta sillä hetkellä, kun signatures/signHash hyväksytään, silloinkin kun allekirjoitus itse saapuu myöhemmin asynkronisen pollauksen kautta. SAD on palvelun todiste siitä, että allekirjoittaja hyväksyi juuri nämä tiivisteet, ja muistissa roikkuva SAD on valtuutus, joka odottaa kuluttamista väärälle asiakirjalle

Oletuksilla THPDFCSCOptions.Defaultista — RequireSAD ja AutoAuthorize molemmat True — palveluntarjoaja lataa credentials/infoin kerran, kysyy THPDFCSCAuthenticationCallbackiltasi authData-arvot (OTP, PIN, mitä tahansa sertifikaatin auth-lohko vaatii) ja lähettää credentials/authorizein. 200 kantaa SAD:n suoraan; 202 kantaa kahvan, jota pollataan credentials/authorizeCheckin kautta enintään MaxPollAttempts (60) kertaa PollIntervalMSin (250 ms) välein. Takaisinkutsu saa palauttaa enintään 32 arvoa, joista kummallakin on ei-tyhjä enintään 256 tavun ID ja enintään 4 096 tavun arvo. Jos verkko katkeaa ennen kuin signHash hyväksytään, automaattisesti saatu SAD säilytetään, joten sama erä voidaan uusia kysymättä allekirjoittajalta uudelleen

HotPDF:n SAD-elinkaaren kaavio: RequireSAD- ja AutoAuthorize-lipuilla palveluntarjoaja lataa credentials/info -merkinnän kerran, kysyy todennustakaisinkutsulta OTP- tai PIN-arvot, lähettää credentials/authorize -pyynnön, pollaa credentials/authorizeCheck -päätepistettä enintään 60 kertaa 250 ms välein kun vastaus on 202, ja pyyhkii Signature Activation Datan sillä hetkellä kun signatures/signHash hyväksytään, säilyttäen saadun SAD:n silloin kun verkko katkesi ennen hyväksyntää
Muistissa roikkuva SAD on valtuutus, joka odottaa kuluttamista väärälle asiakirjalle, ja asetuksilla välitettyä esiasetettua SAD:ia käytetään vain yhden tiivisteen pyyntöön
var
  Options: THPDFCSCOptions;
  Provider: THPDFCSCSignatureProvider;
begin
  Options := THPDFCSCOptions.Default;   // RequireSAD, AutoAuthorize, asynkroninen tila, 2 uusintaa
  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-asiakkaasi; ForceRefresh on True sen jälkeen kun palvelu vastasi 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   // käyttöliittymäsi
        Exit(spsCancelled);
      SetLength(Values, 1);
      Values[0].ID := 'otp';
      Values[0].Value := AnsiString(Otp);
      Result := spsValid;
    end);

Itse Options.SADin kautta välittämäsi SAD käyttäytyy toisin, ja tahallaan. HotPDF ei voi tietää, mille tiivisteille se myönnettiin, joten palveluntarjoaja käyttää esiasetettua SAD:ia vain yhden tiivisteen pyyntöön. Erälle, jolla AutoAuthorize on pois päältä, palveluntarjoaja epäonnistuu virheeseen ”CSC SAD is not pinned to the requested hash batch” arvaamisen sijaan

Miten SignHashBatch allekirjoittaa monta asiakirjaa yhdellä valtuutuksella?

SignHashBatch lähettää yhden credentials/authorizein ja yhden signatures/signHashin enintään MaxBatchSignatures (64) tiivisteelle ja rakentaa molemmat rungot samasta taulukosta niin, että numSignatures, hashesin järjestys ja hashAlgorithmOID ovat identtiset kummassakin kutsussa. Se täsmäys on se, mitä CSC:n multisign-malli vaatii. Silmukoi yhden tiivisteen Sign-metodia neljäkymmentä kertaa ja saat neljäkymmentä valtuutusta; lähetä authorize ja signHash, jotka eroavat toisistaan, ja palvelu voi kuluttaa SAD:n väärälle erälle

Ennen mitään verkkoliikennettä palveluntarjoaja valido erän. Jokaisen pyynnön on oltava tiiviste (sikDigest), jonka pituus on 1–1 024 tavua ja jolla on tiiviste-OID, ja kaikkien pyyntöjen on jaettava yksi allekirjoitusalgoritmin OID, yksi tiiviste-OID ja, RSASSA-PSS:lle, yksi suolan pituus. Monitiivisteinen erä lataa myös credentials/infoin ja palauttaa spsUnsupportedin, kun sertifikaatin multisign-arvo on pienempi kuin erä. SAD kiinnitetään sitten erän sormenjälkeen — SHA-256 versiolabelista, määrästä ja jokaisen pyynnön kohdalla algoritmin OIDista, tiiviste-OIDista, algoritmista, suolan pituudesta ja tiivistetavuista, kukin pituusetuliitteellä. Vaihda kaksi tiivistettä keskenään, niin kyseessä on eri erä, joka tarvitsee tuoreen valtuutuksen

var
  Requests: THPDFCSCSignatureRequests;
  Signatures: THPDFCSCSignatures;
  Status: THPDFSignatureProviderStatus;
  I: Integer;
begin
  SetLength(Requests, Length(Digests));      // Digests: laskemasi SHA-256-arvot
  for I := 0 to High(Digests) do
  begin
    Requests[I] := Default(THPDFSignatureProviderRequest);
    Requests[I].Algorithm := hsaRSAPKCS1v15;           // signAlgo johdetaan kun AlgorithmOID on tyhjä
    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] kuuluu Digests[I]:lle; määrä tarkistettiin pyyntöä vasten
end;

RSASSA-PSS:lle palveluntarjoaja lähettää myös signAlgoParamsin, base64-DER-RSASSA-PSS-params-rakenteen tiivistealgoritmilla, MGF1:llä ja suolan pituudella. Sen rakentaminen tarkoittaa OIDien koodaamista, ja versio 2.748.5 korjasi yhden sen kulman: X.690 §8.19.4 taittaa kaksi ensimmäistä kaarta yhdeksi arvoksi (40 × ensimmäinen + toinen), ja 2-juuren alla toinen kaari yli 39:n työntää kyseisen arvon yli 127:n, jolloin se tarvitsee kantaluku 128:n monitavuisen muodon, jota aiemmat koosteet eivät soveltaneet. Mikään SHA-2-OID ei ole vaikutusalueella — 2.16 taittuu arvoon 96 — mutta epämuodostunut OID nostaa nyt palveluntarjoajan oman virheen EConvertErrorin sijaan

Miksi uusittu pyyntö ei tuota toista allekirjoitusta?

THPDFCSCSignatureProvider panee jokaisen uusittavan kutsun kantamaan deterministisen idempotenssiavaimen ja välimuistittaa valmiit tulokset, joten uusinta kadonneen vastauksen jälkeen palauttaa alkuperäiset allekirjoitukset sen sijaan että pyytäisi HSM:ltä uusia. Avain on csc- ja sitä seuraa operaatiotunnisteen ja vaiheen heksadesimaalinen SHA-256, ja vaihe upottaa erän sormenjäljen sekä valtuutukseen että signHashiin. Tiivistäminen katkaisemisen sijaan on merkitsevää: kaksi pitkää operaatiotunnistetta, joilla on yhteinen etuliite, törmäisivät katkaisun alla, kun taas kiinteäpituinen sisältöosoitteinen avain pysyy ainutlaatuisena ja vakaana yritysten yli

Uusintapolitiikka jaetussa pyyntöpolussa on tarkoituksella kapea:

  • HTTP 401 pakottaa täsmälleen yhden tokenin päivityksen pääsytokenin takaisinkutsun kautta, sitten pyyntö toistetaan kerran, kun pääsytokenin takaisinkutsu on sijoitettu; toinen 401 on lopullinen
  • Muut 4xx-vastaukset ja ctsPermanentFailure päättävät kutsun spsProviderErroriin, ja palvelun error_description päätyy LastErroriin
  • 408, 429, 5xx ja ctsTemporaryFailure uusitaan enintään RetryLimit (oletus 2) kertaa, odottaen Retry-Afteria tai RetryBaseDelayMS × 2attemptia (100 ms perusta), kattolukuna MaxRetryAfterMS (5 000 ms)
  • Odotukset ajavat 25 ms viipaleissa, jotka tarkistavat Cancelin, joten keskeyttävä käyttäjä ei joudu istumaan viiden sekunnin viiveen läpi
  • signHashia uusitaan vain, kun EnableIdempotency on päällä; kytke se pois, ja aikakatkaisu lähetyksen jälkeen on lopullinen, koska kukaan ei voi tietää, käytettiinkö avainta jo
HotPDF:n uusintapolitiikan kaavio: jokainen uusittava kutsu kantaa deterministisen csc- idempotenssiavaimen, joka tiivistetään operaatiotunnisteesta ja vaiheesta, HTTP 401 pakottaa täsmälleen yhden tokenin päivityksen, muut 4xx-vastaukset päättyvät spsProviderErroriin, ja 408, 429, 5xx tai väliaikainen kuljetusvikatila uusitaan enintään RetryLimitin 2 verran odottaen Retry-Afteria tai eksponentiaalista viivettä kattolukuna 5 000 ms
Valmiit erät välimuistitetaan operaatiotunnisteen, sertifikaatin ja sormenjäljen mukaan, ja asynkronisessa tilassa tallennettu responseID antaa toistetun kutsun jatkaa pollausta tiivisteen uudelleenlähetyksen sijaan

Asynkroninen allekirjoitus (operationMode ”A”, oletus) lisää yhden vartiota lisää: responseID tallennetaan ennen kuin signatures/signPollingia pollataan, joten toistettu kutsu samalla operaatiotunnisteella jatkaa pollausta uudelleenlähetyksen sijaan. Valmiit erät istuvat välimuistissa, jonka avaimina ovat operaatiotunniste, sertifikaatti ja sormenjälki, kattolukuna MaxOperationCacheEntries (128), ja ne palautetaan syväkopioina. Kyseinen välimuisti asuu palveluntarjoajainstanssissa eikä selviä uudelleenkäynnistyksestä. Idempotenssiavain selviää, koska se on johdettua eikä satunnaista, joten uudelleenkäynnistetty prosessi, joka käyttää operaatiotunnistettaan uudelleen, lähettää saman avaimen — deduplikoiko palvelu sen perusteella, on palvelun lupaus, ei HotPDF:n

Miten saat CSC-allekirjoituksen PDF:ään?

Välitä palveluntarjoaja HPDFCMSSignPDFStreamWithProviderille yhdessä loppukäyttäjän sertifikaatin kanssa funktiolta GetCertificateChain; HotPDF rakentaa CMS SignedDatan ja palveluntarjoaja allekirjoittaa allekirjoitettujen attribuuttien tiivisteen. Syöte-PDF tarvitsee /ByteRange- ja /Contents-paikanpitäjät, jotka THPDFPage.AddSignedSignatureField kirjoittaa, täsmälleen niin kuin artikkelissa PAdES-allekirjoituksen työnkulku HotPDF:ssä, ja palveluntarjoajamalli on sama, jota käsittelee artikkeli HotPDF:n liitettävät allekirjoituspalveluntarjoajat ML-DSA:lle ja EdDSA:lle

var
  Chain: THPDFCSCCertificateChain;
  SignOpts: THPDFCMSSignOptions;
  Src, Dst: TFileStream;
begin
  if Provider.RefreshCredentialInfo <> spsValid then
    raise Exception.Create(Provider.LastError);
  Chain := Provider.GetCertificateChain;   // CSC luettelee loppukäyttäjän sertifikaatin ensin
  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-paikanpitäjän koon arviointi kulkee EstimateSignatureSizein kautta, joka palauttaa EstimatedSignatureBytesin, kun asetat sen, ja muuten RSA-moduluksen koon sertifikaatin avainpituudesta. ECDSA:lle aseta EstimatedSignatureBytes itse, tai arviointi raportoi spsUnsupportedin. Automaattikokoinen allekirjoitusvarianti allekirjoittaa uudelleen, kun paikanpitäjä osoittautuu liian pieneksi, ja se tekee niin vain palveluntarjoajille, jotka mainostavat spcSafeSignRetryia — mitä THPDFCSCSignatureProvider tekee vain, kun EnableIdempotency on päällä. PAdES-B-T-työnkuluille TimestampDigest pyytää aikaleimatokenin samalta palvelulta signatures/timestampin kautta, kattolukuna MaxTimestampBytes (1 MB)

Mitä CSC-palveluntarjoaja ei tee?

Se ei allekirjoita viestejä, vain tiivisteitä. Ed25519 ja Ed448 puhaassa tilassa antavat palveluntarjoajalle koko allekirjoitettujen attribuuttien viestin (sikMessage), ja erän validoija hylkää sen epämuodostuneena, koska signHash on määritelmällisesti tiivisteisiin pohjautuva. Palveluntarjoajayksikkö kääntyy Free Pascalin alla tavallisilla funktiotyypeillä anonyymien metodien sijaan, mutta palveluntarjoajavetoiset CMS-rakentajat nostavat poikkeuksen FPC:llä tänään, joten CSC-allekirjoituksen upottaminen PDF:ään on Delphi-polku

Se ei myöskään päätä politiikkaa. CredentialInfo raportoi avaimen tilan, sertifikaatin tilan, valtuutustilan, SCAL-tason ja multisign-rajan, mutta palveluntarjoaja ei kiellä käytöstä poistettua avainta tai SCAL1-sertifikaattia omatoimisesti — tarkista ne ennen kuin näytät allekirjoittajalle OTP-kehotteen. Ja yksi palveluntarjoajainstanssi allekirjoittaa yhden erän kerrallaan: SignHashBatch sarjoitetaan sisäisesti, joten kaksi säiettä ei voi kilpailla yhdestä SAD:sta, mikä tarkoittaa, että läpäisy tulee eräyttämisestä, ei palveluntarjoajan jakamisesta säietyöläisten kesken. Onko lopullinen allekirjoitus kvalifioitu, riippuu luottamuspalvelusta ja sen sertifikaatista, ei kirjastosta, joka kantoi tiivisteen sinne

CSC-palveluntarjoaja, CMS- ja PAdES-rakentajat sekä paikalliset ja PKCS#11-palveluntarjoajat toimitetaan kaikki HotPDF Delphi PDF -komponentissa