| | | 1 | | using System.Security.Cryptography.X509Certificates; |
| | | 2 | | using Microsoft.AspNetCore.DataProtection; |
| | | 3 | | using ProjectTemplate.Web.Options; |
| | | 4 | | |
| | | 5 | | namespace ProjectTemplate.Web.Extensions; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Provides extension methods for configuring the persistent ASP.NET Core Data Protection key ring. |
| | | 9 | | /// </summary> |
| | | 10 | | public static class DataProtectionServiceExtensions |
| | | 11 | | { |
| | | 12 | | private const string _keyEncryptionPasswordWithoutPathMessage = |
| | | 13 | | "ProjectTemplate:DataProtection:KeyEncryptionCertificatePassword requires KeyEncryptionCertificatePath."; |
| | | 14 | | |
| | | 15 | | /// <summary> |
| | | 16 | | /// Registers Data Protection with a stable application discriminator and persistent filesystem key ring. |
| | | 17 | | /// </summary> |
| | | 18 | | /// <param name="services">The service collection to configure.</param> |
| | | 19 | | /// <param name="configuration">The application configuration source.</param> |
| | | 20 | | /// <param name="environment">The current hosting environment.</param> |
| | | 21 | | /// <returns>The same service collection instance for chaining.</returns> |
| | | 22 | | public static IServiceCollection AddApplicationDataProtection( |
| | | 23 | | this IServiceCollection services, |
| | | 24 | | IConfiguration configuration, |
| | | 25 | | IHostEnvironment environment) |
| | | 26 | | { |
| | 113 | 27 | | ArgumentNullException.ThrowIfNull(services); |
| | 113 | 28 | | ArgumentNullException.ThrowIfNull(configuration); |
| | 113 | 29 | | ArgumentNullException.ThrowIfNull(environment); |
| | | 30 | | |
| | 113 | 31 | | IConfigurationSection section = configuration.GetSection(ApplicationDataProtectionOptions.SectionName); |
| | | 32 | | |
| | 113 | 33 | | services |
| | 113 | 34 | | .AddOptions<ApplicationDataProtectionOptions>() |
| | 113 | 35 | | .Bind(section) |
| | 113 | 36 | | .Validate( |
| | 113 | 37 | | options => !string.IsNullOrWhiteSpace(options.ApplicationName), |
| | 113 | 38 | | "ProjectTemplate:DataProtection:ApplicationName is required.") |
| | 113 | 39 | | .Validate( |
| | 113 | 40 | | options => !string.IsNullOrWhiteSpace(options.KeyRingPath), |
| | 113 | 41 | | "ProjectTemplate:DataProtection:KeyRingPath is required.") |
| | 113 | 42 | | .Validate( |
| | 113 | 43 | | options => string.IsNullOrEmpty(options.KeyEncryptionCertificatePassword) || |
| | 113 | 44 | | !string.IsNullOrWhiteSpace(options.KeyEncryptionCertificatePath), |
| | 113 | 45 | | _keyEncryptionPasswordWithoutPathMessage) |
| | 113 | 46 | | .ValidateOnStart(); |
| | | 47 | | |
| | 113 | 48 | | ApplicationDataProtectionOptions options = section.Get<ApplicationDataProtectionOptions>() ?? new(); |
| | | 49 | | |
| | 113 | 50 | | string applicationName = !string.IsNullOrWhiteSpace(options.ApplicationName) |
| | 113 | 51 | | ? options.ApplicationName.Trim() |
| | 113 | 52 | | : throw new InvalidOperationException("ProjectTemplate:DataProtection:ApplicationName is required."); |
| | 112 | 53 | | string configuredKeyRingPath = !string.IsNullOrWhiteSpace(options.KeyRingPath) |
| | 112 | 54 | | ? options.KeyRingPath.Trim() |
| | 112 | 55 | | : throw new InvalidOperationException("ProjectTemplate:DataProtection:KeyRingPath is required."); |
| | 111 | 56 | | string keyRingPath = Path.IsPathFullyQualified(configuredKeyRingPath) |
| | 111 | 57 | | ? configuredKeyRingPath |
| | 111 | 58 | | : Path.GetFullPath(configuredKeyRingPath, environment.ContentRootPath); |
| | | 59 | | |
| | 111 | 60 | | IDataProtectionBuilder dataProtectionBuilder = services |
| | 111 | 61 | | .AddDataProtection() |
| | 111 | 62 | | .SetApplicationName(applicationName) |
| | 111 | 63 | | .PersistKeysToFileSystem(new DirectoryInfo(keyRingPath)); |
| | | 64 | | |
| | 111 | 65 | | if (!string.IsNullOrWhiteSpace(options.KeyEncryptionCertificatePath)) |
| | | 66 | | { |
| | 3 | 67 | | X509Certificate2 keyEncryptionCertificate = LoadKeyEncryptionCertificate( |
| | 3 | 68 | | options.KeyEncryptionCertificatePath.Trim(), |
| | 3 | 69 | | options.KeyEncryptionCertificatePassword, |
| | 3 | 70 | | environment.ContentRootPath); |
| | | 71 | | |
| | | 72 | | // ProtectKeysWithCertificate encrypts new keys. UnprotectKeysWithAnyCertificate supplies the same |
| | | 73 | | // certificate for decryption, which is required on Linux and macOS where the framework cannot resolve it |
| | | 74 | | // from a certificate store by thumbprint. |
| | 2 | 75 | | _ = dataProtectionBuilder |
| | 2 | 76 | | .ProtectKeysWithCertificate(keyEncryptionCertificate) |
| | 2 | 77 | | .UnprotectKeysWithAnyCertificate(keyEncryptionCertificate); |
| | | 78 | | } |
| | 108 | 79 | | else if (!string.IsNullOrEmpty(options.KeyEncryptionCertificatePassword)) |
| | | 80 | | { |
| | 1 | 81 | | throw new InvalidOperationException(_keyEncryptionPasswordWithoutPathMessage); |
| | | 82 | | } |
| | | 83 | | |
| | 109 | 84 | | return services; |
| | | 85 | | } |
| | | 86 | | |
| | | 87 | | private static X509Certificate2 LoadKeyEncryptionCertificate( |
| | | 88 | | string configuredCertificatePath, |
| | | 89 | | string? password, |
| | | 90 | | string contentRootPath) |
| | | 91 | | { |
| | 3 | 92 | | string certificatePath = Path.IsPathFullyQualified(configuredCertificatePath) |
| | 3 | 93 | | ? configuredCertificatePath |
| | 3 | 94 | | : Path.GetFullPath(configuredCertificatePath, contentRootPath); |
| | | 95 | | |
| | 3 | 96 | | if (!File.Exists(certificatePath)) |
| | | 97 | | { |
| | 1 | 98 | | throw new InvalidOperationException( |
| | 1 | 99 | | $"ProjectTemplate:DataProtection:KeyEncryptionCertificatePath '{certificatePath}' was not found."); |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | // EphemeralKeySet avoids writing the private key to the user profile or machine key store. macOS does not |
| | | 103 | | // support ephemeral key sets, so the default storage is used there. |
| | 2 | 104 | | X509KeyStorageFlags keyStorageFlags = OperatingSystem.IsMacOS() |
| | 2 | 105 | | ? X509KeyStorageFlags.DefaultKeySet |
| | 2 | 106 | | : X509KeyStorageFlags.EphemeralKeySet; |
| | | 107 | | |
| | 2 | 108 | | X509Certificate2 certificate = X509CertificateLoader.LoadPkcs12FromFile( |
| | 2 | 109 | | certificatePath, |
| | 2 | 110 | | password, |
| | 2 | 111 | | keyStorageFlags); |
| | | 112 | | |
| | 2 | 113 | | if (!certificate.HasPrivateKey) |
| | | 114 | | { |
| | 0 | 115 | | certificate.Dispose(); |
| | 0 | 116 | | throw new InvalidOperationException( |
| | 0 | 117 | | "ProjectTemplate:DataProtection:KeyEncryptionCertificatePath must reference a certificate that includes |
| | | 118 | | } |
| | | 119 | | |
| | 2 | 120 | | return certificate; |
| | | 121 | | } |
| | | 122 | | } |