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; ...@@ -20,6 +20,7 @@ using System.IO;
20 using System.ComponentModel; 20 using System.ComponentModel;
21 using System.Text.RegularExpressions; 21 using System.Text.RegularExpressions;
22 using System.Security.Cryptography.X509Certificates; 22 using System.Security.Cryptography.X509Certificates;
23 using System.Diagnostics;
23 24
24 namespace LIL_VSTT_Plugins 25 namespace LIL_VSTT_Plugins
25 { 26 {
...@@ -558,21 +559,21 @@ namespace LIL_VSTT_Plugins ...@@ -558,21 +559,21 @@ namespace LIL_VSTT_Plugins
558 /// WebTest Client Certificate 559 /// WebTest Client Certificate
559 /// </summary> 560 /// </summary>
560 [DisplayName("Client Certificate")] 561 [DisplayName("Client Certificate")]
561 [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.")] 562 [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.")]
562 public class ClientCertificatePlugin : WebTestPlugin 563 public class ClientCertificatePlugin : WebTestPlugin
563 { 564 {
564 [DisplayName("Certificate Path")] 565 [DisplayName("Certificate Path")]
565 [Description("Sökvägen till certifikatfilen (.P12/.PFX/.PEM med privat nyckel)")] 566 [Description("Sökvägen till certifikatfilen (P12/PFX/PEM med privat nyckel eller CER/DER utan privat nyckel)")]
566 [DefaultValue("")] 567 [DefaultValue("")]
567 public string pCertificatePath { get; set; } 568 public string pCertificatePath { get; set; }
568 569
569 [DisplayName("Certificate Path Parameter")] 570 [DisplayName("Certificate Path Parameter")]
570 [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")] 571 [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")]
571 [DefaultValue("")] 572 [DefaultValue("")]
572 public string pCertificatePathParameter { get; set; } 573 public string pCertificatePathParameter { get; set; }
573 574
574 [DisplayName("Certificate Password")] 575 [DisplayName("Certificate Password")]
575 [Description("Ange lösenordet för att öppna certifikatfilen")] 576 [Description("Ange lösenordet för att öppna skyddade/krypterade filer")]
576 [DefaultValue("")] 577 [DefaultValue("")]
577 public string pCertificatePassword { get; set; } 578 public string pCertificatePassword { get; set; }
578 579
...@@ -598,19 +599,25 @@ namespace LIL_VSTT_Plugins ...@@ -598,19 +599,25 @@ namespace LIL_VSTT_Plugins
598 599
599 private bool haveCert = false; 600 private bool haveCert = false;
600 private X509Certificate2 myClientCertAndKey; 601 private X509Certificate2 myClientCertAndKey;
602 private Regex p12RegExp = new Regex(@"p12$|pfx$",RegexOptions.IgnoreCase);
603 private Regex cerRegExp = new Regex(@"cer$|der$", RegexOptions.IgnoreCase);
604 private Regex pemRegExp = new Regex(@"pem$", RegexOptions.IgnoreCase);
601 605
602 public override void PreWebTest(object sender, PreWebTestEventArgs e) 606 public override void PreWebTest(object sender, PreWebTestEventArgs e)
603 { 607 {
608 Stopwatch sw = new Stopwatch();
609 sw.Start();
610
604 base.PreWebTest(sender, e); 611 base.PreWebTest(sender, e);
605 String certPath, certPass; 612 String certPath, certPass;
606 // Ladda in certifikatet och sätt CertPolicy 613 // Ladda in certifikatet och sätt CertPolicy
607 614
608 if (!String.IsNullOrWhiteSpace(pCertificatePathParameter) && e.WebTest.Context.ContainsKey(pCertificatePathParameter) && !String.IsNullOrWhiteSpace(e.WebTest.Context[pCertificatePathParameter].ToString()) ) 615 if (!String.IsNullOrWhiteSpace(pCertificatePathParameter) && e.WebTest.Context.ContainsKey(pCertificatePathParameter) && !String.IsNullOrWhiteSpace(e.WebTest.Context[pCertificatePathParameter].ToString()) )
609 { 616 {
610 certPath = e.WebTest.Context[pCertificatePathParameter].ToString(); 617 certPath = e.WebTest.Context[pCertificatePathParameter].ToString().Trim();
611 } else 618 } else
612 { 619 {
613 certPath = pCertificatePath; 620 certPath = pCertificatePath.Trim();
614 } 621 }
615 622
616 if (!String.IsNullOrWhiteSpace(pCertificatePasswordParameter) && e.WebTest.Context.ContainsKey(pCertificatePasswordParameter) && !String.IsNullOrWhiteSpace(e.WebTest.Context[pCertificatePasswordParameter].ToString())) 623 if (!String.IsNullOrWhiteSpace(pCertificatePasswordParameter) && e.WebTest.Context.ContainsKey(pCertificatePasswordParameter) && !String.IsNullOrWhiteSpace(e.WebTest.Context[pCertificatePasswordParameter].ToString()))
...@@ -629,11 +636,11 @@ namespace LIL_VSTT_Plugins ...@@ -629,11 +636,11 @@ namespace LIL_VSTT_Plugins
629 return; 636 return;
630 } 637 }
631 638
632 // Check what type of container we have. All files are treated as PEM unless the extension is .pfx or .p12 639 // Check what type of container we have. All files are treated as PEM unless the extension matches our winX509regExp regular expression (see above)
633 // Read certificate and private key depending on type 640 // Read certificate and private key depending on type
634 if (certPath.ToLower().EndsWith(".pfx") || certPath.ToLower().EndsWith(".p12")) 641 if (p12RegExp.IsMatch(certPath))
635 { 642 {
636 if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as PFX/PKCS12"); 643 if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as PFX/P12");
637 try 644 try
638 { 645 {
639 myClientCertAndKey = new X509Certificate2(certPath, certPass, X509KeyStorageFlags.PersistKeySet); 646 myClientCertAndKey = new X509Certificate2(certPath, certPass, X509KeyStorageFlags.PersistKeySet);
...@@ -643,9 +650,21 @@ namespace LIL_VSTT_Plugins ...@@ -643,9 +650,21 @@ namespace LIL_VSTT_Plugins
643 e.WebTest.AddCommentToResult("Error during loading of certificate: " + certPath + " Message: " + ex.Message); 650 e.WebTest.AddCommentToResult("Error during loading of certificate: " + certPath + " Message: " + ex.Message);
644 return; 651 return;
645 } 652 }
646 } else 653 } else if (cerRegExp.IsMatch(certPath))
654 {
655 if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as CER/DER without private key");
656 try
657 {
658 myClientCertAndKey = new X509Certificate2(certPath, certPass);
659 }
660 catch (Exception ex)
661 {
662 e.WebTest.AddCommentToResult("Error during loading of certificate: " + certPath + " Message: " + ex.Message);
663 return;
664 }
665 } else if (pemRegExp.IsMatch(certPath))
647 { 666 {
648 if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as PEM/PKCS8"); 667 if (pDebug) e.WebTest.AddCommentToResult("Certificate file is treated as OpenSSL encoded PEM");
649 668
650 // Use Bouncy Castle to read the certificate and key, then convert to .NET X509Certificate2 and X509Certificate 669 // Use Bouncy Castle to read the certificate and key, then convert to .NET X509Certificate2 and X509Certificate
651 String text; 670 String text;
...@@ -698,19 +717,24 @@ namespace LIL_VSTT_Plugins ...@@ -698,19 +717,24 @@ namespace LIL_VSTT_Plugins
698 } 717 }
699 keyTextBeginPos = text.IndexOf("-----BEGIN", keyTextEndPos); 718 keyTextBeginPos = text.IndexOf("-----BEGIN", keyTextEndPos);
700 } 719 }
701 if (bcKey == null || bcCert == null) 720 if (bcCert == null)
702 { 721 {
703 e.WebTest.AddCommentToResult("Error: PEM file has to contain both certificate and private key"); 722 e.WebTest.AddCommentToResult("Error: PEM file has to contain an x509 certificate");
704 return; 723 return;
705 } 724 }
706 try { 725 try {
707 myClientCertAndKey = new X509Certificate2(Org.BouncyCastle.Security.DotNetUtilities.ToX509Certificate(bcCert)); 726 myClientCertAndKey = new X509Certificate2(Org.BouncyCastle.Security.DotNetUtilities.ToX509Certificate(bcCert));
708 myClientCertAndKey.PrivateKey = Org.BouncyCastle.Security.DotNetUtilities.ToRSA(bcKey.Private as Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters); 727 if(bcKey != null) myClientCertAndKey.PrivateKey = Org.BouncyCastle.Security.DotNetUtilities.ToRSA(bcKey.Private as Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters);
709 } catch (Exception ex) 728 } catch (Exception ex)
710 { 729 {
711 e.WebTest.AddCommentToResult("Error during loading of PEM file: " + certPath + " Message: " + ex.Message); 730 e.WebTest.AddCommentToResult("Error during loading of PEM file: " + certPath + " Message: " + ex.Message);
712 return; 731 return;
713 } 732 }
733 } else
734 {
735 // Unknown or unsuported format
736 e.WebTest.AddCommentToResult("Error during loading of file: " + certPath + " Message: Unsupported format");
737 return;
714 } 738 }
715 739
716 // Check that we have a certificate 740 // Check that we have a certificate
...@@ -721,16 +745,17 @@ namespace LIL_VSTT_Plugins ...@@ -721,16 +745,17 @@ namespace LIL_VSTT_Plugins
721 } 745 }
722 else 746 else
723 { 747 {
724 if (pDebug) e.WebTest.AddCommentToResult("Certificate File " + certPath + " loaded successfully."); 748 if (pDebug) e.WebTest.AddCommentToResult("Certificate File " + certPath + " loaded successfully in " + sw.ElapsedMilliseconds + "ms");
725 } 749 }
726 750
727 // Check that it seems okey 751 // Check that it seems okey
728 if (string.IsNullOrWhiteSpace(myClientCertAndKey.GetCertHashString())) 752 if (string.IsNullOrWhiteSpace(myClientCertAndKey.Thumbprint))
729 { 753 {
730 if (pDebug) e.WebTest.AddCommentToResult("Certificate File " + certPath + " contains no SHA1 hash. Not using it."); 754 if (pDebug) e.WebTest.AddCommentToResult("Certificate File " + certPath + " contains no Thumbprint. Not using it.");
731 return; 755 return;
732 } 756 }
733 if (pDebug) e.WebTest.AddCommentToResult("Loaded client certificate for Subject: [" + myClientCertAndKey.Subject + "] Issued by: [" + myClientCertAndKey.Issuer + "] Expires: [" + myClientCertAndKey.GetExpirationDateString() + "]"); 757 if (pDebug) e.WebTest.AddCommentToResult("Subject: [" + myClientCertAndKey.Subject + "]");
758 if (pDebug) e.WebTest.AddCommentToResult("Issued by: [" + myClientCertAndKey.Issuer + "] Expires: [" + myClientCertAndKey.GetExpirationDateString() + "]");
734 759
735 // Check if the certificate is trusted (i.e. chain can be validated) 760 // Check if the certificate is trusted (i.e. chain can be validated)
736 bool myCertTrusted = false; 761 bool myCertTrusted = false;
...@@ -747,24 +772,19 @@ namespace LIL_VSTT_Plugins ...@@ -747,24 +772,19 @@ namespace LIL_VSTT_Plugins
747 // Check if it is expired or about to expire 772 // Check if it is expired or about to expire
748 if(myClientCertAndKey.NotAfter < DateTime.Now) 773 if(myClientCertAndKey.NotAfter < DateTime.Now)
749 { 774 {
750 e.WebTest.AddCommentToResult("Warning: Client Certificate has expired. Might not be trusted on server. Expired " + myClientCertAndKey.NotAfter.ToString()); 775 e.WebTest.AddCommentToResult("Warning: Client Certificate has expired. Might not be trusted on server.");
751 } else if (myClientCertAndKey.NotBefore > DateTime.Now) 776 } else if (myClientCertAndKey.NotBefore > DateTime.Now)
752 { 777 {
753 e.WebTest.AddCommentToResult("Warning: Client Certificate is not valid yet. Might not be trusted on server. Valid " + myClientCertAndKey.NotBefore.ToString()); 778 e.WebTest.AddCommentToResult("Warning: Client Certificate is not valid yet. Might not be trusted on server. Valid on " + myClientCertAndKey.NotBefore.ToString());
754 } else if (myClientCertAndKey.NotAfter < DateTime.Now.AddDays(14)) 779 } else if (myClientCertAndKey.NotAfter < DateTime.Now.AddDays(14))
755 { 780 {
756 e.WebTest.AddCommentToResult("Warning: Client Certificate will expire in less than 14 days. Better renew it soon. Expires " + myClientCertAndKey.NotAfter.ToString()); 781 e.WebTest.AddCommentToResult("Warning: Client Certificate will expire in less than 14 days. Better renew it soon.");
757 } 782 }
758 783
759 // Check if we have a private key 784 // Check if we have a private key
760 if (!myClientCertAndKey.HasPrivateKey) 785 if (myClientCertAndKey.HasPrivateKey)
761 {
762 // Cant use it without private key
763 e.WebTest.AddCommentToResult("Error: Certificate HAS NO PRIVATE KEY, cannot use it without one.");
764 return;
765 } else
766 { 786 {
767 if (pDebug) e.WebTest.AddCommentToResult("Certificate HAS PRIVATE KEY"); 787 if (pDebug) e.WebTest.AddCommentToResult("Certificate HAS PRIVATE KEY in file");
768 } 788 }
769 789
770 // Check that the certificate exists in the cert store 790 // Check that the certificate exists in the cert store
...@@ -772,31 +792,69 @@ namespace LIL_VSTT_Plugins ...@@ -772,31 +792,69 @@ namespace LIL_VSTT_Plugins
772 cuStore.Open(OpenFlags.ReadWrite); 792 cuStore.Open(OpenFlags.ReadWrite);
773 if(cuStore.Certificates.Contains(myClientCertAndKey)) { 793 if(cuStore.Certificates.Contains(myClientCertAndKey)) {
774 if (pDebug) e.WebTest.AddCommentToResult("Certificate already INSTALLED in Current User Windows Certificate Store"); 794 if (pDebug) e.WebTest.AddCommentToResult("Certificate already INSTALLED in Current User Windows Certificate Store");
795 // Try to load the key from store if we dont have it and verify that it belongs to the certificate
796 if(!myClientCertAndKey.HasPrivateKey)
797 {
798 X509Certificate2Collection certCol = cuStore.Certificates.Find(X509FindType.FindByThumbprint, myClientCertAndKey.Thumbprint, false);
799 if(certCol == null || certCol.Count == 0)
800 {
801 e.WebTest.AddCommentToResult("Error: Certificate could not be loaded from store using it's thumbprint which is very strange. Aborting");
802 return;
803 } else
804 {
805 if(certCol.Count > 1)
806 {
807 if (pDebug) e.WebTest.AddCommentToResult("Certificates thumbprint has more than one match in the Windows User Certificate Store, using the first one.");
808 }
809 X509Certificate2 cert = certCol[0];
810 if (!cert.HasPrivateKey)
811 {
812 e.WebTest.AddCommentToResult("Error: Certificate does not have a corresponding private key in the Windows User Certificate Store. Can not use it for SSL.");
813 return;
814 } else
815 {
816 if (pDebug) e.WebTest.AddCommentToResult("Certificate was found in Windows User Certificate Store using thumbprint and HAS a corresponding PRIVATE KEY");
817 }
818 }
819 }
775 } else 820 } else
776 { 821 {
777 if (pDebug) e.WebTest.AddCommentToResult("Certificate is NOT INSTALLED"); 822 if (pDebug) e.WebTest.AddCommentToResult("Certificate is NOT INSTALLED");
778 if(pInstallTrusted && myCertTrusted || pInstallUntrusted) 823 if (myClientCertAndKey.HasPrivateKey)
779 { 824 {
780 // Try to install certificate 825 if (pInstallTrusted && myCertTrusted || pInstallUntrusted)
781 if (myCertTrusted || !myCertTrusted)
782 { 826 {
827 // Try to install certificate
783 // Install in user store 828 // Install in user store
784 try { 829 try
830 {
785 myClientCertAndKey.FriendlyName = "VSTT"; 831 myClientCertAndKey.FriendlyName = "VSTT";
786 cuStore.Add(myClientCertAndKey); 832 cuStore.Add(myClientCertAndKey);
787 if (pDebug) e.WebTest.AddCommentToResult("Certificate HAS BEEN INSTALLED in the Current User Windows Certificate Store with Friendly Name: VSTT"); 833 if (pDebug) e.WebTest.AddCommentToResult("Certificate HAS BEEN INSTALLED in the Current User Windows Certificate Store with Friendly Name: VSTT");
788 } catch (Exception ex) 834 }
835 catch (Exception ex)
789 { 836 {
790 e.WebTest.AddCommentToResult("Error: COULD NOT INSTALL in the Current User Windows Certificate Store, Message: " + ex.Message); 837 e.WebTest.AddCommentToResult("Error: COULD NOT INSTALL in the Current User Windows Certificate Store, Message: " + ex.Message);
791 return; 838 return;
792 } 839 }
793 } 840 }
841 else
842 {
843 e.WebTest.AddCommentToResult("Error: COULD NOT INSTALL the certificate since you selected NOT to install untrusted certificates.");
844 return;
845 }
846 } else
847 {
848 e.WebTest.AddCommentToResult("Error: COULD NOT INSTALL the certificate since the file did not contain a private key and cannot be used without one.");
849 return;
794 } 850 }
795 } 851 }
796 852
797 // Set the PreRequest method to add the certificate on requests 853 // Set the PreRequest method to add the certificate on requests
798 haveCert = true; 854 haveCert = true;
799 if (pDebug) e.WebTest.AddCommentToResult("Certificate will be ADDED TO REQUESTS"); 855 if (pDebug) e.WebTest.AddCommentToResult("Certificate will be ADDED TO REQUESTS");
856 sw.Stop();
857 if (pDebug) e.WebTest.AddCommentToResult("Certificate processing done in " + sw.ElapsedMilliseconds + "ms");
800 } 858 }
801 859
802 public override void PreRequest(object sender, PreRequestEventArgs e) 860 public override void PreRequest(object sender, PreRequestEventArgs e)
...@@ -811,7 +869,7 @@ namespace LIL_VSTT_Plugins ...@@ -811,7 +869,7 @@ namespace LIL_VSTT_Plugins
811 e.WebTest.Context["Client Certificate"] = myClientCertAndKey.Subject; 869 e.WebTest.Context["Client Certificate"] = myClientCertAndKey.Subject;
812 } else 870 } else
813 { 871 {
814 e.WebTest.Context["Client Certificate"] = "No certificate was added"; 872 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.";
815 } 873 }
816 } 874 }
817 } 875 }
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
8 </Request> 8 </Request>
9 </Items> 9 </Items>
10 <ContextParameters> 10 <ContextParameters>
11 <ContextParameter Name="Valid-PEM-Path" Value="U:\projekt\MjukaCertifikat\Interna_Certifikat_Okt_2016\8946019907112000070.pem" /> 11 <ContextParameter Name="PEM" Value="U:\projekt\MjukaCertifikat\Interna_Certifikat_Okt_2016\8946019907112000070.pem" />
12 </ContextParameters> 12 </ContextParameters>
13 <ValidationRules> 13 <ValidationRules>
14 <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" /> 14 <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 @@ ...@@ -33,7 +33,7 @@
33 <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."> 33 <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.">
34 <RuleParameters> 34 <RuleParameters>
35 <RuleParameter Name="pCertificatePath" Value="" /> 35 <RuleParameter Name="pCertificatePath" Value="" />
36 <RuleParameter Name="pCertificatePathParameter" Value="Valid-PEM-Path" /> 36 <RuleParameter Name="pCertificatePathParameter" Value="PEM" />
37 <RuleParameter Name="pCertificatePassword" Value="" /> 37 <RuleParameter Name="pCertificatePassword" Value="" />
38 <RuleParameter Name="pCertificatePasswordParameter" Value="" /> 38 <RuleParameter Name="pCertificatePasswordParameter" Value="" />
39 <RuleParameter Name="pDebug" Value="True" /> 39 <RuleParameter Name="pDebug" Value="True" />
......