mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 08:41:51 +08:00
82993d0b6f
## Proposed changes Tries to rectify inconsistent behavior: `SecurityConfiguration::BuilldSupportedSecurityPolicies()` advertises RSA policies if ApplicationCertificateType is set as CertificateType, while ApplicationCertificateType is not used when finding actual certificates. Use CertificateIdentifier::SetCertificate(certificate) to recalculate the CertificateType whenever the identifier receives a X509Certificate2. Removed stale if case` if (certType == ObjectTypeIds.ApplicationCertificateType)` from `SecurityConfiguration::FindApplicationCertificateAsync`, since `CertificateIdentifier.MapSecurityPolicyToCertificateTypes(securityPolicy)` never returned 'ApplicationCertificateType' and it was confusing and misleading because it suggested support for abstract ApplicationCertificateType, but normal policy lookup did not actually go through it. Allow application certificate lookup to use entries configured with abstract ApplicationCertificateType as fallback candidates. The fallback loads the candidate certificate first, then validates it against the concrete type required by the selected security policy. 'ApplicationCertificateType' is not a concrete certificate profile and should not be prefered in configuration, even if older configurations may use it. Previously, such entries could be missed during certificate lookup because the selected security policy maps to a concrete type. ## Related Issues - Fixes #3644 ## Types of changes What types of changes does your code introduce? _Put an `x` in the boxes that apply. You can also fill these out after creating the PR._ - [ ] Bugfix (non-breaking change which fixes an issue) - [x] Enhancement (non-breaking change which adds functionality) - [ ] Test enhancement (non-breaking change to increase test coverage) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages) - [ ] Documentation Update (if none of the other choices apply) ## Checklist _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - [ ] I have read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf). - [ ] I ran tests locally with my changes, all passed. - [ ] I fixed all failing tests in the CI pipelines. - [ ] I fixed all introduced issues with CodeQL and LGTM. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [ ] I have added necessary documentation (if appropriate). - [ ] Any dependent changes have been merged and published in downstream modules. ## Further comments If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...
359 lines
16 KiB
C#
359 lines
16 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua.Security.Certificates;
|
|
|
|
namespace Opc.Ua
|
|
{
|
|
/// <summary>
|
|
/// The security configuration for the application.
|
|
/// </summary>
|
|
public partial class SecurityConfiguration
|
|
{
|
|
/// <summary>
|
|
/// The security profiles which are supported for this configuration.
|
|
/// </summary>
|
|
public ArrayOf<string> SupportedSecurityPolicies { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Get the provider which is invoked when a password
|
|
/// for a private key is requested.
|
|
/// </summary>
|
|
public ICertificatePasswordProvider? CertificatePasswordProvider { get; set; }
|
|
|
|
/// <summary>
|
|
/// Adds a certificate as a trusted peer.
|
|
/// </summary>
|
|
public void AddTrustedPeer(byte[] certificate)
|
|
{
|
|
TrustedPeerCertificates.TrustedCertificates =
|
|
TrustedPeerCertificates.TrustedCertificates.AddItem(
|
|
new CertificateIdentifier { RawData = certificate });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the security configuration.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public void Validate(ITelemetryContext telemetry)
|
|
{
|
|
if (m_applicationCertificates.IsNull || m_applicationCertificates.Count == 0)
|
|
{
|
|
throw ServiceResultException.ConfigurationError(
|
|
"ApplicationCertificate must be specified.");
|
|
}
|
|
// ensure mandatory stores are valid
|
|
ValidateStore(TrustedIssuerCertificates, nameof(TrustedIssuerCertificates), telemetry);
|
|
ValidateStore(TrustedPeerCertificates, nameof(TrustedPeerCertificates), telemetry);
|
|
|
|
//ensure optional stores are valid if specified
|
|
if (TrustedHttpsCertificates != null)
|
|
{
|
|
ValidateStore(TrustedHttpsCertificates, nameof(TrustedHttpsCertificates), telemetry);
|
|
}
|
|
if (HttpsIssuerCertificates != null)
|
|
{
|
|
ValidateStore(HttpsIssuerCertificates, nameof(HttpsIssuerCertificates), telemetry);
|
|
}
|
|
if (TrustedUserCertificates != null)
|
|
{
|
|
ValidateStore(TrustedUserCertificates, nameof(TrustedUserCertificates), telemetry);
|
|
}
|
|
if (UserIssuerCertificates != null)
|
|
{
|
|
ValidateStore(UserIssuerCertificates, nameof(UserIssuerCertificates), telemetry);
|
|
}
|
|
|
|
if ((TrustedHttpsCertificates != null && HttpsIssuerCertificates == null) ||
|
|
(HttpsIssuerCertificates != null && TrustedHttpsCertificates == null))
|
|
{
|
|
throw ServiceResultException.ConfigurationError(
|
|
"Either none or both of HttpsIssuerCertificates & TrustedHttpsCertificates stores must be specified.");
|
|
}
|
|
|
|
if ((TrustedUserCertificates != null && UserIssuerCertificates == null) ||
|
|
(UserIssuerCertificates != null && TrustedUserCertificates == null))
|
|
{
|
|
throw ServiceResultException.ConfigurationError(
|
|
"Either none or both of UserIssuerCertificates & TrustedUserCertificates stores must be specified.");
|
|
}
|
|
|
|
// replace subjectName DC=localhost with DC=hostname
|
|
foreach (CertificateIdentifier applicationCertificate in m_applicationCertificates)
|
|
{
|
|
applicationCertificate.SubjectName = Utils.ReplaceDCLocalhost(
|
|
applicationCertificate.SubjectName);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate if the specified store can be opened
|
|
/// throws ServiceResultException
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
private static void ValidateStore(
|
|
CertificateTrustList? storeIdentifier,
|
|
string storeName,
|
|
ITelemetryContext telemetry)
|
|
{
|
|
if (string.IsNullOrEmpty(storeIdentifier?.StorePath))
|
|
{
|
|
throw ServiceResultException.ConfigurationError(
|
|
"{0} StorePath must be specified.", storeName);
|
|
}
|
|
try
|
|
{
|
|
ICertificateStore store = storeIdentifier!.OpenStore(telemetry) ??
|
|
throw ServiceResultException.ConfigurationError(
|
|
"Failed to open {0} store", storeName);
|
|
store.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ILogger<SecurityConfiguration> logger = telemetry.CreateLogger<SecurityConfiguration>();
|
|
logger.LogError(ex, "Failed to open {StoreName} store", storeName);
|
|
throw ServiceResultException.ConfigurationError("{0} store is invalid.", storeName);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find application certificate for a security policy.
|
|
/// </summary>
|
|
public async Task<Certificate?> FindApplicationCertificateAsync(
|
|
string securityPolicy,
|
|
bool privateKey,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken ct = default)
|
|
{
|
|
foreach (NodeId certType in CertificateIdentifier.MapSecurityPolicyToCertificateTypes(
|
|
securityPolicy))
|
|
{
|
|
CertificateIdentifier? id = (ApplicationCertificates.ToArray() ?? []).FirstOrDefault(certId =>
|
|
certId.CertificateType == certType);
|
|
if (id == null)
|
|
{
|
|
if (certType == ObjectTypeIds.RsaSha256ApplicationCertificateType)
|
|
{
|
|
// Fallback to old behavior of looking for an entry with no certificate type specified
|
|
// that will keep old configs working.
|
|
|
|
// undefined certificate type as RsaSha256
|
|
id = (ApplicationCertificates.ToArray() ?? []).FirstOrDefault(
|
|
certId => certId.CertificateType.IsNull);
|
|
}
|
|
}
|
|
|
|
if (id != null)
|
|
{
|
|
if (privateKey)
|
|
{
|
|
return await CertificateIdentifierResolver
|
|
.LoadPrivateKeyAsync(
|
|
id,
|
|
CertificatePasswordProvider,
|
|
applicationUri: null,
|
|
telemetry,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
return await CertificateIdentifierResolver
|
|
.ResolveAsync(
|
|
id,
|
|
registry: null,
|
|
needPrivateKey: false,
|
|
applicationUri: null,
|
|
telemetry,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
Certificate? certificate = await FindAbstractApplicationCertificateAsync(
|
|
certType,
|
|
privateKey,
|
|
telemetry,
|
|
ct).ConfigureAwait(false);
|
|
if (certificate != null)
|
|
{
|
|
return certificate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds a concrete certificate from entries configured with the abstract ApplicationCertificateType.
|
|
/// </summary>
|
|
private async Task<Certificate?> FindAbstractApplicationCertificateAsync(
|
|
NodeId requiredConcreteCertificateType,
|
|
bool privateKey,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken ct)
|
|
{
|
|
foreach (CertificateIdentifier id in (ApplicationCertificates.ToArray() ?? []).Where(certId =>
|
|
certId.CertificateType == ObjectTypeIds.ApplicationCertificateType))
|
|
{
|
|
var candidateId = new CertificateIdentifier
|
|
{
|
|
StoreType = id.StoreType,
|
|
StorePath = id.StorePath,
|
|
SubjectName = id.SubjectName,
|
|
Thumbprint = id.Thumbprint,
|
|
RawData = id.RawData,
|
|
ValidationOptions = id.ValidationOptions,
|
|
CertificateType = NodeId.Null
|
|
};
|
|
|
|
Certificate? certificate = privateKey
|
|
? await CertificateIdentifierResolver
|
|
.LoadPrivateKeyAsync(
|
|
candidateId,
|
|
CertificatePasswordProvider,
|
|
applicationUri: null,
|
|
telemetry,
|
|
ct)
|
|
.ConfigureAwait(false)
|
|
: await CertificateIdentifierResolver
|
|
.ResolveAsync(
|
|
candidateId,
|
|
registry: null,
|
|
needPrivateKey: false,
|
|
applicationUri: null,
|
|
telemetry,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
|
|
if (certificate != null &&
|
|
CertificateIdentifier.ValidateCertificateType(
|
|
certificate,
|
|
requiredConcreteCertificateType))
|
|
{
|
|
return certificate;
|
|
}
|
|
|
|
certificate?.Dispose();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use the list of application certificates to build a list
|
|
/// of supported security policies.
|
|
/// </summary>
|
|
private ArrayOf<string> BuildSupportedSecurityPolicies()
|
|
{
|
|
var securityPolicies = new List<string> { SecurityPolicies.None };
|
|
foreach (CertificateIdentifier applicationCertificate in m_applicationCertificates)
|
|
{
|
|
if (applicationCertificate.CertificateType.IsNull)
|
|
{
|
|
securityPolicies.Add(SecurityPolicies.Basic256Sha256);
|
|
securityPolicies.Add(SecurityPolicies.Aes128_Sha256_RsaOaep);
|
|
securityPolicies.Add(SecurityPolicies.Aes256_Sha256_RsaPss);
|
|
securityPolicies.Add(SecurityPolicies.RSA_DH_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.RSA_DH_ChaChaPoly);
|
|
continue;
|
|
}
|
|
if (applicationCertificate.CertificateType.TryGetValue(out uint identifier))
|
|
{
|
|
switch (identifier)
|
|
{
|
|
case ObjectTypes.EccNistP256ApplicationCertificateType:
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP256);
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP256_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP256_ChaChaPoly);
|
|
break;
|
|
case ObjectTypes.EccNistP384ApplicationCertificateType:
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP256);
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP256_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP256_ChaChaPoly);
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP384);
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP384_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.ECC_nistP384_ChaChaPoly);
|
|
break;
|
|
case ObjectTypes.EccBrainpoolP256r1ApplicationCertificateType:
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1);
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly);
|
|
break;
|
|
case ObjectTypes.EccBrainpoolP384r1ApplicationCertificateType:
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1);
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly);
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP384r1);
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP384r1_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly);
|
|
break;
|
|
case ObjectTypes.EccCurve25519ApplicationCertificateType:
|
|
securityPolicies.Add(SecurityPolicies.ECC_curve25519);
|
|
securityPolicies.Add(SecurityPolicies.ECC_curve25519_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.ECC_curve25519_ChaChaPoly);
|
|
break;
|
|
case ObjectTypes.EccCurve448ApplicationCertificateType:
|
|
securityPolicies.Add(SecurityPolicies.ECC_curve448);
|
|
securityPolicies.Add(SecurityPolicies.ECC_curve448_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.ECC_curve448_ChaChaPoly);
|
|
break;
|
|
case ObjectTypes.RsaMinApplicationCertificateType:
|
|
securityPolicies.Add(SecurityPolicies.Basic128Rsa15);
|
|
securityPolicies.Add(SecurityPolicies.Basic256);
|
|
break;
|
|
case ObjectTypes.ApplicationCertificateType:
|
|
case ObjectTypes.RsaSha256ApplicationCertificateType:
|
|
securityPolicies.Add(SecurityPolicies.Basic256Sha256);
|
|
securityPolicies.Add(SecurityPolicies.Aes128_Sha256_RsaOaep);
|
|
securityPolicies.Add(SecurityPolicies.Aes256_Sha256_RsaPss);
|
|
securityPolicies.Add(SecurityPolicies.RSA_DH_AesGcm);
|
|
securityPolicies.Add(SecurityPolicies.RSA_DH_ChaChaPoly);
|
|
goto case ObjectTypes.RsaMinApplicationCertificateType;
|
|
}
|
|
}
|
|
}
|
|
// filter based on platform support
|
|
var result = new List<string>();
|
|
foreach (string securityPolicyUri in securityPolicies.Distinct())
|
|
{
|
|
if (SecurityPolicies.GetDisplayName(securityPolicyUri) != null)
|
|
{
|
|
result.Add(securityPolicyUri);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|