Comunicação de documentos de transporte à AT a partir da .NET Compact Framework [reactualizado a 2013/06/17]

(post revisto a 2013/06/17, alterado código exemplo e link para componente)


A partir de 1 de Maio, os documentos de transporte terão de ser comunicados previamente à AT, sendo uma das formas disponibilizadas e aquela que se afigura mais prática, a comunicação via webservices, que permite na resposta obter o código atribuío pela AT ao documento transmitido.


Com base na informação num tópico no Portugal-a-programar, comecei a semana passada um projecto de testes para validar se a comunicação em causa poderia ser feita directamente a partir da .NET Compact Framework, o que se afigurava difícil dadas as limitações da .NET CF a lidar com certificados, nomeadamente o carregamento de .pfx e a utilização de .cer para encriptar a informação. Depois de dias a investigar Platform Invokes para o efeito sem sucesso, e de ter confirmado que uma ou outra solução candidata não produzia resultados válidos quando comunicados aos serviços da AT, acabei por fazer aquilo que deveria ter feito em 1º lugar: procurar um componente de terceiros que fizesse o serviço, e assim no www.componentsource.com descobri um componente que, não sendo caro, resolve as duas limitações que tinha encontrado.


O seguinte método em C# devolve o resultado da chamada ao serviço:


public static string testa()
{
    string endpoint = “https://servicos.portaldasfinancas.gov.pt:401/sgdtws/documentosTransporte”;
    string senhaCertificado = “xxxxxxxxxx”;

    string userPortal = “xxxxxxxxx/x”;
    string passwordPortal = “xxxxxxx”;


    string horaCriacaoCifrada;
    string senhaCifrada;
    string nonce;

    SBUtils.Unit.SetLicenseKey(colocar aqui a chave incluída no ficheiro C:\Program Files (x86)\EldoS\SecureBlackbox.NET\Assemblies\NET_CF20\LicenceKey.txt);

    String DataCriacao = DateTime.Now.ToUniversalTime().ToString(“yyyy-MM-ddTHH:mm:ss.ff”) + “Z”;

   
RijndaelManaged rijndaelCipher = new RijndaelManaged();
    rijndaelCipher.GenerateKey();
    rijndaelCipher.Mode = CipherMode.ECB;
    rijndaelCipher.Padding = PaddingMode.PKCS7;
    SymmetricAlgorithm rijn = SymmetricAlgorithm.Create();
    rijn.Key = rijndaelCipher.IV;
    rijn.IV = rijndaelCipher.IV;
    rijn.Mode = CipherMode.ECB;
    MemoryStream msPassFinancas = new MemoryStream();
    CryptoStream csPassFinancas = new CryptoStream(msPassFinancas, rijn.CreateEncryptor(rijn.Key, rijn.IV), CryptoStreamMode.Write);
    using (StreamWriter swPassFinancas = new StreamWriter(csPassFinancas))
    {
        swPassFinancas.Write(passwordPortal);
    }
    MemoryStream msDataCriacao = new MemoryStream();
    CryptoStream csDataCriacao = new CryptoStream(msDataCriacao, rijn.CreateEncryptor(rijn.Key, rijn.IV), CryptoStreamMode.Write);
    using (StreamWriter swDataCriacao = new StreamWriter(csDataCriacao))
    {
        swDataCriacao.Write(DataCriacao);
    }
    senhaCifrada = Convert.ToBase64String(msPassFinancas.ToArray());
    horaCriacaoCifrada = Convert.ToBase64String(msDataCriacao.ToArray());
    RSACryptoServiceProvider AlgRSA = new RSACryptoServiceProvider();

    byte[] rsaModulus = null;
    byte[] rsaPublicKey = null;
    using (TElX509Certificate x509tt = new TElX509Certificate(null))
    {
        //carregar certificado a parir de array de bytes previamente preenchido
        x509tt.LoadFromBufferAuto(_publicCertificate, 0, _publicCertificate.Length,“”);
        int rsaModulusSize = 0;
        int rsaPublicKeySize = 0;
        x509tt.GetRSAParams(ref rsaModulus, ref rsaModulusSize, ref rsaPublicKey, ref rsaPublicKeySize);
        rsaModulus = new byte[rsaModulusSize];
        rsaPublicKey = new byte[rsaPublicKeySize];
        x509tt.GetRSAParams(ref rsaModulus, ref rsaModulusSize, ref rsaPublicKey, ref rsaPublicKeySize);
    }

    RSAParameters rsaP = new RSAParameters()
    {
        Modulus = rsaModulus,
        Exponent = rsaPublicKey
    };
    AlgRSA.ImportParameters(rsaP);
    Byte[] Chave = AlgRSA.Encrypt(rijn.Key, false);
    nonce = Convert.ToBase64String(Chave);

    StringBuilder sb = new StringBuilder();
    sb.Append(“<?xml version=\”1.0\” encoding=\”UTF-8\”?>”);
    sb.Append(“<S:Envelope xmlns:wss=\”http://schemas.xmlsoap.org/ws/2002/12/secext\” xmlns:ns2=\”https://servicos.portaldasfinancas.gov.pt/sgdtws/documentosTransporte/\” xmlns:S=\”http://schemas.xmlsoap.org/soap/envelope/\”>”);
    sb.Append(“<S:Header>”);
    sb.Append(“<wss:Security>”);
    sb.Append(“<wss:UsernameToken>”);
    sb.AppendFormat(“<wss:Username>{0}</wss:Username>”, userPortal);
    sb.AppendFormat(“<wss:Password>{0}</wss:Password>”, senhaCifrada);
    sb.AppendFormat(“<wss:Nonce>{0}</wss:Nonce>”, nonce);
    sb.AppendFormat(“<wss:Created>{0}</wss:Created>”, horaCriacaoCifrada);
    sb.Append(“</wss:UsernameToken>”);
    sb.Append(“</wss:Security>”);
    sb.Append(“</S:Header>”);
    sb.Append(“<S:Body>”);
   
sb.Append(“<ns2:envioDocumentoTransporteRequestElem>”);
    sb.Append(
    @”<TaxRegistrationNumber>xxxxxxxxx</TaxRegistrationNumber>
      <CompanyName>Empresa para teste Lda.</CompanyName>
      <CompanyAddress>
        <Addressdetail>Centro de Empresas de Taveiro</Addressdetail>
       
<City>Taveiro</City>
        <PostalCode>3045-123</PostalCode>
        <Country>PT</Country>
      </CompanyAddress>
      <DocumentNumber>GT AB/1</DocumentNumber>
      <MovementStatus>N</MovementStatus>
      <MovementDate>2013-05-30</MovementDate>
      <MovementType>GT</MovementType>
        <CustomerTaxID>999999990</CustomerTaxID>
    <CustomerAddress>
        <Addressdetail>Urb. Atras Sol Posto</Addressdetail>
        <City>Faro</City>
        <PostalCode>6000-123</PostalCode>
        <Country>PT</Country>
      </CustomerAddress>
      <CustomerName>Empresa Cliente, LDa.</CustomerName>
      <AddressTo>
        <Addressdetail>Urb. Atras Sol Posto</Addressdetail>
        <City>Faro</City>
        <PostalCode>6000-123</PostalCode>
        <Country>PT</Country>
      </AddressTo>
      <AddressFrom>
        <Addressdetail>Centro de Empresas de Taveiro</Addressdetail>
        <City>Taveiro</City>
        <PostalCode>3045-123</PostalCode>
        <Country>PT</Country>
      </AddressFrom>
      <MovementEndTime>2013-05-30T23:04:36.8111658+01:00</MovementEndTime>
      <MovementStartTime>2013-05-30T22:04:36.8101653+01:00</MovementStartTime>
      <VehicleID>00-AA-11</VehicleID>
      <Line>
        <ProductDescription>Ovos</ProductDescription>
        <Quantity>12</Quantity>
        <UnitOfMeasure>UN</UnitOfMeasure>
        <UnitPrice>0.25</UnitPrice>
      </Line>”
);
    sb.Append(“</ns2:envioDocumentoTransporteRequestElem>”);
    sb.Append(“</S:Body>”);
    sb.Append(“</S:Envelope>”);

    TElX509Certificate x509t = new TElX509Certificate(null);
    // carregar o certificado a partir de um array de bytes previamente preenchido
    x509t.LoadFromBufferPFX(_privateCertificate,senhaCertificado);

    TElHTTPSClient cli = new TElHTTPSClient();
    cli.OnCertificateValidate += new SBSSLCommon.TSBCertificateValidateEvent(cli_OnCertificateValidate);
    cli.RequestParameters.CustomHeaders.Add(“SOAPAction=https://servicos.portaldasfinancas.gov.pt/sgdtws/documentosTransporte/”);
    cli.RequestParameters.ContentType = “text/xml; charset=utf-8″;
    cli.RequestParameters.Accept = “text/xml”;

    TElMemoryCertStorage mc = new TElMemoryCertStorage();
    mc.Add(x509t, true); // client certificate
    cli.ClientCertStorage = mc;

    MemoryStream response = new MemoryStream();
    cli.OutputStream = response;

    byte[] byteArray = Encoding.UTF8.GetBytes(sb.ToString());

    cli.Post(endpoint, byteArray);
    response.Position = 0;

    StreamReader reader = new StreamReader(response);
    string responseFromServer = reader.ReadToEnd();
    reader.Close();
    return responseFromServer;
}

static void cli_OnCertificateValidate(object Sender, TElX509Certificate X509Certificate, ref bool Validate)

{

    Validate = true;

}


Com esta abordagem não necessário instalar os certificados na Certificate Store dos dispositivos.

Para compilar, será necessário adicionar uma referência às assemblies SecureBlackBox, SecureBlackBox.HTTP, SecureBlackBox.HTTPCommon, SecureBlackBox.SSL e SecureBlackBox.SSLCommon, instaladas na pasta C:\Program Files (x86)\EldoS\SecureBlackbox.NET\Assemblies\NET_CF20\ e mudar o valor da chave passada na chamada ao método SetLicenseKey. Ter em atenção as chaves no XML que reportam a datas, pois a hora início tem de ser posterior à hora actual, a hora fim quando fornecida tem de ser superior à hora início e suponho que a data do documento tenha que ser igual ou anterior à actual.