บทความเทคนิค

เช็กนโยบาย ECDSA กับ EdDSA ตาม ISO/TS 32002 ด้วย PDFium

PDFium Component for Delphi ตรวจลายเซ็น PDF แบบ ECDSA กับ EdDSA ทุกตัวกับโปรไฟล์ algorithm ของ ISO/TS 32002 แล้วรายงานคำตัดสินใน TPadesSignatureValidation.AlgorithmPolicyStatus ผ่านได้มีแค่ P-256, P-384, P-521, เส้นโค้ง Brainpool r1 สามเส้น, Ed25519 และ Ed448 โดยแต่ละตัวต้องจับคู่ digest ที่ตรงกัน ความไม่ตรงกันจะเรียก ppeiSignatureAlgorithmMismatch ขึ้น นโยบายนี้สำคัญกว่าที่ฟังดู เซ็นบน brainpoolP160r1 หรือ key P-256 เซ็น digest SHA-512 สามารถ verify ผ่านสวยงามในระดับคณิตศาสตร์ Windows CryptoAPI จึงรายงานค่าลายเซ็นว่าใช้ได้ ขณะที่ตัว validate PDF 2.0 แบบเข้มงวดปฏิเสธไฟล์ การเช็กนโยบายปิดช่องว่างนี้ และถูกแยกออกจากคำถามว่าไบต์ของลายเซ็นถูกต้องเชิง crypto หรือไม่อย่างตั้งใจ

ISO/TS 32002 อนุญาตอะไรกันแน่สำหรับลายเซ็นเส้นโค้งวงรี

ISO/TS 32002 อนุญาตเส้นโค้ง ECDSA หกเส้นกับโครงการ EdDSA สองโครงในลายเซ็น PDF เป๊ะ ๆ และผูกแต่ละเส้นโค้งกับขนาด digest ที่มันพกได้ PDFium Component เข้ารหัสตารางนี้ใน PadesCurveDigestAllowed โดยใช้ OID ของเส้นโค้งจากใบรับรองผู้เซ็นเป็น key เส้นโค้ง NIST เข้มงวด: digest ต้องกว้างเท่ากับเส้นโค้งพอดีเป็นบิต จะ SHA-2 หรือ SHA-3 ก็ได้ เส้นโค้ง Brainpool หลวมกว่า รับความกว้างของตัวเองหรือกว้างกว่านั้นก็ยอม:

  • P-256 (1.2.840.10045.3.1.7): ได้แค่ SHA-256 หรือ SHA3-256
  • P-384 (1.3.132.0.34): ได้แค่ SHA-384 หรือ SHA3-384
  • P-521 (1.3.132.0.35): ได้แค่ SHA-512 หรือ SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): digest SHA-2 หรือ SHA-3 ใด ๆ ตั้งแต่ 256 ถึง 512 บิต
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 ขนาด 384 หรือ 512 บิต
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): ได้แค่ SHA-512 หรือ SHA3-512
เมทริกซ์ของโปรไฟล์ algorithm ISO TS 32002 ใน PDFium Component: P-256, P-384 และ P-521 รับเฉพาะความกว้าง digest ที่ตรงกันใน PadesCurveDigestAllowed, brainpoolP256r1 รับ 256 ถึง 512 บิต, brainpoolP384r1 รับ 384 กับ 512, Ed25519 กับ Ed448 ประกาศ SHA-512 กับ SHAKE256 ยาว 512 และความไม่ตรงกันเรียก ppeiSignatureAlgorithmMismatch ขึ้น ส่วนเส้นโค้งนอกโปรไฟล์คืน pcsUnsupported
เส้นโค้ง ECDSA หกเส้นกับโครงการ EdDSA สองโครงผ่านได้ และแต่ละเส้นถูกผูกกับความกว้าง digest ที่พกได้ ที่เหลือทั้งหมดไม่ถูกต้องหรือไม่รองรับ ไม่มีการยอมผ่านเงียบ ๆ เด็ดขาด

EdDSA ไม่มีทางเลือกเส้นโค้งและไม่มีทางเลือก digest นั่นแหละคือเหตุที่กฎของมันเกี่ยวกับการเข้ารหัสมากกว่าความแข็งแรง ตาม RFC 8419 SignerInfo ของ Ed25519 ต้องประกาศ SHA-512 เป็น digestAlgorithm โดยไม่มี parameter และ SignerInfo ของ Ed448 บนเส้นทาง signed-attributes ที่ PAdES ใช้เสมอ ต้องประกาศ id-shake256-len (2.16.840.1.101.3.4.2.18) พร้อม parameter INTEGER ที่ 512 เป๊ะ ๆ สำหรับทั้งสองโครงการ AlgorithmIdentifier ของลายเซ็นและ AlgorithmIdentifier ของ public key ในใบรับรองต้องไม่พก parameter เลย ผู้ผลิตที่เขียน NULL ลงตรงนั้น ซึ่งเป็นนิสัยที่ตัวเข้ารหัส RSA ฝังไว้ใน library ASN.1 จำนวนมาก จะได้ลายเซ็นที่ไม่สอดคล้อง ทั้งที่ key กับค่าลายเซ็นปกติดี

PDFium Component ดึงชุด algorithm สามตัวออกจาก CMS อย่างไร

PDFium เองตอบคำถามนี้ไม่ได้ เพราะ public signature API ของมันอ่าน signature dictionary แต่ไม่ verify CMS และไม่เปิดเผยเส้นโค้งของใบรับรองผู้เซ็น ชั้นตรวจ PAdES ที่สร้างบน PDFium จึง parse CMS SignedData (RFC 5652) เอง InspectPadesSignatureAlgorithm อ่าน digestAlgorithm กับ signatureAlgorithm ของ SignerInfo ตัวแรก แล้วหาใบรับรองผู้เซ็นและอ่าน SubjectPublicKeyInfo เพื่อเอา algorithm ของ key กับเส้นโค้ง การค้นใบรับรองถูกจำกัดขอบเขตอย่างตั้งใจ: ใบรับรองในชุด certificates ของ CMS ถูกพิจารณามากที่สุด 64 ใบ การ match คือการเทียบไบต์เป๊ะ ๆ ของ issuer กับหมายเลข serial จาก issuerAndSerialNumber และโค้ดจะยอมตกไปใช้ "ใบเดียวที่มีอยู่" ต่อเมื่อชุดนั้นมีใบรับรองที่ parse ได้พอดีหนึ่งใบ การหยิบใบรับรอง EC ตัวแรกจากชุดที่ไม่มีลำดับจะง่ายกว่ามาก แต่นั่นจะปล่อยให้ใบรับรอง CA เป็นตัวตัดสินว่าผู้เซ็นใช้เส้นโค้งอะไร

PDFium Component ตรวจชุด algorithm สามตัวของลายเซ็น PDF อย่างไร: InspectPadesSignatureAlgorithm อ่าน digestAlgorithm กับ signatureAlgorithm ของ SignerInfo ตัวแรกใน CMS ตรึงใบรับรองผู้เซ็นด้วยการ match issuerAndSerialNumber เป๊ะ ๆ จากผู้สมัครไม่เกิน 64 ราย อ่าน SubjectPublicKeyInfo เพื่อเอาเส้นโค้ง แล้ว EvaluatePadesSignatureAlgorithm คืน AlgorithmPolicyStatus
PDFium เองไม่ verify CMS และไม่เปิดเผยเส้นโค้งของผู้เซ็น ชั้น PAdES จึง parse SignedData เองแล้วเก็บ OID ดิบทุกตัวไว้ใน record เพื่อให้การปฏิเสธอธิบายได้
uses
  PDFium, FPdfPades;

const
  StatusNames: array[TPadesCryptoStatus] of string =
    ('not checked', 'valid', 'invalid', 'unsupported', 'indeterminate');

var
  Pdf: TPdf;
  R: TPadesValidationResult;
  A: TPadesSignatureAlgorithmInfo;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice-ecdsa-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    for I := 0 to Length(R.Signatures) - 1 do
    begin
      A := R.Signatures[I].AlgorithmInfo;
      Writeln('Signature ', I);
      Writeln('  digestAlgorithm    : ', string(A.DigestAlgorithmOid));
      Writeln('  signatureAlgorithm : ', string(A.SignatureAlgorithmOid));
      Writeln('  public key / curve : ', string(A.PublicKeyAlgorithmOid),
        ' / ', string(A.CurveOid));
      Writeln('  policy             : ',
        StatusNames[R.Signatures[I].AlgorithmPolicyStatus]);
    end;
    if ppeiSignatureAlgorithmMismatch in R.Issues then
      Writeln('At least one signature violates the ISO/TS 32002 profile');
  finally
    Pdf.Free;
  end;
