Class: VerifactuRails::Certificado

Inherits:
Object
  • Object
show all
Defined in:
lib/verifactu_rails/certificado.rb

Overview

Envuelve el certificado del obligado tributario usado para la autenticación mutua TLS contra la AEAT.

En modalidad VERI*FACTU el certificado NO firma los registros: solo autentica el canal. Aun así es la credencial más sensible del sistema, así que esta clase deliberadamente NO sabe leer ficheros ni variables de entorno: recibe los bytes ya cargados. La política de almacenamiento es de la capa de integración.

Constant Summary collapse

AVISO_CADUCIDAD_DIAS =
30
PATRON_NIF =

NIF del titular, tal como lo graba la FNMT en el sujeto.

Sirve para una comprobación que ahorra viajes: la AEAT exige que el titular del certificado coincida con el ObligadoEmision, y si no coinciden responde un 4104 "no está identificado" que parece decir que el NIF no existe.

Es una heurística sobre formatos observados (serialNumber "IDCES-xxxxxxxxX" o el NIF suelto, y CN terminado en "- xxxxxxxxX"), no un campo normalizado, así que devuelve nil si no lo reconoce en vez de inventarse nada.

/\b([A-Z]?\d{7,8}[A-Z0-9])\b/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(certificado:, clave:, cadena: nil) ⇒ Certificado

Returns a new instance of Certificado.



23
24
25
26
27
28
# File 'lib/verifactu_rails/certificado.rb', line 23

def initialize(certificado:, clave:, cadena: nil)
  @certificado = certificado
  @clave = clave
  @cadena = Array(cadena)
  validar!
end

Instance Attribute Details

#cadenaObject (readonly)

Returns the value of attribute cadena.



21
22
23
# File 'lib/verifactu_rails/certificado.rb', line 21

def cadena
  @cadena
end

#certificadoObject (readonly)

Returns the value of attribute certificado.



21
22
23
# File 'lib/verifactu_rails/certificado.rb', line 21

def certificado
  @certificado
end

#claveObject (readonly)

Returns the value of attribute clave.



21
22
23
# File 'lib/verifactu_rails/certificado.rb', line 21

def clave
  @clave
end

Class Method Details

.desde_pkcs12(datos, password) ⇒ Object

Construye desde los bytes de un .p12/.pfx (lo que el usuario descarga de la FNMT). Evitamos obligarle a partirlo en PEM con openssl a mano.



32
33
34
35
36
37
38
39
40
41
# File 'lib/verifactu_rails/certificado.rb', line 32

def self.desde_pkcs12(datos, password)
  raise CertificadoError, 'Datos PKCS12 vacíos' if datos.nil? || datos.empty?

  p12 = OpenSSL::PKCS12.new(datos, password.to_s)
  new(certificado: p12.certificate, clave: p12.key, cadena: p12.ca_certs)
rescue OpenSSL::PKCS12::PKCS12Error => e
  raise CertificadoError,
        'No se pudo abrir el PKCS12: contraseña incorrecta o fichero corrupto ' \
        "(#{e.message})"
end

Instance Method Details

#caduca_pronto?(umbral = AVISO_CADUCIDAD_DIAS) ⇒ Boolean

Los .p12 caducan siempre en mal momento. Que el sistema avise solo.

Returns:

  • (Boolean)


52
53
54
# File 'lib/verifactu_rails/certificado.rb', line 52

def caduca_pronto?(umbral = AVISO_CADUCIDAD_DIAS)
  dias_para_caducar <= umbral
end

#caducado?Boolean

Returns:

  • (Boolean)


43
44
45
# File 'lib/verifactu_rails/certificado.rb', line 43

def caducado?
  Time.now > certificado.not_after
end

#dias_para_caducarObject



47
48
49
# File 'lib/verifactu_rails/certificado.rb', line 47

def dias_para_caducar
  ((certificado.not_after - Time.now) / 86_400).floor
end

#nifObject



78
79
80
81
82
83
84
85
# File 'lib/verifactu_rails/certificado.rb', line 78

def nif
  candidatos = [sujeto['serialNumber'], sujeto['CN']].compact
  candidatos.each do |valor|
    m = valor.upcase.sub(/\AIDCES-/, '').match(PATRON_NIF)
    return m[1] if m && m[1].length == 9
  end
  nil
end

#resumenObject



100
101
102
103
104
# File 'lib/verifactu_rails/certificado.rb', line 100

def resumen
  { titular: sujeto['CN'], organizacion: sujeto['O'],
    caduca: certificado.not_after, dias_restantes: dias_para_caducar,
    sello: sello? }
end

#sello?Boolean

Un certificado de sello de entidad se dirige a un endpoint DISTINTO de la AEAT. Heurística sobre el sujeto; el integrador puede forzarlo en config.

Returns:

  • (Boolean)


62
63
64
65
# File 'lib/verifactu_rails/certificado.rb', line 62

def sello?
  valores = sujeto.values.join(' ').downcase
  valores.include?('sello') || valores.include?('seal')
end

#sujetoObject



56
57
58
# File 'lib/verifactu_rails/certificado.rb', line 56

def sujeto
  certificado.subject.to_a.each_with_object({}) { |(k, v, _), h| h[k] = v }
end

#titularObject

Nombre o razón social del titular, sin el NIF que la FNMT le pega al final del CN ("GARCIA LOPEZ ANA - 89890001K").

Importa porque la AEAT identifica al obligado por el PAR NIF + NombreRazon: con el NIF correcto y un nombre que no cuadre responde 4104, y el mensaje habla solo del NIF aunque en el detalle devuelva los dos campos.



93
94
95
96
97
98
# File 'lib/verifactu_rails/certificado.rb', line 93

def titular
  cn = sujeto['CN']
  return nil if cn.nil?

  cn.sub(/\s*-\s*#{Regexp.escape(nif.to_s)}\s*\z/, '').strip
end