PDA

Ver la Versión Completa : Problema con certificado de AFIP y OpenSSL


giulichajari
26-03-2020, 01:34:55
Hola amigos..Estoy desarrollando un sistema para coenctarme al web service de facturacion electronica de AFIP.

Esta es la unidad de Autenticacion, a la cual llamo como parametro "wsfe":

unit UAutenticacion;

interface
uses
XMLIntf, XMLDoc,Winapi.shellAPI,
dialogs,Classes,SOAPHTTPClient,sysutils,DateUtils;

type
TLogin = class
private

Fservicio: string;
Fsign: string;
Ftoken: string;
Fexpiration: TDateTime;
procedure Setservicio(const Value: string);
procedure Setsign(const Value: string);
procedure Settoken(const Value: string);
procedure Setexpiration(const Value: TDateTime);
public
property expiration:TDateTime read Fexpiration write Setexpiration;
property token:string read Ftoken write Settoken;
property sign:string read Fsign write Setsign;
property servicio:string read Fservicio write Setservicio;

destructor destroy;
function guardarXML():IXMLDocument;
function armarsource:string;
function generarCMS(const xml1:IXMLDocument):WideString;
procedure respuestaXML(const s:string);
function expiro():boolean;
class function unicoLogin:TLogin;
procedure actualizarsigntoken;
constructor Loguearse(s:string);

end;
var
login:TLogin;
implementation

{ TLogin }

uses tra,IniFiles,Forms, LoginCms1,loginresponse, string64, NuevoTicket;
procedure TLogin.actualizarsigntoken;
var
xml1:IXMLDocument;
cms:string;
begin
xml1:=NewXMLDocument();
xml1:=login.guardarXML();
cms:=login.generarCMS(xml1);
login.respuestaXML(cms);
end;


function TLogin.armarsource: string;
var
archivoini:TIniFile;
source,o,serialNumber,C,cn:string;
begin
archivoini:=TIniFile.Create(ExtractFilePath(Application.ExeName)+ 'caja.ini');
o:=archivoini.ReadString('EMPRESA','O','');
serialNumber:=archivoini.ReadString('EMPRESA','CUIT','');
cn:=archivoini.ReadString('EMPRESA','cn','');
C:=archivoini.ReadString('EMPRESA','C','');
source:='C='+C+',o='+o+',serialNumber='+serialNumber+',cn='+cn;
Result:=source;
end;


destructor TLogin.destroy;
begin
inherited;
end;

function TLogin.expiro: boolean;
var
time:string;
diferencia:int64;
begin
diferencia:=2;
time:=FormatDateTime('hh:mm:ss',Now);
if (MinuteSpan(StrToDateTime(time),login.expiration)<2) then
begin
result:=True;
end
else
begin
result:=False;
end;
end;

function TLogin.generarCMS(const xml1:IXMLDocument):WideString;
var
rutabat,linea:string;
archivoCMS:TStringList;
textocms:WideString;
archivoplano:TextFile;
nombrearchivo:string;
begin


rutabat:=ExtractFilePath(Application.ExeName)+'certificado\'+'cms.bat';
ShellExecute(0,'open',PWideChar(rutabat),nil, nil,0);


archivoCMS:=TStringList.Create;

archivoCMS.LoadFromFile(ExtractFilePath(Application.ExeName) + 'loginticketrequest.xml.cms');



result:=archivoCMS.Text;

end;

function TLogin.guardarXML():IXMLDocument;
var
loginticketrequest:IXMLLoginTicketRequestType;
loginheader:IXMLHeaderType;
archivoini:TIniFile;
diai,diaf,horai,time,horaf,horae,expiration:string;
XML1:IXMLDocument;
XMLanterior:IXMLDocument;
startnode,nodohijo,nodoexp:IXMLNode;
begin

time:=FormatDateTime('hh:mm:ss',now);

//crear archivo xml
XML1 := NewXMLDocument;

XML1.FileName:='loginticketrequest.xml';


loginticketrequest:=NewloginTicketRequest;
//asginar valores para loginticketrequest.xml
// version

loginticketrequest.Version:='1.0';
//header
// uniqueid son constantes

loginticketrequest.header.UniqueId:=4325399;
//hora del pedido.. es ahora
diai:=FormatDateTime('yyyy-mm-dd',Now);
diaf:=FormatDateTime('yyyy-mm-dd',IncHour(Now,12));
horai:=FormatDateTime('hh:mm:ss',now);
horaf:=FormatDateTime('hh:mm:ss',IncHour(Now,12));
loginticketrequest.header.GenerationTime:=diai+ 'T' + horai + '-03:00';
//hora de finalizacion (1 dia es el maximo);
loginticketrequest.header.ExpirationTime:=diaf+'T'+horaf + '-03:00';

