使用 .Net Core 3.1 签署 Soap 1.1 主体

Ulf*_*sen 4 wcf ws-security soap cryptography .net-core

我想从带有 C# 的 .Net Core 3.1 连接到需要我根据 WS-Security WS-Policy 2004/09签署Soap 1.1正文的 Web 服务

这是政策要求的文字描述:

AsymmetricBindingAssertion表示使用非对称加密,其中签名必须使用请求者的证书(X509v3)。该InitiatorToken字段指示请求令牌必须是 X509v3 令牌并且它必须包含在所有请求消息中,而该RecipientToken字段指示响应令牌必须是 X509v3 但不会包含在任何消息中。为了识别令牌,将使用 keyIdentifier – 由MustSupportKeyRefIdentitier字段指定 。Timestamp还需要包含在内以规避重放攻击,因此 - 默认情况下 - 这也是签名的。该OnlySignEntireHeadersAndBody字段规定只允许对整个标题或正文进行签名 - 以减轻 XML 签名包装。最后,我们只规定 SOAP Envelope 的 Bodyelement 需要签名。

我添加了一个与Microsoft WCF Web Reference Provider的连接服务在 Visual Studio 2019 中并且所有实体都添加到 Reference.cs 中。我可以在没有 WS-Policy 要求的情况下很好地连接到 SoapUI 中服务的模拟版本。我已经验证了证书和东西,我就是不知道如何在肥皂体上签名。

我无法使用,WSHttpBinding因为它生成 Soap 1.2 并且我尝试使用的服务只理解Soap 1.1

我用 尝试了不同的方法CustomBinding,但似乎它总是烧毁使用AsymmetricSecurityBindingElement.Net Core 中不存在的。

我们在 JavaScript 中有一个实现,可以产生我想要的:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
   xmlns:tns="xx" 
   xmlns:cmn="xxx">
   <soap:Header>
      <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" 
         xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" soap:mustUnderstand="1">
         <wsse:BinarySecurityToken EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" wsu:Id="x509-uidxxx">MIIE...base64=</wsse:BinarySecurityToken>
         <Timestamp xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" Id="_1">
            <Created>2019-09-21T12:33:36Z</Created>
            <Expires>2019-09-21T12:43:36Z</Expires>
         </Timestamp>
         <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
            <SignedInfo>
               <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
               <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
               <Reference URI="#_0">
                  <Transforms>
                     <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                     <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                  </Transforms>
                  <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                  <DigestValue>sc...base64=</DigestValue>
               </Reference>
               <Reference URI="#_1">
                  <Transforms>
                     <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                     <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                  </Transforms>
                  <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                  <DigestValue>5J...base64=</DigestValue>
               </Reference>
            </SignedInfo>
            <SignatureValue>pa...base64=</SignatureValue>
            <KeyInfo>
               <wsse:SecurityTokenReference>
                  <wsse:Reference URI="#x509-uidxxx" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
               </wsse:SecurityTokenReference>
            </KeyInfo>
         </Signature>
      </wsse:Security>
   </soap:Header>
   <soap:Body Id="_0">
      // Lots of stuff
   </soap:Body>
</soap:Envelope>
Run Code Online (Sandbox Code Playgroud)

有谁知道是否可以在 .Net Core 3.1 中使用 C# 使用非对称加密对肥皂体进行签名并生成 Soap 1.1?

小智 7

这是一个迟到的响应,但我有一个类似的要求,即使用 .net core 3.1 调用需要单向 TLS 和 ws-security 的soap 端点。

首先,添加安全标头非常简单。下面是一个 MessageHeader 实现,它添加了带有时间戳的 Security 标头。类 (WsSecurityHeader) 的一个实例用于如下所示的消息检查器。您也可以将此标头烘焙到消息检查器本身中,而不是在消息检查器中使用 WsSecurityHeader,因为无论如何消息检查器都会重写整个soap 消息。

using System;
using System.ServiceModel.Channels;
using System.Xml;

namespace MyClient.WsSecurity
{
    /// <summary>
    /// Adds a WS-Security header to the message, with a Timestamp. The header does not include the message signature,
    /// as the framework provides no mechanism to access the message body inside of a MessageHeader implementation.
    /// </summary>
    public sealed class WsSecurityHeader : MessageHeader
    {
        public override bool MustUnderstand => true;
      
        public override string Name => "Security";
        
        public const string SoapEnvelopeNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
        public const string WsseUtilityNamespaceUrl = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
        public const string WsseNamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";

        public override string Namespace => WsseNamespace;

        protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
        {
            writer.WriteStartElement("wsse", Name, Namespace);
            writer.WriteAttributeString("s", "mustUnderstand", SoapEnvelopeNamespace, "1");

            writer.WriteXmlnsAttribute("wsse", Namespace);
            writer.WriteXmlnsAttribute("wsu", WsseUtilityNamespaceUrl);
        }

        protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
        {
            // Timestamp
            writer.WriteStartElement("wsu", "Timestamp", WsseUtilityNamespaceUrl);

            writer.WriteAttributeString("wsu", "Id", WsseUtilityNamespaceUrl, "ws-security-timestamp");

            writer.WriteStartElement("wsu", "Created", WsseUtilityNamespaceUrl);
            writer.WriteValue(DateTimeOffset.Now.ToString("o"));
            writer.WriteEndElement();

            writer.WriteStartElement("wsu", "Expires", WsseUtilityNamespaceUrl);
            writer.WriteValue(DateTimeOffset.Now.AddMinutes(120).ToString("o"));
            writer.WriteEndElement();

            writer.WriteEndElement(); // Timestamp
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

为了对消息的 Body 元素进行签名,您需要实现一个消息检查器。消息检查器让我们可以访问整个消息,包括正文和标题。我们需要修改两者。下面的消息检查器添加了我们的 Security 标头(WsSecurityHeader 类,如前所示)。我们修改消息的 Body 元素以添加在安全标头中使用的 Id 属性,以标识我们正在签名的元素。然后,我们通过对 Body 元素进行签名来创建签名 xml 元素,并将签名 xml 元素添加到标头中。然后从我们的 XmlDocument 重构整个soap 消息。

using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Xml;
using System.Security.Cryptography.Xml;
using System.IO;

namespace MyClient.WsSecurity
{
    /// <summary>
    /// Adds a ws-security x509 xml body signature to the outgoing message header.  It's annoying that Microsoft contributed to this 
    /// standard but it's not supported in .NET core.
    /// </summary>
    public sealed class WsSecurityMessageInspector : IClientMessageInspector
    {
        public const string BodyIdentifier = "ws-security-body-id"; // This can be whatever xml Id attribute value value we want

        public X509Certificate2 X509Certificate { get; }
     
        public WsSecurityMessageInspector() { }

        public WsSecurityMessageInspector(X509Certificate2 cert)
        {
            X509Certificate = cert;
        }

        public void AfterReceiveReply(ref Message reply, object correlationState) { }

        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            // Add the ws-Security header
            request.Headers.Add(new WsSecurityHeader());
        
            // Get the entire message as an xml doc, so we can sign the body.
            var xml = GetMessageAsString(request);

            XmlDocument doc = new XmlDocument();
            doc.PreserveWhitespace = false;
            doc.LoadXml(xml);
            
            XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
            nsmgr.AddNamespace("soapenv", WsSecurityHeader.SoapEnvelopeNamespace);
            nsmgr.AddNamespace("wsse", WsSecurityHeader.WsseNamespace);

            // The Body is the element we want to sign.
            var body = doc.SelectSingleNode("//soapenv:Body", nsmgr) as XmlElement;

            // Add the Id attribute to the Body, for the Reference element URI..
            var id = doc.CreateAttribute("wsu", "Id", WsSecurityHeader.WsseUtilityNamespaceUrl);
            id.Value = BodyIdentifier;
            body.Attributes.Append(id);

            // Here we do not adopt the SecurityTokenReference recommendation in the KeyInfo
            // section because it is not defined in the XML Signature standard. In lieu of the SecurityTokenReference, we
            // add KeyInfoX509Data directly to the KeyInfo node, in accordance with the XML Signature rfc (rfc3075).  The SignedXml
            // class does not seem to support the SecurityTokenReference, and it's not required.
            var signedXml = new SignedXmlWithUriFix(doc);
            signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA1Url;

            // This cannonicalization method is "recommended" in the ws-security standard, but seems to be required, at least
            // by Data Power. 
            signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;

            // Add the X509 certificate info to the KeyInfo section
            var keyInfo = new KeyInfo();
            var keyInfoData = new KeyInfoX509Data();
            
            keyInfoData.AddIssuerSerial(X509Certificate.IssuerName.Name, X509Certificate.SerialNumber);
            keyInfo.AddClause(keyInfoData);

            signedXml.SigningKey = X509Certificate.PrivateKey;
            signedXml.KeyInfo = keyInfo;

            // Add the reference to the SignedXml object.
            Reference reference = new Reference($"#{BodyIdentifier}");
            reference.DigestMethod = SignedXml.XmlDsigSHA1Url;

            signedXml.AddReference(reference);

            // Compute the signature.
            signedXml.ComputeSignature();
            
            // Get the Signature element
            XmlElement xmlDigitalSignature = signedXml.GetXml();

            // Append the Signature element to the XML document's Security header.
            XmlNode header = doc.SelectSingleNode("//soapenv:Envelope/soapenv:Header/wsse:Security", nsmgr);
            header.AppendChild(doc.ImportNode(xmlDigitalSignature, true));

            // Generate a new message from our XmlDocument.  We have to be careful here so that the XML is serialized 
            // with the same whitespace handling (via XmlWriter) as the signed xml (via XmlDocument). A bit sketchy.
            var newMessage = CreateMessageFromXmlDocument(request, doc);

            request = newMessage;

            return null;
        }

        private Message CreateMessageFromXmlDocument(Message message, XmlDocument doc)
        {
            MemoryStream ms = new MemoryStream();
            using (XmlWriter xmlWriter = XmlWriter.Create(ms, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = false }))
            {
                doc.WriteTo(xmlWriter);
                xmlWriter.Flush();
                xmlWriter.Close();
                ms.Position = 0;
            }
            XmlDictionaryReader xdr = XmlDictionaryReader.CreateTextReader(ms, new XmlDictionaryReaderQuotas());

            var newMessage = Message.CreateMessage(xdr, int.MaxValue, message.Version);

            newMessage.Properties.CopyProperties(message.Properties);

            return newMessage;
        }

        private string GetMessageAsString(Message msg)
        {
            using (var sw = new StringWriter())
            using (var xw = new XmlTextWriter(sw))
            {
                msg.WriteMessage(xw);
                return sw.ToString();
            }
        }

        /// <summary>
        /// The SignedXml class chokes on a URI prefixed with "#", so we override the GetIdElement here.  The #
        /// is allowed by the XML Signature rfc (rfc3075), so this is really a bug fix for SignedXml.
        /// </summary>
        public class SignedXmlWithUriFix : SignedXml
        {
            public SignedXmlWithUriFix(XmlDocument xml) : base(xml)
            {
            }
            
            public SignedXmlWithUriFix(XmlElement xmlElement)
                : base(xmlElement)
            {
            }

            public override XmlElement GetIdElement(XmlDocument doc, string id)
            {
                XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
                nsManager.AddNamespace("wsu", WsSecurityHeader.WsseUtilityNamespaceUrl);

                return doc.SelectSingleNode($"//*[@wsu:Id=\"{id}\"]", nsManager) as XmlElement;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

接下来,创建一个行为并添加消息检查器。

using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace MyClient.WsSecurity
{
    public sealed class WsSecurityHeaderBehavior : IEndpointBehavior
    {
        public X509Certificate2 X509Certificate { get; }
   
        public WsSecurityHeaderBehavior() { }

        public WsSecurityHeaderBehavior(X509Certificate2 cert)
        {
            X509Certificate = cert;
        }

        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            var inspector = new WsSecurityMessageInspector(X509Certificate);
            clientRuntime.ClientMessageInspectors.Add(inspector);
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }

        public void Validate(ServiceEndpoint endpoint) { }
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,将行为添加到您的soap 客户端(有用的提示:重新使用相同的绑定实例和endpointAddress 以允许.net 核心缓存通道工厂——至少我记得它是这样工作的)。不要忘记将您的客户端包装在 using 块中,或者在使用后以其他方式处理它。

var binding = new BasicHttpsBinding();
binding.Security.Mode = BasicHttpsSecurityMode.Transport;

var client= new YourWcfClient(binding, endpointAddress);

// Configure ws-security signing
client.ChannelFactory.Endpoint.EndpointBehaviors.Add(new WsSecurityHeaderBehavior(cert));
Run Code Online (Sandbox Code Playgroud)

此代码已成功用于调用需要带时间戳的单向 TLS 和 ws-security 的 DataPower 端点。可能有更好的方法,但我找不到任何适用于 .net 核心的工作实现。我可能在这里遗漏了一些东西,因为我对 SOAP 和 Ws-Security 的细节都不太熟悉(我只熟悉到足以一起破解它)。祝你好运!