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

enable flat directory certificate store for k8s cert-manager compatibility #328

Merged
merged 3 commits into from
Dec 12, 2023
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ X.509 certificates:
* Running on Windows natively, you cannot use an application certificate store of type `Directory`, since the access to the private key will fail. Use the option `--at X509Store` in this case.
* Running as Linux Docker container, you can map the certificate stores to the host file system by using the Docker run option `-v <hostpkidirectory>:/app/pki`. This will make the certificate persistent over starts.
* Running as Linux Docker container using an X509Store for the application certificate, you need to use the Docker run option `-v x509certstores:/root/.dotnet/corefx/cryptography/x509stores` and the application option `--at X509Store`
* When running in kubernetes context, use option `--at FlatDirectory`. This enables the OPC UA server to consume both public key and private key certificates directly from the /app/pki/own/ path without expecting the `certs` and `private` subdirectories. Furthermore, certificates of type .crt and .key are accepted.

## Resources
- [The OPC Foundation OPC UA .NET reference stack](https://github.com/OPCFoundation/UA-.NETStandard)
Expand Down Expand Up @@ -357,14 +358,15 @@ Options:
Default: 2000
--at, --appcertstoretype=VALUE
the own application cert store type.
(allowed values: Directory, X509Store)
(allowed values: Directory, X509Store, FlatDirectory)
Default: 'Directory'
--ap, --appcertstorepath=VALUE
the path where the own application cert should be
stored
Default (depends on store type):
X509Store: 'CurrentUser\UA_MachineDefault'
Directory: 'pki\own'
FlatDirectory: 'pki\own'
--tp, --trustedcertstorepath=VALUE
the path of the trusted cert store.
Default 'pki\trusted'
Expand Down
39 changes: 25 additions & 14 deletions src/CliOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace OpcPlc;
using Microsoft.Extensions.Logging;
using Mono.Options;
using Opc.Ua;
using OpcPlc.Certs;
using OpcPlc.Helpers;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -83,8 +84,8 @@ public static Mono.Options.OptionSet InitCommandLineOptions()

{ "drurs|dontrejectunknownrevocationstatus", $"Don't reject chain validation with CA certs with unknown revocation status, e.g. when the CRL is not available or the OCSP provider is offline.\nDefault: {DontRejectUnknownRevocationStatus}", (string s) => DontRejectUnknownRevocationStatus = s != null },

{ "ut|unsecuretransport", $"enables the unsecured transport.\nDefault: {EnableUnsecureTransport}", (string s) => EnableUnsecureTransport = s != null },
{ "ut|unsecuretransport", $"enables the unsecured transport.\nDefault: {EnableUnsecureTransport}", (string s) => EnableUnsecureTransport = s != null },

{ "to|trustowncert", $"the own certificate is put into the trusted certificate store automatically.\nDefault: {TrustMyself}", (string s) => TrustMyself = s != null },

{ "msec|maxsessioncount=", $"maximum number of parallel sessions.\nDefault: {MaxSessionCount}", (int i) => MaxSessionCount = i },
Expand All @@ -94,21 +95,31 @@ public static Mono.Options.OptionSet InitCommandLineOptions()

// cert store options
{ "at|appcertstoretype=", $"the own application cert store type. \n(allowed values: Directory, X509Store)\nDefault: '{OpcOwnCertStoreType}'", (string s) => {
if (s.Equals(CertificateStoreType.X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase))
{
OpcOwnCertStoreType = s.Equals(CertificateStoreType.X509Store, StringComparison.OrdinalIgnoreCase) ? CertificateStoreType.X509Store : CertificateStoreType.Directory;
OpcOwnCertStorePath = s.Equals(CertificateStoreType.X509Store, StringComparison.OrdinalIgnoreCase) ? OpcOwnCertX509StorePathDefault : OpcOwnCertDirectoryStorePathDefault;
}
else
{
throw new OptionException();
}
}
switch (s)
{
case CertificateStoreType.X509Store:
OpcOwnCertStoreType = CertificateStoreType.X509Store;
OpcOwnCertStorePath = OpcOwnCertX509StorePathDefault;
break;
case CertificateStoreType.Directory:
OpcOwnCertStoreType = CertificateStoreType.Directory;
OpcOwnCertStorePath = OpcOwnCertDirectoryStorePathDefault;
break;
case FlatDirectoryCertificateStore.StoreTypeName:
OpcOwnCertStoreType = FlatDirectoryCertificateStore.StoreTypeName;
OpcOwnCertStorePath = OpcOwnCertDirectoryStorePathDefault;
break;
default:
throw new OptionException();
}
}
},

