具有WS-Security的WCF服务只需要签名的时间戳
我需要向第三方提供服务,该第三方将发送带有签名时间戳的soap消息 如何配置我的服务以支持此功能 更新 我已经设法接近了我们所关注的Soap消息的格式,但是WCF坚持同时对用户名和时间戳令牌进行签名,有没有办法修改 绑定到只对时间戳签名具有WS-Security的WCF服务只需要签名的时间戳,wcf,ws-security,Wcf,Ws Security,我需要向第三方提供服务,该第三方将发送带有签名时间戳的soap消息 如何配置我的服务以支持此功能 更新 我已经设法接近了我们所关注的Soap消息的格式,但是WCF坚持同时对用户名和时间戳令牌进行签名,有没有办法修改 绑定到只对时间戳签名 进一步更新 以下是我们的要求: 必须对Timestamp元素进行签名 用于签名的证书上的CN名称必须与UsernameToken元素中给定的用户名匹配 用于签名的证书必须在BinarySecurityToken元素中发送 KeyInfo元素只能包含Secur
进一步更新 以下是我们的要求:
- 必须对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>
< /代码> 您可能想考虑一个自定义安全绑定类,它实现了您想要的安全性,而不是WCF默认值。
这些MSDN链接解释了自定义绑定和SecurityBindingElement抽象基类:
您可以通过邮件合同来实现这一点,请参阅:
以下是上述链接中的一个示例:
[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、EncryptAndSignWCF本机不允许对时间戳进行签名,但不允许对用户名进行签名。首先,我很确定这与您面临的问题无关——服务器应该能够处理这两种情况。如果您确实需要用户名,那么我建议在安全性中完全不要使用用户名(例如“AnonymousForceCertificate”的安全模式),然后实现自定义消息编码器,手动将用户名/密码标记推送到正确位置的头中(注意不要更改消息中的任何签名部分,主要是时间戳).像这样的问题有很多,但是没有一个有明确的答案,所以在花了很多时间之后,我把我的答案留给这个8岁的问题,希望它能帮助别人
我必须向一个黑匣子服务器发送一条带有密码摘要和签名时间戳(仅对时间戳进行签名)的SOAP消息,我认为这是Axis2。我用不同的安全配置和SignedXml类的派生变体蒙混过关,成功地使我的消息看起来有些正确,但始终无法生成有效的签名。根据微软的说法,WCF不像非WCF服务器那样规范化,WCF省略了一些名称空间,并以不同的方式重命名名称空间前缀,因此我永远无法让签名匹配
因此,经过大量的尝试和错误,以下是我自己动手做的方式:
定义负责创建整个安全标头的自定义MessageHeader
定义自定义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>
下面是我的邮件检查器的代码:
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;
}
}
创建安全标头XML
这有点难看,但我最终做的是为安全标头的规范化部分创建XML模板,填充值,对SignedInfo部分进行适当的哈希和签名,然后将这些部分组合成完整的安全标头。我更愿意用代码来构建它们,但XmlDocument不会维护我添加的属性的顺序,这会弄乱我的规范化XML和签名,所以我保持简单
为了确保我的部分被正确规范化,我使用了一个名为SC14N的工具。我输入了一个示例XML请求和一个对我想要规范化的部分的引用,以及任何包含的名称空间,它返回了相应的XML。我将它返回的XML保存到一个模板中,将值和ID替换为可以稍后替换的标记。我为Timestamp部分创建了一个模板,为SignedInfo部分创建了一个模板,为整个Security header部分创建了一个模板
间距当然很重要,因此请确保xml保持未格式化状态,如果要加载XmlDocument,最好确保将PreserveWhitespace设置为true:
XmlDocument doc = new XmlDocument() { PreserveWhitespace = true;}
现在我的模板保存在参考资料中,当我需要对时间戳签名时,我将时间戳模板加载到字符串中,用正确的时间戳ID替换标记,创建并过期字段,因此我有类似的内容(使用正确的名称空间,当然没有换行符):
接下来,我需要我的SignedInfo部分的模板。我从我的资源中提取它,并替换相应的标记(在我的例子中,是上面计算的timestamp引用ID和timestamp 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密钥。看
当前用户名密码摘要
我没必要这么做
XmlDocument doc = new XmlDocument() { PreserveWhitespace = true;}
<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);
// 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!
}
// 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);
var bstValue = Convert.ToBase64String(myCertificate.Export(X509ContentType.Cert));