OPCFoundation / UA-.NETStandard

OPC Unified Architecture .NET Standard
Other
1.97k stars 950 forks source link

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

Closed ganko-pi closed 2 weeks ago

ganko-pi commented 3 weeks ago

Type of issue

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 0b23e5f3 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;

///

/// Class containing the entry point of the program. /// 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();
}

}

///

/// Class to manage a OPC UA session. /// 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();
}

}

9. Create a file _OpcUaExample.Config.xml_ and paste the following:
```xml
<?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>
  2. Open the solution UA-.NETStandard/UA Reference.sln
  3. Start the project ConsoleReferenceServer
  4. Start the project OpcUaExample
  5. 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 commented 3 weeks ago

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

ganko-pi commented 3 weeks ago

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