مقاله فنی

بررسی خط‌مشی ECDSA و EdDSA در ISO/TS 32002 با PDFium دلفی

PDFium Component for Delphi هر امضای PDF با ECDSA و EdDSA را در برابر پروفایل الگوریتم ISO/TS 32002 بررسی می‌کند و حکمش را در TPadesSignatureValidation.AlgorithmPolicyStatus گزارش می‌دهد. فقط P-256، P-384، P-521، سه منحنی Brainpool r1، Ed25519 و Ed448 می‌توانند قبول شوند، هر کدام با digest متناظر، و هر ناهمخوانی ppeiSignatureAlgorithmMismatch برمی‌گرداند. این خط‌مشی بیشتر از آن‌که به نظر برسد اهمیت دارد. امضایی روی brainpoolP160r1، یا کلید P-256 که یک digest با SHA-512 را امضا کرده، در سطح ریاضی کاملاً درست verify می‌شود، به‌طوری که Windows CryptoAPI مقدار امضا را سالم گزارش می‌کند در حالی که یک اعتبارسنج سخت‌گیرانهٔ 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 را verify می‌کند و نه منحنی گواهی امضاکننده را در دسترس می‌گذارد. پس لایهٔ بازرسی PAdES ساخته‌شده روی PDFium خودش CMS SignedData (RFC 5652) را parse می‌کند. InspectPadesSignatureAlgorithm مقدار digestAlgorithm و signatureAlgorithm اولین SignerInfo را می‌خواند، بعد گواهی امضاکننده را پیدا و SubjectPublicKeyInfo آن را می‌خواند تا الگوریتم کلید و منحنی به دست بیاید. جست‌وجوی گواهی عمداً محدود است: حداکثر 64 گواهی از مجموعهٔ certificates در CMS بررسی می‌شوند، تطبیق مقایسهٔ بایتی دقیق issuer و شماره سریال از issuerAndSerialNumber است، و کد فقط وقتی مجموعه دقیقاً یک گواهی قابل parse دارد به «همان تنها گواهی موجود» برمی‌گردد. برداشتن اولین گواهی EC از یک مجموعهٔ بی‌ترتیب راحت بود، و اجازه می‌داد یک گواهی CA تصمیم بگیرد امضاکننده از چه منحنی‌ای استفاده کرده

PDFium Component سه‌تایی الگوریتم امضای PDF را چطور بازرسی می‌کند: InspectPadesSignatureAlgorithm مقدار digestAlgorithm و signatureAlgorithm اولین SignerInfo در CMS را می‌خواند، گواهی امضاکننده را با تطبیق دقیق issuerAndSerialNumber در میان حداکثر 64 کاندیدا میخکوب می‌کند، SubjectPublicKeyInfo را برای منحنی می‌خواند و EvaluatePadesSignatureAlgorithm مقدار AlgorithmPolicyStatus را برمی‌گرداند
خود PDFium نه CMS را verify می‌کند و نه منحنی امضاکننده را در دسترس می‌گذارد، پس لایهٔ PAdES خودش SignedData را parse می‌کند و هر 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 واقعاً برای verify به کار برده، پس گواهی گزارش‌شده توسط CryptoAPI حرف آخر را می‌زند. هر ورودی خام در TPadesSignatureAlgorithmInfo فرود می‌آید، از جمله DigestParametersPresent، DigestParameterBits، SignatureParametersPresent و PublicKeyParametersAreNamedCurve، پس رد شدن همیشه از رکورد قابل توضیح است نه از یک خط لاگ

چرا امضای P-256 با SHA3-256 از خط‌مشی می‌افتد؟

امضای P-256 هر وقت که digestAlgorithm در CMS با digestای که signatureAlgorithm مربوط به ECDSA ضمنش دارد ناهمخوان باشد از خط‌مشی PDFium Component می‌افتد، حتی اگر هر دو جداگانه برای منحنی قابل قبول باشند. EvaluatePadesSignatureAlgorithm اول ecdsa-with-SHA256، ecdsa-with-SHA3-256 و هم‌خانواده‌هایشان را به یک digest نگاشت می‌کند، با digestAlgorithm اعلام‌شده مقایسه می‌کند و پیش از آنکه سراغ جدول منحنی برود، هر تفاوتی را با pcsInvalid جواب می‌دهد. این حالت واقعی است: ابزار امضا hash خودش را روی SHA3-256 می‌گذارد اما شناسهٔ هاردکدشدهٔ ecdsa-with-SHA256 را نگه می‌دارد، و نتیجه فایلی است که هیچ verify‌کنندهٔ سازگاری نمی‌تواند یکدست تفسیرش کند. تابع عمومی است، پس ماتریس را می‌شود در یک آزمون واحد بدون ساختن 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 وقتی گواهی id-ecPublicKey بدون پارامتر، با پارامتر ضمنی یا با پارامتر صریح برسد pcsInvalid برمی‌گرداند، چون پارامترهای صریح به مهاجم اجازه می‌دهد منحنی‌ای را توصیف کند که فقط شبیه یک منحنی استاندارد است. منحنی درست نام‌داری که صرفاً در فهرست ISO/TS 32002 نیست، مثل brainpoolP160r1 بالا یا secp256k1، به‌جایش pcsUnsupported می‌گیرد

نامعتبر، پشتیبانی‌نشده یا نامعین: خواندن صادقانهٔ وضعیت

سه وضعیت غیرمعتبرِ AlgorithmPolicyStatus معناهای متفاوتی دارند و تودنشان در یک سطل «رد شده» همان اطلاعاتی را دور می‌ریزد که ممیزان لازم دارند. pcsInvalid یعنی ترکیب الگوریتم شناخته‌شده بدشکل یا ناهمخوان است؛ به TPadesValidationResult.Issues مقدار ppeiSignatureAlgorithmMismatch اضافه می‌کند و IntegrityStatus تجمیعی را به pcsInvalid می‌برد، پس IsCryptographicallyValid حتی وقتی مقدار امضای CMS درست درمی‌آید False برمی‌گرداند. pcsUnsupported یعنی منحنی یا digest بیرون از چیزی است که پروفایل نام می‌برد؛ این یک نتیجهٔ قابلیت است، نه اثبات دستکاری. pcsIndeterminate یعنی گواهی امضاکننده میخکوب نشد، معمولاً CertificateSetی با چند کاندیدا و بدون تطبیق دقیق issuerAndSerialNumber، پس کد از حدس زدن منحنی سر باز می‌زند؛ از v3.124.0 به بعد امضای RSA روی SHA-1 یا digestای با 112 بیت مثل SHA-224 را هم همین‌طور علامت می‌زند که دیگر برای اعتبارسنجی جاری توافق‌شده نیست. همین تفکیک برای EdDSA روی ماشینی که CryptoAPI‌اش نمی‌تواند Ed25519 یا Ed448 را verify کند هم صادق است: CmsSignatureStatus روی pcsUnsupported می‌ماند در حالی که AlgorithmPolicyStatus همچنان می‌تواند pcsValid باشد، چون رمزگذاری درست بود و فقط verify‌کننده غایب بود. اگر دنبال رد شدن از Adobe یا یک اعتبارسنج مبتنی بر DSS هستید، راهنمای این‌که اعتبارسنج‌ها چرا امضاهای PAdES را رد می‌کنند بقیهٔ علل رایج را پوشش می‌دهد

مسیر تصمیم EvaluatePadesSignatureAlgorithm در PDFium Component: ناهمخوانی digest مقدار pcsInvalid و ppeiSignatureAlgorithmMismatch را می‌دهد، منحنی نام‌داری بیرون از پروفایل ISO TS 32002 مثل brainpoolP160r1 مقدار pcsUnsupported را، گواهی امضاکننده‌ای که میخکوب نمی‌شود مقدار pcsIndeterminate را، و از v3.124.0 شاخهٔ RSA مجموعه‌های digest مربوط به ETSI TS 119 312 را اعمال می‌کند، در حالی که اندازه کلید همچنان به اپلیکیشن واگذار است
سه وضعیت غیرمعتبر معناهای متفاوتی دارند: 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 را اعمال می‌کند. digestهای MD2 و MD4 و MD5 مقدار pcsInvalid می‌گیرند. SHA-1 و digestهای 112 بیتی مثل SHA-224 مقدار pcsIndeterminate می‌گیرند، پس امضای SHA-1 نتیجهٔ یکپارچگی کلی‌اش را نگه می‌دارد و برای بازبینی علامت می‌خورد به‌جای رد شدن. digestAlgorithmای که با digest تثبیت‌شده توسط الگوریتم امضا فرق دارد، مثل sha256WithRSAEncryption روی digest با SHA-1، یا الگوریتم امضای RSA روی کلید امضاکنندهٔ غیر-RSA، مقدار pcsInvalid می‌گیرد، با نام ppeiSignatureAlgorithmMismatch گزارش و یکپارچگی را می‌اندازد. گواهی امضاکننده‌ای که پیدا نشود مقدار pcsIndeterminate می‌دهد، همان‌طور که برای ECDSA هم قبلاً می‌داد، و digestهای ناشناخته یا OIDهای RSA غیرامضایی مقدار pcsUnsupported. طول modulus همچنان بررسی نمی‌شود، پارامترهای PSS این‌جا اعتبارسنجی نمی‌شوند (مقالهٔ پارامترهای RSASSA-PSS طبق RFC 4055 پوشش می‌دهد سمت امضا چطور رمزگذاری می‌شوند)، و digestهای SHA-1 یا MD5 جدا از این، issue مستقل ppeiBadDigestAlgorithm را هم برمی‌گردانند. به همین ترتیب، ریاضیات امضا، زنجیرهٔ گواهی و ابطال همچنان کار CmsSignatureStatus و CertificateTrustStatus و RevocationStatus هستند که از Windows CryptoAPI می‌آیند. با pcsValid مثل «پروفایل الگوریتم برقرار است» رفتار کنید، هرگز مثل «این کلید به‌قدر کافی قوی است»

برای یک اپلیکیشن دلفی که فاکتورها، قراردادها یا بسته‌های آرشیوی PDF امضاشده را می‌پذیرد، پیکربندی عملی کوتاه است: ValidatePadesTrust را اجرا کنید، روی ppeiSignatureAlgorithmMismatch رد کنید، pcsUnsupported و pcsIndeterminate را به یک انسان حواله کنید و کف اندازهٔ کلید RSA خودتان را اعمال کنید چون خط‌مشی این کار را نمی‌کند. PDFium Component برای دلفی و Lazarus اعتبارسنج PAdES، سازندهٔ گزارش شواهد و pipeline امضا را با خود حمل می‌کند، پس همان کتابخانه می‌تواند این امضاها را تولید و سرتاسر هم بررسی کند