Skip to content

Instantly share code, notes, and snippets.

@erikwco
Forked from luizvaz/elotech.cs
Created May 1, 2022 23:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erikwco/41780a9e9177b86848004f1e473e0e39 to your computer and use it in GitHub Desktop.
Save erikwco/41780a9e9177b86848004f1e473e0e39 to your computer and use it in GitHub Desktop.
Elotetch - C# SOAP XML Sign Oasis WSSecurity - Palotina/PR
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
namespace NFSeUtil
{
/*
* Sample use:
*
* var content = @"<ConsultarNfseRpsEnvio xmlns=""http://shad.elotech.com.br/schemas/iss/nfse_v1_2.xsd"">
* <IdentificacaoRps>
* <Numero>404</Numero>
* <Serie>E</Serie>
* <Tipo>1</Tipo>
* </IdentificacaoRps>
* <IdentificacaoRequerente>
* <CpfCnpj>
* <Cnpj>80896194000194</Cnpj>
* </CpfCnpj>
* <InscricaoMunicipal>5610</InscricaoMunicipal>
* <Senha>K4KT7GHC</Senha>
* <Homologa>true</Homologa>
* </IdentificacaoRequerente>
* </ConsultarNfseRpsEnvio>";
*
* X509Certificate2 cert = new X509Certificate2("<FILE>", "PASSWORD", X509KeyStorageFlags.MachineKeySet);
* WS_4117909A ws1 = new WS_4117909A(cert, "http://extranet.palotina.pr.gov.br:8088/WebEloWSIssAise/nfse_v1_2.wsdl");
* ws1.FileName = System.IO.Path.Combine("<PATH>", "<FILE>.xml");
* var doc = XDocument.Parse(content);
* var request = doc.ToString(SaveOptions.DisableFormatting);
* var response = ws1.ConsultarNfsePorRps(request);
*
*/
// *** Sign XML with Malformed Id
/*
* https://weblog.west-wind.com/posts/2008/Feb/23/Digitally-Signing-an-XML-Document-and-Verifying-the-Signature
* https://stackoverflow.com/questions/5099156/malformed-reference-element-when-adding-a-reference-based-on-an-id-attribute-w
* https://stackoverflow.com/questions/30579938/generate-digital-signature-but-with-a-specific-namespace-prefix-ds
*/
internal sealed class SignedXmlWithId : SignedXml
{
public SignedXmlWithId(XmlDocument xml)
: base(xml)
{
}
public SignedXmlWithId(XmlElement xmlElement)
: base(xmlElement)
{
}
public override XmlElement GetIdElement(XmlDocument doc, string id)
{
// check to see if it's a standard ID reference
XmlElement idElem = base.GetIdElement(doc, id);
if (idElem == null)
{
XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
nsManager.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
idElem = doc.SelectSingleNode("//*[@wsu:Id=\"" + id + "\"]", nsManager) as XmlElement;
}
return idElem;
}
}
#region
//Classes Auxiliares
public class WS_4117909A
{
private HttpStatusCode cLastCode;
private X509Certificate2 cCertificado;
private string cFileName;
private string Url;
const string STR_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
const string STR_WSSE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
const string STR_WSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
const string STR_DS = "http://www.w3.org/2000/09/xmldsig#";
const string STR_EC = "http://www.w3.org/2001/10/xml-exc-c14n#";
const string sSoapEnvelope =
@"<?xml version=""1.0"" encoding=""utf-8""?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV=""http://schemas.xmlsoap.org/soap/envelope/"">
<SOAP-ENV: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-ENV: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-EC95425802FB9F663F15021186132611"">
</wsse:BinarySecurityToken>
</wsse:Security>
</SOAP-ENV:Header>
<SOAP-ENV:Body xmlns:wsu=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" wsu:Id=""id-1"">
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>";
public X509Certificate2 Certificado
{
get { return cCertificado; }
set { cCertificado = value; }
}
public string FileName
{
get { return cFileName; }
set { cFileName = value; }
}
public HttpStatusCode lastCode { get { return cLastCode; } }
public WS_4117909A(
X509Certificate2 pCertificado,
string pUrl)
{
Certificado = pCertificado;
Url = pUrl;
}
public bool ValidateSoapBodySignature(XmlDocument doc, X509Certificate2 cert)
{
XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);
ns.AddNamespace("SOAP-ENV", STR_SOAPENV);
ns.AddNamespace("wsse", STR_WSSE);
ns.AddNamespace("wsu", STR_WSU);
ns.AddNamespace("ds", STR_WSU);
ns.AddNamespace("ec", STR_WSU);
bool passes = true;
CspParameters cspParams = new CspParameters();
cspParams.KeyContainerName = "XML_DSIG_RSA_KEY";
RSACryptoServiceProvider rsaKey = new RSACryptoServiceProvider(cspParams);
SignedXml signedXml = new SignedXmlWithId(doc);
XmlNodeList nodeList = doc.GetElementsByTagName("Signature");
if (nodeList.Count == 0) nodeList = doc.GetElementsByTagName("ds:Signature");
XmlNodeList certificates = doc.GetElementsByTagName("wsse:BinarySecurityToken");
X509Certificate2 dcert2 = new X509Certificate2(Convert.FromBase64String(certificates[0].InnerText));
foreach (XmlElement element in nodeList)
{
signedXml.LoadXml(element);
passes = passes && signedXml.CheckSignature(dcert2, true);
}
return passes;
}
/// <summary>
/// Create a soap webrequest to [Url]
/// </summary>
/// <returns></returns>
public static HttpWebRequest CreateWebRequest(string url, string action)
{
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url);
webRequest.Headers.Add("SOAPAction", action);
webRequest.ContentType = "text/xml;charset=\"utf-8\"";
webRequest.Accept = "text/xml";
webRequest.Method = "POST";
return webRequest;
}
public XmlDocument AssinaSOAP(string value)
{
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = true;
doc.LoadXml(sSoapEnvelope);
XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);
ns.AddNamespace("SOAP-ENV", STR_SOAPENV);
ns.AddNamespace("wsse", STR_WSSE);
ns.AddNamespace("wsu", STR_WSU);
ns.AddNamespace("ds", STR_WSU);
ns.AddNamespace("ec", STR_WSU);
// *** Grab the body element - this is what we create the signature from
XmlElement body = doc.DocumentElement.SelectSingleNode(@"//SOAP-ENV:Body", ns) as XmlElement;
if (body == null)
throw new ApplicationException("No body tag found");
// *** Fill the body
body.InnerXml = value;
// *** Signed XML will create Xml Signature - Xml fragment
SignedXmlWithId signedXml = new SignedXmlWithId(doc);
// *** Create a KeyInfo structure
KeyInfo keyInfo = new KeyInfo();
keyInfo.Id = "KI-EC95425802FB9F663F15021186132692";
KeyInfoNode keyInfoNode = new KeyInfoNode();
XmlElement wsseSec = doc.CreateElement("wsse", "SecurityTokenReference", STR_WSSE);
wsseSec.SetAttribute("xmlns:wsu", STR_WSU);
wsseSec.SetAttribute("Id", STR_WSU, "STR-EC95425802FB9F663F15021186132713");
XmlElement wsseRef = doc.CreateElement("wsse", "Reference", STR_WSSE);
wsseRef.SetAttribute("URI", "#X509-EC95425802FB9F663F15021186132611");
wsseRef.SetAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3");
wsseSec.AppendChild(wsseRef);
keyInfoNode.Value = wsseSec;
keyInfo.AddClause(keyInfoNode);
// *** The actual key for signing - MAKE SURE THIS ISN'T NULL!
signedXml.SigningKey = this.Certificado.PrivateKey;
// *** provide the certficate info that gets embedded - note this is only
// *** for specific formatting of the message to provide the cert info
signedXml.KeyInfo = keyInfo;
// *** Again unusual - meant to make the document match template
signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
// Set the InclusiveNamespacesPrefixList property.
XmlDsigExcC14NTransform canMethod = (XmlDsigExcC14NTransform)signedXml.SignedInfo.CanonicalizationMethodObject;
canMethod.InclusiveNamespacesPrefixList = "SOAP-ENV";
// *** Now create reference to sign: Point at the Body element
Reference reference = new Reference();
reference.Uri = "#id-1"; // reference wsu:Id=id-6 section in same doc
// Add an enveloped transformation to the reference.
XmlDsigExcC14NTransform env = new XmlDsigExcC14NTransform();
env.InclusiveNamespacesPrefixList = "";
reference.AddTransform(env); // required to match doc
signedXml.AddReference(reference);
// *** Finally create the signature
signedXml.ComputeSignature();
// *** wsse:Security element
XmlElement soapSignature = doc.DocumentElement.SelectSingleNode(@"//wsse:Security", ns) as XmlElement;
if (soapSignature == null)
throw new ApplicationException("No wsse:Security tag found");
// *** Create wsse:BinarySecurityToken element
XmlElement soapToken = doc.DocumentElement.SelectSingleNode(@"//wsse:BinarySecurityToken", ns) as XmlElement;
if (soapToken == null)
throw new ApplicationException("No wsse:BinarySecurityToken tag found");
var export = this.Certificado.Export(X509ContentType.Cert);
var base64 = Convert.ToBase64String(export);
soapToken.InnerText = base64;
// *** Result is an XML node with the signature detail below it
// *** Now let's add the sucker into the SOAP-HEADER
XmlElement signedElement = signedXml.GetXml();
XmlAttribute sId = doc.CreateAttribute("Id");
sId.Value = "SIG-2";
signedElement.Attributes.Append(sId);
// *** And add our signature as content
soapSignature.AppendChild(signedElement);
// *** Now add the signature header into the master header
XmlElement soapHeader = doc.DocumentElement.SelectSingleNode("//SOAP-ENV:Header", ns) as XmlElement;
if (soapHeader == null)
throw new ApplicationException("No SOAP-ENV:Header tag found");
// *** Validate
// var pass = ValidateSoapBodySignature(doc, this.Certificado);
return doc;
}
public String ConsultarNfseRps(String value)
{
HttpWebRequest request = CreateWebRequest(this.Url, "");
XmlDocument soapEnvelopeXml = AssinaSOAP(value);
using (Stream stream = request.GetRequestStream())
{
soapEnvelopeXml.Save(stream);
soapEnvelopeXml.Save(FileName.Replace(".xml", ".soap.xml")); //salvar arquivo retorno
}
try
{
using (WebResponse response = request.GetResponse())
{
using (StreamReader rd = new StreamReader(response.GetResponseStream()))
{
string soapResult = rd.ReadToEnd();
return soapResult;
}
}
}
catch (WebException wex)
{
cLastCode = (wex.Response as HttpWebResponse).StatusCode;
return new StreamReader(wex.Response.GetResponseStream()).ReadToEnd();
}
}
public String ConsultarNfsePorRps(String value)
{
return this.ConsultarNfseRps(value);
}
public String EnviarLoteRps(String value)
{
return null;
}
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment