/// <summary> /// Register the local computer's account on the ACME service /// </summary> /// <returns>true if registration is successful, false otherwise</returns> public async Task <bool> RegisterNewAccount() { WinCertesOptions _options = Program._winCertesOptions; try { InitCertes(); Certes.Acme.Resource.Directory directory = await _acme.GetDirectory(); InitCertes(); IAccountContext accountCtx = await _acme.NewAccount(_options.AccountEmail, true); _options.Registered = true; logger.Info($"Successfully registered account {_options.AccountEmail} with certificate authority {_options.ServiceUri.ToString()}"); if ((directory.Meta != null) && (directory.Meta.TermsOfService != null)) { logger.Info($"Please check the ACME Service ToS at: {directory.Meta.TermsOfService.ToString()}"); } return(true); } catch (Exception exp) { logger.Error($"Failed to register account {_options.AccountEmail} with certificate authority {_options.ServiceUri.ToString()}: {ProcessCertesException(exp)}"); return(false); } }
/// <summary> /// CertesWrapper class constructor /// </summary> /// <param name="serviceUri">The ACME service URI (endin in /directory). If null, defaults to Let's encrypt</param> /// <param name="accountEmail">The email address to be registered within the ACME account. If null, no email will be used</param> public CertesWrapper() { // Just use the settings already prepared nicely _options = Program._winCertesOptions; // Todo - Encrypt this in registry, don't display logger.Debug($"PFX password will be: {_options.PfxPassword}"); logger.Debug($"Uri: {_options.ServiceUri.ToString()}"); // Basic check of private key string key = _options.AccountKey; if (key == null || key.Length < 1500) { // Create new private key _options.AccountKey = KeyFactory.NewKey(KeyAlgorithm.RS256).ToPem(); } // Instantiating HTTP Client AssemblyName certesAssembly = typeof(AcmeContext).Assembly.GetName(); AssemblyName winCertesAssembly = typeof(Program).Assembly.GetName(); _httpClient = new HttpClient(); _httpClient.DefaultRequestHeaders.Add("User-Agent", $"WinCertes/{winCertesAssembly.Version.ToString()} (Certes/{certesAssembly.Version.ToString()}; {Environment.OSVersion.VersionString})"); }
/// <summary> /// Initializes the CertesWrapper, and registers the account if necessary /// </summary> /// <param name="serviceUri">the ACME service URI</param> /// <param name="email">the email account used to register</param> private static void InitCertesWrapper(WinCertesOptions _winCertesOptions) { // We get the CertesWrapper object, that will do most of the job. _certesWrapper = new CertesWrapper(); // If local computer's account isn't registered on the ACME service, we'll do it. if (!_winCertesOptions.Registered) { var regRes = Task.Run(() => _certesWrapper.RegisterNewAccount()).GetAwaiter().GetResult(); if (!regRes) { throw new Exception("Could not register ACME service account"); } } }
static int Main(string[] args) { // Main parameters with their default values string taskName = null; _winCertesOptions = new WinCertesOptions(); if (!Utils.IsAdministrator()) { Console.WriteLine("WinCertes.exe must be launched as Administrator"); return(ERROR); } // Command line options handling and initialization stuff if (!HandleOptions(args)) { return(ERROR_INCORRECT_PARAMETER); } if (_periodic) { taskName = Utils.DomainsToFriendlyName(_domains); } InitWinCertesDirectoryPath(); Utils.ConfigureLogger(_winCertesPath); _config = new RegistryConfig(_extra); _winCertesOptions.WriteOptionsIntoConfiguration(_config); if (_show) { _winCertesOptions.displayOptions(_config); return(0); } // Reset is a full reset ! if (_reset) { IConfig baseConfig = new RegistryConfig(false); baseConfig.DeleteAllParameters(); Utils.DeleteScheduledTasks(); return(0); } // Initialization and renewal/revocation handling try { InitCertesWrapper(_winCertesOptions.ServiceUri, _winCertesOptions.Email); } catch (Exception e) { _logger.Error(e.Message); return(ERROR); } if (_winCertesOptions.Revoke > -1) { RevokeCert(_domains, _winCertesOptions.Revoke); return(0); } // default mode: enrollment/renewal. check if there's something to be done // note that in any case, we want to be able to set the scheduled task (won't do anything if taskName is null) if (!IsThereCertificateAndIsItToBeRenewed(_domains)) { Utils.CreateScheduledTask(taskName, _domains, _extra); return(0); } // Now the real stuff: we register the order for the domains, and have them validated by the ACME service IHTTPChallengeValidator httpChallengeValidator = HTTPChallengeValidatorFactory.GetHTTPChallengeValidator(_winCertesOptions.Standalone, _winCertesOptions.HttpPort, _winCertesOptions.WebRoot); IDNSChallengeValidator dnsChallengeValidator = DNSChallengeValidatorFactory.GetDNSChallengeValidator(_config); if ((httpChallengeValidator == null) && (dnsChallengeValidator == null)) { WriteErrorMessageWithUsage(_options, "Specify either an HTTP or a DNS validation method."); return(ERROR_INCORRECT_PARAMETER); } if (!(Task.Run(() => _certesWrapper.RegisterNewOrderAndVerify(_domains, httpChallengeValidator, dnsChallengeValidator)).GetAwaiter().GetResult())) { if (httpChallengeValidator != null) { httpChallengeValidator.EndAllChallengeValidations(); } return(ERROR); } if (httpChallengeValidator != null) { httpChallengeValidator.EndAllChallengeValidations(); } // We get the certificate from the ACME service var pfxName = Task.Run(() => _certesWrapper.RetrieveCertificate(_domains, _winCertesPath, Utils.DomainsToFriendlyName(_domains))).GetAwaiter().GetResult(); if (pfxName == null) { return(ERROR); } AuthenticatedPFX pfx = new AuthenticatedPFX(_winCertesPath + "\\" + pfxName, _certesWrapper.PfxPassword); CertificateStorageManager certificateStorageManager = new CertificateStorageManager(pfx, ((_winCertesOptions.Csp == null) && (!_winCertesOptions.noCsp))); // Let's process the PFX into Windows Certificate objet. certificateStorageManager.ProcessPFX(); // and we write its information to the WinCertes configuration RegisterCertificateIntoConfiguration(certificateStorageManager.Certificate, _domains); // Import the certificate into the Windows store if (!_winCertesOptions.noCsp) { certificateStorageManager.ImportCertificateIntoCSP(_winCertesOptions.Csp); } // Bind certificate to IIS Site (won't do anything if option is null) Utils.BindCertificateForIISSite(certificateStorageManager.Certificate, _winCertesOptions.BindName); // Execute PowerShell Script (won't do anything if option is null) Utils.ExecutePowerShell(_winCertesOptions.ScriptFile, pfx); // Create the AT task that will execute WinCertes periodically (won't do anything if taskName is null) Utils.CreateScheduledTask(taskName, _domains, _extra); // Let's delete the PFX file RemoveFileAndLog(pfx.PfxFullPath); return(0); }
/// <summary> /// Handles command line options. Command line options overwrite all settings saved in the Registry. /// </summary> /// <remarks> /// WinCertes settings are saved to the Registry. This information is used on subsequent runs to determine /// if registration has been completed and the private key assigned. /// /// This implementation will place all configuration data for a Certificate into its own Registry SubKey. /// For backward compatibility and common defaults, the Registry root "WinCertes" is also used. /// /// The unique certificate settings will be identified via the certificate name. i..e -n {name} or --certname {name} /// /// The Certificate details will normally be stored in a SubKey, but for legacy support, an existing Certificate will /// be maintained under the root key. /// </remarks> /// <param name="args">the command line options</param> /// <returns>True if successful</returns> private static int HandleOptions(string[] args) { // Create the Default Base Registry Key for this certificate's configuration if (_winCertesOptions == null) { _winCertesOptions = new WinCertesOptions(null); } if (!_winCertesOptions.Registry.Initialised) { return(ERROR_REGISTRY_FAILED); } string newName = null; List <string> _domains = new List <string>(); bool areEquivalent; bool newDomainList; // Define the options that may be used by this application _options = new OptionSet() { { "n|certname=", "Unique Certificate name excluding file extension e.g. \"wincertes.com\" (default=first domain name)", v => newName = v }, { "s|service=", "ACME Service URI to be used (optional, defaults to Let's Encrypt)", v => _winCertesOptions.ServiceUri = new Uri(v) }, { "e|email=", "Account email to be used for ACME requests (optional, defaults to no email)", v => _winCertesOptions.AccountEmail = v }, { "d|domain=", "Domain(s) to enroll (mandatory)", v => _domains.Add(v) }, { "w|webserver:", "Toggles the local web server use and sets its {ROOT} directory (default c:\\inetpub\\wwwroot). Activates HTTP validation mode.", v => _winCertesOptions.WebRoot = v ?? "c:\\inetpub\\wwwroot" }, { "p|periodic", "Should WinCertes create the Windows Scheduler task to handle certificate renewal (default=no)", v => _periodic = (v != null) }, { "b|bindname=", "IIS site name to bind the certificate to, e.g. \"Default Web Site\". Defaults to no binding.", v => _winCertesOptions.BindName = v }, { "f|scriptfile=", "PowerShell Script file e.g. \"C:\\Temp\\script.ps1\" to execute upon successful enrollment (default=none)", v => _winCertesOptions.ScriptFile = v }, { "a|standalone", "Activate WinCertes internal WebServer for validation. Activates HTTP validation mode. WARNING: it will use port 80 unless -l is specified.", v => _winCertesOptions.Standalone = (v != null) }, { "r|revoke:", "Should WinCertes revoke the certificate identified by its domains (to be used only with -d or -n). {REASON} is an optional integer between 0 and 5.", (int v) => _winCertesOptions.Revoke = v }, { "k|csp=", "Import the certificate into specified csp. By default WinCertes imports in the default CSP.", v => _winCertesOptions.Csp = v }, { "t|renewal=", "Trigger certificate renewal {N} days before expiration, default 30", (int v) => _winCertesOptions.RenewalDelay = v }, { "l|listenport=", "Listen on port {N} in standalone mode (for use with -a switch, default 80)", (int v) => _winCertesOptions.HttpPort = v }, { "show", "Show current configuration parameters and exit", v => _show = (v != null) }, { "reset", "Reset all configuration parameters for --certname and exit", v => _reset = (v != null) }, { "extra:", "manages additional certificate(s) instead of the default one, with its own settings. Add an integer index optionally to manage more certs.", (int v) => _extra = v }, { "no-csp", "Disable import of the certificate into CSP. Use with caution, at your own risk. REVOCATION WILL NOT WORK IN THAT MODE.", v => _winCertesOptions.noCsp = (v != null) }, { "x|exportcerts", "Should WinCertes export the certificates including PEM format.", v => _winCertesOptions.ExportPem = (v != null) }, { "dnscreatekeys", "Create all DNS values in the registry and exit. Use with --certname. Manually edit registry or include on command line", v => _creatednskeys = (v != null) }, { "dnstype=", "DNS Validator type: acme-dns, win-dns", v => _winCertesOptions.DNSValidatorType = v }, { "dnsurl=", "DNS Server URL: http://blah.net", v => _winCertesOptions.DNSServerURL = v }, { "dnshost=", "DNS Server Host", v => _winCertesOptions.DNSServerHost = v }, { "dnsuser="******"DNS Server Username", v => _winCertesOptions.DNSServerUser = v }, { "dnspassword="******"DNS Server Password", v => _winCertesOptions.DNSServerPassword = v }, { "dnskey=", "DNS Server Account Key", v => _winCertesOptions.DNSServerKey = v }, { "dnssubdomain=", "DNS Server SubDomain", v => _winCertesOptions.DNSServerSubDomain = v }, { "dnszone=", "DNS Server Zone", v => _winCertesOptions.DNSServerZone = v }, { "debug", "Enable extra debug logging", v => _debug = (v != null) }, { "password="******"Certificate password min 16 characters (default=random)", v => _winCertesOptions.PfxPassword = v } }; // Merge options with default/existing configuration List <string> res; try { res = _options.Parse(args); } catch (Exception e) { WriteErrorMessageWithUsage(_options, e.Message); return(ERROR); } // TODO increasing log level executes on the logger, but does not appear to take affect // so include --debug check in Utils.ConfigureLogger //_logger.Debug("Before log level chage: You should not see me"); //if (_debug) Utils.SetLogLevel(LogLevel.Debug); //_logger.Debug("After enabling debug: You should see this message"); if (_winCertesOptions.IsNew && newName != null) { // We can skip backward compatibility checks and jump direct to the named Certificate store in registry _winCertesOptions.IsDefaultCertificate = false; _winCertesOptions.CertificateName = newName; } else { // Backward compatibility: Process default registry with command line parameters and adjust accordingly // Ideally we'd like to retire the original setup (default certificate & one extra) // by having all certificates in their own registry key. if (_extra > -1) { // Non default certificate if (newName != null) { // Don't support both new and legacy certificate naming conventions on the command line WriteErrorMessageWithUsage(_options, "Command line parameter --extra is deprecated, cannot use both --extra and --certname concurrently."); return(ERROR_BAD_CERTIFICATE_NAME); } else { // Legacy support for "extra" string extraIndex = ""; if (_extra > 1) { extraIndex = _extra.ToString(); } newName = "extra" + extraIndex; _winCertesOptions.CertificateName = newName; // Force registry reload - create new Certificate store _winCertesOptions.IsDefaultCertificate = false; } } else { // There should always be a certificate name, even for the default RegistryKey if (_winCertesOptions.CertificateName == null && newName == null) { if (_domains.Count != 0) { _winCertesOptions.CertificateName = _domains[0]; } else if (_winCertesOptions.Domains.Count != 0) { _winCertesOptions.CertificateName = _winCertesOptions.Domains[0]; } } // Check if correct registry key was merged. Certifiate uniqueness is based on domain name list // If nothing exists, this should trigger all certificates to have a unique Certificate store in registry areEquivalent = (_winCertesOptions.Domains.Count == _domains.Count) && !_winCertesOptions.Domains.Except(_domains).Any(); newDomainList = (_winCertesOptions.Domains.Count == 0 && _domains.Count > 0); if (areEquivalent) { // New or default certificate if (_winCertesOptions.Domains.Count == 0) { if (newName != null) { // Named certificate requested, trigger reload for the new certificate store. // i.e. Only the certname was provided on the command line and nothing in defaults (as expected for new setup) _winCertesOptions.IsDefaultCertificate = false; } else { WriteErrorMessageWithUsage(_options, "Insufficient parameters. Please provide Certificate name, domain name(s), or manually configure the registry key"); return(ERROR_NO_DOMAINS); } } // New, or same certificate domain(s) , apply new name from command line (even if null) _winCertesOptions.CertificateName = newName; } else { // Registry and command line Domain list is different. if (!newDomainList) { WriteErrorMessageWithUsage(_options, "Command line parameters do not match WinCertes registry key. Delete key values or correct command line parameters."); return(ERROR_DOMAIN_CONFLICT); } // Different domain name list: New Registry Certificate store is required, but command line name matches default certificate if (newName == _winCertesOptions.CertificateName) { WriteErrorMessageWithUsage(_options, "Certname is used by default Certificate but the domain list has changed. Use a new name or delete existing Domains list from the WinCertes registry key"); return(ERROR_BAD_CERTIFICATE_NAME); } // Trigger use of new Certificate store and reload _winCertesOptions.IsDefaultCertificate = false; } } } // // If there has been a change reload and once the certificate has a name, always use the subkey. // if (!_winCertesOptions.IsDefaultCertificate) { // This is not the default certificate, reload from new Certificate store, create if needed _winCertesOptions = new WinCertesOptions(_winCertesOptions.CertificateName); // Overwrite registry subkey from command line options again _domains = new List <string>(); res = _options.Parse(args); // No need to do any more checks if resetting if (_reset) { return(SUCCESS); } // Helper to create the DNS keys if (_creatednskeys) { return(SUCCESS); } // Domain check areEquivalent = (_winCertesOptions.Domains.Count == _domains.Count) && !_winCertesOptions.Domains.Except(_domains).Any(); newDomainList = (_winCertesOptions.Domains.Count == 0 && _domains.Count > 0); // Is the new Certificate settings compatible or new? if (areEquivalent) { // No info if (_domains.Count == 0) { WriteErrorMessageWithUsage(_options, "At least one domain must be specified on the command line for a new certificate"); return(ERROR_NO_DOMAINS); } // Existing and matching domain list - OK } else { if (newDomainList) { // New Certificate _winCertesOptions.Domains = _domains.ConvertAll(d => d.ToLower()); } else { // Domain list is different. So need a new name or delete the registry certificate key WriteErrorMessageWithUsage(_options, "The domain list has changed for this certificate key \"" + _winCertesOptions.CertificateName + "\""); return(ERROR_BAD_CERTIFICATE_NAME); } } } // // Final validation // if (_winCertesOptions.Revoke > 5) { WriteErrorMessageWithUsage(_options, "Revocation Reason is a number between 0 and 5"); return(ERROR_REVOKE); } if (_winCertesOptions.AccountEmail == null || _winCertesOptions.AccountEmail.Length < 5) { WriteErrorMessageWithUsage(_options, "An email address needs to be provided or stored in the registry"); return(ERROR_NO_EMAIL); } // Final check if ((!_show) && (!_reset) && (_winCertesOptions.Domains.Count == 0)) { WriteErrorMessageWithUsage(_options, "At least one domain must be specified"); return(ERROR_NO_DOMAINS); } return(SUCCESS); }