/* * Copyright (c) 2013 ICM Uniwersytet Warszawski All rights reserved. * See LICENCE.txt file for licensing information. */ package eu.emi.security.authn.x509.helpers.trust; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.security.auth.x500.X500Principal; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1OutputStream; import org.bouncycastle.asn1.ASN1String; import org.bouncycastle.asn1.DERBMPString; import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.DERPrintableString; import org.bouncycastle.asn1.DERT61String; import org.bouncycastle.asn1.DERUTF8String; import org.bouncycastle.asn1.DERUniversalString; import org.bouncycastle.asn1.DERVisibleString; import org.bouncycastle.asn1.x500.AttributeTypeAndValue; import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.MD5Digest; import org.bouncycastle.crypto.digests.SHA1Digest; import eu.emi.security.authn.x509.helpers.CertificateHelpers; /** * Several static methods helping to mangle truststore file paths in openssl style. * * @author K. Benedyczak */ public class OpensslTruststoreHelper { public static final String CERT_REGEXP = "^([0-9a-fA-F]{8})\\.[\\d]+$"; /** * @param certLocation certificate location * @param suffix either '.namespaces' or '.signing_policy' (other will work but rather doesn't make sense) * @return A proper name of a namespaces or signing policy file for the given base * path of CA certificate. */ public static String getNsFile(String certLocation, String suffix) { String fileHash = getFileHash(certLocation, CERT_REGEXP); if (fileHash == null) return null; File f = new File(certLocation); String parent = f.getParent(); if (parent == null) parent = "."; return parent + File.separator + fileHash + suffix; } public static String getFileHash(String path, String regexp) { File f = new File(path); String name = f.getName(); Pattern pattern = Pattern.compile(regexp); Matcher m = pattern.matcher(name); if (!m.matches()) return null; return m.group(1); } public static Collection<File> getFilesWithRegexp(String regexp, File directory) { final Pattern pattern = Pattern.compile(regexp); return FileUtils.listFiles(directory, new IOFileFilter() { @Override public boolean accept(File dir, String name) { return pattern.matcher(name).matches(); } @Override public boolean accept(File file) { return accept(null, file.getName()); } }, null); } public static String getOpenSSLCAHash(X500Principal name, boolean openssl1Mode) { return openssl1Mode ? getOpenSSLCAHashNew(name) : getOpenSSLCAHashOld(name); } /** * Generates the hex hash of the DN used by openssl to name the CA * certificate files. The hash is actually the hex of 8 least * significant bytes of a MD5 digest of the the ASN.1 encoded DN. * * @param name the DN to hash. * @return the 8 character string of the hexadecimal MD5 hash. */ private static String getOpenSSLCAHashOld(X500Principal name) { byte[] bytes = name.getEncoded(); MD5Digest digest = new MD5Digest(); digest.update(bytes, 0, bytes.length); byte output[] = new byte[digest.getDigestSize()]; digest.doFinal(output, 0); String ret = String.format("%02x%02x%02x%02x", output[3] & 0xFF, output[2] & 0xFF, output[1] & 0xFF, output[0] & 0xFF); return ret; } /** * Generates the hex hash of the DN used by openssl 1.0.0 and above to name the CA * certificate files. The hash is actually the hex of 8 least * significant bytes of a SHA1 digest of the the ASN.1 encoded DN after normalization. * <p> * The normalization is performed as follows: * all strings are converted to UTF8, leading, trailing and multiple spaces collapsed, * converted to lower case and the leading SEQUENCE header is removed. * * @param name the DN to hash. * @return the 8 character string of the hexadecimal MD5 hash. */ private static String getOpenSSLCAHashNew(X500Principal name) { byte[] bytes; try { RDN[] c19nrdns = getNormalizedRDNs(name); bytes = encodeWithoutSeqHeader(c19nrdns); } catch (IOException e) { throw new IllegalArgumentException("Can't parse the input DN", e); } Digest digest = new SHA1Digest(); digest.update(bytes, 0, bytes.length); byte output[] = new byte[digest.getDigestSize()]; digest.doFinal(output, 0); return String.format("%02x%02x%02x%02x", output[3] & 0xFF, output[2] & 0xFF, output[1] & 0xFF, output[0] & 0xFF); } public static RDN[] getNormalizedRDNs(X500Principal name) throws IOException { X500Name dn = CertificateHelpers.toX500Name(name); RDN[] rdns = dn.getRDNs(); RDN[] c19nrdns = new RDN[rdns.length]; int i=0; for (RDN rdn: rdns) { AttributeTypeAndValue[] atvs = rdn.getTypesAndValues(); sortAVAs(atvs); AttributeTypeAndValue[] c19natvs = new AttributeTypeAndValue[atvs.length]; for (int j=0; j<atvs.length; j++) { c19natvs[j] = normalizeStringAVA(atvs[j]); } c19nrdns[i++] = new RDN(c19natvs); } return c19nrdns; } private static void sortAVAs(AttributeTypeAndValue[] atvs) throws IOException { for (int i=0; i<atvs.length; i++) for (int j=i+1; j<atvs.length; j++) { if (memcmp(atvs[i].getEncoded(), atvs[j].getEncoded()) < 0) { AttributeTypeAndValue tmp = atvs[i]; atvs[i] = atvs[j]; atvs[j] = tmp; } } } private static int memcmp(byte[] a, byte[] b) { int min = a.length > b.length ? b.length : a.length; for (int i=0; i<min; i++) if (a[i] < b[i]) return -1; else if (a[i] > b[i]) return 1; return a.length - b.length; } private static AttributeTypeAndValue normalizeStringAVA(AttributeTypeAndValue src) { ASN1Encodable srcVal = src.getValue(); if ( !((srcVal instanceof DERPrintableString) || (srcVal instanceof DERUTF8String) || (srcVal instanceof DERIA5String) || (srcVal instanceof DERBMPString) || (srcVal instanceof DERUniversalString) || (srcVal instanceof DERT61String) || (srcVal instanceof DERVisibleString))) return src; ASN1String srcString = (ASN1String) srcVal; String value = srcString.getString(); value = value.trim(); value = value.replaceAll("[ \t\n\f][ \t\n\f]+", " "); value = value.toLowerCase(); DERUTF8String newValue = new DERUTF8String(value); return new AttributeTypeAndValue(src.getType(), newValue); } private static byte[] encodeWithoutSeqHeader(RDN[] rdns) throws IOException { ByteArrayOutputStream bOut = new ByteArrayOutputStream(); ASN1OutputStream aOut = new ASN1OutputStream(bOut); for (RDN rdn: rdns) { aOut.writeObject(rdn); } aOut.close(); return bOut.toByteArray(); } }