diff --git a/EVCB_OCPP.WSServer/DLL/EVCB_OCPP.Packet.dll b/EVCB_OCPP.WSServer/DLL/EVCB_OCPP.Packet.dll index 42f3575..e8dce54 100644 Binary files a/EVCB_OCPP.WSServer/DLL/EVCB_OCPP.Packet.dll and b/EVCB_OCPP.WSServer/DLL/EVCB_OCPP.Packet.dll differ diff --git a/EVCB_OCPP.WSServer/Message/CoreProfileHandler.cs b/EVCB_OCPP.WSServer/Message/CoreProfileHandler.cs index 1698606..0cb0fea 100644 --- a/EVCB_OCPP.WSServer/Message/CoreProfileHandler.cs +++ b/EVCB_OCPP.WSServer/Message/CoreProfileHandler.cs @@ -72,6 +72,7 @@ public partial class ProfileHandler //private readonly IDbContextFactory metervaluedbContextFactory; private readonly IBusinessServiceFactory businessServiceFactory; private readonly IMainDbService mainDbService; + private readonly CertificateService certService; private OuterHttpClient httpClient; public ProfileHandler( @@ -82,6 +83,7 @@ public partial class ProfileHandler MeterValueDbService meterValueDbService, IBusinessServiceFactory businessServiceFactory, IMainDbService mainDbService, + CertificateService certService, ILogger logger, OuterHttpClient httpClient) { @@ -93,6 +95,7 @@ public partial class ProfileHandler this.webDbConnectionFactory = webDbConnectionFactory; this.meterValueDbService = meterValueDbService; this.mainDbService = mainDbService; + this.certService = certService; //this.metervaluedbContextFactory = metervaluedbContextFactory; this.businessServiceFactory = businessServiceFactory; this.httpClient = httpClient; diff --git a/EVCB_OCPP.WSServer/Message/SecurityProfileHandler.cs b/EVCB_OCPP.WSServer/Message/SecurityProfileHandler.cs index 96b07ba..cd277eb 100644 --- a/EVCB_OCPP.WSServer/Message/SecurityProfileHandler.cs +++ b/EVCB_OCPP.WSServer/Message/SecurityProfileHandler.cs @@ -3,12 +3,20 @@ using EVCB_OCPP.Packet.Messages; using System; using Microsoft.Extensions.Logging; using EVCB_OCPP.WSServer.Service.WsService; +using EVCB_OCPP.Packet.Messages.RemoteTrigger; +using Microsoft.EntityFrameworkCore; +using EVCB_OCPP.Packet.Messages.Security; +using Microsoft.IdentityModel.Tokens; +using System.Runtime.ConstrainedExecution; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using System.Text.RegularExpressions; namespace EVCB_OCPP.WSServer.Message { public partial class ProfileHandler { - internal MessageResult ExecuteSecurityRequest(Actions action, WsClientData session, IRequest request) + internal async Task ExecuteSecurityRequest(Actions action, WsClientData session, IRequest request) { MessageResult result = new MessageResult() { Success = false }; @@ -16,8 +24,68 @@ namespace EVCB_OCPP.WSServer.Message { switch (action) { + case Actions.SignCertificate: + { + SignCertificateRequest _request = request as SignCertificateRequest; + SignCertificateConfirmation confirm = new(); + + if (string.IsNullOrEmpty(_request.csr)) + { + result.Success = false; + return result; + } + bool isCsrValid = CheckCsr(session.ChargeBoxId, _request.csr); + if (!isCsrValid) + { + confirm.status = Packet.Messages.SubTypes.GenericStatusEnumType.Rejected; + result.Message = confirm; + result.Success = false; + return result; + } + _ = certService.SignCertificate(session.ChargeBoxId, _request.csr); + confirm.status = Packet.Messages.SubTypes.GenericStatusEnumType.Accepted; + result.Message = confirm; + result.Success = true; + return result; + } + case Actions.SecurityEventNotification: + { + SecurityEventNotificationRequest _request = request as SecurityEventNotificationRequest; + SecurityEventNotificationConfirmation confirm = new(); + + logger.LogInformation("{chargeBoxId} security notification {sects} {sectype} {secmsg}", session.ChargeBoxId, _request.timestamp, _request.type, _request.techInfo); + + result.Message = confirm; + result.Success = true; + return result; + } + case Actions.LogStatusNotification: + { + LogStatusNotificationRequest _request = request as LogStatusNotificationRequest; + LogStatusNotificationConfirmation confirm = new(); + + if (_request.status != Packet.Messages.SubTypes.UploadLogStatusEnumType.Idle) + { + using (var db = await maindbContextFactory.CreateDbContextAsync()) + { + var item = await db.MachineOperateRecords.Where(x => x.ChargeBoxId == session.ChargeBoxId && x.Action == "GetLog" && x.RequestType == 1) + .OrderByDescending(x => x.CreatedOn).FirstOrDefaultAsync(); + if (item != null) + { + item.EvseStatus = (int)_request.status; + item.FinishedOn = DateTime.UtcNow; + } + + await db.SaveChangesAsync(); + } + + } + result.Message = confirm; + result.Success = true; + return result; + } default: { logger.LogWarning(string.Format("Not Implement {0} Logic(ExecuteCoreRequest)", request.GetType().ToString().Replace("OCPPPackage.Messages.Core.", ""))); @@ -37,13 +105,141 @@ namespace EVCB_OCPP.WSServer.Message return result; } - internal MessageResult ExecuteSecurityConfirm(Actions action, WsClientData session, IConfirmation confirm, string requestId) + + private bool CheckCsr(string chargeBoxId, string csrString) + { + string subject = certService.GetCertificateRequestSubject(csrString); + logger.LogInformation("{chargeBoxId} send scr {subject}", chargeBoxId, subject); + if (subject == null) + { + return false; + } + + Dictionary csrInfo = certService.SubjectToDictionary(subject); + if (csrInfo == null || + !csrInfo.ContainsKey("CN") || + csrInfo["CN"] != chargeBoxId) + { + return false; + } + return true; + } + + internal async Task ExecuteSecurityConfirm(Actions action, WsClientData session, IConfirmation confirm, string requestId) { MessageResult result = new MessageResult() { Success = false }; switch (action) { - + case Actions.ExtendedTriggerMessage: + { + ExtendedTriggerMessageConfirmation _confirm = confirm as ExtendedTriggerMessageConfirmation; + //ExtendedTriggerMessageRequest _request = _confirm.GetRequest() as ExtendedTriggerMessageRequest; + using (var db = await maindbContextFactory.CreateDbContextAsync()) + { + var operation = await db.MachineOperateRecords.Where(x => x.SerialNo == requestId && + x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync(); + if (operation != null) + { + operation.FinishedOn = DateTime.UtcNow; + operation.Status = 1;//電樁有回覆 + operation.EvseStatus = (int)_confirm.status;//OK + operation.EvseValue = _confirm.status.ToString(); + await db.SaveChangesAsync(); + } + } + } + break; + case Actions.CertificateSigned: + { + CertificateSignedConfirmation _confirm = confirm as CertificateSignedConfirmation; + using (var db = await maindbContextFactory.CreateDbContextAsync()) + { + var operation = await db.MachineOperateRecords.Where(x => x.SerialNo == requestId && + x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync(); + if (operation != null) + { + operation.FinishedOn = DateTime.UtcNow; + operation.Status = 1;//電樁有回覆 + operation.EvseStatus = (int)_confirm.status;//OK + operation.EvseValue = _confirm.status.ToString(); + await db.SaveChangesAsync(); + } + } + } + break; + case Actions.GetInstalledCertificateIds: + { + GetInstalledCertificateIdsConfirmation _confirm = confirm as GetInstalledCertificateIdsConfirmation; + using (var db = await maindbContextFactory.CreateDbContextAsync()) + { + var operation = await db.MachineOperateRecords.Where(x => x.SerialNo == requestId && + x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync(); + if (operation != null) + { + operation.FinishedOn = DateTime.UtcNow; + operation.Status = 1;//電樁有回覆 + operation.EvseStatus = (int)_confirm.status;//OK + operation.EvseValue = _confirm.status.ToString(); + await db.SaveChangesAsync(); + } + } + } + break; + case Actions.DeleteCertificate: + { + DeleteCertificateConfirmation _confirm = confirm as DeleteCertificateConfirmation; + using (var db = await maindbContextFactory.CreateDbContextAsync()) + { + var operation = await db.MachineOperateRecords.Where(x => x.SerialNo == requestId && + x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync(); + if (operation != null) + { + operation.FinishedOn = DateTime.UtcNow; + operation.Status = 1;//電樁有回覆 + operation.EvseStatus = (int)_confirm.status;//OK + operation.EvseValue = _confirm.status.ToString(); + await db.SaveChangesAsync(); + } + } + } + break; + case Actions.InstallCertificate: + { + InstallCertificateConfirmation _confirm = confirm as InstallCertificateConfirmation; + using (var db = await maindbContextFactory.CreateDbContextAsync()) + { + var operation = await db.MachineOperateRecords.Where(x => x.SerialNo == requestId && + x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync(); + if (operation != null) + { + operation.FinishedOn = DateTime.UtcNow; + operation.Status = 1;//電樁有回覆 + operation.EvseStatus = (int)_confirm.status;//OK + operation.EvseValue = _confirm.status.ToString(); + await db.SaveChangesAsync(); + } + } + } + break; + case Actions.GetLog: + { + GetLogConfirmation _confirm = confirm as GetLogConfirmation; + using (var db = await maindbContextFactory.CreateDbContextAsync()) + { + var operation = await db.MachineOperateRecords.Where(x => x.SerialNo == requestId && + x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync(); + if (operation != null) + { + operation.FinishedOn = DateTime.UtcNow; + operation.Status = 1;//電樁有回覆 + operation.EvseStatus = (int)_confirm.status;//OK + operation.EvseValue = _confirm.status.ToString(); + await db.SaveChangesAsync(); + } + } + } + break; default: { logger.LogWarning(string.Format("Not Implement {0} Logic", confirm.GetType().ToString().Replace("OCPPPackage.Messages.RemoteTrigger.", ""))); diff --git a/EVCB_OCPP.WSServer/Program.cs b/EVCB_OCPP.WSServer/Program.cs index a2b2b57..493bd08 100644 --- a/EVCB_OCPP.WSServer/Program.cs +++ b/EVCB_OCPP.WSServer/Program.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Hosting; using EVCB_OCPP.WSServer.Service.WsService; using EVCB_OCPP.Service; using Microsoft.Extensions.Logging; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; namespace EVCB_OCPP.WSServer { @@ -51,11 +53,19 @@ namespace EVCB_OCPP.WSServer //services.AddTransient(); //services.AddTransient(); }); + builder.WebHost.ConfigureKestrel(opt => { + opt.ConfigureHttpsDefaults(opt => + { + opt.ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.AllowCertificate; + opt.ClientCertificateValidation = ValidateClientCertficatel; + }); + }); var app = builder.Build(); app.UseHeaderRecordService(); app.UseOcppWsService(); app.MapApiServce(); app.Urls.Add("http://*:80"); + app.Urls.Add("https://*:443"); app.Run(); } @@ -69,5 +79,10 @@ namespace EVCB_OCPP.WSServer } return timevalue; } + + private static bool ValidateClientCertficatel(X509Certificate2 clientCertificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + return true; + } } } diff --git a/EVCB_OCPP.WSServer/ProtalServer.cs b/EVCB_OCPP.WSServer/ProtalServer.cs index 4a5cebc..a16a7f8 100644 --- a/EVCB_OCPP.WSServer/ProtalServer.cs +++ b/EVCB_OCPP.WSServer/ProtalServer.cs @@ -21,6 +21,8 @@ using System.Net; using System.Text; using EVCB_OCPP.WSServer.Service.WsService; using System.ServiceModel.Channels; +using EVCB_OCPP.Packet.Messages.RemoteTrigger; +using EVCB_OCPP.Packet.Messages.Security; namespace EVCB_OCPP.WSServer { @@ -697,6 +699,18 @@ namespace EVCB_OCPP.WSServer { session.IsCheckIn = true; + if (session.IsSignedByCPO == false) + { + var _request = new ExtendedTriggerMessageRequest() + { + requestedMessage = Packet.Messages.SubTypes.MessageTriggerEnumType.SignChargePointCertificate + }; + + var uuid = session.queue.store(_request); + string rawRequest = BasicMessageHandler.GenerateRequest(uuid, _request.Action, _request); + SendMsg(session, rawRequest, string.Format("{0} {1}", _request.Action, "Request"), ""); + } + await messageService.SendGetEVSEConfigureRequest(session.ChargeBoxId); await messageService.SendChangeConfigurationRequest( @@ -831,7 +845,7 @@ namespace EVCB_OCPP.WSServer break; case "Security": { - var replyResult = profileHandler.ExecuteSecurityRequest(action, session, (IRequest)analysisResult.Message); + var replyResult = await profileHandler.ExecuteSecurityRequest(action, session, (IRequest)analysisResult.Message); if (replyResult.Success) { string response = BasicMessageHandler.GenerateConfirmation(analysisResult.UUID, (IConfirmation)replyResult.Message); @@ -905,7 +919,7 @@ namespace EVCB_OCPP.WSServer break; case "Security": { - confirmResult = profileHandler.ExecuteSecurityConfirm(action, session, (IConfirmation)analysisResult.Message, analysisResult.RequestId); + confirmResult = await profileHandler.ExecuteSecurityConfirm(action, session, (IConfirmation)analysisResult.Message, analysisResult.RequestId); } break; default: diff --git a/EVCB_OCPP.WSServer/Service/CertificateService.cs b/EVCB_OCPP.WSServer/Service/CertificateService.cs new file mode 100644 index 0000000..2f19f25 --- /dev/null +++ b/EVCB_OCPP.WSServer/Service/CertificateService.cs @@ -0,0 +1,142 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Json; +using System.Runtime; +using System.Runtime.ConstrainedExecution; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace EVCB_OCPP.WSServer.Service +{ + public class CertificateService + { + + public CertificateService( + ServerMessageService messageService, + IConfiguration configuration, + ILogger logger) + { + _serverUrl = configuration["CertificateServerUrl"]; + _cpon = configuration["ChargingPointOperator"]; + this.messageService = messageService; + this.logger = logger; + } + private readonly string _serverUrl; + private readonly string _cpon; + private readonly ServerMessageService messageService; + private readonly ILogger logger; + + public async Task SignCertificate(string chargeBoxId, string csr) + { + HttpClient client = new HttpClient(); + client.BaseAddress = new Uri(_serverUrl); + var signResult = await client.PostAsJsonAsync("cert/csr", new { csr = csr }); + if (!signResult.IsSuccessStatusCode) + { + logger.LogWarning("{chargeBoxId} csr failed {csr}", chargeBoxId, csr); + return; + } + var crt = await signResult.Content.ReadAsStringAsync(); + await messageService.SendCertificateSignedRequest(chargeBoxId, crt); + client.Dispose(); + } + + public Task CheckClientCertificate(string chargeboxId, X509Certificate2 cert) + { + return CheckClientCertificate(chargeboxId, cert.ExportCertificatePem()); + } + + public async Task CheckClientCertificate(string chargeboxId, string certString) + { + var subject = GetCertificateSubject(certString); + logger.LogInformation("{chargeboxId} with cert subject {subject}", chargeboxId, subject); + if (string.IsNullOrEmpty(subject)) + { + return -1; + } + + var crtInfo = SubjectToDictionary(subject); + if (crtInfo == null || + //!crtInfo.ContainsKey("O") || + //crtInfo["O"] != _cpon || + !crtInfo.ContainsKey("CN") || + crtInfo["CN"] != chargeboxId) + { + return -1; + } + + int certServerResult = await CheckClientCertificateRca(certString); + logger.LogInformation("{chargeboxId} with cert authenticate {subject}", chargeboxId, certServerResult); + return certServerResult; + } + + public async Task CheckClientCertificateRca(string certString) + { + using HttpClient client = new HttpClient(); + client.BaseAddress = new Uri(_serverUrl); + var result = await client.PutAsJsonAsync("cert/crt", new { crt = certString }); + if (!result.IsSuccessStatusCode) + { + return -1; + } + var resultContentString = await result.Content.ReadAsStringAsync(); + return Convert.ToInt32(resultContentString); + } + + public Dictionary SubjectToDictionary(string subject) + { + try + { + MatchCollection regexResult = Regex.Matches(subject, "([a-zA-Z]*)=([a-zA-Z0-9]*)"); + if (regexResult.Count == 0) + { + return null; + } + return regexResult.Where(x => x.Success == true && x.Groups.Count == 3).ToDictionary(x => x.Groups[1].Value, x => x.Groups[2].Value); + } + catch (Exception e) + { + logger.LogCritical(e.Message); + logger.LogCritical(e.StackTrace); + return null; + } + } + + public string GetCertificateSubject(string crt) + { + + try + { + byte[] bytes = Encoding.ASCII.GetBytes(crt); + var clientCertificate = new X509Certificate2(bytes); + + return clientCertificate.Subject; + } + catch + { + return null; + } + } + + public string GetCertificateRequestSubject(string csrString) + { + + try + { + var csr = CertificateRequest.LoadSigningRequestPem(csrString, HashAlgorithmName.SHA512); + var test = csr.SubjectName.Name; + return test; + } + catch + { + return null; + } + } + } +} diff --git a/EVCB_OCPP.WSServer/Service/ServerMessageService.cs b/EVCB_OCPP.WSServer/Service/ServerMessageService.cs index 9a3b0e2..6379c21 100644 --- a/EVCB_OCPP.WSServer/Service/ServerMessageService.cs +++ b/EVCB_OCPP.WSServer/Service/ServerMessageService.cs @@ -1,6 +1,9 @@ using EVCB_OCPP.Packet.Features; using EVCB_OCPP.Packet.Messages.Core; using Microsoft.Extensions.Logging; +using static System.Runtime.InteropServices.JavaScript.JSType; +using System.ServiceModel.Channels; +using EVCB_OCPP.Packet.Messages.Security; namespace EVCB_OCPP.WSServer.Service; @@ -52,4 +55,16 @@ public class ServerMessageService } ); } + + internal Task SendCertificateSignedRequest(string chargeBoxId, string crt) + { + return mainDbService.AddServerMessage( + ChargeBoxId: chargeBoxId, + OutAction: Actions.SignCertificate.ToString(), + OutRequest: new CertificateSignedRequest() + { + certificateChain = crt + } + ); + } } diff --git a/EVCB_OCPP.WSServer/Service/WsService/OcppWebsocketService.cs b/EVCB_OCPP.WSServer/Service/WsService/OcppWebsocketService.cs index ba1df1c..8cd38ae 100644 --- a/EVCB_OCPP.WSServer/Service/WsService/OcppWebsocketService.cs +++ b/EVCB_OCPP.WSServer/Service/WsService/OcppWebsocketService.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; using System.Text; namespace EVCB_OCPP.WSServer.Service.WsService; @@ -13,6 +14,7 @@ public static partial class AppExtention { public static void AddOcppWsServer(this IServiceCollection services) { + services.AddTransient(); services.AddTransient(); services.AddSingleton(); } @@ -45,6 +47,7 @@ public class OcppWebsocketService : WebsocketService private readonly IConfiguration configuration; private readonly IMainDbService mainDbService; + private readonly CertificateService certificateService; private readonly ILogger logger; private readonly QueueSemaphore handshakeSemaphore; @@ -53,11 +56,13 @@ public class OcppWebsocketService : WebsocketService IConfiguration configuration, IServiceProvider serviceProvider, IMainDbService mainDbService, + CertificateService certificateService, ILogger logger ) : base(serviceProvider) { this.configuration = configuration; this.mainDbService = mainDbService; + this.certificateService = certificateService; this.logger = logger; handshakeSemaphore = new QueueSemaphore(5); @@ -182,11 +187,46 @@ public class OcppWebsocketService : WebsocketService securityProfile = 0; } - if (securityProfile == 3 && session.UriScheme == "ws") + if (securityProfile == 3 ) { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - logger.LogTrace("{id} {func} {Statuscode}", context.TraceIdentifier, nameof(ValidateHandshake), context.Response.StatusCode); - return false; + if (session.UriScheme == "ws") + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + logger.LogTrace("{id} {func} {Statuscode}", context.TraceIdentifier, nameof(ValidateHandshake), context.Response.StatusCode); + return false; + } + + X509Certificate2 clientCertificate = null; + if (context.Request.Headers.ContainsKey("X-ARR-ClientCert")) + { + byte[] bytes = Encoding.ASCII.GetBytes(context.Request.Headers["X-ARR-ClientCert"]); + clientCertificate = new X509Certificate2(bytes); + } + if (context.Connection.ClientCertificate is not null) + { + clientCertificate = context.Connection.ClientCertificate; + } + if (clientCertificate is null) + { + clientCertificate = await context.Connection.GetClientCertificateAsync(); + } + if (clientCertificate == null) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + logger.LogTrace("{id} {func} {Statuscode}", context.TraceIdentifier, nameof(ValidateHandshake), context.Response.StatusCode); + return false; + } + + int checkResult = await certificateService.CheckClientCertificate(session.ChargeBoxId, clientCertificate); + if (checkResult == -1) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + logger.LogTrace("{id} {func} {Statuscode}", context.TraceIdentifier, nameof(ValidateHandshake), context.Response.StatusCode); + return false; + } + + session.IsSignedByCPO = checkResult == 0; + return true; } if (securityProfile == 1 || securityProfile == 2) diff --git a/EVCB_OCPP.WSServer/Service/WsService/WsClientData.cs b/EVCB_OCPP.WSServer/Service/WsService/WsClientData.cs index 30b3365..486eaa3 100644 --- a/EVCB_OCPP.WSServer/Service/WsService/WsClientData.cs +++ b/EVCB_OCPP.WSServer/Service/WsService/WsClientData.cs @@ -74,6 +74,7 @@ public class WsClientData : WsSession public string CustomerName { get; set; } public string StationId { set; get; } + public bool? IsSignedByCPO { set; get; } = null; public event EventHandler m_ReceiveData; diff --git a/EVCB_OCPP.WSServer/appsettings.json b/EVCB_OCPP.WSServer/appsettings.json index b52e084..8e7b622 100644 --- a/EVCB_OCPP.WSServer/appsettings.json +++ b/EVCB_OCPP.WSServer/appsettings.json @@ -4,6 +4,7 @@ "LocalAuthAPI": "", "apipass": "12345678", "LogProvider": "NLog", + "CertificateServerUrl": "http://localhost:81/", "OCPP20_WSUrl": "ws://ocpp.phihong.com.tw:5004", "OCPP20_WSSUrl": "ws://ocpp.phihong.com.tw:5004", "MaintainMode": 0,