//servicio a acceder
loginticketrequest.Service:=Fservicio;
XML1.XML.Add(loginticketrequest.XML);
XML1.Active:=True;

XML1.SaveToFile(ExtractFilePath(Application.ExeName) + 'loginticketrequest.xml');

Result:=XML1;



end;

constructor TLogin.Loguearse(s:string);
var
XML1:IXMLDocument;
CMS:WideString;
CMS64:WideString;
TRA:TXMLDocument;
begin
self.Fservicio:=s;
XML1:=self.guardarXML;
CMS:=self.generarCMS(XML1);
ShowMessage(CMS);
CMS64:=Base64Encode(CMS);
ShowMessage(CMS64);
self.respuestaXML(CMS64);
end;

procedure TLogin.respuestaXML(const s:string);
var
RIOLogin:THTTPRIO;
xml3:IXMLDocument;
content,expira,nodo:string;
nodohijo,nodohijosign,startnode,startnodesign:IXMLNode;
begin

RIOLogin:=THTTPRIO.Create(nil);
with RIOLogin do
begin
WSDLLocation:='https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl';
Port:='LoginCms';
Service:='LoginCMSService';

end;


xml3:=NewXMLDocument;
content:=(RIOLogin as LoginCms).loginCms(s);
xml3.XML.Text:=content;
xml3.Active:=True;
xml3.SaveToFile(ExtractFilePath(Application.ExeName) + 'respuestaxml.xml');
startnode:=xml3.ChildNodes[1];
nodohijo:=startnode.ChildNodes[0];
nodo:=nodohijo.ChildNodes['expirationTime'].Text;
expira:=Copy(nodo,12,8);
Setexpiration(StrToDateTime(expira));
//setear sign y token
startNodesign := xml3.ChildNodes[1];
nodohijosign:=startnodesign.ChildNodes[1];


Setsign(nodohijosign.ChildNodes['sign'].Text);

Settoken(nodohijosign.ChildNodes['token'].Text);

end;



procedure TLogin.Setexpiration(const Value: TDateTime);
begin
Fexpiration := Value;
end;

procedure TLogin.Setservicio(const Value: string);
begin
Fservicio := Value;
end;

procedure TLogin.Setsign(const Value: string);
begin
Fsign := Value;
end;


procedure TLogin.Settoken(const Value: string);
begin
Ftoken := Value;
end;


class function TLogin.unicoLogin:TLogin;

begin
if login<>nil then
begin
if login.expiro=False then
begin
Result:=login;
end
else
begin
login.actualizarsigntoken;
Result:=login;
end;
end
else
begin
login:=Tlogin.Loguearse('padron-puc-ws-consulta-nivel3');
login.respuestaXML(login.generarCMS(login.guardarXML()));

end;
end;

end.

Y obtengo No se puede decodificar el BASE64.



Como veran en el constructor sigo los pasos de las especificaciones tecnicas de AFIP:



1- Creo el xml con tiempo de expiracion

2-Genero el CMS en un archivo, para lo mismo ejecuto un archivo BAT (Codigo DOS digamos):



c:\OpenSSL-Win32\bin\openssl.exe cms -sign -in G:\despensa\Win32\Debug\LoginTicketRequest.xml -out G:\despensa\Win32\Debug\LoginTicketRequest.xml.cms -signer G:\despensa\Win32\Debug\certificado\certificado.crt -inkey G:\despensa\Win32\Debug\certificado\caruso12021991.key -nodetach -outform PEM

y tambien podran ver que puse dos ShowMessage y segun veo el problema es que me carga el archivo LoginTicketRequest.xml.cms anterior, es decir lo carga y luego se regenera, ese es uno de los problemas principales.
Lo cual no entiendo porque sucede si el codigo del archivo BAT esta antes que la creacion del StringList.
Lo que me parece incorrecto es que en la unidad donde encodeo el cms a base 64 asi como las variables en el constructor son de tipo string, este mismo admite solo 256 caracteres, cuando el texto es mas largo:

unit string64;

interface

uses
Winapi.Windows,Winapi.Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

const
B64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

function Base64Decode(const S: string): string;
function Base64Encode(const S: String): String;

implementation


