Skip to content

Commit d0e89a3

Browse files
mrsuciumregen
andauthored
Support mutual TLS on server https endpoints (#2849)
A new configuration variable <HttpsMutualTls>true</HttpsMutualTls> enables the mutual TLS authentication support. The behavior of the TLS endpoint changes as the following: HttpsMutualTls is true: The server checks the trust on the certificate which is used by the client for TLS authentication. It must be a valid OPC UA application certificate which is trusted. A client can still connect without providing a client certificate, but then it is only able to call discovery services. In order to create a session, the client must use the same application certificate that was used for the TLS channel. HttpsMutualTls is false: - There is no application authentication. The server endpoint uses security None and there is no client application authentication. Instead, only user authentication is used to secure the server, anonymous user authentication is disabled. Discovery service calls are supported. Co-authored-by: Martin Regen <[email protected]>
1 parent a887f90 commit d0e89a3

File tree

17 files changed

+194
-31
lines changed

17 files changed

+194
-31
lines changed

Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
<!-- <ua:SecurityPolicyUri>http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256</ua:SecurityPolicyUri> -->
155155
</ua:UserTokenPolicy>
156156
</UserTokenPolicies>
157+
157158
<DiagnosticsEnabled>true</DiagnosticsEnabled>
158159
<!-- Settings for CTT testing -->
159160
<MaxSessionCount>75</MaxSessionCount>
@@ -265,6 +266,7 @@
265266
</OperationLimits>
266267

267268
<AuditingEnabled>true</AuditingEnabled>
269+
<HttpsMutualTls>true</HttpsMutualTls>
268270
</ServerConfiguration>
269271

270272
<Extensions>

Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public partial class ReferenceServer : ReverseConnectServer
5353
public ITokenValidator TokenValidator { get; set; }
5454

5555
#endregion
56+
5657
#region Overridden Methods
5758
/// <summary>
5859
/// Creates the node managers for the server.
@@ -247,7 +248,7 @@ private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArg
247248
{
248249
VerifyUserTokenCertificate(x509Token.Certificate);
249250
// set AuthenticatedUser role for accepted certificate authentication
250-
args.Identity = new RoleBasedIdentity(new UserIdentity(x509Token),
251+
args.Identity = new RoleBasedIdentity(new UserIdentity(x509Token),
251252
new List<Role>() { Role.AuthenticatedUser });
252253
Utils.LogInfo(Utils.TraceMasks.Security, "X509 Token Accepted: {0}", args.Identity?.DisplayName);
253254

@@ -325,7 +326,7 @@ private IUserIdentity VerifyPassword(UserNameIdentityToken userNameToken)
325326
new LocalizedText(info)));
326327
}
327328
return new RoleBasedIdentity(new UserIdentity(userNameToken),
328-
new List<Role>() { Role.AuthenticatedUser});
329+
new List<Role>() { Role.AuthenticatedUser });
329330
}
330331

331332
/// <summary>

