مقاله فنی

گواهی‌های تست خودامضا در Delphi با CryptoAPI

تابع PLCreateSelfSignedCertificate در PDFlibPas یک گواهی خودامضای RSA/SHA-256 می‌سازد و آن را، کلید خصوصی شامل، مستقیم به یک فایل PFX محافظت‌شده‌با-رمزعبور export می‌کند، با استفاده از هیچ چیز جز Win32 CryptoAPIای که از پیش روی هر دستگاه ویندوز نصب است. هیچ ابزار خارجی، هیچ مرجع صدور گواهی، هیچ گام دستی makecert یا OpenSSL: یک فراخوانی تابع، یک گواهی به‌اندازه‌ی کافی خوب برای هدایت یک تست امضا

سناریویی که این تابع را ارزش داشتن می‌دهد تقریباً همیشه یک خط لوله‌ی CI است. یک تست دود امضا به یک PFX واقعی با یک کلید خصوصی واقعی پشتش نیاز دارد، و check‌کردن یکی درون مخزن مسئله‌ی امنیتی خودش را دارد، چون یک کلید خصوصی commitشده یک کلید خصوصی افشاشده است از همان لحظه‌ای که آن commit فرود می‌آید. Shell‌کردن به makecert.exe یا یک فراخوانی OpenSSL از یک اسکریپت build هم کار می‌کند، اما آنگاه خط لوله به ابزاری بستگی دارد که باید نصب شود، در PATH پیدا شود، و در سراسر هر agent build نسخه-سازگار نگه داشته شود. تولید گواهی درون همان فرآیندی که تست را اجرا می‌کند، با همان فراخوانی‌های Win32 CryptoAPI که ویندوز از پیش عرضه می‌کند، آن وابستگی را کاملاً حذف می‌کند

PLCreateSelfSignedCertificate واقعاً چه چیزی تولید می‌کند؟

PLCreateSelfSignedCertificate یک فایل PFX محافظت‌شده‌با-رمزعبور تولید می‌کند که یک گواهی خودامضای RSA و کلید خصوصی‌اش را نگه می‌دارد، امضاشده با sha256RSA، هدایت‌شده با پنج پارامتر: SubjectName، PFXFileName، PFXPassword، ValidDays، و KeyBits، و یک پرچم موفقیت Boolean ساده برمی‌گرداند. SubjectName یک رشته‌ی کامل X.500 مثل 'CN=Alice, O=Example' می‌پذیرد، و یک نام ساده بدون هیچ علامت = در آن به‌طور خودکار با CN= پیشوند می‌گیرد. ValidDays زیر ۱ به ۳۶۵ فرومی‌گردد، و KeyBits خارج از بازه‌ی ۱۰۲۴ تا ۱۶۳۸۴ به ۲۰۴۸ فرومی‌گردد. PDFlibPas این تابع را از v3.224.0 عرضه کرده، در دسترس نه فقط از واحد Delphi بلکه از طریق سطوح DLL و ActiveX هم، و کامنت مستند خودش صریح است درباره‌ی اینکه کجا مفیدبودنش متوقف می‌شود: هر نمایشگر رایج یک گواهی خودامضا را نامعتمد علامت می‌زند مگر اینکه کسی صراحتاً آن را نصب کند، پس آنچه تولید می‌کند را به‌عنوان یک گواهی برای اعمال یک مسیر کد در نظر بگیرید، نه یک امضا که از کسی خارج از تیم شما خواسته شود به آن اعتماد کند

var
  Success: Boolean;
begin
  Success := PLCreateSelfSignedCertificate(
    'CN=PDFlibPas CI Test, O=Example Corp',
    'ci-test-signer.pfx',
    'a-strong-throwaway-password',
    365,     // ValidDays
    2048);   // KeyBits
  if not Success then
    raise Exception.Create('Self-signed certificate generation failed');
end;

چرا CryptGenKey طول کلید را در پارامتر پرچم‌ها رمزگذاری می‌کند؟

CryptGenKey دو تنظیمِ بی‌ربط را در یک پارامتر تکی dwFlags بسته‌بندی می‌کند. کلمه‌ی پایین پرچم‌های رفتاری را حمل می‌کند، CRYPT_EXPORTABLE در میان آن‌ها، درحالی‌که کلمه‌ی بالا، برای یک کلید تبادل-کلید RSA، طول کلید درخواستی به بیت را حمل می‌کند. سپردن ۲۰۴۸ انگار صرفاً یک پرچم دیگر باشد آن را در کلمه‌ی پایین فرود می‌آورد، جایی که با هیچ پرچم رفتاری‌ای که CryptoAPI تعریف می‌کند مطابقت ندارد، پس فراخوانی یک کلید در هر طول پیش‌فرضی که provider به آن فرومی‌گردد تولید می‌کند نه طولی که فراخواننده فکر می‌کرد درخواست داده. گرفتن یک کلید واقعی ۲۰۴۸بیتی RSA یعنی ابتدا شیفت‌دادن عدد به کلمه‌ی بالا

// Key length lives in the upper 16 bits of the CryptGenKey flags;
// the low word carries behavior flags such as CRYPT_EXPORTABLE.
if not CryptGenKey(hProv, AT_KEYEXCHANGE,
    (Cardinal(KeyBits) shl 16) or CRYPT_EXPORTABLE, hKey) then
  Exit;

اگر CRYPT_EXPORTABLE را فراموش کنید چه اتفاقی می‌افتد؟

CRYPT_EXPORTABLE را از همان مقدار پرچم‌ها بیندازید و CryptGenKey همچنان موفق می‌شود، اما کلید خصوصی تولیدشده را در سطح CSP غیرقابل‌export علامت می‌زند. هر چیز پایین‌دست هم همچنان موفقیت گزارش می‌دهد: CertCreateSelfSignCertificate یک context گواهی معتبر برمی‌گرداند، و PFXExportCertStoreEx، حتی فراخوانی‌شده با EXPORT_PRIVATE_KEYS، در هر صورت موفق می‌شود و یک فایل PFX می‌نویسد که باز می‌شود، تجزیه می‌شود، و کاملاً معمولی به‌نظر می‌رسد. آنچه حمل نمی‌کند کلید خصوصی است، چون CSP امتناع کرد اجازه دهد از کانتینر کلید خارج شود، و PFXExportCertStoreEx هرگز آن امتناع را به‌عنوان دلیلی برای شکست‌دادن کل export در نظر نمی‌گیرد

شکست فقط بعداً، و جای دیگری کاملاً، نمایان می‌شود: یک فراخوانی امضا آن PFX را باز می‌کند، یک گواهی بدون هیچ کلید خصوصی پیوست‌شده پیدا می‌کند، و دقیقاً همان خطایی را گزارش می‌دهد که از یک PFX خراب یا اشتباه می‌گرفتید، نه از یک پرچم گمشده سه لایه بالادست. هرکسی که فقط از سمت امضا دیباگ می‌کند می‌تواند یک بعدازظهر را روی فایل اشتباه بسوزاند پیش از اینکه متوجه شود باگ واقعی یک بیت گمشده‌ی تکی در زمان تولید-کلید است، در یک فراخوانی تابع کاملاً متفاوت، احتمالاً در یک اسکریپت build کاملاً متفاوت

چرا ProvType باید بین CryptAcquireContextW و گواهی مطابقت داشته باشد؟

ProvType باید مطابقت داشته باشد چون CertCreateSelfSignCertificate کلید خصوصی گواهی جدید را از طریق یک رکورد CRYPT_KEY_PROV_INFO حل می‌کند، و یک فیلد در آن رکورد، ProvType، باید همان مقدار نوع CSP دقیق را نام ببرد که به CryptAcquireContextW پاس داده شده وقتی کانتینر کلید باز شد، PROV_RSA_AES، عددی ۲۴، در پیاده‌سازی PDFlibPas. ProvType را روی صفر تنظیم کنید، یا روی هر ثابت providerی جز آنی که کانتینر واقعاً به آن تعلق دارد، و گواهی همچنان می‌تواند ساخته شود، اما لینک ثبت‌شده‌اش به عقب به کلید خصوصی دیگر به کانتینری که آن را نگه می‌دارد حل نمی‌شود، که بعداً به‌عنوان یک شکست امضا یا export نمایان می‌شود که هیچ ربطی به محتوای رمزنگاری واقعی گواهی ندارد

// The provider type used to open the key container must match the
// provider type recorded in the certificate's key-provider info.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, build the subject name blob, then:
KeyProvInfo.ProvType := PROV_RSA_AES;   // same constant, both call sites

گذاشتن همه‌چیز کنار هم: از کانتینر GUID تا PFX محافظت‌شده‌با-رمزعبور