end;

TPdf.ValidatePades บังคับนโยบายนี้เป็นส่วนหนึ่งของรอบประเมินความสอดคล้อง เริ่มจากใบรับรองที่มันหาเจอข้างใน CMS ส่วน TPdf.ValidatePadesTrust รันซ้ำกับใบรับรองผู้เซ็นที่ Windows CryptoAPI ใช้ verify จริง ใบที่ CryptoAPI รายงานจึงมีคำพิพากษาสุดท้าย ข้อมูลดิบทุกตัวลงไปอยู่ใน TPadesSignatureAlgorithmInfo รวมถึง DigestParametersPresent, DigestParameterBits, SignatureParametersPresent และ PublicKeyParametersAreNamedCurve การปฏิเสธจึงอธิบายได้จาก record เสมอ ไม่ต้องไล่จากบรรทัด log

ทำไมลายเซ็น P-256 ที่ใช้ SHA3-256 ถึงไม่ผ่านนโยบาย

ลายเซ็น P-256 ไม่ผ่านนโยบายของ PDFium Component ทุกครั้งที่ digestAlgorithm ใน CMS กับ digest ที่ signatureAlgorithm แบบ ECDSA บอกโดยนัยไม่ตรงกัน แม้ทั้งคู่จะยอมรับได้เป็นรายตัวสำหรับเส้นโค้งก็ตาม EvaluatePadesSignatureAlgorithm แปลง ecdsa-with-SHA256, ecdsa-with-SHA3-256 กับเพื่อนร่วมสายพันธุ์เป็น digest ก่อน เทียบกับ digestAlgorithm ที่ประกาศไว้ แล้วคืน pcsInvalid เมื่อต่างกันแม้แต่นิดเดียว ก่อนจะไปเปิดตารางเส้นโค้ง กรณีนี้เกิดจริง: เครื่องมือเซ็นเปลี่ยน hash ไปใช้ SHA3-256 แต่คง identifier ecdsa-with-SHA256 ที่ hardcode ไว้ ผลคือไฟล์ที่ไม่มี verifier ที่สอดคล้องตัวอื่นตีความได้อย่างสม่ำเสมอ ฟังก์ชันนี้เป็น public จึงตรึงเมทริกซ์ไว้ใน unit test ได้โดยไม่ต้องสร้าง PDF:

var
  Info: TPadesSignatureAlgorithmInfo;
begin
  Info := Default(TPadesSignatureAlgorithmInfo);
  Info.Family := psafEcdsa;
  Info.PublicKeyAlgorithmOid := '1.2.840.10045.2.1';      // id-ecPublicKey
  Info.PublicKeyParametersPresent := True;
  Info.PublicKeyParametersAreNamedCurve := True;
  Info.CurveOid := '1.2.840.10045.3.1.7';                 // P-256
  Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.8';    // SHA3-256
  Info.SignatureAlgorithmOid := '2.16.840.1.101.3.4.3.10'; // ecdsa-with-SHA3-256
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsValid);

  Info.SignatureAlgorithmOid := '1.2.840.10045.4.3.2';    // ecdsa-with-SHA256
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsInvalid); // digest ไม่ตรงกัน

  Info.CurveOid := '1.3.36.3.3.2.8.1.1.1';                // brainpoolP160r1
  Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.1';    // SHA-256 ตอนนี้สอดคล้องแล้ว
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // เส้นโค้งไม่อยู่ในโปรไฟล์
end;

การเข้ารหัสเส้นโค้งได้รับความเข้มงวดแบบเดียวกัน RFC 5480 §2.1.1 ยอมให้ ECParameters เป็น OID เส้นโค้งแบบมีชื่อ, เส้นโค้งโดยนัย (NULL) หรือชุด parameter แบบชัดเจนเต็มรูป และโปรไฟล์ PKIX บังคับแบบมีชื่อ PDFium Component คืน pcsInvalid เมื่อใบรับรอง id-ecPublicKey ไม่พก parameter, พก parameter แบบโดยนัย หรือพก parameter แบบชัดเจน เพราะ parameter แบบชัดเจนเปิดทางให้ผู้โจมตีบรรยายเส้นโค้งที่หน้าตาแค่เหมือนเส้นมาตรฐานตัวหนึ่ง เส้นโค้งที่ตั้งชื่อถูกต้องแต่แค่ไม่อยู่ในรายการ ISO/TS 32002 อย่าง brainpoolP160r1 ข้างบนหรือ secp256k1 จะได้ pcsUnsupported แทน

