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

Sign Soap 1.1 body with .Net Core 3.1

我想使用 C# 从 .Net Core 3.1 连接到需要我根据 WS-Security WS-Policy 2004/09 签署 Soap 1.1 主体的 Web 服务。

这是政策要求的文字说明:

AsymmetricBindingAssertion indicates to use asymmetric encryption, where the requestor’s certificate (X509v3) must be used for the signature. The InitiatorToken field indicates that the request token must be an X509v3 token and that it must be included with all request messages, while the RecipientToken field indicates that response token has to be X509v3 but will not be included in any message. To identify the token, a keyIdentifier will be used – specified by MustSupportKeyRefIdentitier field. Timestamp is also needed for inclusion to circumvent replay attacks and as such - by default - this is also signed. The OnlySignEntireHeadersAndBody field dictates that only the entire header or body is allowed to sign – to mitigate XML Signature wrapping. And lastly, we only dictate that the Bodyelement of the SOAP Envelope needs to be signed.

我在 Visual Studio 2019 年添加了与 Microsoft WCF Web 参考提供程序 的连接服务,并且所有实体都添加在 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>

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

这是一个迟到的回复,但我有类似的要求,使用 .net core 3.1 调用需要 one-way TLS 和 ws-security 的 soap 端点。

首先,添加安全性 header 非常简单。下面是一个 MessageHeader 实现,它添加了带有时间戳的 Security header。 class (WsSecurityHeader) 的一个实例在下面显示的消息检查器中使用。您还可以将此 header 放入消息检查器本身,而不是在消息检查器中使用 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
        }
    }
}

为了签署消息的 Body 元素,您需要实施消息检查器。消息检查器让我们可以访问整个消息,包括 body 和 header。我们需要修改两者。下面的消息检查器添加了我们的 Security header(WsSecurityHeader class,如前所示)。我们修改消息的 Body 元素以添加安全性 header 中使用的 Id 属性,以标识我们正在签署的元素。然后,我们通过对 Body 元素进行签名来创建一个签名 xml 元素,并将签名 xml 元素添加到 header。然后从我们的 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;
            }
        }
    }
}

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

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) { }
    }
}

最后,将行为添加到您的 soap 客户端(有用的提示:re-use 相同的绑定实例和 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));

此代码已成功用于调用需要 one-way TLS 和 ws-security 的 DataPower 端点,并带有时间戳。可能有更好的方法,但我找不到任何适用于 .net core 的有效实现。我可能在这里遗漏了一些东西,因为我对 SOAP 的细节和 Ws-Security 都不是很熟悉(我只是熟悉到足以将其组合在一起)。祝你好运!