【问题标题】:Sign Soap 1.1 body with .Net Core 3.1使用 .Net Core 3.1 签署 Soap 1.1 主体
【发布时间】:2020-03-13 18:51:39
【问题描述】:

我想使用 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 签名包装。最后,我们只规定 Bodyelement SOAP Envelope 需要签名。

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

我不能使用WSHttpBinding,因为它会生成 Soap 1.2,而我尝试使用的服务只能理解 Soap 1.1

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

我们有一个 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 1.1?

【问题讨论】:

    标签: wcf soap .net-core cryptography ws-security


    【解决方案1】:

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

    首先,添加安全标头非常简单。下面是一个 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
            }
        }
    }
    

    为了对消息的 Body 元素进行签名,您需要实现消息检查器。消息检查器让我们可以访问整个消息,包括正文和标题。我们需要修改两者。下面的消息检查器添加了我们的安全标头(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;
                }
            }
        }
    }
    

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

    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 客户端(有用的提示:重新使用相同的绑定实例和 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));
    

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

    【讨论】:

    • 谢谢!是的,这只是您现在可以与 .NET 5 一起使用的一种方法。我们将 .NET 4.7.2 客户端迁移到 .NET 5 后遇到库中缺少AsymmetricSecurityBindingElement 的问题,那么只有这个解决方案才能解决这个问题。
    • 我对这个解决方案有一个小问题。据我所知,在 WS-Security 中,客户端的私钥用于签名,但也有服务器的公钥应该用于加密,我在这里看不到公钥。握手期间是否自动解析公钥?还是只是从答案中省略了?还是我的理解有误?
    • 公钥用于传输层,自动处理证书验证和加密。
    • 所以如果将传输配置为:httpsBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; 并且在配置通道channelFactory.Credentials.ServiceCertificate.DefaultCertificate = // cert with service public key 时应该全部自动完成,对吗?
    • 您似乎正在尝试实现基于证书的客户端身份验证(在 SSL/TLS 传输层中)。就我而言,不需要 SSL/TLS 层的客户端证书身份验证。我的示例中的客户端证书用于对 SOAP 消息体进行签名,这与使用客户端证书在 SSL/TLS 层中对客户端进行身份验证不同。在任何一种情况下,使用服务器公钥的 TLS 加密都由传输层自动处理(在 TLS 握手期间会自动检索服务器的公钥)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-17
    • 1970-01-01
    • 1970-01-01
    • 2010-12-06
    • 1970-01-01
    • 2020-12-04
    • 1970-01-01
    相关资源
    最近更新 更多