使用WS-Security的WCF服务仅需要签名时间戳

时间:2010-09-24 08:54:05

标签: wcf ws-security

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

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

更新 我已经设法接近我们所追求的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>

Serice Endpoint

<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 个答案:

答案 0 :(得分:3)

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

这些MSDN链接解释了Custom Bindings和SecurityBindingElement抽象基类:

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

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

答案 1 :(得分:1)

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

答案 2 :(得分: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

答案 3 :(得分:0)

SO上有很多这样的问题,但是没有一个给出明确的答案,因此在花了很多时间之后,我将这个已有8年历史的问题的答案留给了我,希望对大家有所帮助。

我必须将带有密码摘要并签名的时间戳记(仅对时间戳记进行签名)的SOAP消息发送到黑匣子服务器,我认为它是Axis2。我无所事事地使用了不同的安全性配置以及SignedXml类的派生变体,并成功使我的消息看起来有些正确,但始终无法产生有效的签名。根据Microsoft的说法,WCF不能规范化非WCF服务器的规范,并且WCF省略了一些名称空间,并以不同的方式重命名了名称空间前缀,因此我永远都无法匹配我的签名。

因此,经过大量的反复试验,这是我自己做的方法:

  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. 需要通过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类。这几乎可以让您在插入标头和传输消息之前更改与所需请求有关的任何内容。

此处有一篇很好的文章,它使用此方案向邮件中添加用户名密码随机数:https://weblog.west-wind.com/posts/2012/nov/24/wcf-wssecurity-and-wse-nonce-authentication

您必须创建一个自定义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模板,填充值,对哈希值进行适当签名并对其进行签名,然后将这些片段组合成完整的安全标头。我本来希望用代码来构建它们,但是XmlDocument不会保持我添加的属性的顺序,这使我的规范化XML和我的签名弄乱了,所以我保持简单。

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

间距当然很重要,因此请确保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和时间戳digestValue),然后得到了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创建时间,Timestamp字段,BinarySecurityToken和BinarySecurityTokenID(确保此ID也是在KeyInfo部分中引用),时间戳摘要,ID,最后是我的签名。关于ID的注释,只要值在文档中是唯一的,我认为这些值无关紧要,只要在请求的其他地方引用它们,请确保它们是相同的ID,请查找“#”签名。

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

he。希望这可以节省一些工作,为错别字或无法解释的步骤道歉。如果有人想出了这一切,我希望看到一个优雅的纯WCF实现。