Como assinar o XML em um nó específico (Nota Fiscal)

Bom dia pessoal, estou com dúvidas sobre assinatura em XML em um nó específico do arquivo.

Estou implementando um sistema PHP para emissão automática de nota fiscal de serviço. Atualmente consigo assinar o arquvio XML e enviar via SOAP para o servidor da prefeitura de Salvador/BA.

Entretanto, ao enviar, recebo o erro “Assinatura digital da NFTS incorreta”. Ao estudar a estrutura do XML modelo, vi que existe um campo no XML onde precisa ter uma assinatura da nota fiscal, e não o Signature que assina o documento.

Segue abaixo o modelo da nota fiscal que peguei no site da prefeitura:

<PedidoEnvioNFTS xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://nfse.salvador.ba.gov.br/nfts">
<Cabecalho Versao="1" xmlns="">
    <Remetente>
        <CPFCNPJ>
            <CNPJ>57529050000188</CNPJ>
        </CPFCNPJ>
    </Remetente>
</Cabecalho>
<NFTS xmlns="">
    <TipoDocumento>01</TipoDocumento>
    <ChaveDocumento>
        <InscricaoMunicipal>13580200127</InscricaoMunicipal>
        <SerieNFTS>A</SerieNFTS>
        <NumeroDocumento>202</NumeroDocumento>
    </ChaveDocumento>
    <DataPrestacao>2014-10-02</DataPrestacao>
    <StatusNFTS>N</StatusNFTS>
    <TributacaoNFTS>T</TributacaoNFTS>
    <ValorServicos>100</ValorServicos>
    <ValorDeducoes>0</ValorDeducoes>
    <CodigoServico>103</CodigoServico>
    <AliquotaServicos>0.03</AliquotaServicos>
    <ISSRetidoTomador>false</ISSRetidoTomador>
    <Prestador>
        <CPFCNPJ>
            <CNPJ>32250824000106</CNPJ>
        </CPFCNPJ>
        <RazaoSocialPrestador>Prestador Teste</RazaoSocialPrestador>
        <Endereco>
            <CEP>44020200</CEP>
        </Endereco>
        <Email>[email protected]</Email>
    </Prestador>
    <RegimeTributacao>0</RegimeTributacao>
    <Discriminacao>Emissao de NFTS</Discriminacao>
    <TipoNFTS>1</TipoNFTS>
    <Assinatura>ODY+3VzyGzi3hTi+FtXcWYputJtZat4txp8fQBdMPkei1DsWsd02vpubTUyLLYpE/1cXzlbJFEbKdV3uf3n4LP3rWlUgmWDat+NjCakSysqxq42UCqIQ9Se4QWYKV4pyEIX8iAaB5RmBzzRme2bdLDkcVP+G9FYNqlzdrBhhAwqRR+kkio6Htor17a9WspFIHPVMwKAIobqCvSXEuhvCWMLS3oX47VIkHG/iAD/mZgaJiyIvYwl+jylGZOYw3tZ27Jmk/r2n9IQgHegNo6IMch7m7vgTOVEtilOi0Gx/otfwhj5pRIPVB8txHeu4KFGjzs6sHlcxwhpcppO3pTngEA==</Assinatura>
</NFTS>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
        <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
        <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
        <Reference URI="">
            <Transforms>
                <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
            </Transforms>
            <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>X6CiD8MsW3rwvIgL1EflzMttwK8=</DigestValue>
        </Reference>
    </SignedInfo>
    <SignatureValue>vI50jrPlWfWUq0ld1X9STxFGDfI327knRq3zg/CNtH+X0aoYzYoOPdSKJ2LDdPrrsQFGTaVPqne1XioPmnJpwguVJ5BSlHZy0ebtm2isMkB1N1yjYSv9eUoFzKGksCwNIeI/KB6d780fOAloQ31Gy1sLSDbIcn7B8ylJT5u3nsPBgPRgjHX6fQVNTBJTF0DrbLAkAhzUYw4oBigZvHSfPZ5kxgH91LBTYJfJxHHMAiFqfj+QoUR3f7W5logC/Qu7lz5m0CoCmCMrYa4YK3qyFAFXXDAsqhNbXM6OomQyBR98RdbsiyRncGiz7AktG/bbu9Yf+Xfd0EzpfDKTNXWtgA==</SignatureValue>
    <KeyInfo>
        <X509Data>
            <X509Certificate>_CERTIFICADO_</X509Certificate>
        </X509Data>
    </KeyInfo>
</Signature>

Logo abaixo do 1 tem um nó chamado com uma assinatura digital da nota fiscal, não do documento.

