FixAntenna/NetCore/FixEngine/Session/Impl/CmeSecureLogonStrategy.cs (236 lines of code) (raw):

// Copyright (c) 2021 EPAM Systems // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; using Epam.FixAntenna.Constants.Fixt11; using Epam.FixAntenna.NetCore.Common; using Epam.FixAntenna.NetCore.Common.ResourceLoading; using Epam.FixAntenna.NetCore.Configuration; using Epam.FixAntenna.NetCore.Helpers; using Epam.FixAntenna.NetCore.Message; using NLog; using Version = Epam.FixAntenna.NetCore.Common.Utils.Version; namespace Epam.FixAntenna.NetCore.FixEngine.Session.Impl { /// <summary> /// CME secure logon provides an ability to logon using SHA256 digital signature technique. /// This logon feature has been introduced by CME and provides highest security. /// </summary> internal class CmeSecureLogonStrategy : ILogonCustomizationStrategy { private static readonly ILogger Logger = LogManager.GetLogger(typeof(CmeSecureLogonStrategy).FullName); public const string SessionIdParamName = "Session ID"; public const string AccessIdParamName = "Access ID"; public const string SecretKeyParamName = "Secret Key"; public const string CreationDateParamName = "Creation Date"; public const string ExpirationDateParamName = "Expiration Date"; public const string EnvironmentParamName = "Environment"; public const string CanonicalStringDelimiter = "\n"; public const int AppSystemNameTag = 1603; public const int TradingSystemVersionTag = 1604; public const int AppSystemVendorTag = 1605; public const int EncodedTextLenTag = 354; public const int EncodedTextTag = 355; public const int EncryptedPasswordMethodTag = 1400; public const string EncryptedPasswordMethodDefaultValue = "CME-1-SHA-256"; public const int EncryptedPasswordLenTag = 1401; public const int EncryptedPasswordTag = 1402; public static readonly string DateFormat = "yyyy-MM-dd"; public static readonly int[] RequiredTagsForCanonicalString = new int[] { Tags.MsgSeqNum, Tags.SenderCompID, Tags.SenderSubID, Tags.SendingTime, Tags.TargetSubID, Tags.HeartBtInt, Tags.SenderLocationID, Tags.LastMsgSeqNumProcessed, AppSystemNameTag, TradingSystemVersionTag, AppSystemVendorTag }; private IDictionary<string, IDictionary<string, string>> _keysMap; public void SetSessionParameters(SessionParameters sessionParameters) { var configuration = sessionParameters.Configuration; var secKeyFile = configuration.GetProperty(Config.CmeSecureKeysFile); if (!string.IsNullOrEmpty(secKeyFile)) { try { _keysMap = ParseKeys(secKeyFile); } catch (IOException e) { Logger.Error(e, "Error '{0}' while parsing '{1}'", e.Message, secKeyFile); } } else { Logger.Error("{0} is not defined, CME secure logon strategy will be disabled", Config.CmeSecureKeysFile); } } public void CompleteLogon(FixMessage logonMessage) { var senderCompId = logonMessage.GetTagValueAsString(Tags.SenderCompID); if (!string.IsNullOrEmpty(senderCompId) && senderCompId.Length >= 3) { var sessionId = senderCompId.Substring(0, 3); if (_keysMap.ContainsKey(sessionId)) { var sessionParameters = _keysMap[sessionId]; var expirationDateStr = sessionParameters[ExpirationDateParamName]; if (IsDateExpired(expirationDateStr)) { Logger.Error("Expiration date '{0}' is before now '{1}'. CME secure logon strategy will not be applied.", expirationDateStr, DateTime.Now.ToString(DateFormat)); return; } try { var additionalTags = new FixMessage(); if (!logonMessage.IsTagExists(AppSystemNameTag)) { logonMessage.AddTag(AppSystemNameTag, "FIXAntenna .NET Core"); logonMessage.AddTag(TradingSystemVersionTag, Version.GetProductVersion(typeof(FixVersion), 10)); logonMessage.AddTag(AppSystemVendorTag, "B2BITS"); } if (logonMessage.IsTagExists(Tags.EncryptMethod)) { //cert tool does not accept logon with 98 tag logonMessage.RemoveTag(Tags.EncryptMethod); } if (logonMessage.IsTagExists(Tags.RawDataLength)) { logonMessage.RemoveTag(Tags.RawDataLength); } if (logonMessage.IsTagExists(Tags.RawData)) { logonMessage.RemoveTag(Tags.RawData); } var canonicalRequest = CreateCanonicalRequest(logonMessage); var secretKey = sessionParameters[SecretKeyParamName]; var hmac = CalculateHmac(canonicalRequest, secretKey); var accessIdKey = sessionParameters[AccessIdParamName]; additionalTags.AddTag(EncodedTextLenTag, accessIdKey.Length); additionalTags.AddTag(EncodedTextTag, accessIdKey); additionalTags.AddTag(EncryptedPasswordMethodTag, EncryptedPasswordMethodDefaultValue); additionalTags.AddTag(EncryptedPasswordLenTag, hmac.Length); additionalTags.AddTag(EncryptedPasswordTag, hmac); logonMessage.AddAll(additionalTags); } catch (Exception e) { Logger.Error(e, "Error '{0}' has occurred. CME secure logon strategy will not be applied", e.Message); } } else { Logger.Error("There is no key for session id '{0}' in secKey file.", sessionId); } } else { Logger.Error("SenderCompID '{0}' is less than 3 symbols. CME secure logon strategy will not be applied.", senderCompId); } } public virtual string CreateCanonicalRequest(FixMessage logonMessage) { var canonicalRequest = new StringBuilder(); for (var i = 0; i < RequiredTagsForCanonicalString.Length; i++) { if (i > 0) { canonicalRequest.Append(CanonicalStringDelimiter); } canonicalRequest.Append(logonMessage.GetTagValueAsString(RequiredTagsForCanonicalString[i])); } return canonicalRequest.ToString(); } public virtual bool IsDateExpired(string expirationDateStr) { var expirationDate = DateTime.ParseExact(expirationDateStr, DateFormat, null); return DateTime.Now > expirationDate; } public virtual string CalculateHmac(string canonicalRequest, string userKey) { string hash = null; try { using (var hmac = new HMACSHA256()) { // Initialize HMAC instance with the key // Decode the key first, since it is base64url encoded var key = Convert.FromBase64String(userKey.UrlDecode()); hmac.Key = key; hmac.Initialize(); var txt = canonicalRequest.AsByteArray(); var h = hmac.ComputeHash(txt); // Calculate HMAC, base64url encode the result and strip padding hash = Convert.ToBase64String(h).UrlEncode(); } } catch (Exception e) { Logger.Error(e, e.Message); } return hash; } internal static IDictionary<string, IDictionary<string, string>> ParseKeys(string fileName) { IDictionary<string, IDictionary<string, string>> result = new Dictionary<string, IDictionary<string, string>>(); using (var br = new StreamReader(ResourceLoader.DefaultLoader.LoadResource(fileName))) { if (br.Peek() >= 0) { var headerLine = br.ReadLine(); var sessionIdIdx = headerLine.IndexOf(SessionIdParamName, StringComparison.Ordinal); var accessIdIdx = headerLine.IndexOf(AccessIdParamName, StringComparison.Ordinal); var secretKeyIdx = headerLine.IndexOf(SecretKeyParamName, StringComparison.Ordinal); var creationDateIdx = headerLine.IndexOf(CreationDateParamName, StringComparison.Ordinal); var expirationDateIdx = headerLine.IndexOf(ExpirationDateParamName, StringComparison.Ordinal); var environmentIdx = headerLine.IndexOf(EnvironmentParamName, StringComparison.Ordinal); while (br.Peek() >= 0) { var nextLine = br.ReadLine(); if (!string.IsNullOrEmpty(nextLine)) { var sessionId = GetValueAtIndex(nextLine, sessionIdIdx); IDictionary<string, string> parametersMap = new Dictionary<string, string>(); parametersMap[AccessIdParamName] = GetValueAtIndex(nextLine, accessIdIdx); parametersMap[SecretKeyParamName] = GetValueAtIndex(nextLine, secretKeyIdx); parametersMap[CreationDateParamName] = GetValueAtIndex(nextLine, creationDateIdx); parametersMap[ExpirationDateParamName] = GetValueAtIndex(nextLine, expirationDateIdx); if (environmentIdx > 0) { parametersMap[EnvironmentParamName] = GetValueAtIndex(nextLine, environmentIdx); } if (result.ContainsKey(sessionId)) { var existentKey = result[sessionId]; var existentKeyExpirationDate = DateTime.ParseExact(existentKey[ExpirationDateParamName], DateFormat, null); var newKeyExpirationDate = DateTime.ParseExact(parametersMap[ExpirationDateParamName], DateFormat, null); if (newKeyExpirationDate > existentKeyExpirationDate) { result[sessionId] = parametersMap; } } else { result[sessionId] = parametersMap; } } } } else { throw new IOException("File '" + fileName + "' is empty"); } return result; } } private static string GetValueAtIndex(string line, int startIndex) { var endIndex = line.Substring(startIndex).IndexOf(' '); if (endIndex == -1) { //if there is no space at the end of the line endIndex = line.Substring(startIndex).Length; } return line.Substring(startIndex, endIndex); } } }