Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Certify/Certify.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
<Compile Include="Domain\CertificateAuthorityEnterprise.cs" />
<Compile Include="Domain\PKIObject.cs" />
<Compile Include="Lib\CertEnrollment.cs" />
<Compile Include="Lib\WebEnrollment.cs" />
<Compile Include="Lib\CertAdmin.cs" />
<Compile Include="Lib\CertSidExtension.cs" />
<Compile Include="Lib\ImpersonationHelper.cs" />
Expand Down Expand Up @@ -158,4 +159,4 @@
</PropertyGroup>
<Error Condition="!Exists('..\packages\dnMerge.0.5.15\build\dnMerge.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\dnMerge.0.5.15\build\dnMerge.targets'))" />
</Target>
</Project>
</Project>
91 changes: 88 additions & 3 deletions Certify/Commands/CertRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class Options : DefaultOptions
{
[Option("ca", Required = true, HelpText = "Target certificate authority (format: SERVER\\CA-NAME)")]
public string CertificateAuthority { get; set; }

[Option("username", HelpText = "Username for operation")]
public string Username { get; set; }

Expand Down Expand Up @@ -67,6 +67,15 @@ public class Options : DefaultOptions

[Option("install", HelpText = "Install certificate in the current store")]
public bool Install { get; set; }

[Option("web", HelpText = "Use HTTP web enrollment instead of RPC/DCOM")]
public bool WebEnroll { get; set; }

[Option("web-host", HelpText = "Web enrollment host if different from the CA server (e.g., ca-web.corp.local)")]
public string WebHost { get; set; }

[Option("https", HelpText = "Use HTTPS for web enrollment")]
public bool UseHttps { get; set; }
}

public static int Execute(Options opts)
Expand All @@ -75,8 +84,12 @@ public static int Execute(Options opts)

if (!string.IsNullOrEmpty(opts.CertificateAuthority) && !opts.CertificateAuthority.Contains("\\"))
{
Console.WriteLine("[X] The 'certificate authority' parameter is not of the format 'SERVER\\CA-NAME'.");
return 1;
// Allow --ca without backslash when using --web (just the hostname is sufficient)
if (!opts.WebEnroll)
{
Console.WriteLine("[X] The 'certificate authority' parameter is not of the format 'SERVER\\CA-NAME'.");
return 1;
}
}