Já busquei no google e até agora não identifiquei como fazer a assinatura nesse campo específico, só lembrando que segundo a documentação da Prefeitura, não pode ser cópia do nó que tem quando assina o documento xml, ou seja, no caso, é uma assinatura nova apenas para o documento.

Atualmente estou utilizando a biblioteca Greenter (github) para assinar o arquivo xml, e já chequei a validade da assinatura e está tudo normal, e pelo que tudo indica o que está dando erro é a falta de assinatura da nota fiscal no nó .

Alguém saberia como me ajudar nesse ponto?

Verifique esse meu gist para saber como assinar o XML:
https://gist.github.com/luizvaz/6c3e2c1e6bc7d310898107c9f95476bb

A função assinaXML($docxml, $tag) recebe um XML e o nome da TAG para assinar.
Repare que os arquivos do certificado estão referenciados dentro da função.
Eles precisam estar no formato PEM.

Obrigado pela informação, só que agora estou com dúvida, consigo assinar na tag específica, mas na hora de assinar o lote, para enviar, está retornando que a assinatura não confere. Quando gero apenas uma assinatura e checo no site da receita federal, diz que a assinatura está válida, mas quando gero as 2, da erro de que a assinatura não é válida.
O que estou fazendo é gero um xml com a nota assinada na primeira vez (assina o rps), e depois gero outro xml com a nota assinada no lote, pegando já o xml assinado no rps, mas da erro.
Não estou utilizando o seu repositório como exemplo não, porque estou desenvolvendo o meu próprio, mas a lógica é a mesma, teria como me ajudar?

Nesse caso, o que deve ter acontecido é que não está mantendo o conteúdo assinado de forma intacta. Não pode reformatar ou retirar espaços e quebras de linha.

Mas de qualuer forma, consultado o site.
Seu modelo não confere com o deles.
https://nfse.sefaz.salvador.ba.gov.br/OnLine/Documentos/XML.zip
https://nfse.sefaz.salvador.ba.gov.br/OnLine/Documentos/XSD.zip

O envio do RPS é através do EnviarLoteRpsEnvio.

Note que tem o Lote e a Lista de RPS.
O RPS é assinado pela tag InfRps após o conteúdo e a assinatura fica dentro da tag Rps.
A tag InfRps possui um Id que é referenciado na assinatura, <Reference URI=“#001”>.

A assinatura do LoteRps fica após o conteúdo e dentro da tag EnviarLoteRpsEnvio.
A tag LoteRps possui um Id que também é referenciado na assinatura, exemplo <Reference URI=“#002”>.

Se você obedecer essas regras, deve funcionar.

Meu código já faz isso.
Basta você passar o conteúdo duas vezes para a função.

Obrigado mais uma vez pela resposta, mas eu estou seguindo exatamente essa ordem, mas quando tento validar, ele da erro.
Se eu assinar apenas uma vez o documento, seja ele onde for, funciona, entretanto quando tento fazer a segunda assinatura que da o erro.
Eu to fazendo um modelo da nota fiscal, e assina ela no espaço Rps. Depois disso, antes de enviar eu uso a mesma função para assinar, só que agora no campo EnviarLoteRpsEnvio.
Minha nota fiscal sai igual ao modelo que o funcionário da SEFAZ Salvador me enviou, mas mesmo assim, não consigo nem enviar e nem validar ela na receita federal.
Vou tentar usar o seu código para ver o que consigo.

Inclusive estou utilizando a mesma biblioteca que você, do robrichards, e não estou reformatando nem nada o xml quando tento assinar ele pela segunda vez.

Estou fazendo assim na função:


$doc = new DOMDocument();
$doc->load('nota.xml');
$objDSig = new XMLSecurityDSig('');
$objDSig->setCanonicalMethod(XMLSecurityDSig::C14N);
$objDSig->addReference(
    $doc, 
    XMLSecurityDSig::SHA1, 
    ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 
		'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'],
	['force_uri' => true, 'uri' => 'rpsId23253']	
);
$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'private'));
$objKey->passphrase = '1234';
$objKey->loadKey(dirname(__FILE__).'/cert.pem', true);
$objDSig->sign($objKey);
$objDSig->add509Cert(file_get_contents(dirname(__FILE__).'/cert2.pem'));
$objDSig->appendSignature($doc->getElementsByTagName('Rps')->item(0));
//$objDSig->appendSignature($doc->documentElement);
$doc->save('notaassinada.xml');

