diff --git a/Certify/Certify.csproj b/Certify/Certify.csproj
index 67d5074..d7a9b7e 100644
--- a/Certify/Certify.csproj
+++ b/Certify/Certify.csproj
@@ -120,6 +120,7 @@
+
@@ -158,4 +159,4 @@
-
\ No newline at end of file
+
diff --git a/Certify/Commands/CertRequest.cs b/Certify/Commands/CertRequest.cs
index 61c3045..9746f54 100644
--- a/Certify/Commands/CertRequest.cs
+++ b/Certify/Commands/CertRequest.cs
@@ -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; }
@@ -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)
@@ -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)
@@ -189,8 +202,80 @@ private static void RequestCert(Options opts, IEnumerable 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);
diff --git a/Certify/Commands/CertRequestDownload.cs b/Certify/Commands/CertRequestDownload.cs
index a132e3f..2c4f4eb 100644
--- a/Certify/Commands/CertRequestDownload.cs
+++ b/Certify/Commands/CertRequestDownload.cs
@@ -1,4 +1,4 @@
-using CERTENROLLLib;
+using CERTENROLLLib;
using Certify.Lib;
using CommandLine;
using System;
@@ -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)
@@ -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;
@@ -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))
{
@@ -120,4 +160,4 @@ private static void DownloadCert(Options opts, string private_key)
}
}
-#endif
\ No newline at end of file
+#endif
diff --git a/Certify/Lib/WebEnrollment.cs b/Certify/Lib/WebEnrollment.cs
new file mode 100644
index 0000000..993c481
--- /dev/null
+++ b/Certify/Lib/WebEnrollment.cs
@@ -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