Browse Source

updated from EBUS

Robert 1 year ago
parent
commit
2c1a841393

+ 3 - 0
EVCB_OCPP.WSServer/Message/CoreProfileHandler.cs

@@ -76,6 +76,7 @@ internal partial class ProfileHandler
     //private readonly IDbContextFactory<MeterValueDBContext> metervaluedbContextFactory;
     private readonly IBusinessServiceFactory businessServiceFactory;
     private readonly IMainDbService mainDbService;
+    private readonly CertificateService certService;
     private OuterHttpClient httpClient;
 
     public ProfileHandler(
@@ -89,6 +90,7 @@ internal partial class ProfileHandler
         ILogger<ProfileHandler> logger,
         //BlockingTreePrintService blockingTreePrintService,
         //GoogleGetTimePrintService googleGetTimePrintService,
+        CertificateService certService,
         ServerMessageService messageService,
         OuterHttpClient httpClient)
     {
@@ -102,6 +104,7 @@ internal partial class ProfileHandler
         this.webDbConnectionFactory = webDbConnectionFactory;
         this.meterValueDbService = meterValueDbService;
         this.mainDbService = mainDbService;
+        this.certService = certService;
         //this.metervaluedbContextFactory = metervaluedbContextFactory;
         this.businessServiceFactory = businessServiceFactory;
         this.httpClient = httpClient;

+ 193 - 4
EVCB_OCPP.WSServer/Message/SecurityProfileHandler.cs

@@ -4,12 +4,14 @@ using OCPPServer.Protocol;
 using System;
 using Microsoft.Extensions.Logging;
 using EVCB_OCPP.WSServer.Service.WsService;
+using EVCB_OCPP.Packet.Messages.Security;
+using Microsoft.EntityFrameworkCore;
 
 namespace EVCB_OCPP.WSServer.Message
 {
     internal partial class ProfileHandler
     {
-        internal MessageResult ExecuteSecurityRequest(Actions action, WsClientData session, IRequest request)
+        internal async Task<MessageResult> ExecuteSecurityRequest(Actions action, WsClientData session, IRequest request)
         {
             MessageResult result = new MessageResult() { Success = false };
 
@@ -17,9 +19,68 @@ namespace EVCB_OCPP.WSServer.Message
             {
                 switch (action)
                 {
-                                    var item = await db.MachineOperateRecords.Where(x => x.ChargeBoxId == session.ChargeBoxId && x.Action == "GetLog" && x.RequestType == 1)
+                    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.MachineOperateRecord.Where(x => x.ChargeBoxId == session.ChargeBoxId && x.Action == "GetLog" && x.RequestType == 1)
+                                        .OrderByDescending(x => x.CreatedOn).FirstOrDefaultAsync();
+                                    if (item != null)
+                                    {
+                                        item.EVSE_Status = (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.", "")));
@@ -39,13 +100,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<string,string> csrInfo = certService.SubjectToDictionary(subject);
+            if (csrInfo == null ||
+                !csrInfo.ContainsKey("CN") ||
+                csrInfo["CN"] != chargeBoxId)
+            {
+                return false;
+            }
+            return true;
+        }
+
+        internal async Task<MessageResult> 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.MachineOperateRecord.Where(x => x.SerialNo == requestId &&
+                            x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync();
+                            if (operation != null)
+                            {
+                                operation.FinishedOn = DateTime.UtcNow;
+                                operation.Status = 1;//電樁有回覆
+                                operation.EVSE_Status = (int)_confirm.status;//OK
+                                operation.EVSE_Value = _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.MachineOperateRecord.Where(x => x.SerialNo == requestId &&
+                            x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync();
+                            if (operation != null)
+                            {
+                                operation.FinishedOn = DateTime.UtcNow;
+                                operation.Status = 1;//電樁有回覆
+                                operation.EVSE_Status = (int)_confirm.status;//OK
+                                operation.EVSE_Value = _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.MachineOperateRecord.Where(x => x.SerialNo == requestId &&
+                            x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync();
+                            if (operation != null)
+                            {
+                                operation.FinishedOn = DateTime.UtcNow;
+                                operation.Status = 1;//電樁有回覆
+                                operation.EVSE_Status = (int)_confirm.status;//OK
+                                operation.EVSE_Value = _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.MachineOperateRecord.Where(x => x.SerialNo == requestId &&
+                            x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync();
+                            if (operation != null)
+                            {
+                                operation.FinishedOn = DateTime.UtcNow;
+                                operation.Status = 1;//電樁有回覆
+                                operation.EVSE_Status = (int)_confirm.status;//OK
+                                operation.EVSE_Value = _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.MachineOperateRecord.Where(x => x.SerialNo == requestId &&
+                            x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync();
+                            if (operation != null)
+                            {
+                                operation.FinishedOn = DateTime.UtcNow;
+                                operation.Status = 1;//電樁有回覆
+                                operation.EVSE_Status = (int)_confirm.status;//OK
+                                operation.EVSE_Value = _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.MachineOperateRecord.Where(x => x.SerialNo == requestId &&
+                            x.ChargeBoxId == session.ChargeBoxId && x.Status == 0).FirstOrDefaultAsync();
+                            if (operation != null)
+                            {
+                                operation.FinishedOn = DateTime.UtcNow;
+                                operation.Status = 1;//電樁有回覆
+                                operation.EVSE_Status = (int)_confirm.status;//OK
+                                operation.EVSE_Value = _confirm.status.ToString();
+                                await db.SaveChangesAsync();
+                            }
+                        }
+                    }
+                    break;
                 default:
                     {
                         logger.LogWarning(string.Format("Not Implement {0} Logic", confirm.GetType().ToString().Replace("OCPPPackage.Messages.RemoteTrigger.", "")));

+ 17 - 1
EVCB_OCPP.WSServer/Program.cs

@@ -20,6 +20,9 @@ using EVCB_OCPP.WSServer.Jobs;
 using Microsoft.AspNetCore.Builder;
 using EVCB_OCPP.WSServer.Service.WsService;
 using EVCB_OCPP.Service;
+using Microsoft.AspNetCore.Hosting;
+using System.Security.Cryptography.X509Certificates;
+using System.Net.Security;
 
 namespace EVCB_OCPP.WSServer
 {
@@ -72,7 +75,15 @@ namespace EVCB_OCPP.WSServer
                     //services.AddTransient<BlockingTreePrintService>();
                     //services.AddTransient<GoogleGetTimePrintService>();
                 });
-                //.Build();
+            //.Build();
+            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();
@@ -92,5 +103,10 @@ namespace EVCB_OCPP.WSServer
             }
             return timevalue;
         }
+
+        private static bool ValidateClientCertficatel(X509Certificate2 clientCertificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
+        {
+            return true;
+        }
     }
 }

+ 17 - 2
EVCB_OCPP.WSServer/ProtalServer.cs

@@ -31,6 +31,9 @@ using EVCB_OCPP.WSServer.SuperSocket;
 using Microsoft.Extensions.Logging;
 using EVCB_OCPP.WSServer.Service.WsService;
 using System.Net.WebSockets;
+using EVCB_OCPP.Packet.Messages.Security;
+using Microsoft.AspNetCore.Http.HttpResults;
+using System;
 
 namespace EVCB_OCPP.WSServer
 {
@@ -976,6 +979,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);
 
                                         if (session.CustomerId == new Guid("298918C0-6BB5-421A-88CC-4922F918E85E") || session.CustomerId == new Guid("9E6BFDCC-09FB-4DAB-A428-43FE507600A3"))
@@ -1121,7 +1136,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);
@@ -1196,7 +1211,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:

+ 142 - 0
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<CertificateService> 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<CertificateService> 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<int> CheckClientCertificate(string chargeboxId, X509Certificate2 cert)
+        {
+            return CheckClientCertificate(chargeboxId, cert.ExportCertificatePem());
+        }
+
+        public async Task<int> 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<int> 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<string, string> 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;
+            }
+        }
+    }
+}

+ 15 - 0
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
+            }
+            );
+    }
 }

+ 44 - 4
EVCB_OCPP.WSServer/Service/WsService/OcppWebsocketService.cs

@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using OCPPServer.Protocol;
 using System.Net.WebSockets;
+using System.Security.Cryptography.X509Certificates;
 using System.Text;
 
 namespace EVCB_OCPP.WSServer.Service.WsService;
@@ -14,6 +15,7 @@ public static partial class AppExtention
 {
     public static void AddOcppWsServer(this IServiceCollection services)
     {
+        services.AddTransient<CertificateService>();
         services.AddTransient<WsClientData>();
         services.AddSingleton<OcppWebsocketService>();
     }
@@ -46,6 +48,7 @@ public class OcppWebsocketService : WebsocketService<WsClientData>
 
     private readonly IConfiguration configuration;
     private readonly IMainDbService mainDbService;
+    private readonly CertificateService certificateService;
     private readonly ILogger<OcppWebsocketService> logger;
 
     private readonly QueueSemaphore handshakeSemaphore;
@@ -54,11 +57,13 @@ public class OcppWebsocketService : WebsocketService<WsClientData>
         IConfiguration configuration,
         IServiceProvider serviceProvider,
         IMainDbService mainDbService,
+        CertificateService certificateService,
         ILogger<OcppWebsocketService> logger
         ) : base(serviceProvider)
     {
         this.configuration = configuration;
         this.mainDbService = mainDbService;
+        this.certificateService = certificateService;
         this.logger = logger;
 
         handshakeSemaphore = new QueueSemaphore(5);
@@ -174,11 +179,46 @@ public class OcppWebsocketService : WebsocketService<WsClientData>
             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)

+ 1 - 0
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<string> m_ReceiveData;
 

+ 2 - 1
EVCB_OCPP.WSServer/appsettings.json

@@ -3,6 +3,7 @@
   "WSSPort": "",
   "LocalAuthAPI": "",
   "LogProvider": "NLog",
+  "CertificateServerUrl": "http://localhost:81/",
   "OCPP20_WSUrl": "ws://ocpp.phihong.com.tw:5004",
   "OCPP20_WSSUrl": "ws://ocpp.phihong.com.tw:5004",
   "MaintainMode": 0,
@@ -43,7 +44,7 @@
       "async": true,
       "f": {
         "type": "File",
-        "keepFileOpen":  false,
+        "keepFileOpen": false,
         "fileName": "/home/logs/server/${shortdate}.log",
         "layout": "${longdate} ${uppercase:${level}} ${message}"
       },

+ 660 - 0
cert.patch

@@ -0,0 +1,660 @@
+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<MeterValueDBContext> 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<ProfileHandler> 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<MessageResult> 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<string,string> csrInfo = certService.SubjectToDictionary(subject);
++            if (csrInfo == null ||
++                !csrInfo.ContainsKey("CN") ||
++                csrInfo["CN"] != chargeBoxId)
++            {
++                return false;
++            }
++            return true;
++        }
++
++        internal async Task<MessageResult> 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<BlockingTreePrintService>();
+                     //services.AddTransient<GoogleGetTimePrintService>();
+                 });
++            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<CertificateService> 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<CertificateService> 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<int> CheckClientCertificate(string chargeboxId, X509Certificate2 cert)
++        {
++            return CheckClientCertificate(chargeboxId, cert.ExportCertificatePem());
++        }
++
++        public async Task<int> 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<int> 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<string, string> 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<CertificateService>();
+         services.AddTransient<WsClientData>();
+         services.AddSingleton<OcppWebsocketService>();
+     }
+@@ -45,6 +47,7 @@ public class OcppWebsocketService : WebsocketService<WsClientData>
+ 
+     private readonly IConfiguration configuration;
+     private readonly IMainDbService mainDbService;
++    private readonly CertificateService certificateService;
+     private readonly ILogger<OcppWebsocketService> logger;
+ 
+     private readonly QueueSemaphore handshakeSemaphore;
+@@ -53,11 +56,13 @@ public class OcppWebsocketService : WebsocketService<WsClientData>
+         IConfiguration configuration,
+         IServiceProvider serviceProvider,
+         IMainDbService mainDbService,
++        CertificateService certificateService,
+         ILogger<OcppWebsocketService> 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<WsClientData>
+             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<string> 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,