$doc = new DOMDocument();
$doc->load('notaassinada.xml');
$objDSig = new XMLSecurityDSig('');
$objDSig->setCanonicalMethod(XMLSecurityDSig::C14N);
$objDSig->addReference(
    $doc, 
    XMLSecurityDSig::SHA1, 
    ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 
		'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'],
	['force_uri' => true, 'uri' => 'AGZ001']
);
$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'private'));
$objKey->passphrase = '1234';
$objKey->loadKey(dirname(__FILE__).'/cert.pem', true);
$objDSig->sign($objKey);
$objDSig->add509Cert(file_get_contents(dirname(__FILE__).'/cert2.pem'));
$objDSig->appendSignature($doc->getElementsByTagName('EnviarLoteRpsEnvio')->item(0));
//$objDSig->appendSignature($doc->documentElement);
$doc->save('notaassinada.xml');

Sua implementação está quase certa.
Você precisa adicionar ao Pai do Nó após assinado o conteúdo.
Por isso está dando errado.
Você está modificando o conteúdo do mesmo.

É preciso encontrar o node através de uma expressão XPath ou procurando diretamente.
Eu faço isso nesse trecho:

$tagid = $tag;
...
//extrair a tag com os dados a serem assinados
$xpath = new DOMXPath($xmldoc);
$xpath->registerNamespace('ns', "http://www.abrasf.org.br/nfse.xsd");
$node = $xpath->query("//ns:{$tagid}")->item(0);
if (!isset($node)) {
   $msg = "A tag <$tagid> não existe no XML!!";
   throw new Exception($msg);
}

É preciso colocar o NameSpace corretamente, para que o XPath consiga encontrar o elemento.
Caso contrário, não funciona.

Depois de calculada a assinatura, eu faço a referência informando o próprio node, além do Id do mesmo:

// Assina usando SHA-1
$objDSig->addReference(
   $node,
   XMLSecurityDSig::SHA1,
   [
      "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
      "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
   ],
   [
      'id_name' => 'Id',
      'overwrite' => false
   ]
);

No final de tudo, eu adiciono a assinatura no elemento pai:

// Acrescenta a signature para o XML
$objDSig->appendSignature($node->parentNode);

Dessa forma a assinatura fica localizada nos elementos esperados pela aplicação da Prefeitura.

Pegouuuuuuuuu!!! uhuuuuuuu!!! Obrigado meu querido pela ajuda… O erro que deu por ultimo foi porque eu não tinha referenciado o id do lote

1 curtida

Amigo, só mais uma dúvida, desculpa está incomodando, mas você saberia como faço para pegar o código de verificação da nota fiscal, após ela é gerada? Estou tentando pegar para poder gerar a nota em pdf também, pra poder enviar para o cliente

No XML de consulta da nota.

Você envia a consulta:

<ConsultarLoteRpsEnvio xmlns="http://www.abrasf.org.br/ABRASF/arquivos/nfse.xsd">
    <Prestador>
        <Cnpj>12345678000128</Cnpj>
        <InscricaoMunicipal>11111111111</InscricaoMunicipal>
    </Prestador>
    <Protocolo>27230FEADB2F4F87B42E7744EBF8C40E</Protocolo>
</ConsultarLoteRpsEnvio>

E a resposta possui o código:

<ConsultarLoteRpsResposta xmlns="http://www.abrasf.org.br/ABRASF/arquivos/nfse.xsd">
    <ListaNfse>
        <CompNfse>
            <Nfse>
                <InfNfse id="20091">
                    <Numero>20091</Numero>
                    <CodigoVerificacao>AB3DCD21D</CodigoVerificacao>
                    <DataEmissao>2010-03-01T15:12:03</DataEmissao>
                    <IdentificacaoRps>
                        <Numero>1</Numero>
                        <Serie>1</Serie>
                        <Tipo>1</Tipo>
                    </IdentificacaoRps>

Uma postagem foi mesclada em um tópico existente: Emissao da NFSE salvador ba

@luizvaz , vc teria esse mesmo código em c# ou delphi?
Estou disposto a pagar por ele.
Grato!

Olá André,

Em Delphi, eu aconselho a usar os componentes do Projeto ACBR
Verifique o novo componente ACBrNFSeX

Já em C# eu uso o código abaixo:

public string AssinarXML(string tempFile, string refUri, string tag, bool addToParent = false)
{

    XmlDocument doc = new XmlDocument();
    doc.PreserveWhitespace = true;
    doc.LoadXml(RemoveDiacritics(File.ReadAllText(tempFile)));

    // Create a SignedXml object.
    SignedXml signedXml = new SignedXml(doc);
    //signedXml.Signature.Id = signatureId;
    signedXml.SigningKey = cert.PrivateKey;
    signedXml.SignedInfo.CanonicalizationMethod = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments";

    Reference reference = new Reference(refUri);
    reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
    reference.AddTransform(new XmlDsigC14NTransform());

    signedXml.AddReference(reference);

    KeyInfo keyInfo = new KeyInfo();
    keyInfo.AddClause(new KeyInfoX509Data(cert));

    signedXml.KeyInfo = keyInfo;

    signedXml.ComputeSignature();
    XmlElement xmlDigitalSignature = signedXml.GetXml();

    XmlNodeList tags = doc.GetElementsByTagName(tag);
    XmlElement node = (XmlElement)tags[0]; //Primeiro da lista
    if (addToParent)
        node.ParentNode.AppendChild(xmlDigitalSignature);
    else
        node.AppendChild(xmlDigitalSignature);

    // Save Without header
    XmlWriterSettings settings = new XmlWriterSettings();
    settings.OmitXmlDeclaration = true;
    settings.Indent = false;
    XmlWriter xw = XmlWriter.Create(tempFile, settings);
    doc.Save(xw);
    xw.Close();

    return File.ReadAllText(tempFile);
}