زنجیره‌ی فراخوانی درون PLCreateSelfSignedCertificate یک خط راست را دنبال می‌کند، بازکردن یک کانتینر کلید تازه نام‌گذاری‌شده پس از یک GUID تازه‌تولیدشده پس اجراهای همزمان CI هرگز روی نام‌های کانتینر برخورد نمی‌کنند، تولید جفت کلید RSA درونش با دو پرچمی که در بالا پوشش داده شد، رمزگذاری SubjectName در یک blob نام X.500 از طریق CertStrToNameW، و فراخوانی CertCreateSelfSignCertificate با یک پنجره‌ی اعتبار محاسبه‌شده از ValidDays و سپرده‌شده به‌عنوان یک ساختار ساده به‌شکل SYSTEMTIME. context گواهی حاصل به یک فروشگاه گواهی درون‌حافظه‌ای می‌رود که با CertOpenStore و CERT_STORE_PROV_MEMORY باز شده، صرفاً پس PFXExportCertStoreEx یک فروشگاه برای export‌کردن از آن داشته باشد، چون آن API در برابر یک handle فروشگاه کار می‌کند نه یک context گواهی ساده

// Each call opens a throwaway container named after a fresh GUID:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, self-sign the certificate, export the PFX ...
// then delete the container once the PFX holds its own copy of the key:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_DELETEKEYSET);

خودِ PFXExportCertStoreEx از قرارداد معمولی دوپاسی Win32 پیروی می‌کند: یک‌بار آن را با یک بافر با طول-صفر فرا بخوانید تا بفهمید PFX چند بایت نیاز دارد، آن‌مقدار تخصیص دهید، سپس دوباره فراخوانی کنید تا بافر را پر کند. به‌محض اینکه بایت‌ها روی دیسک باشند، PDFlibPas کانتینر کلید یک‌بارمصرف را با CRYPT_DELETEKEYSET حذف می‌کند به‌جای اینکه پشت سر بگذارد، چون PFX از پیش کپی خودش از هر بایت مواد کلیدی که کانتینر نگه می‌داشت حمل می‌کند. آن پاک‌سازی را رد کنید و هر فراخوانی PLCreateSelfSignedCertificate یک کانتینر کلید یتیم و نام‌گذاری‌شده‌با-GUID را در پروفایل کاربر فراخواننده رها می‌کند، که دقیقاً همان نوع نشتی است که یک agent CI که این تابع را در هر build اجرا می‌کند در طول ماه‌ها انباشته می‌کند پیش از اینکه کسی متوجه شود

آیا یک گواهی خودامضا برای امضای تولیدی امن است استفاده شود؟

نه: یک گواهی خودامضا برای اعمال یک مسیر کد امضا امن است و ناامن برای امضایی که از کسی خارج از تیم انتظار می‌رود به آن اعتماد کند، چون هیچ چیزی آن را به عقب به یک ریشه‌ای که نرم‌افزار طرف اعتمادکننده از پیش به آن اعتماد دارد زنجیر نمی‌کند. گام بعدی طبیعی برای یک PFX مثل این یک فراخوانی امضای واقعی است، پوشش‌داده‌شده در ساخت یک کارگاه مطابقت و امضا در Delphi با PDFlibPas، جایی که یک PFX ساخته‌شده به این روش نیمه‌ی امضایی یک خط لوله را هدایت می‌کند که همچنین preflight PDF/A و ممیزی ByteRange را اجرا می‌کند. با این‌حال امضاکردن فقط نیمی از چیزی است که دور یک گواهی می‌نشیند، و نیمه‌ی دیگر دقیقاً همان‌جایی است که یک برگ خودامضا قرار است شکست بخورد: امضا و اعتبارسنجی PAdES در Delphi با PDFlibPas بررسی‌های زنجیره‌ی اعتماد را پوشش می‌دهد که یک اعتبارسنج مطابقت اجرا می‌کند، و اعتبارسنجی که زنجیره را به عقب تا یک ریشه‌ی مورد اعتماد می‌پیماید هیچ دلیلی برای اعتمادکردن به گواهی‌ای که این تابع پنج دقیقه پیش از هیچ اختراع کرده ندارد

PLCreateSelfSignedCertificate یک تابع از میان APIهای گواهی و امضا در کتابخانه‌ی PDF از PDFlibPas برای Delphi و C++Builder است، و دقیقاً برای همین شکاف توصیف‌شده در اینجا وجود دارد: یک تست امضا که به یک جفت کلید واقعی پشتش نیاز دارد و هیچ چیز خارجی‌ای برای تولید یکی