diff --git a/Applications/ConsoleReferenceClient/ClientSamples.cs b/Applications/ConsoleReferenceClient/ClientSamples.cs index efc833da47..af3bf080c0 100644 --- a/Applications/ConsoleReferenceClient/ClientSamples.cs +++ b/Applications/ConsoleReferenceClient/ClientSamples.cs @@ -857,7 +857,7 @@ await uaClient result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); m_logger.LogInformation( - "ManagedBrowseFullAddressSpace found {Count} references on server in {Duration}ms.", + "ManagedBrowseFullAddressSpace found {Count} references on server in {Duration} ms.", result.Count, stopWatch.ElapsedMilliseconds); @@ -1078,7 +1078,7 @@ BrowseDescriptionCollection browseDescriptionCollection result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); m_logger.LogInformation( - "BrowseFullAddressSpace found {Count} references on server in {Duration}ms.", + "BrowseFullAddressSpace found {Count} references on server in {Duration} ms.", referenceDescriptions.Count, stopWatch.ElapsedMilliseconds); diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index a6f2503138..158c6a601e 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -166,6 +166,38 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1 + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_ChaChaPoly + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_ChaChaPoly + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm + None_1 http://opcfoundation.org/UA/SecurityPolicy#None diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index b56b6e90cd..7d57ca3b83 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -327,7 +327,7 @@ private void ValidateServerNonce( if (!Nonce.ValidateNonce( serverNonce, MessageSecurityMode.SignAndEncrypt, - (uint)m_configuration.SecurityConfiguration.NonceLength)) + m_configuration.SecurityConfiguration.NonceLength)) { if (channelSecurityMode == MessageSecurityMode.SignAndEncrypt || m_configuration.SecurityConfiguration.SuppressNonceValidationErrors) @@ -893,9 +893,11 @@ public virtual void Restore(SessionState state) /// public void Snapshot(out SessionConfiguration sessionConfiguration) { - var serverNonce = Nonce.CreateNonce( - m_endpoint.Description?.SecurityPolicyUri, - m_serverNonce); + byte[]? serverNonce = m_serverNonce != null ? [.. m_serverNonce] : null; + byte[]? clientNonce = m_clientNonce != null ? [.. m_clientNonce] : null; + byte[]? serverEccEphemeralKey = m_eccServerEphemeralKey?.Data != null + ? [.. m_eccServerEphemeralKey.Data] + : null; sessionConfiguration = new SessionConfiguration { SessionName = SessionName, @@ -905,7 +907,8 @@ public void Snapshot(out SessionConfiguration sessionConfiguration) ConfiguredEndpoint = ConfiguredEndpoint, CheckDomain = CheckDomain, ServerNonce = serverNonce, - ServerEccEphemeralKey = m_eccServerEphemeralKey, + ClientNonce = clientNonce, + ServerEccEphemeralKey = serverEccEphemeralKey, UserIdentityTokenPolicy = m_userTokenSecurityPolicyUri }; } @@ -922,9 +925,27 @@ public void Restore(SessionConfiguration sessionConfiguration) : null; m_identity = sessionConfiguration.Identity ?? new UserIdentity(); m_checkDomain = sessionConfiguration.CheckDomain; - m_serverNonce = sessionConfiguration.ServerNonce?.Data; + m_serverNonce = sessionConfiguration.ServerNonce != null + ? [.. sessionConfiguration.ServerNonce] + : null; + m_clientNonce = sessionConfiguration.ClientNonce != null + ? [.. sessionConfiguration.ClientNonce] + : null; m_userTokenSecurityPolicyUri = sessionConfiguration.UserIdentityTokenPolicy; - m_eccServerEphemeralKey = sessionConfiguration.ServerEccEphemeralKey; + if (sessionConfiguration.ServerEccEphemeralKey?.Length > 0) + { + string? ephemeralKeyPolicyUri = !string.IsNullOrEmpty(m_userTokenSecurityPolicyUri) + ? m_userTokenSecurityPolicyUri + : m_endpoint.Description?.SecurityPolicyUri ?? SecurityPolicies.None; + SecurityPolicyInfo ephemeralKeyPolicy = SecurityPolicies.GetInfo(ephemeralKeyPolicyUri); + m_eccServerEphemeralKey = Nonce.CreateNonce( + ephemeralKeyPolicy, + sessionConfiguration.ServerEccEphemeralKey); + } + else + { + m_eccServerEphemeralKey = null; + } lock (m_lock) { @@ -1101,7 +1122,7 @@ public async Task OpenAsync( out bool requireEncryption); // validate the server certificate /certificate chain. - IUserIdentityTokenHandler identityToken = identity.TokenHandler; + using IUserIdentityTokenHandler identityToken = identity.TokenHandler.Copy(); X509Certificate2? serverCertificate = null; byte[]? certificateData = m_endpoint.Description.ServerCertificate; @@ -1139,8 +1160,8 @@ await m_configuration } // create a nonce. - uint length = (uint)m_configuration.SecurityConfiguration.NonceLength; - byte[] clientNonce = Nonce.CreateRandomNonceData(length); + int length = m_configuration.SecurityConfiguration.NonceLength; + m_clientNonce = Nonce.CreateRandomNonceData(length); // send the application instance certificate for the client. BuildCertificateData( @@ -1168,10 +1189,10 @@ await m_configuration bool successCreateSession = false; CreateSessionResponse? response = null; - //if security none, first try to connect without certificate + // if security none, first try to connect without certificate if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) { - //first try to connect with client certificate NULL + // first try to connect with client certificate NULL try { response = await base.CreateSessionAsync( @@ -1180,7 +1201,7 @@ await m_configuration m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl.ToString(), sessionName, - clientNonce, + m_clientNonce, null, sessionTimeout, maxMessageSize, @@ -1203,7 +1224,7 @@ await m_configuration m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl.ToString(), sessionName, - clientNonce, + m_clientNonce, clientCertificateChainData ?? clientCertificateData, sessionTimeout, maxMessageSize, @@ -1252,52 +1273,76 @@ await m_configuration serverSignature, clientCertificateData, clientCertificateChainData, - clientNonce); + m_clientNonce, + serverNonce); // process additional header ProcessResponseAdditionalHeader(response.ResponseHeader, serverCertificate); // create the client signature. - byte[] dataToSign = Utils.Append(serverCertificate?.RawData, serverNonce); - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + // create the client signature. + byte[] dataToSign = securityPolicy.GetClientSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( securityPolicyUri, + m_instanceCertificate, dataToSign); // select the security policy for the user token. - string? tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; - - if (string.IsNullOrEmpty(tokenSecurityPolicyUri)) - { - tokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; - } - - // save previous nonce - byte[]? previousServerNonce = GetCurrentTokenServerNonce(); + string? tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + ? m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None + : identityPolicy.SecurityPolicyUri; // validate server nonce and security parameters for user identity. ValidateServerNonce( identity, serverNonce, tokenSecurityPolicyUri, - previousServerNonce, + m_previousServerNonce, m_endpoint.Description.SecurityMode); - // sign data with user token. - SignatureData userTokenSignature = identityToken.Sign( - dataToSign, - tokenSecurityPolicyUri); + // remember the policy actually used to encrypt the user token + m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; - // encrypt token. - identityToken.Encrypt( - serverCertificate, - serverNonce, - m_userTokenSecurityPolicyUri, - MessageContext, - m_eccServerEphemeralKey, - m_instanceCertificate, - m_instanceCertificateChain, - m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + SignatureData? userTokenSignature = null; + + if (identityToken.Token is X509IdentityToken) + { + // sign data with user token. + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + userTokenSignature = identityToken.Sign( + dataToSign, + tokenSecurityPolicyUri); + } + else + { + // encrypt token. + identityToken.Encrypt( + serverCertificate, + serverNonce, + tokenSecurityPolicyUri, + MessageContext, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + } // copy the preferred locales if provided. if (preferredLocales != null && preferredLocales.Count > 0) @@ -1305,15 +1350,18 @@ await m_configuration m_preferredLocales = [.. preferredLocales]; } + var header = CreateRequestHeaderForActivateSession(securityPolicy, tokenSecurityPolicyUri!); + // activate session. ActivateSessionResponse activateResponse = await ActivateSessionAsync( - null, + header, clientSignature, [], m_preferredLocales, new ExtensionObject(identityToken.Token), userTokenSignature, - ct).ConfigureAwait(false); + ct) + .ConfigureAwait(false); // process additional header ProcessResponseAdditionalHeader(activateResponse.ResponseHeader, serverCertificate); @@ -1342,7 +1390,7 @@ await m_configuration // save nonces. m_sessionName = sessionName; m_identity = identity; - m_previousServerNonce = previousServerNonce; + m_previousServerNonce = m_serverNonce; m_serverNonce = serverNonce; m_serverCertificate = serverCertificate; @@ -1428,12 +1476,20 @@ public async Task UpdateSessionAsync( // get the identity token. string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); // create the client signature. - byte[] dataToSign = Utils.Append(m_serverCertificate?.RawData, serverNonce); - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, + byte[] dataToSign = securityPolicy.GetClientSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( securityPolicyUri, + m_instanceCertificate, dataToSign); // choose a default token. @@ -1442,20 +1498,30 @@ public async Task UpdateSessionAsync( // check that the user identity is supported by the endpoint. UserTokenPolicy identityPolicy = m_endpoint.Description.FindUserTokenPolicy( - identity.TokenType, - identity.IssuedTokenType, - securityPolicyUri) - ?? throw ServiceResultException.Create( - StatusCodes.BadIdentityTokenRejected, - "Endpoint does not support the user identity type provided."); + identity.TokenHandler.Token.PolicyId, + securityPolicyUri); - // select the security policy for the user token. - string? tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; - if (string.IsNullOrEmpty(tokenSecurityPolicyUri)) + if (identityPolicy == null) { - tokenSecurityPolicyUri = securityPolicyUri; + identityPolicy = + m_endpoint.Description.FindUserTokenPolicy( + identity.TokenType, + identity.IssuedTokenType, + securityPolicyUri); + + if (identityPolicy == null) + { + throw ServiceResultException.Create( + StatusCodes.BadIdentityTokenRejected, + "Endpoint does not support the user identity type provided."); + } } + // select the security policy for the user token. + string? tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + ? securityPolicyUri + : identityPolicy.SecurityPolicyUri; + bool requireEncryption = tokenSecurityPolicyUri != SecurityPolicies.None; // validate the server certificate before encrypting tokens. @@ -1476,28 +1542,49 @@ await m_configuration.CertificateValidator.ValidateAsync( m_previousServerNonce, m_endpoint.Description.SecurityMode); - // sign data with user token. - using var identityToken = (IUserIdentityTokenHandler)identity.TokenHandler.Clone(); + // sign/encrypt with a disposable token handler copy to avoid mutating stored credentials. + using IUserIdentityTokenHandler identityToken = identity.TokenHandler.Copy(); identityToken.UpdatePolicy(identityPolicy); - SignatureData userTokenSignature = identityToken.Sign( - dataToSign, - tokenSecurityPolicyUri); + + SignatureData? userTokenSignature = null; + + if (identityToken.Token is X509IdentityToken) + { + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + userTokenSignature = identityToken.Sign( + dataToSign, + tokenSecurityPolicyUri); + } + else + { + // encrypt token. + identityToken.Encrypt( + m_serverCertificate, + serverNonce, + tokenSecurityPolicyUri, + MessageContext, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + } m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; - // encrypt token. - identityToken.Encrypt( - m_serverCertificate, - serverNonce, - m_userTokenSecurityPolicyUri, - MessageContext, - m_eccServerEphemeralKey, - m_instanceCertificate, - m_instanceCertificateChain, - m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + RequestHeader? requestHeader = CreateRequestHeaderForActivateSession( + securityPolicy, + tokenSecurityPolicyUri!); ActivateSessionResponse response = await ActivateSessionAsync( - null, + requestHeader, clientSignature, [], preferredLocales, @@ -2312,13 +2399,9 @@ public async Task ReconnectAsync( // await LoadInstanceCertificateAsync(true, ct).ConfigureAwait(false); - // create the client signature. - byte[] dataToSign = Utils.Append(m_serverCertificate?.RawData, m_serverNonce); + string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); EndpointDescription endpoint = m_endpoint.Description; - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, - endpoint.SecurityPolicyUri, - dataToSign); // check that the user identity is supported by the endpoint. UserTokenPolicy identityPolicy = endpoint.FindUserTokenPolicy( @@ -2337,11 +2420,9 @@ public async Task ReconnectAsync( } // select the security policy for the user token. - string? tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; - if (string.IsNullOrEmpty(tokenSecurityPolicyUri)) - { - tokenSecurityPolicyUri = endpoint.SecurityPolicyUri; - } + string? tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + ? endpoint.SecurityPolicyUri ?? SecurityPolicies.None + : identityPolicy.SecurityPolicyUri; m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; // validate server nonce and security parameters for user identity. @@ -2352,23 +2433,9 @@ public async Task ReconnectAsync( m_previousServerNonce, m_endpoint.Description.SecurityMode); - // sign data with user token. - using var identityToken = (IUserIdentityTokenHandler)m_identity.TokenHandler.Clone(); + // sign/encrypt with a disposable token handler copy to avoid mutating stored credentials. + using IUserIdentityTokenHandler identityToken = m_identity.TokenHandler.Copy(); identityToken.UpdatePolicy(identityPolicy); - SignatureData userTokenSignature = identityToken.Sign( - dataToSign, - tokenSecurityPolicyUri); - - // encrypt token. - identityToken.Encrypt( - m_serverCertificate, - m_serverNonce, - m_userTokenSecurityPolicyUri, - MessageContext, - m_eccServerEphemeralKey, - m_instanceCertificate, - m_instanceCertificateChain, - m_endpoint.Description.SecurityMode != MessageSecurityMode.None); m_logger.LogInformation("Session REPLACING channel for {SessionId}.", SessionId); @@ -2434,9 +2501,83 @@ public async Task ReconnectAsync( } } + ITransportChannel activeChannel = TransportChannel; + + byte[] channelThumbprint = activeChannel.ChannelThumbprint; + byte[] serverChannelCertificate = activeChannel.ServerChannelCertificate; + byte[] clientChannelCertificate = activeChannel.ClientChannelCertificate; + + // HTTPS channels populate the server TLS certificate lazily after the first request. + // Reconnect signs before that, so use the server certificate from the session state. + if ((serverChannelCertificate == null || serverChannelCertificate.Length == 0) && + m_serverCertificate != null) + { + serverChannelCertificate = m_serverCertificate.RawData; + } + + if ((clientChannelCertificate == null || clientChannelCertificate.Length == 0) && + m_instanceCertificate != null) + { + clientChannelCertificate = m_instanceCertificate.RawData; + } + + // create the client signature. + byte[] dataToSign = securityPolicy.GetClientSignatureData( + channelThumbprint, + m_serverNonce, + m_serverCertificate?.RawData, + serverChannelCertificate, + clientChannelCertificate, + m_clientNonce ?? []); + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( + endpoint.SecurityPolicyUri, + m_instanceCertificate, + dataToSign); + + dataToSign = securityPolicy.GetUserTokenSignatureData( + channelThumbprint, + m_serverNonce, + m_serverCertificate?.RawData, + serverChannelCertificate, + m_instanceCertificate?.RawData, + clientChannelCertificate, + m_clientNonce ?? []); + + SignatureData? userTokenSignature = null; + + if (identityToken.Token is X509IdentityToken) + { + userTokenSignature = identityToken.Sign( + dataToSign, + tokenSecurityPolicyUri); + } + else + { + // encrypt token. + identityToken.Encrypt( + m_serverCertificate, + m_serverNonce, + tokenSecurityPolicyUri, + MessageContext, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + } + m_logger.LogInformation("Session RE-ACTIVATING {SessionId}.", SessionId); - var header = new RequestHeader { TimeoutHint = kReconnectTimeout }; + var header = CreateRequestHeaderForActivateSession( + securityPolicy, + tokenSecurityPolicyUri!); + + if (header == null) + { + header = new RequestHeader(); + } + + header.TimeoutHint = kReconnectTimeout; using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromMilliseconds(kReconnectTimeout / 2)); @@ -3877,36 +4018,48 @@ private void ValidateServerSignature( SignatureData serverSignature, byte[]? clientCertificateData, byte[]? clientCertificateChainData, - byte[] clientNonce) + byte[]? clientNonce, + byte[]? serverNonce) { if (serverSignature == null || serverSignature.Signature == null) { m_logger.LogInformation("Server signature is null or empty."); - - //throw ServiceResultException.Create( - // StatusCodes.BadSecurityChecksFailed, - // "Server signature is null or empty."); + return; } // validate the server's signature. - byte[] dataToSign = Utils.Append(clientCertificateData, clientNonce); + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(m_endpoint.Description.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetServerSignatureData( + TransportChannel.ChannelThumbprint, + clientNonce, + TransportChannel.ServerChannelCertificate, + clientCertificateData, + TransportChannel.ClientChannelCertificate, + serverNonce); - if (!SecurityPolicies.Verify( + if (!SecurityPolicies.VerifySignatureData( + serverSignature, + securityPolicy, serverCertificate, - m_endpoint.Description.SecurityPolicyUri, - dataToSign, - serverSignature)) + dataToSign)) { // validate the signature with complete chain if the check with leaf certificate failed. if (clientCertificateChainData != null) { - dataToSign = Utils.Append(clientCertificateChainData, clientNonce); - - if (!SecurityPolicies.Verify( - serverCertificate, - m_endpoint.Description.SecurityPolicyUri, - dataToSign, - serverSignature)) + dataToSign = securityPolicy.GetServerSignatureData( + TransportChannel.ChannelThumbprint, + clientNonce, + TransportChannel.ServerChannelCertificate, + clientCertificateChainData, + TransportChannel.ClientChannelCertificate, + serverNonce); + + if (!SecurityPolicies.VerifySignatureData( + serverSignature, + securityPolicy, + serverCertificate, + dataToSign)) { throw ServiceResultException.Create( StatusCodes.BadApplicationSignatureInvalid, @@ -4168,15 +4321,6 @@ private static void UpdateDescription( return (result, error); } - /// - /// If available, returns the current nonce or null. - /// - private byte[]? GetCurrentTokenServerNonce() - { - ChannelToken? currentToken = (NullableTransportChannel as ISecureChannel)?.CurrentToken; - return currentToken?.ServerNonce; - } - /// /// Processes the response from a publish request. /// @@ -4768,25 +4912,63 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( string? endpointSecurityPolicyUri) { var requestHeader = new RequestHeader(); - string? userTokenSecurityPolicyUri = identityTokenSecurityPolicyUri; + string userTokenSecurityPolicyUri = identityTokenSecurityPolicyUri ?? string.Empty; if (string.IsNullOrEmpty(userTokenSecurityPolicyUri)) { - userTokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + userTokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; } + m_userTokenSecurityPolicyUri = userTokenSecurityPolicyUri; - if (EccUtils.IsEccPolicy(userTokenSecurityPolicyUri)) + var securityPolicy = SecurityPolicies.GetInfo(userTokenSecurityPolicyUri); + + if (securityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { var parameters = new AdditionalParametersType(); parameters.Parameters.Add( new KeyValuePair { - Key = QualifiedName.From("ECDHPolicyUri"), + Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri), Value = userTokenSecurityPolicyUri }); requestHeader.AdditionalHeader = new ExtensionObject(parameters); + + m_logger.LogWarning("Request EphemeralKey for {Policy}.", userTokenSecurityPolicyUri); + } + + return requestHeader; + } + + private RequestHeader? CreateRequestHeaderForActivateSession( + SecurityPolicyInfo securityPolicy, + string userTokenSecurityPolicyUri) + { + var requestHeader = new RequestHeader(); + var parameters = new AdditionalParametersType(); + + if (!string.IsNullOrEmpty(userTokenSecurityPolicyUri)) + { + var userTokenSecurityPolicy = SecurityPolicies.GetInfo(userTokenSecurityPolicyUri); + + if (userTokenSecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) + { + parameters.Parameters.Add( + new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri), + Value = userTokenSecurityPolicyUri + }); + + m_logger.LogWarning("Requesting new EphmeralKey using {SecurityPolicyUri}.", userTokenSecurityPolicyUri); + } + } + + if (parameters.Parameters.Count == 0) + { + return null; } + requestHeader.AdditionalHeader = new ExtensionObject(parameters); return requestHeader; } @@ -4812,7 +4994,30 @@ protected virtual void ProcessResponseAdditionalHeader( { foreach (KeyValuePair ii in parameters.Parameters) { - if (ii.Key == "ECDHKey") + if (ii.Key == AdditionalParameterNames.Padding) + { + var padding = ii.Value.Value as byte[]; + + if (ii.Value.TypeInfo != TypeInfo.Scalars.ByteString || padding == null) + { + m_logger.LogWarning( + "Server returned invalid message padding. Ignored."); + } + else if (padding.Length > 128) + { + m_logger.LogWarning( + "Server returned a {Size}byte message padding that is too long. Ignored.", + padding.Length); + } + else + { + m_logger.LogWarning("Ignoring Padding with {Length} Bytes", padding.Length); + } + + continue; + } + + if (ii.Key == AdditionalParameterNames.ECDHKey) { if (ii.Value.TypeInfo == TypeInfo.Scalars.StatusCode) { @@ -4828,8 +5033,15 @@ protected virtual void ProcessResponseAdditionalHeader( "Server did not provide a valid ECDHKey. User authentication not possible."); } - if (!EccUtils.Verify( - new ArraySegment(key.PublicKey ?? []), + if (key.PublicKey == null || key.Signature == null) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + "Server did not provide a valid ECDHKey. User authentication not possible."); + } + + if (!CryptoUtils.Verify( + new ArraySegment(key.PublicKey), key.Signature, serverCertificate, m_userTokenSecurityPolicyUri)) @@ -4840,8 +5052,10 @@ protected virtual void ProcessResponseAdditionalHeader( } m_eccServerEphemeralKey = Nonce.CreateNonce( - m_userTokenSecurityPolicyUri, + SecurityPolicies.GetInfo(m_userTokenSecurityPolicyUri), key.PublicKey); + + m_logger.LogWarning("Updating ServerEphemeralKey: {Key} bytes", m_eccServerEphemeralKey.Data?.Length ?? 0); } } } @@ -4923,6 +5137,7 @@ protected virtual void ProcessResponseAdditionalHeader( private readonly NodeCache m_nodeCache; private readonly List m_identityHistory = []; private byte[]? m_serverNonce; + private byte[]? m_clientNonce; private byte[]? m_previousServerNonce; private X509Certificate2? m_serverCertificate; private uint m_publishCounter; diff --git a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs index 69357e3708..bcf0fb779c 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs @@ -123,10 +123,18 @@ public SessionState(SessionOptions options) public NodeId AuthenticationToken { get; init; } /// - /// The last server nonce received. + /// The raw bytes of the last server nonce received. + /// Persisting bytes avoids object-serialization ambiguity for Nonce internals. /// [DataMember(IsRequired = true, Order = 80)] - public Nonce? ServerNonce { get; init; } + public byte[]? ServerNonce { get; init; } + + /// + /// The raw bytes of the client nonce used when the session was created. + /// Required for enhanced-policy activate signatures during reconnect. + /// + [DataMember(IsRequired = false, Order = 85)] + public byte[]? ClientNonce { get; init; } /// /// The user identity token policy which was used to create the session. @@ -135,10 +143,10 @@ public SessionState(SessionOptions options) public string? UserIdentityTokenPolicy { get; init; } /// - /// The last server ecc ephemeral key received. + /// The raw bytes of the last server ECC ephemeral key received. /// [DataMember(IsRequired = false, Order = 100)] - public Nonce? ServerEccEphemeralKey { get; init; } + public byte[]? ServerEccEphemeralKey { get; init; } /// /// Allows the list of subscriptions to be saved/restored diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 667c3073e2..2d3fef8d20 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -868,8 +868,10 @@ await DeleteApplicationInstanceCertificateAsync(configuration, id, ct).Configure else { ECCurve? curve = - EccUtils.GetCurveFromCertificateTypeId(id.CertificateType) - ?? throw ServiceResultException.ConfigurationError("The Ecc certificate type is not supported."); + CryptoUtils.GetCurveFromCertificateTypeId(id.CertificateType) + ?? throw new ServiceResultException( + StatusCodes.BadConfigurationError, + "The Ecc certificate type is not supported."); id.Certificate = builder.SetECCurve(curve.Value).CreateForECDsa(); diff --git a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs index 761155fdd5..6d788f01e4 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs @@ -756,7 +756,7 @@ private static bool TryGetECCCurve(NodeId certificateType, out ECCurve curve) return false; } curve = - EccUtils.GetCurveFromCertificateTypeId(certificateType) + CryptoUtils.GetCurveFromCertificateTypeId(certificateType) ?? throw new ServiceResultException( StatusCodes.BadNotSupported, $"The certificate type {certificateType} is not supported."); diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 55d18f0490..25aff8fbba 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -392,6 +392,7 @@ public void HasApplicationSecureAdminAccess(ISystemContext context) } /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1725:Parameter names should match base declaration", Justification = "")] public void HasApplicationSecureAdminAccess( ISystemContext context, CertificateStoreIdentifier trustedStore) @@ -950,7 +951,7 @@ private X509Certificate2 GenerateTemporaryApplicationCertificate( else { ECCurve? curve = - EccUtils.GetCurveFromCertificateTypeId(certificateTypeId) + CryptoUtils.GetCurveFromCertificateTypeId(certificateTypeId) ?? throw new ServiceResultException( StatusCodes.BadNotSupported, "The Ecc certificate type is not supported."); diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index 5c741d5b59..a6d172371e 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -462,14 +462,9 @@ X509Certificate2Collection clientCertificateChain } } - var parameters = - ExtensionObject.ToEncodeable( - requestHeader.AdditionalHeader) as AdditionalParametersType; - - if (parameters != null) - { - parameters = CreateSessionProcessAdditionalParameters(session, parameters); - } + AdditionalParametersType parameters = CreateSessionProcessAdditionalParameters( + session, + requestHeader.AdditionalHeader); await m_semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); try @@ -494,17 +489,12 @@ X509Certificate2Collection clientCertificateChain serverEndpoints = GetEndpointDescriptions(endpointUrl, BaseAddresses, null); // sign the nonce provided by the client. - serverSignature = null; - - // sign the client nonce (if provided). - if (parsedClientCertificate != null && clientNonce != null) - { - byte[] dataToSign = Utils.Append(parsedClientCertificate.RawData, clientNonce); - serverSignature = SecurityPolicies.Sign( - instanceCertificate, - context.SecurityPolicyUri, - dataToSign); - } + serverSignature = CreateSessionServerSignature( + context, + instanceCertificate, + parsedClientCertificate, + clientNonce, + serverNonce); } finally { @@ -584,7 +574,47 @@ X509Certificate2Collection clientCertificateChain } /// - /// Process additional parameters during the ECC session creation and set the session's UserToken security policy + /// Creates the server signature returned during CreateSession. + /// + /// The request context. + /// The instance certificate used to sign data. + /// The client certificate supplied in the request. + /// The client nonce supplied in the request. + /// The server nonce generated for the response. + /// The server signature or null when signing is not required. + protected virtual SignatureData CreateSessionServerSignature( + OperationContext context, + X509Certificate2 instanceCertificate, + X509Certificate2 parsedClientCertificate, + byte[] clientNonce, + byte[] serverNonce) + { + return SessionSecurityPolicyHelper.CreateServerSignature( + context, + instanceCertificate, + parsedClientCertificate, + clientNonce, + serverNonce); + } + + /// + /// Process additional header values during session creation for ephemeral-key user token policies. + /// + /// The session. + /// The additional request header. + /// An AdditionalParametersType object containing the processed parameters. + protected virtual AdditionalParametersType CreateSessionProcessAdditionalParameters( + ISession session, + ExtensionObject additionalHeader) + { + AdditionalParametersType parameters = SessionSecurityPolicyHelper + .DecodeAdditionalParameters(additionalHeader); + + return CreateSessionProcessAdditionalParameters(session, parameters); + } + + /// + /// Process additional parameters during session creation and set the session's user token security policy. /// /// The session /// The additional parameters for the session @@ -593,47 +623,30 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet ISession session, AdditionalParametersType parameters) { - AdditionalParametersType response = null; - - if (parameters != null && parameters.Parameters != null) - { - response = new AdditionalParametersType(); - - foreach (KeyValuePair ii in parameters.Parameters) - { - if (ii.Key == "ECDHPolicyUri") - { - string policyUri = ii.Value.ToString(); + return SessionSecurityPolicyHelper.ProcessCreateSessionAdditionalParameters( + session, + parameters, + m_logger); + } - if (EccUtils.IsEccPolicy(policyUri)) - { - session.SetEccUserTokenSecurityPolicy(policyUri); - EphemeralKeyType key = session.GetNewEccKey(); - response.Parameters.Add( - new KeyValuePair - { - Key = QualifiedName.From("ECDHKey"), - Value = new ExtensionObject(key) - }); - } - else - { - response.Parameters.Add( - new KeyValuePair - { - Key = QualifiedName.From("ECDHKey"), - Value = StatusCodes.BadSecurityPolicyRejected - }); - } - } - } - } + /// + /// Process additional header values during session activation for ephemeral-key user token policies. + /// + /// The session. + /// The additional request header. + /// An AdditionalParametersType object containing the processed parameters. + protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParameters( + ISession session, + ExtensionObject additionalHeader) + { + AdditionalParametersType parameters = SessionSecurityPolicyHelper + .DecodeAdditionalParameters(additionalHeader); - return response; + return ActivateSessionProcessAdditionalParameters(session, parameters); } /// - /// Process additional parameters during ECC session activation + /// Process additional parameters during session activation for ephemeral-key user token policies. /// /// The session /// The additional parameters for the session @@ -642,21 +655,10 @@ protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParam ISession session, AdditionalParametersType parameters) { - AdditionalParametersType response = null; - - EphemeralKeyType key = session.GetNewEccKey(); - - if (key != null) - { - response = new AdditionalParametersType(); - response.Parameters.Add(new KeyValuePair - { - Key = QualifiedName.From("ECDHKey"), - Value = new ExtensionObject(key) - }); - } - - return response; + return SessionSecurityPolicyHelper.ProcessActivateSessionAdditionalParameters( + session, + parameters, + m_logger); } /// @@ -696,10 +698,10 @@ public override async ValueTask ActivateSessionAsync( ISession session = ServerInternal.SessionManager .GetSession(requestHeader.AuthenticationToken); - var parameters = - ExtensionObject.ToEncodeable( - requestHeader.AdditionalHeader) as AdditionalParametersType; - parameters = ActivateSessionProcessAdditionalParameters(session, parameters); + + AdditionalParametersType parameters = ActivateSessionProcessAdditionalParameters( + session, + requestHeader.AdditionalHeader); m_logger.LogInformation("Server - SESSION ACTIVATED."); @@ -2119,109 +2121,109 @@ await configuration if (m_registrationEndpoints != null) { foreach (ConfiguredEndpoint endpoint in m_registrationEndpoints.Endpoints) - { - RegistrationClient client = null; - int i = 0; - - while (i++ < 2) { - try - { - // update from the server. - bool updateRequired = true; + RegistrationClient client = null; + int i = 0; - lock (m_registrationLock) + while (i++ < 2) + { + try { - updateRequired = endpoint.UpdateBeforeConnect; - } + // update from the server. + bool updateRequired = true; - if (updateRequired) - { - await endpoint.UpdateFromServerAsync(MessageContext.Telemetry, ct).ConfigureAwait(false); - } + lock (m_registrationLock) + { + updateRequired = endpoint.UpdateBeforeConnect; + } - lock (m_registrationLock) - { - endpoint.UpdateBeforeConnect = false; - } + if (updateRequired) + { + await endpoint.UpdateFromServerAsync(MessageContext.Telemetry, ct).ConfigureAwait(false); + } - var requestHeader = new RequestHeader - { - Timestamp = DateTime.UtcNow - }; - - // create the client. - X509Certificate2 instanceCertificate = - InstanceCertificateTypesProvider.GetInstanceCertificate( - endpoint.Description?.SecurityPolicyUri ?? - SecurityPolicies.None); - client = await RegistrationClient.CreateAsync( - configuration, - endpoint.Description, - endpoint.Configuration, - instanceCertificate, - ct: ct).ConfigureAwait(false); - - client.OperationTimeout = 10000; - - // register the server. - if (m_useRegisterServer2) - { - var discoveryConfiguration = new ExtensionObjectCollection(); - var mdnsDiscoveryConfig = new MdnsDiscoveryConfiguration + lock (m_registrationLock) { - ServerCapabilities = configuration.ServerConfiguration - .ServerCapabilities, - MdnsServerName = Utils.GetHostName() + endpoint.UpdateBeforeConnect = false; + } + + var requestHeader = new RequestHeader + { + Timestamp = DateTime.UtcNow }; - var extensionObject = new ExtensionObject(mdnsDiscoveryConfig); - discoveryConfiguration.Add(extensionObject); - await client.RegisterServer2Async( - requestHeader, - m_registrationInfo, - discoveryConfiguration, - ct).ConfigureAwait(false); + + // create the client. + X509Certificate2 instanceCertificate = + InstanceCertificateTypesProvider.GetInstanceCertificate( + endpoint.Description?.SecurityPolicyUri ?? + SecurityPolicies.None); + client = await RegistrationClient.CreateAsync( + configuration, + endpoint.Description, + endpoint.Configuration, + instanceCertificate, + ct: ct).ConfigureAwait(false); + + client.OperationTimeout = 10000; + + // register the server. + if (m_useRegisterServer2) + { + var discoveryConfiguration = new ExtensionObjectCollection(); + var mdnsDiscoveryConfig = new MdnsDiscoveryConfiguration + { + ServerCapabilities = configuration.ServerConfiguration + .ServerCapabilities, + MdnsServerName = Utils.GetHostName() + }; + var extensionObject = new ExtensionObject(mdnsDiscoveryConfig); + discoveryConfiguration.Add(extensionObject); + await client.RegisterServer2Async( + requestHeader, + m_registrationInfo, + discoveryConfiguration, + ct).ConfigureAwait(false); + } + else + { + await client.RegisterServerAsync( + requestHeader, + m_registrationInfo, + ct) + .ConfigureAwait(false); + } + + m_registeredWithDiscoveryServer = m_registrationInfo.IsOnline; + return true; } - else + catch (Exception e) { - await client.RegisterServerAsync( - requestHeader, - m_registrationInfo, - ct) - .ConfigureAwait(false); + m_logger.LogWarning( + "RegisterServer{Api} failed for {EndpointUrl}. Exception={ErrorMessage}", + m_useRegisterServer2 ? "2" : string.Empty, + endpoint.EndpointUrl, + e.Message); + m_useRegisterServer2 = !m_useRegisterServer2; } - - m_registeredWithDiscoveryServer = m_registrationInfo.IsOnline; - return true; - } - catch (Exception e) - { - m_logger.LogWarning( - "RegisterServer{Api} failed for {EndpointUrl}. Exception={ErrorMessage}", - m_useRegisterServer2 ? "2" : string.Empty, - endpoint.EndpointUrl, - e.Message); - m_useRegisterServer2 = !m_useRegisterServer2; - } - finally - { - if (client != null) + finally { - try + if (client != null) { - await client.CloseAsync(ct).ConfigureAwait(false); - client = null; - } - catch (Exception e) - { - m_logger.LogWarning( - "Could not cleanly close connection with LDS. Exception={ErrorMessage}", - e.Message); + try + { + await client.CloseAsync(ct).ConfigureAwait(false); + client = null; + } + catch (Exception e) + { + m_logger.LogWarning( + "Could not cleanly close connection with LDS. Exception={ErrorMessage}", + e.Message); + } } } } } - } // retry to start with RegisterServer2 if both failed m_useRegisterServer2 = true; } diff --git a/Libraries/Opc.Ua.Server/Session/ISession.cs b/Libraries/Opc.Ua.Server/Session/ISession.cs index b8a0bd07f1..91d47cbd7d 100644 --- a/Libraries/Opc.Ua.Server/Session/ISession.cs +++ b/Libraries/Opc.Ua.Server/Session/ISession.cs @@ -42,6 +42,11 @@ public interface ISession : IDisposable /// bool Activated { get; } + /// + /// The server application instance certificate used by this session. + /// + X509Certificate2 ServerCertificate { get; } + /// /// The application instance certificate associated with the client. /// @@ -127,7 +132,7 @@ bool Activate( /// Create new ECC ephemeral key /// /// A new ephemeral key - EphemeralKeyType GetNewEccKey(); + EphemeralKeyType GetNewEphemeralKey(); /// /// Checks if the secure channel is currently valid. @@ -171,7 +176,7 @@ bool Activate( /// /// Set the ECC security policy URI /// - void SetEccUserTokenSecurityPolicy(string securityPolicyUri); + void SetUserTokenSecurityPolicy(string securityPolicyUri); /// /// Updates the requested locale ids. diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index 51e0e3576a..d7d48a5313 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -95,6 +95,7 @@ public Session( m_clientIssuerCertificates = clientCertificateChain; SecureChannelId = context.ChannelContext.SecureChannelId; + m_channelThumbprint = context.ChannelContext.ChannelThumbprint; MaxBrowseContinuationPoints = maxBrowseContinuationPoints; m_maxHistoryContinuationPoints = maxHistoryContinuationPoints; EndpointDescription = context.ChannelContext.EndpointDescription; @@ -241,6 +242,11 @@ protected virtual void Dispose(bool disposing) /// public byte[] ClientNonce { get; } + /// + /// The server application instance certificate used by this session. + /// + public X509Certificate2 ServerCertificate => m_serverCertificate; + /// /// The application instance certificate associated with the client. /// @@ -289,12 +295,12 @@ public DateTime ClientLastContactTime /// /// Set the ECC security policy URI /// - public virtual void SetEccUserTokenSecurityPolicy(string securityPolicyUri) + public virtual void SetUserTokenSecurityPolicy(string securityPolicyUri) { lock (m_lock) { - m_eccUserTokenSecurityPolicyUri = securityPolicyUri; - m_eccUserTokenNonce = null; + m_userTokenSecurityPolicyUri = securityPolicyUri; + m_userTokenNonce = null; } } @@ -302,23 +308,23 @@ public virtual void SetEccUserTokenSecurityPolicy(string securityPolicyUri) /// Create new ECC ephemeral key /// /// A new ephemeral key - public virtual EphemeralKeyType GetNewEccKey() + public virtual EphemeralKeyType GetNewEphemeralKey() { lock (m_lock) { - if (m_eccUserTokenSecurityPolicyUri == null) + if (m_userTokenSecurityPolicyUri == null) { return null; } - m_eccUserTokenNonce = Nonce.CreateNonce(m_eccUserTokenSecurityPolicyUri); + m_userTokenNonce = Nonce.CreateNonce(m_userTokenSecurityPolicyUri); - var key = new EphemeralKeyType { PublicKey = m_eccUserTokenNonce.Data }; + var key = new EphemeralKeyType { PublicKey = m_userTokenNonce.Data }; - key.Signature = EccUtils.Sign( + key.Signature = CryptoUtils.Sign( new ArraySegment(key.PublicKey), m_serverCertificate, - m_eccUserTokenSecurityPolicyUri); + m_userTokenSecurityPolicyUri); return key; } @@ -474,15 +480,21 @@ public void ValidateBeforeActivate( StatusCodes.BadApplicationSignatureInvalid); } - byte[] dataToSign = Utils.Append( + var securityPolicy = SecurityPolicies.GetInfo(EndpointDescription.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetClientSignatureData( + context.ChannelContext.ChannelThumbprint, + m_serverNonce.Data, m_serverCertificate.RawData, - m_serverNonce.Data); + context.ChannelContext.ServerChannelCertificate, + context.ChannelContext.ClientChannelCertificate, + ClientNonce); - if (!SecurityPolicies.Verify( - ClientCertificate, + if (!SecurityPolicies.VerifySignatureData( + clientSignature, EndpointDescription.SecurityPolicyUri, - dataToSign, - clientSignature)) + ClientCertificate, + dataToSign)) { // verify for certificate chain in endpoint. // validate the signature with complete chain if the check with leaf certificate failed. @@ -503,15 +515,19 @@ public void ValidateBeforeActivate( byte[] serverCertificateChainData = [.. serverCertificateChainList]; - dataToSign = Utils.Append( + dataToSign = securityPolicy.GetClientSignatureData( + context.ChannelContext.ChannelThumbprint, + m_serverNonce.Data, serverCertificateChainData, - m_serverNonce.Data); - - if (!SecurityPolicies.Verify( - ClientCertificate, - EndpointDescription.SecurityPolicyUri, - dataToSign, - clientSignature)) + context.ChannelContext.ServerChannelCertificate, + context.ChannelContext.ClientChannelCertificate, + ClientNonce); + + if (!SecurityPolicies.VerifySignatureData( + clientSignature, + EndpointDescription.SecurityPolicyUri, + ClientCertificate, + dataToSign)) { throw new ServiceResultException( StatusCodes.BadApplicationSignatureInvalid); @@ -536,12 +552,14 @@ public void ValidateBeforeActivate( // validate the user identity token. identityToken = ValidateUserIdentityToken( + context, userIdentityToken, userTokenSignature, out userTokenPolicy); TraceState("VALIDATED"); } + } /// @@ -825,6 +843,7 @@ private ServiceResult OnUpdateSecurityDiagnostics( /// /// private IUserIdentityTokenHandler ValidateUserIdentityToken( + OperationContext context, ExtensionObject identityToken, SignatureData userTokenSignature, out UserTokenPolicy policy) @@ -997,7 +1016,7 @@ private IUserIdentityTokenHandler ValidateUserIdentityToken( m_serverNonce, securityPolicyUri, m_server.MessageContext, - m_eccUserTokenNonce, + m_userTokenNonce, ClientCertificate, m_clientIssuerCertificates); } @@ -1012,9 +1031,16 @@ private IUserIdentityTokenHandler ValidateUserIdentityToken( // verify the signature. if (securityPolicyUri != SecurityPolicies.None) { - byte[] dataToSign = Utils.Append( + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + byte[] dataToSign = securityPolicy.GetUserTokenSignatureData( + context.ChannelContext.ChannelThumbprint, + m_serverNonce.Data, m_serverCertificate.RawData, - m_serverNonce.Data); + context.ChannelContext.ServerChannelCertificate, + ClientCertificate?.RawData, + context.ChannelContext.ClientChannelCertificate, + ClientNonce ?? []); if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri)) { @@ -1256,8 +1282,9 @@ private void UpdateDiagnosticCounters( private readonly string m_sessionName; private X509Certificate2 m_serverCertificate; private Nonce m_serverNonce; - private string m_eccUserTokenSecurityPolicyUri; - private Nonce m_eccUserTokenNonce; + private byte[] m_channelThumbprint; + private string m_userTokenSecurityPolicyUri; + private Nonce m_userTokenNonce; private readonly X509Certificate2Collection m_clientIssuerCertificates; private readonly int m_maxHistoryContinuationPoints; private readonly SessionSecurityDiagnosticsDataType m_securityDiagnostics; diff --git a/Libraries/Opc.Ua.Server/Session/SessionManager.cs b/Libraries/Opc.Ua.Server/Session/SessionManager.cs index 2f89d780c2..030b294bbb 100644 --- a/Libraries/Opc.Ua.Server/Session/SessionManager.cs +++ b/Libraries/Opc.Ua.Server/Session/SessionManager.cs @@ -220,8 +220,7 @@ public virtual async ValueTask CreateSessionAsync( } // create server nonce. - var serverNonceObject = Nonce.CreateNonce( - context.ChannelContext.EndpointDescription.SecurityPolicyUri); + var serverNonceObject = Nonce.CreateNonce(0); // assign client name. if (string.IsNullOrEmpty(sessionName)) diff --git a/Libraries/Opc.Ua.Server/Session/SessionSecurityPolicyHelper.cs b/Libraries/Opc.Ua.Server/Session/SessionSecurityPolicyHelper.cs new file mode 100644 index 0000000000..ab97e323aa --- /dev/null +++ b/Libraries/Opc.Ua.Server/Session/SessionSecurityPolicyHelper.cs @@ -0,0 +1,161 @@ +/* ======================================================================== + * 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.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; + +namespace Opc.Ua.Server +{ + /// + /// Encapsulates session service security-policy specific processing. + /// + internal static class SessionSecurityPolicyHelper + { + /// + /// Decodes additional request parameters from an additional header. + /// + public static AdditionalParametersType DecodeAdditionalParameters( + ExtensionObject additionalHeader) + { + return ExtensionObject.ToEncodeable(additionalHeader) as AdditionalParametersType; + } + + /// + /// Creates the signature returned by CreateSession. + /// + public static SignatureData CreateServerSignature( + OperationContext context, + X509Certificate2 instanceCertificate, + X509Certificate2 parsedClientCertificate, + byte[] clientNonce, + byte[] serverNonce) + { + if (parsedClientCertificate == null || clientNonce == null) + { + return null; + } + + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(context.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetServerSignatureData( + context.ChannelContext.ChannelThumbprint, + clientNonce, + context.ChannelContext.ServerChannelCertificate, + parsedClientCertificate.RawData, + context.ChannelContext.ClientChannelCertificate, + serverNonce); + + return SecurityPolicies.CreateSignatureData( + context.SecurityPolicyUri, + instanceCertificate, + dataToSign); + } + + /// + /// Processes additional request parameters during CreateSession. + /// + public static AdditionalParametersType ProcessCreateSessionAdditionalParameters( + ISession session, + AdditionalParametersType parameters, + ILogger logger) + { + AdditionalParametersType response = null; + + if (parameters != null && parameters.Parameters != null) + { + response = new AdditionalParametersType(); + + foreach (KeyValuePair ii in parameters.Parameters) + { + if (ii.Key == AdditionalParameterNames.ECDHPolicyUri) + { + string policyUri = ii.Value.ToString(); + logger.LogWarning("Received request for new EphmeralKey using {SecurityPolicyUri}.", policyUri); + + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(policyUri); + + if (securityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) + { + session.SetUserTokenSecurityPolicy(policyUri); + EphemeralKeyType key = session.GetNewEphemeralKey(); + response.Parameters.Add( + new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), + Value = new ExtensionObject(key) + }); + + logger.LogWarning("Returning new EphemeralKey: {PublicKey} bytes.", key.PublicKey?.Length ?? 0); + } + else + { + response.Parameters.Add( + new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), + Value = StatusCodes.BadSecurityPolicyRejected + }); + + logger.LogWarning("Rejecting request for new EphemeralKey using {SecurityPolicyUri}.", policyUri); + } + } + } + } + + return response; + } + + /// + /// Processes additional request parameters during ActivateSession. + /// + public static AdditionalParametersType ProcessActivateSessionAdditionalParameters( + ISession session, + AdditionalParametersType parameters, + ILogger logger) + { + AdditionalParametersType response = null; + EphemeralKeyType key = session.GetNewEphemeralKey(); + + if (key != null) + { + response = new AdditionalParametersType(); + response.Parameters.Add( + new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), + Value = new ExtensionObject(key) + }); + + logger.LogWarning("Returning new EphemeralKey: {PublicKey} bytes.", key.PublicKey?.Length ?? 0); + } + + return response; + } + } +} diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs index 253b809cca..5ad1116bbe 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs @@ -172,6 +172,8 @@ protected virtual void Dispose(bool disposing) /// public string ListenerId { get; private set; } + internal byte[] ServerChannelCertificate { get; set; } + /// /// Opens the listener and starts accepting connection. /// @@ -302,6 +304,8 @@ private void ConfigureWebHost(IWebHostBuilder webHostBuilder) m_logger.LogTrace("Copy of the private key for https was denied: {Message}", ce.Message); } #endif + // save the server certificate so it can be used in the secure channel context. + ServerChannelCertificate = serverCertificate.RawData; var httpsOptions = new HttpsConnectionAdapterOptions { @@ -484,10 +488,13 @@ await WriteServiceResponseAsync(context, serviceResponse, ct) return; } } + var secureChannelContext = new SecureChannelContext( - ListenerId, - endpoint, - RequestEncoding.Binary); + ListenerId, + endpoint, + RequestEncoding.Binary, + context.Connection.ClientCertificate?.RawData, + ServerChannelCertificate); IServiceResponse output = await m_callback.ProcessRequestAsync( diff --git a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs index 28ef021c26..61a5fe3756 100644 --- a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs +++ b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs @@ -374,8 +374,17 @@ public static byte CalculateSecurityLevel( result = 2; break; case SecurityPolicies.Basic256: - logger.LogWarning( - "Deprecated Security Policy Basic256 requested - Not rcommended."); + logger.LogWarning("Deprecated Security Policy Basic256 requested - Not recommended."); + result = 4; + break; + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP384: + logger.LogWarning("Deprecated Security Policy {PolicyUri} requested - Use ECC_nistP[256/384]_AES.", policyUri); + result = 4; + break; + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: + logger.LogWarning("Deprecated Security Policy {PolicyUri} requested - Use ECC_brainpoolP[256/384]r1_AES.", policyUri); result = 4; break; case SecurityPolicies.Basic256Sha256: @@ -387,16 +396,20 @@ public static byte CalculateSecurityLevel( case SecurityPolicies.Aes256_Sha256_RsaPss: result = 10; break; - case SecurityPolicies.ECC_brainpoolP256r1: - result = 11; - break; - case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.RSA_DH_AesGcm: + case SecurityPolicies.RSA_DH_ChaChaPoly: result = 12; break; - case SecurityPolicies.ECC_brainpoolP384r1: - result = 13; + case SecurityPolicies.ECC_brainpoolP256r1_AesGcm: + case SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly: + case SecurityPolicies.ECC_nistP256_AesGcm: + case SecurityPolicies.ECC_nistP256_ChaChaPoly: + result = 12; break; - case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_nistP384_AesGcm: + case SecurityPolicies.ECC_nistP384_ChaChaPoly: + case SecurityPolicies.ECC_brainpoolP384r1_AesGcm: + case SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly: result = 14; break; case SecurityPolicies.None: diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs index 9f45315556..096e64a794 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs @@ -708,7 +708,7 @@ public static NodeId GetCertificateType(X509Certificate2 certificate) case Oids.ECDsaWithSha384: case Oids.ECDsaWithSha256: case Oids.ECDsaWithSha512: - return EccUtils.GetEccCertificateTypeId(certificate); + return CryptoUtils.GetEccCertificateTypeId(certificate); case Oids.RsaPkcs1Sha256: case Oids.RsaPkcs1Sha384: case Oids.RsaPkcs1Sha512: @@ -739,7 +739,7 @@ public static bool ValidateCertificateType( case Oids.ECDsaWithSha384: case Oids.ECDsaWithSha256: case Oids.ECDsaWithSha512: - NodeId certType = EccUtils.GetEccCertificateTypeId(certificate); + NodeId certType = CryptoUtils.GetEccCertificateTypeId(certificate); if (certType.IsNull) { return false; @@ -795,32 +795,45 @@ public static IList MapSecurityPolicyToCertificateTypes(string securityP case SecurityPolicies.Basic256Sha256: case SecurityPolicies.Aes128_Sha256_RsaOaep: case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.RSA_DH_AesGcm: + case SecurityPolicies.RSA_DH_ChaChaPoly: result.Add(ObjectTypeIds.RsaSha256ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP256_AesGcm: + case SecurityPolicies.ECC_nistP256_ChaChaPoly: result.Add(ObjectTypeIds.EccNistP256ApplicationCertificateType); goto case SecurityPolicies.ECC_nistP384; case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_nistP384_AesGcm: + case SecurityPolicies.ECC_nistP384_ChaChaPoly: result.Add(ObjectTypeIds.EccNistP384ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP256r1_AesGcm: + case SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly: result.Add(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); goto case SecurityPolicies.ECC_brainpoolP384r1; case SecurityPolicies.ECC_brainpoolP384r1: + case SecurityPolicies.ECC_brainpoolP384r1_AesGcm: + case SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly: result.Add(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve25519_AesGcm: + case SecurityPolicies.ECC_curve25519_ChaChaPoly: result.Add(ObjectTypeIds.EccCurve25519ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_curve448: + case SecurityPolicies.ECC_curve448_AesGcm: + case SecurityPolicies.ECC_curve448_ChaChaPoly: result.Add(ObjectTypeIds.EccCurve448ApplicationCertificateType); - goto default; + break; case SecurityPolicies.Https: result.Add(ObjectTypeIds.HttpsCertificateType); - goto default; - default: - return result; + break; } + return result; } /// diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs new file mode 100644 index 0000000000..c247383166 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs @@ -0,0 +1,1356 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; +using System.Globalization; +using System.Numerics; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Opc.Ua.Bindings; +using Opc.Ua.Security.Certificates; +#if CURVE25519 +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Crypto.Agreement; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Digests; +#endif + +namespace Opc.Ua +{ + /// + /// Defines functions to implement ECC cryptography. + /// + public static class CryptoUtils + { + /// + /// The name of the NIST P-256 curve. + /// + public const string NistP256 = nameof(NistP256); + + /// + /// The name of the NIST P-384 curve. + /// + public const string NistP384 = nameof(NistP384); + + /// + /// The name of the BrainpoolP256r1 curve. + /// + public const string BrainpoolP256r1 = nameof(BrainpoolP256r1); + + /// + /// The name of the BrainpoolP384r1 curve. + /// + public const string BrainpoolP384r1 = nameof(BrainpoolP384r1); + + internal const string NistP256KeyParameters = "06-08-2A-86-48-CE-3D-03-01-07"; + internal const string NistP384KeyParameters = "06-05-2B-81-04-00-22"; + internal const string BrainpoolP256r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-07"; + internal const string BrainpoolP384r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-0B"; + + /// + /// Returns true if the certificate is an ECC certificate. + /// + public static bool IsEccPolicy(string securityPolicyUri) + { + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + if (info != null) + { + return info.CertificateKeyFamily == CertificateKeyFamily.ECC; + } + + return false; + } + + /// + /// Returns the NodeId for the certificate type for the specified certificate. + /// + public static NodeId GetEccCertificateTypeId(X509Certificate2 certificate) + { + string keyAlgorithm = certificate.GetKeyAlgorithm(); + if (keyAlgorithm != Oids.ECPublicKey) + { + return NodeId.Null; + } + + PublicKey encodedPublicKey = certificate.PublicKey; + switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) + { + // nistP256 + case NistP256KeyParameters: + return ObjectTypeIds.EccNistP256ApplicationCertificateType; + // nistP384 + case NistP384KeyParameters: + return ObjectTypeIds.EccNistP384ApplicationCertificateType; + // brainpoolP256r1 + case BrainpoolP256r1KeyParameters: + return ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType; + // brainpoolP384r1 + case BrainpoolP384r1KeyParameters: + return ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType; + default: + return NodeId.Null; + } + } + + /// + /// returns an ECCCurve if there is a matching supported curve for the provided + /// certificate type id. if no supported ECC curve is found null is returned. + /// + /// the application certificatate type node id + /// the ECCCurve, null if certificatate type id has no matching supported ECC curve + public static ECCurve? GetCurveFromCertificateTypeId(NodeId certificateType) + { + ECCurve? curve = null; + + if (certificateType == ObjectTypeIds.EccApplicationCertificateType || + certificateType == ObjectTypeIds.EccNistP256ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.nistP256; + } + else if (certificateType == ObjectTypeIds.EccNistP384ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.nistP384; + } + else if (certificateType == ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.brainpoolP256r1; + } + else if (certificateType == ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.brainpoolP384r1; + } +#if CURVE25519 + else if (certificateType == ObjectTypeIds.EccCurve25519ApplicationCertificateType) + { + curve = default(ECCurve); + } + else if (certificateType == ObjectTypeIds.EccCurve448ApplicationCertificateType) + { + curve = default(ECCurve); + } +#endif + return curve; + } + + /// + /// Returns the signature algorithm for the specified certificate. + /// + public static string GetECDsaQualifier(X509Certificate2 certificate) + { + if (X509Utils.IsECDsaSignature(certificate)) + { + const string signatureQualifier = "ECDsa"; + PublicKey encodedPublicKey = certificate.PublicKey; + + // New values can be determined by running the dotted-decimal OID value + // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); + + switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) + { + case NistP256KeyParameters: + return NistP256; + case NistP384KeyParameters: + return NistP384; + case BrainpoolP256r1KeyParameters: + return BrainpoolP256r1; + case BrainpoolP384r1KeyParameters: + return BrainpoolP384r1; + default: + return signatureQualifier; + } + } + return string.Empty; + } + + /// + /// Returns the public key for the specified certificate. + /// + public static ECDsa GetPublicKey(X509Certificate2 certificate) + { + return GetPublicKey(certificate, out string[] _); + } + + /// + /// Returns the public key for the specified certificate and outputs the security policy uris. + /// + /// + /// + public static ECDsa GetPublicKey( + X509Certificate2 certificate, + out string[] securityPolicyUris) + { + securityPolicyUris = null; + + if (certificate == null) + { + return null; + } + + string keyAlgorithm = certificate.GetKeyAlgorithm(); + + if (keyAlgorithm != Oids.ECPublicKey) + { + return null; + } + + const X509KeyUsageFlags kSufficientFlags = + X509KeyUsageFlags.KeyAgreement | + X509KeyUsageFlags.DigitalSignature | + X509KeyUsageFlags.NonRepudiation | + X509KeyUsageFlags.CrlSign | + X509KeyUsageFlags.KeyCertSign; + + foreach (X509Extension extension in certificate.Extensions) + { + if (extension.Oid.Value == "2.5.29.15") + { + var kuExt = (X509KeyUsageExtension)extension; + + if ((kuExt.KeyUsages & kSufficientFlags) == 0) + { + return null; + } + } + } + + PublicKey encodedPublicKey = certificate.PublicKey; + string keyParameters = BitConverter.ToString( + encodedPublicKey.EncodedParameters.RawData); + byte[] keyValue = encodedPublicKey.EncodedKeyValue.RawData; + + var ecParameters = default(ECParameters); + + if (keyValue[0] != 0x04) + { + throw new InvalidOperationException("Only uncompressed points are supported"); + } + + byte[] x = new byte[(keyValue.Length - 1) / 2]; + byte[] y = new byte[x.Length]; + + Buffer.BlockCopy(keyValue, 1, x, 0, x.Length); + Buffer.BlockCopy(keyValue, 1 + x.Length, y, 0, y.Length); + + ecParameters.Q.X = x; + ecParameters.Q.Y = y; + + // New values can be determined by running the dotted-decimal OID value + // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); + + switch (keyParameters) + { + case NistP256KeyParameters: + ecParameters.Curve = ECCurve.NamedCurves.nistP256; + securityPolicyUris = [SecurityPolicies.ECC_nistP256]; + break; + case NistP384KeyParameters: + ecParameters.Curve = ECCurve.NamedCurves.nistP384; + securityPolicyUris = [SecurityPolicies.ECC_nistP384, SecurityPolicies + .ECC_nistP256]; + break; + case BrainpoolP256r1KeyParameters: + ecParameters.Curve = ECCurve.NamedCurves.brainpoolP256r1; + securityPolicyUris = [SecurityPolicies.ECC_brainpoolP256r1]; + break; + case BrainpoolP384r1KeyParameters: + ecParameters.Curve = ECCurve.NamedCurves.brainpoolP384r1; + securityPolicyUris = [SecurityPolicies.ECC_brainpoolP384r1, SecurityPolicies + .ECC_brainpoolP256r1]; + break; + default: + throw new NotImplementedException(keyParameters); + } + + return ECDsa.Create(ecParameters); + } + + /// + /// Returns the length of a ECDsa signature of a digest. + /// + /// + public static int GetSignatureLength(X509Certificate2 signingCertificate) + { + if (signingCertificate == null) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityChecksFailed, + "No public key for certificate."); + } + + if (signingCertificate.GetRSAPublicKey() != null) + { + return RsaUtils.GetSignatureLength(signingCertificate); + } + + using ECDsa publicKey = + GetPublicKey(signingCertificate) + ?? throw ServiceResultException.Create( + StatusCodes.BadSecurityChecksFailed, + "No public key for certificate."); + + return publicKey.KeySize / 4; + } + + /// + /// Computes a signature. + /// + public static byte[] Sign( + ArraySegment dataToSign, + X509Certificate2 signingCertificate, + string securityPolicyUri) + { + var info = SecurityPolicies.GetInfo(securityPolicyUri); + return Sign(dataToSign, signingCertificate, info.AsymmetricSignatureAlgorithm); + } + + /// + /// Computes a signature. + /// + /// + public static byte[] Sign( + ArraySegment dataToSign, + X509Certificate2 signingCertificate, + AsymmetricSignatureAlgorithm algorithm) + { + switch (algorithm) + { + case AsymmetricSignatureAlgorithm.None: + return null; + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + return RsaUtils.Rsa_Sign( + dataToSign, + signingCertificate, + HashAlgorithmName.SHA1, + RSASignaturePadding.Pkcs1); + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + return RsaUtils.Rsa_Sign( + dataToSign, + signingCertificate, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + case AsymmetricSignatureAlgorithm.RsaPssSha256: + return RsaUtils.Rsa_Sign( + dataToSign, + signingCertificate, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pss); + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + break; + default: + throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); + } + + // get the algorithm used for the signature. + HashAlgorithmName hashAlgorithm; + + switch (algorithm) + { + case AsymmetricSignatureAlgorithm.EcdsaSha384: + hashAlgorithm = HashAlgorithmName.SHA384; + break; + case AsymmetricSignatureAlgorithm.EcdsaSha256: + hashAlgorithm = HashAlgorithmName.SHA256; + break; + default: + throw new NotSupportedException($"AsymmetricSignatureAlgorithm not supported: {algorithm}"); + } + + ECDsa senderPrivateKey = + signingCertificate.GetECDsaPrivateKey() + ?? throw new ServiceResultException( + StatusCodes.BadCertificateInvalid, + "Missing private key needed for create a signature."); + + using (senderPrivateKey) + { + byte[] signature = senderPrivateKey.SignData( + dataToSign.Array, + dataToSign.Offset, + dataToSign.Count, + hashAlgorithm); + + return signature; + } + } + + /// + /// Verifies a signature. + /// + public static bool Verify( + ArraySegment dataToVerify, + byte[] signature, + X509Certificate2 signingCertificate, + string securityPolicyUri) + { + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + if (info == null) + { + throw new ServiceResultException( + StatusCodes.BadSecurityChecksFailed, + $"Unknown security policy: {securityPolicyUri}"); + } + + return Verify( + dataToVerify, + signature, + signingCertificate, + info.AsymmetricSignatureAlgorithm); + } + + /// + /// Verifies a signature. + /// + public static bool Verify( + ArraySegment dataToVerify, + byte[] signature, + X509Certificate2 signingCertificate, + AsymmetricSignatureAlgorithm algorithm) + { + switch (algorithm) + { + case AsymmetricSignatureAlgorithm.None: + return true; + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + return RsaUtils.Rsa_Verify( + dataToVerify, + signature, + signingCertificate, + HashAlgorithmName.SHA1, + RSASignaturePadding.Pkcs1); + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + return RsaUtils.Rsa_Verify( + dataToVerify, + signature, + signingCertificate, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + case AsymmetricSignatureAlgorithm.RsaPssSha256: + return RsaUtils.Rsa_Verify( + dataToVerify, + signature, + signingCertificate, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pss); + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + break; + default: + return false; + } + + // get the algorithm used for the signature. + HashAlgorithmName hashAlgorithm; + + switch (algorithm) + { + case AsymmetricSignatureAlgorithm.EcdsaSha384: + hashAlgorithm = HashAlgorithmName.SHA384; + break; + case AsymmetricSignatureAlgorithm.EcdsaSha256: + hashAlgorithm = HashAlgorithmName.SHA256; + break; + default: + throw new NotSupportedException($"AsymmetricSignatureAlgorithm not supported: {algorithm}."); + } + + using ECDsa ecdsa = GetPublicKey(signingCertificate); + + return ecdsa.VerifyData( + dataToVerify.Array, + dataToVerify.Offset, + dataToVerify.Count, + signature, + hashAlgorithm); + } + + /// + /// Adds padding to a buffer. Input: buffer with unencrypted data starting at 0; plaintext data starting at offset; no padding. + /// + /// buffer with unencrypted data starting at 0; plaintext data starting at offset; no padding. + /// + /// Additional bytes that will be appended after padding (e.g., HMAC) and must be considered for block alignment. + /// Output: buffer with unencrypted data starting at 0; plaintext data starting at offset; padding added. + private static ArraySegment AddPadding(ArraySegment data, int blockSize, int trailingBytes = 0) + { + int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1; + int paddingSize = blockSize - ((data.Count + paddingByteSize + trailingBytes) % blockSize); + paddingSize %= blockSize; + + int endOfData = data.Offset + data.Count; + int endOfPaddedData = data.Offset + data.Count + paddingSize + paddingByteSize; + + for (int ii = endOfData; ii < endOfPaddedData - paddingByteSize && ii < data.Array.Length; ii++) + { + data.Array[ii] = (byte)(paddingSize & 0xFF); + } + + data.Array[endOfData + paddingSize] = (byte)(paddingSize & 0xFF); + + if (blockSize > byte.MaxValue) + { + data.Array[endOfData + paddingSize + 1] = (byte)((paddingSize & 0xFF) >> 8); + } + + return new ArraySegment(data.Array, data.Offset, data.Count + paddingSize + paddingByteSize); + } + + /// + /// Removes padding from a buffer. Input: buffer with unencrypted data starting at 0; plaintext including padding starting at offset; signature removed. + /// + /// Input: buffer with unencrypted data starting at 0; plaintext including padding starting at offset; signature removed. + /// + /// Output: buffer with unencrypted data starting at 0; plaintext starting at offset; padding excluded. + /// + private static ArraySegment RemovePadding(ArraySegment data, int blockSize) + { + int paddingSize = data.Array[data.Offset + data.Count - 1]; + int paddingByteSize = 1; + + if (blockSize > byte.MaxValue) + { + paddingSize <<= 8; + paddingSize += data.Array[data.Offset + data.Count - 2]; + paddingByteSize = 2; + } + + int notvalid = paddingSize < data.Count ? 0 : 1; + int start = data.Offset + data.Count - paddingSize - paddingByteSize; + + for (int ii = data.Offset; ii < data.Count - paddingByteSize && ii < paddingSize; ii++) + { + if (start < 0 || start + ii >= data.Count) + { + notvalid |= 1; + continue; + } + + notvalid |= data.Array[start + ii] ^ (paddingSize & 0xFF); + } + + if (notvalid != 0) + { + throw new CryptographicException("Invalid padding."); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count - paddingSize - paddingByteSize); + } + + /// + /// Encrypts the buffer using the algorithm specified by the security policy. + /// + public static ArraySegment SymmetricEncryptAndSign( + ArraySegment data, + SecurityPolicyInfo securityPolicy, + byte[] encryptingKey, + byte[] iv, + byte[] signingKey = null, + HMAC hmac = null, + bool signOnly = false, + uint tokenId = 0, + uint lastSequenceNumber = 0) + { + SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm; + + if (algorithm == SymmetricEncryptionAlgorithm.None) + { + return data; + } + + if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm) + { +#if NET8_0_OR_GREATER + return EncryptWithAesGcm(data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); +#else + throw new NotSupportedException("AES-GCM requires .NET 8 or greater."); +#endif + } + + if (algorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305) + { +#if NET8_0_OR_GREATER + return EncryptWithChaCha20Poly1305( + data, + encryptingKey, + iv, + signOnly, + tokenId, + lastSequenceNumber); +#else + throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater."); +#endif + } + + int hashLength = 0; + + if (signingKey != null) + { + if (hmac == null) + { + throw new CryptographicException("Missing HMAC for symmetric signing."); + } + + hashLength = hmac.HashSize / 8; + } + + if (!signOnly) + { + data = AddPadding(data, iv.Length, hashLength); + } + + if (signingKey != null) + { + byte[] hash = hmac.ComputeHash(data.Array, 0, data.Offset + data.Count); + + Buffer.BlockCopy( + hash, + 0, + data.Array, + data.Offset + data.Count, + hash.Length); + + data = new ArraySegment( + data.Array, + data.Offset, + data.Count + hash.Length); + } + + if (!signOnly) + { +#pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector + using var aes = Aes.Create(); + + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + aes.Key = encryptingKey; + aes.IV = iv; + + using ICryptoTransform encryptor = aes.CreateEncryptor(); +#pragma warning restore CA5401 + + encryptor.TransformBlock( + data.Array, + data.Offset, + data.Count, + data.Array, + data.Offset); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count); + } + +#if NET8_0_OR_GREATER + private static byte[] ApplyAeadMask(uint tokenId, uint lastSequenceNumber, byte[] iv) + { + var copy = new byte[iv.Length]; + Buffer.BlockCopy(iv, 0, copy, 0, iv.Length); + + copy[0] ^= (byte)((tokenId & 0x000000FF)); + copy[1] ^= (byte)((tokenId & 0x0000FF00) >> 8); + copy[2] ^= (byte)((tokenId & 0x00FF0000) >> 16); + copy[3] ^= (byte)((tokenId & 0xFF000000) >> 24); + copy[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); + copy[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); + copy[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); + copy[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); + + return copy; + } + + private const int kChaChaPolyIvLength = 12; + private const int kChaChaPolyTagLength = 16; + + private static ArraySegment EncryptWithChaCha20Poly1305( + ArraySegment data, + byte[] encryptingKey, + byte[] iv, + bool signOnly, + uint tokenId, + uint lastSequenceNumber) + { + if (encryptingKey == null || encryptingKey.Length != 32) + { + throw new ArgumentException("ChaCha20-Poly1305 requires a 256-bit (32-byte) key.", nameof(encryptingKey)); + } + + if (iv == null || iv.Length != kChaChaPolyIvLength) + { + throw new ArgumentException("ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv)); + } + + byte[] ciphertext = new byte[signOnly ? 0 : data.Count]; + byte[] tag = new byte[kChaChaPolyTagLength]; // ChaCha20-Poly1305/AES-GCM uses 128-bit authentication tag + + var extraData = new ReadOnlySpan( + data.Array, + 0, + signOnly ? data.Offset + data.Count : data.Offset); + + using var chacha = new ChaCha20Poly1305(encryptingKey); + + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + + chacha.Encrypt( + iv, + signOnly ? Array.Empty() : data, + ciphertext, + tag, + extraData); + + CryptoTrace.Start(ConsoleColor.DarkCyan, "EncryptWithChaCha20Poly1305"); + CryptoTrace.WriteLine($"Data Offset/Count={data.Offset}/{data.Count}"); + CryptoTrace.WriteLine($"TokenId/LastSequenceNumber={tokenId}/{lastSequenceNumber}"); + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.WriteLine($"EncryptedData={CryptoTrace.KeyToString(ciphertext)}"); + CryptoTrace.WriteLine($"Tag={CryptoTrace.KeyToString(tag)}"); + CryptoTrace.WriteLine($"ExtraData={CryptoTrace.KeyToString(extraData.ToArray())}"); + CryptoTrace.Finish("EncryptWithChaCha20Poly1305"); + + // Return layout: [associated data | ciphertext | tag] + if (!signOnly) + { + Buffer.BlockCopy(ciphertext, 0, data.Array, data.Offset, ciphertext.Length); + } + + Buffer.BlockCopy(tag, 0, data.Array, data.Offset + data.Count, tag.Length); + + return new ArraySegment( + data.Array, + 0, + data.Offset + data.Count + kChaChaPolyTagLength); + } +#endif + +#if NET8_0_OR_GREATER + private static ArraySegment DecryptWithChaCha20Poly1305( + ArraySegment data, + byte[] encryptingKey, + byte[] iv, + bool signOnly, + uint tokenId, + uint lastSequenceNumber) + { + if (encryptingKey == null || encryptingKey.Length != 32) + { + throw new ArgumentException("ChaCha20-Poly1305 requires a 256-bit (32-byte) key.", nameof(encryptingKey)); + } + + if (iv == null || iv.Length != kChaChaPolyIvLength) + { + throw new ArgumentException("ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv)); + } + + if (data.Count < kChaChaPolyTagLength) // Must at least contain tag + { + throw new ArgumentException("Ciphertext too short.", nameof(data)); + } + + byte[] plaintext = new byte[data.Count - kChaChaPolyTagLength]; + + var encryptedData = new ArraySegment( + data.Array, + data.Offset, + signOnly ? 0 : data.Count - kChaChaPolyTagLength); + + var tag = new ArraySegment( + data.Array, + data.Offset + data.Count - kChaChaPolyTagLength, + kChaChaPolyTagLength); + + var extraData = new ReadOnlySpan( + data.Array, + 0, + signOnly ? data.Offset + data.Count - kChaChaPolyTagLength : data.Offset); + + using var chacha = new ChaCha20Poly1305(encryptingKey); + + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + + chacha.Decrypt( + iv, + encryptedData, + tag, + signOnly ? [] : plaintext, + extraData); + + CryptoTrace.Start(ConsoleColor.DarkCyan, "DecryptWithChaCha20Poly1305"); + CryptoTrace.WriteLine($"Data Offset/Count={data.Offset}/{data.Count - kChaChaPolyTagLength}"); + CryptoTrace.WriteLine($"TokenId/LastSequenceNumber={tokenId}/{lastSequenceNumber}"); + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.WriteLine($"EncryptedData={CryptoTrace.KeyToString(encryptedData)}"); + CryptoTrace.WriteLine($"Tag={CryptoTrace.KeyToString(tag)}"); + CryptoTrace.WriteLine($"ExtraData={CryptoTrace.KeyToString(extraData.ToArray())}"); + CryptoTrace.Finish("DecryptWithChaCha20Poly1305"); + + // Return layout: [associated data | plaintext] + if (!signOnly) + { + Buffer.BlockCopy(plaintext, 0, data.Array, data.Offset, encryptedData.Count); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count - kChaChaPolyTagLength); + } +#endif + +#if NET8_0_OR_GREATER + private const int kAesGcmIvLength = 12; + private const int kAesGcmTagLength = 16; + + private static ArraySegment EncryptWithAesGcm( + ArraySegment data, + byte[] encryptingKey, + byte[] iv, + bool signOnly, + uint tokenId, + uint lastSequenceNumber) + { + if (encryptingKey == null) + { + throw new ArgumentNullException(nameof(encryptingKey)); + } + + if (iv == null || iv.Length != kAesGcmIvLength) + { + throw new ArgumentException("AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv)); + } + + byte[] ciphertext = new byte[signOnly ? 0 : data.Count]; + byte[] tag = new byte[kAesGcmTagLength]; // AES-GCM uses 128-bit authentication tag + + var extraData = new ReadOnlySpan( + data.Array, + 0, + signOnly ? data.Offset + data.Count : data.Offset); + + using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength); + + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + + aesGcm.Encrypt( + iv, + signOnly ? Array.Empty() : data, + ciphertext, + tag, + extraData); + + CryptoTrace.Start(ConsoleColor.DarkCyan, "EncryptWithAesGcm"); + CryptoTrace.WriteLine($"Data Offset/Count={data.Offset}/{data.Count}"); + CryptoTrace.WriteLine($"TokenId/LastSequenceNumber={tokenId}/{lastSequenceNumber}"); + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.WriteLine($"EncryptedData={CryptoTrace.KeyToString(ciphertext)}"); + CryptoTrace.WriteLine($"Tag={CryptoTrace.KeyToString(tag)}"); + CryptoTrace.WriteLine($"ExtraData={CryptoTrace.KeyToString(extraData.ToArray())}"); + CryptoTrace.Finish("DecryptWithAesGcm"); + + // Return layout: [associated data | ciphertext | tag] + if (!signOnly) + { + Buffer.BlockCopy(ciphertext, 0, data.Array, data.Offset, ciphertext.Length); + } + + Buffer.BlockCopy(tag, 0, data.Array, data.Offset + data.Count, tag.Length); + + return new ArraySegment( + data.Array, + 0, + data.Offset + data.Count + kAesGcmTagLength); + } +#endif + +#if NET8_0_OR_GREATER + private static ArraySegment DecryptWithAesGcm( + ArraySegment data, + byte[] encryptingKey, + byte[] iv, + bool signOnly, + uint tokenId, + uint lastSequenceNumber) + { + if (encryptingKey == null) + { + throw new ArgumentNullException(nameof(encryptingKey)); + } + + if (iv == null || iv.Length != kAesGcmIvLength) + { + throw new ArgumentException("AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv)); + } + + if (data.Count < kAesGcmTagLength) // Must at least contain tag + { + throw new ArgumentException("Ciphertext too short.", nameof(data)); + } + + byte[] plaintext = new byte[data.Count - kAesGcmTagLength]; + + var encryptedData = new ArraySegment( + data.Array, + data.Offset, + signOnly ? 0 : data.Count - kAesGcmTagLength); + + var tag = new ArraySegment( + data.Array, + data.Offset + data.Count - kAesGcmTagLength, + kAesGcmTagLength); + + var extraData = new ReadOnlySpan( + data.Array, + 0, + signOnly ? data.Offset + data.Count - kAesGcmTagLength : data.Offset); + + using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength); + + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + + CryptoTrace.Start(ConsoleColor.DarkCyan, "DecryptWithAesGcm"); + CryptoTrace.WriteLine($"Data Offset/Count={data.Offset}/{data.Count - kAesGcmTagLength}"); + CryptoTrace.WriteLine($"TokenId/LastSequenceNumber={tokenId}/{lastSequenceNumber}"); + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.WriteLine($"EncryptedData={CryptoTrace.KeyToString(encryptedData)}"); + CryptoTrace.WriteLine($"Tag={CryptoTrace.KeyToString(tag)}"); + CryptoTrace.WriteLine($"ExtraData={CryptoTrace.KeyToString(extraData.ToArray())}"); + CryptoTrace.Finish("DecryptWithAesGcm"); + + aesGcm.Decrypt( + iv, + encryptedData, + tag, + signOnly ? [] : plaintext, + extraData); + + // Return layout: [associated data | plaintext] + if (!signOnly) + { + Buffer.BlockCopy(plaintext, 0, data.Array, data.Offset, encryptedData.Count); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count - kAesGcmTagLength); + } +#endif + + /// + /// Decrypts the buffer using the algorithm specified by the security policy. + /// + /// + /// + public static ArraySegment SymmetricDecryptAndVerify( + ArraySegment data, + SecurityPolicyInfo securityPolicy, + byte[] encryptingKey, + byte[] iv, + byte[] signingKey = null, + bool signOnly = false, + uint tokenId = 0, + uint lastSequenceNumber = 0) + { + SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm; + + if (algorithm == SymmetricEncryptionAlgorithm.None) + { + return data; + } + + if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm) + { +#if NET8_0_OR_GREATER + return DecryptWithAesGcm(data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); +#else + throw new NotSupportedException("AES-GCM requires .NET 8 or greater."); +#endif + } + + if (algorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305) + { +#if NET8_0_OR_GREATER + return DecryptWithChaCha20Poly1305( + data, + encryptingKey, + iv, + signOnly, + tokenId, + lastSequenceNumber); +#else + throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater."); +#endif + } + + if (!signOnly) + { + using var aes = Aes.Create(); + + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + aes.Key = encryptingKey; + aes.IV = iv; + + using ICryptoTransform decryptor = aes.CreateDecryptor(); + + decryptor.TransformBlock( + data.Array, + data.Offset, + data.Count, + data.Array, + data.Offset); + } + + int isNotValid = 0; + + if (signingKey != null) + { + using HMAC hmac = securityPolicy.CreateSignatureHmac(signingKey); + byte[] hash = hmac.ComputeHash(data.Array, 0, data.Offset + data.Count - (hmac.HashSize / 8)); + for (int ii = 0; ii < hash.Length; ii++) + { + int index = data.Offset + data.Count - hash.Length + ii; + isNotValid |= data.Array[index] != hash[ii] ? 1 : 0; + } + + data = new ArraySegment( + data.Array, + data.Offset, + data.Count - hash.Length); + } + + if (!signOnly) + { + data = RemovePadding(data, iv.Length); + } + + if (isNotValid != 0) + { + throw new CryptographicException("Invalid signature."); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count); + } + } + +#if X + class FfdheDhWithRsaAuth + { + // ffdhe2048 prime from RFC 7919 (hex, without whitespace). + // (RFC 7919 Appendix A.3 — use this canonical modulus in production.) + const string FFDHE2048_HEX = @" + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 61285C97 FFFFFFFF FFFFFFFF"; + + // Generator for FFDHE groups is 2 + static readonly BigInteger G = new BigInteger(2); + + static void Main() + { + // Parse the RFC hex prime into a positive BigInteger + BigInteger p = ParseHexBigInteger(FFDHE2048_HEX); + + // Recommended: use a short ephemeral exponent (e.g. 256-bit) for performance + // while still achieving ~128-bit security for typical scenarios. + // (RFC7919 and implementation guidance discuss exponent sizing; choose per your threat model.) + int privateBitLength = 256; + + Console.WriteLine("Simulating DH exchange (ffdhe2048) with RSA signing..."); + + // Simulate Alice and Bob + var alice = CreateDhParticipant(p, privateBitLength); + var bob = CreateDhParticipant(p, privateBitLength); + + // Each signs their public value with their own RSA key (DHE-RSA style authentication) + byte[] alicePub = ToBigEndian(alice.PublicValue); + byte[] bobPub = ToBigEndian(bob.PublicValue); + + byte[] aliceSig, bobSig; + using (RSA rsaAlice = RSA.Create(2048)) + using (RSA rsaBob = RSA.Create(2048)) + { + // In real use the RSA keys are persistent (server cert) and public keys/certificates exchanged + aliceSig = rsaAlice.SignData(alicePub, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + bobSig = rsaBob.SignData(bobPub, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + // Each verifies the other's signature (in a real protocol they'd have the peer's cert/public key) + bool verifyAlice = rsaBob.VerifyData(bobPub, bobSig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + bool verifyBob = rsaAlice.VerifyData(alicePub, aliceSig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + Console.WriteLine($"Alice verifies Bob's signature: {verifyAlice}"); + Console.WriteLine($"Bob verifies Alice's signature: {verifyBob}"); + } + + // Compute shared secrets + BigInteger aliceShared = BigInteger.ModPow(bob.PublicValue, alice.PrivateValue, p); + BigInteger bobShared = BigInteger.ModPow(alice.PublicValue, bob.PrivateValue, p); + + if (aliceShared != bobShared) + { + Console.WriteLine("Shared secrets do not match! Aborting."); + return; + } + + byte[] sharedBytes = ToBigEndian(aliceShared); // same as bobShared + + // Derive 32-byte key with HKDF-SHA256 (RFC 5869) + byte[] salt = RandomNumberGenerator.GetBytes(32); // optional but recommended + byte[] info = Encoding.UTF8.GetBytes("ffdhe2048-dhe-rsa-derived-key"); + byte[] aesKey = HkdfSha256(sharedBytes, salt, info, 32); + } + + // Creates a participant with ephemeral private/public values + static (BigInteger PrivateValue, BigInteger PublicValue) CreateDhParticipant(BigInteger p, int privateBitLen) + { + BigInteger priv = GenerateRandomBigInteger(privateBitLen); + BigInteger pub = BigInteger.ModPow(G, priv, p); + return (priv, pub); + } + + // Generate an unsigned positive BigInteger of bitLength bits (big-endian) + static BigInteger GenerateRandomBigInteger(int bitLength) + { + int byteCount = (bitLength + 7) / 8; + byte[] be = RandomNumberGenerator.GetBytes(byteCount); + + // Ensure top bit set so the value has the requested bit length (avoid tiny exponents) + int topBitIndex = (bitLength - 1) % 8; + be[0] |= (byte)(1 << topBitIndex); + + // Convert big-endian to little-endian + sign byte for BigInteger ctor + byte[] le = new byte[be.Length + 1]; // extra zero to force positive + for (int i = 0; i < be.Length; i++) + le[i] = be[be.Length - 1 - i]; + le[le.Length - 1] = 0; + return new BigInteger(le); + } + + // Convert BigInteger to big-endian unsigned byte[] (no leading zero) + static byte[] ToBigEndian(BigInteger value) + { + if (value.Sign < 0) + throw new ArgumentException("value must be non-negative"); + byte[] le = value.ToByteArray(); // little-endian two's complement + // Trim any trailing zero that indicates sign if present + int last = le.Length - 1; + if (le[last] == 0) + { + Array.Resize(ref le, last); + last--; + } + byte[] be = new byte[le.Length]; + for (int i = 0; i < le.Length; i++) + be[i] = le[le.Length - 1 - i]; + return be; + } + + // Parse hex into a positive BigInteger (handles odd-length and ensures positive) + static BigInteger ParseHexBigInteger(string hex) + { + hex = hex.Trim().Replace(" ", "").Replace("\n", "").Replace("\r", ""); + // Ensure even length + if ((hex.Length & 1) == 1) + hex = "0" + hex; + // Use Convert.FromHexString (available in .NET 5+) or implement equivalent + byte[] be = Convert.FromHexString(hex); + // If highest bit of first byte is set, BigInteger.Parse with HexNumber can treat it as negative; + // we therefore construct positive BigInteger from big-endian bytes: + byte[] le = new byte[be.Length + 1]; + for (int i = 0; i < be.Length; i++) + le[i] = be[be.Length - 1 - i]; + le[le.Length - 1] = 0; + return new BigInteger(le); + } + + // Simple HKDF-SHA256 implementation (RFC 5869) + static byte[] HkdfSha256(byte[] ikm, byte[] salt, byte[] info, int outLen) + { + // HKDF-Extract + byte[] prk; + using (var hmac = new HMACSHA256(salt ?? new byte[0])) + { + prk = hmac.ComputeHash(ikm); + } + + // HKDF-Expand + int hashLen = 32; + int n = (outLen + hashLen - 1) / hashLen; + byte[] okm = new byte[outLen]; + byte[] previous = Array.Empty(); + using (var hmac = new HMACSHA256(prk)) + { + int offset = 0; + for (byte i = 1; i <= n; i++) + { + // T(i) = HMAC-PRK( T(i-1) | info | i ) + hmac.Initialize(); + hmac.TransformBlock(previous, 0, previous.Length, null, 0); + if (info != null && info.Length > 0) + hmac.TransformBlock(info, 0, info.Length, null, 0); + byte[] counter = new byte[] { i }; + hmac.TransformFinalBlock(counter, 0, 1); + previous = hmac.Hash!; + int toCopy = Math.Min(hashLen, outLen - offset); + Array.Copy(previous, 0, okm, offset, toCopy); + offset += toCopy; + } + } + return okm; + } + } +#endif + + /// + /// A class to assist with tracing crypto operations. + /// + public static class CryptoTrace + { + /// + /// Enabled crypto traces to stdout. + /// + public static bool Enabled { get; set; } = true; + + /// + /// Starts a trace block. + /// + public static void Start(ConsoleColor color, string format, params object[] args) + { +#if DEBUG + if (Enabled) + { + Console.ForegroundColor = color; + Console.Write("============ "); + Console.Write(format, args); + Console.WriteLine(" ============"); + } +#endif + } + + /// + /// Finishes a trace block. + /// + public static void Finish(string format, params object[] args) + { +#if DEBUG + if (Enabled) + { + Console.Write("============ "); + Console.Write(format, args); + Console.WriteLine(" Finished ============"); + Console.ForegroundColor = ConsoleColor.White; + } +#endif + } + + /// + /// Writes a trace message. + /// + public static void Write(string format, params object[] args) + { +#if DEBUG + if (Enabled) + { + Console.Write(format, args); + } +#endif + } + + /// + /// Writes a trace message. + /// + public static void WriteLine(string format, params object[] args) + { +#if DEBUG + if (Enabled) + { + Console.WriteLine(format, args); + } +#endif + } + + /// + /// Returns a debug string for a key. + /// + public static string KeyToString(ArraySegment key) + { +#if DEBUG + if (Enabled) + { + byte[] bytes = new byte[key.Count]; + Buffer.BlockCopy(key.Array ?? [], key.Offset, bytes, 0, key.Count); + return KeyToString(bytes); + } + else + { + return String.Empty; + } +#else + return String.Empty; +#endif + } + + /// + /// Returns a debug string for a key. + /// + public static string KeyToString(byte[] key) + { +#if DEBUG + if (Enabled) + { + if (key == null || key.Length == 0) + { + return "Len=0:---"; + } + + byte checksum = 0; + + foreach (var item in key) + { + checksum ^= item; + } + + if (key.Length <= 16) + { + return "Len=" + key.Length.ToString(CultureInfo.InvariantCulture) + + ":" + + Utils.ToHexString(key) + + "=>XOR=" + + checksum.ToString(CultureInfo.InvariantCulture); + } + + var text = Utils.ToHexString(key); + return $"Len={key.Length}:{text.Substring(0, 8)}...{text.Substring(text.Length - 8, 8)}=>XOR={checksum}"; + } + else + { + return String.Empty; + } +#else + return String.Empty; +#endif + } + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs deleted file mode 100644 index 1aacf695c8..0000000000 --- a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs +++ /dev/null @@ -1,1280 +0,0 @@ -/* ======================================================================== - * 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.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Opc.Ua.Security.Certificates; -#if CURVE25519 -using Org.BouncyCastle.Pkcs; -using Org.BouncyCastle.X509; -using Org.BouncyCastle.Security; -using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Crypto.Signers; -using Org.BouncyCastle.Crypto.Agreement; -using Org.BouncyCastle.Crypto.Generators; -using Org.BouncyCastle.Crypto.Modes; -using Org.BouncyCastle.Crypto.Digests; -#endif - -namespace Opc.Ua -{ - /// - /// Defines functions to implement ECC cryptography. - /// - public static class EccUtils - { - /// - /// The name of the NIST P-256 curve. - /// - public const string NistP256 = nameof(NistP256); - - /// - /// The name of the NIST P-384 curve. - /// - public const string NistP384 = nameof(NistP384); - - /// - /// The name of the BrainpoolP256r1 curve. - /// - public const string BrainpoolP256r1 = nameof(BrainpoolP256r1); - - /// - /// The name of the BrainpoolP384r1 curve. - /// - public const string BrainpoolP384r1 = nameof(BrainpoolP384r1); - - internal const string NistP256KeyParameters = "06-08-2A-86-48-CE-3D-03-01-07"; - internal const string NistP384KeyParameters = "06-05-2B-81-04-00-22"; - internal const string BrainpoolP256r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-07"; - internal const string BrainpoolP384r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-0B"; - - /// - /// Returns true if the certificate is an ECC certificate. - /// - public static bool IsEccPolicy(string securityPolicyUri) - { - if (securityPolicyUri != null) - { - switch (securityPolicyUri) - { - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return true; - default: - return false; - } - } - - return false; - } - - /// - /// Returns the NodeId for the certificate type for the specified certificate. - /// - public static NodeId GetEccCertificateTypeId(X509Certificate2 certificate) - { - string keyAlgorithm = certificate.GetKeyAlgorithm(); - if (keyAlgorithm != Oids.ECPublicKey) - { - return NodeId.Null; - } - - PublicKey encodedPublicKey = certificate.PublicKey; - switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) - { - // nistP256 - case NistP256KeyParameters: - return ObjectTypeIds.EccNistP256ApplicationCertificateType; - // nistP384 - case NistP384KeyParameters: - return ObjectTypeIds.EccNistP384ApplicationCertificateType; - // brainpoolP256r1 - case BrainpoolP256r1KeyParameters: - return ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType; - // brainpoolP384r1 - case BrainpoolP384r1KeyParameters: - return ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType; - default: - return NodeId.Null; - } - } - - /// - /// returns an ECCCurve if there is a matching supported curve for the provided - /// certificate type id. if no supported ECC curve is found null is returned. - /// - /// the application certificatate type node id - /// the ECCCurve, null if certificatate type id has no matching supported ECC curve - public static ECCurve? GetCurveFromCertificateTypeId(NodeId certificateType) - { - ECCurve? curve = null; - - if (certificateType == ObjectTypeIds.EccApplicationCertificateType || - certificateType == ObjectTypeIds.EccNistP256ApplicationCertificateType) - { - curve = ECCurve.NamedCurves.nistP256; - } - else if (certificateType == ObjectTypeIds.EccNistP384ApplicationCertificateType) - { - curve = ECCurve.NamedCurves.nistP384; - } - else if (certificateType == ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType) - { - curve = ECCurve.NamedCurves.brainpoolP256r1; - } - else if (certificateType == ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType) - { - curve = ECCurve.NamedCurves.brainpoolP384r1; - } -#if CURVE25519 - else if (certificateType == ObjectTypeIds.EccCurve25519ApplicationCertificateType) - { - curve = default(ECCurve); - } - else if (certificateType == ObjectTypeIds.EccCurve448ApplicationCertificateType) - { - curve = default(ECCurve); - } -#endif - return curve; - } - - /// - /// Returns the signature algorithm for the specified certificate. - /// - public static string GetECDsaQualifier(X509Certificate2 certificate) - { - if (X509Utils.IsECDsaSignature(certificate)) - { - const string signatureQualifier = "ECDsa"; - PublicKey encodedPublicKey = certificate.PublicKey; - - // New values can be determined by running the dotted-decimal OID value - // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); - - switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) - { - case NistP256KeyParameters: - return NistP256; - case NistP384KeyParameters: - return NistP384; - case BrainpoolP256r1KeyParameters: - return BrainpoolP256r1; - case BrainpoolP384r1KeyParameters: - return BrainpoolP384r1; - default: - return signatureQualifier; - } - } - return string.Empty; - } - - /// - /// Returns the public key for the specified certificate. - /// - public static ECDsa GetPublicKey(X509Certificate2 certificate) - { - return GetPublicKey(certificate, out string[] _); - } - - /// - /// Returns the public key for the specified certificate and outputs the security policy uris. - /// - /// - /// - public static ECDsa GetPublicKey( - X509Certificate2 certificate, - out string[] securityPolicyUris) - { - securityPolicyUris = null; - - if (certificate == null) - { - return null; - } - - string keyAlgorithm = certificate.GetKeyAlgorithm(); - - if (keyAlgorithm != Oids.ECPublicKey) - { - return null; - } - - const X509KeyUsageFlags kSufficientFlags = - X509KeyUsageFlags.KeyAgreement | - X509KeyUsageFlags.DigitalSignature | - X509KeyUsageFlags.NonRepudiation | - X509KeyUsageFlags.CrlSign | - X509KeyUsageFlags.KeyCertSign; - - foreach (X509Extension extension in certificate.Extensions) - { - if (extension.Oid.Value == "2.5.29.15") - { - var kuExt = (X509KeyUsageExtension)extension; - - if ((kuExt.KeyUsages & kSufficientFlags) == 0) - { - return null; - } - } - } - - PublicKey encodedPublicKey = certificate.PublicKey; - string keyParameters = BitConverter.ToString( - encodedPublicKey.EncodedParameters.RawData); - byte[] keyValue = encodedPublicKey.EncodedKeyValue.RawData; - - var ecParameters = default(ECParameters); - - if (keyValue[0] != 0x04) - { - throw new InvalidOperationException("Only uncompressed points are supported"); - } - - byte[] x = new byte[(keyValue.Length - 1) / 2]; - byte[] y = new byte[x.Length]; - - Buffer.BlockCopy(keyValue, 1, x, 0, x.Length); - Buffer.BlockCopy(keyValue, 1 + x.Length, y, 0, y.Length); - - ecParameters.Q.X = x; - ecParameters.Q.Y = y; - - // New values can be determined by running the dotted-decimal OID value - // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); - - switch (keyParameters) - { - case NistP256KeyParameters: - ecParameters.Curve = ECCurve.NamedCurves.nistP256; - securityPolicyUris = [SecurityPolicies.ECC_nistP256]; - break; - case NistP384KeyParameters: - ecParameters.Curve = ECCurve.NamedCurves.nistP384; - securityPolicyUris = [SecurityPolicies.ECC_nistP384, SecurityPolicies - .ECC_nistP256]; - break; - case BrainpoolP256r1KeyParameters: - ecParameters.Curve = ECCurve.NamedCurves.brainpoolP256r1; - securityPolicyUris = [SecurityPolicies.ECC_brainpoolP256r1]; - break; - case BrainpoolP384r1KeyParameters: - ecParameters.Curve = ECCurve.NamedCurves.brainpoolP384r1; - securityPolicyUris = [SecurityPolicies.ECC_brainpoolP384r1, SecurityPolicies - .ECC_brainpoolP256r1]; - break; - default: - throw new NotImplementedException(keyParameters); - } - - return ECDsa.Create(ecParameters); - } - - /// - /// Returns the length of a ECDsa signature of a digest. - /// - /// - public static int GetSignatureLength(X509Certificate2 signingCertificate) - { - if (signingCertificate == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "No public key for certificate."); - } - using ECDsa publicKey = - GetPublicKey(signingCertificate) - ?? throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "No public key for certificate."); - - return publicKey.KeySize / 4; - } - - /// - /// Returns the hash algorithm for the specified security policy. - /// - /// is null. - /// - public static HashAlgorithmName GetSignatureAlgorithmName(string securityPolicyUri) - { - if (securityPolicyUri == null) - { - throw new ArgumentNullException(nameof(securityPolicyUri)); - } - - switch (securityPolicyUri) - { - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return HashAlgorithmName.SHA256; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - return HashAlgorithmName.SHA384; - default: - throw ServiceResultException.Unexpected( - "Unexpected security policy URI for ECC: {0}", securityPolicyUri); - } - } - - /// - /// Computes an ECDSA signature. - /// - public static byte[] Sign( - ArraySegment dataToSign, - X509Certificate2 signingCertificate, - string securityPolicyUri) - { - HashAlgorithmName algorithm = GetSignatureAlgorithmName(securityPolicyUri); - return Sign(dataToSign, signingCertificate, algorithm); - } - - /// - /// Computes an ECDSA signature. - /// - /// - public static byte[] Sign( - ArraySegment dataToSign, - X509Certificate2 signingCertificate, - HashAlgorithmName algorithm) - { -#if CURVE25519 - var publicKey = signingCertificate.BcCertificate.GetPublicKey(); - - if (publicKey is Ed25519PublicKeyParameters) - { - var signer = new Ed25519Signer(); - - signer.Init(true, signingCertificate.BcPrivateKey); - signer.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - byte[] signature = signer.GenerateSignature(); -#if DEBUG - var verifier = new Ed25519Signer(); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - - if (!verifier.VerifySignature(signature)) - { - throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Could not verify signature."); - } -#endif - return signature; - } - - if (publicKey is Ed448PublicKeyParameters) - { - var signer = new Ed448Signer(new byte[32]); - - signer.Init(true, signingCertificate.BcPrivateKey); - signer.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - byte[] signature = signer.GenerateSignature(); -#if DEBUG - var verifier = new Ed448Signer(new byte[32]); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - - if (!verifier.VerifySignature(signature)) - { - throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Could not verify signature."); - } -#endif - return signature; - } -#endif - ECDsa senderPrivateKey = - signingCertificate.GetECDsaPrivateKey() - ?? throw new ServiceResultException( - StatusCodes.BadCertificateInvalid, - "Missing private key needed for create a signature."); - - using (senderPrivateKey) - { - byte[] signature = senderPrivateKey.SignData( - dataToSign.Array, - dataToSign.Offset, - dataToSign.Count, - algorithm); - -#if DEBUGxxx - using (ECDsa ecdsa = EccUtils.GetPublicKey(new X509Certificate2(signingCertificate.RawData))) - { - if (!ecdsa.VerifyData(dataToSign.Array, dataToSign.Offset, dataToSign.Count, signature, algorithm)) - { - throw new ServiceResultException( - StatusCodes.BadSecurityChecksFailed, - "Could not verify signature."); - } - } -#endif - - return signature; - } - } - - /// - /// Verifies a ECDsa signature. - /// - public static bool Verify( - ArraySegment dataToVerify, - byte[] signature, - X509Certificate2 signingCertificate, - string securityPolicyUri) - { - return Verify( - dataToVerify, - signature, - signingCertificate, - GetSignatureAlgorithmName(securityPolicyUri)); - } - - /// - /// Verifies a ECDsa signature. - /// - public static bool Verify( - ArraySegment dataToVerify, - byte[] signature, - X509Certificate2 signingCertificate, - HashAlgorithmName algorithm) - { -#if CURVE25519 - var publicKey = signingCertificate.BcCertificate.GetPublicKey(); - - if (publicKey is Ed25519PublicKeyParameters) - { - var verifier = new Ed25519Signer(); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count); - - if (!verifier.VerifySignature(signature)) - { - return false; - } - - return true; - } - - if (publicKey is Ed448PublicKeyParameters) - { - var verifier = new Ed448Signer(new byte[32]); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count); - - if (!verifier.VerifySignature(signature)) - { - return false; - } - - return true; - } -#endif - using ECDsa ecdsa = GetPublicKey(signingCertificate); - return ecdsa.VerifyData( - dataToVerify.Array, - dataToVerify.Offset, - dataToVerify.Count, - signature, - algorithm); - } - } - - /// - /// Utility class for encrypting and decrypting secrets using Elliptic Curve Cryptography (ECC). - /// - public class EncryptedSecret - { - /// - /// Create secret - /// - public EncryptedSecret( - IServiceMessageContext context, - string securityPolicyUri, - X509Certificate2Collection senderIssuerCertificates, - X509Certificate2 receiverCertificate, - Nonce receiverNonce, - X509Certificate2 senderCertificate, - Nonce senderNonce, - CertificateValidator validator = null, - bool doNotEncodeSenderCertificate = false) - { - SenderCertificate = senderCertificate; - SenderIssuerCertificates = senderIssuerCertificates; - DoNotEncodeSenderCertificate = doNotEncodeSenderCertificate; - SenderNonce = senderNonce; - ReceiverNonce = receiverNonce; - ReceiverCertificate = receiverCertificate; - Validator = validator; - SecurityPolicyUri = securityPolicyUri; - Context = context; - } - - /// - /// Gets or sets the X.509 certificate of the sender. - /// - public X509Certificate2 SenderCertificate { get; private set; } - - /// - /// Gets or sets the collection of X.509 certificates of the sender's issuer. - /// - public X509Certificate2Collection SenderIssuerCertificates { get; private set; } - - /// - /// Gets or sets a value indicating whether the sender's certificate should not be encoded. - /// - public bool DoNotEncodeSenderCertificate { get; } - - /// - /// Gets or sets the nonce of the sender. - /// - public Nonce SenderNonce { get; private set; } - - /// - /// Gets or sets the nonce of the receiver. - /// - public Nonce ReceiverNonce { get; } - - /// - /// Gets or sets the X.509 certificate of the receiver. - /// - public X509Certificate2 ReceiverCertificate { get; } - - /// - /// Gets or sets the certificate validator. - /// - public CertificateValidator Validator { get; } - - /// - /// Gets or sets the security policy URI. - /// - public string SecurityPolicyUri { get; private set; } - - /// - /// Service message context to use - /// - public IServiceMessageContext Context { get; } - - /// - /// Encrypts a secret using the specified nonce, encrypting key, and initialization vector (IV). - /// - /// The secret to encrypt. - /// The nonce to use for encryption. - /// The key to use for encryption. - /// The initialization vector to use for encryption. - /// The encrypted secret. - /// - private byte[] EncryptSecret( - byte[] secret, - byte[] nonce, - byte[] encryptingKey, - byte[] iv) - { -#if CURVE25519 - bool useAuthenticatedEncryption = false; - if (SenderCertificate.BcCertificate.GetPublicKey() is Ed25519PublicKeyParameters - || SenderCertificate.BcCertificate.GetPublicKey() is Ed448PublicKeyParameters) - { - useAuthenticatedEncryption = true; - } -#endif - byte[] dataToEncrypt = null; - - using (var encoder = new BinaryEncoder(Context)) - { - encoder.WriteByteString(null, nonce); - encoder.WriteByteString(null, secret); - - // add padding. - int paddingSize = iv.Length - ((encoder.Position + 2) % iv.Length); - paddingSize %= iv.Length; - - if (secret.Length + paddingSize < iv.Length) - { - paddingSize += iv.Length; - } - - for (int ii = 0; ii < paddingSize; ii++) - { - encoder.WriteByte(null, (byte)(paddingSize & 0xFF)); - } - - encoder.WriteUInt16(null, (ushort)paddingSize); - - dataToEncrypt = encoder.CloseAndReturnBuffer(); - } -#if CURVE25519 - if (useAuthenticatedEncryption) - { - return EncryptWithChaCha20Poly1305(encryptingKey, iv, dataToEncrypt); - } -#endif - using (var aes = Aes.Create()) - { - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.None; - aes.Key = encryptingKey; - aes.IV = iv; - -#pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector, which could be potentially repeatable - using ICryptoTransform encryptor = aes.CreateEncryptor(); -#pragma warning restore CA5401 // Symmetric encryption uses non-default initialization vector, which could be potentially repeatable - if (dataToEncrypt.Length % encryptor.InputBlockSize != 0) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Input data is not an even number of encryption blocks."); - } - - encryptor.TransformBlock(dataToEncrypt, 0, dataToEncrypt.Length, dataToEncrypt, 0); - } - - return dataToEncrypt; - } - -#if CURVE25519 - /// - /// Encrypts the given data using the ChaCha20Poly1305 algorithm with the provided key and initialization vector (IV). - /// - /// The key used for encryption. - /// The initialization vector used for encryption. - /// The data to be encrypted. - /// The encrypted data. - private static byte[] EncryptWithChaCha20Poly1305(byte[] encryptingKey, byte[] iv, byte[] dataToEncrypt) - { - Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}"); - Utils.Trace($"EncryptIV={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 encryptor = new ChaCha20Poly1305(); - encryptor.Init(true, parameters); - - byte[] ciphertext = new byte[encryptor.GetOutputSize(dataToEncrypt.Length)]; - int length = encryptor.ProcessBytes(dataToEncrypt, 0, dataToEncrypt.Length, ciphertext, 0); - length += encryptor.DoFinal(ciphertext, length); - - if (ciphertext.Length != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"CipherText not the expected size. [{ciphertext.Length} != {length}]"); - } - - return ciphertext; - } - - /// - /// Decrypts the given data using the ChaCha20Poly1305 algorithm with the provided key and initialization vector (IV). - /// - /// The key used for encryption. - /// The initialization vector used for encryption. - /// The data to be decrypted. - /// The offset in the data to start decrypting from. - /// The number of bytes to decrypt. - /// An containing the decrypted data. - /// Thrown if the plaintext is not the expected size or too short, or if the nonce is invalid. - private ArraySegment DecryptWithChaCha20Poly1305( - byte[] encryptingKey, - byte[] iv, - byte[] dataToDecrypt, - int offset, - int count) - { - Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}"); - Utils.Trace($"EncryptIV={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 decryptor = new ChaCha20Poly1305(); - decryptor.Init(false, parameters); - - byte[] plaintext = new byte[decryptor.GetOutputSize(count)]; - int length = decryptor.ProcessBytes(dataToDecrypt, offset, count, plaintext, 0); - length += decryptor.DoFinal(plaintext, length); - - if (plaintext.Length != length || plaintext.Length < iv.Length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"PlainText not the expected size or too short. [{count} != {length}]"); - } - - ushort paddingSize = plaintext[length - 1]; - paddingSize <<= 8; - paddingSize += plaintext[length - 2]; - - int notvalid = (paddingSize < length) ? 0 : 1; - int start = length - paddingSize - 2; - - for (int ii = 0; ii < length - 2 && ii < paddingSize; ii++) - { - if (start < 0 || start + ii >= plaintext.Length) - { - notvalid |= 1; - continue; - } - - notvalid |= plaintext[start + ii] ^ (paddingSize & 0xFF); - } - - if (notvalid != 0) - { - throw new ServiceResultException(StatusCodes.BadNonceInvalid); - } - - return new ArraySegment(plaintext, 0, start); - } -#endif - - /// - /// Decrypts the specified data using the provided encrypting key and initialization vector (IV). - /// - /// The data to decrypt. - /// The offset in the data to start decrypting from. - /// The number of bytes to decrypt. - /// The key to use for decryption. - /// The initialization vector to use for decryption. - /// The decrypted data. - /// Thrown if the input data is not an even number of encryption blocks or if the nonce is invalid. - private static ArraySegment DecryptSecret( - byte[] dataToDecrypt, - int offset, - int count, - byte[] encryptingKey, - byte[] iv) - { -#if CURVE25519 - bool useAuthenticatedEncryption = false; - if (SenderCertificate.BcCertificate.GetPublicKey() is Ed25519PublicKeyParameters - || SenderCertificate.BcCertificate.GetPublicKey() is Ed448PublicKeyParameters) - { - useAuthenticatedEncryption = true; - } - if (useAuthenticatedEncryption) - { - return DecryptWithChaCha20Poly1305(encryptingKey, iv, dataToDecrypt, offset, count); - } -#endif - using (var aes = Aes.Create()) - { - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.None; - aes.Key = encryptingKey; - aes.IV = iv; - - using ICryptoTransform decryptor = aes.CreateDecryptor(); - if (count % decryptor.InputBlockSize != 0) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Input data is not an even number of encryption blocks."); - } - - decryptor.TransformBlock(dataToDecrypt, offset, count, dataToDecrypt, offset); - } - - ushort paddingSize = dataToDecrypt[offset + count - 1]; - paddingSize <<= 8; - paddingSize += dataToDecrypt[offset + count - 2]; - - int notvalid = paddingSize < count ? 0 : 1; - int start = offset + count - paddingSize - 2; - - for (int ii = 0; ii < count - 2 && ii < paddingSize; ii++) - { - if (start < 0 || start + ii >= dataToDecrypt.Length) - { - notvalid |= 1; - continue; - } - - notvalid |= dataToDecrypt[start + ii] ^ (paddingSize & 0xFF); - } - - if (notvalid != 0) - { - throw new ServiceResultException(StatusCodes.BadNonceInvalid); - } - - return new ArraySegment(dataToDecrypt, offset, count - paddingSize); - } - - private static readonly byte[] s_label = System.Text.Encoding.UTF8.GetBytes("opcua-secret"); - - /// - /// Creates the encrypting key and initialization vector (IV) for Elliptic Curve Cryptography (ECC) encryption or decryption. - /// - /// The security policy URI. - /// The sender nonce. - /// The receiver nonce. - /// if set to true, creates the keys for decryption; otherwise, creates the keys for encryption. - /// The encrypting key. - /// The initialization vector (IV). - private static void CreateKeysForEcc( - string securityPolicyUri, - Nonce senderNonce, - Nonce receiverNonce, - bool forDecryption, - out byte[] encryptingKey, - out byte[] iv) - { - int encryptingKeySize; - int blockSize; - HashAlgorithmName algorithmName; - - switch (securityPolicyUri) - { - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - blockSize = 16; - encryptingKeySize = 16; - algorithmName = HashAlgorithmName.SHA256; - break; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - encryptingKeySize = 32; - blockSize = 16; - algorithmName = HashAlgorithmName.SHA384; - break; - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - encryptingKeySize = 32; - blockSize = 12; - algorithmName = HashAlgorithmName.SHA256; - break; - default: - encryptingKeySize = 32; - blockSize = 16; - algorithmName = HashAlgorithmName.SHA256; - break; - } - - encryptingKey = new byte[encryptingKeySize]; - iv = new byte[blockSize]; - - byte[] keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize)); - byte[] salt = Utils.Append(keyLength, s_label, senderNonce.Data, receiverNonce.Data); - - byte[] keyData; - if (forDecryption) - { - keyData = receiverNonce.DeriveKey( - senderNonce, - salt, - algorithmName, - encryptingKeySize + blockSize); - } - else - { - keyData = senderNonce.DeriveKey( - receiverNonce, - salt, - algorithmName, - encryptingKeySize + blockSize); - } - - Buffer.BlockCopy(keyData, 0, encryptingKey, 0, encryptingKey.Length); - Buffer.BlockCopy(keyData, encryptingKeySize, iv, 0, iv.Length); - } - - /// - /// Encrypts a secret using the specified nonce. - /// - /// The secret to encrypt. - /// The nonce to use for encryption. - /// The encrypted secret. - public byte[] Encrypt(byte[] secret, byte[] nonce) - { - byte[] encryptingKey = null; - byte[] iv = null; - byte[] message = null; - int lengthPosition = 0; - - int signatureLength = EccUtils.GetSignatureLength(SenderCertificate); - - using (var encoder = new BinaryEncoder(Context)) - { - // write header. - encoder.WriteNodeId(null, DataTypeIds.EccEncryptedSecret); - encoder.WriteByte(null, (byte)ExtensionObjectEncoding.Binary); - - lengthPosition = encoder.Position; - encoder.WriteUInt32(null, 0); - - encoder.WriteString(null, SecurityPolicyUri); - - byte[] senderCertificate = null; - - if (!DoNotEncodeSenderCertificate) - { - senderCertificate = SenderCertificate.RawData; - - if (SenderIssuerCertificates != null && SenderIssuerCertificates.Count > 0) - { - int blobSize = senderCertificate.Length; - - foreach (X509Certificate2 issuer in SenderIssuerCertificates) - { - blobSize += issuer.RawData.Length; - } - - byte[] blob = new byte[blobSize]; - Buffer.BlockCopy(senderCertificate, 0, blob, 0, senderCertificate.Length); - - int pos = senderCertificate.Length; - - foreach (X509Certificate2 issuer in SenderIssuerCertificates) - { - byte[] data = issuer.RawData; - Buffer.BlockCopy(data, 0, blob, pos, data.Length); - pos += data.Length; - } - - senderCertificate = blob; - } - } - - encoder.WriteByteString(null, senderCertificate); - encoder.WriteDateTime(null, DateTime.UtcNow); - - byte[] senderNonce = SenderNonce.Data; - byte[] receiverNonce = ReceiverNonce.Data; - - encoder.WriteUInt16(null, (ushort)(senderNonce.Length + receiverNonce.Length + 8)); - encoder.WriteByteString(null, senderNonce); - encoder.WriteByteString(null, receiverNonce); - - // create keys. - if (EccUtils.IsEccPolicy(SecurityPolicyUri)) - { - CreateKeysForEcc( - SecurityPolicyUri, - SenderNonce, - ReceiverNonce, - false, - out encryptingKey, - out iv); - } - - // encrypt secret, - byte[] encryptedData = EncryptSecret(secret, nonce, encryptingKey, iv); - - // append encrypted secret. - for (int ii = 0; ii < encryptedData.Length; ii++) - { - encoder.WriteByte(null, encryptedData[ii]); - } - - // save space for signature. - for (int ii = 0; ii < signatureLength; ii++) - { - encoder.WriteByte(null, 0); - } - - message = encoder.CloseAndReturnBuffer(); - } - - int length = message.Length - lengthPosition - 4; - - message[lengthPosition++] = (byte)(length & 0xFF); - message[lengthPosition++] = (byte)((length & 0xFF00) >> 8); - message[lengthPosition++] = (byte)((length & 0xFF0000) >> 16); - message[lengthPosition++] = (byte)((length & 0xFF000000) >> 24); - - // get the algorithm used for the signature. - HashAlgorithmName signatureAlgorithm; - switch (SecurityPolicyUri) - { - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - signatureAlgorithm = HashAlgorithmName.SHA384; - break; - default: - signatureAlgorithm = HashAlgorithmName.SHA256; - break; - } - - var dataToSign = new ArraySegment(message, 0, message.Length - signatureLength); - byte[] signature = EccUtils.Sign(dataToSign, SenderCertificate, signatureAlgorithm); - Buffer.BlockCopy( - signature, - 0, - message, - message.Length - signatureLength, - signatureLength); - return message; - } - - /// - /// Verifies the header for an ECC encrypted message and returns the encrypted data. - /// - /// The data to decrypt. - /// The earliest time allowed for the message signing time. - /// The telemetry context to use to create obvservability instruments - /// The encrypted data. - /// - private ArraySegment VerifyHeaderForEcc( - ArraySegment dataToDecrypt, - DateTime earliestTime, - ITelemetryContext telemetry) - { - using var decoder = new BinaryDecoder( - dataToDecrypt.Array, - dataToDecrypt.Offset, - dataToDecrypt.Count, - Context); - NodeId typeId = decoder.ReadNodeId(null); - - if (typeId != DataTypeIds.EccEncryptedSecret) - { - throw new ServiceResultException(StatusCodes.BadDataTypeIdUnknown); - } - - var encoding = (ExtensionObjectEncoding)decoder.ReadByte(null); - - if (encoding != ExtensionObjectEncoding.Binary) - { - throw new ServiceResultException(StatusCodes.BadDataEncodingUnsupported); - } - - uint length = decoder.ReadUInt32(null); - - // get the start of data. - int startOfData = decoder.Position + dataToDecrypt.Offset; - - SecurityPolicyUri = decoder.ReadString(null); - - if (!EccUtils.IsEccPolicy(SecurityPolicyUri)) - { - throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); - } - - // get the algorithm used for the signature. - HashAlgorithmName signatureAlgorithm; - - switch (SecurityPolicyUri) - { - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - signatureAlgorithm = HashAlgorithmName.SHA384; - break; - default: - signatureAlgorithm = HashAlgorithmName.SHA256; - break; - } - - // extract the send certificate and any chain. - byte[] senderCertificate = decoder.ReadByteString(null); - - if (senderCertificate == null || senderCertificate.Length == 0) - { - if (SenderCertificate == null) - { - throw new ServiceResultException(StatusCodes.BadCertificateInvalid); - } - } - else - { - X509Certificate2Collection senderCertificateChain = Utils.ParseCertificateChainBlob( - senderCertificate, - telemetry); - - SenderCertificate = senderCertificateChain[0]; - SenderIssuerCertificates = []; - - for (int ii = 1; ii < senderCertificateChain.Count; ii++) - { - SenderIssuerCertificates.Add(senderCertificateChain[ii]); - } - - // validate the sender. - Validator?.ValidateAsync(senderCertificateChain, default).GetAwaiter().GetResult(); - } - - // extract the send certificate and any chain. - DateTime signingTime = decoder.ReadDateTime(null); - - if (signingTime < earliestTime) - { - throw new ServiceResultException(StatusCodes.BadInvalidTimestamp); - } - - // extract the policy header. - ushort headerLength = decoder.ReadUInt16(null); - - if (headerLength == 0 || headerLength > length) - { - throw new ServiceResultException(StatusCodes.BadDecodingError); - } - - // read the policy header. - byte[] senderPublicKey = decoder.ReadByteString(null); - byte[] receiverPublicKey = decoder.ReadByteString(null); - - if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8) - { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - "Unexpected policy header length"); - } - - int startOfEncryption = decoder.Position; - - SenderNonce = Nonce.CreateNonce(SecurityPolicyUri, senderPublicKey); - - if (!Utils.IsEqual(receiverPublicKey, ReceiverNonce.Data)) - { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - "Unexpected receiver nonce."); - } - - // check the signature. - int signatureLength = EccUtils.GetSignatureLength(SenderCertificate); - - if (signatureLength >= length) - { - throw new ServiceResultException(StatusCodes.BadDecodingError); - } - - byte[] signature = new byte[signatureLength]; - Buffer.BlockCopy( - dataToDecrypt.Array, - startOfData + (int)length - signatureLength, - signature, - 0, - signatureLength); - - var dataToSign = new ArraySegment( - dataToDecrypt.Array, - 0, - startOfData + (int)length - signatureLength); - - if (!EccUtils.Verify(dataToSign, signature, SenderCertificate, signatureAlgorithm)) - { - throw new ServiceResultException( - StatusCodes.BadSecurityChecksFailed, - "Could not verify signature."); - } - - // extract the encrypted data. - return new ArraySegment( - dataToDecrypt.Array, - startOfEncryption, - (int)length - (startOfEncryption - startOfData + signatureLength)); - } - - /// - /// Decrypts the specified data using the ECC algorithm. - /// - /// The earliest time allowed for the message. - /// The expected nonce value. - /// The data to decrypt. - /// The offset of the data to decrypt. - /// The number of bytes to decrypt. - /// The telemetry context to use to create obvservability instruments - /// The decrypted data. - /// - public byte[] Decrypt( - DateTime earliestTime, - byte[] expectedNonce, - byte[] data, - int offset, - int count, - ITelemetryContext telemetry) - { - ArraySegment dataToDecrypt = VerifyHeaderForEcc( - new ArraySegment(data, offset, count), - earliestTime, - telemetry); - - CreateKeysForEcc( - SecurityPolicyUri, - SenderNonce, - ReceiverNonce, - true, - out byte[] encryptingKey, - out byte[] iv); - - ArraySegment plainText = DecryptSecret( - dataToDecrypt.Array, - dataToDecrypt.Offset, - dataToDecrypt.Count, - encryptingKey, - iv); - - using var decoder = new BinaryDecoder( - plainText.Array, - plainText.Offset, - plainText.Count, - Context); - byte[] actualNonce = decoder.ReadByteString(null); - - if (expectedNonce != null && expectedNonce.Length > 0) - { - int notvalid = expectedNonce.Length == actualNonce.Length ? 0 : 1; - - for (int ii = 0; ii < expectedNonce.Length && ii < actualNonce.Length; ii++) - { - notvalid |= expectedNonce[ii] ^ actualNonce[ii]; - } - - if (notvalid != 0) - { - throw new ServiceResultException(StatusCodes.BadNonceInvalid); - } - } - - return decoder.ReadByteString(null); - } - } -} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs new file mode 100644 index 0000000000..fbda7398d5 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs @@ -0,0 +1,577 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Opc.Ua.Bindings; +#if CURVE25519 +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +#endif + +namespace Opc.Ua +{ + /// + /// Utility class for encrypting and decrypting secrets using Elliptic Curve Cryptography (ECC). + /// + public class EncryptedSecret + { + /// + /// Create secret + /// + public EncryptedSecret( + IServiceMessageContext context, + string securityPolicyUri, + X509Certificate2Collection senderIssuerCertificates, + X509Certificate2 receiverCertificate, + Nonce receiverNonce, + X509Certificate2 senderCertificate, + Nonce senderNonce, + CertificateValidator validator = null, + bool doNotEncodeSenderCertificate = false) + { + SenderCertificate = senderCertificate; + SenderIssuerCertificates = senderIssuerCertificates; + DoNotEncodeSenderCertificate = doNotEncodeSenderCertificate; + SenderNonce = senderNonce; + ReceiverNonce = receiverNonce; + ReceiverCertificate = receiverCertificate; + Validator = validator; + SecurityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + Context = context; + + if (SecurityPolicy == null) + { + throw new ArgumentException($"Cannot resolve SecurityPolicy '{securityPolicyUri}'.", nameof(securityPolicyUri)); + } + } + + /// + /// Gets or sets the X.509 certificate of the sender. + /// + public X509Certificate2 SenderCertificate { get; private set; } + + /// + /// Gets or sets the collection of X.509 certificates of the sender's issuer. + /// + public X509Certificate2Collection SenderIssuerCertificates { get; private set; } + + /// + /// Gets or sets a value indicating whether the sender's certificate should not be encoded. + /// + public bool DoNotEncodeSenderCertificate { get; } + + /// + /// Gets or sets the nonce of the sender. + /// + public Nonce SenderNonce { get; private set; } + + /// + /// Gets or sets the nonce of the receiver. + /// + public Nonce ReceiverNonce { get; } + + /// + /// Gets or sets the X.509 certificate of the receiver. + /// + public X509Certificate2 ReceiverCertificate { get; } + + /// + /// Gets or sets the certificate validator. + /// + public CertificateValidator Validator { get; } + + /// + /// Gets or sets the security policy. + /// + public SecurityPolicyInfo SecurityPolicy { get; private set; } + + /// + /// Service message context to use + /// + public IServiceMessageContext Context { get; } + + private static readonly byte[] s_secretLabel = System.Text.Encoding.UTF8.GetBytes("opcua-secret"); + + /// + /// Creates the encrypting key and initialization vector (IV) for Elliptic Curve Cryptography (ECC) encryption or decryption. + /// + private static void CreateKeysForEcc( + SecurityPolicyInfo securityPolicy, + Nonce localNonce, + Nonce remoteNonce, + bool forDecryption, + out byte[] encryptingKey, + out byte[] iv) + { + CryptoTrace.Start(ConsoleColor.Blue, $"EncryptedSecret {((forDecryption) ? "DECRYPT" : "ENCRYPT")}"); + CryptoTrace.WriteLine($"SecurityPolicy={securityPolicy.Name}"); + CryptoTrace.WriteLine($"LocalNonce={CryptoTrace.KeyToString(localNonce?.Data)}"); + CryptoTrace.WriteLine($"RemoteNonce={CryptoTrace.KeyToString(remoteNonce?.Data)}"); + + int encryptingKeySize = securityPolicy.SymmetricEncryptionKeyLength; + int blockSize = securityPolicy.InitializationVectorLength; + + encryptingKey = new byte[encryptingKeySize]; + iv = new byte[blockSize]; + + byte[] secret = localNonce.GenerateSecret(remoteNonce, null); + byte[] keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize)); + + byte[] salt = Utils.Append( + keyLength, + s_secretLabel, + forDecryption ? remoteNonce.Data : localNonce.Data, + forDecryption ? localNonce.Data : remoteNonce.Data); + + byte[] keyData = localNonce.DeriveKeyData( + secret, + salt, + securityPolicy.KeyDerivationAlgorithm, + encryptingKeySize + blockSize); + + Buffer.BlockCopy(keyData, 0, encryptingKey, 0, encryptingKey.Length); + Buffer.BlockCopy(keyData, encryptingKeySize, iv, 0, iv.Length); + + Console.ForegroundColor = ConsoleColor.Blue; + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.Finish("EncryptedSecret"); + } + + /// + /// Encrypts a secret using the specified nonce. + /// + /// The secret to encrypt. + /// The nonce to use for encryption. + /// The encrypted secret. + public byte[] Encrypt(byte[] secret, byte[] nonce) + { + byte[] encryptingKey = null; + byte[] iv = null; + byte[] message = null; + int lengthPosition = 0; + + int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate); + + using var encoder = new BinaryEncoder(Context); + + // write header. + encoder.WriteNodeId(null, DataTypeIds.EccEncryptedSecret); + encoder.WriteByte(null, (byte)ExtensionObjectEncoding.Binary); + + lengthPosition = encoder.Position; + encoder.WriteUInt32(null, 0); + + encoder.WriteString(null, SecurityPolicy.Uri); + + byte[] senderCertificate = null; + + if (!DoNotEncodeSenderCertificate) + { + senderCertificate = SenderCertificate.RawData; + + if (SenderIssuerCertificates != null && SenderIssuerCertificates.Count > 0) + { + int blobSize = senderCertificate.Length; + + foreach (X509Certificate2 issuer in SenderIssuerCertificates) + { + blobSize += issuer.RawData.Length; + } + + byte[] blob = new byte[blobSize]; + Buffer.BlockCopy(senderCertificate, 0, blob, 0, senderCertificate.Length); + + int pos = senderCertificate.Length; + + foreach (X509Certificate2 issuer in SenderIssuerCertificates) + { + byte[] data = issuer.RawData; + Buffer.BlockCopy(data, 0, blob, pos, data.Length); + pos += data.Length; + } + + senderCertificate = blob; + } + } + + encoder.WriteByteString(null, senderCertificate); + encoder.WriteDateTime(null, DateTime.UtcNow); + + if (ReceiverNonce?.Data == null) + { + throw new ServiceResultException( + StatusCodes.BadArgumentsMissing, + $"The receiver did not provide an ephemeral key."); + } + + byte[] senderNonce = SenderNonce.Data; + byte[] receiverNonce = ReceiverNonce.Data; + + encoder.WriteUInt16(null, (ushort)(senderNonce.Length + receiverNonce.Length + 8)); + int senderNonceStart = encoder.Position; + encoder.WriteByteString(null, senderNonce); + int senderNonceEnd = encoder.Position; + encoder.WriteByteString(null, receiverNonce); + int receiverNonceEnd = encoder.Position; + + // create keys. + CreateKeysForEcc( + SecurityPolicy, + SenderNonce, + ReceiverNonce, + false, + out encryptingKey, + out iv); + + // reserves space for padding and tag that is added by SymmetricEncryptAndSign. + int startOfSecret = encoder.Position; + encoder.WriteByteString(null, nonce); + encoder.WriteByteString(null, secret); + + int paddingCount = 0; + int tagLength = 0; + + switch (SecurityPolicy.SymmetricEncryptionAlgorithm) + { + case SymmetricEncryptionAlgorithm.Aes128Cbc: + case SymmetricEncryptionAlgorithm.Aes256Cbc: + paddingCount = GetPaddingCount(SecurityPolicy.InitializationVectorLength, secret.Length, encoder.Position - startOfSecret); + tagLength = 0; + break; + case SymmetricEncryptionAlgorithm.Aes128Gcm: + case SymmetricEncryptionAlgorithm.Aes256Gcm: + case SymmetricEncryptionAlgorithm.ChaCha20Poly1305: + paddingCount = GetPaddingCount(16, secret.Length, encoder.Position - startOfSecret); + tagLength = SecurityPolicy.SymmetricSignatureLength; + break; + } + + for (int ii = 0; ii < paddingCount; ii++) + { + encoder.WriteByte(null, (byte)paddingCount); + } + + encoder.WriteByte(null, (byte)paddingCount); + encoder.WriteByte(null, 0); + + int endOfSecret = encoder.Position; + + // reserve space for the outer padding that SymmetricEncryptAndSign will add (CBC only). + int outerPaddingSize = 0; + if (SecurityPolicy.SymmetricEncryptionAlgorithm is SymmetricEncryptionAlgorithm.Aes128Cbc or SymmetricEncryptionAlgorithm.Aes256Cbc) + { + int blockSize = SecurityPolicy.InitializationVectorLength; + int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1; + int paddingSize = blockSize - ((endOfSecret - startOfSecret + paddingByteSize) % blockSize); + paddingSize %= blockSize; + outerPaddingSize = paddingSize + paddingByteSize; + + for (int ii = 0; ii < outerPaddingSize; ii++) + { + encoder.WriteByte(null, 0xCD); + } + } + + // save space for tag. + for (int ii = 0; ii < tagLength; ii++) + { + encoder.WriteByte(null, 0xAB); + } + + // save space for signature. + for (int ii = 0; ii < signatureLength; ii++) + { + encoder.WriteByte(null, 0xDE); + } + + message = encoder.CloseAndReturnBuffer(); + + int length = message.Length - lengthPosition - 4; + + message[lengthPosition++] = (byte)(length & 0xFF); + message[lengthPosition++] = (byte)((length & 0xFF00) >> 8); + message[lengthPosition++] = (byte)((length & 0xFF0000) >> 16); + message[lengthPosition++] = (byte)((length & 0xFF000000) >> 24); + + _ = CryptoUtils.SymmetricEncryptAndSign( + new ArraySegment(message, startOfSecret, endOfSecret - startOfSecret), + SecurityPolicy, + encryptingKey, + iv); + + var dataToSign = new ArraySegment(message, 0, message.Length - signatureLength); + + byte[] signature = CryptoUtils.Sign( + dataToSign, + SenderCertificate, + SecurityPolicy.AsymmetricSignatureAlgorithm); + + Buffer.BlockCopy( + signature, + 0, + message, + endOfSecret + outerPaddingSize + tagLength, + signatureLength); + + return message; + } + + private int GetPaddingCount(int blockSize, int secretLength, int dataLength) + { + dataLength += 2; // add padding size + + int paddingCount = + dataLength % blockSize == 0 + ? 0 + : blockSize - dataLength % blockSize; + + if (paddingCount + secretLength < blockSize) + { + paddingCount += blockSize; + } + + return paddingCount; + } + + /// + /// Verifies the header for an ECC encrypted message and returns the encrypted data. + /// + /// The data to decrypt. + /// The earliest time allowed for the message signing time. + /// The telemetry context to use to create obvservability instruments + /// The encrypted data. + /// + private ArraySegment VerifyHeaderForEcc( + ArraySegment dataToDecrypt, + DateTime earliestTime, + ITelemetryContext telemetry) + { + using var decoder = new BinaryDecoder( + dataToDecrypt.Array, + dataToDecrypt.Offset, + dataToDecrypt.Count, + Context); + NodeId typeId = decoder.ReadNodeId(null); + + if (typeId != DataTypeIds.EccEncryptedSecret) + { + throw new ServiceResultException(StatusCodes.BadDataTypeIdUnknown); + } + + var encoding = (ExtensionObjectEncoding)decoder.ReadByte(null); + + if (encoding != ExtensionObjectEncoding.Binary) + { + throw new ServiceResultException(StatusCodes.BadDataEncodingUnsupported); + } + + int length = (int)decoder.ReadUInt32(null) + decoder.Position; + + SecurityPolicy = SecurityPolicies.GetInfo(decoder.ReadString(null)); + + if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None) + { + throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); + } + + // extract the send certificate and any chain. + byte[] senderCertificate = decoder.ReadByteString(null); + + if (senderCertificate == null || senderCertificate.Length == 0) + { + if (SenderCertificate == null) + { + throw new ServiceResultException(StatusCodes.BadCertificateInvalid); + } + } + else + { + X509Certificate2Collection senderCertificateChain = Utils.ParseCertificateChainBlob( + senderCertificate, + telemetry); + + SenderCertificate = senderCertificateChain[0]; + SenderIssuerCertificates = []; + + for (int ii = 1; ii < senderCertificateChain.Count; ii++) + { + SenderIssuerCertificates.Add(senderCertificateChain[ii]); + } + + // validate the sender. + Validator?.ValidateAsync(senderCertificateChain, default).GetAwaiter().GetResult(); + } + + // extract the send certificate and any chain. + DateTime signingTime = decoder.ReadDateTime(null); + + if (signingTime < earliestTime) + { + throw new ServiceResultException(StatusCodes.BadInvalidTimestamp); + } + + // extract the key data length. + ushort headerLength = decoder.ReadUInt16(null); + + if (headerLength == 0 || headerLength > length) + { + throw new ServiceResultException(StatusCodes.BadDecodingError); + } + + // read the key data. + int senderNonceStart = decoder.Position; + byte[] senderPublicKey = decoder.ReadByteString(null); + int senderNonceEnd = decoder.Position; + byte[] receiverPublicKey = decoder.ReadByteString(null); + int receiverNonceEnd = decoder.Position; + + if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + "Unexpected key data length"); + } + + int startOfEncryption = decoder.Position; + + SenderNonce = Nonce.CreateNonce(SecurityPolicy, senderPublicKey); + + if (!Utils.IsEqual(receiverPublicKey, ReceiverNonce.Data)) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + "Unexpected receiver nonce."); + } + + // check the signature. + int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate); + + if (signatureLength >= length) + { + throw new ServiceResultException(StatusCodes.BadDecodingError); + } + + byte[] signature = new byte[signatureLength]; + + Buffer.BlockCopy( + dataToDecrypt.Array, + dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength, + signature, + 0, + signatureLength); + + var dataToSign = new ArraySegment( + dataToDecrypt.Array, + dataToDecrypt.Offset, + dataToDecrypt.Count - signatureLength); + + if (!CryptoUtils.Verify(dataToSign, signature, SenderCertificate, SecurityPolicy.AsymmetricSignatureAlgorithm)) + { + throw new ServiceResultException( + StatusCodes.BadSecurityChecksFailed, + "Could not verify signature."); + } + + // extract the encrypted data. + return new ArraySegment( + dataToDecrypt.Array, + dataToDecrypt.Offset + startOfEncryption, + dataToDecrypt.Count - startOfEncryption - signatureLength); + } + + /// + /// Decrypts the specified data using the ECC algorithm. + /// + /// The earliest time allowed for the message. + /// The expected nonce value. + /// The data to decrypt. + /// The offset of the data to decrypt. + /// The number of bytes to decrypt. + /// The telemetry context to use to create obvservability instruments + /// The decrypted data. + /// + public byte[] Decrypt( + DateTime earliestTime, + byte[] expectedNonce, + byte[] data, + int offset, + int count, + ITelemetryContext telemetry) + { + ArraySegment dataToDecrypt = VerifyHeaderForEcc( + new ArraySegment(data, offset, count), + earliestTime, + telemetry); + + CreateKeysForEcc( + SecurityPolicy, + ReceiverNonce, + SenderNonce, + true, + out byte[] encryptingKey, + out byte[] iv); + + ArraySegment plainText = CryptoUtils.SymmetricDecryptAndVerify( + dataToDecrypt, + SecurityPolicy, + encryptingKey, + iv); + + using var decoder = new BinaryDecoder( + plainText.Array, + plainText.Offset + dataToDecrypt.Offset, + plainText.Count - dataToDecrypt.Offset, + Context); + + byte[] actualNonce = decoder.ReadByteString(null); + + if (expectedNonce != null && expectedNonce.Length > 0) + { + int notvalid = expectedNonce.Length == actualNonce.Length ? 0 : 1; + + for (int ii = 0; ii < expectedNonce.Length && ii < actualNonce.Length; ii++) + { + notvalid |= expectedNonce[ii] ^ actualNonce[ii]; + } + + if (notvalid != 0) + { + throw new ServiceResultException(StatusCodes.BadNonceInvalid); + } + } + + var key = decoder.ReadByteString(null); + var paddingCount = decoder.ReadByte(null); + + int error = 0; + + for (int ii = 0; ii < paddingCount; ii++) + { + var padding = decoder.ReadByte(null); + error |= (padding & ~paddingCount); + } + + var highByte = decoder.ReadByte(null); + + if (error != 0 || highByte != 0) + { + throw new ServiceResultException(StatusCodes.BadDecodingError); + } + + return key; + } + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs index 37bad43a6f..53e4b220d3 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs @@ -28,8 +28,11 @@ * ======================================================================*/ using System; +using System.Collections.Generic; +using System.Numerics; using System.Runtime.Serialization; using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; #if CURVE25519 using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.X509; @@ -48,18 +51,20 @@ namespace Opc.Ua /// /// Represents a cryptographic nonce used for secure communication. /// - [Serializable] - public class Nonce : IDisposable, ISerializable + public class Nonce : IDisposable { + private ECDiffieHellman m_ecdh; + private RSADiffieHellman m_rsadh; + private static readonly RandomNumberGenerator s_rng = RandomNumberGenerator.Create(); + private static uint s_minNonceLength = 32; + /// /// Constructor /// private Nonce() { m_ecdh = null; -#if CURVE25519 - m_bcKeyPair = null; -#endif + m_rsadh = null; } /// @@ -67,162 +72,212 @@ private Nonce() /// public byte[] Data { get; private set; } + internal byte[] GenerateSecret( + Nonce remoteNonce, + byte[] previousSecret) + { + byte[] ikm = null; + CryptoTrace.Start(ConsoleColor.Cyan, $"GenerateSecret"); + +#if NET8_0_OR_GREATER +#if xDEBUG + Span privateKey = stackalloc char[2048]; + + if (m_ecdh != null && m_ecdh.TryExportECPrivateKeyPem(privateKey, out int charsWritten)) + { + CryptoTrace.WriteLine($"Private Key PEM ({charsWritten} chars):"); + } +#endif + + if (m_ecdh != null) + { + ikm = m_ecdh.DeriveRawSecretAgreement(remoteNonce.m_ecdh.PublicKey); + } + else if (m_rsadh != null) + { + ikm = m_rsadh.DeriveRawSecretAgreement(remoteNonce.m_rsadh); + } + +#else // !NET8_0_OR_GREATER (NET78 and NET80) + if (m_ecdh != null) + { + ikm = m_ecdh.DeriveKeyMaterial(remoteNonce.m_ecdh.PublicKey); + } + else if (m_rsadh != null) + { + ikm = m_rsadh.DeriveRawSecretAgreement(remoteNonce.m_rsadh); + } +#endif + CryptoTrace.WriteLine($"IKM-Raw={CryptoTrace.KeyToString(ikm)}"); + CryptoTrace.WriteLine($"Previous-IKM={CryptoTrace.KeyToString(previousSecret)}"); + + if (ikm != null && previousSecret != null) + { + for (int ii = 0; ii < ikm.Length && ii < previousSecret.Length; ii++) + { + ikm[ii] ^= previousSecret[ii]; + } + } + CryptoTrace.WriteLine($"IKM-XOR={CryptoTrace.KeyToString(ikm)}"); + CryptoTrace.Finish("GenerateSecret"); + + return ikm; + } + /// /// Derives a key from the remote nonce, using the specified salt, hash algorithm, and length. /// - /// The remote nonce to use in key derivation. + /// The secret to use in key derivation. /// The salt to use in key derivation. /// The hash algorithm to use in key derivation. /// The length of the derived key. /// The derived key. - public byte[] DeriveKey( - Nonce remoteNonce, + public byte[] DeriveKeyData( + byte[] secret, byte[] salt, - HashAlgorithmName algorithm, + KeyDerivationAlgorithm algorithm, int length) { -#if CURVE25519 - if (m_bcKeyPair != null) - { - var localPublicKey = m_bcKeyPair.Public; + CryptoTrace.Start(ConsoleColor.DarkCyan, $"DeriveKeyData"); + CryptoTrace.WriteLine($"Secret={CryptoTrace.KeyToString(secret)}"); + CryptoTrace.WriteLine($"Salt={CryptoTrace.KeyToString(salt)}"); - if (localPublicKey is X25519PublicKeyParameters) - { - X25519Agreement agreement = new X25519Agreement(); - agreement.Init(m_bcKeyPair.Private); - - var key = new X25519PublicKeyParameters(remoteNonce.Data, 0); - byte[] secret = new byte[agreement.AgreementSize]; - agreement.CalculateAgreement(key, secret, 0); + using HMAC extract = algorithm switch + { + KeyDerivationAlgorithm.HKDFSha256 => new HMACSHA256(salt), + KeyDerivationAlgorithm.HKDFSha384 => new HMACSHA384(salt), + _ => new HMACSHA256(salt) + }; - HkdfBytesGenerator generator = new HkdfBytesGenerator(new Sha256Digest()); - generator.Init(new HkdfParameters(secret, salt, salt)); + byte[] prk = extract.ComputeHash(secret); + CryptoTrace.WriteLine($"PRK={CryptoTrace.KeyToString(prk)}"); - byte[] output = new byte[length]; - generator.GenerateBytes(output, 0, output.Length); - return output; - } + using HMAC expand = algorithm switch + { + KeyDerivationAlgorithm.HKDFSha256 => new HMACSHA256(prk), + KeyDerivationAlgorithm.HKDFSha384 => new HMACSHA384(prk), + _ => new HMACSHA256(prk) + }; - if (localPublicKey is X448PublicKeyParameters) - { - X448Agreement agreement = new X448Agreement(); - agreement.Init(m_bcKeyPair.Private); + byte[] output = new byte[length]; + byte counter = 1; - var key = new X448PublicKeyParameters(remoteNonce.Data, 0); - byte[] secret = new byte[agreement.AgreementSize]; - agreement.CalculateAgreement(key, secret, 0); + byte[] info = new byte[(expand.HashSize / 8) + salt.Length + 1]; + Buffer.BlockCopy(salt, 0, info, 0, salt.Length); + info[salt.Length] = counter++; - HkdfBytesGenerator generator = new HkdfBytesGenerator(new Sha256Digest()); - generator.Init(new HkdfParameters(secret, salt, salt)); + // computer T(1) + byte[] hash = expand.ComputeHash(info, 0, salt.Length + 1); + CryptoTrace.WriteLine($"T(1)={CryptoTrace.KeyToString(hash)}"); - byte[] output = new byte[length]; - generator.GenerateBytes(output, 0, output.Length); - return output; - } + int pos = 0; - throw new NotSupportedException(); - } -#endif - if (m_ecdh != null) + for (int ii = 0; ii < hash.Length && pos < length; ii++) { - byte[] secret = m_ecdh.DeriveKeyFromHmac( - remoteNonce.m_ecdh.PublicKey, - algorithm, - salt, - null, - null); - - byte[] output = new byte[length]; - - HMAC hmac = algorithm.Name switch - { - "SHA256" => new HMACSHA256(secret), - "SHA384" => new HMACSHA384(secret), - _ => new HMACSHA256(secret) - }; - - byte counter = 1; - - byte[] info = new byte[(hmac.HashSize / 8) + salt.Length + 1]; - Buffer.BlockCopy(salt, 0, info, 0, salt.Length); - info[salt.Length] = counter++; + output[pos++] = hash[ii]; + } - byte[] hash = hmac.ComputeHash(info, 0, salt.Length + 1); + while (pos < length) + { + Buffer.BlockCopy(hash, 0, info, 0, hash.Length); + Buffer.BlockCopy(salt, 0, info, hash.Length, salt.Length); + info[^1] = counter++; - int pos = 0; + hash = expand.ComputeHash(info, 0, info.Length); + CryptoTrace.WriteLine($"T({counter - 1})={CryptoTrace.KeyToString(hash)}"); for (int ii = 0; ii < hash.Length && pos < length; ii++) { output[pos++] = hash[ii]; } + } - while (pos < length) - { - Buffer.BlockCopy(hash, 0, info, 0, hash.Length); - Buffer.BlockCopy(salt, 0, info, hash.Length, salt.Length); - info[^1] = counter++; - - hash = hmac.ComputeHash(info, 0, info.Length); - - for (int ii = 0; ii < hash.Length && pos < length; ii++) - { - output[pos++] = hash[ii]; - } - } + CryptoTrace.WriteLine($"KeyData={CryptoTrace.KeyToString(output)}"); + CryptoTrace.Finish("DeriveKeyData"); - return output; - } + return output; + } - return Data; + /// + /// Generates a Nonce for cryptographic functions of a given length. + /// + /// The requested Nonce as a + public static Nonce CreateNonce(int length) + { + return new Nonce { Data = CreateRandomNonceData(length) }; } /// /// Creates a nonce for the specified security policy URI and nonce length. /// - /// The security policy URI. - /// A object containing the generated nonce. - /// is null. public static Nonce CreateNonce(string securityPolicyUri) { - if (securityPolicyUri == null) + var info = SecurityPolicies.GetInfo(securityPolicyUri); + return CreateNonce(info); + } + + /// + /// Creates a nonce for the specified security policy and nonce length. + /// + public static Nonce CreateNonce(SecurityPolicyInfo securityPolicy) + { + if (securityPolicy == null) { - throw new ArgumentNullException(nameof(securityPolicyUri)); + throw new ArgumentNullException(nameof(securityPolicy)); } - switch (securityPolicyUri) + switch (securityPolicy.EphemeralKeyAlgorithm) { - case SecurityPolicies.ECC_nistP256: + case CertificateKeyAlgorithm.RSADH: + return securityPolicy.MinAsymmetricKeyLength switch + { + //2048 => CreateNonce(RSADiffieHellmanGroup.FFDHE2048), + //3072 => CreateNonce(RSADiffieHellmanGroup.FFDHE3072), + //4096 => CreateNonce(RSADiffieHellmanGroup.FFDHE4096), + _ => CreateNonce(RSADiffieHellmanGroup.FFDHE3072) + }; + case CertificateKeyAlgorithm.NistP256: return CreateNonce(ECCurve.NamedCurves.nistP256); - case SecurityPolicies.ECC_nistP384: + case CertificateKeyAlgorithm.NistP384: return CreateNonce(ECCurve.NamedCurves.nistP384); - case SecurityPolicies.ECC_brainpoolP256r1: + case CertificateKeyAlgorithm.BrainpoolP256r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP256r1); - case SecurityPolicies.ECC_brainpoolP384r1: + case CertificateKeyAlgorithm.BrainpoolP384r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP384r1); -#if CURVE25519 - case SecurityPolicies.ECC_curve25519: - return CreateNonceForCurve25519(); - case SecurityPolicies.ECC_curve448: - return CreateNonceForCurve448(); -#endif default: - uint rsaNonceLength = GetNonceLength(securityPolicyUri); - return new Nonce { Data = CreateRandomNonceData(rsaNonceLength) }; + // Basic128Rsa15 keeps the legacy 16-byte nonce for compatibility. + bool enforceMinimumLength = !string.Equals( + securityPolicy.Uri, + SecurityPolicies.Basic128Rsa15, + StringComparison.Ordinal); + return new Nonce + { + Data = CreateRandomNonceData( + securityPolicy.SecureChannelNonceLength, + enforceMinimumLength) + }; } } + /// + /// Creates a new Nonce object for the specified RSA DiffieHellman group. + /// + public static Nonce CreateNonce(RSADiffieHellmanGroup group) + { + var nonce = new Nonce(); + nonce.m_rsadh = RSADiffieHellman.Create(group); + nonce.Data = nonce.m_rsadh.GetNonce(); + return nonce; + } + /// /// Creates a new Nonce object for the specified security policy URI and nonce data. /// - /// The security policy URI. - /// The nonce data. - /// A new Nonce object. - /// is null. - public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) + public static Nonce CreateNonce(SecurityPolicyInfo securityPolicy, byte[] nonceData) { - if (securityPolicyUri == null) + if (securityPolicy == null) { - throw new ArgumentNullException(nameof(securityPolicyUri)); + throw new ArgumentNullException(nameof(securityPolicy)); } if (nonceData == null) @@ -230,24 +285,30 @@ public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) throw new ArgumentNullException(nameof(nonceData)); } - var nonce = new Nonce { Data = nonceData }; + if (securityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.RSADH) + { + var nonce = new Nonce(); + nonce.m_rsadh = RSADiffieHellman.Create(nonceData); + nonce.Data = nonceData; + return nonce; + } - switch (securityPolicyUri) + switch (securityPolicy.EphemeralKeyAlgorithm) { - case SecurityPolicies.ECC_nistP256: + case CertificateKeyAlgorithm.NistP256: return CreateNonce(ECCurve.NamedCurves.nistP256, nonceData); - case SecurityPolicies.ECC_nistP384: + case CertificateKeyAlgorithm.NistP384: return CreateNonce(ECCurve.NamedCurves.nistP384, nonceData); - case SecurityPolicies.ECC_brainpoolP256r1: + case CertificateKeyAlgorithm.BrainpoolP256r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP256r1, nonceData); - case SecurityPolicies.ECC_brainpoolP384r1: + case CertificateKeyAlgorithm.BrainpoolP384r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP384r1, nonceData); - case SecurityPolicies.ECC_curve25519: + case CertificateKeyAlgorithm.Curve25519: return CreateNonceForCurve25519(nonceData); - case SecurityPolicies.ECC_curve448: + case CertificateKeyAlgorithm.Curve448: return CreateNonceForCurve448(nonceData); default: - return nonce; + return new Nonce { Data = nonceData }; } } @@ -255,8 +316,21 @@ public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) /// Generates a Nonce for cryptographic functions of a given length. /// /// The requested Nonce as a - public static byte[] CreateRandomNonceData(uint length) + public static byte[] CreateRandomNonceData(int length) + { + return CreateRandomNonceData(length, true); + } + + /// + /// Generates nonce data with optional minimum length enforcement. + /// + public static byte[] CreateRandomNonceData(int length, bool enforceMinimumLength) { + if (enforceMinimumLength && length < s_minNonceLength) + { + length = (int)s_minNonceLength; + } + byte[] randomBytes = new byte[length]; s_rng.GetBytes(randomBytes); return randomBytes; @@ -268,9 +342,9 @@ public static byte[] CreateRandomNonceData(uint length) public static bool ValidateNonce( byte[] nonce, MessageSecurityMode securityMode, - string securityPolicyUri) + SecurityPolicyInfo securityPolicy) { - return ValidateNonce(nonce, securityMode, GetNonceLength(securityPolicyUri)); + return ValidateNonce(nonce, securityMode, securityPolicy.SecureChannelNonceLength); } /// @@ -279,7 +353,7 @@ public static bool ValidateNonce( public static bool ValidateNonce( byte[] nonce, MessageSecurityMode securityMode, - uint minNonceLength) + int minNonceLength) { // no nonce needed for no security. if (securityMode == MessageSecurityMode.None) @@ -305,38 +379,6 @@ public static bool ValidateNonce( return false; } - /// - /// Returns the length of the symmetric encryption key for a security policy. - /// - public static uint GetNonceLength(string securityPolicyUri) - { - switch (securityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - return 16; - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_curve25519: - return 32; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - // Q.X + Q.Y = 32 + 32 = 64 - return 64; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - // Q.X + Q.Y = 48 + 48 = 96 - return 96; - case SecurityPolicies.ECC_curve448: - // Q.X - return 56; - default: - // Minimum nonce length by default - return s_minNonceLength; - } - } - /// /// Compare Nonce for equality. /// @@ -446,119 +488,298 @@ private static Nonce CreateNonce(ECCurve curve) return new Nonce { Data = senderNonce, m_ecdh = ecdh }; } -#if CURVE25519 /// - /// Creates a new Nonce object to be used in Curve25519 cryptography. + /// Frees any unmanaged resources. /// - /// A new Nonce object. - private static Nonce CreateNonceForCurve25519() + public void Dispose() { - SecureRandom random = new SecureRandom(); - IAsymmetricCipherKeyPairGenerator generator = new X25519KeyPairGenerator(); - generator.Init(new X25519KeyGenerationParameters(random)); - - var keyPair = generator.GenerateKeyPair(); - - byte[] senderNonce = new byte[X25519PublicKeyParameters.KeySize]; - ((X25519PublicKeyParameters)(keyPair.Public)).Encode(senderNonce, 0); - - var nonce = new Nonce() { Data = senderNonce, m_bcKeyPair = keyPair }; - - return nonce; + Dispose(true); + GC.SuppressFinalize(this); } /// - /// Creates a Nonce object using the X448 elliptic curve algorithm. + /// An overrideable version of the Dispose. /// - /// A Nonce object containing the generated nonce data and key pair. - private static Nonce CreateNonceForCurve448() + protected virtual void Dispose(bool disposing) { - SecureRandom random = new SecureRandom(); - IAsymmetricCipherKeyPairGenerator generator = new X448KeyPairGenerator(); - generator.Init(new X448KeyGenerationParameters(random)); + if (disposing) + { + if (m_ecdh != null) + { + m_ecdh.Dispose(); + m_ecdh = null; + } + } + } + } - var keyPair = generator.GenerateKeyPair(); + /// + /// The known RSA Diffie-Hellman groups. + /// + public enum RSADiffieHellmanGroup + { + /// + /// The 2048-bit finite field Diffie-Hellman ephemeral group defined in RFC 7919. + /// + FFDHE2048, - byte[] senderNonce = new byte[X448PublicKeyParameters.KeySize]; - ((X448PublicKeyParameters)(keyPair.Public)).Encode(senderNonce, 0); + /// + /// The 3072-bit finite field Diffie-Hellman ephemeral group defined in RFC 7919. + /// + FFDHE3072, - var nonce = new Nonce() { Data = senderNonce, m_bcKeyPair = keyPair }; + /// + /// The 4096-bit finite field Diffie-Hellman ephemeral group defined in RFC 7919. + /// + FFDHE4096 + } - return nonce; - } -#endif + /// + /// A RSA Diffie-Hellman key exchange implementation. + /// + public class RSADiffieHellman + { + private BigInteger m_privateKey; + private BigInteger m_publicKey; + private int m_nonceLength; + + // ffdhe2048 prime from RFC 7919 (hex, without whitespace). + // (RFC 7919 Appendix A.3 — use this canonical modulus in production.) + const string FFDHE2048_HEX = @" + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 61285C97 FFFFFFFF FFFFFFFF"; + + static readonly Lazy s_P2048 = new(() => RfcTextToBytes(FFDHE2048_HEX)); + + const int k_FFDHE2048_MinExponent = 224; + const int k_FFDHE2048_MaxExponent = 255; + + const string FFDHE3072_HEX = @" + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 611FCFDC DE355B3B 6519035B BC34F4DE F99C0238 + 61B46FC9 D6E6C907 7AD91D26 91F7F7EE 598CB0FA C186D91C + AEFE1309 85139270 B4130C93 BC437944 F4FD4452 E2D74DD3 + 64F2E21E 71F54BFF 5CAE82AB 9C9DF69E E86D2BC5 22363A0D + ABC52197 9B0DEADA 1DBF9A42 D5C4484E 0ABCD06B FA53DDEF + 3C1B20EE 3FD59D7C 25E41D2B 66C62E37 FFFFFFFF FFFFFFFF"; + + static readonly Lazy s_P3072 = new(() => RfcTextToBytes(FFDHE3072_HEX)); + + const int k_FFDHE3072_MinExponent = 275; + const int k_FFDHE3072_MaxExponent = 383; + + const string FFDHE4096_HEX = @" + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 611FCFDC DE355B3B 6519035B BC34F4DE F99C0238 + 61B46FC9 D6E6C907 7AD91D26 91F7F7EE 598CB0FA C186D91C + AEFE1309 85139270 B4130C93 BC437944 F4FD4452 E2D74DD3 + 64F2E21E 71F54BFF 5CAE82AB 9C9DF69E E86D2BC5 22363A0D + ABC52197 9B0DEADA 1DBF9A42 D5C4484E 0ABCD06B FA53DDEF + 3C1B20EE 3FD59D7C 25E41D2B 669E1EF1 6E6F52C3 164DF4FB + 7930E9E4 E58857B6 AC7D5F42 D69F6D18 7763CF1D 55034004 + 87F55BA5 7E31CC7A 7135C886 EFB4318A ED6A1E01 2D9E6832 + A907600A 918130C4 6DC778F9 71AD0038 092999A3 33CB8B7A + 1A1DB93D 7140003C 2A4ECEA9 F98D0ACC 0A8291CD CEC97DCF + 8EC9B55A 7F88A46B 4DB5A851 F44182E1 C68A007E 5E655F6A + FFFFFFFF FFFFFFFF"; + + static readonly Lazy s_P4096 = new(() => RfcTextToBytes(FFDHE4096_HEX)); + + const int k_FFDHE4096_MinExponent = 325; + const int k_FFDHE4096_MaxExponent = 511; + + private static readonly Lazy s_rng = new(() => RandomNumberGenerator.Create()); + + // Generator for FFDHE groups is 2 + static readonly BigInteger s_G = new BigInteger(2); /// - /// Custom deserialization + /// Creates a new RSADiffieHellman instance for the specified group. /// - protected Nonce(SerializationInfo info, StreamingContext context) + public static RSADiffieHellman Create(RSADiffieHellmanGroup group) { - string curveName = info.GetString("CurveName"); + int min = 0; + int max = 0; + BigInteger p; - if (curveName != null) + switch (group) { - var ecParams = new ECParameters - { - Curve = ECCurve.CreateFromFriendlyName(curveName), - Q = new ECPoint - { - X = (byte[])info.GetValue("QX", typeof(byte[])), - Y = (byte[])info.GetValue("QY", typeof(byte[])) - } - }; - m_ecdh = ECDiffieHellman.Create(ecParams); + case RSADiffieHellmanGroup.FFDHE2048: + p = s_P2048.Value; + min = k_FFDHE2048_MinExponent; + max = k_FFDHE2048_MaxExponent; + break; + case RSADiffieHellmanGroup.FFDHE3072: + p = s_P3072.Value; + min = k_FFDHE3072_MinExponent; + max = k_FFDHE3072_MaxExponent; + break; + case RSADiffieHellmanGroup.FFDHE4096: + p = s_P4096.Value; + min = k_FFDHE4096_MinExponent; + max = k_FFDHE4096_MaxExponent; + break; + default: + throw new NotSupportedException("Unsupported RSA DH finite group type."); } - Data = (byte[])info.GetValue("Data", typeof(byte[])); - } - private ECDiffieHellman m_ecdh; -#if CURVE25519 - private AsymmetricCipherKeyPair m_bcKeyPair; -#endif + var dh = new RSADiffieHellman(); - private static readonly RandomNumberGenerator s_rng = RandomNumberGenerator.Create(); - private static uint s_minNonceLength = 32; + byte[] seed = new byte[1]; + s_rng.Value.GetBytes(seed); + int keyLength = seed[0] % (max - min + 1) + min; + + byte[] key = new byte[1 + (keyLength + 7)/ 8]; + s_rng.Value.GetBytes(key); + key[key.Length - 1] = 0; + + dh.m_privateKey = new BigInteger(key); + dh.m_publicKey = BigInteger.ModPow(s_G, dh.m_privateKey, p); + dh.m_nonceLength = max + 1; + + return dh; + } /// - /// Frees any unmanaged resources. + /// Creates a new RSADiffieHellman instance from the nonce. /// - public void Dispose() + public static RSADiffieHellman Create(byte[] nonce) { - Dispose(true); - GC.SuppressFinalize(this); + var dh = new RSADiffieHellman(); + + var bytes = new byte[nonce.Length+1]; + + for (int ii = 0; ii < nonce.Length; ii++) + { + bytes[ii] = nonce[nonce.Length - ii - 1]; + } + + dh.m_publicKey = new BigInteger(bytes); + dh.m_nonceLength = nonce.Length; + + return dh; } /// - /// An overrideable version of the Dispose. + /// Returns the nonce representing the public key. /// - protected virtual void Dispose(bool disposing) + public byte[] GetNonce() { - if (disposing && m_ecdh != null) + var nonce = new byte[m_nonceLength]; + var publicKey = m_publicKey.ToByteArray(); + + for (int ii = 0; ii < publicKey.Length && ii < nonce.Length; ii++) { - m_ecdh.Dispose(); - m_ecdh = null; + nonce[nonce.Length - 1 - ii] = publicKey[ii]; } + + return nonce; } /// - /// Custom serialization + /// Derives the raw secret agreement from the remote key. /// - public void GetObjectData(SerializationInfo info, StreamingContext context) + public byte[] DeriveRawSecretAgreement(RSADiffieHellman remoteKey) { - if (m_ecdh != null) + if (m_privateKey.IsZero) + { + throw new InvalidOperationException("Private key not available."); + } + + BigInteger p; + + switch (m_nonceLength) + { + case 256: + p = s_P2048.Value; + break; + case 384: + p = s_P3072.Value; + break; + case 512: + p = s_P4096.Value; + break; + default: + throw new NotSupportedException("Unsupported RSA DH finite group type."); + } + + var shared = BigInteger.ModPow(remoteKey.m_publicKey, m_privateKey, p); + + var bytes = shared.ToByteArray(); + + if (bytes.Length < m_nonceLength) + { + var padded = new byte[m_nonceLength]; + Array.Copy(bytes, 0, padded, 0, bytes.Length); + bytes = padded; + } + else if (bytes.Length > m_nonceLength) { - ECParameters ecParams = m_ecdh.ExportParameters(false); - info.AddValue("CurveName", ecParams.Curve.Oid.FriendlyName); - info.AddValue("QX", ecParams.Q.X); - info.AddValue("QY", ecParams.Q.Y); + var trucated = new byte[m_nonceLength]; + Array.Copy(bytes, 0, trucated, 0, m_nonceLength); + bytes = trucated; } - else + + // make sure bytes are in big-endian order. + Array.Reverse(bytes); + + return bytes; + } + + private static BigInteger RfcTextToBytes(string rfcText) + { + var bytes = new List(); + var digit = new char[2]; + int pos = 0; + + bytes.Add(0); + + for (int ii = 0; ii < rfcText.Length; ii++) { - info.AddValue("CurveName", null); - info.AddValue("QX", null); - info.AddValue("QY", null); + if (char.IsWhiteSpace(rfcText[ii])) + { + continue; + } + + digit[pos++] = rfcText[ii]; + + if (pos == 2) + { + bytes.Add(Convert.ToByte(new string(digit), 16)); + pos = 0; + } } - info.AddValue("Data", Data); + + bytes.Reverse(); + var integer = new BigInteger(bytes.ToArray()); + return integer; } } } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs b/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs index ae1b90a2a7..4fa506ff06 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs @@ -207,6 +207,8 @@ private StringCollection BuildSupportedSecurityPolicies() 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.TryGetIdentifier(out uint identifier)) @@ -215,23 +217,39 @@ private StringCollection BuildSupportedSecurityPolicies() { 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); @@ -242,6 +260,8 @@ private StringCollection BuildSupportedSecurityPolicies() 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; } } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs index 71a3e70f42..afc35a9ce9 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs @@ -651,7 +651,7 @@ public static bool IsECDsaSignature(X509Certificate2 cert) /// The certificate. public static string GetECDsaQualifier(X509Certificate2 certificate) { - return EccUtils.GetECDsaQualifier(certificate); + return CryptoUtils.GetECDsaQualifier(certificate); } /// diff --git a/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs b/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs new file mode 100644 index 0000000000..880b3d0d64 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Opc.Ua +{ + /// + /// The names of additional parameters used in security-related operations. + /// + public static class AdditionalParameterNames + { + /// + /// The algorith to use for the ephemeral key used to encrypt user identity tokens. + /// + public const string ECDHPolicyUri = "ECDHPolicyUri"; + + /// + /// An ephemeral key used to encrypt user identity tokens. + /// + public const string ECDHKey = "ECDHKey"; + + /// + /// Padding bytes added to randomize the length of messages. + /// + public const string Padding = "Padding"; + } +} diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index ee3bea6ca3..867a17ae28 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -83,43 +83,137 @@ public static class SecurityPolicies /// public const string Aes256_Sha256_RsaPss = BaseUri + "Aes256_Sha256_RsaPss"; + /// + /// The URI for the RSA_DH_AES_GCM security policy. + /// + public const string RSA_DH_AesGcm = BaseUri + "RSA_DH_AesGcm"; + + /// + /// The URI for the RSA_DH_ChaChaPoly security policy. + /// + public const string RSA_DH_ChaChaPoly = BaseUri + "RSA_DH_ChaChaPoly"; + /// /// The URI for the ECC_nistP256 security policy. /// public const string ECC_nistP256 = BaseUri + "ECC_nistP256"; + /// + /// The URI for the ECC_nistP256 security policy with AES-GCM. + /// + public const string ECC_nistP256_AesGcm = ECC_nistP256 + "_AesGcm"; + + /// + /// The URI for the ECC_nistP256 security policy with ChaCha20Poly1305. + /// + public const string ECC_nistP256_ChaChaPoly = ECC_nistP256 + "_ChaChaPoly"; + /// /// The URI for the ECC_nistP384 security policy. /// public const string ECC_nistP384 = BaseUri + "ECC_nistP384"; + /// + /// The URI for the ECC_nistP384 security policy with AES-GCM. + /// + public const string ECC_nistP384_AesGcm = ECC_nistP384 + "_AesGcm"; + + /// + /// The URI for the ECC_nistP384 security policy with ChaCha20Poly1305. + /// + public const string ECC_nistP384_ChaChaPoly = ECC_nistP384 + "_ChaChaPoly"; + /// /// The URI for the ECC_brainpoolP256r1 security policy. /// public const string ECC_brainpoolP256r1 = BaseUri + "ECC_brainpoolP256r1"; + /// + /// The URI for the ECC_brainpoolP256r1 security policy with AES-GCM. + /// + public const string ECC_brainpoolP256r1_AesGcm = ECC_brainpoolP256r1 + "_AesGcm"; + + /// + /// The URI for the ECC_brainpoolP256r1 security policy with ChaCha20Poly1305. + /// + public const string ECC_brainpoolP256r1_ChaChaPoly = ECC_brainpoolP256r1 + "_ChaChaPoly"; + /// /// The URI for the ECC_brainpoolP384r1 security policy. /// public const string ECC_brainpoolP384r1 = BaseUri + "ECC_brainpoolP384r1"; /// - /// The URI for the ECC_curve25519 security policy. + /// The URI for the ECC_brainpoolP384r1 security policy with AES-GCM. + /// + public const string ECC_brainpoolP384r1_AesGcm = ECC_brainpoolP384r1 + "_AesGcm"; + + /// + /// The URI for the ECC_brainpoolP384r1 security policy with ChaCha20Poly1305. + /// + public const string ECC_brainpoolP384r1_ChaChaPoly = ECC_brainpoolP384r1 + "_ChaChaPoly"; + + /// + /// The URI for the ECC_curve25519 security policy.brainpoolP384r1_AesGcm /// public const string ECC_curve25519 = BaseUri + "ECC_curve25519"; /// - /// The URI for the ECC_curve448 security policy. + /// The URI for the ECC_curve25519 security policy with AES-GCM. + /// + public const string ECC_curve25519_AesGcm = ECC_curve25519 + "_AesGcm"; + + /// + /// The URI for the ECC_curve25519 security policy with ChaCha20Poly1305. + /// + public const string ECC_curve25519_ChaChaPoly = ECC_curve25519 + "_ChaChaPoly"; + + /// + /// The URI for the ECC_curve448 deprecated security policy. /// public const string ECC_curve448 = BaseUri + "ECC_curve448"; + /// + /// The URI for the ECC_curve448 security policy with AES-GCM. + /// + public const string ECC_curve448_AesGcm = ECC_curve448 + "_AesGcm"; + + /// + /// The URI for the ECC_curve448 security policy with ChaCha20Poly1305. + /// + public const string ECC_curve448_ChaChaPoly = ECC_curve448 + "_ChaChaPoly"; + /// /// The URI for the Https security policy. /// public const string Https = BaseUri + "Https"; + private static bool SupportsAesGcmPolicy() + { +#if NET8_0_OR_GREATER + return AesGcm.IsSupported; +#else + return false; +#endif + } + + private static bool SupportsChaCha20Poly1305Policy() + { +#if NET8_0_OR_GREATER + return ChaCha20Poly1305.IsSupported; +#else + return false; +#endif + } + private static bool IsPlatformSupportedName(string name) { + // If name contains BaseUri trim the BaseUri part + if (name.StartsWith(BaseUri, StringComparison.Ordinal)) + { + name = name.Substring(BaseUri.Length); + } + // all RSA if (name.Equals(nameof(None), StringComparison.Ordinal) || name.Equals(nameof(Basic256), StringComparison.Ordinal) || @@ -130,6 +224,16 @@ private static bool IsPlatformSupportedName(string name) return true; } + if (name.Equals(nameof(RSA_DH_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy(); + } + + if (name.Equals(nameof(RSA_DH_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy(); + } + if (name.Equals(nameof(Aes256_Sha256_RsaPss), StringComparison.Ordinal) && RsaUtils.IsSupportingRSAPssSign.Value) { @@ -140,32 +244,114 @@ private static bool IsPlatformSupportedName(string name) return Utils.IsSupportedCertificateType( ObjectTypeIds.EccNistP256ApplicationCertificateType); } + if (name.Equals(nameof(ECC_nistP256_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP256ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_nistP256_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP256ApplicationCertificateType); + } if (name.Equals(nameof(ECC_nistP384), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccNistP384ApplicationCertificateType); } + if (name.Equals(nameof(ECC_nistP384_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP384ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_nistP384_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP384ApplicationCertificateType); + } if (name.Equals(nameof(ECC_brainpoolP256r1), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); } + if (name.Equals(nameof(ECC_brainpoolP256r1_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_brainpoolP256r1_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); + } if (name.Equals(nameof(ECC_brainpoolP384r1), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); } + if (name.Equals(nameof(ECC_brainpoolP384r1_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_brainpoolP384r1_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); + } if (name.Equals(nameof(ECC_curve25519), StringComparison.Ordinal) || name.Equals(nameof(ECC_curve448), StringComparison.Ordinal)) { #if CURVE25519 return true; +#endif + } + if (name.Equals(nameof(ECC_curve25519_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448_AesGcm), StringComparison.Ordinal)) + { +#if CURVE25519 + return SupportsAesGcmPolicy(); +#endif + } + if (name.Equals(nameof(ECC_curve25519_ChaChaPoly), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448_ChaChaPoly), StringComparison.Ordinal)) + { +#if CURVE25519 + return SupportsChaCha20Poly1305Policy(); #endif } return false; } + /// + /// Returns the info object associated with the SecurityPolicyUri. + /// Supports both full URI and short name (without BaseUri prefix). + /// + public static SecurityPolicyInfo GetInfo(string securityPolicyUri) + { + if (String.IsNullOrEmpty(securityPolicyUri)) + { + return SecurityPolicyInfo.None; + } + + // Try full URI lookup first (e.g., "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256") + if (s_securityPolicyUriToInfo.Value.TryGetValue(securityPolicyUri, out SecurityPolicyInfo info) && + IsPlatformSupportedName(info.Name)) + { + return info; + } + + // Try short name lookup (e.g., "Basic256Sha256") + if (s_securityPolicyNameToInfo.Value.TryGetValue(securityPolicyUri, out info) && + IsPlatformSupportedName(info.Name)) + { + return info; + } + + return null; + } + /// /// Returns the uri associated with the display name. This includes http and all /// other supported platform security policies. @@ -303,65 +489,70 @@ public static EncryptedData Encrypt( ReadOnlySpan plainText, ILogger logger) { - var encryptedData = new EncryptedData - { - Algorithm = null, - Data = plainText.IsEmpty ? null : plainText.ToArray() - }; + var encryptedData = new EncryptedData { Algorithm = null }; // check if nothing to do. - if (plainText.IsEmpty) + if (plainText.Length == 0 || String.IsNullOrEmpty(securityPolicyUri)) { + encryptedData.Data = plainText.ToArray(); return encryptedData; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - return encryptedData; + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); } - // encrypt data. - switch (securityPolicyUri) + // check if asymmetric encryption is possible. + if (info.AsymmetricEncryptionAlgorithm != AsymmetricEncryptionAlgorithm.None) { - case Basic256: - case Basic256Sha256: - case Aes128_Sha256_RsaOaep: - encryptedData.Algorithm = SecurityAlgorithms.RsaOaep; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.OaepSHA1, - logger); - break; - case Basic128Rsa15: - encryptedData.Algorithm = SecurityAlgorithms.Rsa15; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.Pkcs1, - logger); - break; - case Aes256_Sha256_RsaPss: - encryptedData.Algorithm = SecurityAlgorithms.RsaOaepSha256; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.OaepSHA256, - logger); - break; - case ECC_nistP256: - case ECC_nistP384: - case ECC_brainpoolP256r1: - case ECC_brainpoolP384r1: - return encryptedData; - case None: - break; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + switch (info.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: + { + encryptedData.Algorithm = SecurityAlgorithms.RsaOaep; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.OaepSHA1, + logger); + break; + } + + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: + { + encryptedData.Algorithm = SecurityAlgorithms.Rsa15; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.Pkcs1, + logger); + break; + } + + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: + { + encryptedData.Algorithm = SecurityAlgorithms.RsaOaepSha256; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.OaepSHA256, + logger); + break; + } + } + } + else + { + // No asymmetric encryption is defined for this policy – return the plaintext. + encryptedData.Data = plainText.ToArray(); } return encryptedData; @@ -389,56 +580,68 @@ public static byte[] Decrypt( return dataToDecrypt.Data; } - // decrypt data. - switch (securityPolicyUri) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - case Basic256: - case Basic256Sha256: - case Aes128_Sha256_RsaOaep: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaep) - { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.OaepSHA1, - logger); - } - break; - case Basic128Rsa15: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.Rsa15) + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); + } + + // check if asymmetric encryption is possible. + if (info.AsymmetricEncryptionAlgorithm != AsymmetricEncryptionAlgorithm.None) + { + switch (info.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.Pkcs1, - logger); + if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaep) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.OaepSHA1, + logger); + } + break; } - break; - case Aes256_Sha256_RsaPss: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaepSha256) + + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.OaepSHA256, - logger); + if (dataToDecrypt.Algorithm == SecurityAlgorithms.Rsa15) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.Pkcs1, + logger); + } + break; } - break; - case ECC_nistP256: - case ECC_nistP384: - case ECC_brainpoolP256r1: - case ECC_brainpoolP384r1: - case None: - if (string.IsNullOrEmpty(dataToDecrypt.Algorithm)) + + default: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: { - return dataToDecrypt.Data; + if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaepSha256) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.OaepSHA256, + logger); + } + break; } - break; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + } + } + + if (String.IsNullOrEmpty(dataToDecrypt.Algorithm)) + { + return dataToDecrypt.Data; } throw ServiceResultException.Create( @@ -448,185 +651,279 @@ public static byte[] Decrypt( } /// - /// Signs the data using the SecurityPolicyUri and returns the signature. + /// Creates a signature using the security enhancements if required by the SecurityPolicy. /// - /// - public static SignatureData Sign( - X509Certificate2 certificate, + public static SignatureData CreateSignatureData( string securityPolicyUri, - byte[] dataToSign) + X509Certificate2 signingCertificate, + byte[] secureChannelSecret, + byte[] remoteCertificate, + byte[] remoteChannelCertificate, + byte[] localChannelCertificate, + byte[] remoteNonce, + byte[] localNonce) { var signatureData = new SignatureData(); - // check if nothing to do. - if (dataToSign == null) + // nothing more to do if no encryption. + if (string.IsNullOrEmpty(securityPolicyUri)) { return signatureData; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - return signatureData; + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); } + // create the data to sign. + byte[] dataToSign = (info.SecureChannelEnhancements) + ? Utils.Append( + secureChannelSecret ?? Array.Empty(), + remoteCertificate ?? Array.Empty(), + remoteChannelCertificate ?? Array.Empty(), + localChannelCertificate ?? Array.Empty(), + remoteNonce ?? Array.Empty(), + localNonce ?? Array.Empty()) + : + Utils.Append( + remoteCertificate ?? Array.Empty(), + remoteNonce); + + return CreateSignatureData(info, signingCertificate, dataToSign); + } + + /// + /// Creates a signature on the data provided using the SecurityPolicy. + /// + public static SignatureData CreateSignatureData( + string securityPolicyUri, + X509Certificate2 localCertificate, + byte[] dataToSign) + { + var info = GetInfo(securityPolicyUri); + return CreateSignatureData(info, localCertificate, dataToSign); + } + + /// + /// Creates a signature on the data provided using the SecurityPolicy. + /// + public static SignatureData CreateSignatureData( + SecurityPolicyInfo securityPolicy, + X509Certificate2 localCertificate, + byte[] dataToSign) + { + var signatureData = new SignatureData(); + // sign data. - switch (securityPolicyUri) + switch (securityPolicy.AsymmetricSignatureAlgorithm) { - case Basic256: - case Basic128Rsa15: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: signatureData.Algorithm = SecurityAlgorithms.RsaSha1; - signatureData.Signature = RsaUtils.Rsa_Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA1, - RSASignaturePadding.Pkcs1); break; - case Aes128_Sha256_RsaOaep: - case Basic256Sha256: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: signatureData.Algorithm = SecurityAlgorithms.RsaSha256; - signatureData.Signature = RsaUtils.Rsa_Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); break; - case Aes256_Sha256_RsaPss: + case AsymmetricSignatureAlgorithm.RsaPssSha256: signatureData.Algorithm = SecurityAlgorithms.RsaPssSha256; - signatureData.Signature = RsaUtils.Rsa_Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pss); break; - case ECC_nistP256: - case ECC_brainpoolP256r1: + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: signatureData.Algorithm = null; - signatureData.Signature = EccUtils.Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA256); break; - case ECC_nistP384: - case ECC_brainpoolP384r1: - signatureData.Algorithm = null; - signatureData.Signature = EccUtils.Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA384); - break; - case None: + case AsymmetricSignatureAlgorithm.None: signatureData.Algorithm = null; signatureData.Signature = null; - break; + return signatureData; + ; default: throw ServiceResultException.Create( StatusCodes.BadSecurityPolicyRejected, "Unsupported security policy: {0}", - securityPolicyUri); + securityPolicy.Uri); + } + + if (securityPolicy.SecureChannelEnhancements) + { + signatureData.Signature = null; } + signatureData.Signature = CryptoUtils.Sign( + new ArraySegment(dataToSign), + localCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); + return signatureData; } /// - /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// Creates a signature using the security enhancements if required by the SecurityPolicy. /// - /// - public static bool Verify( - X509Certificate2 certificate, + public static bool VerifySignatureData( + SignatureData signature, string securityPolicyUri, - byte[] dataToVerify, - SignatureData signature) + X509Certificate2 signingCertificate, + byte[] secureChannelSecret, + byte[] localCertificate, + byte[] localChannelCertificate, + byte[] remoteChannelCertificate, + byte[] localNonce, + byte[] remoteNonce) { - // check if nothing to do. - if (signature == null) + var signatureData = new SignatureData(); + + // nothing more to do if no encryption. + if (string.IsNullOrEmpty(securityPolicyUri)) { return true; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); + } + + // create the data to sign. + byte[] dataToVerify = (info.SecureChannelEnhancements) + ? Utils.Append( + secureChannelSecret ?? Array.Empty(), + localCertificate ?? Array.Empty(), + localChannelCertificate ?? Array.Empty(), + remoteChannelCertificate ?? Array.Empty(), + localNonce ?? Array.Empty(), + remoteNonce ?? Array.Empty()) + : + Utils.Append( + localCertificate ?? Array.Empty(), + localNonce); + + return VerifySignatureData(signature, info, signingCertificate, dataToVerify); + } + + /// + /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// + public static bool VerifySignatureData( + SignatureData signature, + string securityPolicyUri, + X509Certificate2 signingCertificate, + byte[] dataToVerify) + { + var info = GetInfo(securityPolicyUri); + return VerifySignatureData(signature, info, signingCertificate, dataToVerify); + } + + /// + /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// + public static bool VerifySignatureData( + SignatureData signature, + SecurityPolicyInfo securityPolicy, + X509Certificate2 signingCertificate, + byte[] dataToVerify) + { + // check if nothing to do. + if (signature == null) { return true; } - // decrypt data. - switch (securityPolicyUri) + // sign data. + switch (securityPolicy.AsymmetricSignatureAlgorithm) { - case Basic256: - case Basic128Rsa15: + // always accept signatures if security is not used. + case AsymmetricSignatureAlgorithm.None: + return true; + + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + { if (signature.Algorithm == SecurityAlgorithms.RsaSha1) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Basic256/Basic128Rsa15: {0}\n" + - "Expected signature algorithm: {1}", - signature.Algorithm, - SecurityAlgorithms.RsaSha1); - case Aes128_Sha256_RsaOaep: - case Basic256Sha256: - if (signature.Algorithm == SecurityAlgorithms.RsaSha256) + break; + } + + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + { + if (String.IsNullOrEmpty(signature.Algorithm) || signature.Algorithm == SecurityAlgorithms.RsaSha256) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Basic256Sha256/Aes128_Sha256_RsaOaep: {0}\n" + - "Expected signature algorithm: {1}", - signature.Algorithm, - SecurityAlgorithms.RsaSha256); - case Aes256_Sha256_RsaPss: - if (signature.Algorithm == SecurityAlgorithms.RsaPssSha256) + break; + } + + case AsymmetricSignatureAlgorithm.RsaPssSha256: + { + if (String.IsNullOrEmpty(signature.Algorithm) || signature.Algorithm == SecurityAlgorithms.RsaPssSha256) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Aes256_Sha256_RsaPss: {0}\n" + - "Expected signature algorithm : {1}", - signature.Algorithm, - SecurityAlgorithms.RsaPssSha256); - case ECC_nistP256: - case ECC_brainpoolP256r1: - return EccUtils.Verify( - new ArraySegment(dataToVerify), - signature.Signature, - certificate, - HashAlgorithmName.SHA256); - case ECC_nistP384: - case ECC_brainpoolP384r1: - return EccUtils.Verify( - new ArraySegment(dataToVerify), - signature.Signature, - certificate, - HashAlgorithmName.SHA384); - // always accept signatures if security is not used. - case None: - return true; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + break; + } + + case AsymmetricSignatureAlgorithm.EcdsaSha256: + { + if (String.IsNullOrEmpty(signature.Algorithm) || signature.Algorithm == securityPolicy.Uri) + { + return CryptoUtils.Verify( + new ArraySegment(dataToVerify), + signature.Signature, + signingCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); + } + + break; + } + + case AsymmetricSignatureAlgorithm.EcdsaSha384: + { + if (String.IsNullOrEmpty(signature.Algorithm) || signature.Algorithm == securityPolicy.Uri) + { + return CryptoUtils.Verify( + new ArraySegment(dataToVerify), + signature.Signature, + signingCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); + } + + break; + } } + + throw ServiceResultException.Create( + StatusCodes.BadSecurityChecksFailed, + "Unexpected SignatureData algorithm: {0}", + signature.Algorithm); } /// @@ -669,6 +966,63 @@ public static bool Verify( return keyValuePairs.ToFrozenDictionary(); #else return new ReadOnlyDictionary(keyValuePairs); +#endif + }); + + /// + /// Creates a dictionary of uris to SecurityPolicyInfo excluding base uri + /// + private static readonly Lazy> s_securityPolicyUriToInfo = + new(() => + { +#if NET8_0_OR_GREATER + return s_securityPolicyNameToInfo.Value.ToFrozenDictionary(k => k.Value.Uri, k => k.Value); +#else + return new ReadOnlyDictionary( + s_securityPolicyNameToInfo.Value.ToDictionary(k => k.Value.Uri, k => k.Value)); +#endif + }); + + /// + /// Creates a dictionary for names to SecurityPolicyInfo excluding base uri + /// + private static readonly Lazy> s_securityPolicyNameToInfo = + new(() => + { + FieldInfo[] policyFields = typeof(SecurityPolicies).GetFields( + BindingFlags.Public | BindingFlags.Static); + + FieldInfo[] infoFields = typeof(SecurityPolicyInfo).GetFields( + BindingFlags.Public | BindingFlags.Static); + + var keyValuePairs = new Dictionary(); + foreach (FieldInfo field in policyFields) + { + string policyUri = (string)field.GetValue(typeof(SecurityPolicies)); + if (field.Name == nameof(BaseUri) || + field.Name == nameof(Https) || + !policyUri.StartsWith(BaseUri, StringComparison.Ordinal)) + { + continue; + } + + // Find the corresponding SecurityPolicyInfo field by name + FieldInfo infoField = Array.Find(infoFields, f => f.Name == field.Name); + if (infoField != null && infoField.FieldType == typeof(SecurityPolicyInfo)) + { + SecurityPolicyInfo info = (SecurityPolicyInfo)infoField.GetValue(null); + keyValuePairs.Add(field.Name, info); + } + else + { + // Fallback to creating a minimal instance for unknown policies + keyValuePairs.Add(field.Name, new SecurityPolicyInfo(policyUri, field.Name)); + } + } +#if NET8_0_OR_GREATER + return keyValuePairs.ToFrozenDictionary(); +#else + return new ReadOnlyDictionary(keyValuePairs); #endif }); } diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs new file mode 100644 index 0000000000..786f714230 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -0,0 +1,1368 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace Opc.Ua +{ + /// + /// Defines constants for key security policies. + /// + public class SecurityPolicyInfo + { + /// + /// Creates a new instance of the class. + /// + /// The unique identifier. + /// The display name. + /// + public SecurityPolicyInfo(string uri, string name = null) + { + if (string.IsNullOrEmpty(uri)) + { + throw new ArgumentException("The URI is not a valid security policy.", nameof(uri)); + } + + Uri = uri; + Name = name ?? SecurityPolicies.GetDisplayName(uri) ?? uri; + } + + /// + /// Short name for the policy. + /// + public string Name { get; } + + /// + /// The unique identifier for the policy. + /// + public string Uri { get; } + + /// + /// Returns true if the policy is considered deprecated and should not be used for new deployments. + /// + public bool IsDeprecated { get; private set; } + + /// + /// The symmetric signature algorithm to use. + /// + public SymmetricSignatureAlgorithm SymmetricSignatureAlgorithm { get; private set; } + + /// + /// The symmetric encryption algorithm to use. + /// + public SymmetricEncryptionAlgorithm SymmetricEncryptionAlgorithm { get; private set; } + + /// + /// The asymmetric signature algorithm to use. + /// + public AsymmetricSignatureAlgorithm AsymmetricSignatureAlgorithm { get; private set; } + + /// + /// The symmetric encryption algorithm to use. + /// + public AsymmetricEncryptionAlgorithm AsymmetricEncryptionAlgorithm { get; private set; } + + /// + /// The minimum length, in bits, for an asymmetric key. + /// + public int MinAsymmetricKeyLength { get; private set; } + + /// + /// The maximum length, in bits, for an asymmetric key. + /// + public int MaxAsymmetricKeyLength { get; private set; } + + /// + /// The key derivation algorithm to use. + /// + public KeyDerivationAlgorithm KeyDerivationAlgorithm { get; private set; } + + /// + /// The length in bytes of the derived key used for message authentication. + /// + public int DerivedSignatureKeyLength { get; private set; } + + /// + /// The asymmetric signature algorithm used to sign certificates. + /// + public AsymmetricSignatureAlgorithm CertificateSignatureAlgorithm { get; private set; } + + /// + /// Returns algorithm family used to create asymmetric key pairs used with Certificates. + /// + public CertificateKeyFamily CertificateKeyFamily { get; private set; } + + /// + /// The algorithm used to create asymmetric key pairs used with Certificates. + /// + public CertificateKeyAlgorithm CertificateKeyAlgorithm { get; private set; } + + /// + /// The algorithm used to create asymmetric key pairs used for EphemeralKeys. + /// + public CertificateKeyAlgorithm EphemeralKeyAlgorithm { get; private set; } + + /// + /// The algorithm used to calculate the thumbprint of the certificate. + /// + public CertificateThumbprintAlgorithm CertificateThumbprintAlgorithm { get; private set; } + + /// + /// The length, in bytes, of the Nonces used when opening a SecureChannel. + /// + public int SecureChannelNonceLength { get; private set; } + + /// + /// The length, in bytes, of the data used to initialize the symmetric algorithm. + /// + public int InitializationVectorLength { get; private set; } + + /// + /// The length, in bytes, of the symmetric signature. + /// + public int SymmetricSignatureLength { get; private set; } + + /// + /// The length, in bytes, of the symmetric encryption key. + /// + public int SymmetricEncryptionKeyLength { get; private set; } + + /// + /// If TRUE, the 1024 based SequenceNumber rules apply to the SecurityPolicy. + /// If FALSE, the 0 based SequenceNumber rules apply. + /// + public bool LegacySequenceNumbers { get; private set; } + + /// + /// If TRUE, the enhancements to the SecureChannel are required for the SecurityPolicy. + /// • Channel-bound Signature calculations in CreateSession/ActivateSession; + /// • Session transfer tokens in ActivateSession; + /// • Chained symmetric key derivation when renewing SecureChannels. + /// • Allow padding when using Authenticated Encryption; + /// + public bool SecureChannelEnhancements { get; private set; } + + /// + /// Whether the padding is required with symmetric encryption. + /// + public bool NoSymmetricEncryptionPadding => + SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.Aes256Gcm || + SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.Aes128Gcm || + SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305; + + /// + /// Returns the derived server key data length. + /// + public int ServerKeyDataLength => + (DerivedSignatureKeyLength + SymmetricEncryptionKeyLength + InitializationVectorLength); + + /// + /// Returns the derived client key data length. + /// + public int ClientKeyDataLength => + (DerivedSignatureKeyLength + SymmetricEncryptionKeyLength + InitializationVectorLength); + + /// + /// Returns the data to be signed by the server when creating a session. + /// + public byte[] GetUserTokenSignatureData( + byte[] channelThumbprint, + byte[] serverNonce, + byte[] serverCertificate, + byte[] serverChannelCertificate, + byte[] clientCertificate, + byte[] clientChannelCertificate, + byte[] clientNonce) + { + byte[] data = null; + + CryptoTrace.Start(ConsoleColor.Yellow, "UserTokenSignatureData"); + + if (SecureChannelEnhancements) + { + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(channelThumbprint)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + CryptoTrace.WriteLine($"ServerCertificate={CryptoTrace.KeyToString(serverCertificate)}"); + CryptoTrace.WriteLine($"ServerChannelCertificate={CryptoTrace.KeyToString(serverChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientCertificate={CryptoTrace.KeyToString(clientCertificate)}"); + CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + + using HashAlgorithm hash = CertificateThumbprintAlgorithm switch + { + CertificateThumbprintAlgorithm.SHA256 => SHA256.Create(), + CertificateThumbprintAlgorithm.SHA384 => SHA384.Create(), + CertificateThumbprintAlgorithm.SHA512 => SHA512.Create(), + _ => throw new NotSupportedException() + }; + + var serverCertificateHash = serverCertificate != null ? hash.ComputeHash(serverCertificate) : null; + var serverChannelCertificateHash = serverChannelCertificate != null ? hash.ComputeHash(serverChannelCertificate) : null; + var clientCertificateHash = clientCertificate != null ? hash.ComputeHash(clientCertificate) : null; + var clientChannelCertificateHash = clientChannelCertificate != null ? hash.ComputeHash(clientChannelCertificate) : null; + + data = Utils.Append( + channelThumbprint, + serverNonce, + serverCertificateHash, + serverChannelCertificateHash, + clientCertificateHash, + clientChannelCertificateHash, + clientNonce); + } + else + { + CryptoTrace.WriteLine($"ServerCertificate={CryptoTrace.KeyToString(serverCertificate)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + + data = Utils.Append( + serverCertificate, + serverNonce); + } + + CryptoTrace.Finish("UserTokenSignatureData"); + return data; + } + + /// + /// Returns the data to be signed by the server when creating a session. + /// + public byte[] GetServerSignatureData( + byte[] channelThumbprint, + byte[] clientNonce, + byte[] serverChannelCertificate, + byte[] clientCertificate, + byte[] clientChannelCertificate, + byte[] serverNonce) + { + byte[] data = null; + + CryptoTrace.Start(ConsoleColor.Yellow, "ServerSignatureData"); + + if (SecureChannelEnhancements) + { + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(channelThumbprint)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + CryptoTrace.WriteLine($"ServerChannelCertificate={CryptoTrace.KeyToString(serverChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + + using HashAlgorithm hash = CertificateThumbprintAlgorithm switch + { + CertificateThumbprintAlgorithm.SHA256 => SHA256.Create(), + CertificateThumbprintAlgorithm.SHA384 => SHA384.Create(), + CertificateThumbprintAlgorithm.SHA512 => SHA512.Create(), + _ => throw new NotSupportedException() + }; + + var serverChannelCertificateHash = serverChannelCertificate != null ? hash.ComputeHash(serverChannelCertificate) : null; + var clientChannelCertificateHash = clientChannelCertificate != null ? hash.ComputeHash(clientChannelCertificate) : null; + + data = Utils.Append( + channelThumbprint, + clientNonce, + serverChannelCertificateHash, + clientChannelCertificateHash, + serverNonce); + } + else + { + CryptoTrace.WriteLine($"ClientCertificate={CryptoTrace.KeyToString(clientCertificate)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + + data = Utils.Append( + clientCertificate, + clientNonce); + } + + CryptoTrace.Finish("ServerSignatureData"); + return data; + } + + /// + /// Returns the data to be signed by the client when creating a session. + /// + public byte[] GetClientSignatureData( + byte[] channelThumbprint, + byte[] serverNonce, + byte[] serverCertificate, + byte[] serverChannelCertificate, + byte[] clientChannelCertificate, + byte[] clientNonce) + { + byte[] data = null; + + CryptoTrace.Start(ConsoleColor.Yellow, "ClientSignatureData"); + + if (SecureChannelEnhancements) + { + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(channelThumbprint)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + CryptoTrace.WriteLine($"ServerCertificate={CryptoTrace.KeyToString(serverCertificate)}"); + CryptoTrace.WriteLine($"ServerChannelCertificate={CryptoTrace.KeyToString(serverChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + + using HashAlgorithm hash = CertificateThumbprintAlgorithm switch + { + CertificateThumbprintAlgorithm.SHA256 => SHA256.Create(), + CertificateThumbprintAlgorithm.SHA384 => SHA384.Create(), + CertificateThumbprintAlgorithm.SHA512 => SHA512.Create(), + _ => throw new NotSupportedException() + }; + + var serverCertificateHash = serverCertificate != null ? hash.ComputeHash(serverCertificate) : null; + var serverChannelCertificateHash = serverChannelCertificate != null ? hash.ComputeHash(serverChannelCertificate) : null; + var clientChannelCertificateHash = clientChannelCertificate != null ? hash.ComputeHash(clientChannelCertificate) : null; + + data = Utils.Append( + channelThumbprint, + serverNonce, + serverCertificateHash, + serverChannelCertificateHash, + clientChannelCertificateHash, + clientNonce); + } + else + { + CryptoTrace.WriteLine($"ServerCertificate={CryptoTrace.KeyToString(serverCertificate)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + + data = Utils.Append( + serverCertificate, + serverNonce); + } + + CryptoTrace.Finish("ClientSignatureData"); + return data; + } + + /// + /// Returns a HMAC based on the symmetric signature algorithm. + /// + public HMAC CreateSignatureHmac(byte[] signingKey) + { +#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms + return SymmetricSignatureAlgorithm switch + { + SymmetricSignatureAlgorithm.HmacSha1 => new HMACSHA1(signingKey), + SymmetricSignatureAlgorithm.HmacSha256 => new HMACSHA256(signingKey), + SymmetricSignatureAlgorithm.HmacSha384 => new HMACSHA384(signingKey), + _ => null + }; +#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms + } + + /// + /// Returns a HashAlgorithmName based on the KeyDerivationAlgorithm. + /// + public HashAlgorithmName GetKeyDerivationHashAlgorithmName() + { + return KeyDerivationAlgorithm switch + { + KeyDerivationAlgorithm.PSha1 => HashAlgorithmName.SHA1, + KeyDerivationAlgorithm.PSha256 => HashAlgorithmName.SHA256, + KeyDerivationAlgorithm.HKDFSha256 => HashAlgorithmName.SHA256, + KeyDerivationAlgorithm.HKDFSha384 => HashAlgorithmName.SHA384, + _ => HashAlgorithmName.SHA256 + }; + } + + /// + /// The security policy that does not provide any security. + /// + public static readonly SecurityPolicyInfo None = new(SecurityPolicies.None) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 0, + InitializationVectorLength = 0, + SymmetricSignatureLength = 0, + MinAsymmetricKeyLength = 0, + MaxAsymmetricKeyLength = 0, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.None, + CertificateKeyFamily = CertificateKeyFamily.None, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.None, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.None, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.None, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.None, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.None, + SecureChannelEnhancements = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 + }; + + /// + /// The security policy that uses SHA1 and 128 bit encryption. This policy is considered insecure and should not be used for new deployments. + /// + public static readonly SecurityPolicyInfo Basic128Rsa15 = new(SecurityPolicies.Basic128Rsa15) + { + DerivedSignatureKeyLength = 128 / 8, + SymmetricEncryptionKeyLength = 128 / 8, + // HMAC-SHA1 produces a 160-bit MAC + SymmetricSignatureLength = 160 / 8, + InitializationVectorLength = 128 / 8, + MinAsymmetricKeyLength = 1024, + MaxAsymmetricKeyLength = 2048, + SecureChannelNonceLength = 16, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha1, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha1, + IsDeprecated = true, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 + }; + + /// + /// The security policy that uses SHA1 and 256 bit encryption. This policy is considered insecure and should not be used for new deployments. + /// + public static readonly SecurityPolicyInfo Basic256 = new(SecurityPolicies.Basic256) + { + DerivedSignatureKeyLength = 192 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + // HMAC-SHA1 produces a 160-bit MAC + SymmetricSignatureLength = 160 / 8, + InitializationVectorLength = 128 / 8, + MinAsymmetricKeyLength = 1024, + MaxAsymmetricKeyLength = 2048, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha1, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha1, + IsDeprecated = true, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 + }; + + /// + /// Aes128_Sha256_RsaOaep is a required minimum security policy. It uses SHA256 and 128 bit encryption. + /// + public static readonly SecurityPolicyInfo Aes128_Sha256_RsaOaep = new(SecurityPolicies.Aes128_Sha256_RsaOaep) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 128 / 8, + SymmetricSignatureLength = 256 / 8, + InitializationVectorLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 + }; + + /// + /// Basic256Sha256 is a required minimum security policy. It uses SHA256 and 256 bit encryption. + /// + public static readonly SecurityPolicyInfo Basic256Sha256 = new(SecurityPolicies.Basic256Sha256) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + SymmetricSignatureLength = 256 / 8, + InitializationVectorLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 + }; + + /// + /// Aes256_Sha256_RsaPss is a optional high security policy. It uses SHA256 and 256 bit encryption. + /// + public static readonly SecurityPolicyInfo Aes256_Sha256_RsaPss = new(SecurityPolicies.Aes256_Sha256_RsaPss) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha256, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPssSha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 256 / 8, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 + }; + + /// + /// ECC curve25519 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve25519 = new(SecurityPolicies.ECC_curve25519) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = false, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// ECC curve25519 is a required minimum security policy. It uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve25519_AesGcm = new(SecurityPolicies.ECC_curve25519_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// ECC curve25519 is a required minimum security policy. It uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve25519_ChaChaPoly = new(SecurityPolicies.ECC_curve25519_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// ECC curve448 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve448 = new(SecurityPolicies.ECC_curve448) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 456, + MaxAsymmetricKeyLength = 456, + SecureChannelNonceLength = 56, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = false, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// ECC curve448 is a required minimum security policy. It uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve448_AesGcm = new(SecurityPolicies.ECC_curve448_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 456, + MaxAsymmetricKeyLength = 456, + SecureChannelNonceLength = 56, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// ECC Curve448 is a required minimum security policy. It uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve448_ChaChaPoly = new(SecurityPolicies.ECC_curve448_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 456, + MaxAsymmetricKeyLength = 456, + SecureChannelNonceLength = 56, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// The ECC nistP256 is a required minimum security policy. + /// + public readonly static SecurityPolicyInfo ECC_nistP256 = new(SecurityPolicies.ECC_nistP256) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 256 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + SecureChannelEnhancements = false, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// The ECC_nistP256_AesGcm is an ECC nistP256 variant that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP256_AesGcm = new(SecurityPolicies.ECC_nistP256_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// The ECC_nistP256_AesGcm is an ECC nistP256 variant that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP256_ChaChaPoly = new(SecurityPolicies.ECC_nistP256_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// The ECC nistP384 is an optional high security policy. + /// + public readonly static SecurityPolicyInfo ECC_nistP384 = new(SecurityPolicies.ECC_nistP384) + { + DerivedSignatureKeyLength = 384 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 384 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384, + SecureChannelEnhancements = false, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// The ECC nistP384 is an optional high security policy that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP384_AesGcm = new(SecurityPolicies.ECC_nistP384_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// The ECC nistP384 is an optional high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP384_ChaChaPoly = new(SecurityPolicies.ECC_nistP384_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// The ECC brainpoolP256r1 is a required minimum security policy. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1 = new(SecurityPolicies.ECC_brainpoolP256r1) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 256 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + SecureChannelEnhancements = false, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// The ECC_brainpoolP256r1_AesGcm is an ECC brainpoolP256 variant that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1_AesGcm = new (SecurityPolicies.ECC_brainpoolP256r1_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// The ECC_brainpoolP256_AES is an ECC brainpoolP256 variant that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1_ChaChaPoly = new(SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// The ECC brainpoolP384r1 is an optional high security policy. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1 = new(SecurityPolicies.ECC_brainpoolP384r1) + { + DerivedSignatureKeyLength = 384 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 384 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384, + SecureChannelEnhancements = false, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// The ECC brainpoolP384r1 is an optional high security policy that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1_AesGcm = new(SecurityPolicies.ECC_brainpoolP384r1_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes256Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// The ECC brainpoolP384r1 is an optional high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1_ChaChaPoly = new(SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 + }; + + /// + /// The RSA_DH_AES_GCM is an high security policy that uses AES GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo RSA_DH_AesGcm = new(SecurityPolicies.RSA_DH_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 384, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.RSADH, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + + /// + /// The RSA_DH_ChaChaPoly is an high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo RSA_DH_ChaChaPoly = new(SecurityPolicies.RSA_DH_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 384, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.RSADH, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 + }; + } + + /// + /// The algorithm family used to generate key pairs. + /// + public enum CertificateKeyFamily + { + /// + /// Does not apply. + /// + None, + + /// + /// The RSA algorithm. + /// + RSA, + + /// + /// Ellipic curve algorithms. + /// + ECC + } + + /// + /// The algorithm used to generate key pairs. + /// + public enum CertificateKeyAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// The RSA algorithm. + /// + RSA, + + /// + /// The Diffie-Hellman algorith with RSA public keys. + /// + RSADH, + + /// + /// The NIST P-256 ellipic curve algorithm. + /// + NistP256, + + /// + /// The NIST P-384 ellipic curve algorithm. + /// + NistP384, + + /// + /// The non-twisted Brainpool P-256 ellipic curve algorithm. + /// + BrainpoolP256r1, + + /// + /// The non-twisted Brainpool P-384 ellipic curve algorithm. + /// + BrainpoolP384r1, + + /// + /// The Edward Curve25519 ellipic curve algorithm. + /// + Curve25519, + + /// + /// The Edward Curve25519 ellipic curve algorithm. + /// + Curve448 + } + + /// + /// The symmetric key derivation algorithm used to create shared keys. + /// + public enum KeyDerivationAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// The P_SHA pseudo-random function with SHA1. This algorithm is considered insecure. + /// + PSha1, + + /// + /// The P_SHA pseudo-random function with SHA256. + /// + PSha256, + + /// + /// The HKDF pseudo-random function with SHA256. + /// + HKDFSha256, + + /// + /// The HKDF pseudo-random function with SHA384. + /// + HKDFSha384 + } + + /// + /// The asymmetric encryption algorithm used to encrypt messages. + /// + public enum AsymmetricEncryptionAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// RSA PKCS #1 v1.5. This algorithm is considered insecure. + /// + RsaPkcs15Sha1, + + /// + /// RSA with OAEP padding with SHA1. This algorithm is considered insecure. + /// + RsaOaepSha1, + + /// + /// RSA with OAEP padding with SHA256 . + /// + RsaOaepSha256 + } + + /// + /// The asymmetric signature algorithm used to sign messages. + /// + public enum AsymmetricSignatureAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// RSA PKCS #1 v1.5 with SHA1. This algorithm is considered insecure. + /// + RsaPkcs15Sha1, + + /// + /// RSA PKCS #1 v1.5 with SHA256. + /// + RsaPkcs15Sha256, + + /// + /// RSA PSS with SHA256. + /// + RsaPssSha256, + + /// + /// ECDSA with SHA256. + /// + EcdsaSha256, + + /// + /// ECDSA with SHA384. + /// + EcdsaSha384, + + /// + /// ECDSA with Curve 25519. + /// + EcdsaPure25519, + + /// + /// ECDSA with Curve 448. + /// + EcdsaPure448 + } + + /// + /// The symmetric signature algorithm used to sign messages. + /// + public enum SymmetricSignatureAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// HMAC with SHA1 + /// + HmacSha1, + + /// + /// HMAC with SHA256 + /// + HmacSha256, + + /// + /// HMAC with SHA384 + /// + HmacSha384, + + /// + /// ChaCha20Poly1305 + /// + ChaCha20Poly1305, + + /// + /// AES GCM with 128 bit key + /// + Aes128Gcm, + + /// + /// AES GCM with 256 bit key + /// + Aes256Gcm + } + + /// + /// The symmetric ecryption algorithm used to encrypt messages. + /// + public enum SymmetricEncryptionAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// AES 128 bit in CBC mode + /// + Aes128Cbc, + + /// + /// AES 256 bit in CBC mode + /// + Aes256Cbc, + + /// + /// AES 128 bit in counter mode + /// + Aes128Ctr, + + /// + /// AES 256 bit in counter mode + /// + Aes256Ctr, + + /// + /// ChaCha20Poly1305 + /// + ChaCha20Poly1305, + + /// + /// AES 128 in GCM mode + /// + Aes128Gcm, + + /// + /// AES 256 in GCM mode + /// + Aes256Gcm + } + + /// + /// The algorithm used to generate certificate thumbprints. + /// + public enum CertificateThumbprintAlgorithm + { + /// + /// The SHA1 algorithm. This algorithm is considered insecure. + /// + SHA1, + + /// + /// The SHA256 algorithm. + /// + SHA256, + + /// + /// The SHA384 algorithm. + /// + SHA384, + + /// + /// The SHA512 algorithm. + /// + SHA512 + } +} diff --git a/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs b/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs index 493493f194..e7b1323b8b 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs @@ -380,6 +380,15 @@ private sealed class ClientChannel : ITransportChannel /// public IServiceMessageContext MessageContext => m_channel.MessageContext; + /// + public byte[] ChannelThumbprint => m_channel?.ChannelThumbprint ?? []; + + /// + public byte[] ClientChannelCertificate => m_channel?.ClientChannelCertificate ?? []; + + /// + public byte[] ServerChannelCertificate => m_channel?.ServerChannelCertificate ?? []; + /// public int OperationTimeout { diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs index 7af466800e..df561dfd8b 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs @@ -80,12 +80,12 @@ public UserTokenPolicy FindUserTokenPolicy(string policyId, string tokenSecurity else if (( policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) + CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri) ) || ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) + !CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + !CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri))) { sameEncryptionAlgorithm ??= policy; } @@ -153,12 +153,12 @@ public UserTokenPolicy FindUserTokenPolicy( else if (( policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) + CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri) ) || ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) + !CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + !CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri))) { sameEncryptionAlgorithm ??= policy; } diff --git a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs index 973a4094a9..2782ca2ced 100644 --- a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs @@ -134,17 +134,22 @@ public IServiceMessageContext MessageContext => m_quotas?.MessageContext ?? throw BadNotConnected(); /// - public ChannelToken? CurrentToken => null; + public ChannelToken CurrentToken => new(); + + /// + public byte[] ChannelThumbprint => []; + + /// + public byte[] ClientChannelCertificate { get; private set; } = []; + + /// + public byte[] ServerChannelCertificate { get; private set; } = []; /// public event ChannelTokenActivatedEventHandler OnTokenActivated { - add - { - } - remove - { - } + add {} + remove {} } /// @@ -410,6 +415,7 @@ private void CreateHttpClient() } #endif handler.ClientCertificates.Add(clientCertificate); + ClientChannelCertificate = clientCertificate.RawData; } Func< @@ -456,7 +462,7 @@ private void CreateHttpClient() } m_quotas.CertificateValidator?.ValidateAsync(validationChain, default).GetAwaiter().GetResult(); - + ServerChannelCertificate = cert.RawData; return true; } catch (Exception ex) @@ -484,6 +490,7 @@ private void CreateHttpClient() #pragma warning disable CA5400 // HttpClient is created without enabling CheckCertificateRevocationList m_client = new HttpClient(handler); #pragma warning restore CA5400 // HttpClient is created without enabling CheckCertificateRevocationList + } catch (Exception ex) { diff --git a/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs b/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs index c5f34f3446..e393444af1 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs @@ -43,14 +43,23 @@ public class SecureChannelContext /// The secure channel identifier. /// The endpoint description. /// The message encoding. + /// The unique hash for the secure channel calculated during channel creation. + /// The client certificate used to establish the secure channel. + /// The server certificate used to establish the secure channel. public SecureChannelContext( string secureChannelId, EndpointDescription endpointDescription, - RequestEncoding messageEncoding) + RequestEncoding messageEncoding, + byte[] clientChannelCertificate, + byte[] serverChannelCertificate, + byte[] channelThumbprint = null) { SecureChannelId = secureChannelId; EndpointDescription = endpointDescription; MessageEncoding = messageEncoding; + ClientChannelCertificate = clientChannelCertificate; + ServerChannelCertificate = serverChannelCertificate; + ChannelThumbprint = channelThumbprint; } /// @@ -71,6 +80,21 @@ public SecureChannelContext( /// The message encoding. public RequestEncoding MessageEncoding { get; } + /// + /// The unique hash for the secure channel calculated during channel creation. + /// + public byte[] ChannelThumbprint { get; } + + /// + /// The client certificate used to establsih the secure channel. + /// + public byte[] ClientChannelCertificate { get; } + + /// + /// The server certificate used to establsih the secure channel. + /// + public byte[] ServerChannelCertificate { get; } + /// /// The active secure channel for the thread. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs index 010a87c61b..c1af365da7 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs @@ -37,6 +37,8 @@ namespace Opc.Ua.Bindings /// public sealed class ChannelToken : IDisposable { + private bool m_disposed; + /// /// Creates an object with default values. /// @@ -51,17 +53,18 @@ private void Dispose(bool disposing) { if (!m_disposed) { - if (disposing) + if (ServerHmac != null) + { + ServerHmac.Dispose(); + ServerHmac = null; + } + + if (ClientHmac != null) { - Utils.SilentDispose(ClientHmac); - Utils.SilentDispose(ServerHmac); - Utils.SilentDispose(ClientEncryptor); - Utils.SilentDispose(ServerEncryptor); + ClientHmac.Dispose(); + ClientHmac = null; } - ClientHmac = null; - ServerHmac = null; - ClientEncryptor = null; - ServerEncryptor = null; + m_disposed = true; } } @@ -123,6 +126,21 @@ public void Dispose() (HiResClock.TickCount - CreatedAtTickCount) > (int)Math.Round(Lifetime * TcpMessageLimits.TokenActivationPeriod); + /// + /// The SecurityPolicy used to encrypt and sign the messages. + /// + public SecurityPolicyInfo SecurityPolicy { get; set; } + + /// + /// The secret used to compute the keys. + /// + internal byte[] Secret { get; set; } + + /// + /// The previous server nonce used to compute the keys. + /// + internal byte[] PreviousSecret { get; set; } + /// /// The nonce provided by the client. /// @@ -136,53 +154,41 @@ public void Dispose() /// /// The key used to sign messages sent by the client. /// - public byte[] ClientSigningKey { get; set; } + internal byte[] ClientSigningKey { get; set; } /// /// The key used to encrypt messages sent by the client. /// - public byte[] ClientEncryptingKey { get; set; } + internal byte[] ClientEncryptingKey { get; set; } /// /// The initialization vector by the client when encrypting a message. /// - public byte[] ClientInitializationVector { get; set; } + internal byte[] ClientInitializationVector { get; set; } /// /// The key used to sign messages sent by the server. /// - public byte[] ServerSigningKey { get; set; } + internal byte[] ServerSigningKey { get; set; } /// /// The key used to encrypt messages sent by the server. /// - public byte[] ServerEncryptingKey { get; set; } + internal byte[] ServerEncryptingKey { get; set; } /// /// The initialization vector by the server when encrypting a message. /// - public byte[] ServerInitializationVector { get; set; } - - /// - /// The SymmetricAlgorithm object used by the client to encrypt messages. - /// - public SymmetricAlgorithm ClientEncryptor { get; set; } + internal byte[] ServerInitializationVector { get; set; } /// - /// The SymmetricAlgorithm object used by the server to encrypt messages. + /// A pre-allocated HMAC used to improve performance for SecurityPolicies that need it. /// - public SymmetricAlgorithm ServerEncryptor { get; set; } + internal HMAC ServerHmac { get; set; } /// - /// The HMAC object used by the client to sign messages. + /// A pre-allocated HMAC used to improve performance for SecurityPolicies that need it. /// - public HMAC ClientHmac { get; set; } - - /// - /// The HMAC object used by the server to sign messages. - /// - public HMAC ServerHmac { get; set; } - - private bool m_disposed; + internal HMAC ClientHmac { get; set; } } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs index c8569f3e0a..089c9a07c4 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs @@ -509,71 +509,6 @@ protected virtual void CompleteReverseHello(Exception e) // intentionally left empty } - /// - /// Sends a fault response secured with the asymmetric keys. - /// - protected void SendServiceFault(uint requestId, ServiceResult fault) - { - m_logger.LogDebug( - "ChannelId {Id}: Request {RequestId}: SendServiceFault={ServiceFault}", - ChannelId, - requestId, - fault.StatusCode); - - BufferCollection chunksToSend = null; - - try - { - // construct fault. - var response = new ServiceFault(); - - response.ResponseHeader.ServiceResult = fault.Code; - - var stringTable = new StringTable(); - - response.ResponseHeader.ServiceDiagnostics = new DiagnosticInfo( - fault, - DiagnosticsMasks.NoInnerStatus, - true, - stringTable, - m_logger); - - response.ResponseHeader.StringTable = stringTable.ToArray(); - - // serialize fault. - byte[] buffer = BinaryEncoder.EncodeMessage(response, Quotas.MessageContext); - - // secure message. - chunksToSend = WriteAsymmetricMessage( - TcpMessageType.Open, - requestId, - ServerCertificate, - ClientCertificate, - new ArraySegment(buffer, 0, buffer.Length)); - - // write the message to the server. - BeginWriteMessage(chunksToSend, null); - chunksToSend = null; - } - catch (Exception e) - { - chunksToSend?.Release(BufferManager, "SendServiceFault"); - - m_logger.LogError( - e, - "ChannelId {Id}: Request {RequestId}: SendServiceFault={ServiceFault}: Unexpected error.", - ChannelId, - requestId, - fault.StatusCode); - - ForceChannelFault( - ServiceResult.Create( - e, - StatusCodes.BadTcpInternalError, - "Unexpected error sending a service fault.")); - } - } - /// /// Handles a reconnect request. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs index 69b0705e8c..5a91a69891 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs @@ -27,6 +27,11 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +using System; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Logging; + namespace Opc.Ua.Bindings { /// @@ -158,6 +163,23 @@ public static bool IsValid(uint messageType) } } + + internal static string GetTypeAndSize(ArraySegment chunk) + { + StringBuilder sb = new StringBuilder(); + + for (int ii = 0; ii < 1; ii++) + { + uint size = BitConverter.ToUInt32(chunk.Array ?? [], 4); + sb.Append(Encoding.ASCII.GetString(chunk.Array ?? [], 0, 4)); + sb.Append("=>"); + sb.Append(BitConverter.ToUInt32(chunk.Array ?? [], 4)); + sb.Append((size != chunk.Count) ? " X " : " O "); + sb.Append(chunk.Count); + } + + return sb.ToString(); + } } /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index 811a38f009..df83e166d2 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -273,7 +274,7 @@ public override void Reconnect( State = TcpChannelState.Open; // send response. - SendOpenSecureChannelResponse(requestId, token, request); + SendOpenSecureChannelResponse(requestId, token, request, true); // send any queued responses. ResetQueuedResponses(OnChannelReconnected); @@ -566,13 +567,28 @@ private bool ProcessOpenSecureChannelRequest( try { + m_oscRequestSignature = null; + byte[] signature; + messageBody = ReadAsymmetricMessage( messageChunk, ServerCertificate, out channelId, out clientCertificate, out requestId, - out sequenceNumber); + out sequenceNumber, + m_oscRequestSignature, + out signature); + + // don't keep signature if secure channel enhancements are not used. + m_oscRequestSignature = (SecurityPolicy.SecureChannelEnhancements) ? signature : null; + + CryptoTrace.Start(ConsoleColor.Magenta, $"ProcessOpenSecureChannelRequest ({(State != TcpChannelState.Opening ? "RENEW" : "OPEN")})"); + CryptoTrace.WriteLine($"ClientCertificate={clientCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"ServerCertificate={ServerCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"RequestSignature={CryptoTrace.KeyToString(signature)}"); + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(ChannelThumbprint)}"); + CryptoTrace.Finish("ProcessOpenSecureChannelRequest"); // check for replay attacks. if (!VerifySequenceNumber(sequenceNumber, "ProcessOpenSecureChannelRequest")) @@ -680,6 +696,14 @@ const string errorSecurityChecksFailed token = CreateToken(); token.TokenId = GetNewTokenId(); token.ServerNonce = CreateNonce(ServerCertificate); + + CryptoTrace.Start(ConsoleColor.Red, $"PreviousSecret"); + CryptoTrace.WriteLine($"PreviousSecret={CryptoTrace.KeyToString(token.PreviousSecret)}"); + CryptoTrace.WriteLine($"CurrentSecret={CryptoTrace.KeyToString(CurrentToken?.Secret)}"); + CryptoTrace.Finish($"PreviousSecret"); + + token.PreviousSecret = CurrentToken?.Secret; + // check the client nonce. token.ClientNonce = request.ClientNonce; if (!ValidateNonce(ClientCertificate, token.ClientNonce)) @@ -791,11 +815,11 @@ const string errorSecurityChecksFailed // send the response. if (requestType == SecurityTokenRequestType.Renew) { - SendOpenSecureChannelResponse(requestId, RenewedToken, request); + SendOpenSecureChannelResponse(requestId, RenewedToken, request, true); } else { - SendOpenSecureChannelResponse(requestId, CurrentToken, request); + SendOpenSecureChannelResponse(requestId, CurrentToken, request, false); } // notify reverse @@ -821,11 +845,14 @@ const string errorSecurityChecksFailed // report the audit event for open secure channel ReportAuditOpenSecureChannelEvent?.Invoke(this, request, ClientCertificate, e); - SendServiceFault( - requestId, ServiceResult.Create( + SendServiceFault( + requestId, + State == TcpChannelState.Open, + ServiceResult.Create( e, StatusCodes.BadTcpInternalError, "Unexpected error processing OpenSecureChannel request.")); + CompleteReverseHello(e); return false; } @@ -861,13 +888,92 @@ protected override void CompleteReverseHello(Exception e) } } + /// + /// Sends a fault response secured with the asymmetric keys. + /// + protected void SendServiceFault(uint requestId, bool renew, ServiceResult fault) + { + m_logger.LogDebug( + "ChannelId {Id}: Request {RequestId}: SendServiceFault={ServiceFault}", + ChannelId, + requestId, + fault.StatusCode); + + BufferCollection chunksToSend = null; + + try + { + // construct fault. + var response = new ServiceFault(); + + response.ResponseHeader.ServiceResult = fault.Code; + + var stringTable = new StringTable(); + + response.ResponseHeader.ServiceDiagnostics = new DiagnosticInfo( + fault, + DiagnosticsMasks.NoInnerStatus, + true, + stringTable, + m_logger); + + response.ResponseHeader.StringTable = stringTable.ToArray(); + + // serialize fault. + byte[] buffer = BinaryEncoder.EncodeMessage(response, Quotas.MessageContext); + byte[] signature = null; + + CryptoTrace.WriteLine($"messageBody={CryptoTrace.KeyToString(buffer)}"); + + // secure message. + chunksToSend = WriteAsymmetricMessage( + TcpMessageType.Open, + requestId, + ServerCertificate, + null, + ClientCertificate, + new ArraySegment(buffer, 0, buffer.Length), + !renew ? m_oscRequestSignature : null, + out signature); + + // write the message to the server. + BeginWriteMessage(chunksToSend, null); + chunksToSend = null; + } + catch (Exception e) + { + chunksToSend?.Release(BufferManager, "SendServiceFault"); + + m_logger.LogError( + e, + "ChannelId {Id}: Request {RequestId}: SendServiceFault={ServiceFault}: Unexpected error.", + ChannelId, + requestId, + fault.StatusCode); + + ForceChannelFault( + ServiceResult.Create( + e, + StatusCodes.BadTcpInternalError, + "Unexpected error sending a service fault.")); + } + } + /// /// Sends an OpenSecureChannel response. /// + /// The request identifier. + /// The security token to return in the response. + /// The OpenSecureChannel request being answered. + /// + /// true when answering a token renewal request. Renewal keeps the same channel, but issues a new + /// security token (new token id and server nonce), false for the initial open. + /// private void SendOpenSecureChannelResponse( uint requestId, ChannelToken token, - OpenSecureChannelRequest request) + OpenSecureChannelRequest request, + bool renew) { m_logger.LogDebug("ChannelId {Id}: SendOpenSecureChannelResponse()", ChannelId); @@ -883,6 +989,7 @@ private void SendOpenSecureChannelResponse( response.ServerNonce = token.ServerNonce; byte[] buffer = BinaryEncoder.EncodeMessage(response, Quotas.MessageContext); + byte[] signature; BufferCollection chunksToSend = WriteAsymmetricMessage( TcpMessageType.Open, @@ -890,9 +997,24 @@ private void SendOpenSecureChannelResponse( ServerCertificate, ServerCertificateChain, ClientCertificate, - new ArraySegment(buffer, 0, buffer.Length)); + new ArraySegment(buffer, 0, buffer.Length), + !renew ? m_oscRequestSignature : null, + out signature); + + if (!renew) + { + ChannelThumbprint = signature; + } + + CryptoTrace.Start(ConsoleColor.Magenta, $"SendOpenSecureChannelResponse ({(renew ? "RENEW" : "OPEN")})"); + CryptoTrace.WriteLine($"ServerCertificate={ServerCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"ClientCertificate={ClientCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"RequestSignature={CryptoTrace.KeyToString(m_oscRequestSignature)}"); + CryptoTrace.WriteLine($"ResponseSignature={CryptoTrace.KeyToString(signature)}"); + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(ChannelThumbprint)}"); + CryptoTrace.Finish("SendOpenSecureChannelResponse"); - // write the message to the server. + // write the response to the client. try { BeginWriteMessage(chunksToSend, null); @@ -1342,6 +1464,7 @@ private bool ValidateDiscoveryServiceCall( private readonly ILogger m_logger; private SortedDictionary m_queuedResponses; private ReverseConnectAsyncResult m_pendingReverseHello; + private byte[] m_oscRequestSignature; private static readonly string s_implementationString = ".NET Standard ServerChannel UA-TCP " + Utils.GetAssemblyBuildNumber(); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs index d25a6c2a22..9f43a754f7 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs @@ -1028,7 +1028,10 @@ private async void OnRequestReceivedAsync( var context = new SecureChannelContext( channel.GlobalChannelId, channel.EndpointDescription, - RequestEncoding.Binary); + RequestEncoding.Binary, + channel.ClientCertificate?.RawData, + channel.ServerCertificate?.RawData, + channel.ChannelThumbprint); IServiceResponse response = await m_callback.ProcessRequestAsync( context, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs index dabb2e5e70..e42f4e0024 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs @@ -68,7 +68,7 @@ protected set /// /// The certificate for the server. /// - protected X509Certificate2 ServerCertificate { get; private set; } + internal X509Certificate2 ServerCertificate { get; private set; } /// /// The server certificate chain. @@ -83,7 +83,20 @@ protected set /// /// The security policy used with the channel. /// - protected string SecurityPolicyUri { get; private set; } + protected string SecurityPolicyUri + { + get => SecurityPolicy.Uri; + + private set + { + SecurityPolicy = SecurityPolicies.GetInfo(value); + } + } + + /// + /// The security policy used with the channel. + /// + protected SecurityPolicyInfo SecurityPolicy { get; private set; } /// /// Whether the channel is restricted to discovery operations. @@ -93,7 +106,7 @@ protected set /// /// The certificate for the client. /// - protected X509Certificate2 ClientCertificate { get; set; } + internal X509Certificate2 ClientCertificate { get; set; } /// /// The client certificate chain. @@ -187,33 +200,28 @@ protected static void CompareCertificates( /// protected byte[] CreateNonce(X509Certificate2 certificate) { - switch (SecurityPolicyUri) + switch (SecurityPolicy.CertificateKeyFamily) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - uint length = Nonce.GetNonceLength(SecurityPolicyUri); - - if (length > 0) + case CertificateKeyFamily.RSA: + if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.RSADH) { - return Nonce.CreateRandomNonceData(length); + m_localNonce = Nonce.CreateNonce(SecurityPolicy); + return m_localNonce.Data; } - break; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - m_localNonce = Nonce.CreateNonce(SecurityPolicyUri); + // Basic128Rsa15 is the only RSA based security policy that allows nonces + // with a length less than 32 bytes for compatibility reasons. + bool enforceMinimumLength = !SecurityPolicy.Uri.Equals( + SecurityPolicies.Basic128Rsa15, + StringComparison.Ordinal); + return Nonce.CreateRandomNonceData( + SecurityPolicy.SecureChannelNonceLength, + enforceMinimumLength); + case CertificateKeyFamily.ECC: + m_localNonce = Nonce.CreateNonce(SecurityPolicy); return m_localNonce.Data; default: return null; } - - return null; } /// @@ -228,18 +236,20 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) } // check the length. - if (nonce == null || nonce.Length != Nonce.GetNonceLength(SecurityPolicyUri)) + if (nonce == null || nonce.Length != SecurityPolicy.SecureChannelNonceLength) { return false; } - switch (SecurityPolicyUri) + switch (SecurityPolicy.CertificateKeyFamily) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: + case CertificateKeyFamily.RSA: + if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.RSADH) + { + m_remoteNonce = Nonce.CreateNonce(SecurityPolicy, nonce); + return true; + } + // try to catch programming errors by rejecting nonces with all zeros. for (int ii = 0; ii < nonce.Length; ii++) { @@ -248,19 +258,13 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) return true; } } - - return false; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - m_remoteNonce = Nonce.CreateNonce(SecurityPolicyUri, nonce); + break; + case CertificateKeyFamily.ECC: + m_remoteNonce = Nonce.CreateNonce(SecurityPolicy, nonce); return true; - default: - return false; } + + return false; } /// @@ -268,19 +272,23 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) /// protected int GetPlainTextBlockSize(X509Certificate2 receiverCertificate) { - switch (SecurityPolicyUri) + if (SecurityPolicy.AsymmetricSignatureAlgorithm == AsymmetricSignatureAlgorithm.None || + SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: + return 1; + } + + switch (SecurityPolicy.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.OaepSHA1); - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.OaepSHA256); - case SecurityPolicies.Basic128Rsa15: + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.Pkcs1); @@ -294,13 +302,17 @@ protected int GetPlainTextBlockSize(X509Certificate2 receiverCertificate) /// protected int GetCipherTextBlockSize(X509Certificate2 receiverCertificate) { - switch (SecurityPolicyUri) + if (SecurityPolicy.AsymmetricSignatureAlgorithm == AsymmetricSignatureAlgorithm.None || + SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.Basic128Rsa15: + return 1; + } + + switch (SecurityPolicy.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: return RsaUtils.GetCipherTextBlockSize(receiverCertificate); default: return 1; @@ -399,21 +411,17 @@ protected int GetAsymmetricHeaderSize( /// protected int GetAsymmetricSignatureSize(X509Certificate2 senderCertificate) { - switch (SecurityPolicyUri) + switch (SecurityPolicy.AsymmetricSignatureAlgorithm) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + case AsymmetricSignatureAlgorithm.RsaPssSha256: return RsaUtils.GetSignatureLength(senderCertificate); - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return EccUtils.GetSignatureLength(senderCertificate); + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + case AsymmetricSignatureAlgorithm.EcdsaPure25519: + case AsymmetricSignatureAlgorithm.EcdsaPure448: + return CryptoUtils.GetSignatureLength(senderCertificate); default: return 0; } @@ -551,18 +559,29 @@ protected BufferCollection WriteAsymmetricMessage( X509Certificate2 receiverCertificate, ArraySegment messageBody) { + byte[] unused = null; return WriteAsymmetricMessage( messageType, requestId, senderCertificate, null, receiverCertificate, - messageBody); + messageBody, + null, + out unused); } /// /// Sends a OpenSecureChannel request. /// + /// The UA TCP message type (for example, Open or OpenFinal). + /// The request identifier used in the sequence header. + /// The certificate used to sign the asymmetric message. + /// The optional sender certificate chain to include in the message header. + /// The receiver certificate used for asymmetric encryption. + /// The encoded message body to send. + /// The signature from the OpenSecureChannel request. + /// Returns the signature generated for the message being written. /// /// protected BufferCollection WriteAsymmetricMessage( @@ -571,8 +590,12 @@ protected BufferCollection WriteAsymmetricMessage( X509Certificate2 senderCertificate, X509Certificate2Collection senderCertificateChain, X509Certificate2 receiverCertificate, - ArraySegment messageBody) + ArraySegment messageBody, + byte[] oscRequestSignature, + out byte[] signature) { + signature = null; + bool success = false; var chunksToSend = new BufferCollection(); @@ -663,7 +686,8 @@ protected BufferCollection WriteAsymmetricMessage( if (SecurityMode != MessageSecurityMode.None) { - if (receiverCertificate.GetRSAPublicKey() != null) + if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None && + receiverCertificate.GetRSAPublicKey() != null) { if (X509Utils.GetRSAPublicKeySize(receiverCertificate) <= TcpMessageLimits.KeySizeExtraPadding) @@ -721,10 +745,30 @@ protected BufferCollection WriteAsymmetricMessage( // put the message size after encryption into the header. UpdateMessageSize(buffer, 0, cipherTextSize + headerSize); + ArraySegment dataToSign; + + if (oscRequestSignature != null && SecurityPolicy.SecureChannelEnhancements) + { + // copy osc request signature if provided before verifying. + dataToSign = new ArraySegment( + buffer, + 0, + encoder.Position + oscRequestSignature.Length); + + Array.Copy( + oscRequestSignature, + 0, + buffer, + encoder.Position, + oscRequestSignature.Length); + } + else + { + dataToSign = new ArraySegment(buffer, 0, encoder.Position); + } + // write the signature. - byte[] signature = Sign( - new ArraySegment(buffer, 0, encoder.Position), - senderCertificate); + signature = Sign(dataToSign, senderCertificate); if (signature != null) { @@ -765,6 +809,7 @@ protected BufferCollection WriteAsymmetricMessage( // ensure the buffers don't get clean up on exit. success = true; + return chunksToSend; } catch (Exception ex) @@ -988,8 +1033,12 @@ protected ArraySegment ReadAsymmetricMessage( out uint channelId, out X509Certificate2 senderCertificate, out uint requestId, - out uint sequenceNumber) + out uint sequenceNumber, + byte[] oscRequestSignature, + out byte[] signature) { + signature = null; + int headerSize; using (var decoder = new BinaryDecoder(buffer, Quotas.MessageContext)) { @@ -1096,23 +1145,43 @@ protected ArraySegment ReadAsymmetricMessage( // extract signature. int signatureSize = GetAsymmetricSignatureSize(senderCertificate); - byte[] signature = new byte[signatureSize]; + signature = new byte[signatureSize]; for (int ii = 0; ii < signatureSize; ii++) { - signature[ii] = plainText.Array[ - plainText.Offset + plainText.Count - signatureSize + ii]; + signature[ii] = plainText.Array[plainText.Offset + plainText.Count - signatureSize + ii]; } - // verify the signature. - var dataToVerify = new ArraySegment( - plainText.Array, - plainText.Offset, - plainText.Count - signatureSize); + ArraySegment dataToVerify; + + if (oscRequestSignature != null && SecurityPolicy.SecureChannelEnhancements) + { + // copy osc request signature if provided before verifying. + dataToVerify = new ArraySegment( + plainText.Array, + plainText.Offset, + plainText.Count - signatureSize + oscRequestSignature.Length); + + Array.Copy( + oscRequestSignature, + dataToVerify.Offset, + dataToVerify.Array, + dataToVerify.Count - oscRequestSignature.Length, + oscRequestSignature.Length); + } + else + { + dataToVerify = new ArraySegment( + plainText.Array, + plainText.Offset, + plainText.Count - signatureSize); + } + // verify the signature. if (!Verify(dataToVerify, signature, senderCertificate)) { m_logger.LogWarning("Could not verify signature on message."); + throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "Could not verify the signature on the message."); @@ -1122,6 +1191,7 @@ protected ArraySegment ReadAsymmetricMessage( int paddingCount = 0; if (SecurityMode != MessageSecurityMode.None && + SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None && receiverCertificate.GetRSAPublicKey() != null) { int paddingEnd; @@ -1194,39 +1264,7 @@ protected ArraySegment ReadAsymmetricMessage( /// protected byte[] Sign(ArraySegment dataToSign, X509Certificate2 senderCertificate) { - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic128Rsa15: - return Rsa_Sign( - dataToSign, - senderCertificate, - HashAlgorithmName.SHA1, - RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Basic256Sha256: - return Rsa_Sign( - dataToSign, - senderCertificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes256_Sha256_RsaPss: - return Rsa_Sign( - dataToSign, - senderCertificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pss); - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return EccUtils.Sign(dataToSign, senderCertificate, HashAlgorithmName.SHA256); - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - return EccUtils.Sign(dataToSign, senderCertificate, HashAlgorithmName.SHA384); - default: - return null; - } + return CryptoUtils.Sign(dataToSign, senderCertificate, SecurityPolicyUri); } /// @@ -1242,53 +1280,11 @@ protected bool Verify( byte[] signature, X509Certificate2 senderCertificate) { - // verify signature. - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - return true; - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - return Rsa_Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA1, - RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Basic256Sha256: - return Rsa_Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes256_Sha256_RsaPss: - return Rsa_Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pss); - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return EccUtils.Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA256); - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - return EccUtils.Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA384); - default: - return false; - } + return CryptoUtils.Verify( + dataToVerify, + signature, + senderCertificate, + SecurityPolicyUri); } /// @@ -1304,48 +1300,51 @@ protected ArraySegment Encrypt( ArraySegment headerToCopy, X509Certificate2 receiverCertificate) { - switch (SecurityPolicyUri) + if (SecurityPolicy.AsymmetricSignatureAlgorithm == AsymmetricSignatureAlgorithm.None || + SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Basic256Sha256: + byte[] encryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Encrypt"); + + Array.Copy( + headerToCopy.Array, + headerToCopy.Offset, + encryptedBuffer, + 0, + headerToCopy.Count); + Array.Copy( + dataToEncrypt.Array, + dataToEncrypt.Offset, + encryptedBuffer, + headerToCopy.Count, + dataToEncrypt.Count); + + return new ArraySegment( + encryptedBuffer, + 0, + dataToEncrypt.Count + headerToCopy.Count); + } + + switch (SecurityPolicy.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: return Rsa_Encrypt( dataToEncrypt, headerToCopy, receiverCertificate, RsaUtils.Padding.OaepSHA1); - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: return Rsa_Encrypt( dataToEncrypt, headerToCopy, receiverCertificate, RsaUtils.Padding.OaepSHA256); - case SecurityPolicies.Basic128Rsa15: + default: + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: return Rsa_Encrypt( dataToEncrypt, headerToCopy, receiverCertificate, RsaUtils.Padding.Pkcs1); - default: - byte[] encryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Encrypt"); - - Array.Copy( - headerToCopy.Array, - headerToCopy.Offset, - encryptedBuffer, - 0, - headerToCopy.Count); - Array.Copy( - dataToEncrypt.Array, - dataToEncrypt.Offset, - encryptedBuffer, - headerToCopy.Count, - dataToEncrypt.Count); - - return new ArraySegment( - encryptedBuffer, - 0, - dataToEncrypt.Count + headerToCopy.Count); } } @@ -1361,6 +1360,30 @@ protected ArraySegment Decrypt( ArraySegment headerToCopy, X509Certificate2 receiverCertificate) { + if (SecurityPolicy.AsymmetricSignatureAlgorithm == AsymmetricSignatureAlgorithm.None || + SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) + { + byte[] decryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Decrypt"); + + Array.Copy( + headerToCopy.Array, + headerToCopy.Offset, + decryptedBuffer, + 0, + headerToCopy.Count); + Array.Copy( + dataToDecrypt.Array, + dataToDecrypt.Offset, + decryptedBuffer, + headerToCopy.Count, + dataToDecrypt.Count); + + return new ArraySegment( + decryptedBuffer, + 0, + dataToDecrypt.Count + headerToCopy.Count); + } + switch (SecurityPolicyUri) { case SecurityPolicies.Basic256: @@ -1371,7 +1394,10 @@ protected ArraySegment Decrypt( headerToCopy, receiverCertificate, RsaUtils.Padding.OaepSHA1); + default: case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.RSA_DH_AesGcm: + case SecurityPolicies.RSA_DH_ChaChaPoly: return Rsa_Decrypt( dataToDecrypt, headerToCopy, @@ -1383,26 +1409,6 @@ protected ArraySegment Decrypt( headerToCopy, receiverCertificate, RsaUtils.Padding.Pkcs1); - default: - byte[] decryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Decrypt"); - - Array.Copy( - headerToCopy.Array, - headerToCopy.Offset, - decryptedBuffer, - 0, - headerToCopy.Count); - Array.Copy( - dataToDecrypt.Array, - dataToDecrypt.Offset, - decryptedBuffer, - headerToCopy.Count, - dataToDecrypt.Count); - - return new ArraySegment( - decryptedBuffer, - 0, - dataToDecrypt.Count + headerToCopy.Count); } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs index bd180e54de..04d509782b 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs @@ -59,12 +59,14 @@ private static byte[] Rsa_Sign( "No private key for certificate."); // create the signature. - return rsa.SignData( + var signature = rsa.SignData( dataToSign.Array, dataToSign.Offset, dataToSign.Count, algorithm, padding); + + return signature; } /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index dcb7548c87..9d8dbb24b5 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -45,12 +45,12 @@ public partial class UaSCUaBinaryChannel /// /// Returns the current security token. /// - protected ChannelToken PreviousToken { get; private set; } + protected internal ChannelToken PreviousToken { get; private set; } /// /// Returns the renewed but not yet activated token. /// - protected ChannelToken RenewedToken { get; private set; } + protected internal ChannelToken RenewedToken { get; private set; } /// /// Called when the token changes @@ -136,11 +136,6 @@ protected void DiscardTokens() OnTokenActivated?.Invoke(null, null); } - /// - /// Indicates that an explicit signature is not present. - /// - private bool AuthenticatedEncryption { get; set; } - /// /// The byte length of the MAC (a.k.a signature) attached to each message. /// @@ -154,71 +149,19 @@ protected void DiscardTokens() /// /// Calculates the symmetric key sizes based on the current security policy. /// + /// protected void CalculateSymmetricKeySizes() { - AuthenticatedEncryption = false; - - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - SymmetricSignatureSize = 20; - m_signatureKeySize = 16; - m_encryptionKeySize = 16; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.Basic256: - SymmetricSignatureSize = 20; - m_signatureKeySize = 24; - m_encryptionKeySize = 32; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.Basic256Sha256: - SymmetricSignatureSize = 32; - m_signatureKeySize = 32; - m_encryptionKeySize = 32; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.Aes128_Sha256_RsaOaep: - SymmetricSignatureSize = 32; - m_signatureKeySize = 32; - m_encryptionKeySize = 16; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.Aes256_Sha256_RsaPss: - SymmetricSignatureSize = 32; - m_signatureKeySize = 32; - m_encryptionKeySize = 32; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - SymmetricSignatureSize = 32; - m_signatureKeySize = 32; - m_encryptionKeySize = 16; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - AuthenticatedEncryption = true; - SymmetricSignatureSize = 16; - m_signatureKeySize = 32; - m_encryptionKeySize = 32; - EncryptionBlockSize = 12; - break; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - SymmetricSignatureSize = 48; - m_signatureKeySize = 48; - m_encryptionKeySize = 32; - EncryptionBlockSize = 16; - break; - default: - SymmetricSignatureSize = 0; - m_signatureKeySize = 0; - m_encryptionKeySize = 0; - EncryptionBlockSize = 1; - break; - } + SecurityPolicyInfo info = SecurityPolicies.GetInfo(SecurityPolicyUri) + ?? throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + SecurityPolicyUri); + + SymmetricSignatureSize = info.SymmetricSignatureLength; + m_signatureKeySize = info.DerivedSignatureKeyLength; + m_encryptionKeySize = info.SymmetricEncryptionKeyLength; + EncryptionBlockSize = info.InitializationVectorLength != 0 ? info.InitializationVectorLength : 1; } private void DeriveKeysWithPSHA( @@ -228,9 +171,15 @@ private void DeriveKeysWithPSHA( ChannelToken token, bool isServer) { + using HMAC hmac = Utils.CreateHMAC(algorithmName, secret); + int length = m_signatureKeySize + m_encryptionKeySize + EncryptionBlockSize; - using HMAC hmac = Utils.CreateHMAC(algorithmName, secret); + if (!isServer && SecurityPolicy.SecureChannelEnhancements) + { + length += hmac.HashSize/8; + } + byte[] output = Utils.PSHA(hmac, null, seed, 0, length); byte[] signingKey = new byte[m_signatureKeySize]; @@ -246,44 +195,52 @@ private void DeriveKeysWithPSHA( token.ServerSigningKey = signingKey; token.ServerEncryptingKey = encryptingKey; token.ServerInitializationVector = iv; + token.ServerHmac = SecurityPolicy.CreateSignatureHmac(signingKey); } else { token.ClientSigningKey = signingKey; token.ClientEncryptingKey = encryptingKey; token.ClientInitializationVector = iv; + token.ClientHmac = SecurityPolicy.CreateSignatureHmac(signingKey); } } private void DeriveKeysWithHKDF( - HashAlgorithmName algorithmName, - byte[] salt, ChannelToken token, - bool isServer) + byte[] salt, + bool isServer, + int length) { - int length = m_signatureKeySize + m_encryptionKeySize + EncryptionBlockSize; + CryptoTrace.WriteLine($"DeriveKeys for {((isServer) ? "SERVER" : "CLIENT")}"); - byte[] output = m_localNonce.DeriveKey(m_remoteNonce, salt, algorithmName, length); + byte[] keyData = m_localNonce.DeriveKeyData( + token.Secret, + salt, + token.SecurityPolicy.KeyDerivationAlgorithm, + length); byte[] signingKey = new byte[m_signatureKeySize]; byte[] encryptingKey = new byte[m_encryptionKeySize]; byte[] iv = new byte[EncryptionBlockSize]; - Buffer.BlockCopy(output, 0, signingKey, 0, signingKey.Length); - Buffer.BlockCopy(output, m_signatureKeySize, encryptingKey, 0, encryptingKey.Length); - Buffer.BlockCopy(output, m_signatureKeySize + m_encryptionKeySize, iv, 0, iv.Length); + Buffer.BlockCopy(keyData, 0, signingKey, 0, signingKey.Length); + Buffer.BlockCopy(keyData, m_signatureKeySize, encryptingKey, 0, encryptingKey.Length); + Buffer.BlockCopy(keyData, m_signatureKeySize + m_encryptionKeySize, iv, 0, iv.Length); if (isServer) { token.ServerSigningKey = signingKey; token.ServerEncryptingKey = encryptingKey; token.ServerInitializationVector = iv; + token.ServerHmac = token.SecurityPolicy.CreateSignatureHmac(signingKey); } else { token.ClientSigningKey = signingKey; token.ClientEncryptingKey = encryptingKey; token.ClientInitializationVector = iv; + token.ClientHmac = token.SecurityPolicy.CreateSignatureHmac(signingKey); } } @@ -292,6 +249,8 @@ private void DeriveKeysWithHKDF( /// protected void ComputeKeys(ChannelToken token) { + token.SecurityPolicy = SecurityPolicies.GetInfo(SecurityPolicyUri); + if (SecurityMode == MessageSecurityMode.None) { return; @@ -300,170 +259,50 @@ protected void ComputeKeys(ChannelToken token) byte[] serverSecret = token.ServerNonce; byte[] clientSecret = token.ClientNonce; - HashAlgorithmName algorithmName = HashAlgorithmName.SHA256; - switch (SecurityPolicyUri) + switch (token.SecurityPolicy.KeyDerivationAlgorithm) { - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: + case KeyDerivationAlgorithm.HKDFSha256: + case KeyDerivationAlgorithm.HKDFSha384: { - algorithmName = HashAlgorithmName.SHA256; - byte[] length = - SecurityMode == MessageSecurityMode.Sign - ? s_hkdfAes128SignOnlyKeyLength - : s_hkdfAes128SignAndEncryptKeyLength; - byte[] serverSalt = Utils.Append( - length, - s_hkdfServerLabel, - serverSecret, - clientSecret); - byte[] clientSalt = Utils.Append( - length, - s_hkdfClientLabel, - clientSecret, - serverSecret); - -#if DEBUG - m_logger.LogDebug("Length={Length}", Utils.ToHexString(length)); - m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt)); - m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt)); -#endif + token.Secret = m_localNonce.GenerateSecret(m_remoteNonce, token.PreviousSecret); - DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); - DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); - break; - } - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - { - algorithmName = HashAlgorithmName.SHA384; - byte[] length = - SecurityMode == MessageSecurityMode.Sign - ? s_hkdfAes256SignOnlyKeyLength - : s_hkdfAes256SignAndEncryptKeyLength; - byte[] serverSalt = Utils.Append( - length, - s_hkdfServerLabel, - serverSecret, - clientSecret); byte[] clientSalt = Utils.Append( - length, + BitConverter.GetBytes((ushort)token.SecurityPolicy.ClientKeyDataLength), s_hkdfClientLabel, clientSecret, serverSecret); -#if DEBUG - m_logger.LogDebug("Length={Length}", Utils.ToHexString(length)); - m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt)); - m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt)); -#endif + DeriveKeysWithHKDF(token, clientSalt, false, token.SecurityPolicy.ClientKeyDataLength); - DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); - DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); - break; - } - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - { - algorithmName = HashAlgorithmName.SHA256; - byte[] length = s_hkdfChaCha20Poly1305KeyLength; byte[] serverSalt = Utils.Append( - length, + BitConverter.GetBytes((ushort)token.SecurityPolicy.ServerKeyDataLength), s_hkdfServerLabel, serverSecret, clientSecret); - byte[] clientSalt = Utils.Append( - length, - s_hkdfClientLabel, - clientSecret, - serverSecret); - -#if DEBUG - m_logger.LogDebug("Length={Length}", Utils.ToHexString(length)); - m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt)); - m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt)); -#endif - DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); - DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); + DeriveKeysWithHKDF(token, serverSalt, true, token.SecurityPolicy.ServerKeyDataLength); + + CryptoTrace.Start(ConsoleColor.Green, $"ComputeKeys (TokenId={token.TokenId})"); + CryptoTrace.WriteLine($"IKM={CryptoTrace.KeyToString(token.Secret)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverSecret)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientSecret)}"); + CryptoTrace.WriteLine($"ServerSalt={CryptoTrace.KeyToString(serverSalt)}"); + CryptoTrace.WriteLine($"ServerEncryptingKey={CryptoTrace.KeyToString(token.ServerEncryptingKey)}"); + CryptoTrace.WriteLine($"ServerInitializationVector={CryptoTrace.KeyToString(token.ServerInitializationVector)}"); + CryptoTrace.WriteLine($"ClientEncryptingKey={CryptoTrace.KeyToString(token.ClientEncryptingKey)}"); + CryptoTrace.WriteLine($"ClientInitializationVector={CryptoTrace.KeyToString(token.ClientInitializationVector)}"); + CryptoTrace.Finish("ComputeKeys"); break; } - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - algorithmName = HashAlgorithmName.SHA1; - goto default; + default: + case KeyDerivationAlgorithm.PSha1: + case KeyDerivationAlgorithm.PSha256: + HashAlgorithmName algorithmName = token.SecurityPolicy.GetKeyDerivationHashAlgorithmName(); DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false); DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true); break; } - - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - // create encryptors. - var aesCbcEncryptorProvider = Aes.Create(); - aesCbcEncryptorProvider.Mode = CipherMode.CBC; - aesCbcEncryptorProvider.Padding = PaddingMode.None; - aesCbcEncryptorProvider.Key = token.ClientEncryptingKey; - aesCbcEncryptorProvider.IV = token.ClientInitializationVector; - token.ClientEncryptor = aesCbcEncryptorProvider; - - var aesCbcDecryptorProvider = Aes.Create(); - aesCbcDecryptorProvider.Mode = CipherMode.CBC; - aesCbcDecryptorProvider.Padding = PaddingMode.None; - aesCbcDecryptorProvider.Key = token.ServerEncryptingKey; - aesCbcDecryptorProvider.IV = token.ServerInitializationVector; - token.ServerEncryptor = aesCbcDecryptorProvider; - break; - default: - // TODO: is this even legal or should we throw? What are the implications - token.ClientEncryptor = null; - token.ServerEncryptor = null; - break; - } - - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: -#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms - token.ServerHmac = new HMACSHA1(token.ServerSigningKey); - token.ClientHmac = new HMACSHA1(token.ClientSigningKey); -#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms - break; - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - token.ServerHmac = new HMACSHA256(token.ServerSigningKey); - token.ClientHmac = new HMACSHA256(token.ClientSigningKey); - break; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - token.ServerHmac = new HMACSHA384(token.ServerSigningKey); - token.ClientHmac = new HMACSHA384(token.ClientSigningKey); - break; - default: - // TODO: is this even legal or should we throw? What are the implications - token.ServerHmac = null; - token.ClientHmac = null; - break; - } } /// @@ -487,20 +326,22 @@ protected BufferCollection WriteSymmetricMessage( int maxCipherTextSize = SendBufferSize - TcpMessageLimits.SymmetricHeaderSize; int maxCipherBlocks = maxCipherTextSize / EncryptionBlockSize; int maxPlainTextSize = maxCipherBlocks * EncryptionBlockSize; + + int signatureSize = SymmetricSignatureSize; + int paddingCountSize = + (SecurityMode != MessageSecurityMode.SignAndEncrypt || token.SecurityPolicy.NoSymmetricEncryptionPadding) + ? 0 + : (EncryptionBlockSize > byte.MaxValue ? 2 : 1); + int maxPayloadSize = maxPlainTextSize - - SymmetricSignatureSize - - 1 - - TcpMessageLimits.SequenceHeaderSize; + signatureSize - + TcpMessageLimits.SequenceHeaderSize - + paddingCountSize; + const int headerSize = TcpMessageLimits.SymmetricHeaderSize + TcpMessageLimits.SequenceHeaderSize; - // no padding byte. - if (AuthenticatedEncryption) - { - maxPayloadSize++; - } - // write the body to stream. var ostrm = new ArraySegmentStream( BufferManager, @@ -540,14 +381,12 @@ protected BufferCollection WriteSymmetricMessage( byte[] buffer = BufferManager.TakeBuffer( SendBufferSize, "WriteSymmetricMessage"); + chunksToProcess.Add(new ArraySegment(buffer, 0, 0)); } var chunksToSend = new BufferCollection(chunksToProcess.Capacity); -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - Span paddingBuffer = stackalloc byte[EncryptionBlockSize]; -#endif int messageSize = 0; for (int ii = 0; ii < chunksToProcess.Count; ii++) @@ -563,6 +402,7 @@ protected BufferCollection WriteSymmetricMessage( var strm = new MemoryStream(chunkToProcess.Array, 0, SendBufferSize); using var encoder = new BinaryEncoder(strm, Quotas.MessageContext, false); + // check if the message needs to be aborted. if (MessageLimitsExceeded( isRequest, @@ -593,6 +433,7 @@ protected BufferCollection WriteSymmetricMessage( limitsExceeded = true; } + // check if the message is complete. else if (ii == chunksToProcess.Count - 1) { @@ -608,25 +449,24 @@ protected BufferCollection WriteSymmetricMessage( count += TcpMessageLimits.SequenceHeaderSize; count += chunkToProcess.Count; - count += SymmetricSignatureSize; + count += paddingCountSize; + count += signatureSize; - // calculate the padding. int padding = 0; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt && - !AuthenticatedEncryption) + if (paddingCountSize > 0) { - // reserve one byte for the padding size. - count++; + padding = EncryptionBlockSize - (count % EncryptionBlockSize); - // use padding as helper to calc the real padding - padding = count % EncryptionBlockSize; - if (padding != 0) + if (padding == EncryptionBlockSize) { - padding = EncryptionBlockSize - padding; + padding = 0; } - count += padding; + if (padding > 0) + { + count += padding; + } } count += TcpMessageLimits.SymmetricHeaderSize; @@ -637,7 +477,6 @@ protected BufferCollection WriteSymmetricMessage( uint sequenceNumber = GetNewSequenceNumber(); encoder.WriteUInt32(null, sequenceNumber); - encoder.WriteUInt32(null, requestId); // skip body. @@ -646,63 +485,27 @@ protected BufferCollection WriteSymmetricMessage( // update message size count. messageSize += chunkToProcess.Count; - // write padding. - if (SecurityMode == MessageSecurityMode.SignAndEncrypt && - !AuthenticatedEncryption) - { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - if (padding > 1) - { - Span buffer = paddingBuffer[..(padding + 1)]; - buffer.Fill((byte)padding); - encoder.WriteRawBytes(buffer); - } - else -#endif - { - for (int jj = 0; jj <= padding; jj++) - { - encoder.WriteByte(null, (byte)padding); - } - } - } + ArraySegment dataToSend; - // calculate and write signature. if (SecurityMode != MessageSecurityMode.None) { - if (AuthenticatedEncryption) - { - strm.Seek(SymmetricSignatureSize, SeekOrigin.Current); - } - else - { - byte[] signature = Sign( - token, - new ArraySegment(chunkToProcess.Array, 0, encoder.Position), - isRequest); - - if (signature != null) - { - encoder.WriteRawBytes(signature, 0, signature.Length); - } - } - } - - if ((SecurityMode == MessageSecurityMode.SignAndEncrypt && - !AuthenticatedEncryption) || - (SecurityMode != MessageSecurityMode.None && AuthenticatedEncryption)) - { - // encrypt the data. - var dataToEncrypt = new ArraySegment( + dataToSend = new ArraySegment( chunkToProcess.Array, TcpMessageLimits.SymmetricHeaderSize, encoder.Position - TcpMessageLimits.SymmetricHeaderSize); - Encrypt(token, dataToEncrypt, isRequest); + + dataToSend = EncryptAndSign(token, dataToSend, isRequest); + } + else + { + dataToSend = new ArraySegment( + chunkToProcess.Array, + 0, + encoder.Position); } // add the header into chunk. - chunksToSend.Add( - new ArraySegment(chunkToProcess.Array, 0, encoder.Position)); + chunksToSend.Add(dataToSend); } // ensure the buffers don't get cleaned up on exit. @@ -717,6 +520,22 @@ protected BufferCollection WriteSymmetricMessage( } } } + private ArraySegment EncryptAndSign( + ChannelToken token, + ArraySegment dataToEncrypt, + bool useClientKeys) + { + return CryptoUtils.SymmetricEncryptAndSign( + dataToEncrypt, + token.SecurityPolicy, + useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey, + useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector, + useClientKeys ? token.ClientSigningKey : token.ServerSigningKey, + useClientKeys ? token.ClientHmac : token.ServerHmac, + this.SecurityMode == MessageSecurityMode.Sign, + token.TokenId, + (uint)(m_localSequenceNumber - 1)); // already incremented to create this message. need the last one sent. + } /// /// Decrypts and verifies a message chunk. @@ -750,6 +569,7 @@ protected ArraySegment ReadSymmetricMessage( { ActivateToken(RenewedToken); } + // check if activation of the new token should be forced. else if (RenewedToken != null && CurrentToken.ActivationRequired) { @@ -803,717 +623,55 @@ protected ArraySegment ReadSymmetricMessage( int headerSize = decoder.Position; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // decrypt the message. - Decrypt( - token, - new ArraySegment( - buffer.Array, - buffer.Offset + headerSize, - buffer.Count - headerSize), - isRequest); - } + var dataToProcess = new ArraySegment( + buffer.Array, + buffer.Offset, + buffer.Count); - int paddingCount = 0; if (SecurityMode != MessageSecurityMode.None) { - int signatureStart = buffer.Offset + buffer.Count - SymmetricSignatureSize; - - // extract signature. - byte[] signature = new byte[SymmetricSignatureSize]; - Array.Copy(buffer.Array, signatureStart, signature, 0, signature.Length); - - // verify the signature. - if (!Verify( - token, - signature, - new ArraySegment( - buffer.Array, - buffer.Offset, - buffer.Count - SymmetricSignatureSize), - isRequest)) - { - m_logger.LogError("ChannelId {Id}: Could not verify signature on message.", Id); - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Could not verify the signature on the message."); - } + dataToProcess = new ArraySegment( + buffer.Array, + buffer.Offset + headerSize, + buffer.Count - headerSize); - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // verify padding. - int paddingStart = signatureStart - 1; - paddingCount = buffer.Array[paddingStart]; - - for (int ii = paddingStart - paddingCount; ii < paddingStart; ii++) - { - if (buffer.Array[ii] != paddingCount) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Could not verify the padding in the message."); - } - } - - // add byte for size. - paddingCount++; - } + dataToProcess = DecryptAndVerify( + token, + dataToProcess, + isRequest); } // extract request id and sequence number. sequenceNumber = decoder.ReadUInt32(null); requestId = decoder.ReadUInt32(null); - // return an the data contained in the message. - int startOfBody = - buffer.Offset + - TcpMessageLimits.SymmetricHeaderSize + - TcpMessageLimits.SequenceHeaderSize; - int sizeOfBody = - buffer.Count - - TcpMessageLimits.SymmetricHeaderSize - - TcpMessageLimits.SequenceHeaderSize - - paddingCount - - SymmetricSignatureSize; - - return new ArraySegment(buffer.Array, startOfBody, sizeOfBody); - } - - /// - /// Returns the symmetric signature for the data. - /// - protected byte[] Sign(ChannelToken token, ArraySegment dataToSign, bool useClientKeys) - { - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_nistP256: - return SymmetricSign(token, dataToSign, useClientKeys); - default: - return null; - } - } - - /// - /// Returns the symmetric signature for the data. - /// - protected bool Verify( - ChannelToken token, - byte[] signature, - ArraySegment dataToVerify, - bool useClientKeys) - { - // verify signature. - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - return true; - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - return SymmetricVerify(token, signature, dataToVerify, useClientKeys); - default: - return false; - } - } - - /// - /// Decrypts the data in a buffer using symmetric encryption. - /// - /// - protected void Encrypt( - ChannelToken token, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - break; - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - SymmetricEncrypt(token, dataToEncrypt, useClientKeys); - break; + headerSize += TcpMessageLimits.SequenceHeaderSize; -#if CURVE25519 - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - { - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // narowing conversion can safely be done on m_localSequenceNumber - SymmetricEncryptWithChaCha20Poly1305( - token, - (uint)m_localSequenceNumber, - dataToEncrypt, - useClientKeys); - break; - } - // narowing conversion can safely be done on m_localSequenceNumber - SymmetricSignWithPoly1305(token, (uint)m_localSequenceNumber, dataToEncrypt, useClientKeys); - break; - } -#endif - default: - throw new NotSupportedException(SecurityPolicyUri); - } + // return only the data contained in the message. + return new ArraySegment( + dataToProcess.Array, + dataToProcess.Offset + headerSize, + dataToProcess.Count - headerSize); } - /// - /// Decrypts the data in a buffer using symmetric encryption. - /// - /// - protected void Decrypt( + private ArraySegment DecryptAndVerify( ChannelToken token, ArraySegment dataToDecrypt, bool useClientKeys) { - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - break; - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - SymmetricDecrypt(token, dataToDecrypt, useClientKeys); - break; - -#if CURVE25519 - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - { - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - SymmetricDecryptWithChaCha20Poly1305( - token, - m_remoteSequenceNumber, - dataToDecrypt, - useClientKeys); - break; - } - - SymmetricVerifyWithPoly1305(token, m_remoteSequenceNumber, dataToDecrypt, useClientKeys); - break; - } -#endif - - default: - throw new NotSupportedException(SecurityPolicyUri); - } - } - -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - /// - /// Signs the message using HMAC. - /// - /// - private static byte[] SymmetricSign( - ChannelToken token, - ReadOnlySpan dataToSign, - bool useClientKeys) - { - // get HMAC object. - HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac; - - // compute hash. - int hashSizeInBytes = hmac.HashSize >> 3; - byte[] signature = new byte[hashSizeInBytes]; - bool result = hmac.TryComputeHash(dataToSign, signature, out int bytesWritten); - - // check result - if (!result || bytesWritten != hashSizeInBytes) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "The computed hash doesn't match the expected size."); - } - - // return signature. - return signature; - } -#else - /// - /// Signs the message using HMAC. - /// - private static byte[] SymmetricSign( - ChannelToken token, - ArraySegment dataToSign, - bool useClientKeys) - { - // get HMAC object. - HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac; - // compute hash. - var istrm = new MemoryStream( - dataToSign.Array, - dataToSign.Offset, - dataToSign.Count, - false); - byte[] signature = hmac.ComputeHash(istrm); - istrm.Dispose(); - - // return signature. - return signature; - } -#endif - -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - /// - /// Verifies a HMAC for a message. - /// - private bool SymmetricVerify( - ChannelToken token, - ReadOnlySpan signature, - ReadOnlySpan dataToVerify, - bool useClientKeys) - { - // get HMAC object. - HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac; - - // compute hash. - int hashSizeInBytes = hmac.HashSize >> 3; - Span computedSignature = stackalloc byte[hashSizeInBytes]; - bool result = hmac.TryComputeHash( - dataToVerify, - computedSignature, - out int bytesWritten); - System.Diagnostics.Debug.Assert(bytesWritten == hashSizeInBytes); - // compare signatures. - if (!result || !computedSignature.SequenceEqual(signature)) - { - string expectedSignature = Utils.ToHexString(computedSignature.ToArray()); - string messageType = Encoding.UTF8.GetString(dataToVerify[..4]); - int messageLength = BitConverter.ToInt32(dataToVerify[4..]); - string actualSignature = Utils.ToHexString(signature); -#else - /// - /// Verifies a HMAC for a message. - /// - private bool SymmetricVerify( - ChannelToken token, - byte[] signature, - ArraySegment dataToVerify, - bool useClientKeys) - { - // get HMAC object. - HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac; - - var istrm = new MemoryStream( - dataToVerify.Array, - dataToVerify.Offset, - dataToVerify.Count, - false); - byte[] computedSignature = hmac.ComputeHash(istrm); - istrm.Dispose(); - // compare signatures. - if (!Utils.IsEqual(computedSignature, signature)) - { - string expectedSignature = Utils.ToHexString(computedSignature); - string messageType = Encoding.UTF8 - .GetString(dataToVerify.Array, dataToVerify.Offset, 4); - int messageLength = BitConverter.ToInt32( - dataToVerify.Array, - dataToVerify.Offset + 4); - string actualSignature = Utils.ToHexString(signature); -#endif - m_logger.LogError( - "Channel{Id}: Could not validate signature. ChannelId={ChannelId}, TokenId={TokenId}, MessageType={MessageType}, Length={Length} ExpectedSignature={ExpectedSignature} ActualSignature={ActualSignature}", - Id, - token.ChannelId, - token.TokenId, - messageType, - messageLength, - expectedSignature, - actualSignature); - - return false; - } - - return true; - } - - /// - /// Encrypts a message using a symmetric algorithm. - /// - /// - private static void SymmetricEncrypt( - ChannelToken token, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - SymmetricAlgorithm encryptingKey = - (useClientKeys ? token.ClientEncryptor : token.ServerEncryptor) - ?? throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - - using ICryptoTransform encryptor = encryptingKey.CreateEncryptor(); - byte[] blockToEncrypt = dataToEncrypt.Array; - - int start = dataToEncrypt.Offset; - int count = dataToEncrypt.Count; - - if (count % encryptor.InputBlockSize != 0) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Input data is not an even number of encryption blocks."); - } - encryptor.TransformBlock(blockToEncrypt, start, count, blockToEncrypt, start); - } - - /// - /// Decrypts a message using a symmetric algorithm. - /// - /// - private static void SymmetricDecrypt( - ChannelToken token, - ArraySegment dataToDecrypt, - bool useClientKeys) - { - // get the decrypting key. - SymmetricAlgorithm decryptingKey = - (useClientKeys ? token.ClientEncryptor : token.ServerEncryptor) - ?? throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - - using ICryptoTransform decryptor = decryptingKey.CreateDecryptor(); - byte[] blockToDecrypt = dataToDecrypt.Array; - - int start = dataToDecrypt.Offset; - int count = dataToDecrypt.Count; - - if (count % decryptor.InputBlockSize != 0) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Input data is not an even number of encryption blocks."); - } - - decryptor.TransformBlock(blockToDecrypt, start, count, blockToDecrypt, start); - } - -#if CURVE25519 - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricEncryptWithChaCha20Poly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var encryptingKey = (useClientKeys) ? token.ClientEncryptingKey : token.ServerEncryptingKey; - - if (encryptingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var iv = (useClientKeys) ? token.ClientInitializationVector : token.ServerInitializationVector; - - if (iv == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - // Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}"); - // Utils.Trace($"EncryptIV1={Utils.ToHexString(iv)}"); - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, iv); - // Utils.Trace($"EncryptIV2={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - var plaintext = dataToEncrypt.Array; - int headerSize = dataToEncrypt.Offset; - int plainTextLength = dataToEncrypt.Offset + dataToEncrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 encryptor = new ChaCha20Poly1305(); - encryptor.Init(true, parameters); - encryptor.ProcessAadBytes(plaintext, 0, headerSize); - - byte[] ciphertext = new byte[encryptor.GetOutputSize(plainTextLength - headerSize) + headerSize]; - Buffer.BlockCopy(plaintext, 0, ciphertext, 0, headerSize); - int length = encryptor.ProcessBytes( - plaintext, - headerSize, - plainTextLength - headerSize, - ciphertext, - headerSize); - length += encryptor.DoFinal(ciphertext, length + headerSize); - - if (ciphertext.Length - headerSize != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Cipher text not the expected size. [{ciphertext.Length - headerSize} != {length}]"); - } - - Buffer.BlockCopy(ciphertext, 0, plaintext, 0, plainTextLength + signatureLength); - - // byte[] mac = new byte[16]; - // Buffer.BlockCopy(plaintext, plainTextLength, mac, 0, signatureLength); - // Utils.Trace($"EncryptMAC1={Utils.ToHexString(encryptor.GetMac())}"); - // Utils.Trace($"EncryptMAC2={Utils.ToHexString(mac)}"); - } - - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricSignWithPoly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, signingKey); - - using (var hash = SHA256.Create()) - { - signingKey = hash.ComputeHash(signingKey); - } - - // Utils.Trace($"SigningKey={Utils.ToHexString(signingKey)}"); - - int signatureLength = 16; - - var plaintext = dataToEncrypt.Array; - int headerSize = dataToEncrypt.Offset; - int plainTextLength = dataToEncrypt.Offset + dataToEncrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - Poly1305 poly = new Poly1305(); - - poly.Init(new KeyParameter(signingKey, 0, signingKey.Length)); - poly.BlockUpdate(plaintext, 0, plainTextLength); - int length = poly.DoFinal(plaintext, plainTextLength); - - if (signatureLength != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Signed data not the expected size. [{plainTextLength + signatureLength} != {length}]"); - } - } - - /// - /// Decrypts a message using a symmetric algorithm. - /// - private static void SymmetricDecryptWithChaCha20Poly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToDecrypt, - bool useClientKeys) - { - var encryptingKey = (useClientKeys) ? token.ClientEncryptingKey : token.ServerEncryptingKey; - - if (encryptingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var iv = (useClientKeys) ? token.ClientInitializationVector : token.ServerInitializationVector; - - if (iv == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - // Utils.Trace($"DecryptKey={Utils.ToHexString(encryptingKey)}"); - // Utils.Trace($"DecryptIV1={Utils.ToHexString(iv)}"); - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, iv); - // Utils.Trace($"DecryptIV2={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - var ciphertext = dataToDecrypt.Array; - int headerSize = dataToDecrypt.Offset; - int cipherTextLength = dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{cipherTextLength}|{signatureLength}|[{cipherTextLength + signatureLength}]"); - - byte[] mac = new byte[16]; - Buffer.BlockCopy(ciphertext, cipherTextLength, mac, 0, signatureLength); - // Utils.Trace($"DecryptMAC={Utils.ToHexString(mac)}"); - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 decryptor = new ChaCha20Poly1305(); - decryptor.Init(false, parameters); - decryptor.ProcessAadBytes(ciphertext, 0, headerSize); - - var plaintext = new byte[ - decryptor.GetOutputSize(cipherTextLength + signatureLength - headerSize) + headerSize - ]; - Buffer.BlockCopy(ciphertext, headerSize, plaintext, 0, headerSize); - - int length = decryptor.ProcessBytes( - ciphertext, - headerSize, - cipherTextLength + signatureLength - headerSize, - plaintext, - headerSize); - length += decryptor.DoFinal(plaintext, length + headerSize); - - if (plaintext.Length - headerSize != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Plain text not the expected size. [{plaintext.Length - headerSize} != {length}]"); - } - - Buffer.BlockCopy(plaintext, 0, ciphertext, 0, cipherTextLength); - } - - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricVerifyWithPoly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToDecrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, signingKey); - // Utils.Trace($"SigningKey={Utils.ToHexString(signingKey)}"); - - using (var hash = SHA256.Create()) - { - signingKey = hash.ComputeHash(signingKey); - } - - int signatureLength = 16; - - var plaintext = dataToDecrypt.Array; - int headerSize = dataToDecrypt.Offset; - int plainTextLength = dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - Poly1305 poly = new Poly1305(); - - poly.Init(new KeyParameter(signingKey, 0, signingKey.Length)); - poly.BlockUpdate(plaintext, 0, plainTextLength); - - byte[] mac = new byte[poly.GetMacSize()]; - int length = poly.DoFinal(mac, 0); - - if (signatureLength != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Signed data not the expected size. [{plainTextLength + signatureLength} != {length}]"); - } - - for (int ii = 0; ii < mac.Length; ii++) - { - if (mac[ii] != plaintext[plainTextLength + ii]) - { - throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, $"Invaid MAC on data."); - } - } - } - - private static void ApplyChaCha20Poly1305Mask(ChannelToken token, uint lastSequenceNumber, byte[] iv) - { - iv[0] ^= (byte)((token.TokenId & 0x000000FF)); - iv[1] ^= (byte)((token.TokenId & 0x0000FF00) >> 8); - iv[2] ^= (byte)((token.TokenId & 0x00FF0000) >> 16); - iv[3] ^= (byte)((token.TokenId & 0xFF000000) >> 24); - iv[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); - iv[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); - iv[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); - iv[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); + return CryptoUtils.SymmetricDecryptAndVerify( + dataToDecrypt, + token.SecurityPolicy, + useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey, + useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector, + useClientKeys ? token.ClientSigningKey : token.ServerSigningKey, + this.SecurityMode == MessageSecurityMode.Sign, + token.TokenId, + (uint)m_remoteSequenceNumber); } -#endif private static readonly byte[] s_hkdfClientLabel = Encoding.UTF8.GetBytes("opcua-client"); private static readonly byte[] s_hkdfServerLabel = Encoding.UTF8.GetBytes("opcua-server"); - private static readonly byte[] s_hkdfAes128SignOnlyKeyLength = BitConverter.GetBytes( - (ushort)32); - private static readonly byte[] s_hkdfAes256SignOnlyKeyLength = BitConverter.GetBytes( - (ushort)48); - private static readonly byte[] s_hkdfAes128SignAndEncryptKeyLength = BitConverter.GetBytes( - (ushort)64); - private static readonly byte[] s_hkdfAes256SignAndEncryptKeyLength = BitConverter.GetBytes( - (ushort)96); - private static readonly byte[] s_hkdfChaCha20Poly1305KeyLength = BitConverter.GetBytes( - (ushort)76); private int m_signatureKeySize; private int m_encryptionKeySize; } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index 61b1e91354..cdab6337c9 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -251,6 +251,15 @@ protected virtual void Dispose(bool disposing) /// public string GlobalChannelId { get; private set; } + /// + internal byte[] ChannelThumbprint { get; set; } + + /// + public byte[] ClientChannelCertificate { get; protected set; } + + /// + public byte[] ServerChannelCertificate { get; protected set; } + /// /// Raised when the state of the channel changes. /// @@ -284,7 +293,7 @@ protected void ChannelStateChanged(TcpChannelState state, ServiceResult reason) /// protected uint GetNewSequenceNumber() { - bool isLegacy = !EccUtils.IsEccPolicy(SecurityPolicyUri); + bool isLegacy = SecurityPolicy.LegacySequenceNumbers; long newSeqNumber = Interlocked.Increment(ref m_sequenceNumber); bool maxValueOverflow = isLegacy @@ -333,8 +342,8 @@ protected bool VerifySequenceNumber(uint sequenceNumber, string context) // Accept the first sequence number depending on security policy if (m_firstReceivedSequenceNumber && ( - !EccUtils.IsEccPolicy(SecurityPolicyUri) || - (EccUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) + !CryptoUtils.IsEccPolicy(SecurityPolicyUri) || + (CryptoUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) { m_remoteSequenceNumber = sequenceNumber; m_firstReceivedSequenceNumber = false; @@ -355,8 +364,8 @@ protected bool VerifySequenceNumber(uint sequenceNumber, string context) // only one rollover per token is allowed and with valid values depending on security policy if (!m_sequenceRollover && ( - !EccUtils.IsEccPolicy(SecurityPolicyUri) || - (EccUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) + !CryptoUtils.IsEccPolicy(SecurityPolicyUri) || + (CryptoUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) { m_sequenceRollover = true; m_remoteSequenceNumber = sequenceNumber; @@ -630,6 +639,8 @@ protected void BeginWriteMessage(BufferCollection buffers, object state) try { + // m_logger.LogWarning("OUT:{Id}", TcpMessageType.GetTypeAndSize(buffers[0])); + Interlocked.Increment(ref m_activeWriteRequests); args.BufferList = buffers; args.Completed += OnWriteComplete; diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index f97a51b9bc..c0e1128632 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -34,6 +34,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -551,6 +552,11 @@ private void SendOpenSecureChannelRequest(bool renew) ChannelToken token = CreateToken(); token.ClientNonce = CreateNonce(ClientCertificate); + if (renew) + { + token.PreviousSecret = CurrentToken?.Secret; + } + // construct the request. var request = new OpenSecureChannelRequest(); request.RequestHeader.Timestamp = DateTime.UtcNow; @@ -565,6 +571,12 @@ private void SendOpenSecureChannelRequest(bool renew) // encode the request. byte[] buffer = BinaryEncoder.EncodeMessage(request, Quotas.MessageContext); + ClientChannelCertificate = ClientCertificate?.RawData; + ServerChannelCertificate = ServerCertificate?.RawData; + + m_oscRequestSignature = null; + byte[] signature; + // write the asymmetric message. BufferCollection? chunksToSend = WriteAsymmetricMessage( TcpMessageType.Open, @@ -572,7 +584,18 @@ private void SendOpenSecureChannelRequest(bool renew) ClientCertificate, ClientCertificateChain, ServerCertificate, - new ArraySegment(buffer, 0, buffer.Length)); + new ArraySegment(buffer, 0, buffer.Length), + m_oscRequestSignature, + out signature); + + // don't keep signature if secure channel enhancements are not used. + m_oscRequestSignature = (SecurityPolicy.SecureChannelEnhancements) ? signature : null; + + CryptoTrace.Start(ConsoleColor.Magenta, $"SendOpenSecureChannelRequest ({(renew ? "RENEW" : "OPEN")})"); + CryptoTrace.WriteLine($"ClientCertificate={ClientCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"ServerCertificate={ServerCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"RequestSignature={CryptoTrace.KeyToString(signature)}"); + CryptoTrace.Finish("SendOpenSecureChannelRequest"); // save token. m_requestedToken = token; @@ -627,13 +650,32 @@ private bool ProcessOpenSecureChannelResponse( uint sequenceNumber; try { + byte[] signature; + messageBody = ReadAsymmetricMessage( messageChunk, ClientCertificate, out channelId, out serverCertificate, out requestId, - out sequenceNumber); + out sequenceNumber, + (State == TcpChannelState.Opening) ? m_oscRequestSignature : null, + out signature); + + if (State == TcpChannelState.Opening) + { + ChannelThumbprint = signature; + } + + CryptoTrace.Start(ConsoleColor.Magenta, $"ProcessOpenSecureChannelResponse ({(State != TcpChannelState.Opening ? "RENEW" : "OPEN")})"); + CryptoTrace.WriteLine($"messageBody={CryptoTrace.KeyToString(messageBody)}"); + CryptoTrace.WriteLine($"messageBody.Offset={messageBody.Offset}"); + CryptoTrace.WriteLine($"ClientCertificate={ClientCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"ServerCertificate={ServerCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"RequestSignature={CryptoTrace.KeyToString(m_oscRequestSignature)}"); + CryptoTrace.WriteLine($"ResponseSignature={CryptoTrace.KeyToString(signature)}"); + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(ChannelThumbprint)}"); + CryptoTrace.Finish("ProcessOpenSecureChannelResponse"); } catch (Exception e) { @@ -1753,5 +1795,6 @@ private bool ProcessResponseMessage(uint messageType, ArraySegment message private List? m_queuedOperations; private readonly ILogger m_logger; private readonly ITelemetryContext m_telemetry; + private byte[]? m_oscRequestSignature; } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index c3658c7230..4ee76828fb 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -121,7 +121,16 @@ public IServiceMessageContext MessageContext => m_quotas?.MessageContext ?? throw BadNotConnected(); /// - public ChannelToken? CurrentToken => m_channel?.CurrentToken; + public ChannelToken CurrentToken => m_channel?.CurrentToken ?? new(); + + /// + public byte[] ChannelThumbprint => m_channel?.ChannelThumbprint ?? []; + + /// + public byte[] ClientChannelCertificate => m_channel?.ClientChannelCertificate ?? []; + + /// + public byte[] ServerChannelCertificate => m_channel?.ServerChannelCertificate ?? []; /// public int OperationTimeout { get; set; } @@ -445,9 +454,11 @@ private UaSCUaBinaryClientChannel CreateChannel( } } + var id = Guid.NewGuid().ToString(); + // create the channel. var channel = new UaSCUaBinaryClientChannel( - Guid.NewGuid().ToString(), + id, m_bufferManager, m_messageSocketFactory, m_quotas, diff --git a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs index 23ce1b64df..7de67cf0f1 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs @@ -116,6 +116,21 @@ public interface ITransportChannel : IDisposable /// EndpointConfiguration EndpointConfiguration { get; } + /// + /// The unique identifier for the secure channel. + /// + byte[] ChannelThumbprint { get; } + + /// + /// The client certificate used to establsih the secure channel. + /// + byte[] ClientChannelCertificate { get; } + + /// + /// The server certificate used to establsih the secure channel. + /// + byte[] ServerChannelCertificate { get; } + /// /// Gets the context used when serializing messages exchanged /// via the channel. Throws if the channel is not yet opened. diff --git a/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs index f0c728bebd..b6787e56b5 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs @@ -41,6 +41,10 @@ namespace Opc.Ua /// internal sealed class NullChannel : ITransportChannel, ISecureChannel { + /// + public ChannelToken CurrentToken + => throw Unexpected(nameof(CurrentToken)); + /// public TransportChannelFeatures SupportedFeatures => throw Unexpected(nameof(SupportedFeatures)); @@ -58,8 +62,16 @@ public IServiceMessageContext MessageContext => throw Unexpected(nameof(MessageContext)); /// - public ChannelToken CurrentToken - => throw Unexpected(nameof(CurrentToken)); + public byte[] ChannelThumbprint + => throw Unexpected(nameof(ChannelThumbprint)); + + /// + public byte[] ClientChannelCertificate + => throw Unexpected(nameof(ClientChannelCertificate)); + + /// + public byte[] ServerChannelCertificate + => throw Unexpected(nameof(ServerChannelCertificate)); /// public int OperationTimeout { get; set; } diff --git a/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityTokenHandler.cs b/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityTokenHandler.cs index 95363b6b1b..5c5972a1df 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityTokenHandler.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityTokenHandler.cs @@ -114,7 +114,9 @@ public void Encrypt( } // handle RSA encryption. - if (!EccUtils.IsEccPolicy(securityPolicyUri)) + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + if (securityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None) { byte[] dataToEncrypt = Utils.Append(DecryptedPassword, receiverNonce); @@ -129,7 +131,8 @@ public void Encrypt( m_token.EncryptionAlgorithm = encryptedData.Algorithm; Array.Clear(dataToEncrypt, 0, dataToEncrypt.Length); } - // handle ECC encryption. + + // handle ECC and RSADH encryption. else { // check if the complete chain is included in the sender issuers. @@ -154,7 +157,7 @@ public void Encrypt( receiverCertificate, receiverEphemeralKey, senderCertificate, - Nonce.CreateNonce(securityPolicyUri), + Nonce.CreateNonce(securityPolicy), null, doNotEncodeSenderCertificate); @@ -190,7 +193,9 @@ public void Decrypt( } // handle RSA encryption. - if (!EccUtils.IsEccPolicy(securityPolicyUri)) + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + if (securityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None) { var encryptedData = new EncryptedData { @@ -234,7 +239,8 @@ public void Decrypt( Array.Copy(decryptedPassword, DecryptedPassword, startOfNonce); Array.Clear(decryptedPassword, 0, decryptedPassword.Length); } - // handle ECC encryption. + + // handle ECC and RSADH encryption. else { var secret = new EncryptedSecret( diff --git a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs index 45817b6cc8..a72bcd00c4 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs @@ -43,6 +43,13 @@ public sealed class X509IdentityTokenHandler : IUserIdentityTokenHandler public X509IdentityTokenHandler(X509IdentityToken token) { m_token = token; + + if (m_token.CertificateData != null) + { + m_certificate = CertificateFactory.Create(m_token.CertificateData); + } + + m_ownsCertificate = true; } /// @@ -66,12 +73,29 @@ public X509IdentityTokenHandler(X509Certificate2 certificate) } Certificate = certificate; + m_ownsCertificate = true; m_token = new X509IdentityToken { CertificateData = certificate.RawData }; } + /// + /// Private constructor for . The cloned handler + /// shares the certificate reference but does not own it, so it will + /// not dispose the certificate. This is necessary because the + /// certificate's private key may reside in protected storage and + /// cannot be deep-copied. + /// + private X509IdentityTokenHandler( + X509IdentityToken token, + X509Certificate2 certificate) + { + m_token = token; + m_certificate = certificate; + m_ownsCertificate = false; + } + /// /// The certificate associated with the token. /// @@ -134,16 +158,13 @@ public SignatureData Sign( byte[] dataToSign, string securityPolicyUri) { - X509Certificate2 certificate = Certificate ?? - CertificateFactory.Create(m_token.CertificateData); + var info = SecurityPolicies.GetInfo(securityPolicyUri); - SignatureData signatureData = SecurityPolicies.Sign( - certificate, - securityPolicyUri, + var signatureData = SecurityPolicies.CreateSignatureData( + info, + m_certificate, dataToSign); - m_token.CertificateData = certificate.RawData; - return signatureData; } @@ -155,16 +176,13 @@ public bool Verify( { try { - X509Certificate2 certificate = Certificate ?? - CertificateFactory.Create(m_token.CertificateData); - - bool valid = SecurityPolicies.Verify( - certificate, - securityPolicyUri, - dataToVerify, - signatureData); + var info = SecurityPolicies.GetInfo(securityPolicyUri); - m_token.CertificateData = certificate.RawData; + bool valid = SecurityPolicies.VerifySignatureData( + signatureData, + info, + m_certificate, + dataToVerify); return valid; } @@ -180,17 +198,19 @@ public bool Verify( /// public void Dispose() { - // TODOL Utils.SilentDispose(m_certificate); + if (m_ownsCertificate) + { + Utils.SilentDispose(m_certificate); + } m_certificate = null; } /// public object Clone() { - return new X509IdentityTokenHandler(Utils.Clone(m_token)) - { - // TODO: m_certificate = m_certificate - }; + return new X509IdentityTokenHandler( + Utils.Clone(m_token), + m_certificate); } /// @@ -204,6 +224,7 @@ public bool Equals(IUserIdentityTokenHandler other) } private readonly X509IdentityToken m_token; + private readonly bool m_ownsCertificate; private X509Certificate2 m_certificate; } } diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index c3c653fbf6..9cbc24cf79 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -666,7 +666,14 @@ public static IPAddress[] GetHostAddresses(string hostNameOrAddress) /// If the platform returns a FQDN, only the host name is returned. public static string GetHostName() { - return Dns.GetHostName().Split('.')[0].ToLowerInvariant(); + var hostName = Dns.GetHostName(); + // If platform returns an IPv4 or IPv6 address return it as is + if (IPAddress.TryParse(hostName, out _)) + { + return hostName; + } + + return hostName.Split('.')[0].ToLowerInvariant(); } /// diff --git a/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs b/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs index 905cbd9bd0..2c034f926a 100644 --- a/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs +++ b/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs @@ -141,7 +141,7 @@ public void Dispose() [Conditional("CHECKED")] private static void DebugCheck() { - Debug.Fail("Using a NullLogger"); + //Debug.Fail("Using a NullLogger"); } } diff --git a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs index c6663d3c13..3ec98875fc 100644 --- a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs @@ -245,7 +245,8 @@ public IEncodeable DecodeMessage(Type expectedType) // lookup message type. Type actualType = Context.Factory.GetSystemType(absoluteId) - ?? throw ServiceResultException.Create( + ?? + throw ServiceResultException.Create( StatusCodes.BadDecodingError, "Cannot decode message with type id: {0}.", absoluteId); diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 815c63a970..2544345e32 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -79,6 +79,31 @@ public ClientTest(string uriScheme = Utils.UriSchemeOpcTcp) ObjectIds.XmlSchema_TypeSystem ]; + public static readonly string[] SupportedEccPolicies = + [ + .. GetSupportedEccPolicyUris(includeCurvePolicies: false) + ]; + + public static readonly string[] SupportedEccX509Policies = + [ + .. SupportedEccPolicies.Where(policyUri => + { + CertificateKeyAlgorithm certificateKeyAlgorithm = + SecurityPolicies.GetInfo(policyUri).CertificateKeyAlgorithm; + return certificateKeyAlgorithm != CertificateKeyAlgorithm.Curve25519 && + certificateKeyAlgorithm != CertificateKeyAlgorithm.Curve448; + }) + ]; + + public static IEnumerable ReconnectSessionOnAlternateChannelWithSavedSessionSecretsEccTestCases() + { + foreach (string securityPolicy in SupportedEccPolicies) + { + yield return new TestCaseData(securityPolicy, true); + yield return new TestCaseData(securityPolicy, false); + } + } + /// /// Set up a Server and a Client instance. /// @@ -841,18 +866,17 @@ await session1.ReadValueAsync( [TestCase(SecurityPolicies.None, false)] [TestCase(SecurityPolicies.Basic256Sha256, true)] [TestCase(SecurityPolicies.Basic256Sha256, false)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1, true)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1, false)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1, true)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1, false)] - [TestCase(SecurityPolicies.ECC_nistP256, true)] - [TestCase(SecurityPolicies.ECC_nistP256, false)] - [TestCase(SecurityPolicies.ECC_nistP384, true)] - [TestCase(SecurityPolicies.ECC_nistP384, false)] + [TestCase(SecurityPolicies.RSA_DH_AesGcm, true)] + [TestCase(SecurityPolicies.RSA_DH_AesGcm, false)] + [TestCase(SecurityPolicies.RSA_DH_ChaChaPoly, true)] + [TestCase(SecurityPolicies.RSA_DH_ChaChaPoly, false)] + [TestCaseSource(nameof(ReconnectSessionOnAlternateChannelWithSavedSessionSecretsEccTestCases))] public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecretsAsync( string securityPolicy, bool anonymous) { + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); + ServiceResultException sre; UserIdentity userIdentity = anonymous @@ -1830,46 +1854,39 @@ public async Task ReadBuildInfoAsync() [Combinatorial] [Order(10100)] public async Task OpenSessionECCUserNamePwdIdentityTokenAsync( - [Values( - SecurityPolicies.ECC_nistP256, - SecurityPolicies.ECC_nistP384, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1 - )] string securityPolicy) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || - (securityPolicy != SecurityPolicies.ECC_brainpoolP256r1 && - securityPolicy != SecurityPolicies.ECC_brainpoolP384r1)) - { - var userIdentity = new UserIdentity("user1", "password"u8); - - // the first channel determines the endpoint - ConfiguredEndpoint endpoint = await ClientFixture - .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) - .ConfigureAwait(false); - Assert.NotNull(endpoint); + [ValueSource(nameof(SupportedEccPolicies))] string securityPolicy) + { + IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); - UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy( - userIdentity.TokenType, - userIdentity.IssuedTokenType, - endpoint.Description.SecurityPolicyUri); - if (identityPolicy == null) - { - NUnit.Framework.Assert.Ignore( - $"No UserTokenPolicy found for {userIdentity.TokenType}" + - $" / {userIdentity.IssuedTokenType}"); - } + var userIdentity = new UserIdentity("user1", "password"u8); - // the active channel - ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity) - .ConfigureAwait(false); - Assert.NotNull(session1); + // the first channel determines the endpoint + ConfiguredEndpoint endpoint = await ClientFixture + .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) + .ConfigureAwait(false); + Assert.NotNull(endpoint); - ServerStatusDataType value1 = - await session1.ReadValueAsync( - VariableIds.Server_ServerStatus).ConfigureAwait(false); - Assert.NotNull(value1); + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy( + userIdentity.TokenType, + userIdentity.IssuedTokenType, + endpoint.Description.SecurityPolicyUri); + if (identityPolicy == null) + { + NUnit.Framework.Assert.Ignore( + $"No UserTokenPolicy found for {userIdentity.TokenType}" + + $" / {userIdentity.IssuedTokenType}"); } + + // the active channel + ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity) + .ConfigureAwait(false); + Assert.NotNull(session1); + + ServerStatusDataType value1 = + await session1.ReadValueAsync( + VariableIds.Server_ServerStatus).ConfigureAwait(false); + Assert.NotNull(value1); } /// @@ -1879,51 +1896,44 @@ await session1.ReadValueAsync( [Combinatorial] [Order(10200)] public async Task OpenSessionECCIssuedIdentityTokenAsync( - [Values( - SecurityPolicies.ECC_nistP256, - SecurityPolicies.ECC_nistP384, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1 - )] string securityPolicy) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || - (securityPolicy != SecurityPolicies.ECC_brainpoolP256r1 && - securityPolicy != SecurityPolicies.ECC_brainpoolP384r1)) - { - const string identityToken = "fakeTokenString"; - - using var issuedToken = new IssuedIdentityTokenHandler( - Profiles.JwtUserToken, - Encoding.UTF8.GetBytes(identityToken)); - var userIdentity = new UserIdentity(issuedToken); - - // the first channel determines the endpoint - ConfiguredEndpoint endpoint = await ClientFixture - .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) - .ConfigureAwait(false); - Assert.NotNull(endpoint); + [ValueSource(nameof(SupportedEccPolicies))] string securityPolicy) + { + IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); - UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy( - userIdentity.TokenType, - userIdentity.IssuedTokenType, - securityPolicy); + const string identityToken = "fakeTokenString"; - if (identityPolicy == null) - { - NUnit.Framework.Assert.Ignore( - $"No UserTokenPolicy found for {userIdentity.TokenType}" + - $" / {userIdentity.IssuedTokenType}"); - } + using var issuedToken = new IssuedIdentityTokenHandler( + Profiles.JwtUserToken, + Encoding.UTF8.GetBytes(identityToken)); + var userIdentity = new UserIdentity(issuedToken); - // the active channel - ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity) - .ConfigureAwait(false); - Assert.NotNull(session1); + // the first channel determines the endpoint + ConfiguredEndpoint endpoint = await ClientFixture + .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) + .ConfigureAwait(false); + Assert.NotNull(endpoint); - ServerStatusDataType value1 = await session1.ReadValueAsync( - VariableIds.Server_ServerStatus).ConfigureAwait(false); - Assert.NotNull(value1); + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy( + userIdentity.TokenType, + userIdentity.IssuedTokenType, + securityPolicy); + + if (identityPolicy == null) + { + NUnit.Framework.Assert.Ignore( + $"No UserTokenPolicy found for {userIdentity.TokenType}" + + $" / {userIdentity.IssuedTokenType}"); } + + // the active channel + ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity) + .ConfigureAwait(false); + Assert.NotNull(session1); + + ServerStatusDataType value1 = await session1.ReadValueAsync( + VariableIds.Server_ServerStatus).ConfigureAwait(false); + Assert.NotNull(value1); } /// @@ -1933,14 +1943,11 @@ public async Task OpenSessionECCIssuedIdentityTokenAsync( [Combinatorial] [Order(10300)] public async Task OpenSessionECCUserCertIdentityTokenAsync( - [Values( - SecurityPolicies.ECC_nistP256, - SecurityPolicies.ECC_nistP384, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1 - )] - string securityPolicy) + [ValueSource(nameof(SupportedEccX509Policies))] string securityPolicy) { + IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); + var eccCurveHashPairs = new ECCurveHashPairCollection { { ECCurve.NamedCurves.nistP256, HashAlgorithmName.SHA256 }, @@ -2326,5 +2333,15 @@ private static void ValidateOperationLimit(uint serverLimit, uint clientLimit) Assert.NotZero(clientLimit); } } + + private static void IgnoreUnsupportedBrainpoolOnMacOs(string securityPolicyUri) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && + (securityPolicyUri.Contains("ECC_brainpoolP256r1", StringComparison.Ordinal) || + securityPolicyUri.Contains("ECC_brainpoolP384r1", StringComparison.Ordinal))) + { + NUnit.Framework.Assert.Ignore("Brainpool curve is not supported on Mac OS."); + } + } } } diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs index 4fc2683f9e..271841716d 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs @@ -105,9 +105,61 @@ public void InitializeSession(ISession session) [DatapointSource] public static readonly string[] Policies = [ - .. SecurityPolicies.GetDisplayNames().Select(SecurityPolicies.GetUri) + .. GetPolicyUrisForTests() ]; + protected static IEnumerable GetSupportedEccPolicyUris( + bool includeCurvePolicies = true) + { + IEnumerable displayNames = SecurityPolicies.GetDisplayNames() + .Where(name => name.StartsWith("ECC_", StringComparison.Ordinal)); + + if (!includeCurvePolicies) + { + displayNames = displayNames + .Where(name => !name.StartsWith("ECC_curve", StringComparison.Ordinal)); + } + + return displayNames.Select(SecurityPolicies.GetUri); + } + + private static IEnumerable GetPolicyUrisForTests() + { + IEnumerable displayNames = SecurityPolicies.GetDisplayNames(); + +#if NETFRAMEWORK + displayNames = displayNames.Where(name => + !name.EndsWith("_AesGcm", StringComparison.Ordinal) && + !name.EndsWith("_ChaChaPoly", StringComparison.Ordinal)); +#endif + + return displayNames.Select(SecurityPolicies.GetUri); + } + + protected async Task IgnoreIfPolicyNotAdvertisedAsync(string securityPolicyUri) + { + Endpoints ??= await ClientFixture.GetEndpointsAsync(ServerUrl).ConfigureAwait(false); + if (Endpoints?.Any(endpoint => + string.Equals( + endpoint.SecurityPolicyUri, + securityPolicyUri, + StringComparison.Ordinal)) != true) + { + string advertisedPolicies = Endpoints == null + ? "" + : string.Join( + ", ", + Endpoints + .Select(endpoint => endpoint.SecurityPolicyUri) + .Where(policy => !string.IsNullOrEmpty(policy)) + .Distinct() + .OrderBy(policy => policy, StringComparer.Ordinal)); + NUnit.Framework.Assert.Ignore( + $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server. " + + $"Advertised: {advertisedPolicies}"); + } + } + /// /// Set up a Server and a Client instance. /// @@ -240,82 +292,28 @@ public virtual async Task CreateReferenceServerFixtureAsync( IssuedTokenType = Profiles.JwtUserToken }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.UserName) - { - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.UserName) - { - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.UserName) - { - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.UserName) - { - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" - }); + foreach (string securityPolicyUri in GetSupportedEccPolicyUris()) + { + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( + new UserTokenPolicy(UserTokenType.UserName) + { + SecurityPolicyUri = securityPolicyUri + }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.Certificate) - { - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.Certificate) - { - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.Certificate) - { - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.Certificate) - { - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" - }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( + new UserTokenPolicy(UserTokenType.Certificate) + { + SecurityPolicyUri = securityPolicyUri + }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.IssuedToken) - { - IssuedTokenType = Profiles.JwtUserToken, - PolicyId = Profiles.JwtUserToken, - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.IssuedToken) - { - IssuedTokenType = Profiles.JwtUserToken, - PolicyId = Profiles.JwtUserToken, - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.IssuedToken) - { - IssuedTokenType = Profiles.JwtUserToken, - PolicyId = Profiles.JwtUserToken, - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.IssuedToken) - { - IssuedTokenType = Profiles.JwtUserToken, - PolicyId = Profiles.JwtUserToken, - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" - }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( + new UserTokenPolicy(UserTokenType.IssuedToken) + { + IssuedTokenType = Profiles.JwtUserToken, + PolicyId = Profiles.JwtUserToken, + SecurityPolicyUri = securityPolicyUri + }); + } ServerFixture.Config.ServerConfiguration.MaxChannelCount = MaxChannelCount; ServerFixture.Config.ServerConfiguration.MaxSubscriptionCount = 1000; diff --git a/Tests/Opc.Ua.Client.Tests/SecurityPolicyBenchmarks.cs b/Tests/Opc.Ua.Client.Tests/SecurityPolicyBenchmarks.cs index 6681494424..c65b919481 100644 --- a/Tests/Opc.Ua.Client.Tests/SecurityPolicyBenchmarks.cs +++ b/Tests/Opc.Ua.Client.Tests/SecurityPolicyBenchmarks.cs @@ -155,13 +155,12 @@ public SecurityPolicyBenchmarks() /// /// Override to exclude None policy from benchmarks to avoid CI test failures. + /// Uses the base policy list so target-specific filtering is preserved. /// public new IEnumerable BenchPolicies() { - // Return all security policies except None - foreach (string displayName in SecurityPolicies.GetDisplayNames()) + foreach (string policyUri in Policies) { - string policyUri = SecurityPolicies.GetUri(displayName); if (policyUri != SecurityPolicies.None) { yield return policyUri; diff --git a/Tests/Opc.Ua.Client.Tests/SessionTests.cs b/Tests/Opc.Ua.Client.Tests/SessionTests.cs index 703b983d25..c5c19ff9e5 100644 --- a/Tests/Opc.Ua.Client.Tests/SessionTests.cs +++ b/Tests/Opc.Ua.Client.Tests/SessionTests.cs @@ -1570,5 +1570,54 @@ public void SaveShouldOnlySaveSpecifiedSubscriptions() Assert.That(loadedSubscriptions.Count, Is.EqualTo(2), "Only the specified subscriptions should be saved"); Assert.That(loadedSubscriptions.Select(s => s.DisplayName), Is.EquivalentTo(["Subscription1", "Subscription3"])); } + + [Test] + public void SaveAndApplySessionConfigurationShouldPersistClientNonce() + { + var source = SessionMock.Create(); + source.SetConnected(); + + byte[] clientNonce = [1, 3, 5, 7, 9]; + byte[] serverNonce = [2, 4, 6, 8]; + + SetPrivateByteArrayField(source, "m_clientNonce", clientNonce); + SetPrivateByteArrayField(source, "m_serverNonce", serverNonce); + + SessionConfiguration configuration = source.SaveSessionConfiguration(); + + Assert.That(configuration.ClientNonce, Is.EquivalentTo(clientNonce)); + Assert.That(configuration.ServerNonce, Is.EquivalentTo(serverNonce)); + + var target = SessionMock.Create(); + + bool success = target.ApplySessionConfiguration(configuration); + + Assert.That(success, Is.True); + Assert.That(GetPrivateByteArrayField(target, "m_clientNonce"), Is.EquivalentTo(clientNonce)); + Assert.That(GetPrivateByteArrayField(target, "m_serverNonce"), Is.EquivalentTo(serverNonce)); + } + + private static byte[] GetPrivateByteArrayField(Session session, string fieldName) + { + return (byte[])typeof(Session) + .GetField( + fieldName, + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance)? + .GetValue(session); + } + + private static void SetPrivateByteArrayField( + Session session, + string fieldName, + byte[] value) + { + typeof(Session) + .GetField( + fieldName, + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance)? + .SetValue(session, value); + } } } diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index a543f99c9e..3d67660776 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -50,6 +50,11 @@ namespace Opc.Ua.Client.Tests [SetUICulture("en-us")] public class SubscriptionTest : ClientTestFramework { + public static readonly string[] SupportedEccPolicies = + [ + .. GetSupportedEccPolicyUris(includeCurvePolicies: false) + ]; + private readonly string m_subscriptionTestXml = Path.Combine( Path.GetTempPath(), "SubscriptionTest.xml"); @@ -449,13 +454,7 @@ await Session.SetPublishingModeAsync( [Order(351)] [Explicit] public Task ReconnectWithSavedSessionSecretsOnlyECCAsync( - [Values( - SecurityPolicies.ECC_nistP256, - SecurityPolicies.ECC_nistP384, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1 - )] - string securityPolicy, + [ValueSource(nameof(SupportedEccPolicies))] string securityPolicy, [Values(true, false)] bool anonymous, [Values(true, false)] bool sequentialPublishing, [Values(true, false)] bool sendInitialValues) @@ -478,7 +477,9 @@ public Task ReconnectWithSavedSessionSecretsOnlyECCAsync( public Task ReconnectWithSavedSessionSecretsOnlyAsync( [Values(SecurityPolicies.None, SecurityPolicies.ECC_nistP256, - SecurityPolicies.Basic256Sha256)] + SecurityPolicies.Basic256Sha256, + SecurityPolicies.RSA_DH_AesGcm, + SecurityPolicies.RSA_DH_ChaChaPoly)] string securityPolicy, [Values(true, false)] bool anonymous, [Values(true, false)] bool sequentialPublishing, @@ -497,6 +498,8 @@ public async Task ReconnectWithSavedSessionSecretsAsync( bool sequentialPublishing, bool sendInitialValues) { + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); + const int kTestSubscriptions = 5; const int kDelay = 2_000; const int kQueueSize = 10; @@ -1629,5 +1632,6 @@ public async Task ConcurrentCreateItemsNoDuplicatesAsync() // Clean up await subscription.DeleteAsync(true, CancellationToken.None).ConfigureAwait(false); } + } } diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Types/X509IdentityTokenHandlerTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Types/X509IdentityTokenHandlerTests.cs new file mode 100644 index 0000000000..7563695e09 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Stack/Types/X509IdentityTokenHandlerTests.cs @@ -0,0 +1,66 @@ +/* ======================================================================== + * 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.Security.Cryptography.X509Certificates; +using NUnit.Framework; +using Opc.Ua.Security.Certificates; +using Assert = NUnit.Framework.Legacy.ClassicAssert; + +namespace Opc.Ua.Core.Tests.Stack.Types +{ + [TestFixture] + [Category("X509IdentityTokenHandler")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + [Parallelizable] + public class X509IdentityTokenHandlerTests + { + [Test] + public void CopyPreservesPrivateKeyForSigning() + { + using X509Certificate2 cert = CertificateBuilder + .Create("CN=User Identity Test Subject, O=OPC Foundation") + .SetRSAKeySize(2048) + .CreateForRSA(); + + using var tokenHandler = new X509IdentityTokenHandler(cert); + using X509IdentityTokenHandler copy = tokenHandler.Copy(); + + Assert.IsTrue(copy.Certificate.HasPrivateKey); + + SignatureData signature = copy.Sign( + [0x01, 0x02, 0x03, 0x04], + SecurityPolicies.Basic256Sha256); + + Assert.NotNull(signature); + Assert.NotNull(signature.Signature); + Assert.Greater(signature.Signature.Length, 0); + } + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs index 9517ceb487..f7534c9f6f 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs @@ -28,6 +28,8 @@ * ======================================================================*/ using System; +using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; @@ -42,23 +44,29 @@ namespace Opc.Ua.Core.Tests.Types.Nonce [Parallelizable] public class NonceTests { + public static readonly string[] SupportedNoncePolicies = + [ + .. SecurityPolicies.GetDisplayNames() + .Where(name => !name.Equals(nameof(SecurityPolicies.None), StringComparison.Ordinal)) + .Select(SecurityPolicies.GetUri) + ]; + + private static readonly HashSet s_supportedPolicyUris = + [ + .. SecurityPolicies.GetDisplayNames().Select(SecurityPolicies.GetUri) + ]; + /// /// Test the CreateNonce - securitypolicy and valid nonceLength /// [Theory] - [TestCase(SecurityPolicies.ECC_nistP256)] - [TestCase(SecurityPolicies.ECC_nistP384)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] - [TestCase(SecurityPolicies.Basic256)] - [TestCase(SecurityPolicies.Basic256Sha256)] - [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] - [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + [TestCaseSource(nameof(SupportedNoncePolicies))] public void ValidateCreateNoncePolicyLength(string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; var nonce = Ua.Nonce.CreateNonce(securityPolicyUri); @@ -72,22 +80,16 @@ public void ValidateCreateNoncePolicyLength(string securityPolicyUri) /// Test the CreateEccNonce - securitypolicy and nonceData /// [Theory] - [TestCase(SecurityPolicies.ECC_nistP256)] - [TestCase(SecurityPolicies.ECC_nistP384)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] - [TestCase(SecurityPolicies.Basic256)] - [TestCase(SecurityPolicies.Basic256Sha256)] - [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] - [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + [TestCaseSource(nameof(SupportedNoncePolicies))] public void ValidateCreateNoncePolicyNonceData(string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; var nonceByLen = Ua.Nonce.CreateNonce(securityPolicyUri); - var nonceByData = Ua.Nonce.CreateNonce(securityPolicyUri, nonceByLen.Data); + var nonceByData = Ua.Nonce.CreateNonce(info, nonceByLen.Data); Assert.IsNotNull(nonceByData); Assert.IsNotNull(nonceByData.Data); @@ -100,39 +102,33 @@ public void ValidateCreateNoncePolicyNonceData(string securityPolicyUri) /// Test the CreateEccNonce - securitypolicy and invalid nonceData /// [Theory] - [TestCase(SecurityPolicies.ECC_nistP256)] - [TestCase(SecurityPolicies.ECC_nistP384)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] - [TestCase(SecurityPolicies.Basic256)] - [TestCase(SecurityPolicies.Basic256Sha256)] - [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] - [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + [TestCaseSource(nameof(SupportedNoncePolicies))] public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; byte[] randomValue = Ua.Nonce.CreateRandomNonceData(nonceLength); - if (securityPolicyUri.Contains("ECC_", StringComparison.Ordinal)) + if (info.CertificateKeyFamily == CertificateKeyFamily.ECC) { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && ( - securityPolicyUri == SecurityPolicies.ECC_nistP256 || - securityPolicyUri == SecurityPolicies.ECC_nistP384)) + securityPolicyUri.Contains("ECC_nistP256", StringComparison.Ordinal) || + securityPolicyUri.Contains("ECC_nistP384", StringComparison.Ordinal))) { NUnit.Framework.Assert .Ignore("No exception is thrown on OSX with NIST curves"); } NUnit.Framework.Assert.Throws(() => - Ua.Nonce.CreateNonce(securityPolicyUri, randomValue)); + Ua.Nonce.CreateNonce(info, randomValue)); } else { - var rsaNonce = Ua.Nonce.CreateNonce(securityPolicyUri, randomValue); + var rsaNonce = Ua.Nonce.CreateNonce(info, randomValue); Assert.AreEqual(rsaNonce.Data, randomValue); } } @@ -143,31 +139,11 @@ public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( /// private static bool IsSupportedByPlatform(string securityPolicyUri) { - if (securityPolicyUri.Equals(SecurityPolicies.ECC_nistP256, StringComparison.Ordinal)) - { - return Utils.IsSupportedCertificateType( - ObjectTypeIds.EccNistP256ApplicationCertificateType); - } - else if (securityPolicyUri.Equals( - SecurityPolicies.ECC_nistP384, - StringComparison.Ordinal)) - { - return Utils.IsSupportedCertificateType( - ObjectTypeIds.EccNistP384ApplicationCertificateType); - } - else if (securityPolicyUri.Equals( - SecurityPolicies.ECC_brainpoolP256r1, - StringComparison.Ordinal)) - { - return Utils.IsSupportedCertificateType( - ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); - } - else if (securityPolicyUri.Equals( - SecurityPolicies.ECC_brainpoolP384r1, + if (securityPolicyUri.StartsWith( + SecurityPolicies.BaseUri, StringComparison.Ordinal)) { - return Utils.IsSupportedCertificateType( - ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); + return s_supportedPolicyUris.Contains(securityPolicyUri); } return true; diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServer.Config.xml b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServer.Config.xml index a8e65fb8b9..ce7b462efd 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServer.Config.xml +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServer.Config.xml @@ -90,18 +90,50 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256 + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_ChaChaPoly + SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384 + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384_ChaChaPoly + SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1 + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_ChaChaPoly + SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1 + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly + diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index f19a1563fa..beb3f97960 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -28,6 +28,7 @@ * ======================================================================*/ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; @@ -57,6 +58,11 @@ namespace Opc.Ua.Gds.Tests [NonParallelizable] public class PushTest { + private static readonly HashSet s_supportedPolicyUris = + [ + .. SecurityPolicies.GetDisplayNames().Select(SecurityPolicies.GetUri) + ]; + /// /// CertificateTypes to run the Test with. /// For ECC types, the additional fourth element is the expected curve friendly name. @@ -78,6 +84,20 @@ public class PushTest ECCurve.NamedCurves.nistP256 }, new object[] + { + nameof(OpcUa.ObjectTypeIds.EccNistP256ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccNistP256ApplicationCertificateType, + SecurityPolicies.ECC_nistP256_AesGcm, + ECCurve.NamedCurves.nistP256 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccNistP256ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccNistP256ApplicationCertificateType, + SecurityPolicies.ECC_nistP256_ChaChaPoly, + ECCurve.NamedCurves.nistP256 + }, + new object[] { nameof(OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType), OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType, @@ -85,6 +105,20 @@ public class PushTest ECCurve.NamedCurves.nistP384 }, new object[] + { + nameof(OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType, + SecurityPolicies.ECC_nistP384_AesGcm, + ECCurve.NamedCurves.nistP384 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType, + SecurityPolicies.ECC_nistP384_ChaChaPoly, + ECCurve.NamedCurves.nistP384 + }, + new object[] { nameof(OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType), OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType, @@ -92,16 +126,50 @@ public class PushTest ECCurve.NamedCurves.brainpoolP256r1 }, new object[] + { + nameof(OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType, + SecurityPolicies.ECC_brainpoolP256r1_AesGcm, + ECCurve.NamedCurves.brainpoolP256r1 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType, + SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly, + ECCurve.NamedCurves.brainpoolP256r1 + }, + new object[] { nameof(OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType), OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType, SecurityPolicies.ECC_brainpoolP384r1, ECCurve.NamedCurves.brainpoolP384r1 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType, + SecurityPolicies.ECC_brainpoolP384r1_AesGcm, + ECCurve.NamedCurves.brainpoolP384r1 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType, + SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly, + ECCurve.NamedCurves.brainpoolP384r1 } ]; public PushTest(string certificateTypeString, NodeId certificateType, string securityPolicyUri, ECCurve? curve) { + if (!s_supportedPolicyUris.Contains(securityPolicyUri)) + { + NUnit.Framework.Assert.Ignore( + $"Security policy {securityPolicyUri} is not supported on this runtime."); + } + if (!Utils.IsSupportedCertificateType(certificateType)) { NUnit.Framework.Assert.Ignore( @@ -110,7 +178,8 @@ public PushTest(string certificateTypeString, NodeId certificateType, string sec // Skip brainpool curves on Mac OS if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && - (securityPolicyUri == SecurityPolicies.ECC_brainpoolP256r1 || securityPolicyUri == SecurityPolicies.ECC_brainpoolP384r1)) + (securityPolicyUri.Contains("ECC_brainpoolP256r1", StringComparison.Ordinal) || + securityPolicyUri.Contains("ECC_brainpoolP384r1", StringComparison.Ordinal))) { NUnit.Framework.Assert.Ignore("Brainpool curve is not supported on Mac OS."); } @@ -179,8 +248,17 @@ await m_pushClient.LoadClientConfigurationAsync(m_server.BasePort) await m_gdsClient.GDSClient.ConnectAsync(m_gdsClient.GDSClient.EndpointUrl) .ConfigureAwait(false); - await m_pushClient.ConnectAsync(m_securityPolicyUri) - .ConfigureAwait(false); + try + { + await m_pushClient.ConnectAsync(m_securityPolicyUri) + .ConfigureAwait(false); + } + catch (ArgumentException ex) when ( + ex.Message.Contains("No endpoint found for SecurityPolicyUri", StringComparison.Ordinal)) + { + NUnit.Framework.Assert.Ignore( + $"Security policy {m_securityPolicyUri} is not advertised by the GDS test server."); + } await ConnectGDSClientAsync(true).ConfigureAwait(false); @@ -764,7 +842,7 @@ public async Task UpdateCertificateSelfSignedAsync(string keyFormat) X509Certificate2 newCert; - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { @@ -1287,7 +1365,7 @@ private async Task CreateCATestCertsAsync(string tempStorePath, ITelemetryContex var certificateStoreIdentifier = new CertificateStoreIdentifier(tempStorePath, false); Assert.IsTrue(EraseStore(certificateStoreIdentifier, telemetry)); const string subjectName = "CN=CA Test Cert, O=OPC Foundation"; - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs index d04e798ce6..1f668787e2 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs @@ -111,7 +111,7 @@ public CRLTests(string certificateTypeString, NodeId certificateType) [OneTimeSetUp] protected void OneTimeSetUp() { - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { diff --git a/Tests/Opc.Ua.Server.Tests/ClientLockoutTests.cs b/Tests/Opc.Ua.Server.Tests/ClientLockoutTests.cs index 15dff4d53b..fba58efb08 100644 --- a/Tests/Opc.Ua.Server.Tests/ClientLockoutTests.cs +++ b/Tests/Opc.Ua.Server.Tests/ClientLockoutTests.cs @@ -66,10 +66,7 @@ public async Task FailedAuthenticationAttemptsAreTrackedAsync() EndpointDescriptionCollection endpoints = m_server.GetEndpoints(); EndpointDescription endpoint = FindTcpEndpoint(endpoints); - var secureChannelContext = new SecureChannelContext( - sessionName, - endpoint, - RequestEncoding.Binary); + SecureChannelContext secureChannelContext = CreateSecureChannelContext(sessionName, endpoint); var requestHeader = new RequestHeader(); CreateSessionResponse createResponse = await m_server.CreateSessionAsync( @@ -130,10 +127,7 @@ public async Task ClientIsLockedOutAfterFiveFailedAttemptsAsync() EndpointDescriptionCollection endpoints = m_server.GetEndpoints(); EndpointDescription endpoint = FindTcpEndpoint(endpoints); - var secureChannelContext = new SecureChannelContext( - sessionName, - endpoint, - RequestEncoding.Binary); + SecureChannelContext secureChannelContext = CreateSecureChannelContext(sessionName, endpoint); var requestHeader = new RequestHeader(); CreateSessionResponse createResponse = await m_server.CreateSessionAsync( @@ -228,10 +222,7 @@ public async Task SuccessfulAuthenticationClearsFailedAttemptsAsync() EndpointDescriptionCollection endpoints = m_server.GetEndpoints(); EndpointDescription endpoint = FindTcpEndpoint(endpoints); - var secureChannelContext = new SecureChannelContext( - sessionName, - endpoint, - RequestEncoding.Binary); + SecureChannelContext secureChannelContext = CreateSecureChannelContext(sessionName, endpoint); var requestHeader = new RequestHeader(); CreateSessionResponse createResponse = await m_server.CreateSessionAsync( @@ -337,5 +328,16 @@ private static EndpointDescription FindTcpEndpoint(EndpointDescriptionCollection endpoint.SecurityPolicyUri = SecurityPolicies.None; return endpoint; } + + private static SecureChannelContext CreateSecureChannelContext(string sessionName, EndpointDescription endpoint) + { + return new SecureChannelContext( + sessionName, + endpoint, + RequestEncoding.Binary, + clientChannelCertificate: null, + serverChannelCertificate: null, + channelThumbprint: null); + } } } diff --git a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs index 3554974699..0aa119eaaa 100644 --- a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs +++ b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs @@ -124,14 +124,109 @@ public async Task LoadConfigurationAsync(string pkiRoot = null) { // add deprecated policies for opc.tcp tests serverConfig - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.Basic256) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic128Rsa15) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256) .AddSignPolicies() .AddSignAndEncryptPolicies() .AddEccSignPolicies() .AddEccSignAndEncryptPolicies(); + + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.Basic128Rsa15); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.Basic128Rsa15); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.Basic256); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.Basic256); + + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.RSA_DH_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.RSA_DH_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.RSA_DH_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.RSA_DH_ChaChaPoly); + + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_nistP256_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_nistP256_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_nistP256_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_nistP256_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_nistP384_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_nistP384_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_nistP384_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_nistP384_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_brainpoolP256r1_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_brainpoolP256r1_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_brainpoolP384r1_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_brainpoolP384r1_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); } if (OperationLimits) @@ -342,6 +437,17 @@ public async Task StopAsync() await Task.Delay(100).ConfigureAwait(false); } + private static void AddPolicyIfSupported( + IApplicationConfigurationBuilderServerSelected serverConfig, + MessageSecurityMode securityMode, + string securityPolicyUri) + { + if (SecurityPolicies.GetInfo(securityPolicyUri) != null) + { + serverConfig.AddPolicy(securityMode, securityPolicyUri); + } + } + private readonly Func m_factory; private readonly ITelemetryContext m_telemetry; private readonly ILogger m_logger; diff --git a/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs b/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs index 53f777e313..225013cc9a 100644 --- a/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs +++ b/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs @@ -94,7 +94,12 @@ public static class ServerFixtureUtils // set security context var secureChannelContext - = new SecureChannelContext(sessionName, endpoint, RequestEncoding.Binary); + = new SecureChannelContext( + sessionName, + endpoint, RequestEncoding.Binary, + null, + null, + null); var requestHeader = new RequestHeader(); // Create session