Bài viết kỹ thuật

Chữ ký số PAdES trong Delphi: Ký và xác thực bằng PDF Library for Delphi

Thẩm định một chữ ký PAdES nghĩa là kiểm tra ba thứ độc lập, và dấu tích xanh trong trình xem chỉ cho bạn biết về thứ thứ ba. Thứ nhất, mảng /ByteRange phải bao đúng những byte cần bao: các đoạn nó nêu tên phải tái dựng chính xác đầu vào mà digest của CMS đã lấy trên đó, không để sót byte nào đã ký nằm ngoài. Thứ hai, chứng thư bên trong CMS phải nối được về một gốc bạn tin cậy và mang thuộc tính đã ký signing-certificate mà PAdES đòi hỏi. Thứ ba, nếu hồ sơ tuyên bố có dấu thời gian, một token RFC 3161 phải ràng giá trị chữ ký vào một thời điểm trước khi chứng thư hết hạn. Acrobat gộp cả ba vào một biểu tượng; một bộ kiểm tra tuân thủ giữ chúng tách bạch, và mã tạo ra những tệp này cũng nên vậy. losLab PDF Library (PDF Library for Delphi) cho bạn phía ký của việc đó, khâu nhúng lại dấu thời gian, cùng các lệnh gọi kiểm toán để soi một ByteRange trước khi bạn đặt niềm tin vào nó

Có một phân biệt làm vấp gần như mọi bản triển khai PAdES đầu tiên, nên đáng nói ra trước khi có bất kỳ dòng mã nào. Một chữ ký ghi với /SubFilter /adbe.pkcs7.detached là chữ ký hoàn toàn chuẩn theo ISO 32000-1 §12.8 và Acrobat sẽ báo là hợp lệ. Nó cũng không phải chữ ký PAdES, vì ETSI EN 319 142-1 đòi ETSI.CAdES.detached ở mọi mức baseline. Một bộ kiểm tra tuân thủ eIDAS từ chối cái thứ nhất và chấp nhận cái thứ hai dù phần mật mã y hệt nhau. Hồ sơ là một tuyên bố mà tài liệu tự đưa ra về chính mình, và đặt tuyên bố đó cho đúng chỉ tốn một lệnh gọi trong PDF Library for Delphi

Điều gì biến một chữ ký PDF thành chữ ký PAdES

ETSI EN 319 142-1 định nghĩa bốn mức baseline xếp chồng lên định dạng CMS. PAdES-B-B là điểm vào: một chữ ký CAdES nằm trong trường chữ ký PDF với SubFilter ETSI.CAdES.detached và một thuộc tính đã ký signing-certificate. PAdES-B-T thêm một dấu thời gian RFC 3161 phủ lên giá trị chữ ký, chứng minh chữ ký đã tồn tại trước một thời điểm mà không ai lùi ngày được. PAdES-B-LT nhúng các chứng thư, CRL và phản hồi OCSP cần cho việc thẩm định vào một Document Security Store, nhờ đó tệp vẫn kiểm chứng được sau khi CA phát hành cho hạ tầng của mình nghỉ hưu. PAdES-B-LTA đóng nắp chồng này bằng một dấu thời gian tài liệu, bảo vệ lại toàn bộ chứng cứ đã tích lũy khi các thuật toán yếu dần

PDF Library for Delphi ánh xạ những khái niệm này lên API sign-process của nó. Dấu hiệu hồ sơ là SetSignProcessCustomSubFilter. Nếu chính sách của bạn cần một chỉ dẫn commitment-type (chứng minh nguồn gốc, chứng minh sự phê duyệt, hoặc một trong các định danh ETSI khác đánh số từ 1 đến 6), thứ đó đi qua SetSignProcessCommitmentType. Một chính sách chữ ký tường minh gắn vào bằng SetSignProcessSignaturePolicy, hàm này nhận OID của chính sách cùng digest của nó. Một giá trị mặc định đáng lưu tâm: khi thuật toán digest để ở chế độ tự động, thư viện chọn SHA-256 cho chữ ký ETSI và adbe.pkcs7.detached, và chỉ lùi về SHA-1 trên đường cũ adbe.pkcs7.sha1. Dù vậy hãy cứ đặt tường minh. Kiểm toán viên sẽ hỏi bạn dùng hàm băm nào, và một giá trị viết rõ trong mã dễ bảo vệ hơn một mặc định mà bạn phải giở sổ tay ra mới giải thích được

Bậc thang các mức baseline PAdES B-B, B-T, B-LT và B-LTA dựng bằng PDF Library for Delphi, cho thấy mỗi mức thêm dấu thời gian, bằng chứng DSS hay dấu thời gian tài liệu tái tạo được lên trên lõi ETSI.CAdES.detached
Mỗi mức baseline ETSI xếp thêm một lớp bảo đảm lên cùng một lõi CAdES, từ signed attributes tới document timestamp có thể gia hạn

Tạo ra chữ ký baseline

