Technical Article

PAdES skaitmeniniai parašai „Delphi“ aplinkoje su „PDFlibPas“

Vieno PAdES parašo patvirtinimas reiškia trijų nepriklausomų dalykų patikrinimą, o žalias varnelis (check mark) peržiūros programoje praneša tik apie trečiąjį. Pirma, masyvas /ByteRange privalo apimti teisingus baitus: jo nurodyti intervalai turi tiksliai atkurti įvesties duomenis, iš kurių buvo apskaičiuota CMS santrauka, nepaliekant jokių pasirašytų baitų už jų ribų. Antra, sertifikatas CMS struktūroje turi būti susietas su patikimu šakniniu sertifikatu ir turėti pasirašytą pasirašymo sertifikato atributą (signing-certificate attribute), kurio reikalauja PAdES. Trečia, jei profilis reikalauja laiko žymos, RFC 3161 žetonas turi susieti parašo reikšmę su laiko momentu, buvusiu iki sertifikato galiojimo pabaigos. „Acrobat“ sujungia visus tris dalykus į vieną piktogramą; atitikties tikrintuvas juos skiria atskirai, ir jūsų kuriamas kodas taip pat turėtų daryti tą patį. „losLab PDF Library“ („PDFlibPas“) suteikia pasirašymo dalį, laiko žymos įterpimą ir audito funkcijas, leidžiančias patikrinti ByteRange prieš pasitikint failu.

Vienas skirtumas klaidina beveik kiekvieną, kuris pirmą kartą kuria PAdES integraciją, todėl jį verta įvardyti prieš rašant kodą. Parašas, sukurtas su parametru /SubFilter /adbe.pkcs7.detached, yra visiškai tvarkingas ISO 32000-1 §12.8 parašas, kurį „Acrobat“ rodys kaip validų. Tačiau tai nėra PAdES parašas, nes ETSI EN 319 142-1 reikalauja reikšmės ETSI.CAdES.detached kiekviename baziniame lygmenyje. eIDAS atitikties tikrinimo sistema atmes pirmąjį ir priims antrąjį, nors kriptografiškai jie yra vienodi. Profilis yra paties dokumento deklaracija apie save, o teisingas jos nustatymas „PDFlibPas“ bibliotekoje atliekamas vienu iškvietimu.

Kas paverčia PDF parašą PAdES parašu

ETSI EN 319 142-1 standartas apibrėžia keturis bazinius lygius, pastatytus ant CMS formato pagrindo. PAdES-B-B yra pradinis taškas: CAdES parašas PDF parašo lauke su SubFilter reikšme ETSI.CAdES.detached ir pasirašytu pasirašymo sertifikato atributu. PAdES-B-T prideda RFC 3161 laiko žymą ant parašo reikšmės, įrodančią, kad parašas egzistavo tam tikru laiko momentu, kurio niekas negali suklastoti. PAdES-B-LT įterpia sertifikatus, CRL sąrašus bei OCSP atsakymus, reikalingus patvirtinimui, į dokumento saugumo saugyklą (Document Security Store – DSS), kad failas išliktų patikrinamas net ir po to, kai jį išdavęs CA nustoja veikti. PAdES-B-LTA užbaigia šią struktūrą dokumento laiko žyma, kuri iš naujo apsaugo sukauptus įrodymus silpnėjant kriptografiniams algoritmams.

„PDFlibPas“ susieja šias sąvokas su savo pasirašymo proceso (sign-process) API. Profilio nustatymas atliekamas per SetSignProcessCustomSubFilter. Jei jūsų taisyklėms reikalingas įsipareigojimo tipo nurodymas (commitment-type: autorystės įrodymas, patvirtinimo įrodymas ar kiti ETSI identifikatoriai nuo 1 iki 6), tai nustatoma naudojant SetSignProcessCommitmentType. Konkreti parašo politika pridedama per SetSignProcessSignaturePolicy, kuri priima politikos OID ir jos santrauką. Viena numatytoji reikšmė reikalauja dėmesio: jei maišos algoritmas paliekamas automatinis, biblioteka parenka SHA-256 ETSI ir adbe.pkcs7.detached parašams, o prie SHA-1 grįžta tik pasenusiame adbe.pkcs7.sha1 kelyje. Nepaisant to, nustatykite jį eksplicitiškai. Auditoriai paklaus, kokį maišos algoritmą naudojote, o aiškiai nurodytą reikšmę kode apginti kur kas lengviau nei numatytąją, kuriai paaiškinti tektų skaityti dokumentaciją.

Bazinio parašo kūrimas

Plokščias API valdo pasirašymą kaip vienkartinę būsenų mašiną: atidaryti procesą su šaltinio failu, jį sukonfigūruoti, įrašyti rezultatą į išvesties failą bei nuskaityti gautą rezultatą. Žemiau pateikta kodo seka sukuria PAdES-B-B parašą naudojant SHA-256. Svarbiausia kodo eilutė netui nieko bendro su pačiu parašu: tai sąmoningai per didelės vietos /Contents rezervavimas, nes tai yra vienintelis dalykas, kurio vėliau negalėsite pakeisti, jei prie parašo reikės pridėti laiko žymą.

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Sig1');
    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256
    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // room for a timestamp later
    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

Metodas NewSignProcessFromFile grąžina 0, jei šaltinio failo išvis nepavyksta atidaryti. Vėliau GetSignProcessResult atskiria gamybinėje aplinkoje pasitaikančias klaidas: 4 reiškia neteisingą PDF slaptažodį, 7 – neteisingą PFX slaptažodį, 9 – sertifikato failą be privataus rakto, 10 – neįrašomą išvesties kelią, 11 – nesėkmę įrašant parašo baitus. Skaitinio kodo išsaugojimas žurnale šalia failo pavadinimo paverčia neaiškų pagalbos pranešimą vienos minutės trukmės diagnostika.

RFC 3161 laiko žymos pridėjimas, kurio biblioteka neatliks už jus

„PDFlibPas“ neturi integruoto TSA (Timestamp Authority) kliento, ir tai yra sąmoninga riba, o ne spraga. Biblioteka apskaičiuoja maišos kodą, kurį turi pasirašyti laiko žymos serveris, ir vėliau įterpia papildytą CMS; HTTP užklausa ir CMS struktūros keitimas paliekami atlikti kviečiančiajai programai. Tam yra svarbi techninė priežastis. „Windows CryptoAPI“ kontrolė, kuri prideda nepasirašytus atributus (CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR), sugenda su klaida CRYPT_E_INVALID_INDEX naudojant atskirtą (detached) SignedData formatą, kurį naudoja PAdES. Todėl papildytas CMS turi būti sugeneruotas per CMS kodavimo modulį jūsų pačių kode. Jokia biblioteka negali tyliai pridėti šio žetono viena paprasta sistemos funkcija, o tos, kurios tai deklaruoja, atlieka modifikaciją nematomoje vietoje.

var
  Pdf: TPDFlib;
  StsId: Integer;
  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
  Pdf := TPDFlib.Create;
  try
    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
    // both calls below are application code: an HTTP POST to your TSA,
    // and a CMS re-encode that attaches the token as an unsigned attribute
    TstDer := RequestTimeStampToken(HashHex);
    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
      raise Exception.Create('timestamp embedding failed');
    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
  finally
    Pdf.Free;
  end;
end;

Stebėkite klaidų kodus šioje vietoje: 12 reiškia, kad nurodytas parašo laukas neegzistuoja, 11 – kad nepavyko išanalizuoti esamo CMS, o 13 – kad papildytas CMS nebetelpa į rezervuotą /Contents vietą. 13-asis kodas yra pats nemaloniausias, nes vienintelis sprendimas yra pasirašyti iš naujo: įprastas laiko žymos žetonas su sertifikatų grandine užima apie 4–6 KB, o B-B žingsnio metu atliktas 8192 baitų rezervavimas buvo skirtas būtent tam, kad šis žingsnis turėtų pakankamai vietos.

Patvirtinimas prasideda nuo ByteRange, o ne nuo sertifikatų grandinės

Žalia varnelė peržiūros programoje yra tiesiog pasitikėjimo sprendimas pagal to kompiuterio sertifikatų saugyklą, o ne struktūrinis failo patvirtinimas. Programinis tikrinimas turėtų prasidėti giliau – nuo klausimo, kurį prieauginiai atnaujinimai padaro sudėtingą: kuriuos baitus iš tikrųjų apima kiekvienas parašas? Kiekvienas čia paminėtas papildymas (antrasis parašas, DSS žodynas ar dokumento laiko žyma) pridedamas kaip prieauginis atnaujinimas, ir kiekvienas iš jų prideda baitų už pradinio parašo /ByteRange ribų. Šie pridėti baitai yra visiškai teisėti. Tikrinimo sistema vis tiek privalo juos klasifikuoti pagal dokumento keitimo politiką (modification policy), o tam tikram laukui taikomas DocMDP lygis yra perskaitomas per GetSignatureDocMDPLevelByName.

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // before Open: SignDoc holds a share lock
  Doc := TPDFlibSignDoc.Create;
  try
    if not Doc.Open('invoice-bt.pdf', '', False) then
      raise Exception.Create('cannot open for audit');
    Names := TStringList.Create;
    try
      Doc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
        if Doc.GetSignatureValueObjNum(Names[I]) > 0 then   // >0 means actually signed
        begin
          B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
          B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
          B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
          B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
          if (B0 = 0) and (B2 + B3 = FileSize) then
            Writeln(Names[I], ': covers the file to EOF')
          else
            Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
        end;
    finally
      Names.Free;
    end;
    Doc.Close;
  finally
    Doc.Free;
  end;
end;

