Ayuda para validar FIEL del SAT desde Java
Estoy intentando leer los archivos .cer, .key y la contraseña que forman parte de la FIEL (firma electrónica) del SAT, utilizo la clase java.security.cert.X509Certificate para leer el archivo .cer y ahí no tengo problemas, mi problema viene cuando intento leer el archivo .key (y su contraseña).
Alguien que me pueda orientar en este aspecto??
Saludos
- Inicie sesión o regístrese para enviar comentarios
Proyecto de facturación
Hay un proyecto publicado en github que ya resolvió ese problema. Será bueno que le eches un lente, lo que necesitas está en el paquete security.
https://github.com/bigdata-mx/factura-electronica
El api criptográfico de
El api criptográfico de ORACLE no ofrece como leer la llave privada si ésta tiene una contraseña.
Yo lo resolví utilizando otra libreria llamada not-yet-commons-ssl, te dejo el código:
import java.security.PrivateKey;
import org.apache.commons.io.FileUtils;
import org.apache.commons.ssl.PKCS8Key;
/** Retorna una llave privada utilizando los datos y la passphrase indicada. Se utiliza apache-commons-ssl para esto
* ya que no hay manera simple de hacerlo directo con java */
public static PrivateKey getPrivateKey(File pkeyFile, String passphrase){
try {
byte[] keyBytes = FileUtils.readFileToByteArray(pkeyFile);
return getPrivateKey(keyBytes, passphrase);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Retorna una llave privada utilizando los datos y la passphrase indicada. Se utiliza not-yet-commons-ssl para esto
* ya que no hay manera simple de hacerlo directo con java */
public static PrivateKey getPrivateKey(byte[] encryptedKey, String passphrase){
try {
PKCS8Key pkcs8 = new PKCS8Key(encryptedKey, passphrase.toCharArray());
return pkcs8.getPrivateKey();
} catch (Exception e) {
throw new RuntimeException("Clave inválida");
}
}
Saludos
Exception
Primero que nada muchas gracias por sus respuestas, sin embargo, en ambas soluciones, al ejecutar la línea
me sale la siguiente excepción:
org.apache.commons.ssl.ProbablyNotPKCS8Exception: asn1 parse failure: java.lang.IllegalArgumentException: DEREncodable must be one of: DERSequence, DERSet, DERTaggedObject
at org.apache.commons.ssl.PKCS8Key.(PKCS8Key.java:190)
at org.apache.commons.ssl.PKCS8Key.(PKCS8Key.java:105)
at firmaElectronica.servicio.Servicio.confirmarFIEL(Servicio.java:119)
at firmaElectronica.vista.GUI.actionPerformed(GUI.java:132)
at javax.swing.AbstractButton.fireActionPerformed(Unknown Source)
at javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source)
at javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source)
at javax.swing.DefaultButtonModel.setPressed(Unknown Source)
at javax.swing.plaf.basic.BasicButtonListener.mouseReleased(Unknown Source)
at java.awt.Component.processMouseEvent(Unknown Source)
at javax.swing.JComponent.processMouseEvent(Unknown Source)
at java.awt.Component.processEvent(Unknown Source)
at java.awt.Container.processEvent(Unknown Source)
at java.awt.Component.dispatchEventImpl(Unknown Source)
at java.awt.Container.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.LightweightDispatcher.retargetMouseEvent(Unknown Source)
at java.awt.LightweightDispatcher.processMouseEvent(Unknown Source)
at java.awt.LightweightDispatcher.dispatchEvent(Unknown Source)
at java.awt.Container.dispatchEventImpl(Unknown Source)
at java.awt.Window.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
at java.awt.EventQueue.access$200(Unknown Source)
at java.awt.EventQueue$3.run(Unknown Source)
at java.awt.EventQueue$3.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source)
at java.awt.EventQueue$4.run(Unknown Source)
at java.awt.EventQueue$4.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source)
at java.awt.EventQueue.dispatchEvent(Unknown Source)
at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.run(Unknown Source)
Alguna idea??
Hmm el error indica que el
Hmm el error indica que el archivo que estas tratando de leer no se encuentra bajo el standar PKCS8, o que si lo esta pero no esta codificado mediante DER.
El archivo que le estas pasando es el .key cierto? el que se te generó con el programa Solcedi del sat junto con el otro archivo archivo de requerimiento de certificacion con extension req. El archivo key contiene la llave privada y debe estar con el standar PKCS8 bajo la codificación DER y el archivo req es solo uno de solicitud para que el SAT te genere tu archivo cer.
Para probar si tu archivo key utiliza el estandar PKCS8 puedes hacerlo con openssl, con la siguiente linea:
Le das enter y te pide la contraseña, al escribir la correcta te da una salida en formato PEM, que viene siendo algo como:
Puedes comprobar la información de la llave privada pegando esa salida en el siguiente link: http://lapo.it/asn1js/
Debe contener una secuencia con 3 elementos, el primero un valor integer, el segundo otra secuencia con dos valores y el tercero un valor string, el cual contiene una secuencia de 9 elementos integer que estos representan actualmente la llave privada en el formato tradicional.
Saludos
Me funciono
Muchas gracias por la ayuda, me funciono a la perfección. Saludos.
WHAT
Por favor no hagan lo que dice Nopalin, al menos ese paso de pegar su llave privada en quién sabe qué página.
Es una llave PRIVADA, y la van a ir a pegar en un sitio, sin cifrado ni nada?
Si openssl pudo leer el archivo pkcs8 y escupir esa llave, con eso es suficiente. En todo caso pueden guardar la llave privada en formato PEM (el texto que se ve ahí, que espero por cierto no sea una llave privada real de Nopalin o un cliente suyo, porque la publicó aquí así que de privada ya no tiene nada). Esa llave en formato PEM se puede leer desde Java si problema, usando Bouncy Castle.
A llave que publique le falta
A llave que publique le falta mas del 50% de información, creo que sigue siendo privada.
Con respecto al publicar la llave en el sitio, tienes razón, fue un comentario sin tener en cuenta seguridad, solo que pudiera observar la info contenido en ella.
Leer .key con BC desde Java
Oye ezamudio una pregunta. Ando trabajando en la implementación de la firma en java con BC y cuando comentas:... "Esa llave en formato PEM se puede leer desde Java si problema, usando Bouncy Castle" ¿El archivo .key no se puede leer desde JAVA usando Bouncy Castle? ¿Para leerlo tendría que guardar la llave como .PEM? Realmente quede un poco perdido con esto :(
que exactamente quieres hacer?
que exactamente quieres hacer? yo tengo un sistema de facturacion electronica en java o es algo fuera de esas tareas?
Muchas gracias por tu
Muchas gracias por tu respuesta.
El primer paso de lo que necesito hacer es algo como esto:
https://www.siat.sat.gob.mx/PTSC/auth/faces/pages/validar/fiel_s.jsf
Es decir validar la firma. Esto lo tengo que hacer en una aplicación java que correrá en el equipo del cliente y todo se tiene que validar en el equipo del cliente. O sea que en este primer paso no puede haber comunicación de la aplicación con ningún servicio del sat o algún otro.
Si algún elemento de la firma no es válido (contraseña, certificado, llave privada) entonces se le indica al cliente. Si es válida la firma entonces la aplicación tendrá que enviar a nuestros servidores el archivo .cer y nosotros lo convertiremos a .pem (por que el SAT así me indico que tiene que ser) para conectarnos al servicio que ofrece el sat para validar el certificado. Si el servicio del SAT indica que el certificado no es válido se le manda un mensaje al cliente. Si el certificado si es válido continua el proceso ya con otros temas como firma de documentos.
Pero bueno, el paso donde estoy atorado es el primero =( .
Tengo una aplicación en java que pide los 3 elementos de la firma. El certificado ya pude leerlo desde java pero validar el .key/contraseña no lo he podido hacer por que no he podido leer el archivo .key. Ya he estado en contacto con el SAT y ellos lo que recomiendan (aunque no limitan) es que para java se use Bouncy Castle. Bouncy Castle ya lo tengo como proveedor de seguridad en JAVA sin problemas. Lo que entendí de lo que comentan en este hilo es que para leer/validar el .key desde java ¿Lo tengo que pasar primero a .PEM? Usando openssl fuera de java ya he logrado convertir el .key a .pem pero esto no me sirve de nada por que la aplicación tiene que correr en el lado del cliente y la aplicación misma tendría que ser capaz de leer/validar la firma en su totalidad.
Cualquier ayuda u orientación en verdad la agradecería mucho.
No es necesario utilizar
No es necesario utilizar bouncy castle, yo utilizo not yet commons ssl y trabaja bastante bien. aquí te dejo un código donde se lee la llave privada (ten en cuenta que tambien utilizo otras librerias para faiclitar el trabajo)
byte[] keyBytes = null;
try {
keyBytes = FileUtils.readFileToByteArray(keyFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
PrivateKey pkey = CertUtils.getPrivateKey(keyBytes, clave);
//validamos que el certificado sea un CSD
X509Certificate cer = CertUtils.getCertificate(cerFile);
//los certificados CSD solo permiten la firma digital
int pathLen = cer.getBasicConstraints();
if(pathLen != -1){
throw new RuntimeException("El certificado no es un CSD, posee el atributo de Autoridad Certificadora (CA)");
}
// boolean[] ku = cer.getKeyUsage();
// if(!ku[0] || !ku[1] || ku[2] || ku[3] || ku[4] || ku[5] || ku[6] || ku[7] || ku[8]){
// throw new RuntimeException("El certificado no es un CSD, no posee los usos correctos");
// }
if(CertUtils.getSerial(cer.getSerialNumber()).length() != 20){
throw new RuntimeException("El numero de serie del certificado debe contener 20 caracteres");
}
//encriptamos un texto y lo desencriptamos para validar que las llaves sean correspondientes
String signatureBase64 = CertUtils.signSha1ConRsa(CertUtils.TEST_MSG, pkey);
if(!CertUtils.verifySha1ConRsa(CertUtils.TEST_MSG, signatureBase64, cer)){
throw new RuntimeException("La llave privada y el certificado no corresponden");
}
X500PrincipalHelper issuer = new X500PrincipalHelper(cer.getIssuerX500Principal());
//este atributo indica que el sello fue emitido por un CA del SAT
String unstructuredName = CertUtils.getAttribute(issuer, CertUtils.UnstructuredName);
if(unstructuredName == null){
int result = JOptionPane.showConfirmDialog(getMainWindow(),
"El certificado no posee el atributo '" + CertUtils.UnstructuredName + "' del SAT, ¿Desea continuar agregándolo?",
"Confirmación", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
if(result != JOptionPane.OK_OPTION){
return;
}
}
X500PrincipalHelper subject = new X500PrincipalHelper(cer.getSubjectX500Principal(), X500Principal.RFC1779);
String rfc = StringUtils.trimToNull(CertUtils.getAttribute(subject, "OID.2.5.4.45"));
if(rfc == null){
int result = JOptionPane.showConfirmDialog(getMainWindow(),
"El sello no contiene el RFC del contribuyente, ¿Desea continuar?",
"Confirmación", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
if(result != JOptionPane.OK_OPTION){
return;
}
} else{
String rfcEmp = "ERD800101ERT";//rfc del emisor
if(StringUtils.contains(rfc, '/')){
String[] tokens = StringUtils.split(rfc, '/');
rfc = StringUtils.trimToEmpty(tokens[0]);
}
if(rfcEmp.equals(rfc) == false){
throw new RuntimeException("EL RFC indicado en el sello es diferente al RFC de la empresa");
}
}
public static final String TEST_MSG = "test_message";
public static PrivateKey getPrivateKey(byte[] encryptedKey, String passphrase){
try {
PKCS8Key pkcs8 = new PKCS8Key(encryptedKey, passphrase.toCharArray());
return pkcs8.getPrivateKey();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Clave del archivo privado (archivo key) inválida");
}
}
public static X509Certificate getCertificate(File file){
InputStream is = null;
try {
is = new FileInputStream(file);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate)cf.generateCertificate(is);
return cert;
} catch (Exception ex) {
throw new RuntimeException(ex);
} finally{
try {
is.close();
} catch (IOException e) { }
}
}
public static String getSerial(BigInteger big){
String hex = big.toString(16);
if(hex.length() % 2 != 0){
return com.aipisoft.common.util.StringUtils.EMPTY;
}
StringBuilder serial = new StringBuilder();
int pares = hex.length() / 2;
for(int i=0; i<pares; i++){
serial.append((char)Integer.parseInt(hex.substring(2*i, (2*i)+2), 16));
}
return serial.toString();
}
public static String signSha1ConRsa(String datos, PrivateKey pkey) {
try{ return CertUtils.signSha1ConRsa(datos.getBytes("UTF8"), pkey); } catch(UnsupportedEncodingException ex){ ex.printStackTrace(); throw new RuntimeException(ex);}
}
public static String signSha1ConRsa(byte[] datos, PrivateKey pkey){
return CertUtils.sign("SHA1withRSA", datos, pkey);
}
public static String signSha256ConRsa(String datos, PrivateKey pkey) {
try{ return CertUtils.signSha256ConRsa(datos.getBytes("UTF8"), pkey); } catch(UnsupportedEncodingException ex){ ex.printStackTrace(); throw new RuntimeException(ex);}
}
public static String signSha256ConRsa(byte[] datos, PrivateKey pkey){
return CertUtils.sign("SHA256withRSA", datos, pkey);
}
/** Firma o sella digitalmente datos usando el algoritmo indicado con la llave privada proporcionada, retorna el resultado en Base64 */
public static String sign(String algorithm, byte[] datos, PrivateKey pkey){
try {
Signature signer = Signature.getInstance(algorithm);
signer.initSign(pkey);
signer.update(datos);
return CertUtils.trimBase64(CertUtils.toBase64(signer.sign()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static boolean verifySha1ConRsa(String plainMsg, String signatureBase64, Certificate certificate){
try{
return CertUtils.verifySha1ConRsa(plainMsg.getBytes("UTF8"), signatureBase64, certificate);
} catch(UnsupportedEncodingException ex){ ex.printStackTrace(); throw new RuntimeException(ex);}
}
public static boolean verifySha1ConRsa(byte[] plainMsg, String signatureBase64, Certificate certificate){
return CertUtils.verify("SHA1withRSA", plainMsg, signatureBase64, certificate);
}
public static boolean verifySha256ConRsa(String plainMsg, String signatureBase64, Certificate certificate){
try{
return CertUtils.verifySha256ConRsa(plainMsg.getBytes("UTF8"), signatureBase64, certificate);
} catch(UnsupportedEncodingException ex){ ex.printStackTrace(); throw new RuntimeException(ex);}
}
public static boolean verifySha256ConRsa(byte[] plainMsg, String signatureBase64, Certificate certificate){
return CertUtils.verify("SHA256withRSA", plainMsg, signatureBase64, certificate);
}
public static boolean verify(String algorithm, byte[] plainMsg, String signatureBase64, Certificate certificate){
return CertUtils.verify(algorithm, plainMsg, CertUtils.fromBase64(signatureBase64), certificate);
}
public static boolean verify(String algorithm, byte[] plainMsg, byte[] signature, Certificate certificate){
try {
Signature signer = Signature.getInstance(algorithm);
signer.initVerify(certificate);
signer.update(plainMsg);
return signer.verify(signature);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String getAttribute(X500PrincipalHelper principal, String attrName){
List values = principal.getAllValues(attrName);
return values.size() > 0 ? values.get(0).toString() : null;
}
* Copyright (c) 2008, Jay Rosenthal and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* <a href="http://www.eclipse.org/legal/epl-v10.html
" title="http://www.eclipse.org/legal/epl-v10.html
">http://www.eclipse.org/legal/epl-v10.html
</a> *
* Contributors:
* Jay Rosenthal - initial API and implementation
*******************************************************************************/
package facturacion;
import java.util.ArrayList;
import java.util.Iterator;
import javax.security.auth.x500.X500Principal;
import org.apache.commons.lang3.StringUtils;
/**
* X500PrincipalHelper
* <p>
* Helper class to extract pieces (attributes) of an X500Principal object for
* display in the UI.
* <p>
* This helper uses the X500Principal.RFC2253 format of X500Principal.getname()
* to parse an X500Principal name into it's component parts.
* <p>
* In principals which contain multiple occurrences of the same attribute,the
* default for all the methods is to return the most significant (first)
* attribute found.
*
*/
public class X500PrincipalHelper {
public static int LEASTSIGNIFICANT = 0;
public static int MOSTSIGNIFICANT = 1;
public final static String attrCN = "CN"; //$NON-NLS-1$
public final static String attrOU = "OU"; //$NON-NLS-1$
public final static String attrO = "O"; //$NON-NLS-1$
public final static String attrC = "C"; //$NON-NLS-1$
public final static String attrL = "L"; //$NON-NLS-1$
public final static String attrST = "ST"; //$NON-NLS-1$
public final static String attrSTREET = "STREET";//$NON-NLS-1$
public final static String attrEMAIL = "EMAILADDRESS"; //$NON-NLS-1$
public final static String attrUID = "UID"; //$NON-NLS-1$
ArrayList rdnNameArray = new ArrayList();
private final static String attrTerminator = "="; //$NON-NLS-1$
public X500PrincipalHelper() {
// Do nothing constructor..
// Wont be useful unless setPrincipal is called...
}
public X500PrincipalHelper(X500Principal principal) {
this(principal, X500Principal.RFC2253);
}
public X500PrincipalHelper(X500Principal principal, String format) {
parseDN(principal.getName(format));
}
/**
* Returns an ArrayList containing all the values for the given attribute
* identifier.
* <p>
*
* @param attributeID
* String containing the X500 name attribute whose values are to
* be returned
* @return ArrayList containing the string values of the requested
* attribute. Values are in the order they occur. May be empty.
*
*/
public ArrayList getAllValues(String attributeID) {
ArrayList retList = new ArrayList();
String searchPart = attributeID + attrTerminator;
for (Iterator iter = rdnNameArray.iterator(); iter.hasNext();) {
ArrayList nameList = (ArrayList) iter.next();
String namePart = (String) nameList.get(0);
if (namePart.startsWith(searchPart)) {
// Return the string starting after the ID string and the = sign
// that follows it.
retList.add(namePart.toString().substring(searchPart.length()));
}
}
return retList;
}
/**
* Gets the Country (C) attribute from the given X500Principal object.
* <p>
*
* @return the C attribute.
*
*/
public String getC() {
return findPart(attrC);
}
/**
* Gets the most significant common name (CN) attribute from the given
* X500Principal object. For names that contains multiple attributes of this
* type. The first (most significant) one will be returned
* <p>
*
* @return the Most significant common name attribute.
*
*/
public String getCN() {
return findPart(attrCN);
}
/**
* Gets the Email Address (EMAILADDRESS) attribute from the given
* X500Principal object.
* <p>
*
* @return the EMAILADDRESS attribute.
*
*/
public String getEMAILDDRESS() {
return findPart(attrEMAIL);
}
/**
* Gets the Locale (L) attribute from the given X500Principal object.
* <p>
*
* @return the L attribute.
*
*/
public String getL() {
return findPart(attrL);
}
/**
* Gets the most significant Organization (O) attribute from the given
* X500Principal object. For names that contains multiple attributes of this
* type. The first (most significant) one will be returned
* <p>
*
* @return the Most significant O attribute.
*
*/
public String getO() {
return findPart(attrO);
}
/**
* Gets the most significant Organizational Unit (OU) attribute from the
* given X500Principal object. For names that contains multiple attributes
* of this type. The first (most significant) one will be returned
* <p>
*
* @return the Most significant OU attribute.
*
*/
public String getOU() {
return findPart(attrOU);
}
/**
* Gets the State (ST) attribute from the given X500Principal object.
* <p>
*
* @return the ST attribute.
*
*/
public String getST() {
return findPart(attrST);
}
/**
* Gets the Street (STREET) attribute from the given X500Principal object.
* <p>
*
* @return the STREET attribute.
*
*/
public String getSTREET() {
return findPart(attrSTREET);
}
public String getUID() {
return findPart(attrUID);
}
/**
* Set the X500Principal name object to be parsed.
* <p>
*
* @param principal
* - X500Principal
*/
public void setPrincipal(X500Principal principal) {
setPrincipal(principal, X500Principal.RFC2253);
}
public void setPrincipal(X500Principal principal, String format) {
parseDN(principal.getName(format));
}
private String findPart(String attributeID) {
return findSignificantPart(attributeID, MOSTSIGNIFICANT);
}
private String findSignificantPart(String attributeID, int significance) {
String retNamePart = null;
String searchPart = attributeID + attrTerminator;
for (Iterator iter = rdnNameArray.iterator(); iter.hasNext();) {
ArrayList nameList = (ArrayList) iter.next();
String namePart = (String) nameList.get(0);
if (namePart.startsWith(searchPart)) {
// Return the string starting after the ID string and the = sign
// that follows it.
retNamePart = namePart.toString().substring(searchPart.length());
// By definition the first one is most significant
if (significance == MOSTSIGNIFICANT)
break;
}
}
return retNamePart;
}
/**
* Derived From: org.eclipse.osgi.internal.verifier - DNChainMatching.java
*
* Takes a distinguished name in canonical form and fills in the rdnArray
* with the extracted RDNs.
*
* @param dn
* the distinguished name in canonical form.
* @throws IllegalArgumentException
* if a formatting error is found.
*/
private void parseDN(String dn) throws IllegalArgumentException {
int startIndex = 0;
char c = '\0';
ArrayList nameValues = new ArrayList();
// Clear the existing array, in case this instance is being re-used
rdnNameArray.clear();
while (startIndex < dn.length()) {
int endIndex;
for (endIndex = startIndex; endIndex < dn.length(); endIndex++) {
c = dn.charAt(endIndex);
if (c == ',' || c == '+')
break;
if (c == '\\') {
endIndex++; // skip the escaped char
}
}
if (endIndex > dn.length())
throw new IllegalArgumentException("unterminated escape " + dn); //$NON-NLS-1$
nameValues.add(StringUtils.trimToEmpty(dn.substring(startIndex, endIndex)));
if (c != '+') {
rdnNameArray.add(nameValues);
if (endIndex != dn.length())
nameValues = new ArrayList();
else
nameValues = null;
}
startIndex = endIndex + 1;
}
if (nameValues != null) {
throw new IllegalArgumentException("improperly terminated DN " + dn); //$NON-NLS-1$
}
}
}
Gracias
Te agrdezco mucho compartir tu código. voy a tratar de familiarizarme un poco con not yet commons ssl para ver si lo puedo implementar . Si no, tendré que volver a Bouncy Castle =(