Skip to content

Data Protection: CreateProtector reads the key store eagerly on .NET 11 (was lazy until first Protect/Unprotect on .NET 10) #67447

Closed

Description

@KennethHoff

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

In .NET 11, KeyRingBasedDataProtectionProvider.CreateProtector(string) now eagerly resolves the current key ring instead of deferring it to the first Protect/Unprotect. Creating a data protector now reads the key store immediately rather than on first use.

This matters at startup: apps create a protector during startup (e.g. app.UseAntiforgery() resolves IAntiforgery, cookie-auth setup, etc.). For apps that persist keys to an external store (PersistKeysToDbContext<T>(), Redis, Azure Blob), the app now performs a key-store round-trip at boot and fails to start if the store is unavailable — whereas on .NET 10 it started and only accessed the store on the first request that used data protection.

Cause: introduced by #64262 ("Spanify DataProtector with IBufferWriter<byte>", commit 249b29c2, on main only — not backported to release/10.0, which is why .NET 10 is unaffected). To return the faster span-based protector when the default encryptor supports it, CreateProtector now resolves the key ring up front to inspect the encryptor — and that is what pulls the key-store read into protector creation:

public IDataProtector CreateProtector(string purpose)
{
    ArgumentNullThrowHelper.ThrowIfNull(purpose);

    var currentKeyRing = _keyRingProvider.GetCurrentKeyRing();        // added in #64262 — reads the key store
    var encryptor = currentKeyRing.DefaultAuthenticatedEncryptor;     // needs the key ring

#if NET
    if (encryptor is ISpanAuthenticatedEncryptor)
    {
        return new KeyRingBasedSpanDataProtector(...);                // faster span path
    }
#endif
    return new KeyRingBasedDataProtector(...);                        // .NET 10 returned here without reading
}

This looks like an unintended side effect of a performance optimization rather than a deliberate decision to read keys at protector creation. Either way it's a behavioral change from .NET 10 that isn't currently documented.

Expected Behavior

Either:

  1. Key-store access stays deferred to the first Protect/Unprotect (no read during CreateProtector), matching .NET 10; or
  2. The change is intentional and is documented as a .NET 11 breaking change, with a supported opt-out. (The existing Microsoft.AspNetCore.DataProtection.KeyManagement.DisableAsyncKeyRingUpdate switch controls async refresh, not eager initialization, so it does not address this.)

Steps To Reproduce

Public minimal repro: https://github.com/KennethHoff/Repros/tree/master/DataProtectionEagerStartupNet11

git clone https://github.com/KennethHoff/Repros && cd Repros/DataProtectionEagerStartupNet11
nix run .#repro          # installs .NET 10 + 11 and runs both
# or, with the SDKs already installed:
dotnet run -f net11.0    # throws from CreateProtector (eager)
dotnet run -f net10.0    # CreateProtector returns; throws from Protect (lazy)

A self-contained console app — no web host, no antiforgery — with an IXmlRepository that throws when read, so where the read happens is visible in the stack trace. The same project runs on both runtimes; only the target framework differs:

using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddDataProtection()
    .AddKeyManagementOptions(options => options.XmlRepository = new TripwireXmlRepository());

using var provider = services.BuildServiceProvider();
var dataProtection = provider.GetRequiredService<IDataProtectionProvider>();

// Step 1 — .NET 11 reads the key store HERE (eager); .NET 10 returns without reading.
var protector = dataProtection.CreateProtector("repro");
Console.WriteLine("CreateProtector returned without reading the key store [.NET 10 path]");

// Step 2 — .NET 10 reads the key store HERE (lazy, on first use).
protector.Protect("hello");

internal sealed class TripwireXmlRepository : IXmlRepository
{
    IReadOnlyCollection<XElement> IXmlRepository.GetAllElements() =>
        throw new InvalidOperationException("IXmlRepository.GetAllElements() called — the key store was read");

    void IXmlRepository.StoreElement(XElement element, string friendlyName) { }
}

.csproj: SDK Microsoft.NET.Sdk, <TargetFrameworks>net10.0;net11.0</TargetFrameworks>, and <FrameworkReference Include="Microsoft.AspNetCore.App" /> (so Data Protection comes from the shared framework and the behavior is driven by the runtime version, not a pinned NuGet package).

Exceptions (if any)

.NET 11 — the read happens in CreateProtector:

System.InvalidOperationException: IXmlRepository.GetAllElements() called — the key store was read
   at TripwireXmlRepository.Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository.GetAllElements()
   at Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager.GetAllKeys()
   at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingProvider.GetCurrentKeyRingCoreNew(DateTime utcNow, Boolean forceRefresh)
   at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtectionProvider.CreateProtector(String purpose)
   at Program.<Main>$(String[] args)

.NET 10 — CreateProtector returns; the read happens in Protect:

System.Security.Cryptography.CryptographicException: An error occurred while trying to encrypt the provided data.
 ---> System.InvalidOperationException: IXmlRepository.GetAllElements() called — the key store was read
   at TripwireXmlRepository.Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository.GetAllElements()
   at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.Protect(Byte[] plaintext)
   at Microsoft.AspNetCore.DataProtection.DataProtectionCommonExtensions.Protect(IDataProtector protector, String plaintext)
   at Program.<Main>$(String[] args)

.NET Version

11.0.100-preview.5.26302.115

Anything else?

Activity

  1. added a commit that references this issue on Jun 27, 2026
  2. Youssef1313 commented on Jun 29, 2026

    @Youssef1313
    Member
  3. self-assigned this
    on Jun 29, 2026
  4. DeagleGross commented on Jun 29, 2026

    @DeagleGross
    Member

    Thanks for report, fix is attached; we'll see if we want to introduce a new ISpanDataProtectionProvider which will distinguish usage patterns. If you have suggestions or use-cases - please share!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions