PDFium VCL وریفیکیشن CMS را بهشکل یک بکاند جایگزینپذیر پشت اینترفیس IPdfCmsVerifier مدیریت میکند، پس اعتبارسنج PAdES میتواند روی ویندوز از طریق CryptoAPI، روی macOS از طریق Keychain، و هرجا OpenSSL حاضر باشد از طریق ConfigureSslCmsVerifier اجرا شود. اینترفیس کوچک است. سه رفتار OpenSSL زیر آن اگر سادهلوحانه پیادهاش کنید جوابهای غلطِ مطمئن تولید میکنند
انگیزه از وقتی روشن میشود که یک اپلیکیشن دلفی از ویندوز بیرون بزند. اعتبارسنجی امضا یکی از آن مناطق کمیاب است که پشته crypto پلتفرم در آن جزئیات پیادهسازی نیست: تصمیم میگیرد کدام گواهیها trusted باشند، کدام الگوریتمها وجود دارند و ابطال چه معنایی دارد. یکی را hard-code کنید و کد پورت نمیشود. بد abstractش کنید و هر پلتفرم جوابی با شکلی متفاوت گزارش میکند که caller نمیتواند مقایسهشان کند
abstraction واقعاً قرار است چه چیزی را حمل کند
دو شکل وریفیکیشن و سه حکم مستقل. امضای PDF از جنس detached است: محتوای امضاشده دو بازه بایتی دو طرف سوراخ /Contents هستند، پس VerifyDetached دو سگمنت میگیرد نه یک بافر. توکن timestamp از جنس attached است و محتوایش را خودش حمل میکند، پس VerifyAttached فقط DER را میگیرد
نتیجه به سه status میشکند چون به سه سؤال متفاوت جواب میدهند و میتوانند ناخوان باشند. SignatureStatus میگوید آیا بایتها با کلید داخل گواهی signer امضا شدهاند. TrustStatus میگوید آیا آن گواهی به چیزی که به آن اعتماد دارید زنجیر میشود. RevocationStatus میگوید آیا گواهی در زمان مربوط هنوز معتبر بوده. سندی با امضای از نظر ریاضی کامل از گواهیای که هرگز نشنیدهایدش valid است، untrusted است و unknown، و لهکردن اینها در یک boolean واحد همان چیزی است که اعتبارسنجها را به دروغگفتن به کاربر میکشاند
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER، میتواند خالی باشد
ConfigureSslCrls(LoadFreshCrls); // DER، میتواند خالی باشد
ConfigureSslCmsVerifier; // بکاند را نصب میکند
Writeln('backend : ', PadesCmsVerificationBackendName);
Writeln('library : ', SslLibraryPath, ' ', SslLibraryVersion);
Writeln('ABI : ', SslAbiLayout); // ulong=<n> long=<n>
Options := TPdfCmsVerifyOptions.Default;
Options.CheckRevocation := True;
Options.CollectChainCertificates := True;
end;
SslAbiLayout شبیه یک جزئیات عجیب به نظر میرسد و نیست. هر کد خطای OpenSSL و هر پرچم store از مرز بهشکل یک unsigned long سی رد میشود، که روی ویندوز چهار بایت و روی Linux و macOS هشت بایت است. آن را بهشکل یک تایپ ثابت 32 بیتی اعلان کنید و کد روی ویندوز کار میکند، بعد روی LP64 بیسروصدا نصف یک مقدار را میخواند. گزارشدادن عرضهای مفروض بهشکل رشتهای که در یک تست میشود رویش assert زد یک رده کامل از ABI drift پلتفرم را به یک چک یکخطی تبدیل میکند. هر کسی که همان مسئله را با CK_ULONG در یک بایندینگ PKCS#11 سر و کله زده فوراً میشناسدش؛ آن قصه در struct packing و عرض CK_ULONG در PKCS#11 است
چرا گذر دوم وریفیکیشن محتوای خالی میبیند؟
چون CMS_verify محتوای detached را در BIO تا انتهای فایل میخواند، و BIOای که خوانده شده برایتان rewind نمیشود. وریفای در دو گذر یک طراحی منطقی است، اول امضای رمزنگارانه بهتنهایی با ارزیابی زنجیره خاموش، بعد ارزیابی کامل، و اگر هر دو گذر یک BIO مشترک داشته باشند به شکلی بهطرزی غیرعادی فریبنده شکست میخورد
گذر دوم صفر بایت محتوا میگیرد. در حالت detached این یک خطا نیست، چون بافر محتوای خالی یک ورودی قانونی است. digest بهسادگی مچ نمیشود، و شکست بهشکل شکست ساختن زنجیره خودش را نشان میدهد نه شکست محتوا، که شما را میفرستد سراغ بازرسی گواهیها و trust storeها در حالی که مسئله واقعی یک موقعیت stream است. برای هر گذر با BIO_new_mem_buf حافظه BIO را از نو بسازید. هزینهاش یک allocation است و امکان ماجرا را کلاً حذف میکند
پرچم no-verify چه چیزی را سرکوب میکند و چه چیزی را نه
CMS_NO_SIGNER_CERT_VERIFY ارزیابی زنجیره را سرکوب میکند نه lookup گواهی signer را. در درون OpenSSL گواهیهای signer را resolve و الصاق میکند قبل از اینکه سراغ پرچم برود، پس بعد از یک گذر اول که آن پرچم را حمل میکند signer از قبل در دسترس است و identifierهای الگوریتمش را میشود بلافاصله خواند. لازم نیست یک وریفیکیشن کامل دوم فقط برای گرفتن گواهی signer اجرا کنید، که همان چیزی است که نام پرچم شما را وسوسه میکند فرضش کنید
یک قاعده مالکیت هم همراهش میآید. ارجاع signer متعلق به ساختار CMS است و نباید مستقل آزاد شود. تا وقتی ساختار زنده است معتبر است، و آزاد کردنش خرابیای تولید میکند که علامتش کلاً جای دیگری ظاهر میشود، معمولاً حین cleanup یک آبجکت بیربط
چرا روشنکردن چک CRL همه امضاها را رد میکند؟
چون OpenSSL فقط CRLها را در برابر چیزی که store از قبل دارد چک میکند و خودش هیچ چیزی fetch نمیکند. دنبال CRL distribution pointها نمیرود و OCSP حرف نمیزند. X509_V_FLAG_CRL_CHECK را روی storeای بدون هیچ CRL ست کنید و هر زنجیرهای با ناتوانی در گرفتن CRL گواهی شکست میخورد. نتیجه شبیه این است که چک ابطال کار میکند و مسئله پیدا میکند. چیزی که هست این است که چک ابطال اصلاً اجرا نشده
پس بکاند پرچم را فقط وقتی ست میکند که ConfigureSslCrls واقعاً دستکم یک CRL داده باشد. بدون آن، RevocationStatus بهشکل pcvsUnsupported برمیگردد، که یک اظهار صادقانه است که به سؤال جواب داده نشد. به همان دلیل OnlineRetrieval روی این بکاند اثری ندارد و هیچ چکپوینت pcvstOnlineRetrieval صادر نمیشود: هیچ مسیر fetchingای وجود ندارد که پیشرفت از آن گزارش شود
این یک موضع طراحی است که ارزش دفاع از آن در حالت کلی را دارد. اعتبارسنجیای که نمیتواند ابطال را چک کند باید همین را بگوید. گزارشدادن گواهی چکنشده بهشکل ابطالنشده رایجترین راه گمراهکردن کاربران توسط ابزارهای اعتبارسنجی امضا است، و دقیقاً از آن رده سردرگمی است که در چرا اعتبارسنجها امضاهای PAdES را رد میکنند کاویده شده
// چکپوینتها به یک UI اجازه میدهند نشان بدهد کدام مرحله در جریان است و
// بگویند یک بکاند واقعاً کدام مراحل را اجرا میکند
type
TSignatureProbe = class
procedure Checkpoint(Stage: TPdfCmsVerifyStage);
end;
procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
case Stage of
pcvstCryptographicSignature: Status('checking the signature');
pcvstChainBuild: Status('building the certificate chain');
pcvstOnlineRetrieval: Status('fetching validation data');
pcvstRevocationCheck: Status('checking revocation');
end;
end;
// سه حکم را جداگانه بخوانید؛ مجازند با هم ناخوان باشند
if Result.SignatureStatus = pcvsValid then
case Result.TrustStatus of
pcvsValid: Report('signed and trusted');
pcvsInvalid: Report('signed, chain rejected');
pcvsUnsupported,
pcvsIndeterminate: Report('signed, trust not established');
end;
if Result.RevocationStatus = pcvsUnsupported then
Report('revocation was not checked on this backend');
باند شدن به کتابخانهای که نمیتوانید pinش کنید
OpenSSL بین 1.0 و 1.1 accessorهای stack خودش را rename کرد، پس همان تابع منطقی بسته به بیلدی که هاست اتفاقاً دارد دو نام اکسپورت ممکن دارد. بایندینگ اول نام جدیدتر را resolve میکند و به قدیمیتر برمیگردد، و فقط وقتی هیچکدام resolve نشد سیمبول غایب را ثبت میکند. این شکل درست برای هر بایندینگ داینامیکی به کتابخانهای است که شما ship نمیکنید: نامهای فعلی را ترجیح بده، نامهای تاریخی را تحمل کن، و فقط غیبت واقعی را گزارش کن
SslMissingSymbols چیزی است که یک لود شکستخورده را به یک رویداد قابلتشخیص تبدیل میکند. نتیجه غیرخالی روی هاستی که واضح است libcrypto رویش نصب است یعنی نسخه نصبشده از APIای که این بیلد هدفش است قدیمیتر است، که یک گفتوگوی پشتیبانی کاملاً متفاوت با یک کتابخانه غایب است. ConfigureSslLibraryPath مورد رایج دیگر را پوشش میدهد، هاستی با چند بیلد OpenSSL که آنی که روی مسیر جستوجوی پیشفرض است آنی نیست که میخواهید
انتخاب یک بکاند بهازای هر پلتفرم
چیدمان عملی این است که در استارتاپ انتخاب کنید و ثبت کنید کدام جواب داد. روی ویندوز، بکاند پلتفرم با certificate storeهایی که یک سازمان از قبل مدیریت میکند یکپارچه میشود، که معمولاً همان چیزی است که میخواهید. روی macOS بکاند Keychain با همان استدلال جور درمیآید و در وریفای امضاها با SecTrust روی macOS توصیف شده. OpenSSL گزینه پرتابل است، و همچنین انتخاب درست وقتی است که به یک سیاست اعتبارسنجی یکسان بین پلتفرمها نیاز دارید نه یکی که از trust store هر پلتفرم پیروی کند
هرکدام را که نصب میکنید، PadesCmsVerificationBackendName را کنار هر حکمی که ثبت میکنید لاگ کنید. یک نتیجه اعتبارسنجی ذخیرهشده بدون بکاندی که تولیدش کرده بعداً قابل بازتولید نیست، چون سه مقدار status بسته به اینکه کدام پشته جواب داده معانی ظریفاً متفاوتی دارند. لایه بازرسی امضا روی همه اینها، شامل اینکه سطوح PAdES چطور گزارش میشوند، در بازرسی امضاهای دیجیتال PDF و سطوح PAdES پوشش داده شده
همهاش بهشکل سورس همراه PDFium Delphi component عرضه میشود، که اینجا بیشتر از همیشه اهمیت دارد: برای یک validator امضا، توانایی خواندن دقیق اینکه یک بکاند کدام پرچمها را ست میکند و کدام چکها را رد میکند یک مزیت اضافه نیست، تنها راه فهمیدن این است که یک تیک سبز در اپلیکیشن شما واقعاً چه ادعایی میکند