Commit 6b3488a7 6b3488a7cf071c15575952a3ec032f3e3464f819 by Christian Gerdes

Support for CER/DER formatted client certificates

Also only files with PEM file extension are treated as PEM files. Unsupported formats will cause an error log message and not be imported.
1 parent a926a62e
......@@ -20,6 +20,7 @@ using System.IO;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Security.Cryptography.X509Certificates;
using System.Diagnostics;
namespace LIL_VSTT_Plugins
{
......@@ -558,21 +559,21 @@ namespace LIL_VSTT_Plugins
/// WebTest Client Certificate
/// </summary>
[DisplayName("Client Certificate")]
[Description("(C) Copyright 2016 LIGHTS IN LINE AB\r\nSätter webtestet att använda ett specifikt client cert för SSL. Certifikatet behöver inte installeras i certstore först.")]
[Description("(C) Copyright 2016 LIGHTS IN LINE AB\r\nSätter webtestet att använda ett specifikt client cert för SSL. Certifikatet installeras automatiskt i Windows User Certificate Store.")]
public class ClientCertificatePlugin : WebTestPlugin
{
[DisplayName("Certificate Path")]
[Description("Sökvägen till certifikatfilen (.P12/.PFX/.PEM med privat nyckel)")]
[Description("Sökvägen till certifikatfilen (P12/PFX/PEM med privat nyckel eller CER/DER utan privat nyckel)")]
[DefaultValue("")]
public string pCertificatePath { get; set; }
[DisplayName("Certificate Path Parameter")]
[Description("Ange namn på parameter som ska användas för sökvägen till certifikatfilen (.P12/.PFX/.PEM). Om parametern saknas eller är tom används Certificate Path")]
[Description("Ange namn på parameter som ska användas för sökvägen till certifikatfilen. Om parametern saknas eller är tom används Certificate Path")]
[DefaultValue("")]
public string pCertificatePathParameter { get; set; }
[DisplayName("Certificate Password")]
[Description("Ange lösenordet för att öppna certifikatfilen")]
[Description("Ange lösenordet för att öppna skyddade/krypterade filer")]
[DefaultValue("")]
public string pCertificatePassword { get; set; }
......@@ -598,19 +599,25 @@ namespace LIL_VSTT_Plugins
private bool haveCert = false;
private X509Certificate2 myClientCertAndKey;
private Regex p12RegExp = new Regex(@"p12$|pfx$",RegexOptions.IgnoreCase);
private Regex cerRegExp = new Regex(@"cer$|der$", RegexOptions.IgnoreCase);
private Regex pemRegExp = new Regex(@"pem$", RegexOptions.IgnoreCase);
public override void PreWebTest(object sender, PreWebTestEventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
base.PreWebTest(sender, e);
String certPath, certPass;
// Ladda in certifikatet och sätt CertPolicy
if (!String.IsNullOrWhiteSpace(pCertificatePathParameter) && e.WebTest.Context.ContainsKey(pCertificatePathParameter) && !String.IsNullOrWhiteSpace(e.WebTest.Context[pCertificatePathParameter].ToString()) )
{
certPath = e.WebTest.Context[pCertificatePathParameter].ToString();
certPath = e.WebTest.Context[pCertificatePathParameter].ToString().Trim();
} else
{
certPath = pCertificatePath;
certPath = pCertificatePath.Trim();
}
if (!String.IsNullOrWhiteSpace(pCertificatePasswordParameter) && e.WebTest.Context.ContainsKey(pCertificatePasswordParameter) && !String.IsNullOrWhiteSpace(e.WebTest.Context[pCertificatePasswordParameter].ToString()))
......@@ -629,11 +636,11 @@ namespace LIL_VSTT_Plugins
return;
}
// Check what type of container we have. All files are treated as PEM unless the extension is .pfx or .p12
// Check what type of container we have. All files are treated as PEM unless the extension matches our winX509regExp regular expression (see above)
// Read certificate and private key depending on type
if (certPath.ToLower().EndsWith(".pfx") || certPath.ToLower().EndsWith(".p12"))
if (p12RegExp.IsMatch(certPath))
{
if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as PFX/PKCS12");
if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as PFX/P12");
try
{
myClientCertAndKey = new X509Certificate2(certPath, certPass, X509KeyStorageFlags.PersistKeySet);
......@@ -643,9 +650,21 @@ namespace LIL_VSTT_Plugins
e.WebTest.AddCommentToResult("Error during loading of certificate: " + certPath + " Message: " + ex.Message);
return;
}
} else
} else if (cerRegExp.IsMatch(certPath))
{
if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as CER/DER without private key");
try
{
myClientCertAndKey = new X509Certificate2(certPath, certPass);
}
catch (Exception ex)
{
e.WebTest.AddCommentToResult("Error during loading of certificate: " + certPath + " Message: " + ex.Message);
return;
}
} else if (pemRegExp.IsMatch(certPath))
{
if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as PEM/PKCS8");
if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as OpenSSL encoded PEM");
// Use Bouncy Castle to read the certificate and key, then convert to .NET X509Certificate2 and X509Certificate
String text;
......@@ -698,19 +717,24 @@ namespace LIL_VSTT_Plugins
}
keyTextBeginPos = text.IndexOf("-----BEGIN", keyTextEndPos);
}
if (bcKey == null || bcCert == null)
if (bcCert == null)
{
e.WebTest.AddCommentToResult("Error: PEM file has to contain both certificate and private key");
e.WebTest.AddCommentToResult("Error: PEM file has to contain an x509 certificate");
return;
}
try {
myClientCertAndKey = new X509Certificate2(Org.BouncyCastle.Security.DotNetUtilities.ToX509Certificate(bcCert));
myClientCertAndKey.PrivateKey = Org.BouncyCastle.Security.DotNetUtilities.ToRSA(bcKey.Private as Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters);
if(bcKey != null) myClientCertAndKey.PrivateKey = Org.BouncyCastle.Security.DotNetUtilities.ToRSA(bcKey.Private as Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters);
} catch (Exception ex)
{
e.WebTest.AddCommentToResult("Error during loading of PEM file: " + certPath + " Message: " + ex.Message);
return;
}
} else
{
// Unknown or unsuported format
e.WebTest.AddCommentToResult("Error during loading of file: " + certPath + " Message: Unsupported format");
return;
}
// Check that we have a certificate
......@@ -721,16 +745,17 @@ namespace LIL_VSTT_Plugins
}
else
{
if (pDebug) e.WebTest.AddCommentToResult("Certificate File " + certPath + " loaded successfully.");
if (pDebug) e.WebTest.AddCommentToResult("Certificate File " + certPath + " loaded successfully in " + sw.ElapsedMilliseconds + "ms");
}
// Check that it seems okey
if (string.IsNullOrWhiteSpace(myClientCertAndKey.GetCertHashString()))
if (string.IsNullOrWhiteSpace(myClientCertAndKey.Thumbprint))
{
if (pDebug) e.WebTest.AddCommentToResult("Certificate File " + certPath + " contains no SHA1 hash. Not using it.");
if (pDebug) e.WebTest.AddCommentToResult("Certificate File " + certPath + " contains no Thumbprint. Not using it.");
return;
}
if (pDebug) e.WebTest.AddCommentToResult("Loaded client certificate for Subject: [" + myClientCertAndKey.Subject + "] Issued by: [" + myClientCertAndKey.Issuer + "] Expires: [" + myClientCertAndKey.GetExpirationDateString() + "]");
if (pDebug) e.WebTest.AddCommentToResult("Subject: [" + myClientCertAndKey.Subject + "]");
if (pDebug) e.WebTest.AddCommentToResult("Issued by: [" + myClientCertAndKey.Issuer + "] Expires: [" + myClientCertAndKey.GetExpirationDateString() + "]");
// Check if the certificate is trusted (i.e. chain can be validated)
bool myCertTrusted = false;
......@@ -747,24 +772,19 @@ namespace LIL_VSTT_Plugins
// Check if it is expired or about to expire
if(myClientCertAndKey.NotAfter < DateTime.Now)
{
e.WebTest.AddCommentToResult("Warning: Client Certificate has expired. Might not be trusted on server. Expired " + myClientCertAndKey.NotAfter.ToString());
e.WebTest.AddCommentToResult("Warning: Client Certificate has expired. Might not be trusted on server.");
} else if (myClientCertAndKey.NotBefore > DateTime.Now)
{
e.WebTest.AddCommentToResult("Warning: Client Certificate is not valid yet. Might not be trusted on server. Valid " + myClientCertAndKey.NotBefore.ToString());
e.WebTest.AddCommentToResult("Warning: Client Certificate is not valid yet. Might not be trusted on server. Valid on " + myClientCertAndKey.NotBefore.ToString());
} else if (myClientCertAndKey.NotAfter < DateTime.Now.AddDays(14))
{
e.WebTest.AddCommentToResult("Warning: Client Certificate will expire in less than 14 days. Better renew it soon. Expires " + myClientCertAndKey.NotAfter.ToString());
e.WebTest.AddCommentToResult("Warning: Client Certificate will expire in less than 14 days. Better renew it soon.");
}
// Check if we have a private key
if (!myClientCertAndKey.HasPrivateKey)
{
// Cant use it without private key
e.WebTest.AddCommentToResult("Error: Certificate HAS NO PRIVATE KEY, cannot use it without one.");
return;
} else
if (myClientCertAndKey.HasPrivateKey)
{
if (pDebug) e.WebTest.AddCommentToResult("Certificate HAS PRIVATE KEY");
if (pDebug) e.WebTest.AddCommentToResult("Certificate HAS PRIVATE KEY in file");
}
// Check that the certificate exists in the cert store
......@@ -772,31 +792,69 @@ namespace LIL_VSTT_Plugins
cuStore.Open(OpenFlags.ReadWrite);
if(cuStore.Certificates.Contains(myClientCertAndKey)) {
if (pDebug) e.WebTest.AddCommentToResult("Certificate already INSTALLED in Current User Windows Certificate Store");
// Try to load the key from store if we dont have it and verify that it belongs to the certificate
if(!myClientCertAndKey.HasPrivateKey)
{
X509Certificate2Collection certCol = cuStore.Certificates.Find(X509FindType.FindByThumbprint, myClientCertAndKey.Thumbprint, false);
if(certCol == null || certCol.Count == 0)
{
e.WebTest.AddCommentToResult("Error: Certificate could not be loaded from store using it's thumbprint which is very strange. Aborting");
return;
} else
{
if(certCol.Count > 1)
{
if (pDebug) e.WebTest.AddCommentToResult("Certificates thumbprint has more than one match in the Windows User Certificate Store, using the first one.");
}
X509Certificate2 cert = certCol[0];
if (!cert.HasPrivateKey)
{
e.WebTest.AddCommentToResult("Error: Certificate does not have a corresponding private key in the Windows User Certificate Store. Can not use it for SSL.");
return;
} else
{
if (pDebug) e.WebTest.AddCommentToResult("Certificate was found in Windows User Certificate Store using thumbprint and HAS a corresponding PRIVATE KEY");
}
}
}
} else
{
if (pDebug) e.WebTest.AddCommentToResult("Certificate is NOT INSTALLED");
if(pInstallTrusted && myCertTrusted || pInstallUntrusted)
if (myClientCertAndKey.HasPrivateKey)
{
// Try to install certificate
if (myCertTrusted || !myCertTrusted)
if (pInstallTrusted && myCertTrusted || pInstallUntrusted)
{
// Try to install certificate
// Install in user store
try {
try
{
myClientCertAndKey.FriendlyName = "VSTT";
cuStore.Add(myClientCertAndKey);
if (pDebug) e.WebTest.AddCommentToResult("Certificate HAS BEEN INSTALLED in the Current User Windows Certificate Store with Friendly Name: VSTT");
} catch (Exception ex)
}
catch (Exception ex)
{
e.WebTest.AddCommentToResult("Error: COULD NOT INSTALL in the Current User Windows Certificate Store, Message: " + ex.Message);
return;
}
}
else
{
e.WebTest.AddCommentToResult("Error: COULD NOT INSTALL the certificate since you selected NOT to install untrusted certificates.");
return;
}
} else
{
e.WebTest.AddCommentToResult("Error: COULD NOT INSTALL the certificate since the file did not contain a private key and cannot be used without one.");
return;
}
}
// Set the PreRequest method to add the certificate on requests
haveCert = true;
if (pDebug) e.WebTest.AddCommentToResult("Certificate will be ADDED TO REQUESTS");
sw.Stop();
if (pDebug) e.WebTest.AddCommentToResult("Certificate processing done in " + sw.ElapsedMilliseconds + "ms");
}
public override void PreRequest(object sender, PreRequestEventArgs e)
......@@ -811,7 +869,7 @@ namespace LIL_VSTT_Plugins
e.WebTest.Context["Client Certificate"] = myClientCertAndKey.Subject;
} else
{
e.WebTest.Context["Client Certificate"] = "No certificate was added";
e.WebTest.Context["Client Certificate"] = "No certificate was specified in this request. Windows will try to automatically choose an installed client certificate if requested by the server.";
}
}
}
......
......@@ -8,7 +8,7 @@
</Request>
</Items>
<ContextParameters>
<ContextParameter Name="Valid-PEM-Path" Value="U:\projekt\MjukaCertifikat\Interna_Certifikat_Okt_2016\8946019907112000070.pem" />
<ContextParameter Name="PEM" Value="U:\projekt\MjukaCertifikat\Interna_Certifikat_Okt_2016\8946019907112000070.pem" />
</ContextParameters>
<ValidationRules>
<ValidationRule Classname="Microsoft.VisualStudio.TestTools.WebTesting.Rules.ValidateResponseUrl, Microsoft.VisualStudio.QualityTools.WebTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" DisplayName="Response URL" Description="Validates that the response URL after redirects are followed is the same as the recorded response URL. QueryString parameters are ignored." Level="Low" ExectuionOrder="BeforeDependents" />
......@@ -33,7 +33,7 @@
<WebTestPlugin Classname="LIL_VSTT_Plugins.ClientCertificatePlugin, LIL_VSTT_Plugins, Version=1.3.0.0, Culture=neutral, PublicKeyToken=null" DisplayName="Client Certificate" Description="(C) Copyright 2016 LIGHTS IN LINE AB&#xD;&#xA;Sätter webtestet att använda ett specifikt client cert för SSL. Certifikatet behöver inte installeras i certstore först.">
<RuleParameters>
<RuleParameter Name="pCertificatePath" Value="" />
<RuleParameter Name="pCertificatePathParameter" Value="Valid-PEM-Path" />
<RuleParameter Name="pCertificatePathParameter" Value="PEM" />
<RuleParameter Name="pCertificatePassword" Value="" />
<RuleParameter Name="pCertificatePasswordParameter" Value="" />
<RuleParameter Name="pDebug" Value="True" />
......