Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Certs #17

Merged
merged 10 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
charset = utf-8

[*.cs]
trim_trailing_whitespace = true
max_line_length = 175

dotnet_style_namespace_match_folder = true:error
dotnet_diagnostic.IDE0130.severity = error

csharp_style_namespace_declarations = file_scoped:error
dotnet_diagnostic.IDE0161.severity = error

dotnet_diagnostic.IDE0005.severity = error

dotnet_diagnostic.CA2007.severity = error
dotnet_diagnostic.CA2016.severity = error

dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
dotnet_naming_rule.private_members_with_underscore.severity = warning
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _

dotnet_naming_rule.const_members_all_caps.symbols = const_fields
dotnet_naming_rule.const_members_all_caps.style = all_caps
dotnet_naming_rule.const_members_all_caps.severity = warning
dotnet_naming_symbols.const_fields.applicable_kinds = field
dotnet_naming_symbols.const_fields.applicable_accessibilities = *
dotnet_naming_symbols.const_fields.required_modifiers = const
dotnet_naming_style.all_caps.capitalization = all_upper
dotnet_naming_style.all_caps.word_separator = _
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,6 @@ __pycache__/
Artifacts/

*.feature.cs
*.DS_Store
*.DS_Store
*.der
*.pfx
2 changes: 2 additions & 0 deletions OPCUA.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.808.4
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OPCUA", "Source\OPCUA.csproj", "{86CF37E6-C870-45F1-B6D4-92719DBF98A5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OPCUA.Specs", "Specifications\OPCUA.Specs.csproj", "{13D6AC82-01DC-4E23-88A0-3EA656B20CEF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Connectors.OPCUA
[![.NET 5.0](https://github.com/RaaLabs/Connectors.OPCUA/actions/workflows/dotnet.yml/badge.svg?branch=main)](https://github.com/RaaLabs/Connectors.OPCUA/actions/workflows/dotnet.yml)
[![dotnet build](https://github.com/RaaLabs/Connectors.OPCUA/actions/workflows/dotnet.yml/badge.svg)](https://github.com/RaaLabs/Connectors.OPCUA/actions/workflows/dotnet.yml)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=RaaLabs_Connectors.OPCUA&metric=sqale_rating&token=237aec8269dd7b80a5ef37b10b858152b085720e)](https://sonarcloud.io/dashboard?id=RaaLabs_Connectors.OPCUA)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=RaaLabs_Connectors.OPCUA&metric=coverage&token=237aec8269dd7b80a5ef37b10b858152b085720e)](https://sonarcloud.io/dashboard?id=RaaLabs_Connectors.OPCUA)

Expand All @@ -19,7 +19,10 @@ The module is configured using a JSON file. `connector.json` represents the conn
"nodeIds": [
"ns=3;i=1002",
"ns=3;i=1001"
]
],
"opcUaServerCertificateIssuer": "Name of certificate issuer", // used to verify untrusted server certificates
"opcUaServerCertificateSubject": "Issuer subject", // used to verify untrusted server certificates
"opcUaServerAutoAcceptUntrustedCertificates": false
}
```

Expand Down Expand Up @@ -91,4 +94,4 @@ Prosys offers a free simulation server, which can be downloaded here: <https://d

The OPC UA server starts automatically once you launch the application. Under `Objects`, you can browse the nodes present in the server, and navigating and clicking the individual nodes, you can find the nodes `NodeId`, which you can use in the `configuration.json` to test the connector using the simulator.

The simulator also displays the server url (connection address) once you start the simulator.
The simulator also displays the server url (connection address) once you start the simulator.
9 changes: 5 additions & 4 deletions Source/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine3.17 AS build-env
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.19 AS build-env
ARG TARGETARCH
WORKDIR /app

VOLUME [ "/app/config" ]
Expand All @@ -7,22 +8,22 @@

WORKDIR /app/Source

RUN --mount=type=secret,id=NUGET_GITHUB_PACKAGES_USERNAME \

Check failure on line 11 in Source/Dockerfile

View workflow job for this annotation

GitHub Actions / dotnet / build

SC2046 warning: Quote this to prevent word splitting.

Check failure on line 11 in Source/Dockerfile

View workflow job for this annotation

GitHub Actions / dotnet / build

SC2046 warning: Quote this to prevent word splitting.
--mount=type=secret,id=NUGET_GITHUB_PACKAGES_TOKEN \
dotnet nuget add source \
--username $(cat /run/secrets/NUGET_GITHUB_PACKAGES_USERNAME) \
--password $(cat /run/secrets/NUGET_GITHUB_PACKAGES_TOKEN) \
--store-password-in-clear-text --name "githubpackagesnuget" "https://nuget.pkg.github.com/RaaLabs/index.json"

RUN dotnet restore --runtime alpine-x64
RUN dotnet restore --runtime="linux-musl-${TARGETARCH/amd64/x64}"

Check failure on line 18 in Source/Dockerfile

View workflow job for this annotation

GitHub Actions / dotnet / build

SC3060 warning: In POSIX sh, string replacement is undefined.

Check failure on line 18 in Source/Dockerfile

View workflow job for this annotation

GitHub Actions / dotnet / build

SC3060 warning: In POSIX sh, string replacement is undefined.

RUN dotnet publish -c Release -o out --no-restore \

Check failure on line 20 in Source/Dockerfile

View workflow job for this annotation

GitHub Actions / dotnet / build

DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.

Check failure on line 20 in Source/Dockerfile

View workflow job for this annotation

GitHub Actions / dotnet / build

SC3060 warning: In POSIX sh, string replacement is undefined.

Check failure on line 20 in Source/Dockerfile

View workflow job for this annotation

GitHub Actions / dotnet / build

DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.

Check failure on line 20 in Source/Dockerfile

View workflow job for this annotation

GitHub Actions / dotnet / build

SC3060 warning: In POSIX sh, string replacement is undefined.
--runtime alpine-x64 \
--runtime="linux-musl-${TARGETARCH/amd64/x64}" \
--self-contained true \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true

FROM mcr.microsoft.com/dotnet/runtime-deps:7.0-alpine3.17 AS final
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine3.19 AS final

WORKDIR /app
COPY --from=build-env /app/Source/out ./
Expand Down
8 changes: 8 additions & 0 deletions Source/OPCUA.CodeStyle.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<NoWarn>$(NoWarn);SYSLIB1006;CS1591;IL2104;IL2026;CS1570;CS1573</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
23 changes: 12 additions & 11 deletions Source/OPCUA.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="./OPCUA.CodeStyle.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<TrimMode>partial</TrimMode>
<AssemblyName>RaaLabs.Edge.Connectors.OPCUA</AssemblyName>
</PropertyGroup>
Expand All @@ -12,15 +13,15 @@
<TrimmerRootAssembly Include="RaaLabs.Edge.Connectors.OPCUA" preserve="all" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="RaaLabs.Edge" Version="1.16.1" />
<PackageReference Include="RaaLabs.Edge.Modules.EventHandling" Version="1.16.1" />
<PackageReference Include="RaaLabs.Edge.Modules.Configuration" Version="1.16.1" />
<PackageReference Include="RaaLabs.Edge.Modules.EdgeHub" Version="1.16.1" />
<PackageReference Include="RaaLabs.Edge.Modules.Diagnostics" Version="1.16.1" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua" Version="1.5.373.121" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.373.121" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration" Version="1.5.373.121" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Core" Version="1.5.373.121" />
<PackageReference Include="RaaLabs.Edge" Version="1.16.3" />
<PackageReference Include="RaaLabs.Edge.Modules.EventHandling" Version="1.16.3" />
<PackageReference Include="RaaLabs.Edge.Modules.Configuration" Version="1.16.3" />
<PackageReference Include="RaaLabs.Edge.Modules.EdgeHub" Version="1.16.3" />
<PackageReference Include="RaaLabs.Edge.Modules.Diagnostics" Version="1.16.3" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua" Version="1.5.374.27" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.374.27" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration" Version="1.5.374.27" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Core" Version="1.5.374.27" />
<PackageReference Include="Polly" Version="7.2.4" />
</ItemGroup>
</Project>
</Project>
55 changes: 43 additions & 12 deletions Source/OpcuaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public async Task<bool> ConnectAsync()
{
_logger.Information("Connecting...");

EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(_opcuaConfiguration.ServerUrl, false);
EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(discoveryUrl:_opcuaConfiguration.ServerUrl, useSecurity:true);
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(_applicationConfiguration);
ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);

Expand All @@ -64,7 +64,7 @@ public async Task<bool> ConnectAsync()
30 * 60 * 1000,
new UserIdentity(),
null
);
).ConfigureAwait(false);

if (opcuaSession != null && opcuaSession.Connected)
{
Expand Down Expand Up @@ -143,24 +143,55 @@ out DiagnosticInfoCollection diagnosticInfos
/// </summary>
private void CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
{
bool certificateAccepted = true;

// ****
// Implement a custom logic to decide if the certificate should be
// accepted or not and set certificateAccepted flag accordingly.
// The certificate can be retrieved from the e.Certificate field
// ***

ServiceResult error = e.Error;
while (error != null)
{
_logger.Information(error.ToString());
error = error.InnerResult;
}

bool certificateAccepted = false;
bool subjectMatch = false;
bool issuerMatch = false;
bool certificateIsNotExpired = false;

if (e.Certificate.Subject == _opcuaConfiguration.OpcUaServerCertificateSubject)
{
subjectMatch = true;
}
else
{
_logger.Information("Subject from server certificate does not match. Expected={0}, Actual={1}", _opcuaConfiguration.OpcUaServerCertificateSubject, e.Certificate.Subject);
}

if (e.Certificate.Issuer == _opcuaConfiguration.OpcUaServerCertificateIssuer)
{
issuerMatch = true;
}
else
{
_logger.Information("Issuer from server certificate does not match. Expected={0}, Actual={1}", _opcuaConfiguration.OpcUaServerCertificateIssuer, e.Certificate.Issuer);
}

DateTimeOffset dateTimeOffsetNow = DateTimeOffset.Now;
if (dateTimeOffsetNow >= e.Certificate.NotBefore && dateTimeOffsetNow <= e.Certificate.NotAfter)
{
certificateIsNotExpired = true;
}

if (subjectMatch && issuerMatch && certificateIsNotExpired)
{
certificateAccepted = true;
}

if (certificateAccepted)
{
_logger.Information("Untrusted Certificate accepted. SubjectName = {0}", e.Certificate.SubjectName);
_logger.Information("Untrusted Certificate accepted. Subject={0}, Issuer={1}", e.Certificate.Subject, e.Certificate.Issuer);
}

else
{
_logger.Information("Untrusted Certificate rejected. Subject={0}, Issuer={1}", e.Certificate.Subject, e.Certificate.Issuer);
}

e.AcceptAll = certificateAccepted;
Expand Down Expand Up @@ -188,7 +219,7 @@ private void CertificateValidation(CertificateValidator sender, CertificateValid

datapoints.Add(opcuaDatapointOutput);
}

return datapoints;
}
}
Expand Down
8 changes: 7 additions & 1 deletion Source/OpcuaConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ public class OpcuaConfiguration : IConfiguration
{
public string ServerUrl { get; }
public ISet<string> NodeIds { get; }
public string OpcUaServerCertificateIssuer { get; }
public string OpcUaServerCertificateSubject { get;}
public bool OpcUaServerAutoAcceptUntrustedCertificates { get; }

public OpcuaConfiguration(string serverUrl, ISet<string> nodeIds)
public OpcuaConfiguration(string serverUrl, ISet<string> nodeIds, string opcUaServerCertificateIssuer, string opcUaServerCertificateSubject, bool opcUaServerAutoAcceptUntrustedCertificates = false)
{
ServerUrl = serverUrl;
NodeIds = nodeIds;
OpcUaServerCertificateIssuer = opcUaServerCertificateIssuer;
OpcUaServerCertificateSubject = opcUaServerCertificateSubject;
OpcUaServerAutoAcceptUntrustedCertificates = opcUaServerAutoAcceptUntrustedCertificates;
}
}
26 changes: 14 additions & 12 deletions Source/OpcuaConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the GPLv2 License. See LICENSE file in the project root for full license information.

using System;
using System.IO;
using Serilog;
using System.Threading.Tasks;
using System.Collections.Generic;
Expand All @@ -19,7 +20,7 @@ namespace RaaLabs.Edge.Connectors.OPCUA;
public class OpcuaConnector : IRunAsync, IProduceEvent<Events.OpcuaDatapointOutput>
{
/// <summary>
///
///
/// </summary>
public event EventEmitter<Events.OpcuaDatapointOutput> SendDatapoint;
private OpcuaClient _opcuaClient;
Expand All @@ -42,13 +43,14 @@ public OpcuaConnector(ILogger logger, OpcuaConfiguration opcuaConfiguration, IMe

var securityConfig = new SecurityConfiguration()
{
AutoAcceptUntrustedCertificates = true // ONLY for debugging/early dev
ApplicationCertificate = new CertificateIdentifier { StoreType = @"Directory", StorePath = Directory.GetCurrentDirectory(), SubjectName = string.Format("DC={0},O={1},CN={2}", "Rafaels-MacBook-Pro.local", "Prosys OPC", "SimulationServer@Rafaels-MacBook-Pro") },
AutoAcceptUntrustedCertificates = opcuaConfiguration.OpcUaServerAutoAcceptUntrustedCertificates
};

var config = new ApplicationConfiguration()
{
ApplicationName = "Raa Labs OPC UA connector",
ApplicationUri = "Raa Labs OPC UA connector",
ApplicationName = "RaaLabsOPCUAConnector",
ApplicationUri = "urn:RaaLabsOPCUAConnector",
ApplicationType = ApplicationType.Client,
TransportConfigurations = new TransportConfigurationCollection(),
TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
Expand All @@ -59,11 +61,11 @@ public OpcuaConnector(ILogger logger, OpcuaConfiguration opcuaConfiguration, IMe

_opcuaAppInstance = new ApplicationInstance()
{
ApplicationName = "Raa Labs OPC UA connector",
ApplicationType = ApplicationType.Client,
ApplicationConfiguration = config
};

_opcuaAppInstance.CheckApplicationInstanceCertificate(false, 2048).GetAwaiter().GetResult();

_nodesToRead = InitializeReadValueIdCollection();
}

Expand All @@ -72,7 +74,7 @@ private ReadValueIdCollection InitializeReadValueIdCollection()
ReadValueIdCollection nodesToRead = new ReadValueIdCollection(){};
foreach (var nodeId in _opcuaConfiguration.NodeIds)
{
// Because nodeId and value cannot be read using the same ReadValueId, but nodeId and value are required
// Because nodeId and value cannot be read using the same ReadValueId, but nodeId and value are required
nodesToRead.Add(new ReadValueId() { NodeId = nodeId, AttributeId = Attributes.NodeId });
nodesToRead.Add(new ReadValueId() { NodeId = nodeId, AttributeId = Attributes.Value });
}
Expand All @@ -84,7 +86,7 @@ public async Task Run()
{
_logger.Information("Raa Labs OPC UA connector");
_opcuaClient = new OpcuaClient(_opcuaAppInstance.ApplicationConfiguration, _opcuaConfiguration, _logger, ClientBase.ValidateResponse);
await _opcuaClient.ConnectAsync();
await _opcuaClient.ConnectAsync().ConfigureAwait(false);

while (true)
{
Expand All @@ -98,10 +100,10 @@ public async Task Run()

await policy.ExecuteAsync(async () =>
{
await ConnectOpcua();
});
await ConnectOpcua().ConfigureAwait(false);
}).ConfigureAwait(false);

await Task.Delay(1000);
await Task.Delay(1000).ConfigureAwait(false);
}
}

Expand All @@ -111,7 +113,7 @@ private async Task ConnectOpcua()
{
if (!_opcuaClient.Session.Connected)
{
await _opcuaClient.ConnectAsync();
await _opcuaClient.ConnectAsync().ConfigureAwait(false);
}

List<Events.OpcuaDatapointOutput> opcuaDatapoints = _opcuaClient.ReadNodes(_nodesToRead);
Expand Down
7 changes: 5 additions & 2 deletions Source/config/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
"nodeIds": [
"ns=3;i=1002",
"ns=3;i=1001"
]
}
],
"opcUaServerCertificateIssuer": "",
"opcUaServerCertificateSubject": "",
"opcUaServerAutoAcceptUntrustedCertificates": false
}
Loading
Loading