مقاله فنی

تأیید امضای ECDSA در PDF با Delphi: از DER به P1363

یک signatureValue از نوع ECDSA درون یک ظرف CMS یک SEQUENCE { INTEGER r, INTEGER s } از نوع DER است. تابع CNG ویندوز، یعنی BCryptVerifySignature، هیچ‌کدام از این‌ها را نمی‌پذیرد: آن یک r || s با عرض ثابت به‌روش IEEE P1363 می‌خواهد، بدون برچسب و بدون طول. HotPDF، مؤلفه بومی VCL برای PDF در Delphi و C++Builder، پیش از وارد‌کردن یک کلید، طبق قواعد سخت‌گیرانه DER بین این دو تبدیل می‌کند

شکستی که این جلوگیری می‌کند، شکستی خاص و ناامیدکننده است. Acrobat سند را باز می‌کند و یک تیک سبز نشان می‌دهد. تأییدکننده خودتان، با پیمایش همان بایت‌ها، نامعتبر برمی‌گرداند، یا CNG بدون توضیح بیشتر STATUS_INVALID_SIGNATURE بازمی‌گرداند. هیچ‌چیزی در امضا اشتباه نیست. آنچه اشتباه است این است که حدود هفتاد بایت ASN.1 به یک API داده شده که شصت‌وچهار بایت عدد صحیح خام انتظار داشته، و این عدم تطابق نامرئی است مگر بدانید کجا را نگاه کنید

چرا BCryptVerifySignature یک امضای معتبر ECDSA را رد می‌کند؟

چون دو سوی این فراخوانی رمزگذاری‌های امضای متفاوتی صحبت می‌کنند، و هیچ‌کدام آن را اعلام نمی‌کنند. ISO 32000-1 §12.8 می‌گوید یک دیکشنری امضا در /Contents یک بلاب CMS حمل می‌کند؛ RFC 5652 §5.3 می‌گوید signatureValue در هر SignerInfo یک OCTET STRING است که محتوایش هرچیزی است که الگوریتم امضا تعریف می‌کند. برای ECDSA آن محتوا ساختار DER از نوع SEC 1 است: یک SEQUENCE که دو INTEGER نگه می‌دارد. این طول متغیر با طراحی است، چون r و s اعداد صحیح‌اند و DER اکتت‌های صفر پیشرو را از اعداد صحیح می‌زداید

IEEE P1363 دیدگاه مخالف را دارد. این استاندارد امضا را به‌صورت الحاق دو مختصات تعریف می‌کند، هرکدام با صفر از چپ تا دقیقاً عرض بایتی میدان منحنی پد شده. امضای P-256 همیشه ۶۴ بایت است. یک رمزگذاری DER همان امضا معمولاً ۷۰ یا ۷۱ بایت است و می‌تواند هرجایی از حدود ۸ تا ۷۲ باشد. فرم DER را به BCryptVerifySignature بدهید و صرفاً بررسی طول، فراخوانی را محکوم می‌کند، به همین دلیل HotPDF پیش از تأیید نرمال‌سازی می‌کند نه بعد از آن

uses
  HPDFECDSA;

// Converts a CMS signatureValue into the fixed-width form CNG expects.
// ARaw comes back as 64 bytes for P-256, 96 for P-384, 132 for P-521.
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
  out ARaw: TBytes): Boolean;
begin
  Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;

قواعد DER که یک تجزیه‌گر امضا نباید سست بگیرد

هر ردی که در اینجا فهرست شده ردی است که HotPDF عمداً انجام می‌دهد، و هرکدام مسیری را می‌بندد که یک تجزیه‌گر بخشنده باز می‌گذاشت. وسوسه هنگام نوشتن یک مبدل این است که دو گره INTEGER را پیدا کنید، محتوایشان را کپی کنید، و ادامه دهید. این روی ورودی خوش‌ساخت کار می‌کند و در سکوت خانواده‌ای از بازرمزگذاری‌های چکش‌خوار را روی ورودی خصمانه می‌پذیرد. پس HPDFECDSANormalizeSignature یک عدد صحیح منفی را رد می‌کند، یعنی هر r یا s که اولین اکتت محتوایش بیت بالا را ست کرده باشد، چون یک اسکالر معتبر ECDSA مثبت است. این تابع مقداری را که کاملاً صفر است رد می‌کند، چون r = 0 یا s = 0 هرگز یک امضای مشروع نیست. این تابع یک اکتت صفر پیشرو زائد را رد می‌کند: X.690 §8.3 دقیقاً یکی را اجازه می‌دهد، و آن هم فقط وقتی اکتت بعدی در غیر این صورت منفی خوانده می‌شد، پس یک 00 که با اکتتی زیر 0x80 دنبال شود یک بازرمزگذاری است، نه یک امضا. این تابع یک سربرگ طول غیرمینیمال را رد می‌کند، چون X.690 §10.1 فرم قطعی رمزگذاری‌شده در کمترین اکتت‌ها را الزام می‌کند، و یک طول فرم‌بلند که می‌توانست فرم‌کوتاه باشد رشته بایتی متفاوتی است که همان معنا را حمل می‌کند. این تابع یک عدد صحیح پهن‌تر از اندازه مختصات منحنی را رد می‌کند، چون آن مقدار نمی‌تواند یک عنصر میدان باشد. و این تابع هر گره پسینی پس از s را رد می‌کند، همراه با یک SEQUENCE بیرونی که طول کلش با طول کل بلاب برابر نیست

