Sign Soap 1.1 body with .Net Core 3.1



我想从带有C#的.Net Core 3.1连接到一个web服务,该web服务要求我根据WS-Security WS-Policy 2004/09签署Soap 1.1主体。

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

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

我在Visual Studio 2019中使用Microsoft WCF Web引用提供程序添加了一个连接的服务,所有实体都添加在Reference.cs中。我可以在SoapUI中连接到该服务的模拟版本,而无需WS-Policy要求。我已经验证了证书和其他东西,我只是不知道如何在soap主体上签名。

我不能使用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主体进行签名并生成soap 1.1吗?

这是一个延迟的响应,但我有类似的要求,使用.net core 3.1调用一个需要单向TLS和ws安全性的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
}
}
}

为了对消息的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;
}
}
}
}

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

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核心缓存通道工厂——至少我记得它是这样工作的(。不要忘记将您的客户端包装在使用块中,或者在使用后以其他方式处理它。

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安全性的DataPower端点,并带有时间戳。可能有更好的方法,但我找不到任何适用于.net核心的实现。我可能错过了一些东西,因为我不太熟悉SOAP的细节,也不太熟悉WsSecurity(我只熟悉到一起破解它(。祝你好运

最新更新