Closed amcasey closed 4 months ago
API Review Notes:
How can this ever be used safely?
ITimeLimitedDataProtector
specifically if we know the expiration is protected data is always short enough.How bad is it really to never delete keys?
Why do we need this to be an abstraction vs something implemented on specific stores? Is there ever someone who would want to call this without knowing exactly what IXmlRepository is being used?
Should XmlKeyManager implement the new interface methods explicitly?
Do we want to create a new interface rather than use DIMs?
#ifdef
ed out of netstandard.Should unsafeIncludeUnexpired
even be a parameter considering you can filter yourself?
Why Action<IReadOnlyCollection<IDeletableElement>>
instead of Func<XElement, bool>
IKey
cannot be created until you have every XElement
so you know whether or not the key is revoked.Should we include RedisXmlRepository
in this?
After a lot of discussion that focused mostly on whether or not we should add an API to delete data protection keys at all considering the potentially risks and relatively small payoff, we did not come to a consensus either way. We will continue this discussion at a later point.
It seems dangerous for a library to do this considering only application developers would know how long they need to be able to unprotect protected data for.
I think this comment was about potential future consumers but, in the event that it was about the key manager described in https://github.com/dotnet/aspnetcore/issues/52916, that's a scenario-specific app and not a library.
It's possible, but inconvenient, to manually trim the FileSystemXmlRepository if you know the implementation details.
And, if you've already violated the abstraction that far (by downcasting and doing your own xml parsing), you might as well just access the files on disk.
DIMs were chosen to avoid the proliferation of too many interfaces, but it does mean that the interface APIs will have to be #ifdefed out of netstandard.
It also means having to add a CanDelete
property, and those will also proliferate. A bool is likely faster than a runtime type check, but that shouldn't matter at the scale at which it would be called.
Your updates here got me wondering why not just add APIs to the store level (IXmlRepository) and specifically don't expose delete on the key manager.
@brockallen Obviously, there's room for debate when a feature is not intended to be conveniently usable, but I'll lay out my personal reasoning: the IXmlRepository is a very thin layer over the backing store. To manipulate keys via the repository, you have to know the XML format (which is documented), not only of individual elements but of how they related (e.g. revocations are stored separately from the keys they revoke). Once you have to implement all that logic yourself (as the app developer who wants to delete keys), it's a small additional step to just manipulate the backing store directly.
Are you aware of a scenario where an app developer would want to delete keys (which requires intimate knowledge of how the app uses them) without also knowing where they're stored (which having an IXmlRepository API would abstract away)?
Ok, yes I see what you're getting at... mainly because the only non-payload thing we have at this level is friendlyName
(and even that isn't really too concrete). It was just a thought to put it at a level where it was sufficiently obscure, but available. But you're right -- in short, there's no identifier at this level. Bummer.
I thought we might be able to use friendlyName
but (a) it's optional and not all impls actually use it and (b) the impls that use it may reject it for containing invalid characters and substitute their own!
This still feels like a case where a specialized tool could address the same needs. If it were to be a public API, we could make it more intuitive to call but harder to implement by dropping the lambdas and making it appear to have random access. The implementations that need this (now) do/could provide random access.
Still not ready to approve this.
Not having deletion could be said to make the storage API conceptually incomplete
Most people will never call this API (since most people never call the existing APIs)
We could have a simpler, random-access version of this API
Do we need unsafeIncludeUnexpired?
Expected to be consumed in 9.0 (i.e. not waiting until 10.0)
The order in which keys are deleted is important - don't delete a revocation before the corresponding key
We could call repository deletion twice - once for keys and again for revocations
@amcasey to go figure out an ordering API (and remove unsafeIncludeUnexpired) and return
We're not going to block the API, even though we don't like it
We should also implement this for the in-memory repository.
namespace Microsoft.AspNetCore.DataProtection.KeyManagement;
public interface IKeyManager
{
+#if NETCOREAPP
+ /// <summary>
+ /// Indicates whether this key manager supports key deletion.
+ /// </summary>
+ /// <remarks>
+ /// Deletion is stronger than revocation. A revoked key is retained and can even be (forcefully) applied.
+ /// A deleted key is indistinguishable from a key that never existed.
+ /// </remarks>
+ bool CanDeleteKeys => false;
+
+ /// <summary>
+ /// Deletes keys matching a predicate.
+ ///
+ /// Use with caution as deleting active keys will normally cause data loss.
+ /// </summary>
+ /// <param name="shouldDelete">
+ /// A predicate applied to each key.
+ /// Returning true will cause the key to be deleted.
+ /// </param>
+ /// <returns>
+ /// True if all attempted deletions succeeded.
+ /// </returns>
+ /// <remarks>
+ /// Generally, keys should only be deleted to save space. If space is not a concern, keys
+ /// should be revoked or allowed to expire instead.
+ ///
+ /// This method will not mutate existing IKey instances. After calling this method,
+ /// all existing IKey instances should be discarded, and GetAllKeys should be called again.
+ /// </remarks>
+ /// <exception cref="NotSupportedException">
+ /// If <see cref="CanDeleteKeys"/> is false.
+ /// </exception>
+ bool DeleteKeys(Func<IKey, bool> shouldDelete) => throw new NotSupportedException();
+#endif
}
public sealed class XmlKeyManager
{
+#if NETCOREAPP
+ public bool CanDeleteKeys { get; }
+ public bool DeleteKeys(Func<IKey, bool> shouldDelete)
+#endif
}
namespace Microsoft.AspNetCore.DataProtection.Repositories;
+/// <summary>
+/// Represents an XML element in an <see cref="IXmlRepository"/> that can be deleted.
+/// </summary>
+public interface IDeletableElement
+{
+ /// <summary>The XML element.</summary>
+ public XElement Element { get; }
+ /// <summary>Elements are deleted in increasing DeletionOrder. <c>null</c> means "don't delete".</summary>
+ public int? DeletionOrder { get; set; }
+}
public interface IXmlRepository
{
+#if NETCOREAPP
+ /// <summary>
+ /// Indicates whether this respository supports removal.
+ /// </summary>
+ bool CanRemoveElements => false;
+
+ /// <summary>
+ /// Removes selected elements from the repository.
+ /// </summary>
+ /// <param name="chooseElements">
+ /// A snapshot of the elements in this repository.
+ /// For each, set <see cref="IDeletableElement.DeletionOrder"/> to a non-<c>null</c> value if it should be deleted.
+ /// Elements are deleted in increasing order. If any deletion fails, the remaining deletions *MUST* be skipped.
+ /// </param>
+ /// <returns>
+ /// True if all deletions succeeded.
+ /// </returns>
+ /// <exception cref="NotSupportedException">
+ /// If <see cref="CanRemoveElements"/> is false.
+ /// </exception>
+ bool RemoveElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements) => throw new NotSupportedException();
+#endif
}
public class FileSystemXmlRepository
{
+ public virtual bool CanRemoveElements { get; }
+ public virtual bool RemoveElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements)
}
public class RegistryXmlRepository
{
+ public virtual bool CanRemoveElements { get; }
+ public virtual bool RemoveElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements)
}
namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
public class EntityFrameworkCoreXmlRepository<TContext>
{
+ public virtual bool CanRemoveElements { get; }
+ public virtual bool RemoveElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements)
}
namespace Microsoft.AspNetCore.DataProtection.StackExchangeRedis;
public class RedisXmlRepository
{
+ public virtual bool CanRemoveElements { get; }
+ public virtual bool RemoveElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements)
}
I don't actually have an implementation for RedisXmlRepository
yet. If it turns out not to be efficiently implementable, it might be preferable to drop that public API and let it return false
for now.
There's no public API for EphemeralXmlRepository
because it's internal.
Make new interfaces, rather than using #if
.
~Approved modulo interface changes.~ Edit: changes incoming.
namespace Microsoft.AspNetCore.DataProtection.KeyManagement;
+/// <summary>
+/// The basic interface for performing key management operations.
+/// </summary>
+/// <remarks>
+/// Instantiations of this interface are expected to be thread-safe.
+/// </remarks>
+public interface IDeletableKeyManager : IKeyManager
+{
+ /// <summary>
+ /// Indicates whether this key manager supports key deletion.
+ /// </summary>
+ /// <remarks>
+ /// Deletion is stronger than revocation. A revoked key is retained and can even be (forcefully) applied.
+ /// A deleted key is indistinguishable from a key that never existed.
+ /// </remarks>
+ bool CanDeleteKeys { get; }
+
+ /// <summary>
+ /// Deletes keys matching a predicate.
+ ///
+ /// Use with caution as deleting active keys will normally cause data loss.
+ /// </summary>
+ /// <param name="shouldDelete">
+ /// A predicate applied to each key.
+ /// Returning true will cause the key to be deleted.
+ /// </param>
+ /// <returns>
+ /// True if all attempted deletions succeeded.
+ /// </returns>
+ /// <remarks>
+ /// Generally, keys should only be deleted to save space. If space is not a concern, keys
+ /// should be revoked or allowed to expire instead.
+ ///
+ /// This method will not mutate existing IKey instances. After calling this method,
+ /// all existing IKey instances should be discarded, and GetAllKeys should be called again.
+ /// </remarks>
+ /// <exception cref="NotSupportedException">
+ /// If <see cref="CanDeleteKeys"/> is false.
+ /// </exception>
+ bool DeleteKeys(Func<IKey, bool> shouldDelete);
+}
// These could be private - they're non-virtual on a sealed type
public sealed class XmlKeyManager
{
+ public bool CanDeleteKeys { get; }
+ public bool DeleteKeys(Func<IKey, bool> shouldDelete)
}
namespace Microsoft.AspNetCore.DataProtection.Repositories;
+ /// <summary>
+ /// Represents an XML element in an <see cref="IXmlRepository"/> that can be deleted.
+ /// </summary>
+ public interface IDeletableElement
+ {
+ /// <summary>The XML element.</summary>
+ public XElement Element { get; }
+ /// <summary>Elements are deleted in increasing DeletionOrder. <c>null</c> means "don't delete".</summary>
+ public int? DeletionOrder { get; set; }
+ }
+ /// <summary>
+ /// The basic interface for storing and retrieving XML elements.
+ /// </summary>
+ public interface IDeletableXmlRepository : IXmlRepository
+ {
+ /// <summary>
+ /// Removes selected elements from the repository.
+ /// </summary>
+ /// <param name="chooseElements">
+ /// A snapshot of the elements in this repository.
+ /// For each, set <see cref="IDeletableElement.DeletionOrder"/> to a non-<c>null</c> value if it should be deleted.
+ /// Elements are deleted in increasing order. If any deletion fails, the remaining deletions *MUST* be skipped.
+ /// </param>
+ /// <returns>
+ /// True if all deletions succeeded.
+ /// </returns>
+ bool RemoveElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements);
+ }
-public class FileSystemXmlRepository : IXmlRepository
+public class FileSystemXmlRepository : IDeletableXmlRepository
{
+ public virtual bool RemoveElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements)
}
-public class RegistryXmlRepository : IXmlRepository
+public class RegistryXmlRepository : IDeletableXmlRepository
{
+ public virtual bool RemoveElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements)
}
IDeletableKeyManager.CanDeleteKeys seems redundant now that there's a new interface.
IDeletableKeyManager.CanDeleteKeys seems redundant now that there's a new interface.
It exposes whether the underlying repository implements its new interface.
API Approved!
Minor change - "remove" -> "delete" for consistency. Approved offline.
namespace Microsoft.AspNetCore.DataProtection.KeyManagement;
+/// <summary>
+/// An extension of <see cref="IKeyManager"/> that supports key deletion.
+/// </summary>+public interface IDeletableKeyManager : IKeyManager
+{
+ /// <summary>
+ /// Indicates whether this key manager supports key deletion.
+ /// </summary>
+ /// <remarks>
+ /// Deletion is stronger than revocation. A revoked key is retained and can even be (forcefully) applied.
+ /// A deleted key is indistinguishable from a key that never existed.
+ /// </remarks>
+ bool CanDeleteKeys { get; }
+
+ /// <summary>
+ /// Deletes keys matching a predicate.
+ ///
+ /// Use with caution as deleting active keys will normally cause data loss.
+ /// </summary>
+ /// <param name="shouldDelete">
+ /// A predicate applied to each key.
+ /// Returning true will cause the key to be deleted.
+ /// </param>
+ /// <returns>
+ /// True if all attempted deletions succeeded.
+ /// </returns>
+ /// <remarks>
+ /// Generally, keys should only be deleted to save space. If space is not a concern, keys
+ /// should be revoked or allowed to expire instead.
+ ///
+ /// This method will not mutate existing IKey instances. After calling this method,
+ /// all existing IKey instances should be discarded, and GetAllKeys should be called again.
+ /// </remarks>
+ /// <exception cref="NotSupportedException">
+ /// If <see cref="CanDeleteKeys"/> is false.
+ /// </exception>
+ bool DeleteKeys(Func<IKey, bool> shouldDelete);
+}
// These could be private - they're non-virtual on a sealed type
-public sealed class XmlKeyManager : IKeyManager
+public sealed class XmlKeyManager : IDeletableKeyManager
{
+ public bool CanDeleteKeys { get; }
+ public bool DeleteKeys(Func<IKey, bool> shouldDelete)
}
namespace Microsoft.AspNetCore.DataProtection.Repositories;
+ /// <summary>
+ /// Represents an XML element in an <see cref="IXmlRepository"/> that can be deleted.
+ /// </summary>
+ public interface IDeletableElement
+ {
+ /// <summary>The XML element.</summary>
+ public XElement Element { get; }
+ /// <summary>Elements are deleted in increasing DeletionOrder. <c>null</c> means "don't delete".</summary>
+ public int? DeletionOrder { get; set; }
+ }
+ /// <summary>
+ /// An extension of <see cref="IXmlRepository"/> that supports deletion of elements.
+ /// </summary>
+ public interface IDeletableXmlRepository : IXmlRepository
+ {
+ /// <summary>
+ /// Deletes selected elements from the repository.
+ /// </summary>
+ /// <param name="chooseElements">
+ /// A snapshot of the elements in this repository.
+ /// For each, set <see cref="IDeletableElement.DeletionOrder"/> to a non-<c>null</c> value if it should be deleted.
+ /// Elements are deleted in increasing order. If any deletion fails, the remaining deletions *MUST* be skipped.
+ /// </param>
+ /// <returns>
+ /// True if all deletions succeeded.
+ /// </returns>
+ bool DeleteElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements);
+ }
-public class FileSystemXmlRepository : IXmlRepository
+public class FileSystemXmlRepository : IDeletableXmlRepository
{
+ public virtual bool DeleteElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements)
}
-public class RegistryXmlRepository : IXmlRepository
+public class RegistryXmlRepository : IDeletableXmlRepository
{
+ public virtual bool DeleteElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements)
}
See revised proposal below.
Background and Motivation
[This supersedes #53502. which turned out to be impractical to implement]
PR: #53860
Data Protection has no facility for deleting keys. The design assumes that they will simply accumulate forever so that data protected with old keys will always be decryptable in an emergency. Per our docs:
However, for a very long-running application, there's a risk of long-defunct keys consuming too many resources. As part of #52916, we should consider allowing deletion.
As a bonus, we can use the Delete call as an excuse to GC redundant mass-revocations, etc.
Proposed API
I didn't do the in-memory or redis repositories, which seemed too ephemeral to benefit from deletion.
Usage Examples
Alternative Designs
IXmlRepository.RemoveElements
could use a complicatedFunc
, rather than a mutatingAction
: https://github.com/dotnet/aspnetcore/pull/53860/commits/20b9eb4bfa4ffbb25b93a5da5cf04cb529de61bdRisks
As with revocation, in memory representations of the deleted key will be unaffected.
We'll have to be careful how we revise/supplement the docs, since key deletion is a foot-gun.
There's no specific handling of concurrent deletions or externally-driven storage changes. Since this is basically a perf optimization, deletion is best effort and there's logging and a boolean result to tell you if not everything happened.
We can't easily bring the new APIs to .net framework, which lacks default interface methods. We can, optionally, bring some of the concrete implementations to framework.