این دو مورد آخر بیش از آنچه به‌نظر می‌رسد اهمیت دارند. بایت‌های پسین پس از SEQUENCE ترفند کلاسیک چکش‌پذیری امضاست: چیز اضافه‌ای بچسبانید، و یک تأییدکننده سهل‌گیر همچنان معتبر می‌گوید در حالی که رشته بایتی که تأیید کرد همان رشته بایتی نیست که امضا شده بود. همان غریزه در تقویت طول ASN.1 توصیف‌شده در یادداشت درباره تجزیه PKCS#12 نیز جاری است، و همین غریزه اینجا هم هست. در یک مسیر تأیید، ساختاری پذیرفته‌شده که هرگز توسط یک امضاکننده منطبق منتشر نشده یک نقص است، نه یک نرمش

عرض مختصات به منحنی تعلق دارد، نه به امضا

HotPDF عرض خروجی را از OID منحنی نام‌دار استخراج می‌کند، هرگز از طول DERی که تازه تجزیه کرده. این نیمه دوم تبدیل است و نیمه‌ای که به‌آسانی می‌توان به‌طور ظریف اشتباه گرفت. RFC 5480 §2.1.1 منحنی را در پارامترهای SubjectPublicKeyInfo گواهی شناسایی می‌کند، و HPDFECDSACurveFromOID سه OID که HotPDF پشتیبانی می‌کند را نگاشت می‌کند: 1.2.840.10045.3.1.7 برای P-256، 1.3.132.0.34 برای P-384، و 1.3.132.0.35 برای P-521. سپس HPDFECDSACoordinateSize ۳۲، ۴۸، یا ۶۶ بایت برمی‌گرداند، و بافر P1363 دو برابر آن است: ۶۴، ۹۶، یا ۱۳۲. هر عدد صحیح رمزگشایی‌شده در نیمه خودش راست‌چین می‌شود، پس یک r کوتاه از چپ با صفر پد می‌شود نه شیفت داده شود. P-521 موردی است که مردم را می‌گیرد، چون ۵۲۱ بیت برابر ۶۵.۱۲۵ بایت است و به ۶۶ گرد می‌شود، که یک امضای ۱۳۲ بایتی می‌دهد که هیچ شهودی از توان‌های دو نمی‌توانست پیش‌بینی کند. کلید عمومی همراه به‌صورت یک نقطه EC غیرفشرده طبق RFC 5480 §2.2 سفر می‌کند، که 0x04 به‌همراه X و Y است، پس HotPDF بررسی می‌کند دقیقاً 1 + 2 * CoordinateSize بایت باشد و با 0x04 شروع شود پیش از لمس‌کردن CNG

var
  Digest, SigDER, PublicPoint: TBytes;
  Curve: THPDFECDSACurve;
  Res: THPDFECDSAVerifyResult;
begin
  // secp256r1, taken from the certificate SubjectPublicKeyInfo parameters
  Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');

  // PublicPoint must be $04 || X || Y, so 1 + 2 * 32 = 65 bytes for P-256
  Res := HPDFECDSAVerifyDigest(Digest, SigDER, PublicPoint, Curve, eseDER);

  case Res of
    evrValid:
      Memo1.Lines.Add('signature verifies');
    evrInvalid:
      Memo1.Lines.Add('signature does not match the digest');
    evrMalformed:
      Memo1.Lines.Add('DER encoding or public point rejected');
    evrUnsupported:
      Memo1.Lines.Add('curve or algorithm not supported here');
    evrProviderUnavailable:
      Memo1.Lines.Add('bcrypt.dll or the curve provider is missing');
    evrProviderError:
      Memo1.Lines.Add('CNG returned an unexpected status');
  end;
end;

به پارامتر آخر توجه کنید. HPDFECDSAVerifyDigest همچنین eseP1363 را برای فراخوان‌کنندگانی می‌پذیرد که از قبل یک امضای عرض‌ثابت دارند، از یک توکن سخت‌افزاری یا یک سرویس امضای دوردست که r || s خام برمی‌گرداند. آن مسیر همچنان بررسی طول و بررسی غیرصفر را روی هر دو نیمه اعمال می‌کند، پس یک بافر با اندازه درست پر از صفر رد می‌شود نه اینکه بدون بررسی به ارائه‌دهنده عبور کند

چرا نام عمومی الگوریتم ECDSA در ویندوزهای قدیمی‌تر شکست می‌خورد؟

چون نام عمومی جدیدتر از پایه استقراری است که در آن انتشار می‌دهید. CNG یک شناسه الگوریتم ECDSA را ارائه می‌دهد که منحنی را از کلید وارد‌شده استنتاج می‌کند، و این روش تمیز نوشتن این کد است، اما BCryptOpenAlgorithmProvider فقط تضمین شده که در نسخه‌های جدیدتر ویندوز آن را حل کند. روی یک ماشین قدیمی‌تر، فراخوانی باز شکست می‌خورد، دسته ارائه‌دهنده nil می‌ماند، و هر تأیید ECDSA در اپلیکیشن شما روی یک امضایی که کاملاً خوب است پشتیبانی‌نشده گزارش می‌کند. HotPDF از این پرتگاه با باز‌کردن شناسه‌های خاص هر منحنی به‌جای آن اجتناب می‌کند. این ابزار ECDSA_P256، ECDSA_P384، و ECDSA_P521 را یک‌بار حل می‌کند، یک دسته ارائه‌دهنده به‌ازای هر منحنی کش می‌کند، و آن‌ها را در finalization واحد می‌بندد. سپس هر تأیید فقط کار ارزان را انجام می‌دهد: وارد‌کردن یک کلید عمومی موقت از یک ECCPUBLICBLOB، فراخوانی BCryptVerifySignature، نابودکردن کلید. بدون LoadLibrary تکراری، بدون GetProcAddress تکراری، بدون باز و بستن ارائه‌دهنده به‌ازای هر امضا. تأیید دسته‌ای چند صد سند تفاوت را حس می‌کند، و همین‌طور یک فرآیند سرویس که در غیر این صورت دسته‌های ارائه‌دهنده را زیر بار می‌چرخاند

کدهای نتیجه درباره این تمایز صادق می‌مانند. evrProviderUnavailable یعنی ماشین نتوانست به HotPDF یک ارائه‌دهنده بدهد؛ evrInvalid یعنی CNG با STATUS_INVALID_SIGNATURE پاسخ داد. فروپاشاندن این دو در یک شکست همان‌گونه است که یک مشکل استقرار به‌عنوان یک سند جعلی گزارش اشتباه می‌شود. همان تفکیک میان شکست محیطی و شکست رمزنگارانه در سرتاسر مدیریت CNG و CAPI در سمت امضا نیز جاری است، که در مقاله درباره امضای فروشگاه گواهی و ترتیب بایت پوشش داده شده است

کدام گواهی این را امضا کرد؟ SignerIdentifier دو چیز متفاوت است

RFC 5652 §5.3 SignerIdentifier را یک CHOICE می‌کند، و تأییدکننده‌ای که فقط یک بازو را رسیدگی کند در سکوت علیه کلید اشتباه تأیید می‌کند. بازوی اول issuerAndSerialNumber است، یک SEQUENCE که نام صادرکننده در DER خام و INTEGER سریال را نگه می‌دارد، و تطبیق آن یک مقایسه بایتی علیه هر گواهی در مجموعه certificates CMS است. بازوی دوم [0] subjectKeyIdentifier است، یک OCTET STRING با برچسب ضمنی، و تطبیق آن نیازمند کاوش در گواهی است نه مقایسه فیلدهای سربرگش

این کاوش لایه‌ای دارد که مردم را غافلگیر می‌کند. شناسه کلید در یک افزونه X.509v3 زندگی می‌کند، پس HotPDF فیلد افزونه‌های [3] از tbsCertificate را می‌پیماید، افزونه‌ای که OID آن 2.5.29.14 است را می‌یابد، BOOLEAN critical اختیاری را رد می‌کند، و OCTET STRING به‌نام extnValue را می‌گیرد. آن رشته اکتت شناسه نیست. طبق RFC 5280 §4.2.1.2 محتوایش خودش DER است، و نوع KeyIdentifier یک OCTET STRING دیگر است، پس یک بار دوم تجزیه می‌کنید تا به بایت‌های واقعی برسید. یک لایه زودتر متوقف شوید و یک پوشش ۲۲بایتی را علیه یک شناسه ۲۰بایتی مقایسه می‌کنید، هیچ گواهی‌ای هرگز مطابقت نمی‌کند، و تأییدکننده به هر ابتکاری که بعداً نوشتید سقوط می‌کند، که خطر واقعی همین است. گرفتن اولین گواهی در مجموعه یک میان‌بر وسوسه‌انگیز است و هرجا CMS یک زنجیره حمل کند، که غالب اوقات همین است، اشتباه است، چون برگ لزوماً اول نمی‌آید. HotPDF فقط زمانی یک گواهی تطبیق‌نیافته را می‌پذیرد که ظرف دقیقاً یکی نگه دارد؛ با چند گواهی حاضر، یک تطبیق دقیق SignerIdentifier اجباری است. تأیید یک دایجست علیه کلید عمومی یک CA میانی خطای دوستانه‌ای تولید نمی‌کند، بلکه یک نامعتبر مطمئن روی سندی که سالم است تولید می‌کند

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('signed.pdf') > 0 then
      for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
        if Pdf.VerifyLoadedSignatureEx(I, Info) = svValid then
          Memo1.Lines.Add(Format('%s: %s %s over %s, signer %s',
            [String(Info.FieldName), String(Info.PublicKeyAlgorithm),
             String(Info.CurveName), String(Info.HashAlgorithm),
             String(Info.SignerName)]))
        else
          Memo1.Lines.Add(Format('%s: not valid', [String(Info.FieldName)]));
  finally
    Pdf.Free;
  end;
end;

THPDFSignatureInfo.CurveName مقدار P-256، P-384، یا P-521 را گزارش می‌کند پس یک لاگ ممیزی ثبت می‌کند کدام منحنی واقعاً استفاده شده نه فقط کلمه ECDSA. سیم‌کشی سطح سند پیرامون این فراخوانی، به‌خصوص چگونگی هش‌شدن قطعات /ByteRange و چرا دایجست باید روی فایل محاسبه شود نه روی درخت آبجکت تجزیه‌شده، موضوع مقاله همراه درباره تأیید امضاهای دیجیتال PDF است

این چه چیزی به شما نمی‌دهد

یک نتیجه سبز از HPDFECDSAVerifyDigest فقط به یک پرسش پاسخ می‌دهد: این بایت‌ها با کلید خصوصی مطابق با این کلید عمومی امضا شده‌اند. این هیچ چیزی درباره این‌که آیا آن کلید به کسی تعلق دارد که باید اعتماد کنید نمی‌گوید. ساخت زنجیره تا یک لنگر اعتماد، ابطال از راه CRL یا OCSP، و بررسی‌های خط‌مشی کار جداگانه‌اند، و هر محصولی که یک امضای معتبر را بدون این‌ها گزارش کند، کمتر از آنچه کاربر فرض می‌کند گزارش می‌کند. تاریخ‌های اعتبار گواهی به‌طور جداگانه در THPDFSignatureInfo نمایش داده می‌شوند دقیقاً به همین دلیل: یک امضا می‌تواند به‌لحاظ رمزنگارانه تأیید شود در حالی که گواهی که آن را ساخته دو سال پیش منقضی شده. پشتیبانی از منحنی نیز عمداً محدود است. سه منحنی اول NIST مدیریت می‌شوند، و یک امضا روی هر منحنی دیگری پشتیبانی‌نشده برمی‌گرداند نه یک حدس. مسیر CNG فقط ویندوزی است، که معامله درستی برای یک مؤلفه VCL است اما ارزش گفتن دارد پیش از آنکه یک سرویس چندسکویی پیرامونش برنامه‌ریزی کنید. و سخت‌گیری قابل‌پیکربندی نیست: هیچ حالت مدارایی وجود ندارد که یک طول DER غیرمینیمال را بپذیرد چون یک امضاکننده قدیمی یکی را منتشر کرده. اگر با چنین فایلی در تولید مواجه شدید، پاسخ صادقانه ثبت‌کردن آن و پیگیری تولیدکننده است، نه گشاد‌کردن تجزیه‌گر تا فایل قبول شود

مسیر تأیید ECDSA که در اینجا شرح داده شد به‌عنوان بخشی از مؤلفه HotPDF استاندارد برای Delphi و C++Builder ارائه می‌شود، در کنار مسیرهای RSA PKCS#1 v1.5 و RSA-PSS و رکورد کامل اطلاعات امضا؛ صفحه محصول مرجع کامل امضای دیجیتال را در بر دارد