9

我需要向将发送带有签名时间戳的肥皂消息的第三方提供服务。

如何配置我的服务以支持此功能?

更新 我已经设法接近我们所追求的 Soap 消息的格式,但 WCF 坚持对用户名和时间戳令牌进行签名,有没有办法修改绑定以仅签名时间戳?


进一步更新 以下是我们的要求:

  • Timestamp 元素必须签名。
  • 用于签名的证书上的 CN 名称必须与 UsernameToken 元素中给出的用户名匹配。
  • 用于签名的证书必须在 BinarySecurityToken 元素中发送。
  • KeyInfo 元素必须只包含一个 SecurityTokenReference 元素,它必须用于引用 BinarySecurityToken。
  • 必须指定规范化算法。
  • 必须指定 SignatureMethod 并且必须是 SHA-1 或 SHA-2 算法。
  • 应该使用分离的签名。

有什么建议么?

当前配置

客户端绑定

<bindings>
  <wsHttpBinding>
    <binding name="WSBC">
      <security mode="TransportWithMessageCredential">
        <transport clientCredentialType="Certificate" proxyCredentialType="None"></transport>
        <message clientCredentialType="UserName" negotiateServiceCredential="false" establishSecurityContext="false" />
      </security>
    </binding>
  </wsHttpBinding>
</bindings>

客户端端点

<client>
  <endpoint address="https://localhost/WcfTestService/Service2.svc"
  behaviorConfiguration="CCB" binding="wsHttpBinding"
  bindingConfiguration="WSBC"
  contract="ServiceReference2.IService2"
  name="wsHttpBinding_IService2" />
</client>

客户行为

<behaviors>
  <endpointBehaviors>
    <behavior name="MBB">
      <clientCredentials>
        <clientCertificate  findValue="03 58 d3 bf 4b e7 67 2e 57 05 47 dc e6 3b 52 7f f8 66 d5 2a"
                            storeLocation="LocalMachine"
                            storeName="My"
                            x509FindType="FindByThumbprint" />
        <serviceCertificate>
          <defaultCertificate findValue="03 58 d3 bf 4b e7 67 2e 57 05 47 dc e6 3b 52 7f f8 66 d5 2a"
                              storeLocation="LocalMachine"
                              storeName="My"
                              x509FindType="FindByThumbprint"  />
        </serviceCertificate>
      </clientCredentials>
    </behavior>
  </endpointBehaviors>
</behaviors>

服务绑定

<bindings>
  <wsHttpBinding>
    <binding name="ICB">
      <security mode="TransportWithMessageCredential">
        <transport clientCredentialType="Certificate" proxyCredentialType="None"></transport>
        <message    clientCredentialType="UserName" 
                    negotiateServiceCredential="false"
                    establishSecurityContext="false" />
      </security>
    </binding>
  </wsHttpBinding>
</bindings>

服务端点

<service name="WcfTestService.Service2" behaviorConfiguration="SCB">
    <endpoint     address="" binding="wsHttpBinding" contract="WcfTestService.IService2"
    bindingConfiguration="ICB" name="MS" />
</service>

服务行为

<behaviors>
  <serviceBehaviors>
    <behavior name="SCB">
      <serviceCredentials>
        <serviceCertificate     findValue="4d a9 d8 f2 fb 4e 74 bd a7 36 d7 20 a8 51 e2 e6 ea 7d 30 08"
                                storeLocation="LocalMachine"
                                storeName="TrustedPeople"   
                                x509FindType="FindByThumbprint" />
        <userNameAuthentication 
            userNamePasswordValidationMode="Custom" 
            customUserNamePasswordValidatorType="WcfTestService.UsernameValidator, WcfTestService" />
        <clientCertificate>
          <authentication certificateValidationMode="None" revocationMode="NoCheck" />
        </clientCertificate>
      </serviceCredentials>
      <serviceMetadata httpGetEnabled="true" />
      <serviceDebug includeExceptionDetailInFaults="false" />
    </behavior>
  </serviceBehaviors>
</behaviors>
4

4 回答 4

5

SO上有很多这样的问题,但没有一个有明确的答案,所以在花了很多时间在这个问题上之后,我将回答这个8年前的问题,希望它能对某人有所帮助。

我必须将带有密码摘要和签名时间戳(仅签名时间戳)的 SOAP 消息发送到黑盒服务器,我认为它是 Axis2。我使用不同的安全配置和 SignedXml 类的派生变体四处游荡,并成功地让我的消息看起来有些正确,但从未能够产生有效的签名。根据微软的说法,WCF 不像非 WCF 服务器那样规范化,WCF 遗漏了一些命名空间并以不同的方式重命名命名空间前缀,因此我永远无法让我的签名匹配。

因此,经过大量试验和错误,这是我的 DIY 方法:

  1. 定义一个负责创建整个安全标头的自定义 MessageHeader。
  2. 定义一个自定义 MessageInspector 来重命名命名空间,添加缺少的命名空间,并将我的自定义安全标头添加到请求标头

这是我需要生成的请求的示例:

<soapenv:Envelope xmlns:ns1="http://somewebsite.com/" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="https://anotherwebsite.com/xsd">
<soapenv: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">
        <wsse:UsernameToken wsu:Id="UsernameToken-1">
            <wsse:Username>username</wsse:Username>
            <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">aABCDiUsrOy8ScJkdABCD/ZABCD=</wsse:Password>
            <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">ABCDxZ8IABCDg/pTK6E0Q==</wsse:Nonce>
            <wsu:Created>2019-03-07T21:31:00.281Z</wsu:Created>
        </wsse:UsernameToken>
        <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-1">...</wsse:BinarySecurityToken>
        <wsu:Timestamp wsu:Id="TS-1">
            <wsu:Created>2019-03-07T21:31:00Z</wsu:Created>
            <wsu:Expires>2019-03-07T21:31:05Z</wsu:Expires>
        </wsu:Timestamp>
        <ds:Signature Id="SIG-1" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:SignedInfo>
                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                    <ec:InclusiveNamespaces PrefixList="ns1 soapenv xsd" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                </ds:CanonicalizationMethod>
                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
                <ds:Reference URI="#TS-1">
                    <ds:Transforms>
                        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                            <ec:InclusiveNamespaces PrefixList="wsse ns1 soapenv xsd" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                        </ds:Transform>
                    </ds:Transforms>
                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                    <ds:DigestValue>ABCDmhUOmjhBRPabcdB1wni53mabcdOzRMo3ABCDVbw=</ds:DigestValue>
                </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>...</ds:SignatureValue>
            <ds:KeyInfo Id="KI-1">
                <wsse:SecurityTokenReference wsu:Id="STR-1">
                    <wsse:Reference URI="#X509-1" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
                </wsse:SecurityTokenReference>
            </ds:KeyInfo>
        </ds:Signature>
    </wsse:Security>
</soapenv:Header>
<soapenv:Body>
    ...
</soapenv:Body>

所以这就是 XML 所说的:

  1. 需要创建带有随机数的密码摘要。
  2. 需要包含 BinarySecurityToken 的 Base64 表示。
  3. Timestamp 需要通过 xml-exc-c14n 规范进行规范化(只是将该部分拉出并重新格式化),确保在标题中包含命名空间 wsse、ns1、soapenv 和 xsd。
  4. 该时间戳部分需要经过 SHA256 哈希处理并添加到 SignedInfo 部分的 DigestValue 字段中。
  5. 需要对带有新 DigestValue 的 SignedInfo 部分进行规范化,确保包括命名空间 ns1、soapenv 和 xsd。
  6. 签名信息需要经过 SHA256 散列,然后 RSA 加密,并将结果添加到 SignatureValue 字段。

自定义消息头

通过注入自定义消息头,我可以在请求头中写出我想要的任何 xml。这篇文章为我指明了正确的方向https://stackoverflow.com/a/39090724/6077517

这是我使用的标题:

class CustomSecurityHeader : MessageHeader
{
    // This is data I'm passing into my header from the MessageInspector 
    // that will be used to create the security header contents
    public HeaderData HeaderData { get; set; }

    // Name of the header
    public override string Name
    {
        get { return "Security"; }
    }