invalid, unsupported หรือ indeterminate: อ่านสถานะอย่างตรงไปตรงมา

สถานะที่ไม่ใช่ valid สามตัวของ AlgorithmPolicyStatus หมายถึงคนละเรื่อง และการทุบใส่ถังเดียวกันว่า "พัง" ก็ทิ้งข้อมูลที่ผู้ตรวจสอบต้องใช้ไป pcsInvalid หมายถึงชุด algorithm ที่รู้จักมันผิดรูปหรือไม่ตรงกัน มันเติม ppeiSignatureAlgorithmMismatch เข้า TPadesValidationResult.Issues แล้วดัน IntegrityStatus รวมไปเป็น pcsInvalid IsCryptographicallyValid จึงคืน False แม้ค่าลายเซ็นของ CMS จะผ่านก็ตาม pcsUnsupported หมายถึงเส้นโค้งหรือ digest อยู่นอกสิ่งที่โปรไฟล์ระบุชื่อ ซึ่งเป็นผลด้านความสามารถ ไม่ใช่หลักฐานว่าถูกแก้ไข pcsIndeterminate หมายถึงตรึงใบรับรองผู้เซ็นไม่ได้ มักเป็น CertificateSet ที่มีหลายตัวเลือกแต่ไม่มีการ match issuerAndSerialNumber เป๊ะ ๆ โค้ดจึงปฏิเสธที่จะเดาเส้นโค้ง ตั้งแต่ v3.124.0 มันยังตีตราลายเซ็น RSA บน SHA-1 หรือ digest 112 บิตอย่าง SHA-224 ด้วย ซึ่งไม่ได้รับฉันทามติสำหรับการตรวจสอบปัจจุบันแล้ว การแบ่งแบบเดียวกันใช้กับ EdDSA บนเครื่องที่ CryptoAPI verify Ed25519 หรือ Ed448 ไม่ได้: CmsSignatureStatus ค้างที่ pcsUnsupported ขณะที่ AlgorithmPolicyStatus ยังเป็น pcsValid ได้ เพราะการเข้ารหัสถูกต้อง มีแค่ตัว verify ที่ขาด ถ้ากำลังไล่ตามการปฏิเสธจาก Adobe หรือตัว validate บน DSS คู่มือว่าทำไมตัว validate ถึงปฏิเสธลายเซ็น PAdES ครอบคลุมสาเหตุทั่วไปอีกล็อต

เส้นทางตัดสินใจของ EvaluatePadesSignatureAlgorithm ใน PDFium Component: digest ไม่ตรงกันตั้ง pcsInvalid กับ ppeiSignatureAlgorithmMismatch, เส้นโค้งที่มีชื่อซึ่งอยู่นอกโปรไฟล์ ISO TS 32002 อย่าง brainpoolP160r1 ตั้ง pcsUnsupported, ใบรับรองผู้เซ็นที่ตรึงไม่ได้ตั้ง pcsIndeterminate และตั้งแต่ v3.124.0 สาขา RSA ใช้ชุด digest ของ ETSI TS 119 312 ส่วนขนาด key ยังเป็นหน้าที่ของแอปพลิเคชัน
สถานะที่ไม่ใช่ valid ทั้งสามหมายถึงคนละเรื่อง: invalid คือหลักฐานว่าชุดค่าผสมพัง, unsupported คือผลด้านความสามารถ และ indeterminate คือโค้ดปฏิเสธที่จะเดา
var
  Pdf: TPdf;
  Options: TPadesTrustValidationOptions;
  R: TPadesValidationResult;
  S: TPadesSignatureValidation;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice-ecdsa-signed.pdf';
    Pdf.Active := True;
    Options := TPadesTrustValidationOptions.Default; // offline ไม่เช็ก revocation
    R := Pdf.ValidatePadesTrust(Options);
    for I := 0 to Length(R.Signatures) - 1 do
    begin
      S := R.Signatures[I];
      if S.IsDocumentTimeStamp then
        Continue;
      case S.AlgorithmPolicyStatus of
        pcsInvalid:       Writeln(I, ': reject, algorithm combination is invalid');
        pcsUnsupported:   Writeln(I, ': manual review, curve or digest outside the profile');
        pcsIndeterminate: Writeln(I, ': manual review, signer not identified or digest no longer agreed');
        pcsValid:
          if S.AlgorithmInfo.Family = psafRsa then
            Writeln(I, ': RSA digest suite accepted, check key size yourself')
          else
            Writeln(I, ': EC/EdDSA profile satisfied');
      end;
    end;
    Writeln('Cryptographically valid: ', R.IsCryptographicallyValid);
  finally
    Pdf.Free;
  end;
end;

pcsValid ไม่การันตีอะไร

AlgorithmPolicyStatus = pcsValid การันตีแค่ว่าลายเซ็น ECDSA หรือ EdDSA ใช้เส้นโค้งที่ได้รับอนุมัติพร้อม digest ที่สอดคล้องและเข้ารหัสถูกต้อง และลายเซ็น RSA ใช้ digest จากชุดปัจจุบัน มันไม่ได้พูดอะไรเลยเรื่องค่าลายเซ็นถูกต้องหรือไม่ ก่อน v3.124.0 สาขา RSA ของ EvaluatePadesSignatureAlgorithm กว้างไว้ตั้งใจ: signatureAlgorithm ใด ๆ ใต้ arc PKCS #1 1.2.840.113549.1.1.* คืน pcsValid รวมถึง sha1WithRSAEncryption รุ่นคร่ำครึด้วย ตั้งแต่ PDFiumPas v3.124.0 สาขา RSA ใช้ชุดลายเซ็นของ ETSI TS 119 312 digest MD2, MD4 และ MD5 เป็น pcsInvalid SHA-1 กับ digest 112 บิตอย่าง SHA-224 เป็น pcsIndeterminate ลายเซ็น SHA-1 จึงคงผล integrity รวมของมันและถูกติดธงให้ทบทวน แทนที่จะถูกปฏิเสธ digestAlgorithm ที่ต่างจาก digest ที่ algorithm ลายเซ็นกำหนดตายตัว อย่าง sha256WithRSAEncryption บน digest SHA-1 หรือ algorithm ลายเซ็น RSA บน key ผู้เซ็นที่ไม่ใช่ RSA เป็น pcsInvalid รายงานเป็น ppeiSignatureAlgorithmMismatch และไม่ผ่าน integrity ใบรับรองผู้เซ็นที่หาไม่เจอให้ pcsIndeterminate เหมือนที่เคยเป็นกับ ECDSA และ digest ที่ไม่รู้จักหรือ OID RSA ที่ไม่ใช่ลายเซ็นให้ pcsUnsupported ความยาว modulus ยังไม่ถูกเช็ก, parameter ของ PSS ไม่ถูกตรวจที่นี่ (บทความเรื่องRSASSA-PSS-params ตาม RFC 4055 เล่าว่ามันถูกเข้ารหัสฝั่งผู้เซ็นอย่างไร) และ digest SHA-1 หรือ MD5 ยังเรียก issue ppeiBadDigestAlgorithm แยกต่างหากขึ้นด้วย เช่นกัน คณิตของลายเซ็น ห่วงโซ่ใบรับรอง และ revocation ยังเป็นงานของ CmsSignatureStatus, CertificateTrustStatus และ RevocationStatus ซึ่งมาจาก Windows CryptoAPI ให้ถือ pcsValid ว่า "โปรไฟล์ algorithm ผ่านครบ" ห้ามถือว่า "key นี้แข็งแรงพอ"

สำหรับแอปพลิเคชัน Delphi ที่รับใบแจ้งหนี้ PDF, สัญญา หรือแพ็กเกจ archive ที่ลงลายเซ็นมา การตั้งค่าที่ใช้งานได้จริงสั้น ๆ: รัน ValidatePadesTrust, ปฏิเสธเมื่อเจอ ppeiSignatureAlgorithmMismatch, ส่ง pcsUnsupported กับ pcsIndeterminate ไปให้คนตัดสิน และบังคับขั้นต่ำขนาด key RSA ของตัวเอง เพราะนโยบายไม่จัดการให้ PDFium Component for Delphi and Lazarus พกตัว validate PAdES, ตัวสร้างรายงานหลักฐาน และ pipeline การเซ็น มาด้วยกัน library เดียวกันจึงผลิตลายเซ็นพวกนี้และตรวจมันตั้งแต่ต้นจนจบได้