Šiame audito kelyje yra dveji spąstai. Metodas TPDFlibSignDoc.Open užrakina failą su išskirtiniu bendro naudojimo užraktu (share lock), todėl tikrintuvas, kuris taip pat nori apskaičiuoti neapdorotų failo baitų maišos kodą CMS patvirtinimui, turi įkelti failą į atmintį prieš atidarydamas jį auditui. Pakeiskite šią eilės tvarką, ir skaitymas nepavyks dėl jūsų pačių uždėto užrakto. Antrieji spąstai yra tylūs: plokščiojo API atitikmuo GetSignProcessByteRange grąžina 32 bitų sveikąjį skaičių (Integer), o tikrieji poslinkiai yra Int64, todėl viršijus 2 GB šis iškvietimas nukerta reikšmę be jokių įspėjimų – būtent todėl šiame pavyzdyje poslinkiai nuskaitomi per audito klasę. Taip pat verta paminėti, kad plokščiame API nėra jokio VerifySignature apvalkalo. Kriptografinius rezultatus grąžina klasės lygio TPDFlibSignatureVerifier (grąžinantis vsValid, vsInvalid arba vsUnknown) arba išorinė tikrinimo sistema, kuria pasitiki jūsų saugumo politika.

Ilgalaikis patvirtinimas: DSS, VRI ir dokumento laiko žyma

PAdES-B-LT lygis egzistuoja dėl to, kad atšaukimo infrastruktūra (revocation infrastructure) yra laikina. Standarto ETSI EN 319 142-1 §5.4.2.2 skyrius apibrėžia dokumento saugumo saugyklą (Document Security Store): dokumento lygio žodyną, kuriame saugomi sertifikatai, CRL sąrašai ir OCSP atsakymai, pasirinktinai indeksuojami kiekvienam parašui per VRI įrašus, susietus su parašo /Contents maišos kodu. „PDFlibPas“ procesas atkartoja laiko žymos struktūrą. Metodas NewPAdESDSSProcessFromFile pradeda procesą; AddPAdESDSSCertificate, AddPAdESDSSCRL ir AddPAdESDSSOCSP priima DER duomenis; AddPAdESDSSVRI susieja parinktą medžiagą su konkrečiu parašu; EndPAdESDSSProcessToFile įrašo viską kaip prieauginį atnaujinimą. Sudėtingiausia dalis lieka jūsų kode. Atšaukimo informacijos gavimas bei sprendimas, ar ji pakankamai nauja įterpimui, yra kviečiančiosios programos užduotis. Biblioteka garantuoja, kad žodynai atitinka standarto struktūrą, bet negali garantuoti, kad jūsų OCSP serveris pateikė teisingą atsakymą.

Archyvinis lygis B-LTA užbaigia šį procesą dokumento laiko žyma, kuri iš naujo apsaugo sukauptus įrodymus silpnėjant kriptografiniams algoritmams. Dokumento laiko žyma yra atskiras parašo laukas, kurio tipas yra DocTimeStamp (vietoj Sig), sukuriamas naudojant SetSignProcessDocTimeStamp su rezervuotu parašo ilgiu. Ji nepakeičia parašo laiko žymos iš B-T žingsnio. Parašo laiko žyma įrodo konkretaus parašo sukūrimo laiką, o dokumento laiko žyma apsaugo visą failą, įskaitant DSS įrodymus, ir yra tas elementas, kurį ilgalaikio saugojimo sistemos atnaujina kas kelerius metus. Brandus archyvinis profilis turi abu šiuos elementus. Senesnėms programoms, kurios neatpažįsta šių struktūrų, metodas TPDFlibSignDoc.EnsurePAdESExtensions užregistruoja ESIC plėtinį dokumento kataloge, pranešdamas, kad faile naudojamos ETSI apibrėžtos funkcijos.

Vieną reakciją verta iš anksto numatyti, nes tai atrodo kaip klaida, nors taip nėra. Peržiūros programa dažnai praneša „validity unknown“ faile, kurio PAdES struktūra yra visiškai teisinga. Pasitikėjimas ir failo struktūra yra skirtingi dalykai. Peržiūros programa tiesiog negali susieti pasirašiusiojo su šakniniu sertifikatu, kuriuo pasitikima tame kompiuteryje (tai įprasta naudojant privačius CA arba testinius sertifikatus), nors ByteRange auditas ir CMS patikrinimas yra sėkmingi. Sprendimas – teisingai įdiegti šakninį sertifikatą arba patikrinti jį pagal ES patikimų sertifikatų sąrašus (jei tikslas yra kvalifikuotas eIDAS statusas), užuot keitus pasirašymo kodą.

Norėdami sužinoti apie parašo laukų analizę, ByteRange išdėstymus ir DocMDP lygius visame dokumentų rinkinyje, skaitykite susijusį straipsnį Atitikties ir pasirašymo darbo aplinka. Pasirašyti dokumentai, kurie taip pat privalo atitikti archyvavimo politiką, aprašyti straipsnyje PDF/A ir PDF/UA preflight patikra Delphi aplinkoje. Pilną API dokumentaciją ir bandomąsias versijas rasite puslapyje losLab PDF Library for Delphi.