Desmistificando a Assinatura em XML

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 7163
Imagem meramente ilustrativa

Antes de começar este artigo, gostaria de agradecer a duas pessoas que me ajudaram e me incentivaram a aprender mais sobre este tema. MaxGama (@Max_Gama) e Victor Gonzales (Pandaa). Sem a ajuda de vocês este seria mais um daqueles projetos que a gente começa e deixa de lado por conta da dificuldade encontrada. Muito obrigado.

A história da minha pesquisa a respeito da assinatura em xml começou a pouco mais de 1 ano e meio, quando eu resolvi aprender como assinar um xml, e quando eu ainda chamava o processo de "assinatura de xml". Naquela época eu ficava (e ainda fico) muito incomodado com a resposta unânime que era dada toda vez que alguém precisava assinar algum XML: "use o ACBr". Sem dúvida o ACBr tem seus méritos, porém eu não concordo que alguém precise instalar toda uma suíte de componentes direcionados a um nicho específico, apenas para fazer a assinatura de um mísero XML. Eu não acredito em bala de prata, por isso tentei criar algo especificamente para realizar a assinatura. Eu pensei que por ser um formato de texto plano, assinar um xml seria mais fácil do que assinar outros tipos de arquivos, afinal, "era só colocar alguns nós especiais com alguns hashes calculados dentro deles, codificados em Base64". Claro, quebrei a cara, e desisti quando comecei a ver algo que se chama canonicalização, que é um dos pontos fundamentais da assinatura em xml, mas que é algo quase humanamente impossível de resolver, dada a quantidade absurda de variáveis envolvidas para a normatização de um documento de formato flexível como é o caso do xml.

Foi então que recentemente (há uns 6 meses!) tomei conhecimento da existência do CryptXML, um conjunto de APIs de baixo nível nativas do Windows, que implementam a criação e a verificação de assinaturas digitais em XML, baseadas na segunda edição do XML Signature Syntax and Processing, uma recomendação da W3C.


"Assinatura em xml" versus "Assinatura de xml"

Eu não vou julgar as pessoas que precisam resolver os problemas de forma rápida sem se aprofundar no assunto. Muitos de nós tem prazos absurdos para cumprir e sobra pouco tempo para aprender de fato o que ocorre nos bastidores. É por isso que eu estou aqui escrevendo este artigo, para que você, que arrumou este tempinho, entenda que aquilo que você normalmente utiliza chamando de assinatura de xml na verdade é um esquema de assinatura digital genérico onde o formato do arquivo assinado ao final é um xml, portanto, é uma assinatura em xml (no formato xml) e não assinatura de xml.

Se ainda não ficou claro, eu estou querendo dizer que usando a assinatura em xml é possível assinar qualquer tipo de arquivo, de simples texto plano a arquivos binários, ou seja, aquilo que até hoje você só usou para assinar seus xml de Nota Fiscal Eletrônica (NF-e), serve para assinar PDFs, imagens, documentos do Word, planilhas do Excel e simples textos planos!

Em suma, falar "Assinatura de xml" é minimizar algo que tem muito potencial, portanto, é assinatura em xml que se fala, grave isso! Neste artigo vou mostrar como realizar tanto a assinatura, como a validação de algo que foi assinado em xml. Se você quiser saber mais sobre a assinatura em XML (XML-DSIG), leia o documento oficial no W3C.

"Enveloped" versus "Enveloping"

Não se assuste com estes nomes, eles são as duas modalidades possíveis de assinatura em xml as quais tentarei explicar de forma simples. Bem, como você já deve saber, um arquivo XML é estruturado de uma forma hierárquica, o que significa que ele contém nós (ou tags) e estes nós podem possuir texto, ou outros nós dentro deles. Com isso em mente e sabendo que a assinatura em XML sempre gera ao final um documento XML que contém tanto a assinatura digital quanto o conteúdo assinado, observe adiante os exemplos para cada uma destas modalidades. A seguir está um exemplo de uma assinatura da modalidade ENVELOPING:

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
        ...
        <Reference URI="#ConteudoAssinado">
            <DigestValue>Digest Aqui</DigestValue>
        </Reference>
    </SignedInfo>
    <SignatureValue>Assinatura Aqui</SignatureValue>
    ...
    <Object Id="ConteudoAssinado">Um pequeno exemplo de texto plano
com espaços, uma quebra de linha e terminado por um ponto.
</Object>
</Signature>

Uma assinatura do tipo ENVELOPING gera um documento XML-DSIG que contém dentro de si, um nó cujo conteúdo é aquilo que foi efetivamente assinado. Em uma assinatura desta modalidade, o nó de nível superior (nó raiz) do xml sempre será o nó <Signature> com o namespace "http://www.w3.org/2000/09/xmldsig#". No exemplo anterior o conteúdo que foi assinado foi exatamente a frase "Um pequeno exemplo de texto plano[NL]com espaços, uma quebra de linha e terminado por um ponto.", onde [NL] foi colocado apenas para identificar que ali haverá uma quebra de linha (CR+LF ou LF). Este tipo de assinatura permite que qualquer tipo de arquivo seja assinado, pois o arquivo em questão pode ser completamente colocado dentro do tag que foi identificado no atributo URI do nó <Reference> (no caso do exemplo, o nó <Object>). Arquivos binários precisam ser codificados em Base64 antes de serem adicionados ao nó de assinatura por motivos óbvios, já que o XML-DSIG é um documento XML, que não pode conter conteúdo binário diretamente.

A única desvantagem da assinatura em xml em modalidade ENVELOPING é que se, por exemplo, um XML for assinado desta maneira, ele não poderá ser usado diretamente, porque ele poderá não passar em alguma validação XML que use um esquema XSD, pois este tipo de validação normalmente requer que os nós estejam em uma ordem e posição específicas dentro do arquivo e ao mover o XML (conteúdo assinado) para dentro de outro nó (no caso do exemplo, <Object>), a validação XSD não vai reconhecer o XML como válido. Para resolver este problema o XSD precisaria estar ciente do formato XML-DSIG na modalidade ENVELOPING e assim buscar o conteúdo assinado dentro do nó identificado no atributo URI do nó <Reference>.


Outra forma de não precisar ter que alterar o esquema XSD de validação ou facilitar sua alteração é utilizar a outra modalidade de assinatura em XML. A seguir está um exemplo de uma assinatura da modalidade ENVELOPED:

<NoRaiz>
    <Nome>Carlos</Nome>
    <Profissao>Analista de Sistemas</Profissao>
    <Idade>41</Idade>
    <CidadeNatal>Recife</CidadeNatal>
    <CidadeAtual>Olinda</CidadeAtual>

    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            ...
            <Reference URI="">
                <DigestValue>Digest Aqui</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>Assinatura Aqui</SignatureValue>
        ...
    </Signature> 
</NoRaiz>

Uma assinatura do tipo ENVELOPED gera um documento XML-DSIG injetando dentro do xml original o nó <Signature> com o namespace "http://www.w3.org/2000/09/xmldsig#". O nó <Signature> se torna, portanto, um nó "irmão" do último nó contido no nó de nível superior do xml (nó raiz do xml). Nesta modalidade de assinatura, o nó de nível superior do xml é o nó de nível superior do xml original que foi assinado. No exemplo anterior a parte vermelha é o xml original que foi assinado ao se injetar o nó <Signature>. Note que o atributo URI do nó <Reference> está propositalmente vazio. Isso significa que a assinatura levou em conta o nó raiz do xml, isto é, <NoRaiz>, incluindo todos os seus filhos EXCETO o "filho bastardo" <Signature>, que não faz parte do xml original.  Em um processo de validação, o CryptXML recupera o arquivo original, removendo o nó <Signature> dele e usado o conteúdo deste nó para validar o xml resultante, que agora é o xml original. Isso tudo é feito automaticamente pela API!

Também é permitido nesta modalidade de assinatura, informar um valor no atributo URI, mas se isso for feito é obrigatória a existência de um nó identificado com um atributo ID correspondente. Quando isso é feito, o nó <Signature> diz respeito apenas ao nó identificado e não mais ao nó raiz, a não ser, claro, que o nó identificado seja o nó raiz, mas isso é desnecessário, pois, como já foi dito, quando URI="" implicitamente o nó que contém o conteúdo assinado é o nó raiz do xml, sempre!

Caso ainda não tenha ficado claro pelos parágrafos anteriores, a maior desvantagem da modalidade ENVELOPED é que com ela só é possível assinar documentos xml, já que o nó de assinatura precisa ser injetado em um xml preexistente, o xml original. Apesar desta ser uma desvantagem de fato, ela traz consigo uma vantagem, que é permitir ou pelo menos facilitar que o xml original, mesmo assinado, ainda possa ser usado diretamente, sem necessidade de extração de dados ou decodificação Base64. Em outras palavras, caso o xml não precise de uma validação xsd, ele ainda pode ser carregado por um programa de destino que considere a estrutura original do xml, pois todos os nós estão lá, em suas posições originais, apenas o nó <Signature> foi adicionado ao final e portanto pode ser ignorado no processo sem maiores problemas. Caso uma validação xsd seja necessária, este xsd pode ser facilmente alterado para desconsiderar e permitir qualquer conteúdo adicional após o conteúdo do xml original (xml esperado).


Antes de prosseguir...

Antes de continuar eu devo explicar que eu desenvolvi as funções mostradas aqui, implementando-as dentro do meu framework "Anak Krakatoa Delphi Framework" (trabalho em andamento), que está disponível gratuitamente no SVN do OSDN (https://osdn.net/projects/akdf/scm/svn/). Fiz desta maneira porque em determinado momento eu precisaria realizar a seleção de um certificado digital e o Krakatoa já possui tal função, além disso é natural pra mim que isso fosse feito, já que tanto a função de assinatura, quanto a função de verificação foram incluídas no Krakatoa. As duas funções que eu vou explicar aqui passo-a-passo mais adiante farão referência a tipos, classes e outras funções existentes no Krakatoa, as quais eu não vou explicar o funcionamento. Sintam-se convidados a baixar o framework para estudar estas funções. Caso você opte por fazer isso, antes de compilar os pacotes do Krakatoa, compile e instale os pacotes do PNGComponents, também disponível no OSDN (https://osdn.net/projects/pngcdxme/scm/svn/). Somente após a compilação e instalação dos pacotes do PNGComponents os pacotes do Krakatoa poderão ser compilados e opcionalmente instalados[1].

Função para assinatura digital (XML-DSIG)

Eu acho que eu já falei de mais, então, vamos pôr a mão na massa a partir de agora. Indo direto ao ponto, abaixo está um trecho de código que mostra a função XmlDSigCreate, responsável por criar uma assinatura em xml. Este trecho também apresenta algumas estruturas que são utilizadas tanto pela função que cria a assinatura digital como pela função que valida uma assinatura digital, a qual será explicada na próxima seção deste artigo.

type
  TXDSSignatureType = (stNone, stEnveloped, stEnveloping);
  TXDSCharSet = (csNone, csUTF8, csUTF16LE, csUTF16BE);
  TXDSCanonicalizationMethod = (cmNone, cmC14N, cmC14NC, cmExclusiveC14N, cmExclusiveC14NC);
  TXDSHash = (hNone, hSHA1, hSHA256, hSHA384, hSHA512);
  TXDSAddCertificate = (acNone, acSigner, acChain);

  TXDSCreateArguments = record
    SignatureType: TXDSSignatureType;
    CertificateContext: PCCERT_CONTEXT;
    AddCertificate: TXDSAddCertificate;
    InputFileName: String;
    InputData: PByte;
    InputSize: DWORD;
    InputCharSet: TXDSCharSet;
    OutputCharSet: TXDSCharSet;
    SignatureId: String;
    SignatureLocation: String;
    SignatureCanonicalizationMethod: TXDSCanonicalizationMethod;
    SignatureHash: TXDSHash;
    ReferenceId: String;
    ReferenceUri: String;
    ReferenceCanonicalizationMethod: TXDSCanonicalizationMethod;
    ReferenceHash: TXDSHash;
    KeyInfoId: String;
    AddKeyValue: Boolean;
  end;


  TXDSCreateResults = record
    OutputData: PByte;
    OutputSize: DWORD;
    ErrorCode: HRESULT;
    ErrorMessage: String;
  end;

  TXDSVerifyArguments = record
    InputFileName: String;
    InputData: PByte;
    InputSize: DWORD;
  end;

  TXDSVerifyResults = record
    CertificateContext: PCCERT_CONTEXT;
    ErrorCode: HRESULT;
    ErrorMessage: String;
  end;

function WriteXml(pvCallbackState: PVOID; const pbData: PBYTE; cbData: ULONG): HRESULT; WINAPI;
begin
  AppendBytes(pbData,cbData,TXDSCreateResults(pvCallbackState^).OutputData,TXDSCreateResults(pvCallbackState^).OutputSize);
  TXDSCreateResults(pvCallbackState^).OutputSize := TXDSCreateResults(pvCallbackState^).OutputSize + cbData;
  Result := S_OK;
end;

function XmlDSigCreate(const AArguments: TXDSCreateArguments; out AResults: TXDSCreateResults): Boolean;
const
  ENVELOPED_TRANSFORM: CRYPT_XML_ALGORITHM = (cbSize: SizeOf(CRYPT_XML_ALGORITHM);
                                              wszAlgorithm: wszURI_XMLNS_TRANSFORM_ENVELOPED;
                                              Encoded: (dwCharset: CRYPT_XML_CHARSET_AUTO;
                                                        cbData: 0;
                                                        pbData: nil));
  TRUEBOOL: BOOL = True;
var
  Signature: HCRYPTXML;
  Reference: HCRYPTXML;
  InputFile: CRYPT_XML_BLOB; // *.xml (ENVELOPED), *.* (ENVELOPING)
  CertificateChainContext: PCCERT_CHAIN_CONTEXT;
  MustFreePrivateKey: BOOL;
  PrivateKey: HCRYPTPROV_OR_NCRYPT_KEY_HANDLE;
  PrivateKeyType: DWORD;
  FreeInputFile: Boolean;
// - ///////////////////////////////////////////////////////////////////////////
procedure CleanUp;
begin
  if Assigned(Signature) then
    CryptXmlClose(Signature);

  if FreeInputFile and Assigned(InputFile.pbData) then
    FreeMem(InputFile.pbData);

  if MustFreePrivateKey and (PrivateKey > 0) then
  begin
    if PrivateKeyType = CERT_NCRYPT_KEY_SPEC then
      NCryptFreeObject(PrivateKey)
    else
      CryptReleaseContext(PrivateKey,0)
  end;

  if Assigned(CertificateChainContext) then
    CertFreeCertificateChain(CertificateChainContext);
end;
// - ///////////////////////////////////////////////////////////////////////////
var
  AlgorithmInfo: PCRYPT_XML_ALGORITHM_INFO;
  SignatureFlags: DWORD;
  ChainPara: CERT_CHAIN_PARA;
  SignatureCanonicalizationMethod: CRYPT_XML_ALGORITHM;
  ReferenceCanonicalizationMethod: CRYPT_XML_ALGORITHM;
  SignatureHashAlgorithm: CRYPT_XML_ALGORITHM;
  ReferenceHashAlgorithm: CRYPT_XML_ALGORITHM;
  OIDInformation: PCCRYPT_OID_INFO;
  CNGAlgorithmId: array [0..1] of LPCWSTR;
  EncodeProperties: array [0..0] of CRYPT_XML_PROPERTY;
  ReferenceFlags: DWORD;
  ReferenceUri: PChar;
  TransformationAlgorithms: array [0..1] of CRYPT_XML_ALGORITHM;
  TransformationAlgorithmsCount: Byte;
  BTSP: TBytesToStringParams;
  InputFileBase64: CRYPT_XML_BLOB;
  KeyInfoParam: CRYPT_XML_KEYINFO_PARAM;
  CertificateChain: array[0..9] of CERT_BLOB; // até 10 certificados na cadeia!
  i: Byte;
begin
  Result := False;
  ZeroMemory(@AResults,SizeOf(TXDSCreateResults));

  // Abaixo estão as variáveis que são usadas em CleanUp. Elas precisam ser
  // inicializadas aqui antes de qualquer chamada a CleanUp
  Signature := nil;
  ZeroMemory(@InputFile,SizeOf(CRYPT_XML_BLOB));
  FreeInputFile := False;
  MustFreePrivateKey := False;
  PrivateKey := 0;
  CertificateChainContext := nil;

  if AArguments.OutputCharSet = csNone then
  begin
    AResults.ErrorCode := 1;
    AResults.ErrorMessage := 'XmlDSigCreate: O charset de saída não foi informado';
    CleanUp; // Não vai fazer nada, mas vamos manter o padrão pra deixar claro.
    Exit;
  end;

  if not Assigned(AArguments.CertificateContext) then
  begin
    AResults.ErrorCode := 2;
    AResults.ErrorMessage := 'XmlDSigCreate: Um certificado não foi informado. Não é possível continuar';
    CleanUp; // Não vai fazer nada, mas vamos manter o padrão pra deixar claro.
  end
  else
  begin
    PrivateKeyType := 0;

    if not CryptAcquireCertificatePrivateKey(AArguments.CertificateContext
                                            ,CRYPT_ACQUIRE_CACHE_FLAG
                                            ,nil
                                            ,@PrivateKey
                                            ,@PrivateKeyType
                                            ,@MustFreePrivateKey) then
    begin
      AResults.ErrorCode := HResultFromWin32(GetLastError);
      AResults.ErrorMessage := 'CryptAcquireCertificatePrivateKey: Não foi possível obter a chave privada a partir do certificado escolhido';
      CleanUp;
    end
    else
    begin
      ZeroMemory(@ChainPara,SizeOf(CERT_CHAIN_PARA));
      ChainPara.cbSize := SizeOf(CERT_CHAIN_PARA);

      if not CertGetCertificateChain(0                              // Usar o "chain engine" padrão
                                    ,AArguments.CertificateContext  // Ponteiro para o contexto do certificado final (certificado de assinatura)
                                    ,nil                            // Usar o horário do sistema
                                    ,nil                            // Não buscar certificado em stores adicionais
                                    ,@ChainPara                     // Critérios de pesquisa
                                    ,0                              // Sem checagem de revogação
                                    ,nil                            // Reservado para uso futuro
                                    ,@CertificateChainContext) then // Cadeia de certificados retornada
      begin
        AResults.ErrorCode := HResultFromWin32(GetLastError);
        AResults.ErrorMessage := 'CertGetCertificateChain: Não foi possível obter a cadeia de certificados do certificado escolhido';
        CleanUp;
      end
      else
      begin
        ZeroMemory(@SignatureCanonicalizationMethod,SizeOf(CRYPT_XML_ALGORITHM));
        SignatureCanonicalizationMethod.cbSize := SizeOf(CRYPT_XML_ALGORITHM);

        case AArguments.SignatureCanonicalizationMethod of
          cmC14N: SignatureCanonicalizationMethod.wszAlgorithm := wszURI_CANONICALIZATION_C14N;
          cmC14NC: SignatureCanonicalizationMethod.wszAlgorithm := wszURI_CANONICALIZATION_C14NC;
          cmExclusiveC14N: SignatureCanonicalizationMethod.wszAlgorithm := wszURI_CANONICALIZATION_EXSLUSIVE_C14N;
          cmExclusiveC14NC: SignatureCanonicalizationMethod.wszAlgorithm := wszURI_CANONICALIZATION_EXSLUSIVE_C14NC;
          else
          begin
            AResults.ErrorCode := 3;
            AResults.ErrorMessage := 'XmlDSigCreate: O método de canonicalização da assinatura digital não foi escolhido';
            CleanUp;
            Exit;
          end;
        end;

        ZeroMemory(@SignatureHashAlgorithm,SizeOf(CRYPT_XML_ALGORITHM));
        SignatureHashAlgorithm.cbSize := SizeOf(CRYPT_XML_ALGORITHM);

        ZeroMemory(@CNGAlgorithmId,SizeOf(CNGAlgorithmId));

        OIDInformation := CryptFindOIDInfo(CRYPT_OID_INFO_OID_KEY
                                          ,AArguments.CertificateContext.pCertInfo.SubjectPublicKeyInfo.Algorithm.pszObjId
                                          ,CRYPT_PUBKEY_ALG_OID_GROUP_ID);

        if (not Assigned(OIDInformation)) or (not Assigned(OIDInformation.pwszCNGAlgid)) then
        begin
          AResults.ErrorCode := CRYPT_XML_E_ALGORITHM;
          AResults.ErrorMessage := 'CryptFindOIDInfo: Não foi possível obter informações sobre o algoritmo de chave pública do certificado. O algorítmo especificado não é suportado';
          CleanUp;
          Exit;
        end;

        CNGAlgorithmId[1] := OIDInformation.pwszCNGAlgid;

        case AArguments.SignatureHash of
          hSHA1: CNGAlgorithmId[0] := BCRYPT_SHA1_ALGORITHM;
          hSHA256: CNGAlgorithmId[0] := BCRYPT_SHA256_ALGORITHM;
          hSHA384: CNGAlgorithmId[0] := BCRYPT_SHA384_ALGORITHM;
          hSHA512: CNGAlgorithmId[0] := BCRYPT_SHA512_ALGORITHM;
          else
          begin
            AResults.ErrorCode := 4;
            AResults.ErrorMessage := 'XmlDSigCreate: O algoritmo de hash da assinatura digital não foi informado';
            CleanUp;
            Exit;
          end;
        end;

        AlgorithmInfo := CryptXmlFindAlgorithmInfo(CRYPT_XML_ALGORITHM_INFO_FIND_BY_CNG_SIGN_ALGID
                                                  ,@CNGAlgorithmId
                                                  ,CRYPT_XML_GROUP_ID_SIGN
                                                  ,0);
        if not Assigned(AlgorithmInfo) then
        begin
          AResults.ErrorCode := CRYPT_XML_E_ALGORITHM;
          AResults.ErrorMessage := 'CryptXmlFindAlgorithmInfo: Não foi possível obter o URI correspondente ao algoritmo de assinatura especificado. O algorítmo especificado não é suportado';
          CleanUp;
          Exit;
        end;

        SignatureHashAlgorithm.wszAlgorithm :=  AlgorithmInfo.wszAlgorithmURI;

        ZeroMemory(@ReferenceHashAlgorithm,SizeOf(CRYPT_XML_ALGORITHM));
        ReferenceHashAlgorithm.cbSize := SizeOf(CRYPT_XML_ALGORITHM);

        case AArguments.ReferenceHash of
          hSHA1: CNGAlgorithmId[0] := BCRYPT_SHA1_ALGORITHM;
          hSHA256: CNGAlgorithmId[0] := BCRYPT_SHA256_ALGORITHM;
          hSHA384: CNGAlgorithmId[0] := BCRYPT_SHA384_ALGORITHM;
          hSHA512: CNGAlgorithmId[0] := BCRYPT_SHA512_ALGORITHM;
          else
          begin
            AResults.ErrorCode := 5;
            AResults.ErrorMessage := 'XmlDSigCreate: O algoritmo de hash do resumo criptográfico (digest) da referência não foi informado';
            CleanUp;
            Exit;
          end;
        end;

        AlgorithmInfo := CryptXmlFindAlgorithmInfo(CRYPT_XML_ALGORITHM_INFO_FIND_BY_CNG_ALGID
                                                  ,CNGAlgorithmId[0]
                                                  ,CRYPT_XML_GROUP_ID_HASH
                                                  ,0);

        if not Assigned(AlgorithmInfo) then
        begin
          AResults.ErrorCode := CRYPT_XML_E_ALGORITHM;
          AResults.ErrorMessage := 'CryptXmlFindAlgorithmInfo: Não foi possível obter o URI correspondente ao algoritmo de digest especificado. O algorítmo especificado não é suportado';
          CleanUp;
          Exit;
        end;

        ReferenceHashAlgorithm.wszAlgorithm := AlgorithmInfo.wszAlgorithmURI;

        case AArguments.InputCharSet of
          csNone: InputFile.dwCharset := CRYPT_XML_CHARSET_AUTO;
          csUTF8: InputFile.dwCharset := CRYPT_XML_CHARSET_UTF8;
          csUTF16LE: InputFile.dwCharset := CRYPT_XML_CHARSET_UTF16LE;
          csUTF16BE: InputFile.dwCharset := CRYPT_XML_CHARSET_UTF16BE;
        end;

        if (AArguments.InputFileName <> '') and (FileExists(AArguments.InputFileName)) then
        begin
          LoadFile(AArguments.InputFileName,InputFile.pbData,InputFile.cbData);
          FreeInputFile := True;
        end
        else if Assigned(AArguments.InputData) and (AArguments.InputSize > 0) then
        begin
          InputFile.pbData := AArguments.InputData;
          InputFile.cbData := AArguments.InputSize;
        end
        else
        begin
          AResults.ErrorCode := 6;
          AResults.ErrorMessage := 'XmlDSigCreate: Não foi informado um arquivo de entrada ou dados a serem assinados. Não é possível continuar';
          CleanUp;
          Exit;
        end;

        ZeroMemory(@TransformationAlgorithms,SizeOf(TransformationAlgorithms));
        TransformationAlgorithmsCount := 0;

        ReferenceUri := '';

        case AArguments.SignatureType of
          stEnveloped: begin
            if InputFile.dwCharset = CRYPT_XML_CHARSET_AUTO then
            begin
              AResults.ErrorCode := 7;
              AResults.ErrorMessage := 'XmlDSigCreate: O charset do xml de entrada não foi informado. Não é possível continuar';
              CleanUp;
              Exit;
            end;

            if AArguments.SignatureLocation <> '' then
            begin
              ZeroMemory(@EncodeProperties,SizeOf(EncodeProperties));

              EncodeProperties[0].dwPropId := CRYPT_XML_PROPERTY_SIGNATURE_LOCATION;
              EncodeProperties[0].pvValue := @AArguments.SignatureLocation;
              EncodeProperties[0].cbValue := SizeOf(LPCWSTR);
            end
            else
            begin
              AResults.ErrorCode := 8;
              AResults.ErrorMessage := 'XmlDSigCreate: Não foi informado o nó onde o nó de assinatura deverá ser incluído. Não é possível continuar';
              CleanUp;
              Exit;
            end;

            AResults.ErrorCode := CryptXmlOpenToEncode(nil
                                                      ,0
                                                      ,PChar(AArguments.SignatureId)
                                                      ,@EncodeProperties
                                                      ,1
                                                      ,@InputFile
                                                      ,@Signature);

            if Failed(AResults.ErrorCode) then
            begin
              AResults.ErrorMessage := 'CryptXmlOpenToEncode: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
              CleanUp;
              Exit;
            end;

            ReferenceFlags := 0;

            if AArguments.ReferenceUri <> '' then
              ReferenceUri := PChar('#' + AArguments.ReferenceUri);

            TransformationAlgorithms[TransformationAlgorithmsCount] := ENVELOPED_TRANSFORM;
            Inc(TransformationAlgorithmsCount);
          end;
          stEnveloping: begin
            if AArguments.ReferenceUri = '' then
            begin
              AResults.ErrorCode := 9;
              AResults.ErrorMessage := 'XmlDSigCreate: Não foi informado o URI da referência. Ele é obrigatório no formato ENCODING. Não é possível continuar';
              CleanUp;
              Exit;
            end;

            AResults.ErrorCode :=  CryptXmlOpenToEncode(nil
                                                       ,0
                                                       ,PChar(AArguments.SignatureId)
                                                       ,nil
                                                       ,0
                                                       ,nil
                                                       ,@Signature);

            if Failed(AResults.ErrorCode) then
            begin
              AResults.ErrorMessage := 'CryptXmlOpenToEncode: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
              CleanUp;
              Exit;
            end;

            ReferenceFlags := CRYPT_XML_FLAG_CREATE_REFERENCE_AS_OBJECT;

            ReferenceUri := PChar('#' + AArguments.ReferenceUri);
          end;
          else
          begin
            AResults.ErrorCode := 10;
            AResults.ErrorMessage := 'XmlDSigCreate: O tipo de assinatura a realizar não foi informado';
            CleanUp;
            Exit;
          end
        end;

        ZeroMemory(@ReferenceCanonicalizationMethod,SizeOf(CRYPT_XML_ALGORITHM));
        ReferenceCanonicalizationMethod.cbSize := SizeOf(CRYPT_XML_ALGORITHM);

        case AArguments.ReferenceCanonicalizationMethod of
          cmC14N: ReferenceCanonicalizationMethod.wszAlgorithm := wszURI_CANONICALIZATION_C14N;
          cmC14NC: ReferenceCanonicalizationMethod.wszAlgorithm := wszURI_CANONICALIZATION_C14NC;
          cmExclusiveC14N: ReferenceCanonicalizationMethod.wszAlgorithm := wszURI_CANONICALIZATION_EXSLUSIVE_C14N;
          cmExclusiveC14NC: ReferenceCanonicalizationMethod.wszAlgorithm := wszURI_CANONICALIZATION_EXSLUSIVE_C14NC;
        end;

        if AArguments.ReferenceCanonicalizationMethod in [cmC14N,cmC14NC,cmExclusiveC14N,cmExclusiveC14NC] then
        begin
          TransformationAlgorithms[TransformationAlgorithmsCount] := ReferenceCanonicalizationMethod;
          Inc(TransformationAlgorithmsCount);
        end;

        Reference := nil;

        AResults.ErrorCode := CryptXmlCreateReference(Signature
                                                     ,ReferenceFlags
                                                     ,PChar(AArguments.ReferenceId)
                                                     ,ReferenceUri
                                                     ,nil
                                                     ,@ReferenceHashAlgorithm
                                                     ,TransformationAlgorithmsCount
                                                     ,@TransformationAlgorithms
                                                     ,@Reference);

        if Failed(AResults.ErrorCode) then
        begin
          AResults.ErrorMessage := 'CryptXmlCreateReference: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
          CleanUp;
          Exit;
        end;

        if AArguments.SignatureType = stEnveloping then
        begin
          BTSP.StringFormat := sfBase64;
          BTSP.Bytes := InputFile.pbData;
          BTSP.Size := InputFile.cbData;

          ZeroMemory(@InputFileBase64,SizeOf(CRYPT_XML_BLOB));

          {$IFDEF UNICODE}
          InputFileBase64.dwCharset := CRYPT_XML_CHARSET_UTF16LE;
          {$ELSE}
          InputFileBase64.dwCharset := CRYPT_XML_CHARSET_UTF8;
          {$ENDIF}

          StringToBinary('<Base64 xmlns="http://www.w3.org/2000/09/xmldsig#">' + BytesToString(BTSP) + '</Base64>',InputFileBase64.pbData,InputFileBase64.cbData);
          try
            AResults.ErrorCode := CryptXmlAddObject(Reference
                                                   ,CRYPT_XML_FLAG_ADD_OBJECT_CREATE_COPY
                                                   ,nil
                                                   ,0
                                                   ,@InputFileBase64
                                                   ,nil);

            if Failed(AResults.ErrorCode) then
            begin
              AResults.ErrorMessage := 'CryptXmlCreateReference: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
              CleanUp;
              Exit;
            end;
          finally
            FreeMem(InputFileBase64.pbData);
          end;
        end;

        SignatureFlags := 0;
        if AArguments.AddKeyValue then
            SignatureFlags := CRYPT_XML_SIGN_ADD_KEYVALUE;

        ZeroMemory(@CertificateChain,SizeOf(CertificateChain));

        ZeroMemory(@KeyInfoParam,SizeOf(CRYPT_XML_KEYINFO_PARAM));

        if AArguments.KeyInfoId <> '' then
          KeyInfoParam.wszId := PChar(AArguments.KeyInfoId);

        KeyInfoParam.rgCertificate := @CertificateChain;

        if AArguments.AddCertificate = acSigner then
          KeyInfoParam.cCertificate := 1
        else if AArguments.AddCertificate = acChain then
          KeyInfoParam.cCertificate := Min(Length(CertificateChain),CertificateChainContext.rgpChain^.cElement);

        if AArguments.AddCertificate in [acSigner,acChain] then
          for i := 0 to Pred(KeyInfoParam.cCertificate) do
          begin
            // É meio confusa a forma de incrementar o ponteiro dessa forma,
            // porém isso economizou uma variável
            CertificateChain[i].cbData := PCERT_CHAIN_ELEMENT(DWORD(CertificateChainContext.rgpChain^.rgpElement^) + SizeOf(CERT_CHAIN_ELEMENT) * i).pCertContext.cbCertEncoded;
            CertificateChain[i].pbData := PCERT_CHAIN_ELEMENT(DWORD(CertificateChainContext.rgpChain^.rgpElement^) + SizeOf(CERT_CHAIN_ELEMENT) * i).pCertContext.pbCertEncoded
          end;

        AResults.ErrorCode := CryptXmlSign(Signature
                                          ,PrivateKey
                                          ,PrivateKeyType
                                          ,SignatureFlags
                                          ,CRYPT_XML_KEYINFO_SPEC_PARAM
                                          ,@KeyInfoParam
                                          ,@SignatureHashAlgorithm
                                          ,@SignatureCanonicalizationMethod);

        if Failed(AResults.ErrorCode) then
        begin
          AResults.ErrorMessage := 'CryptXmlSign: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
          CleanUp;
          Exit;
        end;

        ZeroMemory(@EncodeProperties,SizeOf(EncodeProperties));
        EncodeProperties[0].dwPropId := CRYPT_XML_PROPERTY_DOC_DECLARATION;
        EncodeProperties[0].pvValue := @TRUEBOOL;
        EncodeProperties[0].cbValue := SizeOf(BOOL);

        AResults.ErrorCode := CryptXmlEncode(Signature
                                            ,CRYPT_XML_CHARSET(AArguments.OutputCharSet)
                                            ,@EncodeProperties
                                            ,1
                                            ,@AResults
                                            ,WriteXml);

        if Failed(AResults.ErrorCode) then
        begin
          AResults.ErrorMessage := 'CryptXmlEncode: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
          CleanUp;
          Exit;
        end;
      end;
    end;

    CleanUp;
    Result := True;
  end;
end;

No código acima, as linhas 2 a 6 declaram 5 enumerações necessárias para o funcionamento das funções para criação e verificação da assinatura em XML (XML-DSIG). TXDSSignatureType define os dois valores possíveis para o tipo de assinatura, que pode ser Enveloped ou Enveloping. TXDSCharSet define os conjuntos de caracteres que as funções suportam, os quais podem ser UTF-8, UTF-16 Little Endian e UTF-16 Big Endian. Os membros válidos desta enumeração tem os mesmos valores dos membros válidos de CRYPT_XML_CHARSET localizado em KRK.Xml.CryptXml.pas, portanto eles podem ser convertidos entre si com simples casts. TXDSCanonicalizationMethod define os métodos de canonicalização que as funções suportam, tal como descrito em https://www.w3.org/TR/xmldsig-core1/#sec-AlgID. Os métodos de canonicalização são algoritmos usados para normalizar o XML e tornar possível a assinatura digital nesse formato. Felizmente basta apenas selecionar um algoritmo para que a função de criação de assinatura digital faça todo trabalho que isso implica. TXDSHash define os algoritmos de hash que a função de criação de XML-DSIG suporta, os quais são SHA-1, SHA-256, SHA-384 e SHA-512. Por fim, TXDSAddCertificate, define as opções de adição de certificados juntamente com o XML-DSIG e as 3 opções são válidas, isto é, é possível não adicionar nenhum certificado (acNone), apenas o certificado do assinador (acSigner) ou a cadeia de certificação completa (acChain).

As linhas 8 a 27 definem o record TXDSCreateArguments, que, como o nome sugere, contém todos os argumentos que são passados na função de criação do XML-DSIG (XmlDSigCreate). Os membros desse record são:


As linhas 30 a 35 definem o record TXDSCreateResults, que, como o nome sugere, contém o resultado da execução da função de criação do XML-DSIG (XmlDSigCreate). Os membros desse record são:

As linhas 43 a 47 definem o record TXDSVerifyResults, que, como o nome sugere, contém o resultado da execução da função de verificação do XML-DSIG (XmlDSigVerify). Os membros desse record são:

As linhas 49 a 54 definem uma função de callback usada para consolidar o resultado da criação da assinatura digital. Esta função, usando métodos especiais, preenche os membros OutputData e OutputSize gradativamente até que eles representem o XML-DSIG completo, o qual poderá ser salvo posteriormente.

A linha 56 inicia inicia a definição da função XmlDSigCreate, a qual será brevemente explanada a partir daqui. As linhas 74 a 92 definem o subprocedure CleanUp, utilizado em vários pontos do código de XmlDSigCreate para limpar variáveis criadas. As linhas 127 a 140 possuem uma verificação básica de parâmetros obrigatórios iniciais.


Na linha 145 tentamos obter a chave privada e o tipo da chave do certificado. Neste ponto, ainda não há solicitação de senha ou pin. Isso só será solicitado no  momento em que a chave privada for efetivamente usada, isto é, ao chamar a função de assinatura (CryptXmlSign). Caso a chave privada tenha sido obtida com sucesso, o fluxo segue para a linha 158, onde PrivateKey e PrivateKeyType já estão preenchidas. Nesta mesma linha há a inicialização da variável ChainPara, que serve para configurar os critérios de pesquisa que CertGetCertificateChain (mais adiante) usa para encontrar os certificados da cadeia de certificados. Aqui, não adicionamos nenhum critério especial, tudo está no padrão, que é suficiente.

Na linha 161, a execução da função CertGetCertificateChain constrói a cadeia de certificados sem considerar status de revogação. Aqui poderia ser aplicada alguma regra para impedir a assinatura caso algum certificado da cadeia não seja válido dentro de algum critério. Aqui optei por não limitar de forma algum, ou seja, a assinatura será feita, independentemente do estado do certificado usado e sua cadeia. Caso a construção da cadeia de certificação tenha sido bem sucedida, o fluxo segue para a linha 176, onde já temos todas as informações necessárias que foram extraídas do certificado de assinatura, ou seja, CertificateChainContext, além de PrivateKey e PrivateKeyType já mencionadas anteriormente. Nesta mesma linha há a inicialização da variável SignatureCanonicalizationMethod, responsável por definir o método de canonicalização da assinatura digital. Isso consiste em complementar a definição da variável SignatureCanonicalizationMethod, selecionado o algoritmo adequado de acordo com o argumento escolhido. As linhas 179 a 191 lidam com a escolha feita pelo usuário acerca de qual método utilizar.

Na linha 193 estamos configurando o algoritmo de hash da assinatura digital. Isso consiste em complementar a definição da variável SignatureHashAlgorithm, selecionando o algoritmo adequado de acordo com o argumento escolhido  e com o algoritmo de chave pública do certificado selecionado anteriormente. Em suma, esta variável depende da escolha do usuário e daquilo que o certificado permite, mais especificamente, depende do algoritmo da chave pública do certificado escolhido.

Na linha 196 foi usado SizeOf(CNGAlgorithmId) para que o tamanho total do array de duas posições seja usado. No caso, como temos um array de duas posições onde cada uma delas é um ponteiro, o valor retornado será 8, que está correto (dois ponteiros de 4 bytes cada).

Nas linhas 198 a 210 obtemos o nome do algoritmo da chave pública do certificado de assinatura. Já nas linhas 212 a 238 finalmente a variável SignatureHashAlgorithm é complementada, informando em seu membro wszAlgorithm o nome do algoritmo de chave pública que foi obtido nos passos anteriores.

As linhas 240 a 270 configuram o algoritmo de hash do resumo criptográfico (digest) da referência. Isso consiste em complementar a definição da variável ReferenceHashAlgorithm, selecionando o algoritmo adequado de acordo com o argumento escolhido. A forma de seleção deste algoritmo é muito similar ao que foi explicado anteriormente para a complementação da variável SignatureHashAlgorithm, portanto, não vou detalhar esse procedimento novamente.

As linhas 272 a 295 normalizam o arquivo de entrada, de forma que depois desse bloco de código apenas precisemos trabalhar com um arquivo em formato binário na variável InputFile. Em outras palavras, esta variável fará referência ao conteúdo binário do arquivo e portanto não há como saber de forma genérica qual a codificação (charset) do arquivo ali representado. O membro dwCharset de InputFile só fará diferença no modo de assinatura ENVELOPED, pois é necessário saber com exatidão a codificação do XML de entrada (neste tipo de assinatura apenas XMLs são aceitos). No modo ENVELOPING, como o arquivo será criptografado em Base64, sua codificação é irrelevante e deve ser informado como csNone em AArguments.InputCharSet.


Na linha 297, TransformationAlgorithms é um array de duas posições onde uma destas posições vai ser usada pelo algoritmo de transformação enveloped, aplicado ao tag <Reference>,  quando o tipo de assinatura escolhido tiver sido ENVELOPED e a outra posição será opcionalmente usada por um algoritmo de canonicalização a ser usado, também, no tag <Reference> (AArguments.ReferenceCanonicalizationMethod). Tanto a indicação do algoritmo de transformação enveloped quanto o algoritmo de canonicalização são informados dentro de tags <Transform>, como filhos de <Reference>, por isso, o nome da variável (TransformationAlgorithms) faz sentido. A variável TransformationAlgorithmsCount, na linha 298, mantém um registro da quantidade de algoritmos que o array TransformationAlgorithms contém, e que atualmente só pode receber 0, 1 ou 2.

A linha 302 inicia um grande case que vai realizar tarefas de acordo com o tipo de assinatura definido em AArguments.SignatureType. O primeiro tipo a ser considerado é o tipo stEnveloped, que representa o tipo de assinatura ENVELOPED, onde apenas arquivos XML podem ser assinados, pois <Signature> será injetado neste XML como irmão do último nó filho de um nó específico. Neste modo é necessário informar o nó, dentro do xml de entrada, onde será adicionado o nó <Signature>. AArguments.SignatureLocation pode receber o ID de um nó dentro do xml de entrada no formato "#id" ou um XPath indicando este nó. O CryptXML automaticamente incluirá dentro do nó especificado o nó <Signature>, de forma que ele seja o seu último nó filho.

As linhas 304 a 310 validam a codificação informada para o arquivo de entrada. Como na assinatura do tipo ENVELOPED esta informação é requerida, é necessário verificar se ela foi informada. Mas adiante, nas linhas 312 a 326, é feita outra validação para saber se foi informado o local da assinatura, o local onde o tag <Signature> precisa ser injetado. Essa informação é obrigatória na assinatura do tipo ENVELOPED.

Na linha 314 estamos inicializando a variável EncodeProperties, um array com apenas um elemento. Pode parecer desnecessário inicializar a memória desse array aqui, já que ele possui apenas um elemento e todos os membros deste elemento serão preenchidos, porém, por algum motivo que eu ignoro, caso a memória do array não seja inicializada desta forma, a função CryptXmlOpenToEncode mais adiante não funciona de jeito nenhum, retornando o código de erro para "parâmetro incorreto". Eu imagino que isso tenha algo a ver com a forma como as funções de API lidam com estruturas de dados passados em seus parâmetros como ponteiros. Ao zerar a variável que contém a estrutura, é garantido que todos os seus Bytes não usados sejam sempre zero, satisfazendo assim tais funções de API. Este comportamento "xiita" é característica apenas das funções de API usadas aqui. As rotinas pascal que usam estruturas, lidam com elas de forma mais "relaxada", porém igualmente correta e, no meu entendimento, de forma mais lógica!

Na linha 328, a função CryptXmlOpenToEncode cria o elemento de assinatura <Signature> com o id indicado em AArguments.SignatureId, utilizando as propriedades incluídas no array EncodeProperties, no arquivo xml contido em InputFile. No final da execução, a variável Signature conterá um ponteiro para o elemento de assinatura, o qual pode ser usado em várias outras funções CryptXML. EncodeProperties é um array estático de propriedades, que no caso só tem um elemento. Esta variável é passada para a função CryptXMLOpenToEncode como um ponteiro e, a título de curiosidade, o endereço de um array estático é o endereço de seu primeiro elemento. CryptXmlOpenToEncode pode falhar quando em EncodeProperties.pvValue não existe o indicador do nó onde deve ser injetado o nó <Signature> ou quando aquilo que foi informado não existe no xml de entrada. Se você estiver se perguntando porque eu usei um array de uma posição ao invés de usar uma variável simples, a explicação é também simples: para deixar claro que ali podem ser usadas mais de uma propriedade e que para isso basta usar um array estático com mais posições, indicando no parâmetro cProperty (de CryptXmlOpenToEncode)  a quantidade de elementos neste array que contém propriedades válidas, em suma, é apenas para que eu me lembre no futuro que isso é possível.


No formato ENVELOPED, AArguments.ReferenceUri não é obrigatório e pode ser deixado em branco (uma string vazia), porém, caso ele seja informado, é necessário prefixá-lo com #. E é exatamente isso que está sendo feito nas linhas 345 e 346. Já no formato ENVELOPING, AArguments.ReferenceUri é obrigatório, sendo assim, nas linhas 352 a 358, validamos este parâmetro.

Na linha 360 estamos executando CryptXmlOpenToEncode para criar o elemento de assinatura (<Signature>). Quando usamos o formato ENVELOPING, a criação do elemento de assinatura é "pura", isto é, CryptXmlOpenToEncode vai criar apenas o elemento de assinatura e a variável Signature conterá a referência a este elemento. Note que não usamos InputFile aqui pois no formato ENVELOPING ele pode ser qualquer arquivo, inclusive um arquivo binário. Também observe que não usamos EncodeProperties, pois estas propriedades, na implementação atual, indicam o nó dentro do xml indicado em InputFile, onde o nó <Signature> será incluído. Como InputFile pode não ser um xml, então EncodeProperties também não está sendo usado, pois não faz sentido no modo ENVELOPING.

Na linha 375, no modo ENVELOPING, usamos este flag para indicar que ao criar a referência (tag <Reference>), um nó <Object> precisa ser criado automaticamente dentro de <Signature>. A referência será criada em <SignedInfo> e apontará, por meio do atributo URI, ao nó <Object>. Já na linha 377, como AArguments.ReferenceUri é obrigatório, concatena-se aquilo que foi informado com "#" a fim de montar o URI como o XML-DSIG requer. Se o fluxo chegar neste ponto, AArguments.ReferenceUri conterá um valor, pois isso já foi validado nas linhas 352 a 358.

Na linha 388 é garantida a existência de um elemento de assinatura digital (<Signature>) na variável Signature, que fará referência apenas ao elemento <Signature> (ENVELOPING) ou ao xml de entrada completo com o elemento <Signature> adicionado (ENVELOPED). Nesta linha, e até a linha 402 estamos configurando o método de canonicalização da referência. Isso consiste em complementar a definição da variável ReferenceCanonicalizationMethod, selecionado o algoritmo adequado de acordo com o argumento escolhido. Note que esta variável é atribuída à segunda posição do array TransformationAlgorithms, o qual já foi mencionado anteriormente e cuja primeira posição foi atribuída na linha 297.

Na linha 406, finalmente é criada a referência. Grosso modo, cria um handle para um nó <Reference>, o qual contém as configurações de assinatura daquilo que efetivamente será assinado. Após a execução bem sucedida da função CryptXmlCreateReference, a variável Reference conterá este handle.

Nas linhas 423 a 455 realizamos os procedimentos necessários para utilizar a função CryptXmlAddObject para adicionar o conteúdo do objeto quando o tipo de assinatura é ENVELOPING. Neste formato de assinatura, o arquivo carregado em InputFile será adicionado ao objeto criado automaticamente por CryptXmlCreateReference, a qual foi executada com o flag CRYPT_XML_FLAG_CREATE_REFERENCE_AS_OBJECT. InputFile será adicionado codificado em Base64 em um nó <Base64>, contido dentro do nó <Object>. O nó <Base64> não faz parte da especificação XML-DSIG, ele é apenas um artifício utilizado para permitir que se possa adicionar um texto (Base64) ao nó <Object>, pois na implementação CryptXML este nó só aceita xml dentro de si. Na linha 429, podemos ver novamente algo que parece totalmente desnecessário, mas se essa limpeza não for feita, mais adiante, a função CryptXmlSign falha com o clássico erro de "parâmetro incorreto". Nas linhas 431 a 435 estamos configurando InputFileBase64.dwCharset, o qual precisa corresponder àquilo que está em InputFileBase64.pbData. Se o conteúdo for ANSI, usa UTF8, se for UNICODE, usa utf-16 LE (padrão do Windows e do Delphi). Como o conteúdo será um Base64, usar UTF-8 quando não for UNICODE não causará problemas, porque o conteúdo em Base64 não contém caracteres ambíguos entre ANSI e UTF8, o que os torna iguais nesse caso específico. A linha 437 cria em formato binário uma string que contém a codificação em Base64 dentro do nó <Base64>. As linhas 438 a 454, finalmente, adicionam o conteúdo XML de InputFileBase64 no nó <Object>.


A partir da linha 457 iniciamos os procedimentos finais para a assinatura digital usando a função CryptXmlSign. Primeiramente configuramos a variável SignatureFlags, que atualmente, opcionalmente, só recebe um valor possível mediante a escolha do usuário. Quando AArguments.AddKeyValue é configurado como true o flag CRYPT_XML_SIGN_ADD_KEYVALUE será adicionado e isso instrui a função CryptXmlSign a incluir no XML-DSIG um tag <KeyValue>, que conterá uma chave pública.

Na linha 461 estamos inicializando a variável CertificateChain, que é um array de 10 posições onde cada posição é um CERT_BLOB. Esta variável serve para armazenar os certificados da cadeia de certificação que constituem o certificado que for utilizado para realizar a assinatura digital.

Na linha 463, zeramos a variável KeyInfoParam e nas linhas 465 a 468 estamos preenchendo esta variável, primeiro, opcionalmente, atribuímos a KeyInfoParam.wszId um id informado pelo usuário, o qual será aplicado ao elemento <KeyInfo> dentro do XML-DSIG final, depois, atribuímos a KeyInfoParam.rgCertificate um ponteiro para a variável CertificateChain. KeyInfoParam.rgCertificate aponta para CertificateChain porque a função CryptXmlSign vai precisar conhecer os certificados da cadeia de certificação por meio de KeyInfoParam, que é passado por parâmetro para esta função.

Nas linhas 470 a 473, de acordo com a opção de adição de certificados, devemos adicionar todos os certificados (cadeia de certificação) ou apenas o certificado final (certificado do assinador). Ao usar a cadeia de certificados, a quantidade de certificados a adicionar na assinatura será a quantidade de certificados na cadeia de certificados desde que a quantidade não exceda 10 certificados, pois a variável CertificateChain só tem 10 posições. KeyInfoParam.cCertificate indica quantos certificados existem em KeyInfoParam.rgCertificate, logo, este valor será a quantidade de certificados na cadeia de certificação até o limite de 10. A quantidade de certificados na cadeia de certificação é variável, mas para facilitar o código eu utilizei um array de 10 posições fixas pois imagino que dificilmente haja um certificado com mais de 10 níveis na cadeia de certificação.

A variável CertificateChainContext, obtida na linha 161 e cuja obtenção foi explicada anteriormente neste artigo, contém os contextos com todas as informações de todos os certificados da cadeia de certificação, entretanto, o array CertificateChain só precisa dos dados binários de cada um dos certificados, por isso, nas linhas 475 a 482, estamos varrendo os contextos existentes em CertificateChainContext e colocando os dados binários dos certificados em itens do array CertificateChain.

Todos os certificados da cadeia de certificação sempre serão adicionados ao array CertificateChain, porém eles só serão incluídos no XML-DSIG se AArguments.AddCertificate = acChain. Note nas linhas linhas 470 a 473 que quando AArguments.AddCertificate = acSigner, KeyInfoParam.cCertificate recebe o valor 1, o que significa que não importa quantos certificados existam em CertificateChain, apenas 1 deles será incluído no XML-DSIG, no caso, CertificateChain[0], o qual é o certificado do assinador, sempre!

Finalmente, na linha 484 estamos executando a função CryptXmlSign, que vai efetivamente criar o documento XML-DSIG de acordo com todos os parâmetros especificados anteriormente. Entretanto, mesmo quando esta função é bem sucedida, nenhum arquivo físico será gerado, o documento encontra-se na memória, acessível apenas por meio da variável Signature. Para gerar o arquivo físico, usamos a função CryptXmlEncode.


Nas linhas 500 a 503, estamos inicializando e configurando a variável EncodeProperties, que já foi usada anteriormente em outra ocasião. Aqui ela vai ser configurada de forma a indicar à função CryptXmlEncode que é necessário incluir o cabeçalho xm no arquivo final. Isso é conseguido atribuindo CRYPT_XML_PROPERTY_DOC_DECLARATION a dwPropId.

Na linha 505 há a execução de CryptXmlEncode. Seu primeiro argumento é a variável Signature, que contém a referência ao XML-DSIG na memória. No segundo argumento é informado qual a codificação pretendida para o arquivo final gerado. No terceiro e quarto argumentos, respectivamente, são informadas as propriedades de codificação, o array EncodeProperties, e quantas propriedades válidas existem neste array, que no caso é 1. O quinto argumento recebe um ponteiro para a estrutura de retorno da função XmlDSigCreate e no sexto argumento informamos o nome de uma função de callback que será responsável por montar o arquivo final, a função WriteXml. O quinto argumento ficará disponível dentro da função de callback por meio do argumento pvCallbackState de WriteXml. Os membros OutputData e OutputSize de AResults serão gradativamente preenchidos pela função de callback e poderão ser usados posteriormente para montar o arquivo XML-DSIG final.

Função para verificação da assinatura digital (XML-DSIG)

Abaixo está um trecho de código que mostra a função XmlDSigVerify, responsável por validar uma assinatura em xml (XML-DIG). Este trecho fará referência a estruturas que foram explicadas na seção anterior.

function XmlDSigVerify(const AArguments: TXDSVerifyArguments; out AResults: TXDSVerifyResults): Boolean;
var
  InputFile: CRYPT_XML_BLOB;
  FreeInputFile: Boolean;
  DocumentContextHandle: HCRYPTXML;
  KeyHandle: BCRYPT_KEY_HANDLE;
// - ///////////////////////////////////////////////////////////////////////////
procedure CleanUp;
begin
  if Assigned(DocumentContextHandle) then
    CryptXmlClose(DocumentContextHandle);

  if FreeInputFile and Assigned(InputFile.pbData) then
    FreeMem(InputFile.pbData);

  if KeyHandle > 0 then
    BCryptDestroyKey(KeyHandle);

  // Quando Result for false, significa que CleanUp foi chamada a partir de
  // algum erro, logo, apenas neste caso libera o contexto do certificado, caso
  // haja um contexto já obtido previamente
  if (not Result) and Assigned(AResults.CertificateContext) then
  begin
    CertFreeCertificateContext(AResults.CertificateContext);
    AResults.CertificateContext := nil;
  end;
end;
// - ///////////////////////////////////////////////////////////////////////////
var
  DocumentContext: PCRYPT_XML_DOC_CTXT;
  Signature: PCRYPT_XML_SIGNATURE; // array de elementos <Signature>
  KeyInfo: PCRYPT_XML_KEY_INFO;
  KeyInfoItem: PCRYPT_XML_KEY_INFO_ITEM; // array de elementos filhos de <KeyInfo>
  i: Byte;
  X509DataItem: PCRYPT_XML_X509DATA_ITEM; // array de elementos filhos de <X509Data>
  j: Byte;
  Reference: PCRYPT_XML_REFERENCE;
  Status: CRYPT_XML_STATUS;
begin
  Result := False;
  ZeroMemory(@AResults,SizeOf(TXDSVerifyResults));

  ZeroMemory(@InputFile,SizeOf(CRYPT_XML_BLOB));
  DocumentContextHandle := nil;
  FreeInputFile := False;
  KeyHandle := 0;

  if (AArguments.InputFileName <> '') and (FileExists(AArguments.InputFileName)) then
  begin
    LoadFile(AArguments.InputFileName,InputFile.pbData,InputFile.cbData);
    FreeInputFile := True;
  end
  else if Assigned(AArguments.InputData) and (AArguments.InputSize > 0) then
  begin
    InputFile.pbData := AArguments.InputData;
    InputFile.cbData := AArguments.InputSize;
  end
  else
  begin
    AResults.ErrorCode := 1;
    AResults.ErrorMessage := 'XmlDSigVerify: Não foi informado um arquivo de entrada ou dados a serem validados. Não é possível continuar';
    CleanUp;
    Exit;
  end;

  AResults.ErrorCode := CryptXmlOpenToDecode(nil
                                            ,0
                                            ,nil
                                            ,0
                                            ,@InputFile
                                            ,@DocumentContextHandle);

  if Failed(AResults.ErrorCode) then
  begin
    CleanUp;
    AResults.ErrorMessage := 'CryptXmlOpenToDecode: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
    Exit;
  end;

  DocumentContext := nil;

  AResults.ErrorCode := CryptXmlGetDocContext(DocumentContextHandle
                                             ,@DocumentContext);

  if Failed(AResults.ErrorCode) then
  begin
    CleanUp;
    AResults.ErrorMessage := 'CryptXmlGetDocContext: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
    Exit;
  end;

  if DocumentContext.cSignature = 0 then
  begin
    AResults.ErrorCode := 2;
    AResults.ErrorMessage := 'XmlDSigVerify: O documento não possui um elemento <Signature>';
    CleanUp;
    Exit;
  end;

  Signature := DocumentContext.rgpSignature^;

  KeyInfo := Signature.pKeyInfo;

  i := 0;

  repeat
    KeyInfoItem := PCRYPT_XML_KEY_INFO_ITEM(Integer(KeyInfo.rgKeyInfo) + SizeOf(CRYPT_XML_KEY_INFO_ITEM) * i);

    if KeyInfoItem.dwType = CRYPT_XML_KEYINFO_TYPE_X509DATA then
    begin
      j := 0;

      repeat
        X509DataItem := PCRYPT_XML_X509DATA_ITEM(Integer(KeyInfoItem.X509Data.rgX509Data) + SizeOf(CRYPT_XML_X509DATA_ITEM) * j);

        if X509DataItem.dwType = CRYPT_XML_X509DATA_TYPE_CERTIFICATE then
        begin
          AResults.CertificateContext := CertCreateCertificateContext(X509_ASN_ENCODING
                                                                     ,X509DataItem.Certificate.pbData
                                                                     ,X509DataItem.Certificate.cbData);

          if Assigned(AResults.CertificateContext) then
          begin
            if CryptImportPublicKeyInfoEx2(X509_ASN_ENCODING
                                          ,@AResults.CertificateContext.pCertInfo.SubjectPublicKeyInfo
                                          ,CRYPT_OID_INFO_PUBKEY_SIGN_KEY_FLAG
                                          ,nil
                                          ,KeyHandle) then
            begin
              Break;
            end
          end;
        end;

        inc(j);
      until j = KeyInfoItem.X509Data.cX509Data;

      if KeyHandle > 0 then
        Break;
    end;

    inc(i);
  until i = KeyInfo.cKeyInfo;

  if KeyHandle = 0 then
  begin
    AResults.ErrorCode := CRYPT_XML_E_SIGNER;
    AResults.ErrorMessage := 'XmlDSigVerify: Não foi possível obter a chave pública do assinador';
    CleanUp;
    Exit;
  end;

  AResults.ErrorCode := CryptXmlVerifySignature(DocumentContext.rgpSignature^.hSignature
                                               ,KeyHandle
                                               ,0);
  if Failed(AResults.ErrorCode) then
  begin
    CleanUp;
    AResults.ErrorMessage := 'CryptXmlVerifySignature: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar';
    Exit;
  end;

  i := 0;

  repeat
    Reference := PCRYPT_XML_REFERENCE(Integer(Signature.SignedInfo.rgpReference^) + SizeOf(CRYPT_XML_REFERENCE) * i);

    AResults.ErrorCode := CryptXmlGetStatus(Reference.hReference
                                           ,@Status);

    if Failed(AResults.ErrorCode) then
    begin
      CleanUp;
      AResults.ErrorMessage := 'CryptXmlGetStatus: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar para a referência';
      Exit;
    end;

    if (Status.dwErrorStatus and CRYPT_XML_STATUS_ERROR_DIGEST_INVALID) = CRYPT_XML_STATUS_ERROR_DIGEST_INVALID then
    begin
      AResults.ErrorCode := CRYPT_XML_E_INVALID_DIGEST;
      AResults.ErrorMessage := 'XmlDSigVerify: Referencia[' + IntToStr(i) + '] (id="' + Reference^.wszId + '", uri="' + Reference^.wszUri + '") -> Resumo criptográfico (digest) inválido';
      CleanUp;
      Exit;
    end
    else if (Status.dwErrorStatus and CRYPT_XML_STATUS_ERROR_NOT_RESOLVED) = CRYPT_XML_STATUS_ERROR_NOT_RESOLVED then
    begin
      AResults.ErrorCode := CRYPT_XML_E_UNRESOLVED_REFERENCE;
      AResults.ErrorMessage := 'XmlDSigVerify: Referencia[' + IntToStr(i) + '] (id="' + Reference^.wszId + '", uri="' + Reference^.wszUri + '") -> Referência inexistente';
      CleanUp;
      Exit;
    end;

    inc(i);
  until i = Signature.SignedInfo.cReference;

  AResults.ErrorCode := CryptXmlGetStatus(Signature.hSignature
                                         ,@Status);

  if Failed(AResults.ErrorCode) then
  begin
    CleanUp;
    AResults.ErrorMessage := 'CryptXmlGetStatus: Erro 0x' + IntToHex(AResults.ErrorCode,8) + 'h ao executar para a assinatura';
    Exit;
  end;

  if (Status.dwErrorStatus and CRYPT_XML_STATUS_ERROR_DIGEST_INVALID) = CRYPT_XML_STATUS_ERROR_DIGEST_INVALID then
  begin
    AResults.ErrorCode := CRYPT_XML_E_INVALID_DIGEST;
    AResults.ErrorMessage := 'XmlDSigVerify: Referencia[' + IntToStr(i) + '] (id="' + Reference^.wszId + '", uri="' + Reference^.wszUri + '") -> Resumo criptográfico (digest) inválido';
    CleanUp;
    Exit;
  end
  else if (Status.dwErrorStatus and CRYPT_XML_STATUS_ERROR_NOT_RESOLVED) = CRYPT_XML_STATUS_ERROR_NOT_RESOLVED then
  begin
    AResults.ErrorCode := CRYPT_XML_E_UNRESOLVED_REFERENCE;
    AResults.ErrorMessage := 'XmlDSigVerify: Referencia[' + IntToStr(i) + '] (id="' + Reference^.wszId + '", uri="' + Reference^.wszUri + '") -> Referência inexistente';
    CleanUp;
    Exit;
  end;

  Result := True;
  CleanUp;
end;

No código acima, as linhas 48 a 64 servem para normalizar o arquivo de entrada, de forma que após esse bloco de código, apenas precisemos trabalhar com um arquivo em formato binário, cujo conteúdo estará na variável InputFile e na linha 66 obtém-se um handle para o contexto do documento a ser validado a partir da variável InputFile. Este handle é colocado na variável DocumentContextHandle.

Na linha 82, a partir do handle do contexto do documento, obtém-se o contexto do documento propriamente dito e o colocamos na variável DocumentContext e nas linhas 92 a 98 é feita uma validação a fim de saber se o contexto do documento possui o tag <Signature>, que é o tag que identifica o documento xml como sendo um XML-DSIG.

Na linha 100 obtemos uma referência ao primeiro nó <Signature> e colocamos essa referência na variável Signature. O membro DocumentContext.rgpSignature é o array que seria circulado para trabalhar com cada um dos nós <Signature> do arquivo. Como minha implementação só vai suportar um elemento deste tipo, basta considerar a variável Signature como sendo uma referência direta àquilo que existe no único nó <Signature> existente no xml. DocumentContext.cSignature indica a quantidade de nós <Signature> existentes no xml.

Na linha 102 obtemos uma referência ao nó <KeyInfo> (contido no nó <Signature>) e colocamos esse referência na variável KeyInfo. Signature.pKeyInfo é a estrutura que define o nó <KeyInfo>, o qual pode conter <KeyName>, <KeyValue>, <RetrievalMethod> e/ou <X509Data>.

As linhas 106 a 143 definem um loop que vai circular pelo array KeyInfo.rgKeyInfo. Varrer este array e seus membros possibilita que nós acessemos todos os nós contidos dentro do nó <KeyInfo>. Na linha 107, KeyInfoItem representa cada item de KeyInfo.rgKeyInfo, o qual é um array que contém os filhos de <KeyInfo>. A cada iteração, KeyInfoItem tem seu ponteiro incrementado para apontar para o próximo item do array. A quantidade de elementos neste array está no membro KeyInfo.cKeyInfo. Cada elemento deste array é um PCRYPT_XML_KEY_INFO_ITEM, o qual pode personificar cada um dos tipos de filhos possíveis de <KeyInfo> a saber: <KeyName>, <KeyValue>, <RetrievalMethod> ou <X509Data>. Destes, nesta implementação, nos interessa apenas os nós <X509Data>, por isso, na linha 109, apenas se o item da vez for um <X509Data>, é que devemos fazer algo, pois estamos interessados apenas nos dados dos certificados.

Abra o arquivo KRK.Xml.CryptXml.pas, procure a estrutura CRYPT_XML_KEY_INFO_ITEM e note que ela contém uma parte variável (case) que por meio do membro dwType identifica o que está sendo representado de fato na estrutura. O CryptXML prevê que existam outros tipos de nós filhos de <KeyInfo> através do membro "Custom" do tipo CRYPT_XML_BLOB, que é um tipo genérico que serve para conter qualquer tipo de dado por meio dos seus membros, dwCharset, cbData e pbData. 


As linhas 113 a 136 definem um loop que vai circular pelo array KeyInfoItem.X509Data.rgX509Data. Varrer este array e seus membros possibilita que nós acessemos todos os nós contidos dentro do nó <X509Data>. Na linha 114, X509DataItem representa cada item de KeyInfoItem.X509Data.rgX509Data, o qual é um array que contém os filhos de <X509Data>. A cada iteração, X509DataItem tem seu ponteiro incrementado para apontar para o próximo item do array. A quantidade de elementos neste array está no membro KeyInfoItem.X509Data.cX509Data. Cada elemento deste array é um PCRYPT_XML_X509DATA_ITEM, o qual pode personificar cada um dos tipos de filhos possíveis de <X509Data> a saber: <X509IssuerSerial>, <X509SKI>, <X509SubjectName>, <X509Certificate> ou <X509CRL>. Destes, nesta implementação, nos interessa apenas os nós <X509Certificate>, por isso, na linha 116, apenas se o item da vez for um <X509Certificate>, é que devemos fazer algo, pois estamos interessados apenas nos dados binários dos certificados.

Abra o arquivo KRK.Xml.CryptXml.pas, procure a estrutura CRYPT_XML_X509DATA_ITEM e note que ela contém uma parte variável (case) que por meio do membro dwType identifica o que está sendo representado de fato na estrutura. O CryptXML prevê que existam outros tipos de nós filhos de <X509Data> através do membro "Custom" do tipo CRYPT_XML_BLOB, que é um tipo genérico que serve para conter qualquer tipo de dado por meio dos seus membros, dwCharset, cbData e pbData. 

Na linha 118, obtém-se o contexto do certificado a partir do código binário de um certificado informado. Cada <X509Certificate> contém o código binário de um certificado, no formato X509 codificado em Base64. O formato X509 é conhecido como DER (Distinguished Encoding Rules) e trata-se de um formato binário (não texto). Não confundir com o formato CER (Canonical Encoding Rules), que são arquivos DER codificados em Base64, portanto o conteúdo de cada nó <X509Certificate> é um CER, que é um DER (X509) codificado em Base64 pois o XML-DSIG é um formato de texto e não aceitaria o código binário de um certificado. Também é importante não confundir um arquivo em formato CER com arquivo de extensão .cer. Um arquivo de extensão .cer  (de certificado) pode conter dados CER ou DER. Arquivos em formato CER são também chamados de "arquivos com conteúdo X.509 codificados em Base64", o que é muito pertinente.

No Windows, certificados podem ser exportados como CER ou DER, ambos em um arquivo de extensão .cer (de certificado). No certmgr, escolha um certificado com o botão direito do mouse, vá em todas as tarefas e clique em exportar. Se o certificado tem uma chave privada e você escolher exportar junto com essa chave, não será possível salvar um arquivo em formato CER ou DER (ambos, x.509), pois estes formatos representam apenas o certificado e não suas chaves criptográficas. Para exportar a chave junto, é necessário usar um arquivo .pfx, que trata-se de um contêiner de certificados e chaves, cujo formato é proprietário da Microsoft. Curiosidade adicional: Arquivos DER começam sempre com o Byte de valor "30h". Para maiores informações acesse as páginas a seguir:

https://docs.microsoft.com/en-us/windows/win32/seccrypto/encoding-and-decoding-a-certificate-context

https://docs.microsoft.com/en-us/windows/win32/seccrypto/decoding-a-cert-info-structure

https://docs.microsoft.com/en-us/windows/win32/seccrypto/encoding-a-cert-info-structure

https://stackoverflow.com/questions/22743415/what-are-the-differences-between-pem-cer-and-der/22743616

https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/ns-wincrypt-cert_context

https://blogs.getcertifiedgetahead.com/der-and-cer/


Caso um contexto para o certificado tenha sido obtido com sucesso, na linha 124, importa-se a chave pública do mesmo, usando a função CryptImportPublicKeyInfoEx2. Caso a execução desta função seja bem sucedida ela retornará true, retornará um handle para a chave pública em KeyHandle e entrará na condição que vai encerrar o loop mais interno. A regra que eu estabeleci foi apenas retornar o certificado usado para assinar. Se houver um único certificado ou se houver uma cadeia de certificação, o primeiro certificado SEMPRE será o certificado usado para assinar, por isso, ao achar o primeiro elemento <X509Certificate> e obter a sua chave pública sai do loop interno (governado por "j") imediatamente.

Na linha 138, já processamos todos os filhos de <X509Data> no loop interno (governado por "j"), concluindo assim a tarefa de obter o certificado usado para assinar e sua chave pública. Ao chegar neste ponto, caso KeyHandle > 0, significa que já obtivemos a chave pública do primeiro certificado contido no XML-DSIG, portanto não precisamos mais continuar no loop externo (governado por "i"), e, sendo assim, saímos dele aqui (break).

Na linha 145 verificamos se após a execução dos loops anteriores a variável KeyHandle contém um handle válido e caso não contenha, caso o fluxo chegue nesse ponto sem uma chave pública, significa que algo errado aconteceu. Neste caso, interrompe a operação de validação imediatamente.

Caso o fluxo de execução chegue na linha 153, temos o certificado usado para assinar e sua chave pública. Podemos agora realizar a validação da assinatura digital usando a função CryptXmlVerifySignature.

Caso o fluxo de execução alcance a linha 165, significa que a assinatura digital foi validada com sucesso e agora é necessário validar o digest (resumo criptográfico) da(s) referência(s). O loop que se inicia aqui vai circular pelo array Signature.SignedInfo.rgpReference. Dentro do loop, a variável Reference representa cada item de Signature.SignedInfo.rgpReference, o qual é um array que contém todos os nós <Reference> do xml. A cada iteração, Reference tem seu ponteiro incrementado para apontar para o próximo item do array. A quantidade de elementos neste array está no membro Signature.SignedInfo.cReference. Cada elemento deste array é um PCRYPT_XML_REFERENCE. A implementação original verificava referências externas também, mas como não vou lidar com isso, não incluí esta verificação. Vou lidar apenas com referências internas a fim de tornar a implementação mais simples.

Na linha 166 eu realmente não sei se está correta a forma de incremento do ponteiro, porque Signature.SignedInfo.rgpReference é um ponteiro duplo, um ponteiro para um ponteiro para um CRYPT_XML_REFERENCE, o que faz com que o array seja um array de ponteiros, logo, cada um só estaria distante um do outro de 4 Bytes e não SizeOf(CRYPT_XML_REFERENCE). Posso estar errado quanto a isso, mas no dia que eu achar um arquivo com mais de uma referência para testar, eu terei a confirmação. Por ora vou considerar que a implementação está correta.

Na linha 168, o status é obtido para cada referencia, mas mais adiante no código você verá que ele pode ser obtido da assinatura, diretamente, como forma de "prova dos nove" da assinatura como um todo. A execução da função CryptXmlGetStatus obtém o status a partir do handle da referência atual (Reference.hReference) e entre as linhas 171 e 191 são feitas as validações pertinentes.

Na linha 196, a validade do digest (resumo criptográfico) da(s) referência(s) foi verificada e foi bem sucedida, agora feremos o mesmo, só que para a assinatura (<Signature>). A execução da função CryptXmlGetStatus obtém o status a partir do handle da assinatura digital (Signature.hSignature) e entre as linhas 199 e 219 são feitas as validações pertinentes.

Finalmente, na linha 221, caso o fluxo chegue neste ponto, significa que tudo ocorreu bem, neste caso limpa tudo que tiver sobrado na memória e configura o resultado como true.


O caso especial da assinatura das NF-e

O formato de XML-DSIG usado na NF-e é monolítico, isto é,  ele não permite usar qualquer algoritmo de digest e força a utilização de URI no nó <Reference>, o qual é opcional de acordo com o padrão estabelecido pela W3C. Além disso, o XML-DSIG usado nas NF-e são sempre do tipo ENVELOPED o que é natural, visto que trata-se da assinatura de um documento XML, porém a naturalidade acaba por aí, pois os validadores de NF-e são tão estritos que não permitem que nós adicionais sejam incluídos no XML da NF-e, tirando parte da flexibilidade que se pode conseguir com esse tipo de XML-DSIG. Em outras palavras, tais validadores usam um esquema xsd que obrigam que o XML-DSIG da NF-e sempre seja rigorosamente igual a um formato preestabelecido.

Por conta desse tipo de bizarrice que o Governo impõe, caso você deseje usar a função XmlDSigCreate para assinar suas NF-e, você terá de usar opções específicas. Em suma, por mais que você possa assinar corretamente uma NF-e usando as opções que bem entender, para que os validadores oficiais de NF-e não reclamem de sua NF-e você precisará usar as opções que eles exigem. A tabela a seguir mostra quais opções precisam ter valores específicos para que a assinatura digital de uma NF-e seja bem sucedida. As opções apresentadas nesta tabela são as opções do programa de exemplo anexado a este artigo, cuja tela é mostrada mais adiante.

Opção Valores permitidos Observações
Formato da assinatura Enveloped O formato Enveloped só permite que documentos XML sejam assinados, portanto ele é ideal para as NF-e, entretanto, devido ao esquema estrito utilizado pelos validadores, não é possível utilizar um XML com nós adicionais customizados. Por mais que a definição de uma NF-e não precise extensões nos seus XML, impedir que isso seja feito, no meu entendimento, é desnecessário.
Hash de <Reference> SHA1 Segundo o documento oficial que define o XML-DSIG (https://www.w3.org/TR/xmldsig-core1), em sua seção 6.2.1, a utilização do algoritmo SHA-1 é desencorajada por conta dos problemas de colisão. Mas quem definiu o esquema da NF-e parece não se importar muito com a segurança ao obrigar o uso deste algoritmo.
Hash de <Signature> SHA1 Idem
Adicionar certificado? Apenas assinador A especificação do formato XML-DSIG prevê que mais de um certificado possa ser adicionado ao documento (cadeia de certificação, por exemplo), mas para a NF-e colocar mais de um certificado no documento é um erro que a invalida por completo.
URI de <Reference> ID do nó <infNFe> Segundo a especificação do formato XML-DSIG o atributo URI do nó <Reference>, dentre outras funções, serve para indicar qual nó no documento atual que deve ser o "alvo" da assinatura digital. Isso parece óbvio, porém seria desnecessário informar este valor, caso o documento da NF-e contivesse um elemento de nível superior simples. Atualmente o que se tem é <NFe> como elemento raiz e <infNFe> como seu filho único imedita, mas se <infNFe> fosse dispensado, todos os nós úteis estariam diretamente em <NFe> e o atributo URI do nó <Reference> poderia ser uma string vazia, o que automaticamente indicaria que o alvo da assinatura digital deveria ser o nó raiz. Em suma, com a implementação atual da NF-e, existem mais nós, mais dados e mais exigências. Na NF-e, o ID do nó <infNFe> é a chave da NF-e (44 dígitos numéricos) precedida da letras "NFe".
Local de <Signature> /NFe <NFe> é o nó raiz do documento da NF-e, portanto, o elemento <Signature> será injetado como filho deste nó, e consequentemente ele será um nó irmão do nó <infNFe>. A especificação do formato XML-DSIG diz que o nó <Signature> pode ser injetado em qualquer nó existente no XML, mas o esquema usado nos validadores oficiais não permite que nenhum outro nó seja usado. De todas as exigências, esta é a única com a qual eu concordo, pois isso, de fato, mantém as coisas mais organizadas.
Método de Canonicalização de <Signature> Canonical XML 1.0
ou
Canonical XML 1.0 (WC)
De acordo com a especificação do formato XML-DSIG é possível usar 4 métodos de canonicalização (Canonical XML 1.0, Canonical XML 1.0 With Comments, Exclusive XML Canonicalization 1.0 e Exclusive XML Canonicalization 1.0 With Comments), mas para o atributo <Signature> da NF-e apenas pode ser usado Canonical XML 1.0 ou sua versão "With Comments".
Método de Canonicalização de <Reference> Canonical XML 1.0 De acordo com a especificação do formato XML-DSIG é possível usar 4 métodos de canonicalização (como foi dito anteriormente), mas para o atributo <Reference> da NF-e apenas pode ser usado Canonical XML 1.0.
Adicionar Key Value desmarcado O XML-DSIG prevê a inclusão de uma chave pública, que pode ser útil para validar a assinatura digital, porém caso esta informação seja incluída na NF-e assinada os validadores invalidarão toda a NF-e.

Obs.: minha visão é de uma pessoa que não trabalha com automação comercial, por isso eu posso achar que algumas dessas exigências engessantes são absurdas.

Esta imagem mostra a tela inicial do programa de exemplo anexado a este artigo, o qual pode
ser baixado no link mais adiante.

Como validar a solução?

A primeira pergunta que pode vir na sua cabeça depois de ter lido isso tudo é "Posso confiar nisso? Funciona mesmo?". A resposta é sim, funciona e você pode confiar, mas se mesmo assim você quiser ver com seus próprios olhos antes de implementar no seu sistema, existem dois serviços online que podem ser usados para validar a assinatura em XML.

O primeiro serviço é o Verificador de Conformidade do ITI (Instituto Nacional de Tecnologia da Informação) (https://verificador.iti.gov.br). Ele é capaz de validar qualquer tipo de XML-DSIG (ENVELOPED ou ENVELOPING) com quaisquer das opções disponibilizadas na função de assinatura apresentada neste artigo. Seguem algumas imagens das validações testadas com esse serviço e uma breve descrição de cada uma delas:

Este é o resultado da validação de um arquivo XML-DSIG no formato ENVELOPED. Foi assinada uma NFe codificada em UTF-8 usando opções padrão do programa de exemplo anexado a este artigo. É possível ver algumas informações do certificado que foi usado para assinar, o qual encontra-se expirado, porém a informação que nos interessa está destacada na imagem, provando que a função de assinatura gera um XML-DSIG correto!
O resultado visto na imagem anterior é exatamente o mesmo para um XML-DSIG em formato ENVELOPING como o da imagem atual. Note que no modo ENVELOPING, o conteúdo assinado é codificado em Base64 e incluído em um nó <base64>, dentro do nó <Object>, o qual possui um atributo "id" que é referenciado no nó <Reference> por meio do atributo "URI". No modo ENVELOPING é obrigatório informar um valor para o atributo URI, pois ele será usado para identificar onde está o conteúdo que foi objeto da assinatura digital. Como o conteúdo assinado está codificado em Base64, este modo pode ser usado para assinar qualquer tipo de arquivo, incluindo arquivos binários, como imagens, executáveis, documentos .pdf ou .doc, etc.

O segundo serviço que eu quero apresentar é o Validador de Mensagens do Projeto NFe disponibilizado pela SEFAZ do Rio Grande do Sul (https://www.sefaz.rs.gov.br/nfe/nfe-val.aspx). Este serviço valida exclusivamente as NFe, isto é, arquivos XML-DSIG no formato ENVELOPED e com opções específicas, tal como apresentado no tópico anterior deste artigo. Ao assinar uma NFe com as opções adequadas e utilizar este validador, o seguinte resultado será apresentado:

Esta imagem mostra o resultado da validação de uma NFe propriamente dita. A assinatura desta NFe usou opções específicas necessárias para que sua validação fosse bem sucedida. Note que é possível ver algumas informações do certificado, bem como a indicação de que a assinatura digital é válida. Os erros mostrados não dizem respeito a assinatura digital, mas sim aos dados da NFe, a qual, claro é um NFe fictícia.

  Arquivos anexados  
Arquivo Descrição Tamanho Modificado em
Download this file (xmldsig.zip) xmldsig.zip Exemplo de utilização das funções de assinatura e verificação usando CryptXml 45 KB 22/01/2021 às 14:59

1 Para testar as funções de assinatura e verificação, não é necessário instalar os pacotes em tempo de design do Krakatoa. Basta apenas a construção do pacote KRKLib e o posterior ajuste de seu Library Path