aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-10-14 15:56:41 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-10-14 15:56:41 -0400
commite2164d088eb5209e2baecf09fbee06d6326fe910 (patch)
tree4efbb5278078ad4bb7ce1726f8745eb4a2eedb93 /plugins/VNLib.Plugins.Essentials.Accounts/src/MFA
parentb7ce7b48168d56931cae337bf1268b067edb7dce (diff)
track core updates & pki multi key
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts/src/MFA')
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/PkiAuthPublicKey.cs72
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs163
2 files changed, 185 insertions, 50 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/PkiAuthPublicKey.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/PkiAuthPublicKey.cs
new file mode 100644
index 0000000..a941852
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/PkiAuthPublicKey.cs
@@ -0,0 +1,72 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Accounts
+* File: PkiAuthPublicKey.cs
+*
+* PkiAuthPublicKey.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Accounts 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 Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+using VNLib.Hashing.IdentityUtility;
+
+namespace VNLib.Plugins.Essentials.Accounts.MFA
+{
+ /// <summary>
+ /// A json serializable JWK format public key for PKI authentication
+ /// </summary>
+ public record class PkiAuthPublicKey : IJsonWebKey
+ {
+ [JsonPropertyName("kid")]
+ public string? KeyId { get; set; }
+
+ [JsonPropertyName("kty")]
+ public string? KeyType { get; set; }
+
+ [JsonPropertyName("crv")]
+ public string? Curve { get; set; }
+
+ [JsonPropertyName("x")]
+ public string? X { get; set; }
+
+ [JsonPropertyName("y")]
+ public string? Y { get; set; }
+
+ [JsonPropertyName("alg")]
+ public string Algorithm { get; set; } = string.Empty;
+
+ [JsonIgnore]
+ public JwkKeyUsage KeyUse => JwkKeyUsage.Signature;
+
+ ///<inheritdoc/>
+ public string? GetKeyProperty(string propertyName)
+ {
+ return propertyName switch
+ {
+ "kid" => KeyId,
+ "kty" => KeyType,
+ "crv" => Curve,
+ "x" => X,
+ "y" => Y,
+ "alg" => Algorithm,
+ _ => null,
+ };
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
index bd434ae..f1d14f4 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
@@ -39,12 +39,11 @@ using VNLib.Plugins.Essentials.Sessions;
namespace VNLib.Plugins.Essentials.Accounts.MFA
{
- internal static class UserMFAExtensions
+ public static class UserMFAExtensions
{
public const string WEBAUTHN_KEY_ENTRY = "mfa.fido";
public const string TOTP_KEY_ENTRY = "mfa.totp";
public const string SESSION_SIG_KEY = "mfa.sig";
-
public const string USER_PKI_ENTRY = "mfa.pki";
/// <summary>
@@ -97,7 +96,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// <param name="entry">The <see cref="MFAEntry"/> to modify the TOTP configuration of</param>
/// <returns>The raw secret that was encrypted and stored in the <see cref="MFAEntry"/>, to send to the client</returns>
/// <exception cref="OutOfMemoryException"></exception>
- public static byte[] MFAGenreateTOTPSecret(this IUser user, MFAConfig config)
+ internal static byte[] MFAGenreateTOTPSecret(this IUser user, MFAConfig config)
{
_ = config.TOTPConfig ?? throw new NotSupportedException("The loaded configuration does not support TOTP");
//Generate a random key
@@ -118,7 +117,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// <returns>True if the user has TOTP configured and code matches against its TOTP secret entry, false otherwise</returns>
/// <exception cref="FormatException"></exception>
/// <exception cref="OutOfMemoryException"></exception>
- public static bool VerifyTOTP(this MFAConfig config, IUser user, uint code)
+ internal static bool VerifyTOTP(this MFAConfig config, IUser user, uint code)
{
//Get the base32 TOTP secret for the user and make sure its actually set
string base32Secret = user.MFAGetTOTPSecret();
@@ -126,13 +125,33 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
{
return false;
}
- //Alloc buffer with zero o
- using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc(base32Secret.Length, true);
- ERRNO count = VnEncoding.TryFromBase32Chars(base32Secret, buffer);
- //Verify the TOTP using the decrypted secret
- bool isValid = count && VerifyTOTP(code, buffer.AsSpan(0, count), config.TOTPConfig);
- //Zero out the buffer
- MemoryUtil.InitializeBlock(buffer.Span);
+
+ int length = base32Secret.Length;
+ bool isValid;
+
+ if (length > 256)
+ {
+ //Alloc buffer with zero o
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage(base32Secret.Length, true);
+
+ ERRNO count = VnEncoding.TryFromBase32Chars(base32Secret, buffer);
+ //Verify the TOTP using the decrypted secret
+ isValid = count && VerifyTOTP(code, buffer.AsSpan(0, count), config.TOTPConfig);
+ //Zero out the buffer
+ MemoryUtil.InitializeBlock(buffer.Span);
+ }
+ else
+ {
+ //stack alloc buffer
+ Span<byte> buffer = stackalloc byte[base32Secret.Length];
+
+ ERRNO count = VnEncoding.TryFromBase32Chars(base32Secret, buffer);
+ //Verify the TOTP using the decrypted secret
+ isValid = count && VerifyTOTP(code, buffer[..(int)count], config.TOTPConfig);
+ //Zero out the buffer
+ MemoryUtil.InitializeBlock(buffer);
+ }
+
return isValid;
}
@@ -202,8 +221,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
#endregion
#region PKI
- const int JWK_KEY_BUFFER_SIZE = 2048;
-
+
/// <summary>
/// Gets a value that determines if the user has PKI enabled
/// </summary>
@@ -220,43 +238,46 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// <returns>True if the user has PKI enabled, the key was recovered, the key id matches, and the JWT signature is verified</returns>
public static bool PKIVerifyUserJWT(this IUser user, JsonWebToken jwt, string keyId)
{
- //Recover key data from user, it may not be enabled
- using ReadOnlyJsonWebKey? jwk = RecoverKey(user);
-
- if(jwk == null)
- {
- return false;
- }
+ /*
+ * Since multiple keys can be stored, we need to recover the key that matches the desired key id
+ */
+ PkiAuthPublicKey? pub = PkiGetAllPublicKeys(user)?.FirstOrDefault(p => keyId.Equals(p.KeyId, StringComparison.Ordinal));
- //Confim the key id matches
- if(!keyId.Equals(jwk.KeyId, StringComparison.OrdinalIgnoreCase))
+ if(pub == null)
{
return false;
}
-
+
//verify the jwt
- return jwt.VerifyFromJwk(jwk);
+ return jwt.VerifyFromJwk(pub);
}
-
- public static void PKISetUserKey(this IUser user, IReadOnlyDictionary<string, string>? keyFields)
+
+ /// <summary>
+ /// Stores an array of public keys in the user's account object
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="authKeys">The array of jwk format keys to store for the user</param>
+ public static void PKISetPublicKeys(this IUser user, PkiAuthPublicKey[]? authKeys)
{
- if(keyFields == null)
+ if(authKeys == null || authKeys.Length == 0)
{
user[USER_PKI_ENTRY] = null!;
return;
}
//Serialize the key data
- byte[] keyData = JsonSerializer.SerializeToUtf8Bytes(keyFields, Statics.SR_OPTIONS);
-
- //convert to base32 string before writing user data
- string base64 = Convert.ToBase64String(keyData);
+ byte[] keyData = JsonSerializer.SerializeToUtf8Bytes(authKeys, Statics.SR_OPTIONS);
- //Store key data
- user[USER_PKI_ENTRY] = base64;
+ //convert to base64 string before writing user data
+ user[USER_PKI_ENTRY] = VnEncoding.ToBase64UrlSafeString(keyData, false);
}
- private static ReadOnlyJsonWebKey? RecoverKey(IUser user)
+ /// <summary>
+ /// Gets all public keys stored in the user's account object
+ /// </summary>
+ /// <param name="user"></param>
+ /// <returns>The array of public keys if the exist</returns>
+ public static PkiAuthPublicKey[]? PkiGetAllPublicKeys(this IUser user)
{
string? keyData = user[USER_PKI_ENTRY];
@@ -264,25 +285,67 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
{
return null;
}
+
+ //Alloc bin buffer for base64 conversion
+ using UnsafeMemoryHandle<byte> binBuffer = MemoryUtil.UnsafeAllocNearestPage(keyData.Length, true);
- //Get buffer to recover the key data from
- byte[] buffer = ArrayPool<byte>.Shared.Rent(JWK_KEY_BUFFER_SIZE);
- try
+ //Recover base64 bytes from key data
+ ERRNO bytes = VnEncoding.Base64UrlDecode(keyData, binBuffer.Span);
+ if (!bytes)
{
- //Recover base64 bytes from key data
- ERRNO bytes = VnEncoding.TryFromBase64Chars(keyData, buffer);
- if (!bytes)
- {
- return null;
- }
- //Recover json from the decoded binary data
- return new ReadOnlyJsonWebKey(buffer.AsSpan(0, bytes));
+ return null;
}
- finally
+
+ //Deserialize the the key array
+ return JsonSerializer.Deserialize<PkiAuthPublicKey[]>(binBuffer.AsSpan(0, bytes), Statics.SR_OPTIONS);
+ }
+
+ /// <summary>
+ /// Removes a single pki key by it's id
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="keyId">The id of the key to remove</param>
+ public static void PKIRemovePublicKey(this IUser user, string keyId)
+ {
+ //get all keys
+ PkiAuthPublicKey[]? keys = PkiGetAllPublicKeys(user);
+ if(keys == null)
+ {
+ return;
+ }
+
+ //remove the key
+ keys = keys.Where(k => !keyId.Equals(k.KeyId, StringComparison.Ordinal)).ToArray();
+
+ //store the new key array
+ PKISetPublicKeys(user, keys);
+ }
+
+ /// <summary>
+ /// Adds a single pki key to the user's account object, or overwrites
+ /// and existing key with the same id
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="key">The key to add to the list of user-keys</param>
+ public static void PKIAddPublicKey(this IUser user, PkiAuthPublicKey key)
+ {
+ //get all keys
+ PkiAuthPublicKey[]? keys = PkiGetAllPublicKeys(user);
+
+ if (keys == null)
+ {
+ keys = new PkiAuthPublicKey[] { key };
+ }
+ else
{
- MemoryUtil.InitializeBlock(buffer.AsSpan());
- ArrayPool<byte>.Shared.Return(buffer);
+ //remove the key if it already exists, then append the new key
+ keys = keys.Where(k => !key.KeyId.Equals(k.KeyId, StringComparison.Ordinal))
+ .Append(key)
+ .ToArray();
}
+
+ //store the new key array
+ PKISetPublicKeys(user, keys);
}
#endregion
@@ -416,9 +479,9 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
return new(upgradeJwt.Compile(), VnEncoding.ToBase32String(secret));
}
- public static void MfaUpgradeSecret(this in SessionInfo session, string? base32Signature) => session[SESSION_SIG_KEY] = base32Signature!;
+ internal static void MfaUpgradeSecret(this in SessionInfo session, string? base32Signature) => session[SESSION_SIG_KEY] = base32Signature!;
- public static string? MfaUpgradeSecret(this in SessionInfo session) => session[SESSION_SIG_KEY];
+ internal static string? MfaUpgradeSecret(this in SessionInfo session) => session[SESSION_SIG_KEY];
}
readonly record struct MfaUpgradeMessage(string ClientJwt, string SessionKey);