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

Failed connection due to revoked certificate with unsupported hash algorithm when using X509Store for certificates #2823

Open
1 of 5 tasks
ganko-pi opened this issue Oct 29, 2024 · 2 comments
Assignees

Comments

@ganko-pi
Copy link

Type of issue

  • Bug
  • Enhancement
  • Compliance
  • Question
  • Help wanted

Current Behavior

A System.Security.Cryptography.CryptographicException with message "Hash algorithm 1.2.840.113549.1.1.2 is not supported." occurs during connection to an OPC UA server when there is a certificate on a certificate revocation list (CRL) in the Windows certificate store which has an unsupported hash algorithm (in my specific case MD2 with OID 1.2.840.113549.1.1.2) even if it is not associated with OPC UA in any way. The error occurs in Opc.Ua.X509CertificateStore in line 255 because the IssuerName could not be extracted due to the unknown hash.

Expected Behavior

The CryptographicException should not fail the connection to the OPC UA server. If the certificate really belongs to the OPC UA connection the connection fails at a later point anyway.

Steps To Reproduce

  1. Operating system: Microsoft Windows 10
  2. Tested with commit 0b23e5f on branch release/1.5.374
  3. Have a certificate with an invalid hash (example MD2) in the CRL of Intermediate Certification Authorities in the Windows certificate store (I was not able to find a way to create a certificate with an invalid hash and add it to a revocation list so I cannot give instructions on this)
  4. Clone the UA-.NETStandard repository from GitHub (https://github.com/OPCFoundation/UA-.NETStandard)
  5. Create a new C# console project with the name OpcUaExample and .NET 8
  6. Add the project Opc.Ua.Client.csproj from UA-.NETStandard/Libraries/Opc.Ua.Client and Opc.Ua.Core.csproj from UA-.NETStandard/Stack/Opc.Ua.Core to the solution
  7. Add a project reference to Opc.Ua.Client to OpcUaExample
  8. Replace the contents of Program.cs with the following:
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using ISession = Opc.Ua.Client.ISession;

namespace OpcUaExample;

/// <summary>
/// Class containing the entry point of the program.
/// </summary>
public class Program
{

    /// <summary>
    /// Entry point of the program.
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    public static async Task Main()
    {
        using OpcUaSessionKeeper opcUaSessionKeeper = new("opc.tcp://localhost:62541/Quickstarts/ReferenceServer");

        await opcUaSessionKeeper.ConnectOpcUaSession();
    }
}



/// <summary>
/// Class to manage a OPC UA session.
/// </summary>
public class OpcUaSessionKeeper : IDisposable
{
    private readonly string _opcUaUri;
    private ISession? _opcUaSession;
    private readonly ushort _opcUaCertificateLifetimeInMonths = 180;

    /// <summary>
    /// Constructor to instantiate an <see cref="OpcUaSessionKeeper"/>.
    /// </summary>
    public OpcUaSessionKeeper(string uri)
    {
        _opcUaUri = uri;
    }

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

    /// <summary>
    /// Creates a new OPC UA <see cref="ISession"/> and connects to it.
    /// </summary>
    public async Task ConnectOpcUaSession()
    {
        // define the OPC UA client application
        ApplicationInstance application = new()
        {
            ApplicationType = ApplicationType.Client,
        };

        // load the application configuration
        string applicationConfigurationFilePath = Path.Combine(AppContext.BaseDirectory, "OpcUaExample.Config.xml");
        ApplicationConfiguration config = await application.LoadApplicationConfiguration(applicationConfigurationFilePath, silent: false);

        try
        {
            // check the application certificate.
            await application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0, _opcUaCertificateLifetimeInMonths);
        }
        catch (Exception ex)
        {
            string baseLoggingMessage = "Exception occured during check of certificate.";
            Console.WriteLine("{0} Cause: {1}: {2}.", baseLoggingMessage, ex.GetType(), ex.Message);

            // try deleting the old certificate and creating a new one
            await application.DeleteApplicationInstanceCertificate();
            application.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Certificate = null;

            // create a new application certificate
            await application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0, _opcUaCertificateLifetimeInMonths);

            Console.WriteLine("Deleted the old application certificate and created a new one successfully.");
        }

        // SessionTimeOut >= KeepAliveTimeout
        uint sessionTimeOutMs = (uint)TimeSpan.FromSeconds(60).TotalMilliseconds;
        int keepAliveIntervalMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds;

        string serverUri = _opcUaUri;
        UserIdentity? userIdentity = null;

        Console.WriteLine("Connecting to OPC UA server {0}.", serverUri);

        // configure endpoint for OPC UA
        EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(config, serverUri, useSecurity: true);
        EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(config);
        ConfiguredEndpoint endpoint = new(null, endpointDescription, endpointConfiguration);

        Session session = await Session.Create(
            config,
            endpoint,
            updateBeforeConnect: false,
            sessionName: config.ApplicationName,
            sessionTimeOutMs,
            userIdentity,
            preferredLocales: null
        );

        session.KeepAliveInterval = keepAliveIntervalMs;

        Console.WriteLine("New session for OPC UA created with session name {0} for server {1}.", session.SessionName, serverUri);

        _opcUaSession = session;
    }

    /// <summary>
    /// Closes the OPC UA <see cref="ISession"/> if a connection exists.
    /// </summary>
    public async Task DisconnectOpcUaSession()
    {
        if (_opcUaSession == null)
        {
            return;
        }

        _opcUaSession.KeepAlive -= RecoverSessionOnError;
        await _opcUaSession.CloseAsync();
        _opcUaSession.Dispose();
    }
}
  1. Create a file OpcUaExample.Config.xml and paste the following:
<?xml version="1.0" encoding="utf-8"?>
<ApplicationConfiguration
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd"
  xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd"
>
	<ApplicationName>OpcUaExample</ApplicationName>
	<ApplicationUri>urn:localhost:OpcUaExample</ApplicationUri>
	<ApplicationType>Client_1</ApplicationType>

	<SecurityConfiguration>

		<!-- Where the application instance certificate is stored (MachineDefault) -->
		<ApplicationCertificate>
			<StoreType>X509Store</StoreType>
			<StorePath>CurrentUser\My</StorePath>
			<SubjectName>CN=OpcUaExample,DC=localhost</SubjectName>
		</ApplicationCertificate>

		<!-- Where the issuer certificate are stored (certificate authorities) -->
		<TrustedIssuerCertificates>
			<StoreType>X509Store</StoreType>
			<StorePath>CurrentUser\Ca</StorePath>
		</TrustedIssuerCertificates>

		<!-- Where the trust list is stored -->
		<TrustedPeerCertificates>
			<StoreType>X509Store</StoreType>
			<StorePath>CurrentUser\TrustedPeople</StorePath>
		</TrustedPeerCertificates>

		<!-- The directory used to store invalid certficates for later review by the administrator. -->
		<RejectedCertificateStore>
			<StoreType>X509Store</StoreType>
			<StorePath>CurrentUser\Disallowed</StorePath>
		</RejectedCertificateStore>

		<!-- WARNING: The following setting (to automatically accept untrusted certificates) should be used
    for easy debugging purposes ONLY and turned off for production deployments! -->
		<AutoAcceptUntrustedCertificates>true</AutoAcceptUntrustedCertificates>

		<!-- WARNING: SHA1 signed certficates are by default rejected and should be phased out. 
       only nano and embedded profiles are allowed to use sha1 signed certificates. -->
		<RejectSHA1SignedCertificates>true</RejectSHA1SignedCertificates>
		<RejectUnknownRevocationStatus>true</RejectUnknownRevocationStatus>
		<MinimumCertificateKeySize>2048</MinimumCertificateKeySize>
		<AddAppCertToTrustedStore>false</AddAppCertToTrustedStore>
		<SendCertificateChain>true</SendCertificateChain>

		<!-- Where the User trust list is stored-->
		<TrustedUserCertificates>
			<StoreType>X509Store</StoreType>
			<StorePath>CurrentUser\AddressBook</StorePath>
		</TrustedUserCertificates>

	</SecurityConfiguration>

	<TransportConfigurations></TransportConfigurations>

	<TransportQuotas>
		<OperationTimeout>120000</OperationTimeout>
		<MaxStringLength>4194304</MaxStringLength>
		<MaxByteStringLength>4194304</MaxByteStringLength>
		<MaxArrayLength>65535</MaxArrayLength>
		<MaxMessageSize>4194304</MaxMessageSize>
		<MaxBufferSize>65535</MaxBufferSize>
		<ChannelLifetime>300000</ChannelLifetime>
		<SecurityTokenLifetime>3600000</SecurityTokenLifetime>
	</TransportQuotas>

	<ClientConfiguration>
		<DefaultSessionTimeout>60000</DefaultSessionTimeout>
		<WellKnownDiscoveryUrls>
			<ua:String>opc.tcp://{0}:4840</ua:String>
			<ua:String>http://{0}:52601/UADiscovery</ua:String>
			<ua:String>http://{0}/UADiscovery/Default.svc</ua:String>
		</WellKnownDiscoveryUrls>
		<DiscoveryServers></DiscoveryServers>
		<MinSubscriptionLifetime>10000</MinSubscriptionLifetime>

		<OperationLimits>
			<MaxNodesPerRead>2500</MaxNodesPerRead>
			<MaxNodesPerHistoryReadData>1000</MaxNodesPerHistoryReadData>
			<MaxNodesPerHistoryReadEvents>1000</MaxNodesPerHistoryReadEvents>
			<MaxNodesPerWrite>2500</MaxNodesPerWrite>
			<MaxNodesPerHistoryUpdateData>1000</MaxNodesPerHistoryUpdateData>
			<MaxNodesPerHistoryUpdateEvents>1000</MaxNodesPerHistoryUpdateEvents>
			<MaxNodesPerMethodCall>2500</MaxNodesPerMethodCall>
			<MaxNodesPerBrowse>2500</MaxNodesPerBrowse>
			<MaxNodesPerRegisterNodes>2500</MaxNodesPerRegisterNodes>
			<MaxNodesPerTranslateBrowsePathsToNodeIds>2500</MaxNodesPerTranslateBrowsePathsToNodeIds>
			<MaxNodesPerNodeManagement>2500</MaxNodesPerNodeManagement>
			<MaxMonitoredItemsPerCall>2500</MaxMonitoredItemsPerCall>
		</OperationLimits>

	</ClientConfiguration>

