מאמר טכני

בדיקות מדיניות ECDSA ו-EdDSA של ISO/TS 32002 ב-PDFium לדלפי

‏PDFium Component עבור Delphi בודק כל חתימת ECDSA ו-EdDSA של PDF מול פרופיל האלגוריתמים של ISO/TS 32002 ומדווח את פסק הדין ב-TPadesSignatureValidation.AlgorithmPolicyStatus. רק P-256, P-384, P-521, שלוש עקמומיות ה-Brainpool r1, Ed25519 ו-Ed448 יכולות לעבור, כל אחת עם digest תואם, וחוסר התאמה מעלה את ppeiSignatureAlgorithmMismatch. המדיניות הזאת חשובה יותר משהיא נשמעת. חתימה על brainpoolP160r1, או מפתח P-256 שחותם digest של SHA-512, יכולה לאמת היטב ברמת המתמטיקה, כך ש-Windows CryptoAPI מדווח על ערך החתימה כתקין בזמן ש-validator מחמיר של PDF 2.0 דוחה את הקובץ. בדיקת המדיניות סוגרת את הפער הזה, והיא במכוון נפרדת מהשאלה אם בייטי החתימה נכונים קריפטוגרפית

מה ISO/TS 32002 באמת מרשה לחתימות עקומות אליפטיות?

‏ISO/TS 32002 מרשה בדיוק שש עקמומיות ECDSA ושתי סכמות EdDSA בחתימות PDF, וקושר כל עקומה אל גדלי ה-digest שהיא רשאית לשאת. PDFium Component מקודד את הטבלה הזאת ב-PadesCurveDigestAllowed, עם מפתח של OID העקומה מתעודת החותם. עקומות ה-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 בלבד
מטריצת פרופיל האלגוריתמים של 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 שלו בלי פרמטרים, ו-SignerInfo של Ed448, על נתיב ה-signed-attributes ש-PAdES תמיד משתמשת בו, חייב להכריז על id-shake256-len (2.16.840.1.101.3.4.2.18) עם פרמטר INTEGER של בדיוק 512. לשתי הסכמות ה-AlgorithmIdentifier של החתימה וה-AlgorithmIdentifier של המפתח הציבורי בתעודה חייבים להיות ללא פרמטרים כלל. מפיק שכותב שם NULL — ההרגל שמקודדי RSA אימנו בספריות ASN.1 רבות — מפיק חתימה שאינה תואמת למרות שהמפתח וערך החתימה תקינים

איך PDFium Component מחלץ את שלשת האלגוריתמים מה-CMS

‏PDFium עצמו לא יכול לענות על השאלה הזאת, כי ה-API הציבורי שלו לחתימות קורא את מילון החתימה אבל לא מאמת את ה-CMS ולא חושף את העקומה של תעודת החותם. לכן שכבת הבחינה של PAdES שנבנתה על גבי PDFium, המתוארת בחקירת חתימות דיגיטליות של PDF ורמות PAdES עם PDFium, מפרסרת בעצמה את ה-CMS SignedData (RFC 5652). InspectPadesSignatureAlgorithm קורא את ה-digestAlgorithm וה-signatureAlgorithm של ה-SignerInfo הראשון, ואז מוצא את תעודת החותם וקורא את ה-SubjectPublicKeyInfo שלה כדי לקבל את אלגוריתם המפתח ואת העקומה. החיפוש של התעודה חסום במכוון: לכל היותר 64 תעודות מקבוצת ה-certificates של ה-CMS נבחנות, ההתאמה היא השוואת בייטים מדויקת של המנפיק והמספר הסידורי מתוך issuerAndSerialNumber, והקוד נופל בחזרה אל "התעודה היחידה שיש" רק כשהקבוצה מחזיקה בדיוק תעודה אחת ניתנת לפרסור. לבחור את תעודת ה-EC הראשונה מקבוצה לא מסודרת זה קל, וזה ייתן לתעודת CA להחליט באיזו עקומה השתמש החותם לכאורה

איך PDFium Component בוחן שלשת האלגוריתמים של חתימת PDF: InspectPadesSignatureAlgorithm קורא את ה-digestAlgorithm וה-signatureAlgorithm של ה-SignerInfo הראשון ב-CMS, מעגן את תעודת החותם באמצעות התאמת issuerAndSerialNumber מדויקת בין לכל היותר 64 מועמדים, קורא את ה-SubjectPublicKeyInfo עבור העקומה, ו-EvaluatePadesSignatureAlgorithm מחזיר את ה-AlgorithmPolicyStatus
‏PDFium עצמו לא מאמת את ה-CMS ולא חושף את עקומת החותם, ולכן שכבת ה-PAdES מפרסרת את ה-SignedData ושומרת כל OID גולמי ברקורד לדחייה שאפשר להסביר
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 באמת השתמש בה לאימות, כך שלתעודה ש-CryptoAPI מדווח יש את המילה האחרונה. כל קלט גולמי נוחת ב-TPadesSignatureAlgorithmInfo, כולל DigestParametersPresent, DigestParameterBits, SignatureParametersPresent ו-PublicKeyParametersAreNamedCurve, כך שדחייה תמיד ניתנת להסבר מתוך הרקורד ולא מתוך שורת יומן

למה חתימת 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 אבל משאיר מזהה ecdsa-with-SHA256 קידוד-קשה, והתוצאה היא קובץ שאין verifier תואם שיכול לפרש אותו בעקביות. הפונקציה ציבורית, ולכן אפשר לעגן את המטריצה בבדיקת יחידה בלי לבנות 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) או קבוצת פרמטרים מפורשת מלאה, ופרופילי ה-PKIX דורשים את הצורה הנקובה. PDFium Component מחזיר pcsInvalid כשתעודת id-ecPublicKey נושאת בלי פרמטרים, פרמטרים מרומזים או פרמטרים מפורשים, כי פרמטרים מפורשים נותנים לתוקף לתאר עקומה שרק דומה לסטנדרטית. עקומה נקובה כהלכה שפשוט חסרה מהרשימה של ISO/TS 32002, כמו brainpoolP160r1 למעלה או secp256k1, מקבלת pcsUnsupported במקום

פסול, בלתי נתמך או לא ידוע: לקרוא את הסטטוס ביושר

שלושת הסטטוסים שאינם valid של AlgorithmPolicyStatus משמעותם דברים שונים, וצמצום שלהם לדלי "נכשל" אחד משליך את המידע שמבקרי הרגולציה צריכים. pcsInvalid אומר שצירוף אלגוריתמים מוכר שגוי-צורה או לא תואם; הוא מוסיף את ppeiSignatureAlgorithmMismatch אל TPadesValidationResult.Issues ומושך את ה-IntegrityStatus המצטבר אל pcsInvalid, כך ש-IsCryptographicallyValid מחזיר False גם כשערך חתימת ה-CMS מאומת. pcsUnsupported אומר שהעקומה או ה-digest נמצאים מחוץ למה שהפרופיל מכנה, תוצאת יכולת, לא הוכחת זיוף. pcsIndeterminate אומר שלא הצליחו לעגן את תעודת החותם, בדרך כלל CertificateSet עם כמה מועמדים ובלי התאמת issuerAndSerialNumber מדויקת, ולכן הקוד מסרב לנחש את העקומה; מאז v3.124.0 הוא גם מסמן חתימת RSA מעל SHA-1 או digest של 112 סיביות כמו SHA-224, שאינו מוסכם עוד עבור אימות עכשיו. אותו פיצול חל על EdDSA במכונה שה-CryptoAPI שלה לא מסוגל לאמת Ed25519 או Ed448: ה-CmsSignatureStatus נשאר pcsUnsupported בזמן שה-AlgorithmPolicyStatus עדיין יכול להיות pcsValid, כי הקידוד היה נכון ורק ה-verifier חסר. אם אתם רודפים אחרי דחייה מ-Adobe או מ-validator מבוסס DSS, המדריך למה validators דוחים חתימות PAdES מכסה את הסיבות הנפוצות האחרות

נתיב ההחלטה של EvaluatePadesSignatureAlgorithm ב-PDFium Component: אי-התאמת digest מציב pcsInvalid ו-ppeiSignatureAlgorithmMismatch, עקומה נקובה מחוץ לפרופיל ISO TS 32002 כמו brainpoolP160r1 מציב pcsUnsupported, תעודת חותם שלא ניתן לעגן מציב pcsIndeterminate, ומאז v3.124.0 ענף ה-RSA מפעיל את סוויטות ה-digest של ETSI TS 119 312, כשגודל המפתח נותר בידי האפליקציה
שלושת הסטטוסים שאינם 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; // אופליין, בלי בדיקת ביטול
    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 תחת קשת ה-PKCS #1 של 1.2.840.113549.1.1.* החזיר pcsValid, כולל ה-sha1WithRSAEncryption המורש. מאז PDFiumPas v3.124.0 ענף ה-RSA מפעיל את סוויטות החתימה של ETSI TS 119 312. digests של MD2, MD4 ו-MD5 הם pcsInvalid. SHA-1 ו-digests של 112 סיביות כמו SHA-224 הם pcsIndeterminate, כך שחתימת SHA-1 שומרת על תוצאת ה-integrity הכוללת שלה ומסומנת לסקירה במקום להידחות. digestAlgorithm ששונה מה-digest שהאלגוריתם של החתימה קובע, כמו sha256WithRSAEncryption מעל digest של SHA-1, או אלגוריתם חתימת RSA על מפתח חותם שאינו RSA, הוא pcsInvalid, מדווח כ-ppeiSignatureAlgorithmMismatch ונכשל ב-integrity. תעודת חותם שלא נמצאת נותנת pcsIndeterminate, כפי שכבר היה עבור ECDSA, ו-digests שאינם מזוהים או OIDs של RSA שאינם חתימה נותנים pcsUnsupported. אורך המודולוס עדיין לא נבדק, פרמטרי PSS לא מאומתים כאן (המאמר על פרמטרי RSASSA-PSS של RFC 4055 מכסה איך הם מקודדים בצד החתימה), ו-digests של SHA-1 או MD5 מעלים בנוסף את ה-issue הנפרד של ppeiBadDigestAlgorithm. בדומה, מתמטיקת החתימה, שרשרת התעודות והביטול נשארים באחריות של CmsSignatureStatus, CertificateTrustStatus ו-RevocationStatus, שמגיעים מ-Windows CryptoAPI. התייחסו אל pcsValid בתור "פרופיל האלגוריתמים עומד", לעולם לא בתור "המפתח הזה חזק מספיק"

לאפליקציית Delphi שמקבלת חשבוניות, חוזים או חבילות ארכיון של PDF חתומים, ההגדרה המעשית קצרה: הריצו ValidatePadesTrust, דחו על ppeiSignatureAlgorithmMismatch, הפנו pcsUnsupported ו-pcsIndeterminate לבן אדם, ואכפו סף גודל מפתח RSA משלכם כי המדיניות לא תעשה זאת. PDFium Component עבור Delphi ו-Lazarus משלח את ה-validator של ה-PAdES, את בונה דוחות הראיות ואת צינור החתימה, כך שאותה ספרייה יכולה להפיק את החתימות האלה ולבדוק אותן מקצה לקצה