API phẳng điều khiển việc ký như một máy trạng thái chạy một lần: mở một tiến trình trên tệp nguồn, cấu hình nó, kết thúc ra tệp đầu ra, đọc mã kết quả. Trình tự bên dưới tạo ra một chữ ký PAdES-B-B với SHA-256. Dòng quan trọng nhất lại chẳng liên quan gì tới bản thân chữ ký. Đó là phần dành chỗ /Contents cố ý làm rộng dư, bởi đó là thứ duy nhất bạn không đổi được về sau nếu có lúc phải thêm dấu thời gian cho chữ ký này

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);  // chừa chỗ cho dấu thời gian sau này
    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;

NewSignProcessFromFile trả về 0 khi hoàn toàn không mở được nguồn. Sau đó, GetSignProcessResult tách bạch những kiểu hỏng thật sự xảy ra trên môi trường sản xuất: 4 nghĩa là sai mật khẩu PDF, 7 là sai mật khẩu PFX, 9 là tệp chứng thư không có khóa riêng, 10 là đường dẫn đầu ra không ghi được, 11 là lỗi trong lúc áp các byte chữ ký. Ghi mã số đó cạnh tên tệp đầu vào sẽ biến một phiếu hỗ trợ mơ hồ thành một chẩn đoán một phút

Thêm dấu thời gian RFC 3161 mà thư viện sẽ không lấy hộ bạn

PDF Library for Delphi không đi kèm TSA client, và đó là một ranh giới cố ý chứ không phải lỗ hổng. Thư viện tính hàm băm mà nhà cấp dấu thời gian phải ký đối chứng rồi nhúng lại khối CMS đã bổ sung sau đó; phần trao đổi HTTP và ca mổ CMS ở giữa thuộc về bên gọi. Có một lý do kỹ thuật cứng cho sự phân chia này. Điều khiển của Windows CryptoAPI vốn trên danh nghĩa thêm được các thuộc tính chưa ký, tức CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, lại hỏng với CRYPT_E_INVALID_INDEX trên bố cục SignedData tách rời mà PAdES dùng. Vậy nên khối CMS nâng cao phải đến từ một bộ mã hóa CMS do chính bạn kiểm soát. Không thư viện nào có thể lặng lẽ gấp token vào bằng một lệnh gọi hệ thống, và cái nào tuyên bố làm được thì đang mổ ở chỗ bạn không nhìn thấy

Pipeline thêm dấu thời gian RFC 3161 vào chữ ký PAdES trong Delphi, tách phần băm và nhúng của PDF Library for Delphi khỏi yêu cầu TSA và việc mã hóa lại CMS của caller bên trong khoảng /Contents đã dành sẵn
Thư viện hash và nhúng lại trong khi mã của bạn lấy token và thực hiện phẫu thuật CMS, và kết quả phải nằm gọn trong vùng đặt chỗ /Contents 8192 byte
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);
    // cả hai lệnh gọi dưới đây là mã ứng dụng: một HTTP POST tới TSA của bạn,
    // và một lượt mã hóa lại CMS gắn token vào như thuộc tính chưa ký
    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;

Hãy để ý các mã kết quả ở đây: 12 nghĩa là trường chữ ký được nêu tên không tồn tại, 11 là không phân tích được khối CMS hiện có, còn 13 là khối CMS đã bổ sung không còn vừa chỗ giữ /Contents đã dành sẵn. Mã 13 mới là cái đau, vì cách chữa duy nhất là ký lại: một token dấu thời gian điển hình cùng chuỗi chứng thư của nó chiếm 4 tới 6 KB, và phần dành 8192 byte làm ở bước B-B tồn tại chính là để bước này có chỗ đáp xuống

Thẩm định bắt đầu từ ByteRange, không phải từ chuỗi chứng thư

Dấu tích xanh trong trình xem là một quyết định về mức tin cậy dựa trên kho chứng thư của chính máy đó, không phải phán quyết về cấu trúc của tệp. Việc thẩm định bằng mã nên bắt đầu từ tầng thấp hơn, với câu hỏi mà incremental update khiến trở nên tinh vi: mỗi chữ ký thực sự bao phủ những byte nào? Mọi phần nâng cao bàn ở đây, dù là chữ ký thứ hai, một DSS dictionary hay một dấu thời gian tài liệu, đều đến qua incremental update, và mỗi lần cập nhật nối thêm byte nằm ngoài /ByteRange của chữ ký trước. Những byte nối thêm đó là hợp lệ. Bộ thẩm định vẫn phải phân loại chúng theo chính sách sửa đổi của tài liệu, và mức DocMDP theo từng trường mà chính sách đó nằm trong có thể đọc bằng GetSignatureDocMDPLevelByName

Kiểm toán bố cục byte của một PDF đã ký trong Delphi cho thấy các đoạn ByteRange bao phủ, các byte /Contents bị loại, các cập nhật tăng dần nối thêm nằm ngoài phạm vi và phán quyết độ bao phủ so với kích thước tệp
Hai vùng được bao phủ, loại trừ byte của chính chữ ký, kể đúng câu chuyện bao phủ, và các bản cập nhật nối thêm được phân loại theo chính sách DocMDP thay vì bị coi là mối đe dọa
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // trước Open: SignDoc giữ một 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 nghĩa là đã ký thật
        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;

