Skip to content
71 changes: 64 additions & 7 deletions Certify/Commands/CertRequest.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<Tuple<SubjectAltNameType, string>>();

void AddSubjectAltNames(IEnumerable<string> names, SubjectAltNameType type)
Expand Down Expand Up @@ -118,6 +131,9 @@ private static void RequestCert(Options opts, IEnumerable<Tuple<SubjectAltNameTy
Console.WriteLine();
Console.WriteLine($"[*] Current user context : {WindowsIdentity.GetCurrent().Name}");

if (!string.IsNullOrEmpty(opts.Username))
Console.WriteLine($"[*] Authenticating as : {opts.Username}");

var subject_name = opts.SubjectName;

if (string.IsNullOrEmpty(subject_name))
Expand All @@ -136,7 +152,7 @@ private static void RequestCert(Options opts, IEnumerable<Tuple<SubjectAltNameTy
Console.WriteLine($"[*] No subject name specified, using current context as subject.");
}
}

if (string.IsNullOrEmpty(subject_name))
{
subject_name = "CN=User";
Expand All @@ -156,7 +172,7 @@ private static void RequestCert(Options opts, IEnumerable<Tuple<SubjectAltNameTy
if (opts.ApplicationPolicies != null && opts.ApplicationPolicies.Any())
Console.WriteLine($"[*] Application Policies : {string.Join(", ", opts.ApplicationPolicies)}");

var csr = CertEnrollment.CreateCertRequestMessage(opts.TemplateName, subject_name, sans,
var csr = CertEnrollment.CreateCertRequestMessage(opts.TemplateName, subject_name, sans,
opts.SidExtension, opts.ApplicationPolicies, opts.KeySize, opts.MachineContext);

Console.WriteLine();
Expand All @@ -173,10 +189,10 @@ private static void RequestCert(Options opts, IEnumerable<Tuple<SubjectAltNameTy
Console.WriteLine(csr.Item2);
}
else
{
{
try
{
int request_id = CertEnrollment.SendCertificateRequest(opts.CertificateAuthority, csr.Item1);
int request_id = CertEnrollment.SendCertificateRequest(opts.CertificateAuthority, csr.Item1, opts.Username, opts.Password);

Console.WriteLine($"[*] Request ID : {request_id}");
Console.WriteLine();
Expand All @@ -186,9 +202,9 @@ private static void RequestCert(Options opts, IEnumerable<Tuple<SubjectAltNameTy
var certificate_pem = string.Empty;

if (!opts.Install)
certificate_pem = CertEnrollment.DownloadCert(opts.CertificateAuthority, request_id);
certificate_pem = CertEnrollment.DownloadCert(opts.CertificateAuthority, request_id, opts.Username, opts.Password);
else
certificate_pem = CertEnrollment.DownloadAndInstallCert(opts.CertificateAuthority, request_id, X509CertificateEnrollmentContext.ContextUser);
certificate_pem = CertEnrollment.DownloadAndInstallCert(opts.CertificateAuthority, request_id, X509CertificateEnrollmentContext.ContextUser, opts.Username, opts.Password);

if (opts.OutputPem)
{
Expand Down Expand Up @@ -229,6 +245,47 @@ private static string GetCurrentComputerDN()
{
return $"CN={System.Net.Dns.GetHostEntry("").HostName}";
}

// Reads a password from the console without echoing it to the screen.
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);
}
}
}
}

Expand Down
11 changes: 9 additions & 2 deletions Certify/Commands/EnumCas.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Certify.Domain;
using Certify.Domain;
using Certify.Lib;
using CommandLine;
using System;
Expand Down Expand Up @@ -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)
Expand All @@ -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}'");

Expand Down
10 changes: 8 additions & 2 deletions Certify/Commands/EnumPkiObjects.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Certify.Domain;
using Certify.Domain;
using Certify.Lib;
using CommandLine;
using System;
Expand All @@ -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)
Expand All @@ -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}'");

Expand Down
62 changes: 60 additions & 2 deletions Certify/Commands/EnumTemplates.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Certify.Domain;
using Certify.Domain;
using Certify.Lib;
using CommandLine;
using System;
using System.Collections.Generic;
using System.Data;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Security;
using System.Security.Principal;

namespace Certify.Commands
Expand Down Expand Up @@ -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)
Expand All @@ -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}'");

Expand Down Expand Up @@ -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);
}
}
}
}
98 changes: 98 additions & 0 deletions Certify/Lib/ImpersonationHelper.cs
Original file line number Diff line number Diff line change
@@ -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
Loading