package net.i2p.crypto; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.CertificateEncodingException; import java.security.cert.CertStore; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.CRL; import java.security.cert.CRLException; import java.security.cert.X509Certificate; import java.security.cert.X509CRL; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.KeySpec; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import javax.security.auth.x500.X500Principal; import net.i2p.I2PAppContext; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.util.Log; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SystemVersion; /** * Java X.509 certificate utilities, consolidated from various places. * * @since 0.9.9 */ public final class CertUtil { private static final String CERT_DIR = "certificates"; private static final String REVOCATION_DIR = "revocations"; private static final int LINE_LENGTH = 64; /** * Write a certificate to a file in base64 format. * * @return success * @since 0.8.2, moved from SSLEepGet in 0.9.9 */ public static boolean saveCert(Certificate cert, File file) { OutputStream os = null; try { os = new SecureFileOutputStream(file); exportCert(cert, os); return true; } catch (CertificateEncodingException cee) { error("Error writing X509 Certificate " + file.getAbsolutePath(), cee); return false; } catch (IOException ioe) { error("Error writing X509 Certificate " + file.getAbsolutePath(), ioe); return false; } finally { try { if (os != null) os.close(); } catch (IOException foo) {} } } /** * Writes the private key and all certs in base64 format. * Does NOT close the stream. Throws on all errors. * * @param pk non-null * @param certs certificate chain, null or empty to export pk only * @throws InvalidKeyException if the key does not support encoding * @throws CertificateEncodingException if a cert does not support encoding * @since 0.9.24 */ public static void exportPrivateKey(PrivateKey pk, Certificate[] certs, OutputStream out) throws IOException, GeneralSecurityException { exportPrivateKey(pk, out); if (certs == null) return; for (int i = 0; i < certs.length; i++) { exportCert(certs[i], out); } } /** * Modified from: * http://www.exampledepot.com/egs/java.security.cert/ExportCert.html * * Writes a certificate in base64 format. * Does NOT close the stream. Throws on all errors. * * @since 0.9.24, pulled out of saveCert(), public since 0.9.25 */ public static void exportCert(Certificate cert, OutputStream out) throws IOException, CertificateEncodingException { // Get the encoded form which is suitable for exporting byte[] buf = cert.getEncoded(); writePEM(buf, "CERTIFICATE", out); } /** * Modified from: * http://www.exampledepot.com/egs/java.security.cert/ExportCert.html * * Writes a private key in base64 format. * Does NOT close the stream. Throws on all errors. * * @throws InvalidKeyException if the key does not support encoding * @since 0.9.24 */ private static void exportPrivateKey(PrivateKey pk, OutputStream out) throws IOException, InvalidKeyException { // Get the encoded form which is suitable for exporting byte[] buf = pk.getEncoded(); if (buf == null) throw new InvalidKeyException("encoding unsupported for this key"); writePEM(buf, "PRIVATE KEY", out); } /** * Modified from: * http://www.exampledepot.com/egs/java.security.cert/ExportCert.html * * Writes data in base64 format. * Does NOT close the stream. Throws on all errors. * * @since 0.9.25 consolidated from other methods */ private static void writePEM(byte[] buf, String what, OutputStream out) throws IOException { PrintWriter wr = new PrintWriter(new OutputStreamWriter(out, "UTF-8")); wr.println("-----BEGIN " + what + "-----"); String b64 = Base64.encode(buf, true); // true = use standard alphabet for (int i = 0; i < b64.length(); i += LINE_LENGTH) { wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length()))); } wr.println("-----END " + what + "-----"); wr.flush(); if (wr.checkError()) throw new IOException("Failed write to " + out); } /** * Get a value out of the subject distinguished name. * * Warning - unsupported in Android (no javax.naming), returns null. * * @param type e.g. "CN" * @return value or null if not found */ public static String getSubjectValue(X509Certificate cert, String type) { X500Principal p = cert.getSubjectX500Principal(); return getValue(p, type); } /** * Get a value out of the issuer distinguished name. * * Warning - unsupported in Android (no javax.naming), returns null. * * @param type e.g. "CN" * @return value or null if not found * @since 0.9.24 */ public static String getIssuerValue(X509Certificate cert, String type) { X500Principal p = cert.getIssuerX500Principal(); return getValue(p, type); } /** * Get a value out of a X500Principal. * * Warning - unsupported in Android (no javax.naming), returns null. * * @param type e.g. "CN" * @return value or null if not found */ private static String getValue(X500Principal p, String type) { if (SystemVersion.isAndroid()) { error("Don't call this in Android", new UnsupportedOperationException("I did it")); return null; } if (p == null) return null; type = type.toUpperCase(Locale.US); String subj = p.getName(); // Use reflection for this to avoid VerifyErrors on some Androids try { Class<?> ldapName = Class.forName("javax.naming.ldap.LdapName"); Constructor<?> ldapCtor = ldapName.getConstructor(String.class); Object name = ldapCtor.newInstance(subj); Method getRdns = ldapName.getDeclaredMethod("getRdns"); Class<?> rdnClass = Class.forName("javax.naming.ldap.Rdn"); Method getType = rdnClass.getDeclaredMethod("getType"); Method getValue = rdnClass.getDeclaredMethod("getValue"); for (Object rdn : (List) getRdns.invoke(name)) { if (type.equals(((String) getType.invoke(rdn)).toUpperCase(Locale.US))) return (String) getValue.invoke(rdn); } } catch (ClassNotFoundException e) { } catch (IllegalAccessException e) { } catch (InstantiationException e) { } catch (InvocationTargetException e) { } catch (NoSuchMethodException e) { } return null; } private static void error(String msg, Throwable t) { log(I2PAppContext.getGlobalContext(), Log.ERROR, msg, t); } //private static void error(I2PAppContext ctx, String msg, Throwable t) { // log(ctx, Log.ERROR, msg, t); //} private static void log(I2PAppContext ctx, int level, String msg, Throwable t) { Log l = ctx.logManager().getLog(CertUtil.class); l.log(level, msg, t); } /** * Get the Java public key from a X.509 certificate file. * Throws if the certificate is invalid (e.g. expired). * * This DOES check for revocation. * * @return non-null, throws on all errors including certificate invalid * @since 0.9.24 moved from SU3File private method */ public static PublicKey loadKey(File kd) throws IOException, GeneralSecurityException { X509Certificate cert = loadCert(kd); if (isRevoked(cert)) throw new CRLException("Certificate is revoked"); return cert.getPublicKey(); } /** * Get the certificate from a X.509 certificate file. * Throws if the certificate is invalid (e.g. expired). * * This does NOT check for revocation. * * @return non-null, throws on all errors including certificate invalid * @since 0.9.24 adapted from SU3File private method */ public static X509Certificate loadCert(File kd) throws IOException, GeneralSecurityException { InputStream fis = null; try { fis = new FileInputStream(kd); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate)cf.generateCertificate(fis); cert.checkValidity(); return cert; } catch (IllegalArgumentException iae) { // java 1.8.0_40-b10, openSUSE // Exception in thread "main" java.lang.IllegalArgumentException: Input byte array has wrong 4-byte ending unit // at java.util.Base64$Decoder.decode0(Base64.java:704) throw new GeneralSecurityException("cert error", iae); } finally { try { if (fis != null) fis.close(); } catch (IOException foo) {} } } /** * Get a single Private Key from an input stream. * Does NOT close the stream. * * @return non-null, non-empty, throws on all errors including certificate invalid * @since 0.9.25 */ public static PrivateKey loadPrivateKey(InputStream in) throws IOException, GeneralSecurityException { try { String line; while ((line = DataHelper.readLine(in)) != null) { if (line.startsWith("---") && line.contains("BEGIN") && line.contains("PRIVATE")) break; } if (line == null) throw new IOException("no private key found"); StringBuilder buf = new StringBuilder(128); while ((line = DataHelper.readLine(in)) != null) { if (line.startsWith("---")) break; buf.append(line.trim()); } if (buf.length() <= 0) throw new IOException("no private key found"); byte[] data = Base64.decode(buf.toString(), true); if (data == null) throw new CertificateEncodingException("bad base64 cert"); PrivateKey rv = null; // try all the types for (SigAlgo algo : EnumSet.allOf(SigAlgo.class)) { try { KeySpec ks = new PKCS8EncodedKeySpec(data); String alg = algo.getName(); KeyFactory kf = KeyFactory.getInstance(alg); rv = kf.generatePrivate(ks); break; } catch (GeneralSecurityException gse) { //gse.printStackTrace(); } } if (rv == null) throw new InvalidKeyException("unsupported key type"); return rv; } catch (IllegalArgumentException iae) { // java 1.8.0_40-b10, openSUSE // Exception in thread "main" java.lang.IllegalArgumentException: Input byte array has wrong 4-byte ending unit // at java.util.Base64$Decoder.decode0(Base64.java:704) throw new GeneralSecurityException("key error", iae); } } /** * Get one or more certificates from an input stream. * Throws if any certificate is invalid (e.g. expired). * Does NOT close the stream. * * This does NOT check for revocation. * * @return non-null, non-empty, throws on all errors including certificate invalid * @since 0.9.25 */ public static List<X509Certificate> loadCerts(InputStream in) throws IOException, GeneralSecurityException { try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); Collection<? extends Certificate> certs = cf.generateCertificates(in); List<X509Certificate> rv = new ArrayList<X509Certificate>(certs.size()); for (Certificate cert : certs) { if (!(cert instanceof X509Certificate)) throw new GeneralSecurityException("not a X.509 cert"); X509Certificate xcert = (X509Certificate) cert; xcert.checkValidity(); rv.add(xcert); } if (rv.isEmpty()) throw new IOException("no certs found"); return rv; } catch (IllegalArgumentException iae) { // java 1.8.0_40-b10, openSUSE // Exception in thread "main" java.lang.IllegalArgumentException: Input byte array has wrong 4-byte ending unit // at java.util.Base64$Decoder.decode0(Base64.java:704) throw new GeneralSecurityException("cert error", iae); } finally { try { in.close(); } catch (IOException foo) {} } } /** * Write a CRL to a file in base64 format. * * @return success * @since 0.9.25 */ public static boolean saveCRL(X509CRL crl, File file) { OutputStream os = null; try { os = new SecureFileOutputStream(file); exportCRL(crl, os); return true; } catch (CRLException ce) { error("Error writing X509 CRL " + file.getAbsolutePath(), ce); return false; } catch (IOException ioe) { error("Error writing X509 CRL " + file.getAbsolutePath(), ioe); return false; } finally { try { if (os != null) os.close(); } catch (IOException foo) {} } } /** * Writes a CRL in base64 format. * Does NOT close the stream. Throws on all errors. * * @throws CRLException if the crl does not support encoding * @since 0.9.25 */ public static void exportCRL(X509CRL crl, OutputStream out) throws IOException, CRLException { byte[] buf = crl.getEncoded(); writePEM(buf, "X509 CRL", out); } /** * Is the certificate revoked? * This loads the CRLs from disk. * For efficiency, call loadCRLs() and then pass to isRevoked(). * * @since 0.9.25 */ public static boolean isRevoked(Certificate cert) { return isRevoked(I2PAppContext.getGlobalContext(), cert); } /** * Is the certificate revoked? * This loads the CRLs from disk. * For efficiency, call loadCRLs() and then pass to isRevoked(). * * @since 0.9.25 */ public static boolean isRevoked(I2PAppContext ctx, Certificate cert) { CertStore store = loadCRLs(ctx); return isRevoked(store, cert); } /** * Is the certificate revoked? * * @since 0.9.25 */ public static boolean isRevoked(CertStore store, Certificate cert) { try { for (CRL crl : store.getCRLs(null)) { if (crl.isRevoked(cert)) return true; } } catch (GeneralSecurityException gse) {} return false; } /** * Load CRLs from standard locations. * * @return non-null, possibly empty * @since 0.9.25 */ public static CertStore loadCRLs() { return loadCRLs(I2PAppContext.getGlobalContext()); } /** * Load CRLs from standard locations. * * @return non-null, possibly empty * @since 0.9.25 */ public static CertStore loadCRLs(I2PAppContext ctx) { Set<X509CRL> crls = new HashSet<X509CRL>(8); File dir = new File(ctx.getBaseDir(), CERT_DIR); dir = new File(dir, REVOCATION_DIR); loadCRLs(crls, dir); boolean diff = true; try { diff = !ctx.getBaseDir().getCanonicalPath().equals(ctx.getConfigDir().getCanonicalPath()); } catch (IOException ioe) {} if (diff) { File dir2 = new File(ctx.getConfigDir(), CERT_DIR); dir2 = new File(dir2, REVOCATION_DIR); loadCRLs(crls, dir2); } //System.out.println("Loaded " + crls.size() + " CRLs"); CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(crls); try { CertStore store = CertStore.getInstance("Collection", ccsp); return store; } catch (GeneralSecurityException gse) { // shouldn't happen error("CertStore", gse); throw new UnsupportedOperationException(gse); } } /** * Load CRLs from the directory into the set. * * @since 0.9.25 */ private static void loadCRLs(Set<X509CRL> crls, File dir) { if (dir.exists() && dir.isDirectory()) { File[] files = dir.listFiles(); if (files != null) { for (int i = 0; i < files.length; i++) { File f = files[i]; if (!f.isFile()) continue; if (f.getName().endsWith(".crl")) { try { X509CRL crl = loadCRL(f); crls.add(crl); } catch (IOException ioe) { error("Cannot load CRL from " + f, ioe); } catch (GeneralSecurityException crle) { error("Cannot load CRL from " + f, crle); } } } } } } /** * Load a CRL. * * @return non-null, possibly empty * @since 0.9.25 */ private static X509CRL loadCRL(File file) throws IOException, GeneralSecurityException { InputStream in = null; try { in = new FileInputStream(file); return loadCRL(in); } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} } } /** * Load a CRL. Does NOT Close the stream. * * @return non-null * @since 0.9.25 public since 0.9.26 */ public static X509CRL loadCRL(InputStream in) throws GeneralSecurityException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); return (X509CRL) cf.generateCRL(in); } public static final void main(String[] args) { if (args.length < 2) { System.out.println("Usage: [loadcert | loadcrl | loadcrldir | loadcrldirs | isrevoked | loadprivatekey] file"); System.exit(1); } try { File f = new File(args[1]); if (args[0].equals("loadcert")) { X509Certificate cert = loadCert(f); System.out.println(net.i2p.util.HexDump.dump(cert.getEncoded())); } else if (args[0].equals("loadcrl")) { loadCRL(f); } else if (args[0].equals("loadcrldir")) { Set<X509CRL> crls = new HashSet<X509CRL>(8); loadCRLs(crls, f); System.out.println("Found " + crls.size() + " CRLs"); } else if (args[0].equals("loadcrldirs")) { CertStore store = loadCRLs(I2PAppContext.getGlobalContext()); Collection<? extends CRL> crls = store.getCRLs(null); System.out.println("Found " + crls.size() + " CRLs"); } else if (args[0].equals("isrevoked")) { Certificate cert = loadCert(f); boolean rv = isRevoked(I2PAppContext.getGlobalContext(), cert); System.out.println("Revoked? " + rv); } else { System.out.println("Usage: [loadcert | loadcrl | loadprivatekey] file"); } } catch (Exception e) { e.printStackTrace(); System.exit(1); } } }