    // Header namespace
    public override string Namespace
    {
        get { return "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; }
    }

    // Additional namespace I needed
    public string wsuNamespace
    {
        get { return "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"; }
    }

    // This is where the start tag of the header gets written
    // add any required namespaces here
    protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
    {
        writer.WriteStartElement("wsse", Name, Namespace);
        writer.WriteXmlnsAttribute("wsse", Namespace);
        writer.WriteXmlnsAttribute("wsu", wsuNamespace);
    }

    // This is where the header content will be written into the request
    protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
    {
        XmlDocument xmlDoc = MyCreateSecurityHeaderFunction(HeaderData); // My function that creates the security header contents.
        var securityElement = doc.FirstChild; // This is the "<security.." portion of the xml returned
        foreach(XmlNode node in securityElement.ChildNodes)
        {
            writer.WriteNode(node.CreateNavigator(), false);
        }
        return;
    }
}

消息检查器

要将标头放入请求中,我会覆盖 MessageInspector 类。这几乎可以让您在插入标头和传输消息之前更改任何您想要的请求。

这里有一篇关于它的好文章,它使用此方案将用户名密码 Nonce 添加到消息中:https ://weblog.west-wind.com/posts/2012/nov/24/wcf-wssecurity-and-wse-nonce -验证

您必须创建一个自定义 EndpointBehavior 来注入检查器。

public class CustomInspectorBehavior : IEndpointBehavior
{
    // Data I'm passing to my EndpointBehavior that will be used to create the security header
    public HeaderData HeaderData
    {
        get { return this.messageInspector.HeaderData; }
        set { this.messageInspector.HeaderData = value; }
    }

    // My custom MessageInspector class
    private MessageInspector messageInspector = new MessageInspector();

    public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
    }

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

    public void Validate(ServiceEndpoint endpoint)
    {
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        // Add the custom message inspector here
        clientRuntime.MessageInspectors.Add(messageInspector);
    }
}

这是我的消息检查器的代码:

public class MessageInspector : IClientMessageInspector
{
    // Data to be used to create the security header
    public HeaderData HeaderData { get; set; }

    public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
    {
        var lastResponseXML = reply.ToString(); // Not necessary but useful for debugging if you want to see the response.
    }

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
    {
        // This might not be necessary for your case but I remove a bunch of unnecessary WCF-created headers from the request.
        List<string> removeHeaders = new List<string>() { "Action", "VsDebuggerCausalityData", "ActivityId" };
        for (int h = request.Headers.Count() - 1; h >= 0; h--)
        {
            if (removeHeaders.Contains(request.Headers[h].Name))
            {
                request.Headers.RemoveAt(h);
            }
        }

        // Make changes to the request.
        // For this case I'm adding/renaming namespaces in the header.
        var container = XElement.Parse(request.ToString()); // Parse request into XElement
        // Change "s" namespace to "soapenv"
        container.Add(new XAttribute(XNamespace.Xmlns + "soapenv", "http://schemas.xmlsoap.org/soap/envelope/"));
        container.Attributes().Where(a => a.Name.LocalName == "s").Remove();
        // Add other missing namespace
        container.Add(new XAttribute(XNamespace.Xmlns + "ns1", "http://somewebsite.com/"));
        container.Add(new XAttribute(XNamespace.Xmlns + "xsd", "http://anotherwebsite.com/xsd"));
        requestXml = container.ToString();

        // Create a new message out of the updated request.
        var ms = new MemoryStream();
        var sr = new StreamWriter(ms);
        var writer = new StreamWriter(ms);
        writer.Write(requestXml);
        writer.Flush();
        ms.Position = 0;

        var reader = XmlReader.Create(ms);
        request = Message.CreateMessage(reader, int.MaxValue, request.Version);

        // Add my custom security header
        // This is responsible for writing the security headers to the message
        CustomSecurityHeader header = new CustomSecurityHeader();
        // Pass data required to build security header
        header.HeaderData = new HeaderData()
        {
            Certificate = this.HeaderData.Certificate,
            Username = this.HeaderData.Username,
            Password = this.HeaderData.Password
            // ... Whatever else might be needed
        };

        // Add custom header to request headers
        request.Headers.Add(header);

        return request;
    }
}

将消息检查器添加到客户端代理

我的绑定非常简单,因为我自己添加了所有安全内容并且不希望添加任何意外的标头。

// IMPORTANT - my service required TLS 1.2, add this to make that happen
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;

// Encoding
var encoding = new TextMessageEncodingBindingElement();
encoding.MessageVersion = MessageVersion.Soap11;

