Commit 46926db7 authored by Anton Ryzhov's avatar Anton Ryzhov

Extract loading and saving certificates; some certificate cache improving

parent e765f16a
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Network
{
/// <summary>
/// An object that holds the cached certificate
/// </summary>
internal class CachedCertificate
internal sealed class CachedCertificate
{
internal CachedCertificate()
{
LastAccess = DateTime.Now;
}
internal X509Certificate2 Certificate { get; set; }
/// <summary>
/// last time this certificate was used
/// Usefull in determining its cache lifetime
/// Certificate creation task.
/// </summary>
internal Task<X509Certificate2> CreationTask { get; set; }
/// <summary>
/// Last time this certificate was used.
/// Useful in determining its cache lifetime.
/// </summary>
internal DateTime LastAccess { get; set; }
}
......
......@@ -4,8 +4,8 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Network.Certificate;
......@@ -30,7 +30,6 @@ namespace Titanium.Web.Proxy.Network
/// Bug #468 Reported.
/// </summary>
DefaultWindows = 1
}
/// <summary>
......@@ -47,7 +46,7 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
private readonly ConcurrentDictionary<string, CachedCertificate> certificateCache;
private readonly ConcurrentDictionary<string, Task<X509Certificate2>> pendingCertificateCreationTasks;
private readonly CancellationTokenSource clearCertificatesTokenSource;
private ICertificateMaker certEngine;
......@@ -55,12 +54,12 @@ namespace Titanium.Web.Proxy.Network
private string issuer;
private bool pfxFileExists;
private X509Certificate2 rootCertificate;
private string rootCertificateName;
private ICertificateStorage certificateStorage;
/// <summary>
/// Initializes a new instance of the <see cref="CertificateManager"/> class.
/// </summary>
......@@ -100,10 +99,11 @@ namespace Titanium.Web.Proxy.Network
CertificateEngine = CertificateEngine.BouncyCastle;
certificateCache = new ConcurrentDictionary<string, CachedCertificate>();
pendingCertificateCreationTasks = new ConcurrentDictionary<string, Task<X509Certificate2>>();
}
private bool clearCertificates { get; set; }
clearCertificatesTokenSource = new CancellationTokenSource();
certificateStorage = new DefaultCertificateStorage();
}
/// <summary>
/// Is the root certificate used by this proxy is valid?
......@@ -122,7 +122,7 @@ namespace Titanium.Web.Proxy.Network
internal bool MachineTrustRoot { get; set; }
/// <summary>
/// Whether trust operations should be done with elevated privillages
/// Whether trust operations should be done with elevated privileges
/// Will prompt with UAC if required. Works only on Windows.
/// </summary>
internal bool TrustRootAsAdministrator { get; set; }
......@@ -215,14 +215,24 @@ namespace Titanium.Web.Proxy.Network
}
/// <summary>
/// Save all fake certificates in folder "crts" (will be created in proxy dll directory).
/// Save all fake certificates using <seealso cref="CertificateStorage"/>.
/// <para>for can load the certificate and not make new certificate every time. </para>
/// </summary>
public bool SaveFakeCertificates { get; set; } = false;
/// <summary>
/// The service to save fake certificates.
/// The default storage saves certificates in folder "crts" (will be created in proxy dll directory).
/// </summary>
public ICertificateStorage CertificateStorage
{
get => certificateStorage;
set => certificateStorage = value ?? new DefaultCertificateStorage();
}
/// <summary>
/// Overwrite Root certificate file.
/// <para>true : replace an existing .pfx file if password is incorect or if RootCertificate = null.</para>
/// <para>true : replace an existing .pfx file if password is incorrect or if RootCertificate = null.</para>
/// </summary>
public bool OverwritePfxFile { get; set; } = true;
......@@ -241,52 +251,7 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
public void Dispose()
{
}
private string getRootCertificateDirectory()
{
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
// dynamically loaded assemblies returns string.Empty location
if (assemblyLocation == string.Empty)
{
assemblyLocation = Assembly.GetEntryAssembly().Location;
}
string path = Path.GetDirectoryName(assemblyLocation);
if (path == null)
{
throw new NullReferenceException();
}
return path;
}
private string getCertificatePath()
{
string path = getRootCertificateDirectory();
string certPath = Path.Combine(path, "crts");
if (!Directory.Exists(certPath))
{
Directory.CreateDirectory(certPath);
}
return certPath;
}
private string getRootCertificatePath()
{
string path = getRootCertificateDirectory();
string fileName = PfxFilePath;
if (fileName == string.Empty)
{
fileName = Path.Combine(path, "rootCert.pfx");
StorageFlag = X509KeyStorageFlags.Exportable;
}
return fileName;
clearCertificatesTokenSource.Dispose();
}
/// <summary>
......@@ -412,42 +377,36 @@ namespace Titanium.Web.Proxy.Network
/// <returns></returns>
internal X509Certificate2 CreateCertificate(string certificateName, bool isRootCertificate)
{
X509Certificate2 certificate = null;
X509Certificate2 certificate;
try
{
if (!isRootCertificate && SaveFakeCertificates)
{
string path = getCertificatePath();
string subjectName = ProxyConstants.CNRemoverRegex.Replace(certificateName, string.Empty);
subjectName = subjectName.Replace("*", "$x$");
string certificatePath = Path.Combine(path, subjectName + ".pfx");
if (!File.Exists(certificatePath))
{
certificate = makeCertificate(certificateName, false);
string subjectName = ProxyConstants.CNRemoverRegex
.Replace(certificateName, string.Empty)
.Replace("*", "$x$");
// store as cache
try
{
var exported = certificate.Export(X509ContentType.Pkcs12);
File.WriteAllBytes(certificatePath, exported);
certificate = certificateStorage.LoadCertificate(subjectName, StorageFlag);
}
catch (Exception e)
{
ExceptionFunc(new Exception("Failed to save fake certificate.", e));
ExceptionFunc(new Exception("Failed to load fake certificate.", e));
certificate = null;
}
}
else
if (certificate == null)
{
certificate = makeCertificate(certificateName, false);
try
{
certificate = new X509Certificate2(certificatePath, string.Empty, StorageFlag);
certificateStorage.SaveCertificate(subjectName, certificate);
}
catch
catch (Exception e)
{
// if load failed create again
certificate = makeCertificate(certificateName, false);
ExceptionFunc(new Exception("Failed to save fake certificate.", e));
}
}
}
......@@ -459,6 +418,7 @@ namespace Titanium.Web.Proxy.Network
catch (Exception e)
{
ExceptionFunc(e);
certificate = null;
}
return certificate;
......@@ -472,40 +432,41 @@ namespace Titanium.Web.Proxy.Network
internal async Task<X509Certificate2> CreateCertificateAsync(string certificateName)
{
// check in cache first
if (certificateCache.TryGetValue(certificateName, out var cached))
var item = certificateCache.GetOrAdd(certificateName, _ =>
{
cached.LastAccess = DateTime.Now;
return cached.Certificate;
}
// handle burst requests with same certificate name
// by checking for existing task for same certificate name
if (pendingCertificateCreationTasks.TryGetValue(certificateName, out var task))
var cached = new CachedCertificate();
cached.CreationTask = Task.Run(() =>
{
return await task;
}
var certificate = CreateCertificate(certificateName, false);
// run certificate creation task & add it to pending tasks
task = Task.Run(() =>
{
var result = CreateCertificate(certificateName, false);
if (result != null)
{
certificateCache.TryAdd(certificateName, new CachedCertificate
{
Certificate = result
// see http://www.albahari.com/threading/part4.aspx for the explanation
// why Thread.MemoryBarrier is used here and below
cached.Certificate = certificate;
Thread.MemoryBarrier();
cached.CreationTask = null;
Thread.MemoryBarrier();
return certificate;
});
}
return result;
return cached;
});
pendingCertificateCreationTasks.TryAdd(certificateName, task);
// cleanup pending tasks & return result
var certificate = await task;
pendingCertificateCreationTasks.TryRemove(certificateName, out task);
item.LastAccess = DateTime.Now;
return certificate;
if (item.Certificate != null)
{
return item.Certificate;
}
// handle burst requests with same certificate name
// by checking for existing task
Thread.MemoryBarrier();
var task = item.CreationTask;
Thread.MemoryBarrier();
// return result
return item.Certificate ?? await task;
}
/// <summary>
......@@ -513,8 +474,8 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
internal async void ClearIdleCertificates()
{
clearCertificates = true;
while (clearCertificates)
var cancellationToken = clearCertificatesTokenSource.Token;
while (!cancellationToken.IsCancellationRequested)
{
var cutOff = DateTime.Now.AddMinutes(-1 * CertificateCacheTimeOutMinutes);
......@@ -526,7 +487,14 @@ namespace Titanium.Web.Proxy.Network
}
// after a minute come back to check for outdated certificates in cache
await Task.Delay(1000 * 60);
try
{
await Task.Delay(1000 * 60, cancellationToken);
}
catch (TaskCanceledException)
{
return;
}
}
}
......@@ -535,7 +503,7 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
internal void StopClearIdleCertificates()
{
clearCertificates = false;
clearCertificatesTokenSource.Cancel();
}
/// <summary>
......@@ -557,10 +525,21 @@ namespace Titanium.Web.Proxy.Network
return true;
}
if (!OverwritePfxFile && pfxFileExists)
if (!OverwritePfxFile)
{
try
{
var rootCert = certificateStorage.LoadRootCertificate(PfxFilePath, PfxPassword, X509KeyStorageFlags.Exportable);
if (rootCert != null)
{
return false;
}
}
catch
{
// root cert cannot be loaded
}
}
try
{
......@@ -577,15 +556,14 @@ namespace Titanium.Web.Proxy.Network
{
try
{
Directory.Delete(getCertificatePath(), true);
certificateStorage.Clear();
}
catch
{
// ignore
}
string fileName = getRootCertificatePath();
File.WriteAllBytes(fileName, RootCertificate.Export(X509ContentType.Pkcs12, PfxPassword));
certificateStorage.SaveRootCertificate(PfxFilePath, PfxPassword, RootCertificate);
}
catch (Exception e)
{
......@@ -602,16 +580,9 @@ namespace Titanium.Web.Proxy.Network
/// <returns></returns>
public X509Certificate2 LoadRootCertificate()
{
string fileName = getRootCertificatePath();
pfxFileExists = File.Exists(fileName);
if (!pfxFileExists)
{
return null;
}
try
{
return new X509Certificate2(fileName, PfxPassword, StorageFlag);
return certificateStorage.LoadRootCertificate(PfxFilePath, PfxPassword, X509KeyStorageFlags.Exportable);
}
catch (Exception e)
{
......@@ -904,6 +875,7 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
public void ClearRootCertificate()
{
certificateStorage.Clear();
certificateCache.Clear();
rootCertificate = null;
}
......
using System;
using System.IO;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
namespace Titanium.Web.Proxy.Network
{
internal sealed class DefaultCertificateStorage : ICertificateStorage
{
private const string defaultCertificateDirectoryName = "crts";
private const string defaultCertificateFileExtension = ".pfx";
private const string defaultRootCertificateFileName = "rootCert" + defaultCertificateFileExtension;
private string rootCertificatePath;
private string certificatePath;
public X509Certificate2 LoadRootCertificate(string name, string password, X509KeyStorageFlags storageFlags)
{
string filePath = getRootCertificatePath(name);
return loadCertificate(filePath, password, storageFlags);
}
public void SaveRootCertificate(string name, string password, X509Certificate2 certificate)
{
string filePath = getRootCertificatePath(name);
byte[] exported = certificate.Export(X509ContentType.Pkcs12, password);
File.WriteAllBytes(filePath, exported);
}
/// <inheritdoc />
public X509Certificate2 LoadCertificate(string subjectName, X509KeyStorageFlags storageFlags)
{
string filePath = Path.Combine(getCertificatePath(), subjectName + defaultCertificateFileExtension);
return loadCertificate(filePath, string.Empty, storageFlags);
}
/// <inheritdoc />
public void SaveCertificate(string subjectName, X509Certificate2 certificate)
{
string filePath = Path.Combine(getCertificatePath(), subjectName + defaultCertificateFileExtension);
byte[] exported = certificate.Export(X509ContentType.Pkcs12);
File.WriteAllBytes(filePath, exported);
}
public void Clear()
{
if (Directory.Exists(getCertificatePath()))
{
Directory.Delete(getCertificatePath(), true);
}
certificatePath = null;
}
private X509Certificate2 loadCertificate(string filePath, string password, X509KeyStorageFlags storageFlags)
{
byte[] exported;
try
{
exported = File.ReadAllBytes(filePath);
}
catch (IOException)
{
// file or directory not found
return null;
}
return new X509Certificate2(exported, password, storageFlags);
}
private string getRootCertificatePath(string filePath)
{
if (Path.IsPathRooted(filePath))
{
return filePath;
}
return Path.Combine(getRootCertificateDirectory(),
string.IsNullOrEmpty(filePath) ? defaultRootCertificateFileName : filePath);
}
private string getCertificatePath()
{
if (certificatePath == null)
{
string path = getRootCertificateDirectory();
string certPath = Path.Combine(path, defaultCertificateDirectoryName);
if (!Directory.Exists(certPath))
{
Directory.CreateDirectory(certPath);
}
certificatePath = certPath;
}
return certificatePath;
}
private string getRootCertificateDirectory()
{
if (rootCertificatePath == null)
{
string assemblyLocation = GetType().Assembly.Location;
// dynamically loaded assemblies returns string.Empty location
if (assemblyLocation == string.Empty)
{
assemblyLocation = Assembly.GetEntryAssembly().Location;
}
string path = Path.GetDirectoryName(assemblyLocation);
rootCertificatePath = path ?? throw new NullReferenceException();
}
return rootCertificatePath;
}
}
}
using System.Security.Cryptography.X509Certificates;
namespace Titanium.Web.Proxy.Network
{
public interface ICertificateStorage
{
/// <summary>
/// Loads the root certificate from the storage.
/// </summary>
X509Certificate2 LoadRootCertificate(string name, string password, X509KeyStorageFlags storageFlags);
/// <summary>
/// Saves the root certificate to the storage.
/// </summary>
void SaveRootCertificate(string name, string password, X509Certificate2 certificate);
/// <summary>
/// Loads certificate from the storage. Returns true if certificate does not exist.
/// </summary>
X509Certificate2 LoadCertificate(string subjectName, X509KeyStorageFlags storageFlags);
/// <summary>
/// Stores certificate into the storage.
/// </summary>
void SaveCertificate(string subjectName, X509Certificate2 certificate);
/// <summary>
/// Clears the storage.
/// </summary>
void Clear();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment