Retirer la protection aux charges utiles dont les clés ont été révoquées dans ASP.NET Core

Les API de protection des données ASP.NET Core ne sont pas principalement destinées à la persistance indéfinie des charges utiles confidentielles. D’autres technologies telles que Windows CNG DPAPI et Azure Rights Management sont plus adaptées au scénario de stockage indéfini et disposent de fonctionnalités de gestion des clés fortes. Cela dit, rien n’empêche un développeur d’utiliser les API de protection des données ASP.NET Core pour la protection à long terme de données confidentielles. Les clés ne sont jamais supprimées de l’anneau de clés. IDataProtector.Unprotect peut ainsi toujours récupérer les charges utiles existantes tant que les clés sont disponibles et valides.

Un problème survient toutefois lorsque le développeur tente de supprimer la protection des données protégées avec une clé révoquée, car IDataProtector.Unprotect lève une exception dans ce cas. Cela peut convenir aux charges utiles temporaires ou de courte durée (comme les jetons d’authentification), car ces types de charges utiles peuvent facilement être recréés par le système et, au pire, le visiteur du site peut être amené à se reconnecter. Toutefois, pour les charges utiles persistantes, le fait que Unprotect soit jeté peut entraîner une perte de données inacceptable.

IPersistedDataProtector

Pour prendre en charge le scénario de l’autorisation de la fin de la protection des charges utiles, même en cas de clés révoquées, le système de protection des données contient un type IPersistedDataProtector. Pour obtenir une instance de IPersistedDataProtector, obtenez simplement une instance de IDataProtector de manière normale et essayez de caster le IDataProtector sur IPersistedDataProtector.

Notes

Toutes les instances IDataProtector ne peuvent pas être converties en IPersistedDataProtector. Les développeurs doivent utiliser le C# en tant qu’opérateur (ou son équivalent) pour éviter les exceptions de runtime causées par des casts non valides, et ils doivent être prêts à gérer le cas d’échec de manière appropriée.

IPersistedDataProtector expose la surface de l’API suivante :

DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors,
     out bool requiresMigration, out bool wasRevoked) : byte[]

Cette API prend la charge utile protégée (sous forme de tableau d’octets) et retourne la charge utile non protégée. Il n’y a aucune surcharge basée sur des chaînes. Les deux paramètres sortants sont les suivants.

  • requiresMigration : est défini sur true si la clé utilisée pour protéger cette charge utile n’est plus la clé active par défaut, par exemple, la clé utilisée pour protéger cette charge utile est ancienne et une opération de roulement de clés a été menée depuis lors. L’appelant peut souhaiter reprotéger la charge utile en fonction de ses besoins professionnels.

  • wasRevoked : est défini sur true si la clé utilisée pour protéger cette charge utile a été révoquée.

Avertissement

Faites preuve d’une extrême prudence lors de la transmission de ignoreRevocationErrors: true à la méthode DangerousUnprotect. Si, après avoir appelé cette méthode, la valeur wasRevoked est définie sur true, la clé utilisée pour protéger cette charge utile a été révoquée et l’authenticité de la charge utile doit être traitée comme suspecte. Dans ce cas, ne continuez à utiliser la charge utile non protégée que si vous disposez d’une assurance de son authenticité, par exemple, qu’elle provient d’une base de données sécurisée au lieu d’être envoyée par un client web non approuvé.

using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;

public class Program
{
    public static void Main(string[] args)
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDataProtection()
            // point at a specific folder and use DPAPI to encrypt keys
            .PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
            .ProtectKeysWithDpapi();
        var services = serviceCollection.BuildServiceProvider();

        // get a protector and perform a protect operation
        var protector = services.GetDataProtector("Sample.DangerousUnprotect");
        Console.Write("Input: ");
        byte[] input = Encoding.UTF8.GetBytes(Console.ReadLine());
        var protectedData = protector.Protect(input);
        Console.WriteLine($"Protected payload: {Convert.ToBase64String(protectedData)}");

        // demonstrate that the payload round-trips properly
        var roundTripped = protector.Unprotect(protectedData);
        Console.WriteLine($"Round-tripped payload: {Encoding.UTF8.GetString(roundTripped)}");

        // get a reference to the key manager and revoke all keys in the key ring
        var keyManager = services.GetService<IKeyManager>();
        Console.WriteLine("Revoking all keys in the key ring...");
        keyManager.RevokeAllKeys(DateTimeOffset.Now, "Sample revocation.");

        // try calling Protect - this should throw
        Console.WriteLine("Calling Unprotect...");
        try
        {
            var unprotectedPayload = protector.Unprotect(protectedData);
            Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }

        // try calling DangerousUnprotect
        Console.WriteLine("Calling DangerousUnprotect...");
        try
        {
            IPersistedDataProtector persistedProtector = protector as IPersistedDataProtector;
            if (persistedProtector == null)
            {
                throw new Exception("Can't call DangerousUnprotect.");
            }

            bool requiresMigration, wasRevoked;
            var unprotectedPayload = persistedProtector.DangerousUnprotect(
                protectedData: protectedData,
                ignoreRevocationErrors: true,
                requiresMigration: out requiresMigration,
                wasRevoked: out wasRevoked);
            Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
            Console.WriteLine($"Requires migration = {requiresMigration}, was revoked = {wasRevoked}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Input: Hello!
 * Protected payload: CfDJ8LHIzUCX1ZVBn2BZ...
 * Round-tripped payload: Hello!
 * Revoking all keys in the key ring...
 * Calling Unprotect...
 * CryptographicException: The key {...} has been revoked.
 * Calling DangerousUnprotect...
 * Unprotected payload: Hello!
 * Requires migration = True, was revoked = True
 */