Example #1
0
        /// <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);
            }
        }
Example #2
0
        /// <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})");
        }
Example #3
0
        /// <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");
                }
            }
        }
Example #4
0
        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);
        }
Example #5
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);
        }