Thank you so much @joe-sindoni. I had been struggling for hours with WCF to get it working with WS Security. In my case, I had to make a few modifications (with Copilot's help) so the server would validate my signature:
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);
nsmgr.AddNamespace("wsu", WsSecurityHeader.WsseUtilityNamespaceUrl);
nsmgr.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
// 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);
// Get the Security header
XmlNode securityHeader = doc.SelectSingleNode("//soapenv:Envelope/soapenv:Header/wsse:Security", nsmgr);
// Add BinarySecurityToken
string certId = "X509-" + Guid.NewGuid().ToString();
XmlElement binarySecurityToken = doc.CreateElement("wsse", "BinarySecurityToken", WsSecurityHeader.WsseNamespace);
binarySecurityToken.SetAttribute("Id", WsSecurityHeader.WsseUtilityNamespaceUrl, certId);
binarySecurityToken.SetAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3");
binarySecurityToken.SetAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
binarySecurityToken.InnerText = Convert.ToBase64String(X509Certificate.GetRawCertData());
securityHeader.AppendChild(binarySecurityToken);
var signedXml = new SignedXmlWithUriFix(doc);
signedXml.SigningKey = X509Certificate.PrivateKey;
signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA1Url;
signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
// Add the X509 certificate info to the KeyInfo section
var keyInfo = new KeyInfo();
// Create SecurityTokenReference to refer to the BinarySecurityToken
XmlElement securityTokenReference = doc.CreateElement("wsse", "SecurityTokenReference", WsSecurityHeader.WsseNamespace);
XmlElement reference = doc.CreateElement("wsse", "Reference", WsSecurityHeader.WsseNamespace);
reference.SetAttribute("URI", $"#{certId}");
reference.SetAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3");
securityTokenReference.AppendChild(reference);
// Add the SecurityTokenReference to KeyInfo
KeyInfoNode keyInfoNode = new KeyInfoNode(securityTokenReference);
keyInfo.AddClause(keyInfoNode);
signedXml.KeyInfo = keyInfo;
// Add the reference to the SignedXml object
Reference xmlReference = new Reference($"#{BodyIdentifier}");
xmlReference.DigestMethod = SignedXml.XmlDsigSHA1Url;
// Add transform
xmlReference.AddTransform(new XmlDsigExcC14NTransform());
signedXml.AddReference(xmlReference);
// Compute the signature
signedXml.ComputeSignature();
// Get the Signature element and append to security header
XmlElement xmlDigitalSignature = signedXml.GetXml();
securityHeader.AppendChild(doc.ImportNode(xmlDigitalSignature, true));
// Generate a new message from our XmlDocument
var newMessage = CreateMessageFromXmlDocument(request, doc);
request = newMessage;
return null;
}