{ "ap|appcertstorepath=", "the path where the own application cert should be stored\nDefault (depends on store type):\n" +
$"X509Store: '{OpcOwnCertX509StorePathDefault}'\n" +
$"Directory: '{OpcOwnCertDirectoryStorePathDefault}'", (string s) => OpcOwnCertStorePath = s
$"Directory: '{OpcOwnCertDirectoryStorePathDefault}'" +
$"FlatDirectory: '{OpcOwnCertDirectoryStorePathDefault}'",
(string s) => OpcOwnCertStorePath = s
},

{ "tp|trustedcertstorepath=", $"the path of the trusted cert store.\nDefault '{OpcTrustedCertDirectoryStorePathDefault}'", (string s) => OpcTrustedCertStorePath = s },
Expand Down Expand Up @@ -171,7 +182,7 @@ public static Mono.Options.OptionSet InitCommandLineOptions()

{ "rc|removecert=", "remove cert(s) with the given thumbprint(s) (comma separated values).", (string s) => ThumbprintsToRemove = ParseListOfStrings(s)
},

{"daa|disableanonymousauth", $"flag to disable anonymous authentication.\nDefault: {Program.DisableAnonymousAuth}", (string s) => Program.DisableAnonymousAuth = s != null },
{"dua|disableusernamepasswordauth", $"flag to disable username/password authentication.\nDefault: {Program.DisableUsernamePasswordAuth}", (string s) => Program.DisableUsernamePasswordAuth = s != null },
{"dca|disablecertauth", $"flag to disable certificate authentication.\nDefault: {Program.DisableCertAuth}", (string s) => Program.DisableCertAuth = s != null },
Expand Down
233 changes: 233 additions & 0 deletions src/FlatDirectoryCertificateStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
namespace OpcPlc.Certs;

using Opc.Ua;
using Opc.Ua.Security.Certificates;
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

/// <summary>
/// Flat directory certificate store that does not have internal
/// hierarchy with certs/crl/private subdirectories.
/// </summary>
public sealed class FlatDirectoryCertificateStore : ICertificateStore
{
private const string CrtExtension = ".crt";
private const string KeyExtension = ".key";

private readonly DirectoryCertificateStore _innerStore;

/// <summary>
/// Identifier for flat directory certificate store.
/// </summary>
public const string StoreTypeName = "FlatDirectory";

/// <summary>
/// Initializes a new instance of the <see cref="FlatDirectoryCertificateStore"/> class.
/// </summary>
public FlatDirectoryCertificateStore()
{
_innerStore = new DirectoryCertificateStore(noSubDirs: true);
}

/// <inheritdoc/>
public string StoreType => StoreTypeName;

/// <inheritdoc/>
public string StorePath => _innerStore.StorePath;

/// <inheritdoc/>
public bool SupportsLoadPrivateKey => _innerStore.SupportsLoadPrivateKey;

/// <inheritdoc/>
public bool SupportsCRLs => _innerStore.SupportsCRLs;

/// <inheritdoc/>
public void Dispose()
{
_innerStore.Dispose();
}

/// <inheritdoc/>
public void Open(string location, bool noPrivateKeys = true)
{
ArgumentNullException.ThrowIfNull(location);
_innerStore.Open(location, noPrivateKeys);
}

/// <inheritdoc/>
public void Close()
{
_innerStore.Close();
}

/// <inheritdoc/>
public Task Add(X509Certificate2 certificate, string password = null)
{
return _innerStore.Add(certificate, password);
}

/// <inheritdoc/>
public Task<bool> Delete(string thumbprint)
{
return _innerStore.Delete(thumbprint);
}

/// <inheritdoc/>
public async Task<X509Certificate2Collection> Enumerate()
{
X509Certificate2Collection certificatesCollection = await _innerStore.Enumerate().ConfigureAwait(false);
if (!_innerStore.Directory.Exists)
{
return certificatesCollection;
}

foreach (FileInfo file in _innerStore.Directory.GetFiles('*' + CrtExtension))
{
try
{
var certificates = new X509Certificate2Collection();
certificates.ImportFromPemFile(file.FullName);
certificatesCollection.AddRange(certificates);
foreach (X509Certificate2 certificate in certificates)
{
Utils.LogInfo("Enumerate certificates - certificate added {thumbprint}", certificate.Thumbprint);
}
}
catch (Exception e)
{
Utils.LogError(e, "Could not load certificate from file: {fileName}", file.FullName);
}
}

return certificatesCollection;
}

/// <inheritdoc/>
public Task AddCRL(X509CRL crl)
{
return _innerStore.AddCRL(crl);
}

/// <inheritdoc/>
public Task<bool> DeleteCRL(X509CRL crl)
{
return _innerStore.DeleteCRL(crl);
}

/// <inheritdoc/>
public Task<X509CRLCollection> EnumerateCRLs()
{
return _innerStore.EnumerateCRLs();
}

/// <inheritdoc/>
public Task<X509CRLCollection> EnumerateCRLs(X509Certificate2 issuer, bool validateUpdateTime = true)
{
return _innerStore.EnumerateCRLs(issuer, validateUpdateTime);
}

/// <inheritdoc/>
public async Task<X509Certificate2Collection> FindByThumbprint(string thumbprint)
{
X509Certificate2Collection certificatesCollection = await _innerStore.FindByThumbprint(thumbprint).ConfigureAwait(false);

if (!_innerStore.Directory.Exists)
{
return certificatesCollection;
}

foreach (FileInfo file in _innerStore.Directory.GetFiles('*' + CrtExtension))
{
try
{
var certificates = new X509Certificate2Collection();
certificates.ImportFromPemFile(file.FullName);
foreach (X509Certificate2 certificate in certificates)
{
if (string.Equals(certificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
{
Utils.LogInfo("Find by thumbprint: {thumbprint} - found", thumbprint);
certificatesCollection.Add(certificate);
}
}
}
catch (Exception e)
{
Utils.LogError(e, "Could not load certificate from file: {fileName}", file.FullName);
}
}

return certificatesCollection;
}

/// <inheritdoc/>
public Task<StatusCode> IsRevoked(X509Certificate2 issuer, X509Certificate2 certificate)
{
return _innerStore.IsRevoked(issuer, certificate);
}

/// <inheritdoc/>
public async Task<X509Certificate2> LoadPrivateKey(string thumbprint, string subjectName, string password)
{
if (!_innerStore.Directory.Exists)
{
return await _innerStore.LoadPrivateKey(thumbprint, subjectName, password).ConfigureAwait(false);
}

foreach (FileInfo file in _innerStore.Directory.GetFiles('*' + CrtExtension))
{
try
{
var keyFile = new FileInfo(file.FullName.Replace(CrtExtension, KeyExtension, StringComparison.OrdinalIgnoreCase));
if (keyFile.Exists)
{
using var certificate = new X509Certificate2(file.FullName);
if (!MatchCertificate(certificate, thumbprint, subjectName))
{
continue;
}

X509Certificate2 privateKeyCertificate = X509Certificate2.CreateFromPemFile(file.FullName, keyFile.FullName);

Utils.LogInfo("Loading private key succeeded for {thumbprint} - {subjectName}", thumbprint, subjectName);
return privateKeyCertificate;
}
}
catch (Exception e)
{
Utils.LogError(e, "Could not load private key for certificate file: {fileName}", file.FullName);
}
}

return await _innerStore.LoadPrivateKey(thumbprint, subjectName, password).ConfigureAwait(false);
}

private bool MatchCertificate(X509Certificate2 certificate, string thumbprint, string subjectName)
{
if (!string.IsNullOrEmpty(thumbprint) &&
!string.Equals(certificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
{
return false;
}

if (!string.IsNullOrEmpty(subjectName) &&
!X509Utils.CompareDistinguishedName(subjectName, certificate.Subject) &&
(
subjectName.Contains('=', StringComparison.OrdinalIgnoreCase) ||
!X509Utils.ParseDistinguishedName(certificate.Subject).Any(s => s.Equals("CN=" + subjectName, StringComparison.Ordinal))))
{
return false;
}

// skip if not RSA certificate
if (X509Utils.GetRSAPublicKeySize(certificate) < 0)
{
return false;
}

return true;
}
}
21 changes: 21 additions & 0 deletions src/FlatDirectoryCertificateStoreType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace OpcPlc.Certs;

using Opc.Ua;

/// <summary>
/// Defines type for <see cref="FlatDirectoryCertificateStore"/>.
/// </summary>
public sealed class FlatDirectoryCertificateStoreType : ICertificateStoreType
{
/// <inheritdoc/>
public ICertificateStore CreateStore()
{
return new FlatDirectoryCertificateStore();
}

/// <inheritdoc/>
public bool SupportsStorePath(string storePath)
{
return !string.IsNullOrEmpty(storePath);
}
}
Loading
Loading