From d88e2931c646ac73d0d4b384149db9e96d67b72d Mon Sep 17 00:00:00 2001 From: Luiz Henrique Cassettari Date: Tue, 19 Sep 2023 15:30:37 -0300 Subject: [PATCH] Version 1.0.1 - `Certificate` features `GetSignedFileSubject` and `GetSignedFileIssuer` --- CHANGELOG.md | 5 +- README.md | 16 +++- ricaun.Security.WinTrust.Tests/Tests.cs | 2 +- .../Tests_Certificate.cs | 88 +++++++++++++++++++ ricaun.Security.WinTrust/Certificate.cs | 86 +++++++++++++++++- .../ricaun.Security.WinTrust.csproj | 2 +- 6 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 ricaun.Security.WinTrust.Tests/Tests_Certificate.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2423b..fd1a699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.0.1] / 2023-09-19 +- `Certificate` features `GetSignedFileSubject` and `GetSignedFileIssuer` + ## [1.0.0] / 2023-09-19 - First Release - [vNext]: ../../compare/1.0.0...HEAD +[1.0.1]: ../../compare/1.0.0...1.0.1 [1.0.0]: ../../compare/1.0.0 \ No newline at end of file diff --git a/README.md b/README.md index 74465d0..3fe6cca 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,26 @@ bool result = WinTrust.VerifyEmbeddedSignature(@"C:\Windows\explorer.exe"); ``` ### Certificate -Utility class to check if the file is signed. +Utility class to check the `Certificate` file is signed, subject and issuer. ```csharp bool result = Certificate.IsSignedFile(@"C:\Windows\explorer.exe"); ``` +If you want to get the subject or issuer of the file, you can use the following methods: + +``` csharp +string subject = Certificate.GetSignedFileSubject(@"C:\Windows\explorer.exe"); +string issuer = Certificate.GetSignedFileIssuer(@"C:\Windows\explorer.exe"); +``` + +If you want to get a specific field of the subject or issuer, you can use the following methods: + +``` csharp +string communName = Certificate.GetSignedFileSubject(@"C:\Windows\explorer.exe", "cn"); // "Microsoft Windows" +string organization = Certificate.GetSignedFileIssuer(@"C:\Windows\explorer.exe", "o"); // "Microsoft Corporation" +``` + ### Dummy Certificate The [signfile.pfx](ricaun.Security.WinTrust.Tests/signfile) is a dummy certificate created to sign the `ConsoleAppSignedNotTrusted.exe` file and test the `WinTrust.VerifyEmbeddedSignature` method. diff --git a/ricaun.Security.WinTrust.Tests/Tests.cs b/ricaun.Security.WinTrust.Tests/Tests.cs index bda34d4..03c7be6 100644 --- a/ricaun.Security.WinTrust.Tests/Tests.cs +++ b/ricaun.Security.WinTrust.Tests/Tests.cs @@ -30,7 +30,7 @@ public void VerifyEmbeddedSignature_ShouldBe(string fileName, bool isSignedTrust [TestCase("C:\\Windows\\notepad.exe", false)] [TestCase("C:\\Windows\\explorer.exe", true)] - public void VerifyEmbeddedSignaturePath_ShouldBe(string filePath, bool isSigned) + public void Windows_VerifyEmbeddedSignature_ShouldBe(string filePath, bool isSigned) { if (File.Exists(filePath) == false) Assert.Ignore("File not found"); diff --git a/ricaun.Security.WinTrust.Tests/Tests_Certificate.cs b/ricaun.Security.WinTrust.Tests/Tests_Certificate.cs new file mode 100644 index 0000000..eb96b36 --- /dev/null +++ b/ricaun.Security.WinTrust.Tests/Tests_Certificate.cs @@ -0,0 +1,88 @@ +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace ricaun.Security.WinTrust.Tests +{ + public class Tests_Certificate + { + string Directory => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + [TestCase("ConsoleAppSigned.exe", "Sectigo Public Code Signing CA R36")] + [TestCase("ConsoleAppSignedNotTrusted.exe", "signfile")] + public void GetSignedFileIssuerCode_ShouldBe(string fileName, string communName) + { + var filePath = Path.Combine(Directory, fileName); + var result = Certificate.GetSignedFileIssuer(filePath, "cn"); + Assert.AreEqual(communName, result); + } + + [TestCase("ConsoleAppSigned.exe", "ricaun")] + [TestCase("ConsoleAppSignedNotTrusted.exe", "signfile")] + public void GetSignedFileSubjectCode_ShouldBe(string fileName, string communName) + { + var filePath = Path.Combine(Directory, fileName); + var result = Certificate.GetSignedFileSubject(filePath, "cn"); + Assert.AreEqual(communName, result); + } + + [TestCase("ConsoleApp.exe", false)] + [TestCase("ConsoleAppSigned.exe", true)] + [TestCase("ConsoleAppSignedNotTrusted.exe", true)] + public void GetSignedFileIssuer_ShouldBe(string fileName, bool isNotEmpty) + { + var filePath = Path.Combine(Directory, fileName); + var result = Certificate.GetSignedFileIssuer(filePath); + Assert.AreEqual(isNotEmpty, !string.IsNullOrEmpty(result)); + } + + [TestCase("ConsoleApp.exe", false)] + [TestCase("ConsoleAppSigned.exe", true)] + [TestCase("ConsoleAppSignedNotTrusted.exe", true)] + public void GetSignedFileSubject_ShouldBe(string fileName, bool isNotEmpty) + { + var filePath = Path.Combine(Directory, fileName); + var result = Certificate.GetSignedFileSubject(filePath); + Assert.AreEqual(isNotEmpty, !string.IsNullOrEmpty(result)); + } + + [TestCase("C:\\Windows\\explorer.exe", "cn", "Microsoft Windows")] + [TestCase("C:\\Windows\\explorer.exe", "o", "Microsoft Corporation")] + public void Windows_GetSignedFileSubjectCode_ShouldBe(string filePath, string communName, string expected) + { + if (File.Exists(filePath) == false) + Assert.Ignore("File not found"); + + var result = Certificate.GetSignedFileSubject(filePath, communName); + Assert.AreEqual(expected, result); + } + + [Test] + public void Internal_SplitAndGetStartWith() + { + var baseString = "CN=signfile, OU=signfile1, O=signfile2, L=signfile3, S=signfile4, C=signfile5"; + var expectedValues = new Dictionary() { + {"cn","signfile"}, + {"ou","signfile1"}, + {"o","signfile2"}, + {"l","signfile3"}, + {"s","signfile4"}, + {"c","signfile5"}, + }; + + foreach (var expectedValue in expectedValues) + { + var code = expectedValue.Key; + var expected = expectedValue.Value; + + Assert.AreEqual(expected, Certificate.SplitAndGetStartWith(baseString, code)); + Assert.AreEqual(expected, Certificate.SplitAndGetStartWith(baseString, code + '='), "= should be Ignore"); + Assert.AreEqual(expected, Certificate.SplitAndGetStartWith(baseString, code.ToUpper()), "UpperCase be Ignore"); + } + + Assert.IsEmpty(Certificate.SplitAndGetStartWith(baseString, "a")); + Assert.IsEmpty(Certificate.SplitAndGetStartWith(baseString, "b")); + } + } +} \ No newline at end of file diff --git a/ricaun.Security.WinTrust/Certificate.cs b/ricaun.Security.WinTrust/Certificate.cs index da3a144..7119652 100644 --- a/ricaun.Security.WinTrust/Certificate.cs +++ b/ricaun.Security.WinTrust/Certificate.cs @@ -1,12 +1,15 @@ namespace ricaun.Security.WinTrust { + using System.Linq; + using System.Security.Cryptography.X509Certificates; + /// /// Certificate /// public sealed class Certificate { /// - /// Try to to check if the file is signed. + /// Try to to check if the file is signed. /// /// /// @@ -14,7 +17,7 @@ public static bool IsSignedFile(string fileName) { try { - System.Security.Cryptography.X509Certificates.X509Certificate.CreateFromSignedFile(fileName); + X509Certificate.CreateFromSignedFile(fileName); return true; } catch @@ -22,5 +25,82 @@ public static bool IsSignedFile(string fileName) return false; } } + + /// + /// Get SignedFileIssuer + /// + /// + /// + /// Return if the is not signed. + public static string GetSignedFileIssuer(string fileName) + { + if (IsSignedFile(fileName)) + { + return X509Certificate.CreateFromSignedFile(fileName).Issuer; + } + return string.Empty; + } + + /// + /// Get SignedFileSubject + /// + /// + /// + /// Return if the is not signed. + public static string GetSignedFileSubject(string fileName) + { + if (IsSignedFile(fileName)) + { + return X509Certificate.CreateFromSignedFile(fileName).Subject; + } + return string.Empty; + } + + /// + /// Get SignedFileIssuer with the start with. + /// + /// + /// + /// Return if the is not signed. + public static string GetSignedFileIssuer(string fileName, string code) + { + var subject = GetSignedFileIssuer(fileName); + + if (string.IsNullOrEmpty(subject)) return string.Empty; + + return SplitAndGetStartWith(subject, code); + } + + /// + /// Get SignedFileSubject with the start with. + /// + /// + /// + /// Return if the is not signed. + public static string GetSignedFileSubject(string fileName, string code) + { + var subject = GetSignedFileSubject(fileName); + + if (string.IsNullOrEmpty(subject)) return string.Empty; + + return SplitAndGetStartWith(subject, code); + } + + internal static string SplitAndGetStartWith(string subject, string codeStartWith) + { + const char ConstEqual = '='; + const char ConstSeparator = ','; + + var texts = subject.Split(ConstSeparator) + .Select(s => s.Trim()); + + codeStartWith = codeStartWith.Trim().TrimEnd(ConstEqual) + ConstEqual; + + if (texts.FirstOrDefault(t => t.StartsWith(codeStartWith, System.StringComparison.InvariantCultureIgnoreCase)) is string text) + { + return text.Remove(0, codeStartWith.Length); + } + return string.Empty; + } } -} +} \ No newline at end of file diff --git a/ricaun.Security.WinTrust/ricaun.Security.WinTrust.csproj b/ricaun.Security.WinTrust/ricaun.Security.WinTrust.csproj index cb2e8a8..3f8eea4 100644 --- a/ricaun.Security.WinTrust/ricaun.Security.WinTrust.csproj +++ b/ricaun.Security.WinTrust/ricaun.Security.WinTrust.csproj @@ -33,7 +33,7 @@ ricaun.Security.WinTrust - 1.0.0 + 1.0.1 {AC121D4A-F00B-4B94-AD1B-ED7665C4803A}