foreach (var x in opts.ApplicationPolicies)
Expand Down Expand Up @@ -189,8 +202,80 @@ private static void RequestCert(Options opts, IEnumerable<Tuple<SubjectAltNameTy
Console.WriteLine("[+] Private Key :");
Console.WriteLine(csr.Item2);
}
else if (opts.WebEnroll)
{
// Web enrollment path — submit via HTTP instead of RPC/DCOM
var caHost = !string.IsNullOrEmpty(opts.WebHost)
? opts.WebHost
: opts.CertificateAuthority.Contains("\\")
? opts.CertificateAuthority.Split('\\')[0]
: opts.CertificateAuthority;

var scheme = opts.UseHttps ? "https" : "http";
Console.WriteLine($"[*] Enrollment method : Web enrollment ({scheme}://{caHost}/certsrv/)");
Console.WriteLine();

try
{
var webResult = WebEnrollment.SubmitRequest(caHost, csr.Item1, opts.TemplateName, opts.UseHttps);

Console.WriteLine($"[*] CA Response : {webResult.StatusMessage}");

if (webResult.RequestId > 0)
Console.WriteLine($"[*] Request ID : {webResult.RequestId}");

Console.WriteLine();

if (webResult.Success && !string.IsNullOrEmpty(webResult.Certificate))
{
if (opts.OutputPem)
{
Console.WriteLine("[*] Certificate (PEM) :");
Console.WriteLine();
Console.Write(csr.Item2);
Console.Write(webResult.Certificate);
}
else
{
Console.WriteLine("[*] Certificate (PFX) :");
Console.WriteLine();
Console.WriteLine(Convert.ToBase64String(CertTransformUtil.MakePfx(webResult.Certificate, csr.Item2)));
}
}
else
{
Console.WriteLine("[*] Private Key (PEM) :");
Console.WriteLine();

if (opts.OutputPem)
Console.Write(csr.Item2);
else
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(csr.Item2)));

if (webResult.RequestId > 0)
{
Console.WriteLine();
Console.WriteLine($"[*] Retrieve the certificate once approved:");
Console.WriteLine($" Certify.exe request-download --ca {opts.CertificateAuthority} --id {webResult.RequestId} --web --web-host {caHost}{(opts.UseHttps ? " --https" : "")}");
}
}
}
catch (Exception e)
{
Console.WriteLine($"[X] Error requesting the certificate via web enrollment: {e.Message}");
Console.WriteLine();
Console.WriteLine("[*] Private Key (PEM) :");
Console.WriteLine();

if (opts.OutputPem)
Console.Write(csr.Item2);
else
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(csr.Item2)));
}
}
else
{
// Standard RPC/DCOM enrollment path
try
{
int request_id = CertEnrollment.SendCertificateRequest(opts.CertificateAuthority, csr.Item1);
Expand Down
62 changes: 51 additions & 11 deletions Certify/Commands/CertRequestDownload.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CERTENROLLLib;
using CERTENROLLLib;
using Certify.Lib;
using CommandLine;
using System;
Expand Down Expand Up @@ -37,6 +37,15 @@ public class Options : DefaultOptions

[Option("install-user", SetName = "InstallUser", HelpText = "Install certificate in the user store")]
public bool InstallUser { get; set; }

[Option("web", HelpText = "Download via HTTP web enrollment instead of RPC/DCOM")]
public bool WebEnroll { get; set; }

[Option("web-host", HelpText = "Web enrollment host if different from the CA server")]
public string WebHost { get; set; }

[Option("https", HelpText = "Use HTTPS for web enrollment download")]
public bool UseHttps { get; set; }
}

public static int Execute(Options opts)
Expand All @@ -45,8 +54,11 @@ public static int Execute(Options opts)

if (!string.IsNullOrEmpty(opts.CertificateAuthority) && !opts.CertificateAuthority.Contains("\\"))
{
Console.WriteLine("[X] The 'certificate authority' parameter is not of the format 'SERVER\\CA-NAME'.");
return 1;
if (!opts.WebEnroll)
{
Console.WriteLine("[X] The 'certificate authority' parameter is not of the format 'SERVER\\CA-NAME'.");
return 1;
}
}

var private_key = string.Empty;
Expand Down Expand Up @@ -80,16 +92,44 @@ private static void DownloadCert(Options opts, string private_key)
Console.WriteLine();
Console.WriteLine($"[*] Certificate Authority : {opts.CertificateAuthority}");
Console.WriteLine($"[*] Request ID : {opts.RequestId}");
Console.WriteLine();

var certificate_pem = string.Empty;

if (!opts.InstallMachine && !opts.InstallUser)
certificate_pem = CertEnrollment.DownloadCert(opts.CertificateAuthority, opts.RequestId);
else if (opts.InstallMachine)
certificate_pem = CertEnrollment.DownloadAndInstallCert(opts.CertificateAuthority, opts.RequestId, X509CertificateEnrollmentContext.ContextMachine);
else if (opts.InstallUser)
certificate_pem = CertEnrollment.DownloadAndInstallCert(opts.CertificateAuthority, opts.RequestId, X509CertificateEnrollmentContext.ContextUser);
if (opts.WebEnroll)
{
// Web enrollment download path
var caHost = !string.IsNullOrEmpty(opts.WebHost)
? opts.WebHost
: opts.CertificateAuthority.Contains("\\")
? opts.CertificateAuthority.Split('\\')[0]
: opts.CertificateAuthority;

var scheme = opts.UseHttps ? "https" : "http";
Console.WriteLine($"[*] Download method : Web enrollment ({scheme}://{caHost}/certsrv/)");
Console.WriteLine();

try
{
certificate_pem = WebEnrollment.DownloadCert(caHost, opts.RequestId, opts.UseHttps);
}
catch (Exception e)
{
Console.WriteLine($"[X] Failed to download certificate via web enrollment: {e.Message}");
return;
}
}
else
{
// Standard RPC/DCOM download path
Console.WriteLine();

if (!opts.InstallMachine && !opts.InstallUser)
certificate_pem = CertEnrollment.DownloadCert(opts.CertificateAuthority, opts.RequestId);
else if (opts.InstallMachine)
certificate_pem = CertEnrollment.DownloadAndInstallCert(opts.CertificateAuthority, opts.RequestId, X509CertificateEnrollmentContext.ContextMachine);
else if (opts.InstallUser)
certificate_pem = CertEnrollment.DownloadAndInstallCert(opts.CertificateAuthority, opts.RequestId, X509CertificateEnrollmentContext.ContextUser);
}

if (!string.IsNullOrEmpty(private_key))
{
Expand Down Expand Up @@ -120,4 +160,4 @@ private static void DownloadCert(Options opts, string private_key)
}
}

#endif
#endif
133 changes: 133 additions & 0 deletions Certify/Lib/WebEnrollment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;

#if !DISARMED

namespace Certify.Lib
{
public class WebEnrollResult
{
public bool Success { get; set; }
public string Certificate { get; set; }
public int RequestId { get; set; }
public string StatusMessage { get; set; }
}

class WebEnrollment
{
public static WebEnrollResult SubmitRequest(string caHost, string csrBase64, string templateName, bool useHttps = false)
{
var result = new WebEnrollResult();
var scheme = useHttps ? "https" : "http";
var submitUrl = $"{scheme}://{caHost}/certsrv/certfnsh.asp";

try
{
var certAttrib = "CertificateTemplate:" + templateName;
var postData = "Mode=newreq&CertRequest="
+ Uri.EscapeDataString(csrBase64)
+ "&CertAttrib=" + Uri.EscapeDataString(certAttrib)
+ "&TargetStoreFlags=0&SaveCert=yes&ThumbPrint=";

var req = CreateRequest(submitUrl, useHttps);
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";

var bodyBytes = Encoding.UTF8.GetBytes(postData);
req.ContentLength = bodyBytes.Length;
using (var s = req.GetRequestStream())
s.Write(bodyBytes, 0, bodyBytes.Length);

string responseHtml;
using (var resp = (HttpWebResponse)req.GetResponse())
using (var sr = new StreamReader(resp.GetResponseStream()))
responseHtml = sr.ReadToEnd();

var m = Regex.Match(responseHtml, @"certnew\.cer\?ReqID=(\d+)");
if (!m.Success)
{
result.Success = false;

if (responseHtml.Contains("Access is denied"))
{
result.StatusMessage = "Access denied by the CA.";
}
else if (responseHtml.Contains("Pending"))
{
result.StatusMessage = "The certificate is still pending.";
var pending = Regex.Match(responseHtml, @"Your Request Id is (\d+)");
if (pending.Success)
result.RequestId = int.Parse(pending.Groups[1].Value);
}
else
{
var em = Regex.Match(responseHtml, @"Disposition\s*message[^>]*>\s*([^<]+)");
result.StatusMessage = em.Success
? em.Groups[1].Value.Trim()
: "The submission failed with an unknown error.";
}

return result;
}

result.RequestId = int.Parse(m.Groups[1].Value);
result.Success = true;
result.StatusMessage = "The certificate has been issued.";
result.Certificate = DownloadCert(caHost, result.RequestId, useHttps);
}
catch (WebException ex)
{
result.Success = false;
result.StatusMessage = $"HTTP error: {ex.Message}";

if (ex.Response != null)
{
try
{
using (var sr = new StreamReader(ex.Response.GetResponseStream()))
{
var body = sr.ReadToEnd();
if (body.Length > 500) body = body.Substring(0, 500);
result.StatusMessage += "\n" + body;
}
}
catch { }
}
}

return result;
}

public static string DownloadCert(string caHost, int requestId, bool useHttps = false)
{
var scheme = useHttps ? "https" : "http";
var certUrl = $"{scheme}://{caHost}/certsrv/certnew.cer?ReqID={requestId}&Enc=b64";

var req = CreateRequest(certUrl, useHttps);
req.Method = "GET";

using (var resp = (HttpWebResponse)req.GetResponse())
using (var sr = new StreamReader(resp.GetResponseStream()))
return sr.ReadToEnd();
}

private static HttpWebRequest CreateRequest(string url, bool useHttps)
{
var req = (HttpWebRequest)WebRequest.Create(url);
req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
req.Timeout = 30000;
req.ReadWriteTimeout = 30000;
req.UseDefaultCredentials = true;

if (useHttps)
ServicePointManager.ServerCertificateValidationCallback = (s, c, ch, e) => true;

return req;
}
}
}

#endif