/* ======================================================================== * 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 { /// /// The security configuration for the application. /// public partial class SecurityConfiguration { /// /// The security profiles which are supported for this configuration. /// public ArrayOf SupportedSecurityPolicies { get; private set; } /// /// Get the provider which is invoked when a password /// for a private key is requested. /// public ICertificatePasswordProvider? CertificatePasswordProvider { get; set; } /// /// Adds a certificate as a trusted peer. /// public void AddTrustedPeer(byte[] certificate) { TrustedPeerCertificates.TrustedCertificates = TrustedPeerCertificates.TrustedCertificates.AddItem( new CertificateIdentifier { RawData = certificate }); } /// /// Validates the security configuration. /// /// 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); } } /// /// Validate if the specified store can be opened /// throws ServiceResultException /// /// 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 logger = telemetry.CreateLogger(); logger.LogError(ex, "Failed to open {StoreName} store", storeName); throw ServiceResultException.ConfigurationError("{0} store is invalid.", storeName); } } /// /// Find application certificate for a security policy. /// public async Task 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; } /// /// Finds a concrete certificate from entries configured with the abstract ApplicationCertificateType. /// private async Task 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; } /// /// Use the list of application certificates to build a list /// of supported security policies. /// private ArrayOf BuildSupportedSecurityPolicies() { var securityPolicies = new List { 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(); foreach (string securityPolicyUri in securityPolicies.Distinct()) { if (SecurityPolicies.GetDisplayName(securityPolicyUri) != null) { result.Add(securityPolicyUri); } } return result; } } }