|
55 | 55 | import java.util.Properties; |
56 | 56 | import java.util.Set; |
57 | 57 | import java.util.concurrent.ConcurrentHashMap; |
| 58 | +import java.util.concurrent.ExecutionException; |
| 59 | +import java.util.concurrent.TimeUnit; |
58 | 60 | import java.util.concurrent.atomic.AtomicBoolean; |
59 | 61 | import javax.security.auth.x500.X500Principal; |
60 | 62 | import javax.xml.bind.DatatypeConverter; |
61 | 63 |
|
| 64 | +import com.google.common.cache.CacheBuilder; |
| 65 | +import com.google.common.cache.CacheLoader; |
| 66 | +import com.google.common.cache.LoadingCache; |
62 | 67 | import org.apache.commons.io.FileUtils; |
63 | 68 | import org.apache.commons.logging.Log; |
64 | 69 | import org.apache.commons.logging.LogFactory; |
|
80 | 85 | import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; |
81 | 86 | import org.bouncycastle.openssl.jcajce.JcaPEMWriter; |
82 | 87 | import org.bouncycastle.operator.ContentSigner; |
83 | | -import org.bouncycastle.operator.OperatorCreationException; |
84 | 88 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; |
85 | 89 | import org.bouncycastle.pkcs.PKCS10CertificationRequest; |
86 | 90 | import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; |
|
97 | 101 |
|
98 | 102 | public class CryptoPrimitives implements CryptoSuite { |
99 | 103 | private static final Log logger = LogFactory.getLog(CryptoPrimitives.class); |
| 104 | + private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); |
100 | 105 | private static final Config config = Config.getConfig(); |
101 | 106 | private static final boolean IS_TRACE_LEVEL = logger.isTraceEnabled(); |
102 | 107 |
|
@@ -144,16 +149,10 @@ Provider setUpExplicitProvider(String securityProviderClassName) throws Instanti |
144 | 149 | } |
145 | 150 |
|
146 | 151 | Class<?> aClass = Class.forName(securityProviderClassName); |
147 | | - if (null == aClass) { |
148 | | - throw new InstantiationException(format("Getting class for security provider %s returned null ", securityProviderClassName)); |
149 | | - } |
150 | 152 | if (!Provider.class.isAssignableFrom(aClass)) { |
151 | 153 | throw new InstantiationException(format("Class for security provider %s is not a Java security provider", aClass.getName())); |
152 | 154 | } |
153 | 155 | Provider securityProvider = (Provider) aClass.newInstance(); |
154 | | - if (securityProvider == null) { |
155 | | - throw new InstantiationException(format("Creating instance of security %s returned null ", aClass.getName())); |
156 | | - } |
157 | 156 | return securityProvider; |
158 | 157 | } |
159 | 158 |
|
@@ -207,61 +206,51 @@ public Certificate bytesToCertificate(byte[] certBytes) throws CryptoException { |
207 | 206 | * @param pemCertificate |
208 | 207 | * @return |
209 | 208 | */ |
210 | | - |
211 | | - private X509Certificate getX509Certificate(byte[] pemCertificate) throws CryptoException { |
| 209 | + private X509Certificate getX509CertificateInternal(byte[] pemCertificate) throws CryptoException { |
212 | 210 | X509Certificate ret = null; |
213 | 211 | CryptoException rete = null; |
214 | 212 |
|
215 | 213 | List<Provider> providerList = new LinkedList<>(Arrays.asList(Security.getProviders())); |
216 | | - if (SECURITY_PROVIDER != null) { //Add if overridden |
| 214 | + if (SECURITY_PROVIDER != null) { |
| 215 | + // Add if overridden. Note it is added to the end of the provider list so is only invoked if all other providers fail. |
217 | 216 | providerList.add(SECURITY_PROVIDER); |
218 | 217 | } |
219 | | - try { |
220 | | - providerList.add(BouncyCastleProvider.class.newInstance()); // bouncy castle is there always. |
221 | | - } catch (Exception e) { |
222 | | - logger.warn(e); |
223 | 218 |
|
224 | | - } |
| 219 | + providerList.add(BOUNCY_CASTLE_PROVIDER); |
| 220 | + |
225 | 221 | for (Provider provider : providerList) { |
226 | 222 | try { |
227 | 223 | if (null == provider) { |
228 | | - continue; |
| 224 | + continue; |
229 | 225 | } |
230 | 226 | CertificateFactory certFactory = CertificateFactory.getInstance(CERTIFICATE_FORMAT, provider); |
231 | | - if (null != certFactory) { |
232 | | - try (ByteArrayInputStream bis = new ByteArrayInputStream(pemCertificate)) { |
233 | | - Certificate certificate = certFactory.generateCertificate(bis); |
234 | | - |
235 | | - if (certificate instanceof X509Certificate) { |
236 | | - ret = (X509Certificate) certificate; |
237 | | - rete = null; |
238 | | - break; |
239 | | - } |
240 | | - } |
| 227 | + try (ByteArrayInputStream bis = new ByteArrayInputStream(pemCertificate)) { |
| 228 | + Certificate certificate = certFactory.generateCertificate(bis); |
241 | 229 |
|
| 230 | + if (certificate instanceof X509Certificate) { |
| 231 | + ret = (X509Certificate) certificate; |
| 232 | + rete = null; |
| 233 | + break; |
| 234 | + } |
242 | 235 | } |
243 | 236 | } catch (Exception e) { |
244 | | - |
245 | 237 | rete = new CryptoException(e.getMessage(), e); |
246 | | - |
247 | 238 | } |
248 | | - |
249 | 239 | } |
250 | 240 |
|
251 | 241 | if (null != rete) { |
252 | | - |
253 | 242 | throw rete; |
254 | | - |
255 | 243 | } |
256 | 244 |
|
257 | 245 | if (ret == null) { |
258 | | - |
259 | 246 | logger.error("Could not convert pem bytes"); |
260 | | - |
261 | 247 | } |
262 | 248 |
|
263 | 249 | return ret; |
| 250 | + } |
264 | 251 |
|
| 252 | + private X509Certificate getX509Certificate(byte[] pemCertificate) throws CryptoException { |
| 253 | + return getCertificateValue(pemCertificate).getX509(); |
265 | 254 | } |
266 | 255 |
|
267 | 256 | /** |
@@ -309,20 +298,14 @@ public boolean verify(byte[] pemCertificate, String signatureAlgorithm, byte[] s |
309 | 298 | } |
310 | 299 | } |
311 | 300 |
|
312 | | - try { |
313 | | - |
314 | | - X509Certificate certificate = getX509Certificate(pemCertificate); |
| 301 | + X509Certificate certificate = getValidCertificate(pemCertificate); |
315 | 302 |
|
| 303 | + try { |
316 | 304 | if (certificate != null) { |
317 | | - |
318 | | - isVerified = validateCertificate(certificate); |
319 | | - if (isVerified) { // only proceed if cert is trusted |
320 | | - |
321 | | - Signature sig = Signature.getInstance(signatureAlgorithm); |
322 | | - sig.initVerify(certificate); |
323 | | - sig.update(plainText); |
324 | | - isVerified = sig.verify(signature); |
325 | | - } |
| 305 | + Signature sig = Signature.getInstance(signatureAlgorithm); |
| 306 | + sig.initVerify(certificate); |
| 307 | + sig.update(plainText); |
| 308 | + isVerified = sig.verify(signature); |
326 | 309 | } |
327 | 310 | } catch (InvalidKeyException e) { |
328 | 311 | CryptoException ex = new CryptoException("Cannot verify signature. Error is: " |
@@ -570,14 +553,8 @@ boolean validateCertificate(byte[] certPEM) { |
570 | 553 | } |
571 | 554 |
|
572 | 555 | try { |
573 | | - |
574 | | - X509Certificate certificate = getX509Certificate(certPEM); |
575 | | - if (null == certificate) { |
576 | | - throw new Exception("Certificate transformation returned null"); |
577 | | - } |
578 | | - |
579 | | - return validateCertificate(certificate); |
580 | | - } catch (Exception e) { |
| 556 | + return x509Cache.get(new CertKey(certPEM)).isValid(); |
| 557 | + } catch (ExecutionException | CryptoException e) { |
581 | 558 | logger.error("Cannot validate certificate. Error is: " + e.getMessage() + "\r\nCertificate (PEM, hex): " |
582 | 559 | + DatatypeConverter.printHexBinary(certPEM)); |
583 | 560 | return false; |
@@ -1007,4 +984,169 @@ public byte[] certificateToDER(String certificatePEM) { |
1007 | 984 | return content; |
1008 | 985 | } |
1009 | 986 |
|
| 987 | + |
| 988 | + private static final long X509_RECHECK_MILLIS = TimeUnit.MINUTES.toMillis(30); |
| 989 | + |
| 990 | + /** |
| 991 | + * Cache for key for X.509 certificates. |
| 992 | + */ |
| 993 | + private static class CertKey { |
| 994 | + /** Pre-generated hash code for performance. */ |
| 995 | + final int hashCode; |
| 996 | + |
| 997 | + /** The raw PEM data. */ |
| 998 | + final byte[] pemData; |
| 999 | + |
| 1000 | + |
| 1001 | + /** |
| 1002 | + * New instance. |
| 1003 | + * |
| 1004 | + * @param pemData the PEM data |
| 1005 | + */ |
| 1006 | + CertKey(byte[] pemData) { |
| 1007 | + // defensive copy |
| 1008 | + this.pemData = pemData.clone(); |
| 1009 | + |
| 1010 | + // pre-calculate the hash code |
| 1011 | + hashCode = Arrays.hashCode(pemData); |
| 1012 | + } |
| 1013 | + |
| 1014 | + |
| 1015 | + @Override |
| 1016 | + public boolean equals(Object o) { |
| 1017 | + if (this == o) { |
| 1018 | + return true; |
| 1019 | + } |
| 1020 | + if (!(o instanceof CertKey)) { |
| 1021 | + return false; |
| 1022 | + } |
| 1023 | + CertKey certKey = (CertKey) o; |
| 1024 | + return hashCode == certKey.hashCode && Arrays.equals(pemData, certKey.pemData); |
| 1025 | + } |
| 1026 | + |
| 1027 | + |
| 1028 | + @Override |
| 1029 | + public int hashCode() { |
| 1030 | + return hashCode; |
| 1031 | + } |
| 1032 | + |
| 1033 | + } |
| 1034 | + |
| 1035 | + |
| 1036 | + /** A cached X.509 certificate. */ |
| 1037 | + private class CertValue { |
| 1038 | + /** Link back to the cache key. */ |
| 1039 | + final CertKey key; |
| 1040 | + |
| 1041 | + /** Indicator if the X.509 certificate is currently valid. Note: it may become valid at some point in the future. */ |
| 1042 | + boolean isValid; |
| 1043 | + |
| 1044 | + /** The next time this certificate will be checked, in milliseconds since the epoch. */ |
| 1045 | + long nextCheckTime; |
| 1046 | + |
| 1047 | + /** The certificate derived from the PEM data. */ |
| 1048 | + X509Certificate x509; |
| 1049 | + |
| 1050 | + |
| 1051 | + /** |
| 1052 | + * New instance. |
| 1053 | + * |
| 1054 | + * @param key the cache key |
| 1055 | + */ |
| 1056 | + CertValue(CertKey key) { |
| 1057 | + this.key = key; |
| 1058 | + x509 = null; |
| 1059 | + isValid = false; |
| 1060 | + nextCheckTime = Long.MIN_VALUE; |
| 1061 | + } |
| 1062 | + |
| 1063 | + |
| 1064 | + /** |
| 1065 | + * Get the certificate if it is valid. |
| 1066 | + * |
| 1067 | + * @return the certificate, or null |
| 1068 | + * |
| 1069 | + * @throws CryptoException if the certificate cannot be parsed. |
| 1070 | + */ |
| 1071 | + public synchronized X509Certificate getValid() throws CryptoException { |
| 1072 | + return isValid() ? getX509() : null; |
| 1073 | + } |
| 1074 | + |
| 1075 | + |
| 1076 | + /** |
| 1077 | + * Get the X.509 certificate. |
| 1078 | + * |
| 1079 | + * @return the certificate |
| 1080 | + * |
| 1081 | + * @throws CryptoException if the PEM data cannot be parsed. |
| 1082 | + */ |
| 1083 | + public synchronized X509Certificate getX509() throws CryptoException { |
| 1084 | + if (x509 == null) { |
| 1085 | + // If the security providers change then something that could not be parsed could become parsable. |
| 1086 | + try { |
| 1087 | + x509 = getX509CertificateInternal(key.pemData); |
| 1088 | + } catch (CryptoException e) { |
| 1089 | + isValid = false; |
| 1090 | + nextCheckTime = System.currentTimeMillis() + X509_RECHECK_MILLIS; |
| 1091 | + logger.error("Unable to recover X.509 certificate from provided binary data", e); |
| 1092 | + throw e; |
| 1093 | + } |
| 1094 | + } |
| 1095 | + return x509; |
| 1096 | + } |
| 1097 | + |
| 1098 | + |
| 1099 | + /** |
| 1100 | + * Is the X.509 certificate currently valid? |
| 1101 | + * |
| 1102 | + * @return true if the certificate is valid |
| 1103 | + * |
| 1104 | + * @throws CryptoException if the certificate cannot be validated |
| 1105 | + */ |
| 1106 | + public synchronized boolean isValid() throws CryptoException { |
| 1107 | + // Only re-check if a reasonable amount of time has passed. |
| 1108 | + long now = System.currentTimeMillis(); |
| 1109 | + if (nextCheckTime >= now) { |
| 1110 | + return isValid; |
| 1111 | + } |
| 1112 | + |
| 1113 | + nextCheckTime = now + X509_RECHECK_MILLIS; |
| 1114 | + isValid = validateCertificate(getX509()); |
| 1115 | + return isValid; |
| 1116 | + } |
| 1117 | + |
| 1118 | + } |
| 1119 | + |
| 1120 | + |
| 1121 | + private final LoadingCache<CertKey, CertValue> x509Cache = CacheBuilder.newBuilder().maximumSize(1000).build(new CacheLoader<CertKey, CertValue>() { |
| 1122 | + @Override |
| 1123 | + public CertValue load(CertKey key) { |
| 1124 | + return new CertValue(key); |
| 1125 | + } |
| 1126 | + }); |
| 1127 | + |
| 1128 | + public void clearCertificateCache() { |
| 1129 | + x509Cache.invalidateAll(); |
| 1130 | + } |
| 1131 | + |
| 1132 | + private CertValue getCertificateValue(byte[] pemCertificate) throws CryptoException { |
| 1133 | + try { |
| 1134 | + return x509Cache.get(new CertKey(pemCertificate)); |
| 1135 | + } catch (ExecutionException e) { |
| 1136 | + Throwable thrown = e.getCause(); |
| 1137 | + if (thrown instanceof CryptoException) { |
| 1138 | + throw (CryptoException) thrown; |
| 1139 | + } |
| 1140 | + if (thrown instanceof Exception) { |
| 1141 | + throw new CryptoException("Error whilst processing certificate", (Exception) thrown); |
| 1142 | + } |
| 1143 | + throw new CryptoException("Error whilst processing certificate", e); |
| 1144 | + } |
| 1145 | + } |
| 1146 | + |
| 1147 | + |
| 1148 | + private X509Certificate getValidCertificate(byte[] pemCertificate) throws CryptoException { |
| 1149 | + return getCertificateValue(pemCertificate).getValid(); |
| 1150 | + } |
| 1151 | + |
1010 | 1152 | } |
0 commit comments