</ApplicationConfiguration>
  1. Make sure that the configuration file is copied to output directory, e.g. with adding the following to OpcUaExample.csproj:
<ItemGroup>
  <None Update="OpcUaExample.Config.xml">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>
  1. Open the solution UA-.NETStandard/UA Reference.sln
  2. Start the project ConsoleReferenceServer
  3. Start the project OpcUaExample
  4. OpcUaExample fails with CryptographicException due to an unsupported hash algorithm of a certificate

Environment

- OS: Microsoft Windows 10
- Environment: Visual Studio 2022 17.11.5
- Runtime: .NET 8.0
- Git branch: release/1.5.374
- Git commit: 0b23e5f
- Component: Opc.Ua.Core
- Server: Reference Server
- Client: self-made

Anything else?

Log

Type = System.Security.Cryptography.CryptographicException
Message = Hash algorithm 1.2.840.113549.1.1.2 is not supported. 
Source = Opc.Ua.Security.Certificates
TargetSite = System.Security.Cryptography.HashAlgorithmName GetHashAlgorithmName(System.String)
HResult = -2146233087
StackTrace =
      at Opc.Ua.Security.Certificates.Oids.GetHashAlgorithmName(String oid) in C:\source\repos\UA-.NETStandard\Libraries\Opc.Ua.Security.Certificates\Common\Oids.cs:line 209
      at Opc.Ua.Security.Certificates.X509Signature.Decode(Byte[] crl) in C:\source\repos\UA-.NETStandard\Libraries\Opc.Ua.Security.Certificates\X509Crl\X509Signature.cs:line 140
      at Opc.Ua.Security.Certificates.X509Signature..ctor(Byte[] signedBlob) in C:\source\repos\UA-.NETStandard\Libraries\Opc.Ua.Security.Certificates\X509Crl\X509Signature.cs:line 68
      at Opc.Ua.Security.Certificates.X509CRL.Decode(Byte[] crl) in C:\source\repos\UA-.NETStandard\Libraries\Opc.Ua.Security.Certificates\X509Crl\X509Crl.cs:line 217
      at Opc.Ua.Security.Certificates.X509CRL.EnsureDecoded() in C:\source\repos\UA-.NETStandard\Libraries\Opc.Ua.Security.Certificates\X509Crl\X509Crl.cs:line 367
      at Opc.Ua.Security.Certificates.X509CRL.get_IssuerName() in C:\source\repos\UA-.NETStandard\Libraries\Opc.Ua.Security.Certificates\X509Crl\X509Crl.cs:line 102
      at Opc.Ua.X509CertificateStore.IsRevoked(X509Certificate2 issuer, X509Certificate2 certificate) in C:\source\repos\UA-.NETStandard\Stack\Opc.Ua.Core\Security\Certificates\X509CertificateStore\X509CertificateStore.cs:line 255
      at Opc.Ua.CertificateValidator.GetIssuerNoExceptionAsync(X509Certificate2 certificate, CertificateIdentifierCollection explicitList, CertificateStoreIdentifier certificateStore, Boolean checkRecovationStatus) in C:\source\repos\UA-.NETStandard\Stack\Opc.Ua.Core\Security\Certificates\CertificateValidator.cs:line 1045
      at Opc.Ua.CertificateValidator.GetIssuersNoExceptionsOnGetIssuer(X509Certificate2Collection certificates, List`1 issuers, Dictionary`2 validationErrors) in C:\source\repos\UA-.NETStandard\Stack\Opc.Ua.Core\Security\Certificates\CertificateValidator.cs:line 903
      at Opc.Ua.CertificateValidator.InternalValidateAsync(X509Certificate2Collection certificates, ConfiguredEndpoint endpoint, CancellationToken ct) in C:\source\repos\UA-.NETStandard\Stack\Opc.Ua.Core\Security\Certificates\CertificateValidator.cs:line 1145
      at Opc.Ua.CertificateValidator.ValidateAsync(X509Certificate2Collection chain, ConfiguredEndpoint endpoint, CancellationToken ct) in C:\source\repos\UA-.NETStandard\Stack\Opc.Ua.Core\Security\Certificates\CertificateValidator.cs:line 510
      at Opc.Ua.Client.Session.OpenAsync(String sessionName, UInt32 sessionTimeout, IUserIdentity identity, IList`1 preferredLocales, Boolean checkDomain, CancellationToken ct) in C:\source\repos\UA-.NETStandard\Libraries\Opc.Ua.Client\Session\SessionAsync.cs:line 104
      at Opc.Ua.Client.Session.Create(ISessionInstantiator sessionInstantiator, ApplicationConfiguration configuration, ITransportWaitingConnection connection, ConfiguredEndpoint endpoint, Boolean updateBeforeConnect, Boolean checkDomain, String sessionName, UInt32 sessionTimeout, IUserIdentity identity, IList`1 preferredLocales, CancellationToken ct) in C:\source\repos\UA-.NETStandard\Libraries\Opc.Ua.Client\Session\Session.cs:line 1174

Suggested fix

File: https://github.com/OPCFoundation/UA-.NETStandard/blob/release/1.5.374/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs
Line: 254 (first in foreach)

try
{
    X500DistinguishedName issuerName = crl.IssuerName;
}
catch (CryptographicException)
{
    continue;
}
@romanett romanett self-assigned this Oct 30, 2024
@romanett
Copy link
Contributor

@ganko-pi thank you for the Details i will take a look, could you eventually provide the crl containing the "invalid" certificate?

@ganko-pi
Copy link
Author

Here is the crl containing the certificate with an MD2 hash. The file extension must be changed back from .crl.txt to .crl due to GitHub not allowing the upload of a file with .crl.
cert_with_md2_hash.crl.txt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants