All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.snowflake.client.core.SFTrustManager Maven / Gradle / Ivy

/*
 * Copyright (c) 2012-2018 Snowflake Computing Inc. All rights reserved.
 */

package net.snowflake.client.core;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import net.snowflake.client.log.SFLogger;
import net.snowflake.client.log.SFLoggerFactory;
import net.snowflake.client.util.SFPair;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.ssl.SSLInitializationException;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.ocsp.CertID;
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.TBSCertificate;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.RevokedStatus;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.DigestCalculator;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;


/**
 * SFTrustManager is a composite of TrustManager of the default JVM
 * TrustManager and Snowflake OCSP revocation status checker. Use this
 * when initializing SSLContext object.
 *
 * 
 * {@code
 * TrustManager[] trustManagers = {new SFTrustManager()};
 * SSLContext sslContext = SSLContext.getInstance("TLS");
 * sslContext.init(null, trustManagers, null);
 * }
 * 
*/ class SFTrustManager implements X509TrustManager { private static final SFLogger LOGGER = SFLoggerFactory.getLogger(SFTrustManager.class); private static final ASN1ObjectIdentifier OIDocsp = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1").intern(); private static final ASN1ObjectIdentifier SHA1RSA = new ASN1ObjectIdentifier("1.2.840.113549.1.1.5").intern(); private static final ASN1ObjectIdentifier SHA256RSA = new ASN1ObjectIdentifier("1.2.840.113549.1.1.11").intern(); private static final ASN1ObjectIdentifier SHA384RSA = new ASN1ObjectIdentifier("1.2.840.113549.1.1.12").intern(); private static final ASN1ObjectIdentifier SHA512RSA = new ASN1ObjectIdentifier("1.2.840.113549.1.1.13").intern(); /** * Object mapper for JSON encoding and decoding */ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /** * OCSP response cache file name. Should be identical to other driver's * cache file name. */ public static final String OCSP_CACHE_FILE_NAME = "ocsp_response_cache.json"; /** * OCSP response file cache directory */ private static final File CACHE_DIR; static { // SF_OCSP_RESPONSE_CACHE_DIR is used to specify the directory including // OCSP response cache file. String cacheDir = System.getenv("SF_OCSP_RESPONSE_CACHE_DIR"); if (cacheDir != null) { CACHE_DIR = new File(cacheDir); } else { // use user home directory to store the OCSP cache file String homeDir = System.getProperty("user.home"); if (homeDir == null) { // use tmp dir if not exists. homeDir = System.getProperty("java.io.tmpdir"); } if (Constants.getOS() == Constants.OS.WINDOWS) { CACHE_DIR = new File( new File(new File(new File(homeDir, "AppData"), "Local"), "Snowflake"), "Caches"); } else if (Constants.getOS() == Constants.OS.MAC) { CACHE_DIR = new File(new File(new File(homeDir, "Library"), "Caches"), "Snowflake"); } else { CACHE_DIR = new File(new File(homeDir, ".cache"), "snowflake"); } } if (!CACHE_DIR.exists() && !CACHE_DIR.mkdirs()) { throw new RuntimeException( String.format( "Failed to locate or create the OCSP response cache directory: %s", CACHE_DIR) ); } File cacheFileTmp = new File(CACHE_DIR, OCSP_CACHE_FILE_NAME); try { // create an empty file if not exists and return true. // If exists. the method returns false. // In this particular case, it doesn't matter as long as the file is // writable. cacheFileTmp.createNewFile(); } catch (IOException | SecurityException ex) { throw new RuntimeException( String.format( "Failed to touch the OCSP response cache file: %s", cacheFileTmp.getAbsoluteFile()) ); } } private static final Charset DEFAULT_FILE_ENCODING = Charset.forName("UTF-8"); /** * Default OCSP Cache server host name */ public static final String DEFAULT_OCSP_CACHE_HOST = "http://ocsp.snowflakecomputing.com"; /** * OCSP response cache server URL. */ private static String SF_OCSP_RESPONSE_CACHE_SERVER_URL = String.format( "%s/%s", DEFAULT_OCSP_CACHE_HOST, OCSP_CACHE_FILE_NAME); /** * OCSP Response Cache server Retry URL pattern */ private static String SF_OCSP_RESPONSE_CACHE_SERVER_RETRY_URL_PATTERN; /** * Tolerable validity date range ratio. */ private static final float TOLERABLE_VALIDITY_RANGE_RATIO = 0.01f; /** * Maximum clocktime skew (ms) */ private static final long MAX_CLOCK_SKEW = 900000L; /** * Minimum cache warm up time (ms) */ private static final long MIN_CACHE_WARMUP_TIME = 18000000L; /** * OCSP response cache entry expiration time (s) */ private static final long CACHE_EXPIRATION = 86400L; /** * OCSP response cache lock file expiration time (ms) */ private static final long CACHE_FILE_LOCK_EXPIRATION = 60000L; /** * Maximum retry counter (times) */ private static final int MAX_RETRY_COUNTER = 100; /** * Initial sleeping time in retry (ms) */ private static final long INITIAL_SLEEPING_TIME = 1000L; /** * Maximum sleeping time in retry (ms) */ private static final long MAX_SLEEPING_TIME = 16000L; /** * Map from signature algorithm ASN1 object to the name. */ private static final Map SIGNATURE_OID_TO_STRING = new HashMap<>(); static { SIGNATURE_OID_TO_STRING.put(SHA1RSA, "SHA1withRSA"); SIGNATURE_OID_TO_STRING.put(SHA256RSA, "SHA256withRSA"); SIGNATURE_OID_TO_STRING.put(SHA384RSA, "SHA384withRSA"); SIGNATURE_OID_TO_STRING.put(SHA512RSA, "SHA512withRSA"); } /** * Map from OCSP response code to a string representation. */ private static final Map OCSP_RESPONSE_CODE_TO_STRING = new HashMap<>(); static { OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.SUCCESSFUL, "successful"); OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.MALFORMED_REQUEST, "malformedRequest"); OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.INTERNAL_ERROR, "internalError"); OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.TRY_LATER, "tryLater"); OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.SIG_REQUIRED, "sigRequired"); OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.UNAUTHORIZED, "unauthorized"); } static { // Add Bouncy Castle to the security provider. This is required to // verify the signature on OCSP response and attached certificates. Security.addProvider(new BouncyCastleProvider()); } private static JcaX509CertificateConverter CONVERTER_X509 = new JcaX509CertificateConverter(); /** * RootCA cache */ private static Map ROOT_CA = new HashMap<>(); private final static Object ROOT_CA_LOCK = new Object(); /** * OCSP Response cache */ private final static Map> OCSP_RESPONSE_CACHE = new HashMap<>(); private final static Object OCSP_RESPONSE_CACHE_LOCK = new Object(); private static boolean WAS_CACHE_UPDATED = false; private static boolean WAS_CACHE_READ = false; /** * Date and timestamp format */ private final static SimpleDateFormat DATE_FORMAT_UTC = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); static { DATE_FORMAT_UTC.setTimeZone(TimeZone.getTimeZone("UTC")); } /** * The default JVM Trust manager. */ private final X509TrustManager trustManager; /** * OCSP response cache file */ private final File cacheFile; private final File cacheFileLock; /** * Use OCSP response cache server? */ private final boolean useOcspResponseCacheServer; /** * Constructor with the cache file. If not specified, the default cachefile * is used. * * @param cacheFile cache file. * @param useOcspResponseCacheServer true if use OCSP response cache server is used. */ SFTrustManager(File cacheFile, boolean useOcspResponseCacheServer) { this.trustManager = getTrustManager( KeyManagerFactory.getDefaultAlgorithm()); File cacheFileTmp; if (cacheFile == null) { cacheFileTmp = new File(CACHE_DIR, OCSP_CACHE_FILE_NAME); } else { // Mainly for tests. In order to change the location of cache directory // cacheFileTmp = cacheFile; } String canonicalPath = null; try { canonicalPath = cacheFileTmp.getCanonicalPath(); } catch (IOException ex) { LOGGER.debug("Failed to get the canonical location of the OCSP " + "cache file. No cache will be used."); } this.cacheFile = canonicalPath != null ? new File(canonicalPath) : null; this.cacheFileLock = canonicalPath != null ? new File(canonicalPath + ".lck") : null; synchronized (OCSP_RESPONSE_CACHE_LOCK) { if (!WAS_CACHE_READ) { // read cache file once readOcspResponseCacheFile(this.cacheFile, this.cacheFileLock); WAS_CACHE_READ = true; } } this.useOcspResponseCacheServer = useOcspResponseCacheServer; } /** * Reset OCSP Cache server URL * * @param ocspCacheServerUrl OCSP Cache server URL */ public static void resetOCSPResponseCacherServerURL(String ocspCacheServerUrl) { if (ocspCacheServerUrl == null || SF_OCSP_RESPONSE_CACHE_SERVER_RETRY_URL_PATTERN != null) { return; } SF_OCSP_RESPONSE_CACHE_SERVER_URL = ocspCacheServerUrl; if (!SF_OCSP_RESPONSE_CACHE_SERVER_URL.startsWith(DEFAULT_OCSP_CACHE_HOST)) { try { URL url = new URL(SF_OCSP_RESPONSE_CACHE_SERVER_URL); if (url.getPort() > 0) { SF_OCSP_RESPONSE_CACHE_SERVER_RETRY_URL_PATTERN = String.format("%s://%s:%d/retry/", url.getProtocol(), url.getHost(), url.getPort()) + "%s/%s"; } else { SF_OCSP_RESPONSE_CACHE_SERVER_RETRY_URL_PATTERN = String.format("%s://%s/retry/", url.getProtocol(), url.getHost()) + "%s/%s"; } } catch (IOException e) { throw new RuntimeException( String.format( "Failed to parse SF_OCSP_RESPONSE_CACHE_SERVER_URL: %s", SF_OCSP_RESPONSE_CACHE_SERVER_URL)); } } else { // default OCSP doesn't support retry endpoint yet SF_OCSP_RESPONSE_CACHE_SERVER_RETRY_URL_PATTERN = null; } } /** * Get TrustManager for the algorithm. * This is mainly used to get the JVM default trust manager and * cache all of the root CA. * * @param algorithm algorithm. * @return TrustManager object. */ private X509TrustManager getTrustManager(String algorithm) { try { TrustManagerFactory factory = TrustManagerFactory.getInstance(algorithm); factory.init((KeyStore) null); X509TrustManager ret = null; for (TrustManager tm : factory.getTrustManagers()) { // Multiple TrustManager may be attached. We just need X509 Trust // Manager here. if (tm instanceof X509TrustManager) { ret = (X509TrustManager) tm; break; } } if (ret == null) { return null; } synchronized (ROOT_CA_LOCK) { // cache root CA certificates for later use. if (ROOT_CA.size() == 0) { for (X509Certificate cert : ret.getAcceptedIssuers()) { Certificate bcCert = Certificate.getInstance(cert.getEncoded()); ROOT_CA.put(bcCert.getSubject().hashCode(), bcCert); } } } return ret; } catch (NoSuchAlgorithmException | KeyStoreException | CertificateEncodingException ex) { throw new SSLInitializationException(ex.getMessage(), ex); } } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // default behavior trustManager.checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { trustManager.checkServerTrusted(chain, authType); this.validateRevocationStatus(chain); } @Override public X509Certificate[] getAcceptedIssuers() { return trustManager.getAcceptedIssuers(); } /** * Certificate Revocation checks * * @param chain chain of certificates attached. * @throws CertificateException if any certificate validation fails */ void validateRevocationStatus(X509Certificate[] chain) throws CertificateException { final List bcChain = convertToBouncyCastleCertificate(chain); final List> pairIssuerSubjectList = getPairIssuerSubject(bcChain); synchronized (OCSP_RESPONSE_CACHE_LOCK) { boolean isCached = isCached(pairIssuerSubjectList); if (this.useOcspResponseCacheServer && !isCached) { LOGGER.debug( "Downloading OCSP response cache from the server. URL: {}", SF_OCSP_RESPONSE_CACHE_SERVER_URL); readOcspResponseCacheServer(); // if the cache is downloaded from the server, it should be written // to the file cache at all times. WAS_CACHE_UPDATED = true; } executeRevocationStatusChecks(pairIssuerSubjectList); if (WAS_CACHE_UPDATED) { writeOcspResponseCacheFile(this.cacheFile, this.cacheFileLock); WAS_CACHE_UPDATED = false; } } } /** * Executes the revocation status checks for all chained certificates * * @param pairIssuerSubjectList a list of pair of issuer and subject certificates. * @throws CertificateException raises if any error occurs. */ private void executeRevocationStatusChecks( List> pairIssuerSubjectList) throws CertificateException { long currentTimeSecond = new Date().getTime() / 1000L; try { for (SFPair pairIssuerSubject : pairIssuerSubjectList) { executeOneRevoctionStatusCheck(pairIssuerSubject, currentTimeSecond); } } catch (IOException ex) { LOGGER.debug("Failed to decode CertID. Ignored."); } } /** * Executes a single revocation status check * * @param pairIssuerSubject a pair of issuer and subject certificate * @param currentTimeSecond the current timestamp * @throws IOException raises if encoding fails. * @throws CertificateException if certificate exception is raised. */ private void executeOneRevoctionStatusCheck( SFPair pairIssuerSubject, long currentTimeSecond) throws IOException, CertificateException { OCSPReq req = createRequest(pairIssuerSubject); CertID cid = req.getRequestList()[0].getCertID().toASN1Primitive(); OcspResponseCacheKey keyOcspResponse = new OcspResponseCacheKey( cid.getIssuerNameHash().getEncoded(), cid.getIssuerKeyHash().getEncoded(), cid.getSerialNumber().getValue()); long sleepTime = INITIAL_SLEEPING_TIME; CertificateException error = null; boolean success = false; for (int retry = 0; retry < MAX_RETRY_COUNTER; ++retry) { SFPair value0 = OCSP_RESPONSE_CACHE.get(keyOcspResponse); OCSPResp ocspResp; try { if (value0 == null) { LOGGER.debug("not hit cache."); ocspResp = fetchOcspResponse(pairIssuerSubject, req); OCSP_RESPONSE_CACHE.put( keyOcspResponse, SFPair.of(currentTimeSecond, ocspResp)); WAS_CACHE_UPDATED = true; } else { LOGGER.debug("hit cache."); ocspResp = value0.right; } LOGGER.debug("validating. {}", CertificateIDToString(req.getRequestList()[0].getCertID())); validateRevocationStatusMain(pairIssuerSubject, ocspResp); success = true; break; } catch (CertificateException ex) { if (OCSP_RESPONSE_CACHE.containsKey(keyOcspResponse)) { LOGGER.debug("deleting the invalid OCSP cache."); OCSP_RESPONSE_CACHE.remove(keyOcspResponse); WAS_CACHE_UPDATED = true; } error = ex; LOGGER.debug("Retrying {}/{} after sleeping {}(ms)", retry + 1, MAX_RETRY_COUNTER, sleepTime); try { Thread.sleep(sleepTime); sleepTime = maxLong(MAX_SLEEPING_TIME, sleepTime * 2); } catch (InterruptedException ex0) { // nop } } if (!success) { // still not success, raise an error. throw error; } } } /** * Is OCSP Response cached? * * @param pairIssuerSubjectList a list of pair of issuer and subject certificates * @return true if all of OCSP response are cached else false */ private boolean isCached(List> pairIssuerSubjectList) { long currentTimeSecond = new Date().getTime() / 1000L; boolean isCached = true; try { for (SFPair pairIssuerSubject : pairIssuerSubjectList) { OCSPReq req = createRequest(pairIssuerSubject); CertificateID certificateId = req.getRequestList()[0].getCertID(); LOGGER.debug(CertificateIDToString(certificateId)); CertID cid = certificateId.toASN1Primitive(); OcspResponseCacheKey k = new OcspResponseCacheKey( cid.getIssuerNameHash().getEncoded(), cid.getIssuerKeyHash().getEncoded(), cid.getSerialNumber().getValue()); SFPair res = OCSP_RESPONSE_CACHE.get(k); if (res == null) { LOGGER.debug("Not all OCSP responses for the certificate is in the cache."); isCached = false; break; } else if (currentTimeSecond - CACHE_EXPIRATION > res.left) { LOGGER.debug("Cache for CertID expired."); isCached = false; break; } } } catch (IOException ex) { LOGGER.debug("Failed to encode CertID."); } return isCached; } /** * CertificateID to string * * @param certificateID CertificateID * @return a string representation of CertificateID */ private static String CertificateIDToString(CertificateID certificateID) { return String.format("CertID. NameHash: %s, KeyHash: %s, Serial Number: %s", byteToHexString(certificateID.getIssuerNameHash()), byteToHexString(certificateID.getIssuerKeyHash()), MessageFormat.format("{0,number,#}", certificateID.getSerialNumber())); } /** * Decodes OCSP Response Cache key from JSON * * @param elem A JSON element * @return OcspResponseCacheKey object */ private static SFPair> decodeCacheFromJSON(Map.Entry elem) throws IOException { long currentTimeSecond = new Date().getTime() / 1000; byte[] certIdDer = Base64.decodeBase64(elem.getKey()); DLSequence rawCertId = (DLSequence) ASN1ObjectIdentifier.fromByteArray(certIdDer); ASN1Encodable[] rawCertIdArray = rawCertId.toArray(); byte[] issuerNameHashDer = ((DEROctetString) rawCertIdArray[1]).getEncoded(); byte[] issuerKeyHashDer = ((DEROctetString) rawCertIdArray[2]).getEncoded(); BigInteger serialNumber = ((ASN1Integer) rawCertIdArray[3]).getValue(); OcspResponseCacheKey k = new OcspResponseCacheKey( issuerNameHashDer, issuerKeyHashDer, serialNumber); JsonNode ocspRespBase64 = elem.getValue(); if (!ocspRespBase64.isArray() || ocspRespBase64.size() != 2) { LOGGER.debug("Invalid cache file format."); return null; } long producedAt = ocspRespBase64.get(0).asLong(); byte[] ocspRespDer = Base64.decodeBase64(ocspRespBase64.get(1).asText()); if (currentTimeSecond - CACHE_EXPIRATION <= producedAt) { // add cache OCSPResp v0 = new OCSPResp(ocspRespDer); return SFPair.of(k, SFPair.of(producedAt, v0)); } else { // delete cache return SFPair.of(k, SFPair.of(producedAt, (OCSPResp) null)); } } /** * Encode OCSP Response Cache to JSON * * @return JSON object */ private static ObjectNode encodeCacheToJSON() { try { ObjectNode out = OBJECT_MAPPER.createObjectNode(); for (Map.Entry> elem : OCSP_RESPONSE_CACHE.entrySet()) { OcspResponseCacheKey key = elem.getKey(); SFPair value0 = elem.getValue(); long currentTimeSecond = value0.left; OCSPResp value = value0.right; DigestCalculator digest = new SHA1DigestCalculator(); AlgorithmIdentifier algo = digest.getAlgorithmIdentifier(); ASN1OctetString nameHash = ASN1OctetString.getInstance(key.nameHash); ASN1OctetString keyHash = ASN1OctetString.getInstance(key.keyHash); ASN1Integer serialNumber = new ASN1Integer(key.serialNumber); CertID cid = new CertID(algo, nameHash, keyHash, serialNumber); ArrayNode vout = OBJECT_MAPPER.createArrayNode(); vout.add(currentTimeSecond); vout.add(Base64.encodeBase64String(value.getEncoded())); out.set( Base64.encodeBase64String(cid.toASN1Primitive().getEncoded()), vout); } return out; } catch (IOException ex) { LOGGER.debug("Failed to encode ASN1 object."); } return null; } /** * Reads the OCSP response cache file. *

* Must be synchronized by OCSP_RESPONSE_CACHE_LOCK. */ private static void readOcspResponseCacheFile(File cacheFile, File cacheFileLock) { if (cacheFile == null || !checkCacheLockFile(cacheFile, cacheFileLock)) { // no cache or the cache is not valid. return; } try { if (!cacheFile.exists()) { LOGGER.debug( "OCSP response cache file doesn't exists. File: {}", cacheFile); return; } try (Reader reader = new InputStreamReader( new FileInputStream(cacheFile), DEFAULT_FILE_ENCODING)) { JsonNode m = OBJECT_MAPPER.readTree(reader); readJsonStoreCache(m); } } catch (IOException ex) { LOGGER.debug( "Failed to read the OCSP response cache file. " + "No worry. It will fetch OCSP from the Snowflake cache server " + "or CA server. File: {}, Err: {}", cacheFile, ex); } } /** * Reads the OCSP response cache from the server. *

* Must be synchronized by OCSP_RESPONSE_CACHE_LOCK. */ private static void readOcspResponseCacheServer() { long sleepTime = INITIAL_SLEEPING_TIME; Exception error = null; for (int retry = 0; retry < MAX_RETRY_COUNTER; ++retry) { try { HttpClient client = getHttpClient(); URI uri = new URI(SF_OCSP_RESPONSE_CACHE_SERVER_URL); HttpGet get = new HttpGet(uri); HttpResponse response = client.execute(get); if (response == null || response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { throw new IOException( String.format( "Failed to get the OCSP response from the OCSP " + "cache server: HTTP: %d", response != null ? response.getStatusLine().getStatusCode() : -1)); } ByteArrayOutputStream out = new ByteArrayOutputStream(); IOUtils.copy(response.getEntity().getContent(), out); JsonNode m = OBJECT_MAPPER.readTree(out.toByteArray()); readJsonStoreCache(m); LOGGER.debug("Successfully downloaded OCSP cache from the server."); return; } catch (IOException | URISyntaxException ex) { error = ex; LOGGER.debug("Retrying {}/{} after sleeping {}(ms)", retry + 1, MAX_RETRY_COUNTER, sleepTime); try { Thread.sleep(sleepTime); sleepTime = maxLong(MAX_SLEEPING_TIME, sleepTime * 2); } catch (InterruptedException ex0) { // nop } } } LOGGER.debug( "Failed to read the OCSP response cache from the server. " + "Server: {}, Err: {}", SF_OCSP_RESPONSE_CACHE_SERVER_URL, error); } private static void readJsonStoreCache(JsonNode m) throws IOException { if (m == null || !m.getNodeType().equals(JsonNodeType.OBJECT)) { LOGGER.debug("Invalid cache file format."); return; } for (Iterator> itr = m.fields(); itr.hasNext(); ) { SFPair> ky = decodeCacheFromJSON(itr.next()); if (ky != null && ky.right != null && ky.right.right != null) { // valid range. cache the result in memory OCSP_RESPONSE_CACHE.put(ky.left, ky.right); } else if (ky != null && OCSP_RESPONSE_CACHE.containsKey(ky.left)) { // delete it from the cache if no OCSP response is back. OCSP_RESPONSE_CACHE.remove(ky.left); WAS_CACHE_UPDATED = true; } } } /** * Lock cache file by creating a lock directory * * @return true if success or false */ private static boolean lockCacheFile(File cacheFileLock) { return cacheFileLock.mkdirs(); } /** * Unlock cache file by deleting a lock directory * * @return true if success or false */ private static boolean unlockCacheFile(File cacheFileLock) { return cacheFileLock.delete(); } /** * Delete old lock cache directory if exists. If no lock file exists, * this method returns true. * * @return true if ok to read the cache file or false */ private static boolean checkCacheLockFile(File cacheFile, File cacheFileLock) { long currentTime = new Date().getTime(); long cacheFileTs = fileCreationTime(cacheFile); if (!cacheFileLock.exists() && cacheFileTs > 0 && currentTime - CACHE_EXPIRATION <= cacheFileTs) { LOGGER.debug("No cache file lock directory exists and cache file is up to date."); return true; } long lockFileTs = fileCreationTime(cacheFileLock); if (lockFileTs < 0) { // failed to get the timestamp of lock directory return false; } if (lockFileTs < currentTime - CACHE_FILE_LOCK_EXPIRATION) { // old lock file if (!cacheFileLock.delete()) { LOGGER.debug( "Failed to delete the directory. Dir: {}", cacheFileLock); return false; } LOGGER.debug("Deleted the cache lock directory, because it was old."); return currentTime - CACHE_EXPIRATION <= cacheFileTs; } LOGGER.debug(""); return false; } /** * Gets file/dir creation time in epoch (ms) * * @return epoch time in ms */ private static long fileCreationTime(File targetFile) { if (!targetFile.exists()) { LOGGER.debug("File not exists. File: {}", targetFile); return -1; } try { Path cacheFileLockPath = Paths.get(targetFile.getAbsolutePath()); BasicFileAttributes attr = Files.readAttributes( cacheFileLockPath, BasicFileAttributes.class); return attr.creationTime().toMillis(); } catch (IOException ex) { LOGGER.debug( "Failed to get creation time. File/Dir: {}, Err: {}", targetFile, ex); } return -1; } /** * Writes OCSP response cache file. */ private static void writeOcspResponseCacheFile(File cacheFile, File cacheFileLock) { LOGGER.debug("Writing OCSP response cache file. File={}", cacheFile); if (cacheFile == null || !tryLockCacheFile(cacheFileLock)) { // no cache file or it failed to lock file return; } // NOTE: must unlock cache file try { ObjectNode json = encodeCacheToJSON(); if (json == null) { return; } try (Writer writer = new OutputStreamWriter( new FileOutputStream(cacheFile), DEFAULT_FILE_ENCODING)) { writer.write(json.toString()); } } catch (IOException ex) { LOGGER.debug( "Failed to write the OCSP response cache file. File: {}", cacheFile); } finally { if (!unlockCacheFile(cacheFileLock)) { LOGGER.debug("Failed to unlock cache file"); } } } /** * Tries to lock the cache file * * @return true if success or false */ private static boolean tryLockCacheFile(File cacheFileLock) { int cnt = 0; boolean locked = false; while (cnt < 100 && !(locked = lockCacheFile(cacheFileLock))) { try { Thread.sleep(100); } catch (InterruptedException ex) { // doesn't matter } ++cnt; } if (!locked) { LOGGER.debug("Failed to lock the OCSP response cache file."); } return locked; } /** * Fetches OCSP response from OCSP server * * @param pairIssuerSubject a pair of issuer and subject certificates * @param req OCSP Request object * @return OCSP Response object * @throws CertificateEncodingException if any other error occurs */ private OCSPResp fetchOcspResponse( SFPair pairIssuerSubject, OCSPReq req) throws CertificateEncodingException { try { byte[] ocspReqDer = req.getEncoded(); String ocspReqDerBase64 = Base64.encodeBase64String(ocspReqDer); Set ocspUrls = getOcspUrls(pairIssuerSubject.right); String ocspUrl = ocspUrls.iterator().next(); // first one URL url; if (SF_OCSP_RESPONSE_CACHE_SERVER_RETRY_URL_PATTERN != null) { url = new URL(SF_OCSP_RESPONSE_CACHE_SERVER_RETRY_URL_PATTERN.format( ocspUrl, ocspReqDerBase64 )); } else { url = new URL(String.format("%s/%s", ocspUrl, ocspReqDerBase64)); } LOGGER.debug( "not hit cache. Fetching OCSP response from CA OCSP server. {0}", url.toString()); long sleepTime = INITIAL_SLEEPING_TIME; boolean success = false; HttpResponse response = null; for (int retry = 0; retry < MAX_RETRY_COUNTER; ++retry) { HttpClient client = getHttpClient(); HttpGet get = new HttpGet(url.toString()); response = client.execute(get); if (response != null && response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { success = true; LOGGER.debug( "Successfully downloaded OCSP response from CA server. " + "URL: {}", url.toString()); break; } LOGGER.debug("Retrying {}/{} after sleeping {}(ms)", retry + 1, MAX_RETRY_COUNTER, sleepTime); try { Thread.sleep(sleepTime); sleepTime = maxLong(MAX_SLEEPING_TIME, sleepTime * 2); } catch (InterruptedException ex0) { // nop } } if (!success) { throw new CertificateEncodingException( String.format( "Failed to get OCSP response. StatusCode: %d, URL: %s", response == null ? null : response.getStatusLine().getStatusCode(), ocspUrl)); } ByteArrayOutputStream out = new ByteArrayOutputStream(); IOUtils.copy(response.getEntity().getContent(), out); OCSPResp ocspResp = new OCSPResp(out.toByteArray()); if (ocspResp.getStatus() != OCSPResp.SUCCESSFUL) { throw new CertificateEncodingException( String.format("Failed to get OCSP response. Status: %s", OCSP_RESPONSE_CODE_TO_STRING.get(ocspResp.getStatus()))); } return ocspResp; } catch (IOException ex) { throw new CertificateEncodingException("Failed to encode object.", ex); } } /** * Validates the certificate revocation status * * @param pairIssuerSubject a pair of issuer and subject certificates * @param ocspResp OCSP Response object * @throws CertificateException raises if any other error occurs */ private void validateRevocationStatusMain( SFPair pairIssuerSubject, OCSPResp ocspResp) throws CertificateException { try { Date currentTime = new Date(); BasicOCSPResp basicOcspResp = (BasicOCSPResp) (ocspResp.getResponseObject()); X509CertificateHolder[] attachedCerts = basicOcspResp.getCerts(); X509CertificateHolder signVerifyCert; if (attachedCerts.length > 0) { LOGGER.debug( "Certificate is attached for verification. " + "Verifying it by the issuer certificate."); signVerifyCert = attachedCerts[0]; verifySignature( new X509CertificateHolder(pairIssuerSubject.left.getEncoded()), signVerifyCert.getSignature(), CONVERTER_X509.getCertificate(signVerifyCert).getTBSCertificate(), signVerifyCert.getSignatureAlgorithm()); LOGGER.debug( "Verifying OCSP signature by the attached certificate public key." ); } else { LOGGER.debug("Certificate is NOT attached for verification. " + "Verifying OCSP signature by the issuer public key."); signVerifyCert = new X509CertificateHolder( pairIssuerSubject.left.getEncoded()); } verifySignature( signVerifyCert, basicOcspResp.getSignature(), basicOcspResp.getTBSResponseData(), basicOcspResp.getSignatureAlgorithmID()); validateBasicOcspResponse(currentTime, basicOcspResp); } catch (IOException | OCSPException ex) { throw new CertificateEncodingException( "Failed to check revocation status.", ex); } } /** * Validates OCSP Basic OCSP response. * * @param currentTime the current timestamp. * @param basicOcspResp BasicOcspResponse data. * @throws CertificateEncodingException raises if any failure occurs. */ private void validateBasicOcspResponse( Date currentTime, BasicOCSPResp basicOcspResp) throws CertificateEncodingException { for (SingleResp singleResps : basicOcspResp.getResponses()) { Date thisUpdate = singleResps.getThisUpdate(); Date nextUpdate = singleResps.getNextUpdate(); LOGGER.debug("Current Time: {}, This Update: {}, Next Update: {}", currentTime, thisUpdate, nextUpdate); CertificateStatus certStatus = singleResps.getCertStatus(); if (certStatus != CertificateStatus.GOOD) { if (certStatus instanceof RevokedStatus) { RevokedStatus status = (RevokedStatus) certStatus; int reason; try { reason = status.getRevocationReason(); } catch (IllegalStateException ex) { reason = -1; } Date revocationTime = status.getRevocationTime(); throw new CertificateEncodingException( String.format( "The certificate has been revoked. Reason: %d, Time: %s", reason, DATE_FORMAT_UTC.format(revocationTime))); } else { // Unknown status throw new CertificateEncodingException( "Failed to validate the certificate for UNKNOWN reason."); } } if (!isValidityRange(currentTime, thisUpdate, nextUpdate)) { throw new CertificateEncodingException( String.format( "The validity is out of range: " + "Current Time: %s, This Update: %s, Next Update: %s", DATE_FORMAT_UTC.format(currentTime), DATE_FORMAT_UTC.format(thisUpdate), DATE_FORMAT_UTC.format(nextUpdate))); } } LOGGER.debug("OK. Verified the certificate revocation status."); } /** * Verifies the signature of the data * * @param cert a certificate for public key. * @param sig signature in a byte array. * @param data data in a byte array. * @param idf algorithm identifier object. * @throws CertificateException raises if the verification fails. */ private static void verifySignature( X509CertificateHolder cert, byte[] sig, byte[] data, AlgorithmIdentifier idf) throws CertificateException { try { String algorithm = SIGNATURE_OID_TO_STRING.get(idf.getAlgorithm()); if (algorithm == null) { throw new NoSuchAlgorithmException( String.format("Unsupported signature OID. OID: %s", idf)); } Signature signer = Signature.getInstance( algorithm, BouncyCastleProvider.PROVIDER_NAME); X509Certificate c = CONVERTER_X509.getCertificate(cert); signer.initVerify(c.getPublicKey()); signer.update(data); if (!signer.verify(sig)) { throw new CertificateEncodingException( String.format("Failed to verify the signature. Potentially the " + "data was not generated by by the cert, %s", cert.getSubject())); } } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException | SignatureException ex) { throw new CertificateEncodingException( "Failed to verify the signature.", ex); } } /** * Converts Byte array to hex string * * @param bytes a byte array * @return a string in hexadecimal code */ private static String byteToHexString(byte[] bytes) { final char[] hexArray = "0123456789ABCDEF".toCharArray(); char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } /** * Creates a OCSP Request * * @param pairIssuerSubject a pair of issuer and subject certificates * @return OCSPReq object */ private OCSPReq createRequest( SFPair pairIssuerSubject) { Certificate issuer = pairIssuerSubject.left; Certificate subject = pairIssuerSubject.right; OCSPReqBuilder gen = new OCSPReqBuilder(); try { DigestCalculator digest = new SHA1DigestCalculator(); X509CertificateHolder certHolder = new X509CertificateHolder(issuer.getEncoded()); CertificateID certId = new CertificateID( digest, certHolder, subject.getSerialNumber().getValue()); gen.addRequest(certId); return gen.build(); } catch (OCSPException | IOException ex) { throw new RuntimeException("Failed to build a OCSPReq."); } } /** * Converts X509Certificate to Bouncy Castle Certificate * * @param chain an array of X509Certificate * @return a list of Bouncy Castle Certificate */ private List convertToBouncyCastleCertificate( X509Certificate[] chain) { final List bcChain = new ArrayList<>(); for (X509Certificate cert : chain) { try { bcChain.add(Certificate.getInstance(cert.getEncoded())); } catch (CertificateEncodingException ex) { throw new RuntimeException("Failed to decode the certificate DER data"); } } return bcChain; } /** * Creates a pair of Issuer and Subject certificates * * @param bcChain a list of bouncy castle Certificate * @return a list of paif of Issuer and Subject certificates */ private List> getPairIssuerSubject( List bcChain) { List> pairIssuerSubject = new ArrayList<>(); for (int i = 0, len = bcChain.size(); i < len; ++i) { Certificate bcCert = bcChain.get(i); if (bcCert.getIssuer().equals(bcCert.getSubject())) { continue; // skipping ROOT CA } if (i < len - 1) { pairIssuerSubject.add(SFPair.of(bcChain.get(i + 1), bcChain.get(i))); } else { synchronized (ROOT_CA_LOCK) { // no root CA certificate is attached in the certificate chain, so // getting one from the root CA from JVM. Certificate issuer = ROOT_CA.get(bcCert.getIssuer().hashCode()); if (issuer == null) { throw new RuntimeException("Failed to find the root CA."); } pairIssuerSubject.add(SFPair.of(issuer, bcChain.get(i))); } } } return pairIssuerSubject; } /** * Gets OCSP URLs associated with the certificate. * * @param bcCert Bouncy Castle Certificate * @return a set of OCSP URLs */ private Set getOcspUrls(Certificate bcCert) { TBSCertificate bcTbsCert = bcCert.getTBSCertificate(); Extensions bcExts = bcTbsCert.getExtensions(); if (bcExts == null) { throw new RuntimeException("Failed to get Tbs Certificate."); } Set ocsp = new HashSet<>(); for (Enumeration en = bcExts.oids(); en.hasMoreElements(); ) { ASN1ObjectIdentifier oid = (ASN1ObjectIdentifier) en.nextElement(); Extension bcExt = bcExts.getExtension(oid); if (bcExt.getExtnId() == Extension.authorityInfoAccess) { // OCSP URLS are included in authorityInfoAccess DLSequence seq = (DLSequence) bcExt.getParsedValue(); for (ASN1Encodable asn : seq) { ASN1Encodable[] pairOfAsn = ((DLSequence) asn).toArray(); if (pairOfAsn.length == 2) { ASN1ObjectIdentifier key = (ASN1ObjectIdentifier) pairOfAsn[0]; if (key == OIDocsp) { // ensure OCSP and not CRL GeneralName gn = GeneralName.getInstance(pairOfAsn[1]); ocsp.add(gn.getName().toString()); } } } } } return ocsp; } /** * Gets HttpClient object * * @return HttpClient */ private static HttpClient getHttpClient() { // using the default HTTP client return HttpUtil.getHttpClient(); } private static long maxLong(long v1, long v2) { return v1 > v2 ? v1 : v2; } /** * Calculates the tolerable validity time beyond the next update. *

* Sometimes CA's OCSP response update is delayed beyond the clock skew * as the update is not populated to all OCSP servers for certain period. * * @param thisUpdate the last update * @param nextUpdate the next update * @return the tolerable validity beyond the next update. */ private static long calculateTolerableVadility(Date thisUpdate, Date nextUpdate) { return maxLong((long) ((float) (nextUpdate.getTime() - thisUpdate.getTime()) * TOLERABLE_VALIDITY_RANGE_RATIO), MIN_CACHE_WARMUP_TIME); } /** * Checks the validity * * @param currentTime the current time * @param thisUpdate the last update timestamp * @param nextUpdate the next update timestamp * @return true if valid or false */ private static boolean isValidityRange(Date currentTime, Date thisUpdate, Date nextUpdate) { long tolerableValidity = calculateTolerableVadility(thisUpdate, nextUpdate); return thisUpdate.getTime() - MAX_CLOCK_SKEW <= currentTime.getTime() && currentTime.getTime() <= nextUpdate.getTime() + tolerableValidity; } /** * OCSP response cache key object */ static class OcspResponseCacheKey { final byte[] nameHash; final byte[] keyHash; final BigInteger serialNumber; OcspResponseCacheKey(byte[] nameHash, byte[] keyHash, BigInteger serialNumber) { this.nameHash = nameHash; this.keyHash = keyHash; this.serialNumber = serialNumber; } public int hashCode() { int ret = Arrays.hashCode(this.nameHash) * 37; ret = ret * 10 + Arrays.hashCode(this.keyHash) * 37; ret = ret * 10 + this.serialNumber.hashCode(); return ret; } public boolean equals(Object obj) { if (!(obj instanceof OcspResponseCacheKey)) { return false; } OcspResponseCacheKey target = (OcspResponseCacheKey) obj; return Arrays.equals(this.nameHash, target.nameHash) && Arrays.equals(this.keyHash, target.keyHash) && this.serialNumber.equals(target.serialNumber); } public String toString() { return String.format( "OcspResponseCacheKey: NameHash: %s, KeyHash: %s, SerialNumber: %s", byteToHexString(nameHash), byteToHexString(keyHash), serialNumber.toString()); } } /** * SHA1 Digest Calculator used in OCSP Req. */ static class SHA1DigestCalculator implements DigestCalculator { private ByteArrayOutputStream bOut = new ByteArrayOutputStream(); public AlgorithmIdentifier getAlgorithmIdentifier() { return new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1); } public OutputStream getOutputStream() { return bOut; } public byte[] getDigest() { byte[] bytes = bOut.toByteArray(); bOut.reset(); Digest sha1 = new SHA1Digest(); sha1.update(bytes, 0, bytes.length); byte[] digest = new byte[sha1.getDigestSize()]; sha1.doFinal(digest, 0); return digest; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy