From d9e94ee756cf94f938d85baa347fb5c540923070 Mon Sep 17 00:00:00 2001 From: Mahdi Hamedani Nezhad <150752864+thegreatmhn@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:12:35 -0700 Subject: [PATCH 1/6] Update LdapOperations.cs add authentication module to ldapoperation. --- Certify/Lib/LdapOperations.cs | 51 ++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/Certify/Lib/LdapOperations.cs b/Certify/Lib/LdapOperations.cs index 38ac88c..d1ec2d8 100644 --- a/Certify/Lib/LdapOperations.cs +++ b/Certify/Lib/LdapOperations.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.DirectoryServices; using System.Linq; @@ -11,14 +11,23 @@ class LdapOperations public string ConfigurationPath { get; } public string LdapServer { get; } + // Stored once, reused by every method below instead of literal strings. + private readonly string _username; + private readonly string _password; + private readonly AuthenticationTypes _authType; + public LdapOperations() : this(null, null) { } - public LdapOperations(string domain, string server) + public LdapOperations(string domain, string server, string username = null, string password = null) { + _username = username; + _password = password; + _authType = AuthenticationTypes.Secure; + string root_dse_path; if (domain == null) @@ -27,7 +36,13 @@ public LdapOperations(string domain, string server) root_dse_path = $"LDAP://{domain}/RootDSE"; using (var root_dse = new DirectoryEntry(root_dse_path)) + { + root_dse.Username = _username; + root_dse.Password = _password; + root_dse.AuthenticationType = _authType; ConfigurationPath = $"{root_dse.Properties["configurationNamingContext"][0]}"; + } + if (server == null) LdapServer = string.Empty; @@ -35,6 +50,22 @@ public LdapOperations(string domain, string server) LdapServer = $"{server}/"; } + // Single place that builds an authenticated DirectoryEntry. + // Every method now calls this instead of repeating Username/Password/AuthenticationType assignments. + private DirectoryEntry GetDirectoryEntry(string path) + { + var entry = new DirectoryEntry(path); + + if (!string.IsNullOrEmpty(_username)) + { + entry.Username = _username; + entry.Password = _password; + entry.AuthenticationType = _authType; + } + + return entry; + } + public IEnumerable GetPKIObjects() { var pki_objects = new List(); @@ -42,7 +73,7 @@ public IEnumerable GetPKIObjects() // Container location per MS-WCCE 2.2.2.11.2 Enrollment Services Container // - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/3ec073ec-9b91-4bee-964e-56f22a93a28c - using (var root = new DirectoryEntry($"LDAP://{LdapServer}CN=Public Key Services,CN=Services,{ConfigurationPath}")) + using (var root = GetDirectoryEntry($"LDAP://{LdapServer}CN=Public Key Services,CN=Services,{ConfigurationPath}")) { using (var ds = new DirectorySearcher(root) { SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner }) { @@ -98,7 +129,7 @@ public IEnumerable GetEnterpriseCAs(string ca_na // Container location per MS-WCCE 2.2.2.11.2 Enrollment Services Container // - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/3ec073ec-9b91-4bee-964e-56f22a93a28c - using (var root = new DirectoryEntry($"LDAP://{LdapServer}CN=Enrollment Services,CN=Public Key Services,CN=Services,{ConfigurationPath}")) + using (var root = GetDirectoryEntry($"LDAP://{LdapServer}CN=Enrollment Services,CN=Public Key Services,CN=Services,{ConfigurationPath}")) { using (var ds = new DirectorySearcher(root) { SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner }) { @@ -136,7 +167,7 @@ public CertificateAuthority GetNtAuthCertificates() // Container location per MS-WCCE 2.2.2.11.3 NTAuthCertificates Object // - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/f1004c63-8508-43b5-9b0b-ee7880183745 - using (var root = new DirectoryEntry($"LDAP://{LdapServer}CN=NTAuthCertificates,CN=Public Key Services,CN=Services,{ConfigurationPath}")) + using (var root = GetDirectoryEntry($"LDAP://{LdapServer}CN=NTAuthCertificates,CN=Public Key Services,CN=Services,{ConfigurationPath}")) { using (var ds = new DirectorySearcher(root) { Filter = "(objectClass=certificationAuthority)" }) { @@ -168,7 +199,7 @@ public IEnumerable GetCertificateTemplates(List use // Container location per MS-WCCE 2.2.2.11.1 Certificates Templates Container // - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/9279abb2-3dfa-4631-845c-43c187ac4b44 - using (var root = new DirectoryEntry($"LDAP://{LdapServer}CN=Certificate Templates,CN=Public Key Services,CN=Services,{ConfigurationPath}")) + using (var root = GetDirectoryEntry($"LDAP://{LdapServer}CN=Certificate Templates,CN=Public Key Services,CN=Services,{ConfigurationPath}")) { using (var ds = new DirectorySearcher(root) { SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner, Filter = "(objectclass=pKICertificateTemplate)" }) { @@ -207,7 +238,7 @@ public IEnumerable GetCertificateTemplates(List use public DirectoryEntry GetCertificateTemplateEntry(string template) { - using (var root = new DirectoryEntry($"LDAP://{LdapServer}CN=Certificate Templates,CN=Public Key Services,CN=Services,{ConfigurationPath}")) + using (var root = GetDirectoryEntry($"LDAP://{LdapServer}CN=Certificate Templates,CN=Public Key Services,CN=Services,{ConfigurationPath}")) { using (var ds = new DirectorySearcher(root) { SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner, Filter = $"(&(objectclass=pKICertificateTemplate)(name={template}))" }) { @@ -231,7 +262,7 @@ public List GetRootCAs() // Container location per MS-WCCE 2.2.2.11.4 Certification Authorities Container // - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/6c446198-f670-4885-97a9-cbc50a2b96b4 - using (var root = new DirectoryEntry($"LDAP://{LdapServer}CN=Certification Authorities,CN=Public Key Services,CN=Services,{ConfigurationPath}")) + using (var root = GetDirectoryEntry($"LDAP://{LdapServer}CN=Certification Authorities,CN=Public Key Services,CN=Services,{ConfigurationPath}")) { using (var ds = new DirectorySearcher(root) { Filter = "(objectCategory=certificationAuthority)" }) { @@ -260,7 +291,7 @@ public List GetEnterpriseOids() { var oids = new List(); - using (var root = new DirectoryEntry($"LDAP://{LdapServer}CN=OID,CN=Public Key Services,CN=Services,{ConfigurationPath}")) + using (var root = GetDirectoryEntry($"LDAP://{LdapServer}CN=OID,CN=Public Key Services,CN=Services,{ConfigurationPath}")) { using (var ds = new DirectorySearcher(root) { Filter = "(objectClass=msPKI-Enterprise-Oid)" }) { @@ -286,7 +317,7 @@ public List GetEnterpriseOids() public CertificateEnterpriseOid GetEnterpriseOid(string oid) { - using (var root = new DirectoryEntry($"LDAP://{LdapServer}CN=OID,CN=Public Key Services,CN=Services,{ConfigurationPath}")) + using (var root = GetDirectoryEntry($"LDAP://{LdapServer}CN=OID,CN=Public Key Services,CN=Services,{ConfigurationPath}")) { using (var ds = new DirectorySearcher(root) { Filter = $"(msPKI-Cert-Template-OID={oid})" }) { From 5e0bc7fc7f8ea49ecfdea8a9148a3b50950533f6 Mon Sep 17 00:00:00 2001 From: Mahdi Hamedani Nezhad <150752864+thegreatmhn@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:20:10 -0700 Subject: [PATCH 2/6] Update EnumPkiObjects.cs --- Certify/Commands/EnumPkiObjects.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Certify/Commands/EnumPkiObjects.cs b/Certify/Commands/EnumPkiObjects.cs index 917e402..4e7d9f7 100644 --- a/Certify/Commands/EnumPkiObjects.cs +++ b/Certify/Commands/EnumPkiObjects.cs @@ -1,4 +1,4 @@ -using Certify.Domain; +using Certify.Domain; using Certify.Lib; using CommandLine; using System; @@ -25,6 +25,12 @@ public class Options : DefaultOptions [Option("show-admins", HelpText = "Include admin permissions")] public bool ShowAdmins { get; set; } + + [Option('u', "username", HelpText = "Username for LDAP bind (format: user@domain.fqdn). Omit to bind as the current user.")] + public string Username { get; set; } + + [Option('p', "password", HelpText = "Password for LDAP bind. If omitted while --username is set, you'll be prompted (input hidden).")] + public string Password { get; set; } } public static int Execute(Options opts) @@ -37,7 +43,7 @@ public static int Execute(Options opts) return 1; } - var ldap = new LdapOperations(opts.Domain, opts.LdapServer); + var ldap = new LdapOperations(opts.Domain, opts.LdapServer, opts.Username, opts.Password); Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); From c2fe2dbf251c718bacf4b8bce1cbd2b028a4f5db Mon Sep 17 00:00:00 2001 From: Mahdi Hamedani Nezhad <150752864+thegreatmhn@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:21:26 -0700 Subject: [PATCH 3/6] Update EnumTemplates.cs --- Certify/Commands/EnumTemplates.cs | 62 ++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/Certify/Commands/EnumTemplates.cs b/Certify/Commands/EnumTemplates.cs index 3096857..9a221d7 100644 --- a/Certify/Commands/EnumTemplates.cs +++ b/Certify/Commands/EnumTemplates.cs @@ -1,4 +1,4 @@ -using Certify.Domain; +using Certify.Domain; using Certify.Lib; using CommandLine; using System; @@ -6,6 +6,7 @@ using System.Data; using System.DirectoryServices.AccountManagement; using System.Linq; +using System.Security; using System.Security.Principal; namespace Certify.Commands @@ -56,6 +57,12 @@ public class Options : DefaultOptions [Option("show-all-perms", HelpText = "Show all permission details")] public bool ShowAllPermissions { get; set; } + + [Option('u', "username", HelpText = "Username for LDAP bind (format: user@domain.fqdn). Omit to bind as the current user.")] + public string Username { get; set; } + + [Option('p', "password", HelpText = "Password for LDAP bind. If omitted while --username is set, you'll be prompted (input hidden).")] + public string Password { get; set; } } public static int Execute(Options opts) @@ -74,7 +81,15 @@ public static int Execute(Options opts) return 1; } - var ldap = new LdapOperations(opts.Domain, opts.LdapServer); + // If a username was given but no password, prompt interactively instead of + // requiring the password on the command line (avoids it sitting in shell + // history / process listings). + if (!string.IsNullOrEmpty(opts.Username) && string.IsNullOrEmpty(opts.Password)) + { + opts.Password = ReadPasswordMasked($"Password for {opts.Username}: "); + } + + var ldap = new LdapOperations(opts.Domain, opts.LdapServer, opts.Username, opts.Password); Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); @@ -247,5 +262,48 @@ public static int Execute(Options opts) return 0; } + + // Reads a password from the console without echoing it back to the screen. + // Keeps the value in a SecureString while typing, then converts to a plain + // string only at the point LdapOperations needs it. + private static string ReadPasswordMasked(string prompt) + { + Console.Write(prompt); + + var secure = new SecureString(); + + ConsoleKeyInfo key; + while ((key = Console.ReadKey(intercept: true)).Key != ConsoleKey.Enter) + { + if (key.Key == ConsoleKey.Backspace) + { + if (secure.Length > 0) + { + secure.RemoveAt(secure.Length - 1); + Console.Write("\b \b"); + } + continue; + } + + if (key.KeyChar != '\0') + { + secure.AppendChar(key.KeyChar); + Console.Write('*'); + } + } + + Console.WriteLine(); + secure.MakeReadOnly(); + + var ptr = System.Runtime.InteropServices.Marshal.SecureStringToGlobalAllocUnicode(secure); + try + { + return System.Runtime.InteropServices.Marshal.PtrToStringUni(ptr); + } + finally + { + System.Runtime.InteropServices.Marshal.ZeroFreeGlobalAllocUnicode(ptr); + } + } } } From 1e59f033dbccd3799f20219ea8c9e4620826e20a Mon Sep 17 00:00:00 2001 From: Mahdi Hamedani Nezhad <150752864+thegreatmhn@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:33:25 -0700 Subject: [PATCH 4/6] Update EnumCas.cs add authentication to enumcas --- Certify/Commands/EnumCas.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Certify/Commands/EnumCas.cs b/Certify/Commands/EnumCas.cs index eab5116..e7c7fe1 100644 --- a/Certify/Commands/EnumCas.cs +++ b/Certify/Commands/EnumCas.cs @@ -1,4 +1,4 @@ -using Certify.Domain; +using Certify.Domain; using Certify.Lib; using CommandLine; using System; @@ -40,6 +40,13 @@ public class Options : DefaultOptions [Option("skip-web-checks", HelpText = "Skip web service checks")] public bool SkipWebServiceChecks { get; set; } + + [Option('u', "username", HelpText = "Username for LDAP bind (format: user@domain.fqdn). Omit to bind as the current user.")] + public string Username { get; set; } + + [Option('p', "password", HelpText = "Password for LDAP bind. If omitted while --username is set, you'll be prompted (input hidden).")] + public string Password { get; set; } + } public static int Execute(Options opts) @@ -58,7 +65,7 @@ public static int Execute(Options opts) return 1; } - var ldap = new LdapOperations(opts.Domain, opts.LdapServer); + var ldap = new LdapOperations(opts.Domain, opts.LdapServer, opts.Username, opts.Password); Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); From 7dd67d50d87e12072aa5dc248aab1376a0bfe0d6 Mon Sep 17 00:00:00 2001 From: Mahdi Hamedani Nezhad <150752864+thegreatmhn@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:59:58 +0330 Subject: [PATCH 5/6] Add Impersonation Helper Lib this library is for request command to help impersonate another user when requesting certificate, this is for non-joined usage --- Certify/Lib/ImpersonationHelper.cs | 98 ++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 Certify/Lib/ImpersonationHelper.cs diff --git a/Certify/Lib/ImpersonationHelper.cs b/Certify/Lib/ImpersonationHelper.cs new file mode 100644 index 0000000..02cbfe8 --- /dev/null +++ b/Certify/Lib/ImpersonationHelper.cs @@ -0,0 +1,98 @@ +using System; +using System.Runtime.InteropServices; +using System.Security.Principal; + +#if !DISARMED + +namespace Certify.Lib +{ + // Allows running a block of code impersonated as a different Windows user. + // Usage: + // using (ImpersonationHelper.Impersonate(username, domain, password)) + // { + // // code runs as the supplied user + // } + // // back to original identity here + internal class ImpersonationHelper : IDisposable + { + private readonly WindowsImpersonationContext _context; + private readonly SafeTokenHandle _token; + + private const int LOGON32_LOGON_NEW_CREDENTIALS = 9; // best for network access to remote resources + private const int LOGON32_PROVIDER_WINNT50 = 3; + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool LogonUser( + string lpszUsername, + string lpszDomain, + string lpszPassword, + int dwLogonType, + int dwLogonProvider, + out SafeTokenHandle phToken); + + private ImpersonationHelper(WindowsImpersonationContext context, SafeTokenHandle token) + { + _context = context; + _token = token; + } + + // Returns null if username is empty — callers can use it in a null-safe using block + public static ImpersonationHelper Impersonate(string username, string password) + { + if (string.IsNullOrEmpty(username)) + return null; + + // Split domain\user or user@domain into parts LogonUser expects + string domain = "."; + string user = username; + + if (username.Contains("\\")) + { + var parts = username.Split(new[] { '\\' }, 2); + domain = parts[0]; + user = parts[1]; + } + else if (username.Contains("@")) + { + // UPN format — pass as-is with empty domain + domain = string.Empty; + user = username; + } + + if (!LogonUser(user, domain, password, LOGON32_LOGON_NEW_CREDENTIALS, LOGON32_PROVIDER_WINNT50, out var token)) + throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), + $"LogonUser failed for '{username}'. Check credentials."); + + var identity = new WindowsIdentity(token.DangerousGetHandle()); + var context = identity.Impersonate(); + + return new ImpersonationHelper(context, token); + } + + public void Dispose() + { + _context?.Undo(); + _context?.Dispose(); + _token?.Dispose(); + } + } + + // Safe wrapper around a Win32 token handle + internal class SafeTokenHandle : SafeHandle + { + private SafeTokenHandle() : base(IntPtr.Zero, true) { } + + public override bool IsInvalid => handle == IntPtr.Zero; + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr hObject); + + protected override bool ReleaseHandle() + { + return CloseHandle(handle); + } + } +} + +#endif \ No newline at end of file From 922303ff7198ed184083afa02b45a1277dedbf69 Mon Sep 17 00:00:00 2001 From: Mahdi Hamedani Nezhad <150752864+thegreatmhn@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:30:57 -0700 Subject: [PATCH 6/6] Update CertRequest.cs add authentication to request command --- Certify/Commands/CertRequest.cs | 71 +++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/Certify/Commands/CertRequest.cs b/Certify/Commands/CertRequest.cs index 5a996a5..98db420 100644 --- a/Certify/Commands/CertRequest.cs +++ b/Certify/Commands/CertRequest.cs @@ -1,8 +1,9 @@ -using CERTENROLLLib; +using CERTENROLLLib; using Certify.Lib; using CommandLine; using System; using System.Collections.Generic; +using System.Security; using System.Security.Principal; using System.Text; using System.Text.RegularExpressions; @@ -60,6 +61,12 @@ public class Options : DefaultOptions [Option("install", HelpText = "Install certificate in the current store")] public bool Install { get; set; } + + [Option('u', "username", HelpText = "Username for authentication (format: user@domain.fqdn). Omit to use the current user.")] + public string Username { get; set; } + + [Option('p', "password", HelpText = "Password for authentication. If omitted while --username is set, you'll be prompted (input hidden).")] + public string Password { get; set; } } public static int Execute(Options opts) @@ -87,6 +94,12 @@ public static int Execute(Options opts) return 1; } + // Prompt for password if a username was given but no password was provided + if (!string.IsNullOrEmpty(opts.Username) && string.IsNullOrEmpty(opts.Password)) + { + opts.Password = ReadPasswordMasked($"Password for {opts.Username}: "); + } + var sans = new List>(); void AddSubjectAltNames(IEnumerable names, SubjectAltNameType type) @@ -118,6 +131,9 @@ private static void RequestCert(Options opts, IEnumerable 0) + { + secure.RemoveAt(secure.Length - 1); + Console.Write("\b \b"); + } + continue; + } + + if (key.KeyChar != '\0') + { + secure.AppendChar(key.KeyChar); + Console.Write('*'); + } + } + + Console.WriteLine(); + secure.MakeReadOnly(); + + var ptr = System.Runtime.InteropServices.Marshal.SecureStringToGlobalAllocUnicode(secure); + try + { + return System.Runtime.InteropServices.Marshal.PtrToStringUni(ptr); + } + finally + { + System.Runtime.InteropServices.Marshal.ZeroFreeGlobalAllocUnicode(ptr); + } + } } }