Có hai cái bẫy nằm trên đường kiểm toán này. TPDFlibSignDoc.Open giữ tệp bằng một share lock độc quyền, nên một bộ thẩm định đồng thời muốn băm các byte thô của tệp để kiểm chứng CMS thì phải đọc tệp vào bộ nhớ trước khi mở nó để kiểm toán. Đảo thứ tự đó thì lượt đọc hỏng vì đúng cái khóa bạn tự đặt ra. Cái bẫy thứ hai lặng chứ không ồn: hàm tương ứng ở API phẳng là GetSignProcessByteRange trả về Integer trong khi các offset bên dưới là Int64, nên vượt mốc 2 GB thì lệnh gọi phẳng cắt cụt mà không kêu ca, và đó là lý do ví dụ này lấy offset qua lớp kiểm toán. Cũng đáng nêu tên một sự vắng mặt. Tầng phẳng hoàn toàn không có lớp bọc VerifySignature. Phán quyết về mật mã đến từ TPDFlibSignatureVerifier ở tầng lớp, hàm này trả về vsValid, vsInvalid hoặc vsUnknown, hoặc từ một bộ thẩm định ngoài mà chính sách tuân thủ của bạn vốn đã tin cậy

Thẩm định dài hạn: DSS, VRI và dấu thời gian tài liệu

PAdES-B-LT tồn tại bởi hạ tầng thu hồi chứng thư cũng có tuổi thọ hữu hạn. ETSI EN 319 142-1 §5.4.2.2 quy định Document Security Store: một dictionary ở cấp tài liệu mang các chứng thư, CRL và phản hồi OCSP, tùy chọn đánh chỉ mục theo từng chữ ký qua các mục VRI khóa bằng hàm băm của /Contents mỗi chữ ký. Luồng của PDF Library for Delphi phản chiếu thiết kế của phần dấu thời gian. NewPAdESDSSProcessFromFile mở tiến trình; AddPAdESDSSCertificate, AddPAdESDSSCRLAddPAdESDSSOCSP nhận các khối DER; AddPAdESDSSVRI ràng phần tư liệu đã chọn vào một chữ ký; EndPAdESDSSProcessToFile ghi tất cả dưới dạng incremental update. Phần khó vẫn ở phía bạn. Việc lấy tư liệu thu hồi về, và phán đoán xem nó có đủ tươi mới để đáng nhúng vào hay không, là việc của bên gọi. Thư viện bảo đảm các dictionary đúng chuẩn về cấu trúc; nó không thể bảo đảm rằng bộ trả lời OCSP của bạn đã nói thật

Điểm cuối dành cho lưu trữ, B-LTA, thêm một dấu thời gian tài liệu: một trường chữ ký riêng có kiểu là DocTimeStamp chứ không phải Sig, tạo ra qua SetSignProcessDocTimeStamp với một độ dài chữ ký được dành sẵn. Nó không thay thế dấu thời gian chữ ký từ bước B-T. Dấu thời gian chữ ký chứng minh một chữ ký cụ thể đã tồn tại khi nào; dấu thời gian tài liệu bảo vệ cả tệp, kể cả chứng cứ trong DSS, và là thành phần mà một kho lưu trữ dài hạn gia hạn vài năm một lần khi các thuật toán yếu dần. Một hồ sơ lưu trữ chín chắn mang cả hai. Với những trình đọc ra đời trước các cấu trúc này, TPDFlibSignDoc.EnsurePAdESExtensions ghi phần mở rộng nhà phát triển ESIC vào catalog của tài liệu, công bố rằng tệp có dùng các tính năng do ETSI định nghĩa

Có một phản ứng đối với tất cả những điều này đáng chặn trước, vì nó trông như lỗi mà không phải. Một trình xem thường báo "validity unknown" trên một tệp có cấu trúc PAdES hoàn toàn đúng. Sự tin cậy và cấu trúc là hai trục độc lập. Đơn giản là trình xem không nối được người ký về một gốc mà nó tin trên máy đó, chuyện thường gặp với các CA riêng và chứng thư thử nghiệm, ngay cả khi cả phần kiểm toán ByteRange lẫn kiểm chứng CMS đều qua. Cách chữa là phân phối chứng thư gốc cho đúng cách, hoặc đánh giá dựa trên các danh sách tin cậy của EU khi mục tiêu thật sự là trạng thái eIDAS đủ điều kiện, chứ không phải đụng vào mã ký

Về góc nhìn từ phía kiểm toán, tức liệt kê các trường chữ ký trên cả một kho tài liệu, kết xuất bố cục ByteRange và đọc hàng loạt mức DocMDP, xem bài đồng hành về workbench tuân thủ và ký. Những tài liệu đã ký mà còn phải thỏa chính sách lưu trữ thì thuộc về quy trình mô tả trong preflight PDF/A và PDF/UA trong Delphi. Tài liệu API đầy đủ và bản tải dùng thử nằm trên trang sản phẩm losLab PDF Library for Delphi