function Base64Encode(const S: String): String;
var
InBuf: array[0..2] of Byte;
OutBuf: array[0..3] of Char;
iI, iJ: Integer;
begin
SetLength(Result, ((Length(S) + 2) div 3) * 4);
for iI := 1 to ((Length(S) + 2) div 3) do
begin
if Length(S) < (iI * 3) then
Move(S[(iI - 1) * 3 + 1], InBuf, Length(S) - (iI - 1) * 3)
else
Move(S[(iI - 1) * 3 + 1], InBuf, 3);
OutBuf[0] := B64Table[((InBuf[0] and $FC) shr 2) + 1];
OutBuf[1] := B64Table[(((InBuf[0] and $3) shl 4) or ((InBuf[1] and $F0) shr 4)) + 1];
OutBuf[2] := B64Table[(((InBuf[1] and $F) shl 2) or ((InBuf[2] and $C0) shr 6)) + 1];
OutBuf[3] := B64Table[(InBuf[2] and $3F) + 1];
Move(OutBuf, Result[(iI - 1) * 4 + 1], 4);
end;
if Length(S) mod 3 = 1 then
begin
Result[Length(Result) - 1] := '=';
Result[Length(Result)] := '=';
end
else if Length(S) mod 3 = 2 then
Result[Length(Result)] := '=';
end;

function Base64Decode(const S: string): string;
var
OutBuf: array[0..2] of Byte;
InBuf: array[0..3] of Byte;
iI, iJ: Integer;
begin
if Length(S) mod 4 <> 0 then
raise Exception.Create('Base64: Incorrect string format');
SetLength(Result, ((Length(S) div 4) - 1) * 3);
for iI := 1 to (Length(S) div 4) - 1 do
begin
Move(S[(iI - 1) * 4 + 1], InBuf, 4);
for iJ := 0 to 3 do
case InBuf[iJ] of
43: InBuf[iJ] := 62;
48..57: Inc(InBuf[iJ], 4);
65..90: Dec(InBuf[iJ], 65);
97..122: Dec(InBuf[iJ], 71);
else
InBuf[iJ] := 63;
end;
OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
OutBuf[2] := (InBuf[2] shl 6) or (InBuf[3] and $3F);
Move(OutBuf, Result[(iI - 1) * 3 + 1], 3);
end;
if Length(S) <> 0 then
begin
Move(S[Length(S) - 3], InBuf, 4);
if InBuf[2] = 61 then
begin
for iJ := 0 to 1 do
case InBuf[iJ] of
43: InBuf[iJ] := 62;
48..57: Inc(InBuf[iJ], 4);
65..90: Dec(InBuf[iJ], 65);
97..122: Dec(InBuf[iJ], 71);
else
InBuf[iJ] := 63;
end;
OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
Result := Result + Char(OutBuf[0]);
end
else if InBuf[3] = 61 then
begin
for iJ := 0 to 2 do
case InBuf[iJ] of
43: InBuf[iJ] := 62;
48..57: Inc(InBuf[iJ], 4);
65..90: Dec(InBuf[iJ], 65);
97..122: Dec(InBuf[iJ], 71);
else
InBuf[iJ] := 63;
end;
OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
Result := Result + Char(OutBuf[0]) + Char(OutBuf[1]);
end
else
begin
for iJ := 0 to 3 do
case InBuf[iJ] of
43: InBuf[iJ] := 62;
48..57: Inc(InBuf[iJ], 4);
65..90: Dec(InBuf[iJ], 65);
97..122: Dec(InBuf[iJ], 71);
else
InBuf[iJ] := 63;
end;
OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
OutBuf[2] := (InBuf[2] shl 6) or (InBuf[3] and $3F);
Result := Result + Char(OutBuf[0]) + Char(OutBuf[1]) + Char(OutBuf[2]);
end;
end;
end;
end.

Entonces quise convertir el certificado con openssl directamente a base64. Para lo mismo hago lo siguiente:

c:\OpenSSL-Win32\bin\openssl.exe x509 -inform der -in G:\despensa\Win32\Debug\certificado\certificado.crt -out G:\despensa\Win32\Debug\certificado\certificado.pem

Pero me devuelve:

unable to load certificate 7200:error:0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag:.\crypto\asn1\tasn_dec.c:1320: 7200:error:0D07803A:asn1 encoding routines:ASN1_ITEM_EX_D2I:nested asn1 error:.\crypto\asn1\tasn_dec.c:382:Type=X509

aledieb
29-03-2020, 20:13:44
Hola, lo que te devuelve el openssl empieza con -----BEGIN PKCS7----- y termina con -----END PKCS7----- esas dos líneas debes eliminarlas para poder obtener el token y el sign de la afip.

Espero que te sea eso. Por las dudas te paso el comando openssl que yo uso, tal vez halla una diferencia.

openssl smime -sign -in E:\tmp\ticketsf.xml -out E:\tmp\ticketf.xml
-inkey E:\certificados\ClavePriv.txt -signer E:\certificados\Certificado.crt -outform PEM -nodetach