Sendo que a variável cert é previamente atribuída e é do tipo X509Certificate2.

No .NET Core você precisa dos imports:

using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Xml;
using System.Xml.Linq;

Para criar o XML eu faço assim:

public string asXML()
{
var doc = new XDocument(new XDeclaration("1.0", "UTF-8", ""));
//ID
string Id = (string.Format("{0:yyMMdd}", DateTime.Now) + Math.Floor(DateTime.Now.TimeOfDay.TotalMilliseconds).ToString().PadLeft(9, '0'));
string Num = (string.Format("{0:yyMMdd}", DateTime.Now) + Math.Floor(DateTime.Now.TimeOfDay.TotalSeconds).ToString().PadLeft(5, '0'));

//GerarNfseEnvio
XNamespace xmlns = XNamespace.Get("http://www.abrasf.org.br/nfse.xsd");
XElement gerarNfseEnvio = new XElement(xmlns + "GerarNfseEnvio");
doc.Add(gerarNfseEnvio);

//RPS
XElement Rps = new XElement(xmlns + "Rps");
gerarNfseEnvio.Add(Rps);

XElement InfRps = new XElement(xmlns + "InfDeclaracaoPrestacaoServico");
Rps.Add(InfRps);
InfRps.Add(new XAttribute("Id", "A" + this.id));

InfRps.Add(
    new XElement(xmlns + "Rps",
        new XAttribute("Id", "B" + this.id),
        new XElement(xmlns + "IdentificacaoRps",
            new XElement(xmlns + "Numero", this.id), //Prefeitura usa AIDF?
            new XElement(xmlns + "Serie", this.serie),
            new XElement(xmlns + "Tipo", this.tipo)),
        new XElement(xmlns + "DataEmissao", this.dataEmissao.ToString("yyyy-MM-dd")),
        new XElement(xmlns + "Status", 1)
    )
);
InfRps.Add(new XElement(xmlns + "Competencia", this.dataEmissao.ToString("yyyy-MM-dd")));

...

var tempFile = System.IO.Path.Combine(path, filename);
doc.Save(tempFile);

//Assinar
return AssinarXML(tempFile, "#A" + this.id, "InfDeclaracaoPrestacaoServico", true);
}

@luizvaz muito obrigado pela ajuda. No caso em questão, tenho que assinar os rps e o lote. A rotina de assinatura faz isso, alterando os parametros passados, correto?
No caso, tenho que assinar um xml com lote de rps pré fornecido
segue o link do xml modelo que a Sintese forneceu

https://drive.google.com/file/d/1eT0_qnA6Xkebps_GVSNBrmPJe7dTOun9/view?usp=sharing
segue o link do xml que tenho que assinar
https://drive.google.com/file/d/1sW6KWkwy19gcy-VbrHfZOPtpkK8gvN-k/view?usp=sharing

Se puder me dar uma ajuda nessa rotina em c# que vc me passou fico muito agradecido.

Primeiro, começe com o básico, informe apenas um RPS dentro do LoteRPS.

Você vai chamar a rotina duas vezes.
Na primeira você passa a tag InfDeclaracaoPrestacaoServico.
Com o resultado do processamento, você chama a segunda vez passando a tag LoteRps.

Chame assim:

AssinarXML(tempFile, "#RPS1", "InfDeclaracaoPrestacaoServico", true);
AssinarXML(tempFile, "#LOTE1", "InfDeclaracaoPrestacaoServico", true);

O resultado assinado estará no arquivo tempFile.

Assim que estiver funcionando, você terá como fazer para mais de um RPS:

AssinarXML(tempFile, "#RPS1", "InfDeclaracaoPrestacaoServico", true);
AssinarXML(tempFile, "#RPS2", "InfDeclaracaoPrestacaoServico", true);
AssinarXML(tempFile, "#LOTE1", "InfDeclaracaoPrestacaoServico", true);

Este tópico foi fechado automaticamente 30 dias depois da última resposta. Novas respostas não são mais permitidas.