From 78901f761e5b8358d02d1841bee4c60d97c94760 Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 19 Mar 2023 13:56:27 -0400 Subject: RestSharp version update, PKI optional login endpoint --- .../src/Endpoints/LoginEndpoint.cs | 21 +- .../src/Endpoints/MFAEndpoint.cs | 101 ++-- .../src/Endpoints/PkiLoginEndpoint.cs | 577 +++++++++++++++++++++ 3 files changed, 646 insertions(+), 53 deletions(-) create mode 100644 plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs (limited to 'plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints') diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs index e78d2da..ea6bab1 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs @@ -29,6 +29,8 @@ using System.Threading.Tasks; using System.Security.Cryptography; using System.Text.Json.Serialization; +using FluentValidation; + using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; @@ -42,7 +44,6 @@ using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Users; using static VNLib.Plugins.Essentials.Statics; - namespace VNLib.Plugins.Essentials.Accounts.Endpoints { @@ -142,7 +143,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints } //Make sure the account has not been locked out - if (webm.Assert(!UserLoginLocked(user), LOCKED_ACCOUNT_MESSAGE)) + if (webm.Assert(!UserLoginLocked(user, entity.RequestedTimeUtc), LOCKED_ACCOUNT_MESSAGE)) { goto Cleanup; } @@ -302,7 +303,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints return VfReturnType.VirtualSkip; } - bool locked = UserLoginLocked(user); + bool locked = UserLoginLocked(user, entity.RequestedTimeUtc); //Make sure the account has not been locked out if (!webm.Assert(locked == false, LOCKED_ACCOUNT_MESSAGE)) @@ -383,27 +384,27 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints webm.Success = true; //Write to log Log.Verbose("Successful login for user {uid}...", user.UserID[..8]); - } + } - public bool UserLoginLocked(IUser user) + public bool UserLoginLocked(IUser user, DateTimeOffset now) { //Recover last counter value TimestampedCounter flc = user.FailedLoginCount(); - - if(flc.Count < MaxFailedLogins) + + if (flc.Count < MaxFailedLogins) { //Period exceeded return false; } - + //See if the flc timeout period has expired - if (flc.LastModified.Add(FailedCountTimeout) < DateTimeOffset.UtcNow) + if (flc.LastModified.Add(FailedCountTimeout) < now) { //clear flc flag user.FailedLoginCount(0); return false; } - + //Count has been exceeded, and has not timed out yet return true; } diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs index 0b015a4..998cee4 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs @@ -78,21 +78,27 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints protected override async ValueTask GetAsync(HttpEntity entity) { - List enabledModes = new(2); + string[] enabledModes = new string[3]; //Load the MFA entry for the user using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); //Set the TOTP flag if set - if (!string.IsNullOrWhiteSpace(user?.MFAGetTOTPSecret())) + if (user?.MFATotpEnabled() == true) { - enabledModes.Add("totp"); + enabledModes[0] = "totp"; } //TODO Set fido flag if enabled if (!string.IsNullOrWhiteSpace("")) { - enabledModes.Add("fido"); + enabledModes[1] = "fido"; + } + + //PKI enabled + if (user?.PKIEnabled() == true) + { + enabledModes[2] = "pki"; } //Return mfa modes as an array @@ -176,45 +182,8 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints return VfReturnType.VirtualSkip; } - //generate a new secret (passing the buffer which will get copied to an array because the pw bytes can be modified during encryption) - byte[] secretBuffer = user.MFAGenreateTOTPSecret(MultiFactor); - //Alloc output buffer - UnsafeMemoryHandle outputBuffer = MemoryUtil.UnsafeAlloc(4096, true); - - try - { - //Encrypt the secret for the client - ERRNO count = entity.TryEncryptClientData(secretBuffer, outputBuffer.Span); - - if (!count) - { - webm.Result = "There was an error updating your credentials"; - //If this code is running, the client should have a valid public key stored, but log it anyway - Log.Warn("TOTP secret encryption failed, for requested user {uid}", entity.Session.UserID); - break; - } - - webm.Result = new TOTPUpdateMessage() - { - Issuer = MultiFactor.TOTPConfig.IssuerName, - Digits = MultiFactor.TOTPConfig.TOTPDigits, - Period = (int)MultiFactor.TOTPConfig.TOTPPeriod.TotalSeconds, - Algorithm = MultiFactor.TOTPConfig.TOTPAlg.ToString(), - //Convert the secret to base64 string to send to client - Base64EncSecret = Convert.ToBase64String(outputBuffer.Span[..(int)count]) - }; - - //set success flag - webm.Success = true; - } - finally - { - //dispose the output buffer - outputBuffer.Dispose(); - MemoryUtil.InitializeBlock(secretBuffer.AsSpan()); - } - //Only write changes to the db of operation was successful - await user.ReleaseAsync(); + //Update TOTP secret for user + await UpdateUserTotp(entity, user, webm); } break; default: @@ -313,5 +282,51 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints return VfReturnType.BadRequest; } } + + private async Task UpdateUserTotp(HttpEntity entity, IUser user, WebMessage webm) + { + //generate a new secret (passing the buffer which will get copied to an array because the pw bytes can be modified during encryption) + byte[] secretBuffer = user.MFAGenreateTOTPSecret(MultiFactor); + //Alloc output buffer + UnsafeMemoryHandle outputBuffer = MemoryUtil.UnsafeAlloc(4096, true); + + try + { + //Encrypt the secret for the client + ERRNO count = entity.TryEncryptClientData(secretBuffer, outputBuffer.Span); + + if (!count) + { + webm.Result = "There was an error updating your credentials"; + + //If this code is running, the client should have a valid public key stored, but log it anyway + Log.Warn("TOTP secret encryption failed, for requested user {uid}", entity.Session.UserID); + } + else + { + webm.Result = new TOTPUpdateMessage() + { + Issuer = MultiFactor.TOTPConfig.IssuerName, + Digits = MultiFactor.TOTPConfig.TOTPDigits, + Period = (int)MultiFactor.TOTPConfig.TOTPPeriod.TotalSeconds, + Algorithm = MultiFactor.TOTPConfig.TOTPAlg.ToString(), + //Convert the secret to base64 string to send to client + Base64EncSecret = Convert.ToBase64String(outputBuffer.Span[..(int)count]) + }; + + //set success flag + webm.Success = true; + + //Only write changes to the db of operation was successful + await user.ReleaseAsync(); + } + } + finally + { + //dispose the output buffer + outputBuffer.Dispose(); + MemoryUtil.InitializeBlock(secretBuffer.AsSpan()); + } + } } } \ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs new file mode 100644 index 0000000..06ccd60 --- /dev/null +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs @@ -0,0 +1,577 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Accounts +* File: PkiLoginEndpoint.cs +* +* PkiLoginEndpoint.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; +using System.Net; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Security.Cryptography; +using System.Text.Json.Serialization; + +using FluentValidation; + +using VNLib.Utils; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Hashing.IdentityUtility; +using VNLib.Plugins.Essentials.Users; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.Validation; +using VNLib.Plugins.Essentials.Accounts.MFA; +using VNLib.Plugins.Essentials.Accounts.Validators; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Users; + +namespace VNLib.Plugins.Essentials.Accounts.Endpoints +{ + [ConfigurationName("pki_auth_endpoint")] + internal sealed class PkiLoginEndpoint : UnprotectedWebEndpoint + { + public const string INVALID_MESSAGE = "Your assertion is invalid, please regenerate and try again"; + + private static readonly ImmutableArray AllowedCurves = new string[3] { "P-256", "P-384", "P-521"}.ToImmutableArray(); + private static readonly ImmutableArray AllowedAlgs = new string[3] { "ES256", "ES384", "ES512" }.ToImmutableArray(); + + private static JwtLoginValidator LwValidator { get; } = new(); + private static IValidator AuthValidator { get; } = AuthenticationInfo.GetValidator(); + private static IValidator UserJwkValidator { get; } = GetKeyValidator(); + + private readonly JwtEndpointConfig _config; + private readonly IUserManager _users; + + + /* + * Default protections sessions should be fine (most strict) + * No cross-site/cross origin/bad referrer etc + */ + //protected override ProtectionSettings EndpointProtectionSettings { get; } = new(); + + + public PkiLoginEndpoint(PluginBase plugin, IConfigScope config) + { + string? path = config["path"].GetString(); + InitPathAndLog(path, plugin.Log); + + //Load config + _config = config.DeserialzeAndValidate(); + _users = plugin.GetOrCreateSingleton(); + + Log.Verbose("PKI endpoint enabled"); + } + + protected override ERRNO PreProccess(HttpEntity entity) + { + return base.PreProccess(entity) && !entity.Session.IsNew; + } + + protected override async ValueTask PostAsync(HttpEntity entity) + { + //Conflict if user is logged in + if (entity.IsClientAuthorized(AuthorzationCheckLevel.Any)) + { + entity.CloseResponse(HttpStatusCode.Conflict); + return VfReturnType.VirtualSkip; + } + + ValErrWebMessage webm = new(); + + //Get the login message from the client + JwtLoginMessage? login = await entity.GetJsonFromFileAsync(); + + if(webm.Assert(login != null, INVALID_MESSAGE)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Validate login message + if(!LwValidator.Validate(login, webm)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + IUser? user = null; + JsonWebToken jwt; + try + { + //We can try to recover the jwt data + jwt = JsonWebToken.Parse(login.LoginJwt); + } + catch (KeyNotFoundException) + { + webm.Result = INVALID_MESSAGE; + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + catch (FormatException) + { + webm.Result = INVALID_MESSAGE; + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + try + { + AuthenticationInfo authInfo; + + //Get the signed payload message + using (JsonDocument payload = jwt.GetPayload()) + { + long unixSec = payload.RootElement.GetProperty("iat").GetInt64(); + + DateTimeOffset clientIat = DateTimeOffset.FromUnixTimeSeconds(unixSec); + + if (clientIat.Add(_config.MaxJwtTimeDifference) < entity.RequestedTimeUtc) + { + webm.Result = INVALID_MESSAGE; + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + if (clientIat.Subtract(_config.MaxJwtTimeDifference) > entity.RequestedTimeUtc) + { + webm.Result = INVALID_MESSAGE; + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Recover the authenticator information + authInfo = new() + { + EmailAddress = payload.RootElement.GetPropString("sub"), + KeyId = payload.RootElement.GetPropString("keyid"), + SerialNumber = payload.RootElement.GetPropString("serial"), + }; + } + + //Validate auth info + if (!AuthValidator.Validate(authInfo, webm)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Get the user from the email address + user = await _users.GetUserFromEmailAsync(authInfo.EmailAddress!, entity.EventCancellation); + + if (webm.Assert(user != null, INVALID_MESSAGE)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Check failed login count + if(webm.Assert(UserLoginLocked(user, entity.RequestedTimeUtc) == false, INVALID_MESSAGE)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Now we can verify the signed message against the stored key + if (webm.Assert(user.PKIVerifyUserJWT(jwt, authInfo.KeyId) == true, INVALID_MESSAGE)) + { + //increment flc on invalid signature + user.FailedLoginIncrement(); + await user.ReleaseAsync(); + + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Account status must be active + if(webm.Assert(user.Status == UserStatus.Active, INVALID_MESSAGE)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Must be local account + if (webm.Assert(user.IsLocalAccount(), INVALID_MESSAGE)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //User is has been authenticated + + //Authorize the user + entity.GenerateAuthorization(login, user, webm); + + //Write user data + await user.ReleaseAsync(); + + webm.Success = true; + + //Return user data + webm.Result = new AccountData() + { + EmailAddress = user.EmailAddress, + }; + + //Close response, user is now logged-in + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + catch + { + entity.InvalidateLogin(); + throw; + } + finally + { + user?.Dispose(); + jwt.Dispose(); + } + } + + /* + * This endpoint also enables + */ + protected override async ValueTask PatchAsync(HttpEntity entity) + { + //Check for config flag + if (!_config.EnableKeyUpdate) + { + return VfReturnType.Forbidden; + } + //This endpoint requires valid authorization + if (!entity.IsClientAuthorized(AuthorzationCheckLevel.Critical)) + { + entity.CloseResponse(HttpStatusCode.Unauthorized); + return VfReturnType.VirtualSkip; + } + + ValErrWebMessage webm = new(); + + //Get the request body + using JsonDocument? request = await entity.GetJsonFromFileAsync(); + + if(webm.Assert(request != null, "The request message is not valid")) + { + entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); + return VfReturnType.VirtualSkip; + } + + //Get the jwk from the request body + using ReadOnlyJsonWebKey jwk = new(request.RootElement); + + //Validate the user's jwk + if(!UserJwkValidator.Validate(jwk, webm)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Get the user account + using IUser? user = await _users.GetUserFromIDAsync(entity.Session.UserID, entity.EventCancellation); + + //Confirm not null, this should only happen if user is removed from table while still logged in + if(webm.Assert(user != null, "You may not configure PKI authentication")) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Local account is required + if (webm.Assert(user.IsLocalAccount(), "You do not have a local account, you may not configure PKI authentication")) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + try + { + //Try to get the ECDA instance to confirm the key data could be recovered properly + using ECDsa? testAlg = jwk.GetECDsaPublicKey(); + + if (webm.Assert(testAlg != null, "Your JWK is not valid")) + { + webm.Result = "Your JWK is not valid"; + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + } + catch(Exception ex) + { + Log.Debug(ex); + webm.Result = "Your JWK is not valid"; + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Extract the user's EC key minimum parameters + IReadOnlyDictionary keyParams = ExtractKeyData(jwk); + + //Update user's key params + user.PKISetUserKey(keyParams); + + //publish changes + await user.ReleaseAsync(); + + webm.Result = "Successfully updated your PKI authentication method"; + webm.Success = true; + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask DeleteAsync(HttpEntity entity) + { + //Check for config flag + if (!_config.EnableKeyUpdate) + { + return VfReturnType.Forbidden; + } + + //This endpoint requires valid authorization + if (!entity.IsClientAuthorized(AuthorzationCheckLevel.Critical)) + { + entity.CloseResponse(HttpStatusCode.Unauthorized); + return VfReturnType.VirtualSkip; + } + + ValErrWebMessage webm = new(); + + //Get the user account + using IUser? user = await _users.GetUserFromIDAsync(entity.Session.UserID, entity.EventCancellation); + + //Confirm not null, this should only happen if user is removed from table while still logged in + if (webm.Assert(user != null, "You may not configure PKI authentication")) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Local account is required + if (webm.Assert(user.IsLocalAccount(), "You do not have a local account, you may not configure PKI authentication")) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Remove the key + user.PKISetUserKey(null); + await user.ReleaseAsync(); + + webm.Result = "You have successfully disabled PKI login"; + webm.Success = true; + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + public bool UserLoginLocked(IUser user, DateTimeOffset now) + { + //Recover last counter value + TimestampedCounter flc = user.FailedLoginCount(); + + if (flc.Count < _config.MaxFailedLogins) + { + //Period exceeded + return false; + } + + //See if the flc timeout period has expired + if (flc.LastModified.AddSeconds(_config.FailedCountTimeoutSec) < now) + { + //clear flc flag + user.FailedLoginCount(0); + return false; + } + + //Count has been exceeded, and has not timed out yet + return true; + } + + private static IReadOnlyDictionary ExtractKeyData(ReadOnlyJsonWebKey key) + { + Dictionary keyData = new(); + + keyData["kty"] = key.KeyType!; + keyData["use"] = "sig"; + keyData["crv"] = key.GetKeyProperty("crv")!; + keyData["kid"] = key.KeyId!; + keyData["alg"] = key.Algorithm!; + keyData["x"] = key.GetKeyProperty("x")!; + keyData["y"] = key.GetKeyProperty("y")!; + + return keyData; + } + + private sealed class JwtLoginValidator : ClientSecurityMessageValidator + { + public JwtLoginValidator() :base() + { + //Basic jwt validator + RuleFor(l => l.LoginJwt) + .NotEmpty() + .MinimumLength(50) + .IllegalCharacters(); + } + } + + sealed class JwtLoginMessage : IClientSecInfo + { + [JsonPropertyName("pubkey")] + public string? PublicKey { get; set; } + [JsonPropertyName("clientid")] + public string? ClientId { get; set; } + [JsonPropertyName("login")] + public string? LoginJwt { get; set; } + } + + sealed class JwtEndpointConfig : IOnConfigValidation + { + [JsonIgnore] + public TimeSpan MaxJwtTimeDifference { get; set; } = TimeSpan.FromSeconds(30); + + [JsonPropertyName("jwt_time_dif_sec")] + public uint TimeDiffSeconds + { + get => (uint)MaxJwtTimeDifference.TotalSeconds; + set => TimeSpan.FromSeconds(value); + } + + [JsonPropertyName("enable_key_update")] + public bool EnableKeyUpdate { get; set; } = true; + + [JsonPropertyName("max_login_attempts")] + public int MaxFailedLogins { get; set; } = 10; + + [JsonPropertyName("failed_attempt_timeout_sec")] + public double FailedCountTimeoutSec { get; set; } = 300; + + public void Validate() + { + Validator.ValidateAndThrow(this); + } + + private static IValidator Validator { get; } = GetValidator(); + private static IValidator GetValidator() + { + InlineValidator val = new(); + + val.RuleFor(c => c.TimeDiffSeconds) + .GreaterThan((uint)1) + .WithMessage("You must specify a JWT IAT time difference greater than 0 seconds"); + + val.RuleFor(c => c.MaxFailedLogins) + .GreaterThan(0); + + val.RuleFor(c => c.FailedCountTimeoutSec) + .GreaterThan(0); + + + return val; + } + + } + + readonly record struct AuthenticationInfo + { + public readonly string? EmailAddress { get; init; } + + public readonly string? KeyId { get; init; } + + public readonly string? SerialNumber { get; init; } + + public static IValidator GetValidator() + { + InlineValidator val = new(); + + val.RuleFor(l => l.EmailAddress) + .NotEmpty() + .Length(5, 100) + .EmailAddress(); + + val.RuleFor(l => l.SerialNumber) + .NotEmpty() + .Length(5, 50) + .AlphaNumericOnly(); + + val.RuleFor(l => l.KeyId) + .NotEmpty() + .Length(2, 50) + .AlphaNumericOnly(); + + return val; + } + } + + private static IValidator GetKeyValidator() + { + InlineValidator val = new(); + + val.RuleFor(a => a.KeyType) + .NotEmpty() + .Must(kt => "EC".Equals(kt, StringComparison.Ordinal)) + .WithMessage("The supplied key is not an EC curve key"); + + val.RuleFor(a => a.Use) + .NotEmpty() + .Must(u => "sig".Equals(u, StringComparison.OrdinalIgnoreCase)) + .WithMessage("Your key must be configured for signatures"); + + val.RuleFor(a => a.GetKeyProperty("crv")) + .NotEmpty() + .WithName("crv") + .Must(p => AllowedCurves.Contains(p, StringComparer.Ordinal)) + .WithMessage("Your key's curve is not supported"); + + val.RuleFor(c => c.KeyId) + .NotEmpty() + .Length(10, 100) + .IllegalCharacters(); + + val.RuleFor(a => a.Algorithm) + .NotEmpty() + .Must(a => AllowedAlgs.Contains(a, StringComparer.Ordinal)) + .WithMessage("Your key's signature algorithm is not supported"); + + //Confirm the x axis parameter is valid + val.RuleFor(a => a.GetKeyProperty("x")) + .NotEmpty() + .WithName("x") + .Length(10, 200) + .WithMessage("Your key's X EC point public key parameter is not valid") + .IllegalCharacters() + .WithMessage("Your key's X EC point public key parameter conatins invaid characters"); + + + //Confirm the y axis point is valid + val.RuleFor(a => a.GetKeyProperty("y")) + .NotEmpty() + .WithName("y") + .Length(10, 200) + .WithMessage("Your key's Y EC point public key parameter is not valid") + .IllegalCharacters() + .WithMessage("Your key's Y EC point public key parameter conatins invaid characters"); + + return val; + } + } +} \ No newline at end of file -- cgit