// Transport
var transport = new HttpsTransportBindingElement();

CustomBinding binding = new CustomBinding();
binding.Elements.Add(encoding);
binding.Elements.Add(transport);

var myProxy = new MyProxyClass(binding, new EndpointAddress(endpoint));

// Add message inspector behavior to alter security header.
// data contains info to create the header such as username, password, certificate, etc.
MessageInspector = new CustomInspectorBehavior() { HeaderData = data }; 
myProxy.ChannelFactory.Endpoint.EndpointBehaviors.Add(MessageInspector);

创建安全标头 XML

这有点难看,但我最终做的是创建安全标头的规范化部分的 XML 模板,填写值,散列并适当地签署 SignedInfo 部分,然后将这些部分组合成一个完整的安全标头。我更愿意在代码中构建它们,但 XmlDocument 不会维护我添加的属性的顺序,这会弄乱我的规范化 XML 和我的签名,所以我保持简单。

为了确保我的部分被正确规范化,我使用了一个名为 SC14N https://www.cryptosys.net/sc14n/index.html的工具。我输入了一个示例 XML 请求和对我想要规范化的部分的引用以及任何包含的命名空间,它返回了适当的 XML。我将它返回的 XML 保存到模板中,用我以后可以替换的标签替换值和 ID。我为 Timestamp 部分创建了一个模板,为 SignedInfo 部分创建了一个模板,并为整个 Security header 部分创建了一个模板。

间距当然很重要,因此请确保 xml 保持未格式化,并且如果您正在加载 XmlDocument,确保将 PreserveWhitespace 设置为 true 总是一个好主意:

XmlDocument doc = new XmlDocument() { PreserveWhitespace = true;}

所以现在我将模板保存在资源中,当我需要对时间戳进行签名时,我将时间戳模板加载到字符串中,用正确的时间戳 ID、创建和过期字段替换标签,所以我有这样的东西(用正确的命名空间,当然没有换行符):

<wsu:Timestamp xmlns:ns1="..." xmlns:soapenv="..." xmlns:wsse=".." xmlns:wsu=".." wsu:Id="TI-3">
    <wsu:Created>2019-05-07T21:31:00Z</wsu:Created>
    <wsu:Expires>2019-05-07T21:36:00Z</wsu:Expires>
</wsu:Timestamp>

然后获取哈希:

// Get hash of timestamp.
SHA256Managed shHash = new SHA256Managed();
var fileBytes = System.Text.Encoding.UTF8.GetBytes(timestampXmlString);
var hashBytes = shHash.ComputeHash(fileBytes);
var digestValue = Convert.ToBase64String(hashBytes);

接下来我需要一个我的 SignedInfo 部分的模板。我从我的资源中提取它,并替换适当的标签(在我的例子中是时间戳参考 ID 和上面计算的时间戳摘要值),然后我得到了 SignedInfo 部分的哈希:

// Get hash of the signed info
SHA256Managed shHash = new SHA256Managed();
fileBytes = System.Text.Encoding.UTF8.GetBytes(signedInfoXmlString);
hashBytes = shHash.ComputeHash(fileBytes);
var signedInfoHashValue = Convert.ToBase64String(hashBytes);

然后我签署签名信息的哈希以获取签名:

using (var rsa = MyX509Certificate.GetRSAPrivateKey())
{
    var signatureBytes = rsa.SignHash(hashBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
    SignatureValue = Convert.ToBase64String(signatureBytes); // This is my signature!
}

如果失败,请确保您的证书设置正确,它还应该有一个私钥。如果您正在运行旧版本的框架,您可能需要跳过一些环节才能获得 RSA 密钥。见https://stackoverflow.com/a/38380835/6077517

用户名密码摘要随机数

我不必签署用户名,但我必须计算密码摘要。它被定义为 Base64( SHA1(Nonce + CreationTime + Password) )。

    // Create nonce
    SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
    var nonce = Guid.NewGuid().ToString("N");
    var nonceHash = sha1Hasher.ComputeHash(Encoding.UTF8.GetBytes(nonce));
    var NonceValue = Convert.ToBase64String(nonceHash);

    var NonceCreatedTime = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddThh:mm:ss.fffZ");

    // Create password digest Base64( SHA1(Nonce + Created + Password) )
    var nonceBytes = Convert.FromBase64String(NonceValue); // Important - convert from Base64
    var createdBytes = Encoding.UTF8.GetBytes(NonceCreatedTime);
    var passwordBytes = Encoding.UTF8.GetBytes(Password);

    var concatBytes = new byte[nonceBytes.Length + createdBytes.Length + passwordBytes.Length];
    System.Buffer.BlockCopy(nonceBytes, 0, concatBytes, 0, nonceBytes.Length);
    System.Buffer.BlockCopy(createdBytes, 0, concatBytes, nonceBytes.Length, createdBytes.Length);
    System.Buffer.BlockCopy(passwordBytes, 0, concatBytes, nonceBytes.Length + createdBytes.Length, passwordBytes.Length);

    // Hash the combined buffer
    var hashedConcatBytes = sha1Hasher.ComputeHash(concatBytes);
    var PasswordDigest = Convert.ToBase64String(hashedConcatBytes);

就我而言,有一个额外的问题是密码需要进行 SHA1 哈希处理。如果您在 SoapUI 中设置 WS-Security 用户名,这就是 SoapUI 所称的“PasswordDigest Ext”。请记住,如果您仍然遇到身份验证问题,我花了很多时间才意识到我需要先对密码进行哈希处理。

还有一件事我不知道该怎么做,这里是如何从您的 X509 证书中获取 Base64 二进制安全令牌值:

var bstValue = Convert.ToBase64String(myCertificate.Export(X509ContentType.Cert));

最后,我从资源中提取我的 Security 标头模板并替换我收集或计算的所有相关值:UsernameTokenId、Username、Password Digest、Nonce、UsernameToken Created time、Timestamp fields、BinarySecurityToken 和 BinarySecurityTokenID(确保在KeyInfo 部分)、时间戳摘要、ID,最后是我的签名。关于 ID 的注释,我认为这些值并不重要,只要它们在文档中是唯一的,只要确保它们是相同的 ID,如果它们在请求中的其他地方被引用,查找“#”符号。

已编译的 XML 安全标头字符串被加载到 XmlDocument 中(请记住保留空格)并传递给自定义 MessageHeader 以在 CustomHeader.OnWriteHeaderContents 中序列化(参见上面的 CustomHeader)。

唷。希望这将为某人节省大量工作,为拼写错误或无法解释的步骤道歉。如果有人想出来的话,我很想看到一个优雅的纯 WCF 实现。

于 2019-03-08T21:12:33.247 回答
3

您可能需要考虑一个自定义安全绑定类,该类以您想要的方式实现安全性,而不是 WCF 默认值。

这些 MSDN 链接解释了自定义绑定和 SecurityBindingElement 抽象基类:

http://msdn.microsoft.com/en-us/library/ms730305.aspx

http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.securitybindingelement.aspx

于 2011-02-23T16:16:30.980 回答
1

WCF 本身不允许对时间戳进行签名,但不允许对用户名进行签名。首先,我很确定这与您面临的问题无关 - 服务器应该能够处理这两种情况。如果您确实需要它,那么我建议在安全性中根本不使用用户名(例如“anonymousForCertificate”的安全模式),然后实现自定义消息编码器以手动将用户名/密码标签推送到正确位置的标题中(采取注意不要更改消息中的任何签名部分,主要是时间戳)。

于 2013-08-14T15:11:25.843 回答
0

您可以使用消息合同执行此操作,请参阅:http: //msdn.microsoft.com/en-us/library/ms730255.aspx

以下是来自上述链接的示例:

[MessageContract]
public class PatientRecord 
{
   [MessageHeader(ProtectionLevel=None)] public int recordID;
   [MessageHeader(ProtectionLevel=Sign)] public string patientName;
   [MessageHeader(ProtectionLevel=EncryptAndSign)] public string SSN;
   [MessageBodyMember(ProtectionLevel=None)] public string comments;
   [MessageBodyMember(ProtectionLevel=Sign)] public string diagnosis;
   [MessageBodyMember(ProtectionLevel=EncryptAndSign)] public string medicalHistory;
}

注意保护级别 None、Sign、EncryptAndSign

于 2011-02-24T15:01:35.030 回答