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