diff options
author | vman <public@vaughnnugent.com> | 2022-12-09 13:54:16 -0500 |
---|---|---|
committer | vman <public@vaughnnugent.com> | 2022-12-09 13:54:16 -0500 |
commit | 8b5f3eebb9f8d9bd55e922a809ffa3bd52e33401 (patch) | |
tree | 024fcdd2445b2fe37fc96d2870879d0f6aa5626f /VNLib.Plugins.Extensions.Loading | |
parent | c9d9e6d23ad7b6fdf25f30de9b4a84be23885e16 (diff) |
Sql essentials classes moved, secret loading updates
Diffstat (limited to 'VNLib.Plugins.Extensions.Loading')
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs | 3 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs | 2 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs | 1 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs | 16 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/PrivateKey.cs | 102 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs | 23 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/S3Config.cs | 9 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/SecretResult.cs | 61 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Loading/VaultSecrets.cs | 122 |
9 files changed, 320 insertions, 19 deletions
diff --git a/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs b/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs index 2dffe88..5da16ec 100644 --- a/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs +++ b/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs @@ -28,9 +28,8 @@ using System.Threading; using System.Reflection; using McMaster.NETCore.Plugins; - -using VNLib.Utils; using System.Runtime.Loader; +using VNLib.Utils.Resources; namespace VNLib.Plugins.Extensions.Loading { diff --git a/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs b/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs index 62b898c..21f2fcb 100644 --- a/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs +++ b/VNLib.Plugins.Extensions.Loading/ConfigurationExtensions.cs @@ -192,7 +192,7 @@ namespace VNLib.Plugins.Extensions.Loading ClientId = s3conf.GetPropString("access_key"), ServerAddress = s3conf.GetPropString("server_address"), UseSsl = s3conf.TryGetValue("use_ssl", out JsonElement el) && el.GetBoolean(), - ClientSecret = plugin.TryGetSecretAsync(S3_SECRET_KEY).Result, + ClientSecret = plugin.TryGetSecretAsync(S3_SECRET_KEY), Region = s3conf.GetPropString("region"), }; } diff --git a/VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs b/VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs index b57ba6f..e9f3ff0 100644 --- a/VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs +++ b/VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs @@ -29,6 +29,7 @@ using System.Threading.Tasks; using VNLib.Utils; using VNLib.Utils.Extensions; using VNLib.Utils.Logging; +using VNLib.Utils.Resources; namespace VNLib.Plugins.Extensions.Loading.Events { diff --git a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs b/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs index bfe0de1..7c8caee 100644 --- a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs +++ b/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs @@ -72,15 +72,15 @@ namespace VNLib.Plugins.Extensions.Loading { PasswordHashing Passwords; //Get the global password system secret (pepper) - string pepperEl = plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY).Result ?? throw new KeyNotFoundException($"Missing required key '{PASSWORD_HASHING_KEY}' in secrets"); - - byte[] pepper = Convert.FromBase64String(pepperEl); - - //wipe the pepper string - Utils.Memory.Memory.UnsafeZeroMemory<char>(pepperEl); + using SecretResult pepperEl = plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY).Result ?? throw new KeyNotFoundException($"Missing required key '{PASSWORD_HASHING_KEY}' in secrets"); + + byte[] pepper = pepperEl.GetFromBase64(); ERRNO cb(Span<byte> buffer) { + //No longer valid peper if plugin is unloaded as its set to zero, so we need to protect it + plugin.ThrowIfUnloaded(); + pepper.CopyTo(buffer); return pepper.Length; } @@ -210,7 +210,7 @@ namespace VNLib.Plugins.Extensions.Loading Task deferred = Task.Run(asyncTask); //Add task to deferred list - plugin.DeferredTasks.Add(deferred); + plugin.ObserveTask(deferred); try { //Await the task results @@ -224,7 +224,7 @@ namespace VNLib.Plugins.Extensions.Loading finally { //Remove task when complete - plugin.DeferredTasks.Remove(deferred); + plugin.RemoveObservedTask(deferred); } } } diff --git a/VNLib.Plugins.Extensions.Loading/PrivateKey.cs b/VNLib.Plugins.Extensions.Loading/PrivateKey.cs new file mode 100644 index 0000000..336f6a4 --- /dev/null +++ b/VNLib.Plugins.Extensions.Loading/PrivateKey.cs @@ -0,0 +1,102 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: PrivateKey.cs +* +* PrivateKey.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Text; +using System.Security.Cryptography; + +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// <summary> + /// A container for a PKSC#8 encoed private key + /// </summary> + public sealed class PrivateKey : VnDisposeable + { + private readonly byte[] _utf8RawData; + + /// <summary> + /// Decodes the PKCS#8 encoded private key from a secret, as an EC private key + /// and recovers the ECDsa algorithm from the key + /// </summary> + /// <returns>The <see cref="ECDsa"/> algoritm from the private key</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="CryptographicException"></exception> + public ECDsa GetECDsa() + { + //Alloc buffer + using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(_utf8RawData.Length); + //Get base64 bytes from utf8 + ERRNO count = VnEncoding.Base64UrlDecode(_utf8RawData, buffer.Span); + //Parse the private key + ECDsa alg = ECDsa.Create(); + alg.ImportPkcs8PrivateKey(buffer.Span[..(int)count], out _); + //Wipe the buffer + Memory.InitializeBlock(buffer.Span); + return alg; + } + + /// <summary> + /// Decodes the PKCS#8 encoded private key from a secret, as an RSA private key + /// </summary> + /// <returns>The <see cref="RSA"/> algorithm from the private key</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="CryptographicException"></exception> + public RSA GetRSA() + { + //Alloc buffer + using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(_utf8RawData.Length); + //Get base64 bytes from utf8 + ERRNO count = VnEncoding.Base64UrlDecode(_utf8RawData, buffer.Span); + //Parse the private key + RSA alg = RSA.Create(); + alg.ImportPkcs8PrivateKey(buffer.Span[..(int)count], out _); + //Wipe the buffer + Memory.InitializeBlock(buffer.Span); + return alg; + } + + internal PrivateKey(SecretResult secret) + { + //Alloc and get utf8 + byte[] buffer = new byte[secret.Result.Length]; + int count = Encoding.UTF8.GetBytes(secret.Result, buffer); + //Verify length + if(count != buffer.Length) + { + throw new FormatException("UTF8 deocde failed"); + } + //Store + _utf8RawData = buffer; + } + + protected override void Free() + { + Memory.InitializeBlock(_utf8RawData.AsSpan()); + } + } +} diff --git a/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs b/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs index 84d858f..0c2c222 100644 --- a/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs +++ b/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs @@ -27,6 +27,7 @@ using System.Linq; using System.Text.Json; using System.Reflection; using System.Collections.Generic; +using System.Runtime.CompilerServices; using VNLib.Plugins.Extensions.Loading.Events; @@ -37,6 +38,8 @@ namespace VNLib.Plugins.Extensions.Loading.Routing /// </summary> public static class RoutingExtensions { + private static readonly ConditionalWeakTable<IEndpoint, PluginBase?> _pluginRefs = new(); + /// <summary> /// Constructs and routes the specific endpoint type for the current plugin /// </summary> @@ -60,6 +63,10 @@ namespace VNLib.Plugins.Extensions.Loading.Routing ScheduleIntervals(plugin, endpoint, endpointType, null); //Route the endpoint plugin.Route(endpoint); + + //Store ref to plugin for endpoint + _pluginRefs.Add(endpoint, plugin); + return endpoint; } else @@ -75,6 +82,10 @@ namespace VNLib.Plugins.Extensions.Loading.Routing ScheduleIntervals(plugin, endpoint, endpointType, conf); //Route the endpoint plugin.Route(endpoint); + + //Store ref to plugin for endpoint + _pluginRefs.Add(endpoint, plugin); + return endpoint; } } @@ -99,6 +110,18 @@ namespace VNLib.Plugins.Extensions.Loading.Routing return plugin.Route<T>(configAttr?.ConfigVarName); } + /// <summary> + /// Gets the plugin that loaded the current endpoint + /// </summary> + /// <param name="ep"></param> + /// <returns>The plugin that loaded the current endpoint</returns> + /// <exception cref="InvalidOperationException"></exception> + public static PluginBase GetPlugin(this IEndpoint ep) + { + _ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase); + return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed"); + } + private static void ScheduleIntervals<T>(PluginBase plugin, T endpointInstance, Type epType, IReadOnlyDictionary<string, JsonElement>? endpointLocalConfig) where T : IEndpoint { List<EventHandle> registered = new(); diff --git a/VNLib.Plugins.Extensions.Loading/S3Config.cs b/VNLib.Plugins.Extensions.Loading/S3Config.cs index de24522..6d4ae4d 100644 --- a/VNLib.Plugins.Extensions.Loading/S3Config.cs +++ b/VNLib.Plugins.Extensions.Loading/S3Config.cs @@ -22,7 +22,7 @@ * along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. */ -#nullable enable +using System.Threading.Tasks; namespace VNLib.Plugins.Extensions.Loading { @@ -30,9 +30,14 @@ namespace VNLib.Plugins.Extensions.Loading { public string? ServerAddress { get; init; } public string? ClientId { get; init; } - public string? ClientSecret { get; init; } + public Task<SecretResult?> ClientSecret { get; init; } public string? BaseBucket { get; init; } public bool? UseSsl { get; init; } public string? Region { get; init; } + + public S3Config() + { + ClientSecret = Task.FromResult<SecretResult?>(null); + } } } diff --git a/VNLib.Plugins.Extensions.Loading/SecretResult.cs b/VNLib.Plugins.Extensions.Loading/SecretResult.cs new file mode 100644 index 0000000..15323f3 --- /dev/null +++ b/VNLib.Plugins.Extensions.Loading/SecretResult.cs @@ -0,0 +1,61 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: SecretResult.cs +* +* SecretResult.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Utils; +using VNLib.Utils.Extensions; +using VNLib.Utils.Memory; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// <summary> + /// The result of a secret fetch operation + /// </summary> + public sealed class SecretResult : VnDisposeable + { + private readonly char[] _secretChars; + + /// <summary> + /// The protected raw result value + /// </summary> + public ReadOnlySpan<char> Result => _secretChars; + + + internal SecretResult(ReadOnlySpan<char> value) => _secretChars = value.ToArray(); + + ///<inheritdoc/> + protected override void Free() + { + Memory.InitializeBlock(_secretChars.AsSpan()); + } + + internal static SecretResult ToSecret(string? result) + { + SecretResult res = new(result.AsSpan()); + Memory.UnsafeZeroMemory<char>(result); + return res; + } + } +} diff --git a/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs b/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs index c429312..468600f 100644 --- a/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs +++ b/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs @@ -39,11 +39,15 @@ using VaultSharp.V1.AuthMethods.Token; using VaultSharp.V1.AuthMethods.AppRole; using VaultSharp.V1.SecretsEngines.PKI; +using VNLib.Utils; +using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; +using VNLib.Hashing.IdentityUtility; namespace VNLib.Plugins.Extensions.Loading { + /// <summary> /// Adds loading extensions for secure/centralized configuration secrets /// </summary> @@ -76,19 +80,19 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static Task<string?> TryGetSecretAsync(this PluginBase plugin, string secretName) + public static Task<SecretResult?> TryGetSecretAsync(this PluginBase plugin, string secretName) { //Get the secret from the config file raw string? rawSecret = TryGetSecretInternal(plugin, secretName); if (rawSecret == null) { - return Task.FromResult<string?>(null); + return Task.FromResult<SecretResult?>(null); } //Secret is a vault path, or return the raw value if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult<string?>(rawSecret); + return Task.FromResult<SecretResult?>(new(rawSecret.AsSpan())); } return GetSecretFromVaultAsync(plugin, rawSecret); } @@ -102,7 +106,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="UriFormatException"></exception> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static Task<string?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath) + public static Task<SecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath) { //print the path for debug if (plugin.IsDebug()) @@ -128,7 +132,7 @@ namespace VNLib.Plugins.Extensions.Loading string mount = path[..lastSep].ToString(); string secret = path[(lastSep + 1)..].ToString(); - async Task<string?> execute() + async Task<SecretResult?> execute() { //Try load client IVaultClient? client = _vaults.GetValue(plugin, TryGetVaultLoader).Value; @@ -137,7 +141,7 @@ namespace VNLib.Plugins.Extensions.Loading //run read async Secret<SecretData> result = await client.V1.Secrets.KeyValue.V2.ReadSecretAsync(path:secret, mountPoint:mount); //Read the secret - return result.Data.Data[secretTableKey].ToString(); + return SecretResult.ToSecret(result.Data.Data[secretTableKey].ToString()); } return Task.Run(execute); @@ -321,5 +325,111 @@ namespace VNLib.Plugins.Extensions.Loading //init lazy return new (LoadVault, LazyThreadSafetyMode.PublicationOnly); } + + /// <summary> + /// Gets the Secret value as a byte buffer + /// </summary> + /// <param name="secret"></param> + /// <returns>The base64 decoded secret as a byte[]</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static byte[] GetFromBase64(this SecretResult secret) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + + //Temp buffer + using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(secret.Result.Length); + + //Get base64 + if(Convert.TryFromBase64Chars(secret.Result, buffer, out int count)) + { + //Copy to array + byte[] value = buffer.Span[..count].ToArray(); + //Clear block before returning + Memory.InitializeBlock<byte>(buffer); + + return value; + } + + throw new InternalBufferTooSmallException("internal buffer too small"); + } + + /// <summary> + /// Recovers a certificate from a PEM encoded secret + /// </summary> + /// <param name="secret"></param> + /// <returns>The <see cref="X509Certificate2"/> parsed from the PEM encoded data</returns> + /// <exception cref="ArgumentNullException"></exception> + public static X509Certificate2 GetCertificate(this SecretResult secret) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + return X509Certificate2.CreateFromPem(secret.Result); + } + + /// <summary> + /// Gets the secret value as a secret result + /// </summary> + /// <param name="secret"></param> + /// <returns>The document parsed from the secret value</returns> + public static JsonDocument GetJsonDocument(this SecretResult secret) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + //Alloc buffer, utf8 so 1 byte per char + using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length); + //Get utf8 bytes + int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span); + //Reader and parse + Utf8JsonReader reader = new(buffer.Span[..count]); + return JsonDocument.ParseValue(ref reader); + } + + /// <summary> + /// Gets a SPKI encoded public key from a secret + /// </summary> + /// <param name="secret"></param> + /// <returns>The <see cref="PublicKey"/> parsed from the SPKI public key</returns> + /// <exception cref="ArgumentNullException"></exception> + public static PublicKey GetPublicKey(this SecretResult secret) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + //Alloc buffer, base64 is larger than binary value so char len is large enough + using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length); + //Get base64 bytes + ERRNO count = VnEncoding.TryFromBase64Chars(secret.Result, buffer.Span); + //Parse the SPKI from base64 + return PublicKey.CreateFromSubjectPublicKeyInfo(buffer.Span[..(int)count], out _); + } + + /// <summary> + /// Gets the value of the <see cref="SecretResult"/> as a <see cref="PrivateKey"/> + /// container + /// </summary> + /// <param name="secret"></param> + /// <returns>The <see cref="PrivateKey"/> from the secret value</returns> + /// <exception cref="FormatException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public static PrivateKey GetPrivateKey(this SecretResult secret) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + return new PrivateKey(secret); + } + + /// <summary> + /// Gets a <see cref="ReadOnlyJsonWebKey"/> from a secret value + /// </summary> + /// <param name="secret"></param> + /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the result</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public static ReadOnlyJsonWebKey GetJsonWebKey(this SecretResult secret) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + //Alloc buffer, utf8 so 1 byte per char + using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length); + //Get utf8 bytes + int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span); + return new ReadOnlyJsonWebKey(buffer.Span[..count]); + } } } |