diff --git a/CSharpSoapToolkit/CSharpSoapToolkit/CSharpSoapToolkit.csproj b/CSharpSoapToolkit/CSharpSoapToolkit/CSharpSoapToolkit.csproj index 06b03c3..08aaf97 100644 --- a/CSharpSoapToolkit/CSharpSoapToolkit/CSharpSoapToolkit.csproj +++ b/CSharpSoapToolkit/CSharpSoapToolkit/CSharpSoapToolkit.csproj @@ -1,101 +1,103 @@ - - - - - Debug - AnyCPU - {509FFBAE-003B-44B4-8119-B74F1DEA7FC1} - Exe - CSharpSoapToolkit - CSharpSoapToolkit - v4.7.2 - 512 - true - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\Portable.BouncyCastle.1.9.0\lib\net40\BouncyCastle.Crypto.dll - - - - - - - - - - - - - - - - - - - - True - True - Reference.svcmap - - - - - - - - - - - - - - - Reference.svcmap - - - Reference.svcmap - - - - Designer - - - - - - - - - - - - - - - WCF Proxy Generator - Reference.cs - - - + + + + + Debug + AnyCPU + {509FFBAE-003B-44B4-8119-B74F1DEA7FC1} + Exe + CSharpSoapToolkit + CSharpSoapToolkit + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Portable.BouncyCastle.1.9.0\lib\net40\BouncyCastle.Crypto.dll + + + + + + + + + + + + + + + + + + + + True + True + Reference.svcmap + + + + + + + + + + + + + + + + + Reference.svcmap + + + Reference.svcmap + + + + Designer + + + + + + + + + + + + + + + WCF Proxy Generator + Reference.cs + + + \ No newline at end of file diff --git a/CSharpSoapToolkit/CSharpSoapToolkit/ISecureCertificateStore.cs b/CSharpSoapToolkit/CSharpSoapToolkit/ISecureCertificateStore.cs new file mode 100644 index 0000000..ed18af3 --- /dev/null +++ b/CSharpSoapToolkit/CSharpSoapToolkit/ISecureCertificateStore.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; + +namespace CSharpSoapToolkit +{ + /// + /// Ensure any custom implentations are secure + /// + public interface ISecureCertificateStore + { + X509Certificate2 MerchantCertificate { get; } + } +} diff --git a/CSharpSoapToolkit/CSharpSoapToolkit/InspectorBehavior.cs b/CSharpSoapToolkit/CSharpSoapToolkit/InspectorBehavior.cs index f7943fd..1508986 100644 --- a/CSharpSoapToolkit/CSharpSoapToolkit/InspectorBehavior.cs +++ b/CSharpSoapToolkit/CSharpSoapToolkit/InspectorBehavior.cs @@ -1,68 +1,73 @@ -using System.ServiceModel; -using System.ServiceModel.Channels; -using System.ServiceModel.Description; -using System.ServiceModel.Dispatcher; - -namespace CSharpSoapToolkit -{ - public class InspectorBehavior : IEndpointBehavior - { - public InspectorBehavior() - { - // not calling the base implementation - } - - public void Validate(ServiceEndpoint endpoint) - { - // not calling the base implementation - } - - public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) - { - // not calling the base implementation - } - - public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) - { - // not calling the base implementation - } - - public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) - { - clientRuntime.ClientMessageInspectors.Add(new ClientInspector()); - } - } - - public class ClientInspector : IClientMessageInspector - { - public MessageHeader[] Headers { get; set; } - - public ClientInspector(params MessageHeader[] headers) - { - Headers = headers; - } - - public object BeforeSendRequest(ref Message request, IClientChannel channel) - { - MessageBuffer buffer = request.CreateBufferedCopy(int.MaxValue); - Message copy = buffer.CreateMessage(); - - if (Headers != null) - { - for (int i = Headers.Length - 1; i >= 0; i--) - request.Headers.Insert(0, Headers[i]); - } - - SoapEnvelopeUtility.AddSecurityElements(ref copy); - - request = copy; - - return null; - } - - public void AfterReceiveReply(ref Message reply, object correlationState) - { - // not calling the base implementation - } - } -} +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Description; +using System.ServiceModel.Dispatcher; + +namespace CSharpSoapToolkit +{ + public class InspectorBehavior : IEndpointBehavior + { + private ISecureCertificateStore _secureCertificateStore; + + public InspectorBehavior(ISecureCertificateStore secureCertificateStore) + { + // not calling the base implementation + _secureCertificateStore = secureCertificateStore; + } + + public void Validate(ServiceEndpoint endpoint) + { + // not calling the base implementation + } + + public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) + { + // not calling the base implementation + } + + public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) + { + // not calling the base implementation + } + + public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) + { + clientRuntime.ClientMessageInspectors.Add(new ClientInspector(_secureCertificateStore)); + } + } + + public class ClientInspector : IClientMessageInspector + { + public MessageHeader[] Headers { get; set; } + private ISecureCertificateStore _secureCertificateStore; + + public ClientInspector(ISecureCertificateStore secureCertificateStore, params MessageHeader[] headers) + { + _secureCertificateStore = secureCertificateStore; + Headers = headers; + } + + public object BeforeSendRequest(ref Message request, IClientChannel channel) + { + MessageBuffer buffer = request.CreateBufferedCopy(int.MaxValue); + Message copy = buffer.CreateMessage(); + + if (Headers != null) + { + for (int i = Headers.Length - 1; i >= 0; i--) + request.Headers.Insert(0, Headers[i]); + } + + SoapEnvelopeUtility.AddSecurityElements(ref copy, _secureCertificateStore); + + request = copy; + + return null; + } + + public void AfterReceiveReply(ref Message reply, object correlationState) + { + // not calling the base implementation + } + } +} diff --git a/CSharpSoapToolkit/CSharpSoapToolkit/Sample.cs b/CSharpSoapToolkit/CSharpSoapToolkit/Sample.cs index 8467b2b..6f85f68 100644 --- a/CSharpSoapToolkit/CSharpSoapToolkit/Sample.cs +++ b/CSharpSoapToolkit/CSharpSoapToolkit/Sample.cs @@ -1,96 +1,96 @@ -using CSharpSoapToolkit.com.cybersource.stub; -using System; -using System.ServiceModel; - -namespace CSharpSoapToolkit -{ - public class Sample - { - public static void Main(string[] args) - { - var userDefinedProperties = PropertiesUtility.LoadProperties(); - - RequestMessage request = new RequestMessage(); - - request.merchantID = userDefinedProperties["MERCHANT_ID"]; - - // Before using this example, replace the generic value with your - // reference number for the current transaction. - request.merchantReferenceCode = "your_merchant_reference_code"; - - // To help us troubleshoot any problems that you may encounter, - // please include the following information about your application. - request.clientLibrary = ".NET WCF"; - request.clientLibraryVersion = Environment.Version.ToString(); - request.clientEnvironment = Environment.OSVersion.Platform + Environment.OSVersion.Version.ToString(); - - // This section contains a sample transaction request for the authorization - // service with complete billing, payment card, and purchase (two items) information. - request.ccAuthService = new CCAuthService(); - request.ccAuthService.run = "true"; - - BillTo billTo = new BillTo(); - billTo.firstName = "John"; - billTo.lastName = "Doe"; - billTo.street1 = "1295 Charleston Road"; - billTo.city = "Mountain View"; - billTo.state = "CA"; - billTo.postalCode = "94043"; - billTo.country = "US"; - billTo.email = "null@cybersource.com"; - billTo.ipAddress = "10.7.111.111"; - request.billTo = billTo; - - Card card = new Card(); - card.accountNumber = "4111111111111111"; - card.expirationMonth = "12"; - card.expirationYear = "2035"; - request.card = card; - - PurchaseTotals purchaseTotals = new PurchaseTotals(); - purchaseTotals.currency = "USD"; - request.purchaseTotals = purchaseTotals; - - request.item = new Item[2]; - - Item item = new Item(); - item.id = "0"; - item.unitPrice = "12.34"; - item.quantity = "2"; - request.item[0] = item; - - item = new Item(); - item.id = "1"; - item.unitPrice = "56.78"; - request.item[1] = item; - - try - { - TransactionProcessorClient proc = new TransactionProcessorClient(); - - proc.Endpoint.EndpointBehaviors.Add(new InspectorBehavior()); - - ReplyMessage reply = proc.runTransaction(request); - - // To retrieve individual reply fields, follow these examples. - Console.WriteLine("decision = " + reply.decision); - Console.WriteLine("reasonCode = " + reply.reasonCode); - Console.WriteLine("requestID = " + reply.requestID); - Console.WriteLine("requestToken = " + reply.requestToken); - Console.WriteLine("ccAuthReply.reasonCode = " + reply.ccAuthReply.reasonCode); - } - catch (TimeoutException e) - { - Console.WriteLine("TimeoutException: " + e.Message + "\n" + e.StackTrace); - } - catch (FaultException e) - { - Console.WriteLine("FaultException: " + e.Message + "\n" + e.StackTrace); - } - catch (CommunicationException e) - { - Console.WriteLine("CommunicationException: " + e.Message + "\n" + e.StackTrace); - } - } - } -} +using CSharpSoapToolkit.com.cybersource.stub; +using System; +using System.ServiceModel; + +namespace CSharpSoapToolkit +{ + public class Sample + { + public static void Main(string[] args) + { + var userDefinedProperties = PropertiesUtility.LoadProperties(); + + RequestMessage request = new RequestMessage(); + + request.merchantID = userDefinedProperties["MERCHANT_ID"]; + + // Before using this example, replace the generic value with your + // reference number for the current transaction. + request.merchantReferenceCode = "your_merchant_reference_code"; + + // To help us troubleshoot any problems that you may encounter, + // please include the following information about your application. + request.clientLibrary = ".NET WCF"; + request.clientLibraryVersion = Environment.Version.ToString(); + request.clientEnvironment = Environment.OSVersion.Platform + Environment.OSVersion.Version.ToString(); + + // This section contains a sample transaction request for the authorization + // service with complete billing, payment card, and purchase (two items) information. + request.ccAuthService = new CCAuthService(); + request.ccAuthService.run = "true"; + + BillTo billTo = new BillTo(); + billTo.firstName = "John"; + billTo.lastName = "Doe"; + billTo.street1 = "1295 Charleston Road"; + billTo.city = "Mountain View"; + billTo.state = "CA"; + billTo.postalCode = "94043"; + billTo.country = "US"; + billTo.email = "null@cybersource.com"; + billTo.ipAddress = "10.7.111.111"; + request.billTo = billTo; + + Card card = new Card(); + card.accountNumber = "4111111111111111"; + card.expirationMonth = "12"; + card.expirationYear = "2035"; + request.card = card; + + PurchaseTotals purchaseTotals = new PurchaseTotals(); + purchaseTotals.currency = "USD"; + request.purchaseTotals = purchaseTotals; + + request.item = new Item[2]; + + Item item = new Item(); + item.id = "0"; + item.unitPrice = "12.34"; + item.quantity = "2"; + request.item[0] = item; + + item = new Item(); + item.id = "1"; + item.unitPrice = "56.78"; + request.item[1] = item; + + try + { + TransactionProcessorClient proc = new TransactionProcessorClient(); + + proc.Endpoint.EndpointBehaviors.Add(new InspectorBehavior(new ToolkitCertificateStore())); //or your owm ISecureCertificateStore + + ReplyMessage reply = proc.runTransaction(request); + + // To retrieve individual reply fields, follow these examples. + Console.WriteLine("decision = " + reply.decision); + Console.WriteLine("reasonCode = " + reply.reasonCode); + Console.WriteLine("requestID = " + reply.requestID); + Console.WriteLine("requestToken = " + reply.requestToken); + Console.WriteLine("ccAuthReply.reasonCode = " + reply.ccAuthReply.reasonCode); + } + catch (TimeoutException e) + { + Console.WriteLine("TimeoutException: " + e.Message + "\n" + e.StackTrace); + } + catch (FaultException e) + { + Console.WriteLine("FaultException: " + e.Message + "\n" + e.StackTrace); + } + catch (CommunicationException e) + { + Console.WriteLine("CommunicationException: " + e.Message + "\n" + e.StackTrace); + } + } + } +} diff --git a/CSharpSoapToolkit/CSharpSoapToolkit/SecurityUtility.cs b/CSharpSoapToolkit/CSharpSoapToolkit/SecurityUtility.cs index df5df25..6da246b 100644 --- a/CSharpSoapToolkit/CSharpSoapToolkit/SecurityUtility.cs +++ b/CSharpSoapToolkit/CSharpSoapToolkit/SecurityUtility.cs @@ -1,175 +1,178 @@ -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography.X509Certificates; -using System.Security.Cryptography; -using System.Xml; -using System; -using System.Security.Cryptography.Xml; - -namespace CSharpSoapToolkit -{ - public class SecurityUtility - { - private static readonly IDictionary _userDefinedProperties; - - static SecurityUtility() - { - // Load Properties - try - { - _userDefinedProperties = PropertiesUtility.LoadProperties(); - } - catch (IOException e) - { - throw new Exception(e.Message); - } - } - - public static string GenerateBinarySecurityToken() - { - var certificate = ExtractMerchantCertificateFromFile(); - var certificateBytes = certificate.GetRawCertData(); - return Convert.ToBase64String(certificateBytes); - } - - private static X509Certificate2 ExtractMerchantCertificateFromFile() - { - // (i) Get certificate - return CertificateCacheUtility.FetchCachedCertificate(PropertiesUtility.GetKeyFilePath(), _userDefinedProperties["KEY_PASS"]); - } - - public static void CreateDetachedSignature(ref XmlDocument xmlDoc, RSA privateKey, XmlElement securityTokenReference) - { - // (i) Import XmlDocument into SignedXmlWithId - var signedXml = new SignedXmlWithId(xmlDoc) - { - SigningKey = privateKey - }; - - // (ii) Create Reference - var reference = new Reference - { - Uri = "#Body" - }; - - // (iii) Create Transform - var envTransform = new XmlDsigExcC14NTransform(); - reference.AddTransform(envTransform); - - signedXml.AddReference(reference); - - // (iv) Create Signed Info - var keyInfo = new KeyInfo(); - keyInfo.AddClause(new KeyInfoNode(securityTokenReference)); - - signedXml.KeyInfo = keyInfo; - - signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; - - // (v) Sign Context to create detached signature - signedXml.ComputeSignature(); - - var xmlDigitalSignature = signedXml.GetXml(); - - // (vi) Create Namespace Manager to handle different namespaces - XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable); - namespaceManager.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/"); - namespaceManager.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); - namespaceManager.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); - namespaceManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); - - // (vii) Add prefix to Signature Element - XmlNode signatureElement = xmlDoc.SelectSingleNode("//s:Envelope/s:Header/wsse:Security", namespaceManager); - signatureElement.AppendChild(xmlDoc.ImportNode(xmlDigitalSignature, true)); - - xmlDigitalSignature = signedXml.GetXml(); - SetPrefix("ds", xmlDigitalSignature); - - signedXml.LoadXml(xmlDigitalSignature); - signedXml.SignedInfo.References.Clear(); - - // (viii) Recompute detached signature and add to XML - signedXml.ComputeSignature(); - string recomputedSignature = Convert.ToBase64String(signedXml.SignatureValue); - - ReplaceSignature(xmlDigitalSignature, recomputedSignature); - - signatureElement.RemoveChild(signatureElement.ChildNodes[1]); - signatureElement.AppendChild(xmlDigitalSignature); - } - - // Used to add prefix to all elements inside Signature child elements - private static void ReplaceSignature(XmlElement signature, string newValue) - { - if (signature == null) throw new ArgumentNullException(nameof(signature)); - if (signature.OwnerDocument == null) throw new ArgumentException("No owner document", nameof(signature)); - - XmlNamespaceManager namespaceManager = new XmlNamespaceManager(signature.OwnerDocument.NameTable); - namespaceManager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); - - XmlNode signatureValue = signature.SelectSingleNode("ds:SignatureValue", namespaceManager); - if (signatureValue == null) - throw new Exception("Signature does not contain 'ds:SignatureValue'"); - - signatureValue.InnerXml = newValue; - } - - private static void SetPrefix(string prefix, XmlNode node) - { - if (string.IsNullOrEmpty(node.Prefix)) - { - node.Prefix = prefix; - } - foreach (XmlNode n in node.ChildNodes) - { - SetPrefix(prefix, n); - } - } - - public static RSA GetKeyFromCertificate() - { - var certificate = CertificateCacheUtility.FetchCachedCertificate(PropertiesUtility.GetKeyFilePath(), _userDefinedProperties["KEY_PASS"]); - - // Get the private key - var privateKey = certificate.GetRSAPrivateKey(); - - if (privateKey == null) - { - throw new InvalidOperationException("No RSA private key found in the certificate."); - } - - return privateKey; - } - } - - // This class is needed because the SignedXml class cannot process elements with Id attribute - public class SignedXmlWithId : SignedXml - { - public SignedXmlWithId(XmlDocument xml) : base(xml) - { - } - - public SignedXmlWithId(XmlElement xmlElement) - : base(xmlElement) - { - } - - public override XmlElement GetIdElement(XmlDocument doc, string id) - { - XmlElement elementById = doc.GetElementById(id); - - if (elementById != null) - { - return elementById; - } - - // check to see if it's a standard ID reference - XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable); - nsManager.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); - - XmlElement idElement = doc.SelectSingleNode("//*[@wsu:Id=\"" + id + "\"]", nsManager) as XmlElement; - - return idElement; - } - } +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using System.Xml; +using System; +using System.Security.Cryptography.Xml; + +namespace CSharpSoapToolkit +{ + public class SecurityUtility + { + //private static readonly IDictionary _userDefinedProperties; + + //static SecurityUtility() + //{ + // // Load Properties + // try + // { + // _userDefinedProperties = PropertiesUtility.LoadProperties(); + // } + // catch (IOException e) + // { + // throw new Exception(e.Message); + // } + //} + + public static string GenerateBinarySecurityToken(ISecureCertificateStore secureCertificateStore) + { + //var certificate = ExtractMerchantCertificateFromFile(); + var certificate = secureCertificateStore.MerchantCertificate; + + var certificateBytes = certificate.GetRawCertData(); + return Convert.ToBase64String(certificateBytes); + } + + //private static X509Certificate2 ExtractMerchantCertificateFromFile() + //{ + // // (i) Get certificate + // return CertificateCacheUtility.FetchCachedCertificate(PropertiesUtility.GetKeyFilePath(), _userDefinedProperties["KEY_PASS"]); + //} + + public static void CreateDetachedSignature(ref XmlDocument xmlDoc, RSA privateKey, XmlElement securityTokenReference) + { + // (i) Import XmlDocument into SignedXmlWithId + var signedXml = new SignedXmlWithId(xmlDoc) + { + SigningKey = privateKey + }; + + // (ii) Create Reference + var reference = new Reference + { + Uri = "#Body" + }; + + // (iii) Create Transform + var envTransform = new XmlDsigExcC14NTransform(); + reference.AddTransform(envTransform); + + signedXml.AddReference(reference); + + // (iv) Create Signed Info + var keyInfo = new KeyInfo(); + keyInfo.AddClause(new KeyInfoNode(securityTokenReference)); + + signedXml.KeyInfo = keyInfo; + + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + + // (v) Sign Context to create detached signature + signedXml.ComputeSignature(); + + var xmlDigitalSignature = signedXml.GetXml(); + + // (vi) Create Namespace Manager to handle different namespaces + XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable); + namespaceManager.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/"); + namespaceManager.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); + namespaceManager.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); + namespaceManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); + + // (vii) Add prefix to Signature Element + XmlNode signatureElement = xmlDoc.SelectSingleNode("//s:Envelope/s:Header/wsse:Security", namespaceManager); + signatureElement.AppendChild(xmlDoc.ImportNode(xmlDigitalSignature, true)); + + xmlDigitalSignature = signedXml.GetXml(); + SetPrefix("ds", xmlDigitalSignature); + + signedXml.LoadXml(xmlDigitalSignature); + signedXml.SignedInfo.References.Clear(); + + // (viii) Recompute detached signature and add to XML + signedXml.ComputeSignature(); + string recomputedSignature = Convert.ToBase64String(signedXml.SignatureValue); + + ReplaceSignature(xmlDigitalSignature, recomputedSignature); + + signatureElement.RemoveChild(signatureElement.ChildNodes[1]); + signatureElement.AppendChild(xmlDigitalSignature); + } + + // Used to add prefix to all elements inside Signature child elements + private static void ReplaceSignature(XmlElement signature, string newValue) + { + if (signature == null) throw new ArgumentNullException(nameof(signature)); + if (signature.OwnerDocument == null) throw new ArgumentException("No owner document", nameof(signature)); + + XmlNamespaceManager namespaceManager = new XmlNamespaceManager(signature.OwnerDocument.NameTable); + namespaceManager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + + XmlNode signatureValue = signature.SelectSingleNode("ds:SignatureValue", namespaceManager); + if (signatureValue == null) + throw new Exception("Signature does not contain 'ds:SignatureValue'"); + + signatureValue.InnerXml = newValue; + } + + private static void SetPrefix(string prefix, XmlNode node) + { + if (string.IsNullOrEmpty(node.Prefix)) + { + node.Prefix = prefix; + } + foreach (XmlNode n in node.ChildNodes) + { + SetPrefix(prefix, n); + } + } + + public static RSA GetKeyFromCertificate(ISecureCertificateStore secureCertificateStore) + { + //var certificate = CertificateCacheUtility.FetchCachedCertificate(PropertiesUtility.GetKeyFilePath(), _userDefinedProperties["KEY_PASS"]); + var certificate = secureCertificateStore.MerchantCertificate; + + // Get the private key + var privateKey = certificate.GetRSAPrivateKey(); + + if (privateKey == null) + { + throw new InvalidOperationException("No RSA private key found in the certificate."); + } + + return privateKey; + } + } + + // This class is needed because the SignedXml class cannot process elements with Id attribute + public class SignedXmlWithId : SignedXml + { + public SignedXmlWithId(XmlDocument xml) : base(xml) + { + } + + public SignedXmlWithId(XmlElement xmlElement) + : base(xmlElement) + { + } + + public override XmlElement GetIdElement(XmlDocument doc, string id) + { + XmlElement elementById = doc.GetElementById(id); + + if (elementById != null) + { + return elementById; + } + + // check to see if it's a standard ID reference + XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable); + nsManager.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); + + XmlElement idElement = doc.SelectSingleNode("//*[@wsu:Id=\"" + id + "\"]", nsManager) as XmlElement; + + return idElement; + } + } } \ No newline at end of file diff --git a/CSharpSoapToolkit/CSharpSoapToolkit/SoapEnvelopeUtility.cs b/CSharpSoapToolkit/CSharpSoapToolkit/SoapEnvelopeUtility.cs index 1a766ce..c7fca15 100644 --- a/CSharpSoapToolkit/CSharpSoapToolkit/SoapEnvelopeUtility.cs +++ b/CSharpSoapToolkit/CSharpSoapToolkit/SoapEnvelopeUtility.cs @@ -1,86 +1,86 @@ -using System.IO; -using System.ServiceModel.Channels; -using System.Xml; - -namespace CSharpSoapToolkit -{ - public class SoapEnvelopeUtility - { - public static void AddSecurityElements(ref Message request) - { - // (i) Import Request into XmlDocument - XmlDocument xmlDoc = new XmlDocument { PreserveWhitespace = true }; - using (var tempMemoryStream = new MemoryStream()) - { - using (var writer = XmlWriter.Create(tempMemoryStream)) - { - request.WriteMessage(writer); - writer.Flush(); - tempMemoryStream.Position = 0; - xmlDoc.Load(tempMemoryStream); - } - } - - // (ii) Create Namespace Manager to handle different namespaces - XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable); - namespaceManager.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/"); - namespaceManager.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); - namespaceManager.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); - - // (iii) Retrieve SOAP Body and SOAP Header - XmlNode soapBody = xmlDoc.SelectSingleNode("//s:Envelope/s:Body", namespaceManager); - XmlNode soapHeader = xmlDoc.SelectSingleNode("//s:Envelope/s:Header", namespaceManager); - - if (soapHeader == null) - { - soapHeader = xmlDoc.CreateElement("s", "Header", "http://schemas.xmlsoap.org/soap/envelope/"); - xmlDoc.DocumentElement.InsertBefore(soapHeader, soapBody); - } - - if (soapBody != null) - { - XmlAttribute bodyIdAttr = xmlDoc.CreateAttribute("wsu", "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); - bodyIdAttr.Value = "Body"; - soapBody.Attributes.Append(bodyIdAttr); - } - - // (iv) Add Binary Security Token envelope - XmlElement tokenElement = xmlDoc.CreateElement("wsse", "BinarySecurityToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); - tokenElement.SetAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"); - tokenElement.SetAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"); - - XmlAttribute tokenElementAttribute = xmlDoc.CreateAttribute("wsu", "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); - tokenElementAttribute.Value = "X509Token"; - tokenElement.Attributes.Append(tokenElementAttribute); - tokenElement.InnerXml = SecurityUtility.GenerateBinarySecurityToken(); - - // (v) Add Security envelope - XmlElement securityElement = xmlDoc.CreateElement("wsse", "Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); - securityElement.SetAttribute("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); - securityElement.AppendChild(tokenElement); - soapHeader.AppendChild(securityElement); - - // (vi) Add Security Token Reference element - XmlElement securityTokenReferenceElement = xmlDoc.CreateElement("wsse", "SecurityTokenReference", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); - XmlElement referenceElement = xmlDoc.CreateElement("wsse", "Reference", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); - referenceElement.SetAttribute("URI", "#X509Token"); - securityTokenReferenceElement.AppendChild(referenceElement); - - // (vii) Combine Binary Security Token with Signature - SecurityUtility.CreateDetachedSignature(ref xmlDoc, SecurityUtility.GetKeyFromCertificate(), securityTokenReferenceElement); - - // (viii) Export back to Request object - var memoryStream = new MemoryStream(); // This has to remain open. Do NOT use `using` statement. - xmlDoc.Save(memoryStream); - memoryStream.Position = 0; - - var readerSettings = new XmlReaderSettings - { - CloseInput = true - }; - - var xmlReader = XmlReader.Create(memoryStream, readerSettings); - request = Message.CreateMessage(xmlReader, int.MaxValue, request.Version); - } - } +using System.IO; +using System.ServiceModel.Channels; +using System.Xml; + +namespace CSharpSoapToolkit +{ + public class SoapEnvelopeUtility + { + public static void AddSecurityElements(ref Message request, ISecureCertificateStore secureCertificateStore) + { + // (i) Import Request into XmlDocument + XmlDocument xmlDoc = new XmlDocument { PreserveWhitespace = true }; + using (var tempMemoryStream = new MemoryStream()) + { + using (var writer = XmlWriter.Create(tempMemoryStream)) + { + request.WriteMessage(writer); + writer.Flush(); + tempMemoryStream.Position = 0; + xmlDoc.Load(tempMemoryStream); + } + } + + // (ii) Create Namespace Manager to handle different namespaces + XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable); + namespaceManager.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/"); + namespaceManager.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); + namespaceManager.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); + + // (iii) Retrieve SOAP Body and SOAP Header + XmlNode soapBody = xmlDoc.SelectSingleNode("//s:Envelope/s:Body", namespaceManager); + XmlNode soapHeader = xmlDoc.SelectSingleNode("//s:Envelope/s:Header", namespaceManager); + + if (soapHeader == null) + { + soapHeader = xmlDoc.CreateElement("s", "Header", "http://schemas.xmlsoap.org/soap/envelope/"); + xmlDoc.DocumentElement.InsertBefore(soapHeader, soapBody); + } + + if (soapBody != null) + { + XmlAttribute bodyIdAttr = xmlDoc.CreateAttribute("wsu", "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); + bodyIdAttr.Value = "Body"; + soapBody.Attributes.Append(bodyIdAttr); + } + + // (iv) Add Binary Security Token envelope + XmlElement tokenElement = xmlDoc.CreateElement("wsse", "BinarySecurityToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); + tokenElement.SetAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"); + tokenElement.SetAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"); + + XmlAttribute tokenElementAttribute = xmlDoc.CreateAttribute("wsu", "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); + tokenElementAttribute.Value = "X509Token"; + tokenElement.Attributes.Append(tokenElementAttribute); + tokenElement.InnerXml = SecurityUtility.GenerateBinarySecurityToken(secureCertificateStore); + + // (v) Add Security envelope + XmlElement securityElement = xmlDoc.CreateElement("wsse", "Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); + securityElement.SetAttribute("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); + securityElement.AppendChild(tokenElement); + soapHeader.AppendChild(securityElement); + + // (vi) Add Security Token Reference element + XmlElement securityTokenReferenceElement = xmlDoc.CreateElement("wsse", "SecurityTokenReference", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); + XmlElement referenceElement = xmlDoc.CreateElement("wsse", "Reference", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); + referenceElement.SetAttribute("URI", "#X509Token"); + securityTokenReferenceElement.AppendChild(referenceElement); + + // (vii) Combine Binary Security Token with Signature + SecurityUtility.CreateDetachedSignature(ref xmlDoc, SecurityUtility.GetKeyFromCertificate(secureCertificateStore), securityTokenReferenceElement); + + // (viii) Export back to Request object + var memoryStream = new MemoryStream(); // This has to remain open. Do NOT use `using` statement. + xmlDoc.Save(memoryStream); + memoryStream.Position = 0; + + var readerSettings = new XmlReaderSettings + { + CloseInput = true + }; + + var xmlReader = XmlReader.Create(memoryStream, readerSettings); + request = Message.CreateMessage(xmlReader, int.MaxValue, request.Version); + } + } } \ No newline at end of file diff --git a/CSharpSoapToolkit/CSharpSoapToolkit/ToolkitCertificateStore.cs b/CSharpSoapToolkit/CSharpSoapToolkit/ToolkitCertificateStore.cs new file mode 100644 index 0000000..0e0525e --- /dev/null +++ b/CSharpSoapToolkit/CSharpSoapToolkit/ToolkitCertificateStore.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.NetworkInformation; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; + +namespace CSharpSoapToolkit +{ + /// + /// Makes use of PropertiesUtility, CertificateCacheUtility and the Portable.BouncyCastle dependancy + /// if you choose not to use ToolkitCertificateStore, then the above are optional + /// + public class ToolkitCertificateStore : ISecureCertificateStore + { + private static readonly IDictionary _userDefinedProperties; + + static ToolkitCertificateStore() + { + // Load Properties + try + { + _userDefinedProperties = PropertiesUtility.LoadProperties(); + } + catch (IOException e) + { + throw new Exception(e.Message); + } + } + + public X509Certificate2 MerchantCertificate { + get + { + return ExtractMerchantCertificateFromFile(); + } + } + + private static X509Certificate2 ExtractMerchantCertificateFromFile() + { + // (i) Get certificate + return CertificateCacheUtility.FetchCachedCertificate(PropertiesUtility.GetKeyFilePath(), _userDefinedProperties["KEY_PASS"]); + } + } +} diff --git a/CSharpSoapToolkit/README.md b/CSharpSoapToolkit/README.md index 5723e41..ce336d4 100644 --- a/CSharpSoapToolkit/README.md +++ b/CSharpSoapToolkit/README.md @@ -31,6 +31,9 @@ You must create a P12 certificate. See the [REST Getting Started Developer Guide With this change to use a P12 certificate in your C# SOAP toolkit configuration, the new requirements for your application will be: - .NET Framework 4.7.2 and later Redistributable Package + +If using the ToolkitCertificateStore utility then these are the additional requirements for your application: + - [NuGet Command-Line Interface](https://learn.microsoft.com/en-us/nuget/reference/nuget-exe-cli-reference?tabs=windows) - Portable.BouncyCastle @@ -52,7 +55,7 @@ Follow these steps to upgrade your existing C# code: ### Modifying `app.config` -1. Add the following sections to the top of your `app.config` file: +1. Add the following sections to the top of your `app.config` file (If using the ToolkitCertificateStore utility): ```xml @@ -86,7 +89,7 @@ Follow these steps to upgrade your existing C# code: ``` -### Adding new dependency +### Adding new dependency (If using the ToolkitCertificateStore utility) 1. Add this dependency to the `packages.config` file: @@ -112,16 +115,23 @@ Follow these steps to upgrade your existing C# code: ### Adding new files to project -1. Add your P12 certificate to the `KEY_DIRECTORY`. +1. Add your P12 certificate to the `KEY_DIRECTORY`(If using the ToolkitCertificateStore utility). This `KEY_DIRECTORY` location must be accessible by your code. Ensure that your code has permissions to read this location. 2. Copy these files to your project directory: - - [CertificateCacheUtility.cs](CSharpSoapToolkit\CertificateCacheUtility.cs) - [InspectorBehavior.cs](CSharpSoapToolkit\InspectorBehavior.cs) - [PropertiesUtility.cs](CSharpSoapToolkit\PropertiesUtility.cs) - [SecurityUtility.cs](CSharpSoapToolkit\SecurityUtility.cs) - [SoapEnvelopeUtility.cs](CSharpSoapToolkit\SoapEnvelopeUtility.cs) + - [ISecureCertificateStore.cs](CSharpSoapToolkit\ISecureCertificateStore.cs) + +If using the ToolkitCertificateStore utility then copy these addional files: + - [ToolkitCertificateStore.cs](CSharpSoapToolkit\ToolkitCertificateStore.cs) + - [CertificateCacheUtility.cs](CSharpSoapToolkit\CertificateCacheUtility.cs) + - [PropertiesUtility.cs](CSharpSoapToolkit\PropertiesUtility.cs) + +If you are not using ToolkitCertificateStore then implement your own secure ISecureCertificateStore 3. Import the files above to your project. @@ -143,7 +153,7 @@ Follow these steps to upgrade your existing C# code: ```csharp TransactionProcessorClient proc = new TransactionProcessorClient(); - proc.Endpoint.EndpointBehaviors.Add(new InspectorBehavior()); + proc.Endpoint.EndpointBehaviors.Add(new InspectorBehavior(new ToolkitCertificateStore())); //or your owm ISecureCertificateStore ReplyMessage reply = proc.runTransaction(request); ```