Libraries/Opc.Ua.Client/CoreClientUtils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ public static EndpointDescription SelectEndpoint(
307307
public static Uri GetDiscoveryUrl(string discoveryUrl)
308308
{
309309
// needs to add the '/discovery' back onto non-UA TCP URLs.
310-
if (discoveryUrl.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal))
310+
if (Utils.IsUriHttpRelatedScheme(discoveryUrl))
311311
{
312312
if (!discoveryUrl.EndsWith(ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase))
313313
{

Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,13 @@ public IApplicationConfigurationBuilderServerOptions SetAuditingEnabled(bool aud
779779
return this;
780780
}
781781

782+
/// <inheritdoc/>
783+
public IApplicationConfigurationBuilderServerOptions SetHttpsMutualTls(bool mutualTlsEnabeld)
784+
{
785+
ApplicationConfiguration.ServerConfiguration.HttpsMutualTls = mutualTlsEnabeld;
786+
return this;
787+
}
788+
782789
/// <inheritdoc/>
783790
public IApplicationConfigurationBuilderClientOptions SetDefaultSessionTimeout(int defaultSessionTimeout)
784791
{

Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ public interface IApplicationConfigurationBuilderServerOptions :
265265

266266
/// <inheritdoc cref="ServerConfiguration.AuditingEnabled"/>
267267
IApplicationConfigurationBuilderServerOptions SetAuditingEnabled(bool auditingEnabled);
268+
269+
/// <inheritdoc cref="ServerConfiguration.HttpsMutualTls"/>
270+
IApplicationConfigurationBuilderServerOptions SetHttpsMutualTls(bool mTlsEnabled);
268271
}
269272

270273
/// <summary>

Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1313

1414
using System;
1515
using System.Collections.Generic;
16+
using System.Linq;
1617
using System.Security.Cryptography.X509Certificates;
1718

1819

@@ -92,30 +93,44 @@ X509Certificate2Collection instanceCertificateChain
9293

9394
uris.Add(uri.Uri);
9495

95-
// Only support one policy with HTTPS
96-
// So pick the first policy with security mode sign and encrypt
9796
ServerSecurityPolicy bestPolicy = null;
98-
foreach (ServerSecurityPolicy policy in securityPolicies)
97+
bool httpsMutualTls = configuration.ServerConfiguration.HttpsMutualTls;
98+
if (!httpsMutualTls)
9999
{
100-
if (policy.SecurityMode != MessageSecurityMode.SignAndEncrypt)
100+
// Only use security None without mutual TLS authentication!
101+
// When the mutual TLS authentication is not used, anonymous access is disabled
102+
// Then the only protection against unauthorized access is user authorization
103+
bestPolicy = new ServerSecurityPolicy() {
104+
SecurityMode = MessageSecurityMode.None,
105+
SecurityPolicyUri = SecurityPolicies.None
106+
};
107+
}
108+
else
109+
{
110+
// Only support one secure policy with HTTPS and mutual authentication
111+
// So pick the first policy with security mode sign and encrypt
112+
foreach (ServerSecurityPolicy policy in securityPolicies)
101113
{
102-
continue;
103-
}
114+
if (policy.SecurityMode != MessageSecurityMode.SignAndEncrypt)
115+
{
116+
continue;
117+
}
104118

105-
bestPolicy = policy;
106-
break;
107-
}
119+
bestPolicy = policy;
120+
break;
121+
}
108122

109-
// Pick the first policy from the list if no policies with sign and encrypt defined
110-
if (bestPolicy == null)
111-
{
112-
bestPolicy = securityPolicies[0];
123+
// Pick the first policy from the list if no policies with sign and encrypt defined
124+
if (bestPolicy == null)
125+
{
126+
bestPolicy = securityPolicies[0];
127+
}
113128
}
114129

115-
EndpointDescription description = new EndpointDescription();
116-
117-
description.EndpointUrl = uri.ToString();
118-
description.Server = serverDescription;
130+
var description = new EndpointDescription {
131+
EndpointUrl = uri.ToString(),
132+
Server = serverDescription
133+
};
119134

120135
if (instanceCertificate != null)
121136
{
@@ -142,6 +157,12 @@ X509Certificate2Collection instanceCertificateChain
142157
description.UserIdentityTokens = serverBase.GetUserTokenPolicies(configuration, description);
143158
description.TransportProfileUri = Profiles.HttpsBinaryTransport;
144159

160+
// if no mutual TLS authentication is used, anonymous user tokens are not allowed
161+
if (!httpsMutualTls)
162+
{
163+
description.UserIdentityTokens = new UserTokenPolicyCollection(description.UserIdentityTokens.Where(token => token.TokenType != UserTokenType.Anonymous));
164+
}
165+
145166
ITransportListener listener = Create();
146167
if (listener != null)
147168
{

Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1414
using System.Collections.Generic;
1515
using System.IO;
1616
using System.Net;
17+
using System.Net.Security;
1718
using System.Security.Authentication;
1819
using System.Security.Cryptography;
1920
using System.Security.Cryptography.X509Certificates;
@@ -24,6 +25,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
2425
using Microsoft.AspNetCore.Http;
2526
using Microsoft.AspNetCore.Server.Kestrel.Https;
2627
using Microsoft.Extensions.Hosting;
28+
using Opc.Ua.Security.Certificates;
2729

2830

2931
namespace Opc.Ua.Bindings
@@ -174,6 +176,7 @@ public void Open(
174176
m_listenerId = Guid.NewGuid().ToString();
175177

176178
m_uri = baseAddress;
179+
m_discovery = m_uri.AbsolutePath?.TrimEnd('/') + ConfiguredEndpoint.DiscoverySuffix;
177180
m_descriptions = settings.Descriptions;
178181
var configuration = settings.Configuration;
179182

@@ -205,6 +208,7 @@ public void Open(
205208
m_serverCertificate = settings.ServerCertificate;
206209
m_serverCertificateChain = settings.ServerCertificateChain;
207210

211+
m_mutualTlsEnabled = settings.HttpsMutualTls;
208212
// start the listener
209213
Start();
210214
}
@@ -283,9 +287,10 @@ public void Start()
283287

284288
var httpsOptions = new HttpsConnectionAdapterOptions() {
285289
CheckCertificateRevocation = false,
286-
ClientCertificateMode = ClientCertificateMode.NoCertificate,
290+
ClientCertificateMode = m_mutualTlsEnabled ? ClientCertificateMode.AllowCertificate : ClientCertificateMode.NoCertificate,
287291
// note: this is the TLS certificate!
288292
ServerCertificate = serverCertificate,
293+
ClientCertificateValidation = ValidateClientCertificate,
289294
};
290295

291296
#if NET462
@@ -370,6 +375,21 @@ public async Task SendAsync(HttpContext context)
370375

371376
IServiceRequest input = (IServiceRequest)BinaryDecoder.DecodeMessage(buffer, null, m_quotas.MessageContext);
372377

378+
if (m_mutualTlsEnabled && input.TypeId == DataTypeIds.CreateSessionRequest)
379+
{
380+
// Match tls client certificate against client application certificate provided in CreateSessionRequest
381+
byte[] tlsClientCertificate = context.Connection.ClientCertificate?.RawData;
382+
byte[] opcUaClientCertificate = ((CreateSessionRequest)input).ClientCertificate;
383+
384+
if (tlsClientCertificate == null || !Utils.IsEqual(tlsClientCertificate, opcUaClientCertificate))
385+
{
386+
message = "Client TLS certificate does not match with ClientCertificate provided in CreateSessionRequest";
387+
Utils.LogError(message);
388+
await WriteResponseAsync(context.Response, message, HttpStatusCode.Unauthorized).ConfigureAwait(false);
389+
return;
390+
}
391+
}
392+
373393
// extract the JWT token from the HTTP headers.
374394
if (input.RequestHeader == null)
375395
{
@@ -463,6 +483,8 @@ public async Task SendAsync(HttpContext context)
463483
await WriteResponseAsync(context.Response, message, HttpStatusCode.InternalServerError).ConfigureAwait(false);
464484
}
465485

486+
487+
466488
/// <summary>
467489
/// Called when a UpdateCertificate event occured.
468490
/// </summary>
@@ -524,11 +546,41 @@ private static async Task<byte[]> ReadBodyAsync(HttpRequest req)
524546
return memory.ToArray();
525547
}
526548
}
549+
550+
/// <summary>
551+
/// Validate TLS client certificate at TLS handshake.
552+
/// </summary>
553+
/// <param name="clientCertificate">Client certificate</param>
554+
/// <param name="chain">Certificate chain</param>
555+
/// <param name="sslPolicyErrors">SSl policy errors</param>
556+
private bool ValidateClientCertificate(
557+
X509Certificate2 clientCertificate,
558+
X509Chain chain,
559+
SslPolicyErrors sslPolicyErrors)
560+
{
561+
if (sslPolicyErrors == SslPolicyErrors.None)
562+
{
563+
// certificate is valid
564+
return true;
565+
}
566+
567+
try
568+
{
569+
m_quotas.CertificateValidator.Validate(clientCertificate);
570+
}
571+
catch (Exception)
572+
{
573+
return false;
574+
}
575+
576+
return true;
577+
}
527578
#endregion
528579

529580
#region Private Fields
530581
private string m_listenerId;
531582
private Uri m_uri;
583+
private string m_discovery;
532584
private readonly string m_uriScheme;
533585
private EndpointDescriptionCollection m_descriptions;
534586
private ChannelQuotas m_quotas;
@@ -537,6 +589,7 @@ private static async Task<byte[]> ReadBodyAsync(HttpRequest req)
537589
private IWebHost m_host;
538590
private X509Certificate2 m_serverCertificate;
539591
private X509Certificate2Collection m_serverCertificateChain;
592+
private bool m_mutualTlsEnabled;
540593
#endregion
541594
}
542595
}

Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,7 @@ private void Initialize()
15091509
m_maxTrustListSize = 0;
15101510
m_multicastDnsEnabled = false;
15111511
m_auditingEnabled = false;
1512+
m_httpsMutualTls = true;
15121513
}
15131514

15141515
/// <summary>
@@ -1942,6 +1943,17 @@ public bool AuditingEnabled
19421943
get { return m_auditingEnabled; }
19431944
set { m_auditingEnabled = value; }
19441945
}
1946+
1947+
/// <summary>
1948+
/// Whether mTLS is required/enforced by the HttpsTransportListener
1949+
/// </summary>
1950+
/// <value><c>true</c> if mutual TLS is enabled; otherwise, <c>false</c>.</value>
1951+
[DataMember(IsRequired = false, Order = 38)]
1952+
public bool HttpsMutualTls
1953+
{
1954+
get { return m_httpsMutualTls; }
1955+
set { m_httpsMutualTls = value; }
1956+
}
19451957
#endregion
19461958

19471959
#region Private Members
@@ -1980,6 +1992,7 @@ public bool AuditingEnabled
19801992
private ReverseConnectServerConfiguration m_reverseConnect;
19811993
private OperationLimits m_operationLimits;
19821994
private bool m_auditingEnabled;
1995+
private bool m_httpsMutualTls;
19831996
#endregion
19841997
}
19851998
#endregion

Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
<xs:element name="ReverseConnect" type="ReverseConnectServerConfiguration" minOccurs="0" />
169169
<xs:element name="OperationLimits" type="OperationLimits" minOccurs="0" />
170170
<xs:element name="AuditingEnabled" type="xs:boolean" minOccurs="0" />
171+
<xs:element name="HttpsMutualTls" type="xs:boolean" minOccurs="0" />
171172
</xs:sequence>
172173
</xs:extension>
173174
</xs:complexContent>

Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1515
using System.Globalization;
1616
using System.IO;
1717
using System.Runtime.Serialization;
18+
using System.Security.Cryptography.X509Certificates;
1819
using System.Threading;
1920
using System.Threading.Tasks;
2021
using System.Xml;
@@ -130,7 +131,7 @@ public static ConfiguredEndpointCollection Load(string filePath)
130131
{
131132
string discoveryUrl = endpoint.Description.EndpointUrl;
132133

133-
if (discoveryUrl.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal))
134+
if (Utils.IsUriHttpRelatedScheme(discoveryUrl))
134135
{
135136
discoveryUrl += ConfiguredEndpoint.DiscoverySuffix;
136137
}
@@ -530,7 +531,7 @@ public void SetApplicationDescription(string serverUri, ApplicationDescription s
530531
}
531532

532533
if (endpointUrl != null &&
533-
endpointUrl.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) &&
534+
Utils.IsUriHttpRelatedScheme(endpointUrl) &&
534535
endpointUrl.EndsWith(ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase))
535536
{
536537
endpointUrl = endpointUrl.Substring(0, endpointUrl.Length - ConfiguredEndpoint.DiscoverySuffix.Length);
@@ -815,7 +816,7 @@ public ConfiguredEndpoint(
815816

816817
if (baseUrl != null)
817818
{
818-
if (baseUrl.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) &&
819+
if (Utils.IsUriHttpRelatedScheme(baseUrl) &&
819820
baseUrl.EndsWith(DiscoverySuffix, StringComparison.Ordinal))
820821
{
821822
baseUrl = baseUrl.Substring(0, baseUrl.Length - DiscoverySuffix.Length);
@@ -1208,7 +1209,7 @@ public Uri GetDiscoveryUrl(Uri endpointUrl)
12081209
// attempt to construct a discovery url by appending 'discovery' to the endpoint.
12091210
if (discoveryUrls == null || discoveryUrls.Count == 0)
12101211
{
1211-
if (endpointUrl.Scheme.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal))
1212+
if (Utils.IsUriHttpRelatedScheme(endpointUrl.Scheme))
12121213
{
12131214
return new Uri(Utils.Format("{0}{1}", endpointUrl, DiscoverySuffix));
12141215
}

0 commit comments

Comments
 (0)