غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: دو دقیقه

ASP.NET Core از مکانیزم «Data protection» برای تولید کلیدهای رمزنگاری اطلاعات موقتی خود استفاده می‌کند. این روش در دو حالت هاست برنامه‌ها توسط IIS و یا عدم تنظیمات ذخیره سازی آن‌ها به صورت دائمی، اطلاعات خود را در حافظه نگه‌داری می‌کند و با ری‌استارت شدن سرور و یا IIS، این کلیدها از دست رفته و مجددا تولید می‌شوند. به این ترتیب کاربران شاهد این مشکلات خواهند بود:
الف) چون کوکی‌ها و یا توکن‌های آن‌ها دیگر قابل رمزگشایی نیستند (به علت باز تولید کلیدهای رمزنگاری و رمزگشایی اطلاعات)، مجبور به لاگین مجدد خواهند شد (تا کوکی‌های جدیدی برای آن‌ها تولید شوند). همچنین آنتی‌فورجری توکن‌های آن‌ها نیز مجددا باید تولید شوند.
ب) تمام اطلاعات محافظت شده‌ی توسط Data protection API قابل رمزگشایی نخواهند بود.


تنظیم Data protection API مخصوص برنامه‌های هاست شده‌ی توسط IIS

برای اینکه کلیدهای رمزنگاری اطلاعات برنامه‌های وب به صورت دائمی ذخیره شوند و با ری‌استارت سرور از دست نروند، یکی از سه روش ذیل را می‌توان بکار گرفت:

1) اسکریپت پاور شل ذیل را اجرا کنید:
نحوه‌ی اجرای آن نیز به صورت ذیل است و پس از آن، نام Application pool مخصوص برنامه ذکر می‌شود:
 .\Provision-AutoGenKeys.ps1 DefaultAppPool
در این حالت کلیدهای رمزنگاری اطلاعات به صورت دائمی به رجیستری ویندوز اضافه می‌شوند. این کلیدها به صورت خودکار توسط مکانیزم DPAPI ویندوز، رمزنگاری می‌شوند.

2) به تنظیمات پیشرفته‌ی Application pool برنامه در IIS مراجعه کرده و خاصیت Load user profile آن‌را true کنید.


در این حالت کلیدها به صورت دائمی در پوشه‌ی پروفایل کاربر مخصوص Application pool برنامه، به صورت رمزنگاری شده‌ی توسط مکانیزم DPAPI ویندوز، ذخیره خواهند شد.

3) یک SSL Certificate معتبر را تهیه کنید و یا اگر از یک self signed certificate استفاده می‌کنید باید آن‌‌را در Trusted Root store ویندوز قرار دهید. سپس از روش PersistKeysToFileSystem استفاده کنید.
public void ConfigureServices(IServiceCollection services)
{
   services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
    .ProtectKeysWithCertificate("thumbprint");
}

اگر از یک web farm استفاده می‌کنید، روش سوم ذکر شده، تنها روشی است که از آن می‌توانید استفاده کنید. یک پوشه‌ی اشتراکی قابل دسترسی بین سرورها را ایجاد کنید که دربرگیرنده‌ی X509 certificate شما باشد. سپس این پوشه و مجوز موجود در آن‌را توسط روش فوق به برنامه معرفی کنید.
  • #
    ‫۷ سال قبل، جمعه ۱۰ شهریور ۱۳۹۶، ساعت ۰۸:۲۸
    البته مشکل عدم رمزگشایی بعد از ریست شدن سرور مختص ویندوز نیست. در حالت پیش فرض محل ذخیره کلیدهای رمزنگاری تولید شده در حافظه است و همانطور که اشاره کردید باید با فراخوانی متد PersistKeysToFileSystem محلی را برای ذخیره سازی دائمی آنها تدارک دید. این محل میتواند پوشه ای در هاست شما باشد. (با توجه به عدم پشتیبانی از دات نت core توسط سرویس دهندگان در حال حاضر) و همچنین میتوان با پیاده‌سازی سفارشی از واسطهای IXmlDecryptor و IXmlEncryptor  و تزریق آنها به سیستم از یک Certificate غیرمعتبر استفاده کرد و نیازی به ثبت آن در Root store نیست. 
    نظر شخصی بنده اینست که کلاسهای پیاده سازی شده برای رمزنگاری و رمزگشایی به شدت نگاه امنیتی بالایی را تدارک دیده است که در بیشتر سناریوها واقعا نیازی به اینهمه سطح از پیچیدگی وجود ندارد. در واقع میتوان با پیاده سازی واسط IDataProtectionProvider و تزریق آن به سیستم از روش رمزنگاری و رمزگشایی دلخواهی استفاده کرد.
  • #
    ‫۷ سال قبل، شنبه ۱۱ شهریور ۱۳۹۶، ساعت ۰۰:۳۷
    یک نکته‌ی تکمیلی: روش ذخیره سازی کلید موقتی تولید شده در بانک اطلاعاتی بجای حافظه‌ی سرور

    سیستم data protection به همراه اینترفیسی است به نام IXmlRepository که از آن می‌توان برای مشخص سازی محل ذخیره سازی XML ایی اطلاعات کلید تولید شده استفاده کرد. این امکان هم وجود دارد که این اینترفیس را طوری پیاده سازی کرد تا اطلاعات را درون بانک اطلاعاتی ذخیره کند. به صورت ذیل:
    ابتدا کلاس AppDataProtectionKey را به عنوان یک موجودیت جدید به سیستم EF معرفی می‌کنیم:
    public class AppDataProtectionKey
    {
        public int Id { get; set; }
        public string FriendlyName { get; set; }
        public string XmlData { get; set; }
    }
    کار این جدول، ذخیره سازی اطلاعات کلید موقتی است تا پس از ری استارت سرور، این اطلاعات از دست نروند و قابلیت بازیابی خودکار را داشته باشند.


    سپس آن‌را به Context برنامه به صورت ذیل اضافه می‌کنیم:
     public virtual DbSet<AppDataProtectionKey> AppDataProtectionKeys { get; set; }
    با این تنظیمات:
    modelBuilder.Entity<AppDataProtectionKey>(builder =>
    {
       builder.ToTable("AppDataProtectionKeys");
       builder.HasIndex(e => e.FriendlyName).IsUnique();
    });

    در ادامه پیاده سازی ویژه‌ی ذیل را از IXmlRepository، که از اطلاعات فوق استفاده می‌کند، تهیه خواهیم کرد:
        public class DataProtectionKeyService : IXmlRepository
        {
            private readonly IServiceProvider _serviceProvider;
    
            public DataProtectionKeyService(IServiceProvider serviceProvider)
            {
                _serviceProvider = serviceProvider;
                _serviceProvider.CheckArgumentIsNull(nameof(_serviceProvider));
            }
    
            public IReadOnlyCollection<XElement> GetAllElements()
            {
                return _serviceProvider.RunScopedContext<ReadOnlyCollection<XElement>>(context =>
                {
                    var dataProtectionKeys = context.Set<AppDataProtectionKey>();
                    return new ReadOnlyCollection<XElement>(dataProtectionKeys.Select(k => XElement.Parse(k.XmlData)).ToList());
                });
            }
    
            public void StoreElement(XElement element, string friendlyName)
            {
                // We need a separate context to call its SaveChanges several times,
                // without using the current request's context and changing its internal state.
                _serviceProvider.RunScopedContext(context =>
                {
                    var dataProtectionKeys = context.Set<AppDataProtectionKey>();
                    var entity = dataProtectionKeys.SingleOrDefault(k => k.FriendlyName == friendlyName);
                    if (null != entity)
                    {
                        entity.XmlData = element.ToString();
                        dataProtectionKeys.Update(entity);
                    }
                    else
                    {
                        dataProtectionKeys.Add(new AppDataProtectionKey
                        {
                            FriendlyName = friendlyName,
                            XmlData = element.ToString()
                        });
                    }
                    context.SaveChanges();
                });
            }
        }
    در این اینترفیس نحوه‌ی دسترسی به یک context جدید، اندکی متفاوت است از حالت‌های متداول. در اینجا چون می‌خواهیم این کلاس تاثیری را بر روی واحد کار درخواست جاری نگذارد، یک context جدید را برای آن وهله سازی می‌کنیم و از context موجود در طی طول عمر درخواست جاری استفاده نخواهیم کرد.
    اطلاعات متدهای سرویس فوق به صورت خودکار توسط سیستم data-protection تامین می‌شوند. تنها کاری را که در اینجا انجام داده‌ایم، گوش فرادادن به این تغییرات و ذخیره سازی آن‌ها در بانک اطلاعاتی است.

    مرحله‌ی آخر کار، معرفی این تغییرات به سیستم است که نحوه‌ی انجام آن‌را در ذیل مشاهده می‌کنید:
            private static void addCustomDataProtection(this IServiceCollection services, SiteSettings siteSettings)
            {
                services.AddScoped<IXmlRepository, DataProtectionKeyService>();
                services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(serviceProvider =>
                {
                    return new ConfigureOptions<KeyManagementOptions>(options =>
                    {
                        var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
                        using (var scope = scopeFactory.CreateScope())
                        {
                            options.XmlRepository = scope.ServiceProvider.GetService<IXmlRepository>();
                        }
                    });
                });
                services
                    .AddDataProtection()
                    .SetDefaultKeyLifetime(siteSettings.CookieOptions.ExpireTimeSpan)
                    .SetApplicationName(siteSettings.CookieOptions.CookieName)
                    .UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
                    {
                        EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
                        ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
                    });
            }
    ابتدا محل تامین سرویس IXmlRepository مشخص شده‌است. سپس روش مقدار دهی XmlRepository  را ملاحظه می‌کنید که باید به این صورت باشد. مقدار آن نیز از سرویس DataProtectionKeyService سفارشی ما تامین می‌شود. در انتها طول عمر کلید تولید شده، نام برنامه و الگوریتم‌های مدنظر تنظیم شده‌اند.

    همین مقدار تنظیم سبب خواهد شد تا به صورت خودکار اطلاعات موقتی کلیدهای رمزنگاری سیستم data-protection در بانک اطلاعاتی ذخیره شده و یا بازیابی شوند.

    این تغییرات به پروژه‌ی DNTIdentity اعمال شده‌اند.
    • #
      ‫۶ سال و ۶ ماه قبل، دوشنبه ۱۴ اسفند ۱۳۹۶، ساعت ۱۳:۲۳
      -چه زمانی به بانک اطلاعاتی رجوع میکند؟
      -به ازای هر درخواست به بانک اطلاعاتی رجوع میکند؟
      برای سناریو هایی که تعداد کاربران زیاد، به همراه تعداد درخواست‌های زیاد وسرعت بالا مورد نیاز است، این روش بهینه میباشد؟(البته قبول دارم که باید جهت این نیاز سرور‌های اختصاصی تهیه کرد)
      -سرعت کاهش پیدا نمیکند؟
      بهتر نیست از FileSystem بهره برد؟
      • #
        ‫۶ سال و ۶ ماه قبل، دوشنبه ۱۴ اسفند ۱۳۹۶، ساعت ۱۳:۳۸
        در پروژه‌ی DNTIdentity از این روش استفاده شده‌است. اگر برنامه را اجرا کنید و داخل متدهای کلاس DataProtectionKeyService آن break point قرار دهید، پس از آغاز اولیه برنامه، دیگر فراخوانی نمی‌شوند. بنابراین تاثیر منفی بر روی کارآیی برنامه ندارد.
        • #
          ‫۶ سال و ۶ ماه قبل، دوشنبه ۱۴ اسفند ۱۳۹۶، ساعت ۱۹:۲۴
          قبلا تست کرده بودم، ولی متوجه نمیشم چه زمانی به بانک رجوع میکنه!
    • #
      ‫۶ سال و ۴ ماه قبل، سه‌شنبه ۱۸ اردیبهشت ۱۳۹۷، ساعت ۲۰:۴۲
      با سلام و احترام
      بعد از ارتقاء به ASP.NET Core 2.1 فیلد serviceProvider در متد GetAllElements پس از مراجعه dispose می‌شود و این در حالی است که اگر
      services.AddScoped<IXmlRepository, DataProtectionKeyService>()
      را به مقدار
      services.AddSingleton<IXmlRepository, DataProtectionKeyService>()
      تغییر دهیم مشکل رفع می‌شود.
      آیا دلیل این موضوع تغییرات مربوط به Program.cs و متد CreateWebHostBuilder می‌باشد؟
      با تشکر
        • #
          ‫۶ سال و ۴ ماه قبل، چهارشنبه ۱۹ اردیبهشت ۱۳۹۷، ساعت ۰۵:۵۰
          آیا به جای استفاده از قطعه کد
          services.AddSingleton<IXmlRepository, DataProtectionKeyService>();
          services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(serviceProvider =>
          {
          return new ConfigureOptions<KeyManagementOptions>(options =>
          {
          var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
          using (var scope = scopeFactory.CreateScope())
          {
          options.XmlRepository = scope.ServiceProvider.GetService<IXmlRepository>();
          }
          });
          });

          با در نظر گرفتن مستندات، استفاده از کد زیر صحیح است ؟
          services.Configure<KeyManagementOptions>(options => options.XmlRepository = new DataProtectionKeyService(services.BuildServiceProvider()));
          • #
            ‫۶ سال و ۴ ماه قبل، چهارشنبه ۱۹ اردیبهشت ۱۳۹۷، ساعت ۰۶:۰۶
            در عمل یکی هستند.
    • #
      ‫۴ سال و ۴ ماه قبل، چهارشنبه ۲۷ فروردین ۱۳۹۹، ساعت ۰۸:۰۷
      بعد از Expire شدن کلید چه اتفاقی برای اطلاعاتی که قبلا با کلید قبلی Encrypt شدن میافته؟
      برای بخشی از اطلاعات از این روش برای رمزنگاری استفاده کردم آیا اون اطلاعات دیگر قابل خواندن نیست؟
  • #
    ‫۶ سال و ۶ ماه قبل، دوشنبه ۱۴ اسفند ۱۳۹۶، ساعت ۱۳:۱۹
    سلام؛
    -برای روش 3 ، حتما باید Certificate تهیه بشه؟
    -در سرور‌های اشتراکی دسترسی به IIs وجود نداره، و از DefaultAppPool استفاده میشه، بنابراین راه حلی به غیر از استفاده از بانک اطلاعاتی وجود نداره؟
    مثلا استفاده از فایل
    در PersistKeysToFileSystem  اشاره ای به استفاده اجباری از Cetificate  نشده!
    -اگر از
     services.AddDataProtection
    به تنهایی استفاده کنیم چه اتفاقی می‌افتد؟مشکل با این  گزینه حل نشد، در صورتیکه اجباری به استفاده از PesistKeysToFileSysytem نمیباشد؛ مطابق گفته
    سرور‌های اشتراکی جدید netcore را پوشش داده‌اند.
    • #
      ‫۶ سال و ۶ ماه قبل، دوشنبه ۱۴ اسفند ۱۳۹۶، ساعت ۱۴:۱۲
      - services.AddDataProtection یعنی همان مقدمه‌ی بحث؛ یا ذخیره سازی کلیدها در حافظه به صورت پیش‌فرض. مابقی بحث جهت دائمی کردن این کلیدها است. البته دائمی کردن هم طول عمری دارد.
      - در سرورهای اشتراکی یا از روش «یک نکته‌ی تکمیلی: روش ذخیره سازی کلید موقتی تولید شده در بانک اطلاعاتی بجای حافظه‌ی سرور » استفاده کنید، یا با هاست تماس بگیرید و تنظیم گزینه‌ی 2 یا همان Load user profile به true را به آن‌ها اعلام کنید (چون تنظیمات برنامه‌های ASP.NET Core با نگارش‌های قبلی یکی نیست؛ این یک مورد را هم بهتر است به لیست تنظیمات اولیه‌ی برنامه اضافه کنند).
      - در حالت سوم، ذکر Certificate برای رمزنگاری اطلاعات ضروری است؛ در غیراینصورت این کلیدها به صورت معمولی و واضح ذخیره خواهند شد.
  • #
    ‫۴ سال و ۱۱ ماه قبل، چهارشنبه ۳ مهر ۱۳۹۸، ساعت ۱۸:۳۹
    یک نکته‌ی تکمیلی: پیاده سازی IXmlRepository مایکروسافت برای EF Core

    از زمان ارائه‌ی NET Core 2.2.، بسته‌ی نیوگت جدید Microsoft.AspNetCore.DataProtection.EntityFrameworkCore ارائه شده‌است که کار آن دقیقا شبیه به پیاده سازی «یک نکته‌ی تکمیلی: روش ذخیره سازی کلید موقتی تولید شده در بانک اطلاعاتی بجای حافظه‌ی سرور» است که در نظرات فوق ارائه شد.
    برای استفاده‌ی از آن، ابتدا بسته‌ی نیوگت آن‌را به برنامه اضافه کنید:
    dotnet add package Microsoft.AspNetCore.DataProtection.EntityFrameworkCore

    سپس Context ای را که بر اساس اینترفیس IDataProtectionKeyContext آن پیاده سازی شده‌است و دارای DbSet جدید از نوع DataProtectionKey است، تعریف کنید:
        public class MyKeysContext : DbContext, IDataProtectionKeyContext
        {
            // A recommended constructor overload when using EF Core 
            // with dependency injection.
            public MyKeysContext(DbContextOptions<MyKeysContext> options) 
                : base(options) { }
    
           // This maps to the table that stores keys.
            public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
        }
    که با اجرای مهاجرت‌ها، یک جدول جدید را با سه فیلد زیر، ایجاد می‌کند:
    public int Id { get; set; }
    public string FriendlyName { get; set; }
    public string XmlData { get; set; }

    در آخر روش معرفی این Context به سیستم DataProtection به صورت زیر است:
    public void ConfigureServices(IServiceCollection services)
    {
       // using Microsoft.AspNetCore.DataProtection;
        services.AddDataProtection()
            .PersistKeysToDbContext<MyKeysContext>();
    }
    به این ترتیب، به صورت خودکار، اطلاعات موقتی کلیدهای رمزنگاری سیستم data-protection در بانک اطلاعاتی ذخیره شده و یا بازیابی می‌شوند.
  • #
    ‫۴ سال و ۱۱ ماه قبل، دوشنبه ۱۵ مهر ۱۳۹۸، ساعت ۲۳:۳۷
    یک نکته‌ی تکمیلی: روش محافظت از کلیدهای سیستم DataProtection با یک مجوز SSL

    اگر برنامه‌های ASP.NET Core را اجرا کرده باشید، عموما در ابتدای آن یک پیام محافظت نشده بودن کلیدهای سیستم DataProtection را لاگ می‌کند. برای رفع این مشکل می‌توان این مراحل را طی کرد:
    الف) نیاز به یک مجوز SSL داریم که دارای private key هم باشد.
    برای این منظور سه دستور زیر را صادر کنید تا یک فایل pfx مناسب سیستم DataProtection تولید شود:
    "C:\Program Files\Git\usr\bin\openssl.exe" genrsa -out private.key 2048
    "C:\Program Files\Git\usr\bin\openssl.exe" req -new -x509 -key private.key -out publickey.cer -days 1398
    "C:\Program Files\Git\usr\bin\openssl.exe" pkcs12 -export -out idp.pfx -inkey private.key -in publickey.cer
    این دستورات از openssl.exe برنامه‌ی Git for windows استفاده می‌کنند. اگر فایل pfx نهایی دارای private key نباشد (روش فوق این مشکل را ندارد)، حین استفاده‌ی از آن در برنامه، با خطاهایی مانند «کلید یافت نشد» و یا «access denied» مواجه می‌شوید.

    ب) خواندن فایل pfx در برنامه
    روش خواندن فایل‌های pfx به صورت زیر است:
    private static X509Certificate2 loadCertificateFromFile(string filePath, string password)
    {
        // NOTE:
        // You should check out the identity of your application pool and make sure
        // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work.
    
        // For decryption the certificate must be in the certificate store. It's a limitation of how EncryptedXml works.
        using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
        {
          store.Open(OpenFlags.ReadWrite);
          store.Add(new X509Certificate2(filePath, password, X509KeyStorageFlags.Exportable));
        }
    
        return new X509Certificate2(
                filePath,
                password,
                keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet
                                 | X509KeyStorageFlags.Exportable);
    }
    دو نکته در اینجا مهم هستند: اگر از IIS استفاده می‌کنید، روشن کردن گزینه‌ی «Load user profile» را در Application pool برنامه فراموش نکنید، تا سیستم RSA به خوبی کار کند. همچنین در اینجا قسمت store.Add الزامی است. از این جهت که ASP.NET Core برای کار decryption کلیدها، فقط به اطلاعات X509Store سیستم مراجعه می‌کند و کاری به فایل pfx ما ندارد.

    ج) معرفی مجوز تولید شده به سیستم
    دراینجا آخرین مرحله، ذکر متد ProtectKeysWithCertificate به همراه مجوزی است که تولید کردیم:
    services
       .AddDataProtection()
       .SetDefaultKeyLifetime(...)
       .SetApplicationName(...)
       .ProtectKeysWithCertificate(loadCertificateFromFile("path ...", "123"));
    اکنون اگر برنامه را اجرا کنید، از فایل pfx تولیدی، برای رمزنگاری کلیدهای سیستم DataProtection استفاده خواهد شد.