예제 #1
0
        /// <summary>
        /// Request certificate from the ACME server
        /// </summary>
        /// <param name="binding"></param>
        /// <returns></returns>
        public CertificateInfo RequestCertificate(ICsrPlugin csrPlugin, RunLevel runLevel, Renewal renewal, Target target, OrderDetails order)
        {
            // What are we going to get?
            var pfxFileInfo = new FileInfo(PfxFilePath(renewal));

            // Determine/check the common name
            var identifiers     = target.GetHosts(false);
            var commonNameUni   = target.CommonName;
            var commonNameAscii = string.Empty;

            if (!string.IsNullOrWhiteSpace(commonNameUni))
            {
                var idn = new IdnMapping();
                commonNameAscii = idn.GetAscii(commonNameUni);
                if (!identifiers.Contains(commonNameAscii, StringComparer.InvariantCultureIgnoreCase))
                {
                    _log.Warning($"Common name {commonNameUni} provided is invalid.");
                    commonNameAscii = identifiers.First();
                    commonNameUni   = idn.GetUnicode(commonNameAscii);
                }
            }

            // Determine the friendly name
            var friendlyName = renewal.FriendlyName;

            if (string.IsNullOrEmpty(friendlyName))
            {
                friendlyName = target.FriendlyName;
            }
            if (string.IsNullOrEmpty(friendlyName))
            {
                friendlyName = commonNameUni;
            }

            // Try using cached certificate first to avoid rate limiting during
            // (initial?) deployment troubleshooting. Real certificate requests
            // will only be done once per day maximum unless the --force parameter
            // is used.
            var cache = CachedInfo(renewal);

            if (cache != null &&
                cache.CacheFile.LastWriteTime > DateTime.Now.AddDays(Settings.Default.CertificateCacheDays * -1) &&
                cache.Match(target))
            {
                if (runLevel.HasFlag(RunLevel.IgnoreCache))
                {
                    _log.Warning("Cached certificate available but not used with --{switch}. Use 'Renew specific' or " +
                                 "'Renew all' in the main menu to run unscheduled renewals without hitting rate limits.",
                                 nameof(MainArguments.Force).ToLower());
                }
                else
                {
                    _log.Warning("Using cached certificate for {friendlyName}. To force issue of a new certificate within " +
                                 "24 hours, delete the .pfx file from the CertificatePath or run with the --{switch} switch. " +
                                 "Be ware that you might run into rate limits doing so.",
                                 friendlyName,
                                 nameof(MainArguments.Force).ToLower());
                    return(cache);
                }
            }

            var csr      = csrPlugin.GenerateCsr(commonNameAscii, identifiers);
            var csrBytes = csr.CreateSigningRequest();

            order = _client.SubmitCsr(order, csrBytes);
            File.WriteAllText(GetPath(renewal, "-csr.pem"), _pemService.GetPem("CERTIFICATE REQUEST", csrBytes));

            _log.Information("Requesting certificate {friendlyName}", friendlyName);
            var rawCertificate = _client.GetCertificate(order);

            if (rawCertificate == null)
            {
                throw new Exception($"Unable to get certificate");
            }

            var certificate       = new X509Certificate2(rawCertificate);
            var certificateExport = certificate.Export(X509ContentType.Cert);
            var crtPem            = _pemService.GetPem("CERTIFICATE", certificateExport);

            // Get issuer certificate
            var issuerCertificate       = new X509Certificate2(rawCertificate.Skip(certificateExport.Length).ToArray());
            var issuerCertificateExport = issuerCertificate.Export(X509ContentType.Cert);
            var issuerPem = _pemService.GetPem("CERTIFICATE", issuerCertificateExport);

            // Build pfx archive
            var pfx                = new bc.Pkcs.Pkcs12Store();
            var bcCertificate      = _pemService.ParsePem <bc.X509.X509Certificate>(crtPem);
            var bcCertificateEntry = new bc.Pkcs.X509CertificateEntry(bcCertificate);
            var bcCertificateAlias = bcCertificate.SubjectDN.ToString();
            var bcPrivateKeyEntry  = new bc.Pkcs.AsymmetricKeyEntry(csrPlugin.GetPrivateKey());

            pfx.SetCertificateEntry(bcCertificateAlias, bcCertificateEntry);
            pfx.SetKeyEntry(bcCertificateAlias, bcPrivateKeyEntry, new[] { bcCertificateEntry });

            var bcIssuer      = _pemService.ParsePem <bc.X509.X509Certificate>(issuerPem);
            var bcIssuerEntry = new bc.Pkcs.X509CertificateEntry(bcIssuer);
            var bcIssuerAlias = bcIssuer.SubjectDN.ToString();

            pfx.SetCertificateEntry(bcIssuerAlias, bcIssuerEntry);

            var pfxStream = new MemoryStream();

            pfx.Save(pfxStream, null, new bc.Security.SecureRandom());
            pfxStream.Position = 0;
            using (var pfxStreamReader = new BinaryReader(pfxStream))
            {
                var tempPfx = new X509Certificate2(
                    pfxStreamReader.ReadBytes((int)pfxStream.Length),
                    (string)null,
                    X509KeyStorageFlags.MachineKeySet |
                    X509KeyStorageFlags.PersistKeySet |
                    X509KeyStorageFlags.Exportable);
                if (csrPlugin.CanConvert())
                {
                    try
                    {
                        var converted = csrPlugin.Convert(tempPfx.PrivateKey);
                        if (converted != null)
                        {
                            tempPfx.PrivateKey = converted;
                        }
                    }
                    catch
                    {
                        _log.Warning("Private key conversion error.");
                    }
                }

                tempPfx.FriendlyName = $"{friendlyName} {DateTime.Now.ToUserString()}";
                File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, renewal.PfxPassword?.Value));
                pfxFileInfo.Refresh();
            }

            // Update LastFriendlyName so that the user sees
            // the most recently issued friendlyName in
            // the WACS GUI
            renewal.LastFriendlyName = friendlyName;

            // Recreate X509Certificate2 with correct flags for Store/Install
            return(new CertificateInfo()
            {
                Certificate = ReadForUse(pfxFileInfo, renewal.PfxPassword?.Value),
                CacheFile = pfxFileInfo,
                CacheFilePassword = renewal.PfxPassword?.Value
            });
        }
예제 #2
0
        /// <summary>
        /// Request certificate from the ACME server
        /// </summary>
        /// <param name="csrPlugin">Plugin used to generate CSR if it has not been provided in the target</param>
        /// <param name="runLevel"></param>
        /// <param name="renewal"></param>
        /// <param name="target"></param>
        /// <param name="order"></param>
        /// <returns></returns>
        public async Task <CertificateInfo> RequestCertificate(ICsrPlugin?csrPlugin, RunLevel runLevel, Order order)
        {
            if (order.Details == null)
            {
                throw new InvalidOperationException("No order details found");
            }
            // What are we going to get?
            var cacheKey     = CacheKey(order);
            var pfxFileInfo  = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}{PfxPostFix}"));
            var friendlyName = $"{order.FriendlyNameIntermediate} @ {_inputService.FormatDate(DateTime.Now)}";

            // Generate the CSR here, because we want to save it
            // in the certificate cache folder even though we might
            // not need to submit it to the server in case of a
            // cached order
            order.Target.CsrBytes = order.Target.UserCsrBytes;
            if (order.Target.CsrBytes == null)
            {
                if (csrPlugin == null)
                {
                    throw new InvalidOperationException("Missing CsrPlugin");
                }
                if (order.KeyPath == null)
                {
                    throw new InvalidOperationException("Missing ReusedKeyPath");
                }
                var csr = await csrPlugin.GenerateCsr(order.KeyPath, order.Target);

                var keySet = await csrPlugin.GetKeys();

                order.Target.CsrBytes   = csr.GetDerEncoded();
                order.Target.PrivateKey = keySet.Private;
            }

            if (order.Target.CsrBytes == null)
            {
                throw new InvalidOperationException("No CsrBytes found");
            }

            // Store CSR for future reference
            ClearCache(order, postfix: $"*{CsrPostFix}");
            var csrPath = GetPath(order.Renewal, $"-{cacheKey}{CsrPostFix}");
            await File.WriteAllTextAsync(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", order.Target.CsrBytes));

            _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath));

            // Check order status
            if (order.Details.Payload.Status != AcmeClient.OrderValid)
            {
                // Finish the order by sending the CSR to
                // the server, which can then generate the
                // certificate.
                _log.Verbose("Submitting CSR");
                order.Details = await _client.SubmitCsr(order.Details, order.Target.CsrBytes);

                if (order.Details.Payload.Status != AcmeClient.OrderValid)
                {
                    _log.Error("Unexpected order status {status}", order.Details.Payload.Status);
                    throw new Exception($"Unable to complete order");
                }
            }

            // Download the certificate from the server
            _log.Information("Downloading certificate {friendlyName}", order.FriendlyNameIntermediate);
            var certInfo = default(AcmeCertificate);

            try
            {
                certInfo = await _client.GetCertificate(order.Details);
            }
            catch (Exception ex)
            {
                throw new Exception($"Unable to get certificate", ex);
            }
            if (certInfo == null || certInfo.Certificate == null)
            {
                throw new Exception($"Unable to get certificate");
            }
            var alternatives = new List <X509Certificate2Collection>
            {
                ParseCertificate(certInfo.Certificate, friendlyName, order.Target.PrivateKey)
            };

            if (certInfo.Links != null)
            {
                foreach (var alt in certInfo.Links["alternate"])
                {
                    try
                    {
                        var altCertRaw = await _client.GetCertificate(alt);

                        var altCert = ParseCertificate(altCertRaw, friendlyName, order.Target.PrivateKey);
                        alternatives.Add(altCert);
                    }
                    catch (Exception ex)
                    {
                        _log.Warning("Unable to get alternate certificate: {ex}", ex.Message);
                    }
                }
            }
            var selected = Select(alternatives);

            ClearCache(order, postfix: $"*{PfxPostFix}");
            ClearCache($"{order.Renewal.Id}*{PfxPostFixLegacy}");
            await File.WriteAllBytesAsync(pfxFileInfo.FullName, selected.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value) !);

            _log.Debug("Certificate written to cache file {path} in certificate cache folder {folder}. It will be " +
                       "reused when renewing within {x} day(s) as long as the --source and --csr parameters remain the same and " +
                       "the --force switch is not used.",
                       pfxFileInfo.Name,
                       pfxFileInfo.Directory !.FullName,
                       _settings.Cache.ReuseDays);

            if (csrPlugin != null)
            {
                try
                {
                    var cert = selected.
                               OfType <X509Certificate2>().
                               Where(x => x.HasPrivateKey).
                               FirstOrDefault();
                    if (cert != null)
                    {
                        var certIndex  = selected.IndexOf(cert);
                        var newVersion = await csrPlugin.PostProcess(cert);

                        if (newVersion != cert)
                        {
                            newVersion.FriendlyName = friendlyName;
                            selected[certIndex]     = newVersion;
                            await File.WriteAllBytesAsync(pfxFileInfo.FullName, selected.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value) !);

                            newVersion.Dispose();
                        }
                    }
                }
                catch (Exception ex)
                {
                    _log.Warning("Private key conversion error: {ex}", ex.Message);
                }
            }

            pfxFileInfo.Refresh();

            // Update LastFriendlyName so that the user sees
            // the most recently issued friendlyName in
            // the WACS GUI
            order.Renewal.LastFriendlyName = order.FriendlyNameBase;

            // Recreate X509Certificate2 with correct flags for Store/Install
            return(FromCache(pfxFileInfo, order.Renewal.PfxPassword?.Value));
        }
예제 #3
0
        /// <summary>
        /// Request certificate from the ACME server
        /// </summary>
        /// <param name="csrPlugin">Plugin used to generate CSR if it has not been provided in the target</param>
        /// <param name="runLevel"></param>
        /// <param name="renewal"></param>
        /// <param name="target"></param>
        /// <param name="order"></param>
        /// <returns></returns>
        public async Task <CertificateInfo> RequestCertificate(ICsrPlugin?csrPlugin, RunLevel runLevel, Order order)
        {
            if (order.Details == null)
            {
                throw new InvalidOperationException();
            }

            // What are we going to get?
            var cacheKey    = CacheKey(order);
            var pfxFileInfo = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}{PfxPostFix}"));

            // Determine/check the common name
            var identifiers     = order.Target.GetHosts(false);
            var commonNameUni   = order.Target.CommonName;
            var commonNameAscii = string.Empty;

            if (!string.IsNullOrWhiteSpace(commonNameUni))
            {
                var idn = new IdnMapping();
                commonNameAscii = idn.GetAscii(commonNameUni);
                if (!identifiers.Contains(commonNameAscii, StringComparer.InvariantCultureIgnoreCase))
                {
                    _log.Warning($"Common name {commonNameUni} provided is invalid.");
                    commonNameAscii = identifiers.First();
                    commonNameUni   = idn.GetUnicode(commonNameAscii);
                }
            }

            // Determine the friendly name base (for the renewal)
            var friendlyNameBase = order.Renewal.FriendlyName;

            if (string.IsNullOrEmpty(friendlyNameBase))
            {
                friendlyNameBase = order.Target.FriendlyName;
            }
            if (string.IsNullOrEmpty(friendlyNameBase))
            {
                friendlyNameBase = commonNameUni;
            }

            // Determine the friendly name for this specific certificate
            var friendlyNameIntermediate = friendlyNameBase;

            if (!string.IsNullOrEmpty(order.FriendlyNamePart))
            {
                friendlyNameIntermediate += $" [{order.FriendlyNamePart}]";
            }
            var friendlyName = $"{friendlyNameIntermediate} @ {_inputService.FormatDate(DateTime.Now)}";

            // Try using cached certificate first to avoid rate limiting during
            // (initial?) deployment troubleshooting. Real certificate requests
            // will only be done once per day maximum unless the --force parameter
            // is used.
            var cache = CachedInfo(order);

            if (cache != null && cache.CacheFile != null)
            {
                if (cache.CacheFile.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1))
                {
                    if (runLevel.HasFlag(RunLevel.IgnoreCache))
                    {
                        _log.Warning("Cached certificate available on disk but not used due to --{switch} switch.",
                                     nameof(MainArguments.Force).ToLower());
                    }
                    else
                    {
                        _log.Warning("Using cached certificate for {friendlyName}. To force a new request of the " +
                                     "certificate within {days} days, run with the --{switch} switch.",
                                     friendlyNameIntermediate,
                                     _settings.Cache.ReuseDays,
                                     nameof(MainArguments.Force).ToLower());
                        return(cache);
                    }
                }
            }

            if (order.Details.Payload.Status != AcmeClient.OrderValid)
            {
                // Clear cache and write new cert
                ClearCache(order.Renewal, postfix: CsrPostFix);

                if (order.Target.CsrBytes == null)
                {
                    if (csrPlugin == null)
                    {
                        throw new InvalidOperationException("Missing csrPlugin");
                    }
                    // Backwards compatible with existing keys, which are not split per order yet.
                    var keyFile = new FileInfo(GetPath(order.Renewal, $".keys"));
                    if (!keyFile.Exists)
                    {
                        keyFile = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}.keys"));
                    }
                    var csr = await csrPlugin.GenerateCsr(keyFile.FullName, commonNameAscii, identifiers);

                    var keySet = await csrPlugin.GetKeys();

                    order.Target.CsrBytes   = csr.GetDerEncoded();
                    order.Target.PrivateKey = keySet.Private;
                    var csrPath = GetPath(order.Renewal, $"-{cacheKey}{CsrPostFix}");
                    File.WriteAllText(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", order.Target.CsrBytes));
                    _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath));
                }

                _log.Verbose("Submitting CSR");
                order.Details = await _client.SubmitCsr(order.Details, order.Target.CsrBytes);

                if (order.Details.Payload.Status != AcmeClient.OrderValid)
                {
                    _log.Error("Unexpected order status {status}", order.Details.Payload.Status);
                    throw new Exception($"Unable to complete order");
                }
            }

            _log.Information("Requesting certificate {friendlyName}", friendlyNameIntermediate);
            var rawCertificate = await _client.GetCertificate(order.Details);

            if (rawCertificate == null)
            {
                throw new Exception($"Unable to get certificate");
            }

            // Build pfx archive including any intermediates provided
            var          text        = Encoding.UTF8.GetString(rawCertificate);
            var          pfx         = new bc.Pkcs.Pkcs12Store();
            var          startIndex  = 0;
            var          endIndex    = 0;
            const string startString = "-----BEGIN CERTIFICATE-----";
            const string endString   = "-----END CERTIFICATE-----";

            while (true)
            {
                startIndex = text.IndexOf(startString, startIndex);
                if (startIndex < 0)
                {
                    break;
                }
                endIndex = text.IndexOf(endString, startIndex);
                if (endIndex < 0)
                {
                    break;
                }
                endIndex += endString.Length;
                var pem           = text[startIndex..endIndex];
예제 #4
0
        /// <summary>
        /// Request certificate from the ACME server
        /// </summary>
        /// <param name="binding"></param>
        /// <returns></returns>
        public CertificateInfo RequestCertificate(ICsrPlugin csrPlugin, Renewal renewal, Target target, OrderDetails order)
        {
            // What are we going to get?
            var pfxFileInfo = new FileInfo(PfxFilePath(renewal));

            // Determine/check the common name
            var identifiers = target.GetHosts(false);
            var commonName  = target.CommonName;

            if (!string.IsNullOrWhiteSpace(commonName))
            {
                var idn = new IdnMapping();
                commonName = idn.GetAscii(commonName);
                if (!identifiers.Contains(commonName, StringComparer.InvariantCultureIgnoreCase))
                {
                    _log.Warning($"Common name {commonName} provided is invalid.");
                    commonName = identifiers.First();
                }
            }

            // Try using cached certificate first to avoid rate limiting during
            // (initial?) deployment troubleshooting. Real certificate requests
            // will only be done once per day maximum.
            if (pfxFileInfo.Exists && pfxFileInfo.LastWriteTime > DateTime.Now.AddDays(-1))
            {
                try
                {
                    var cached = new CertificateInfo()
                    {
                        Certificate = ReadForUse(pfxFileInfo, renewal.PfxPassword),
                        PfxFile     = pfxFileInfo
                    };
                    var idn = new IdnMapping();
                    if (cached.SubjectName == commonName &&
                        cached.HostNames.Count == identifiers.Count() &&
                        cached.HostNames.All(h => identifiers.Contains(idn.GetAscii(h))))
                    {
                        if (_options.Force)
                        {
                            _log.Warning("Cached certificate available but not used with --{switch}. Use 'Renew specific' or 'Renew all' in the main menu to run unscheduled renewals without hitting rate limits.", nameof(MainArguments.Force).ToLower());
                        }
                        else
                        {
                            _log.Warning("Using cached certificate for {friendlyName}. To force issue of a new certificate within 24 hours, delete the .pfx file from the CertificatePath or run with the --{switch} switch. Be ware that you might run into rate limits doing so.", renewal.FriendlyName, nameof(MainArguments.Force).ToLower());
                            return(cached);
                        }
                    }
                }
                catch
                {
                    // File corrupt or invalid password?
                    _log.Warning("Unable to read from certificate cache");
                }
            }

            var csr      = csrPlugin.GenerateCsr(commonName, identifiers);
            var csrBytes = csr.CreateSigningRequest();

            order = _client.SubmitCsr(order, csrBytes);

            if (Settings.Default.SavePrivateKeyPem)
            {
                File.WriteAllText(GetPath(renewal, "-key.pem"), GetPem(csrPlugin.GeneratePrivateKey()));
            }
            File.WriteAllText(GetPath(renewal, "-csr.pem"), GetPem("CERTIFICATE REQUEST", csrBytes));

            _log.Information("Requesting certificate {friendlyName}", renewal.FriendlyName);
            var rawCertificate = _client.GetCertificate(order);

            if (rawCertificate == null)
            {
                throw new Exception($"Unable to get certificate");
            }

            var certificate       = new X509Certificate2(rawCertificate);
            var certificateExport = certificate.Export(X509ContentType.Cert);

            var crtDerFile = GetPath(renewal, $"-crt.der");
            var crtPemFile = GetPath(renewal, $"-crt.pem");
            var crtPem     = GetPem("CERTIFICATE", certificateExport);

            _log.Information("Saving certificate to {crtDerFile}", _certificatePath);
            File.WriteAllBytes(crtDerFile, certificateExport);
            File.WriteAllText(crtPemFile, crtPem);

            // Get issuer certificate and save in DER and PEM formats
            var chain = new X509Chain();

            chain.Build(certificate);
            X509Certificate2 issuerCertificate = chain.ChainElements[1].Certificate;
            var issuerCertificateExport        = issuerCertificate.Export(X509ContentType.Cert);
            var issuerPem = GetPem("CERTIFICATE", issuerCertificateExport);

            File.WriteAllBytes(GetPath(renewal, "-crt.der", "ca-"), issuerCertificateExport);
            File.WriteAllText(GetPath(renewal, "-crt.pem", "ca-"), issuerPem);

            // Generate combined files
            File.WriteAllText(GetPath(renewal, "-chain.pem", "ca-"), crtPem + issuerPem);

            // Build pfx archive
            var pfx                = new bc.Pkcs.Pkcs12Store();
            var bcCertificate      = ParsePem <bc.X509.X509Certificate>(crtPem);
            var bcCertificateEntry = new bc.Pkcs.X509CertificateEntry(bcCertificate);
            var bcCertificateAlias = bcCertificate.SubjectDN.ToString();
            var bcPrivateKeyEntry  = new bc.Pkcs.AsymmetricKeyEntry(csrPlugin.GeneratePrivateKey());

            pfx.SetCertificateEntry(bcCertificateAlias, bcCertificateEntry);
            pfx.SetKeyEntry(bcCertificateAlias, bcPrivateKeyEntry, new[] { bcCertificateEntry });

            var bcIssuer      = ParsePem <bc.X509.X509Certificate>(issuerPem);
            var bcIssuerEntry = new bc.Pkcs.X509CertificateEntry(bcIssuer);
            var bcIssuerAlias = bcIssuer.SubjectDN.ToString();

            pfx.SetCertificateEntry(bcIssuerAlias, bcIssuerEntry);

            using (var pfxStream = new MemoryStream())
            {
                pfx.Save(pfxStream, null, new bc.Security.SecureRandom());
                pfxStream.Position = 0;
                using (var pfxStreamReader = new BinaryReader(pfxStream))
                {
                    var tempPfx = new X509Certificate2(
                        pfxStreamReader.ReadBytes((int)pfxStream.Length),
                        (string)null,
                        X509KeyStorageFlags.MachineKeySet |
                        X509KeyStorageFlags.PersistKeySet |
                        X509KeyStorageFlags.Exportable);
                    if (csrPlugin.CanConvert())
                    {
                        try
                        {
                            var converted = csrPlugin.Convert(tempPfx.PrivateKey);
                            if (converted != null)
                            {
                                tempPfx.PrivateKey = converted;
                            }
                        }
                        catch
                        {
                            _log.Warning("Private key conversion error.");
                        }
                    }

                    tempPfx.FriendlyName = FriendlyName(renewal);
                    File.WriteAllBytes(pfxFileInfo.FullName, tempPfx.Export(X509ContentType.Pfx, renewal.PfxPassword));
                    pfxFileInfo.Refresh();
                }
            }

            // Recreate X509Certificate2 with correct flags for Store/Install
            return(new CertificateInfo()
            {
                Certificate = ReadForUse(pfxFileInfo, renewal.PfxPassword),
                PfxFile = pfxFileInfo
            });
        }
        /// <summary>
        /// Request certificate from the ACME server
        /// </summary>
        /// <param name="csrPlugin">Plugin used to generate CSR if it has not been provided in the target</param>
        /// <param name="runLevel"></param>
        /// <param name="renewal"></param>
        /// <param name="target"></param>
        /// <param name="order"></param>
        /// <returns></returns>
        public async Task <CertificateInfo> RequestCertificate(ICsrPlugin?csrPlugin, RunLevel runLevel, Order order)
        {
            if (order.Details == null)
            {
                throw new InvalidOperationException("No order details found");
            }

            // What are we going to get?
            var cacheKey    = CacheKey(order);
            var pfxFileInfo = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}{PfxPostFix}"));

            // Determine/check the common name
            var identifiers     = order.Target.GetHosts(false);
            var commonNameUni   = order.Target.CommonName;
            var commonNameAscii = string.Empty;

            if (!string.IsNullOrWhiteSpace(commonNameUni))
            {
                var idn = new IdnMapping();
                commonNameAscii = idn.GetAscii(commonNameUni);
                if (!identifiers.Contains(commonNameAscii, StringComparer.InvariantCultureIgnoreCase))
                {
                    _log.Warning($"Common name {commonNameUni} provided is invalid.");
                    commonNameAscii = identifiers.First();
                    commonNameUni   = idn.GetUnicode(commonNameAscii);
                }
            }

            // Determine the friendly name base (for the renewal)
            var friendlyNameBase = order.Renewal.FriendlyName;

            if (string.IsNullOrEmpty(friendlyNameBase))
            {
                friendlyNameBase = order.Target.FriendlyName;
            }
            if (string.IsNullOrEmpty(friendlyNameBase))
            {
                friendlyNameBase = commonNameUni;
            }

            // Determine the friendly name for this specific certificate
            var friendlyNameIntermediate = friendlyNameBase;

            if (!string.IsNullOrEmpty(order.FriendlyNamePart))
            {
                friendlyNameIntermediate += $" [{order.FriendlyNamePart}]";
            }
            var friendlyName = $"{friendlyNameIntermediate} @ {_inputService.FormatDate(DateTime.Now)}";

            // Try using cached certificate first to avoid rate limiting during
            // (initial?) deployment troubleshooting. Real certificate requests
            // will only be done once per day maximum unless the --force parameter
            // is used.
            var cache = CachedInfo(order);

            if (cache != null && cache.CacheFile != null)
            {
                if (cache.CacheFile.LastWriteTime > DateTime.Now.AddDays(_settings.Cache.ReuseDays * -1))
                {
                    if (runLevel.HasFlag(RunLevel.IgnoreCache))
                    {
                        _log.Warning("Cached certificate available on disk but not used due to --{switch} switch.",
                                     nameof(MainArguments.Force).ToLower());
                    }
                    else
                    {
                        _log.Warning("Using cached certificate for {friendlyName}. To force a new request of the " +
                                     "certificate within {days} days, run with the --{switch} switch.",
                                     friendlyNameIntermediate,
                                     _settings.Cache.ReuseDays,
                                     nameof(MainArguments.Force).ToLower());
                        return(cache);
                    }
                }
            }

            if (order.Details.Payload.Status != AcmeClient.OrderValid)
            {
                // Clear cache and write new cert
                ClearCache(order.Renewal, postfix: CsrPostFix);

                if (order.Target.CsrBytes == null)
                {
                    if (csrPlugin == null)
                    {
                        throw new InvalidOperationException("Missing csrPlugin");
                    }
                    // Backwards compatible with existing keys, which are not split per order yet.
                    var keyFile = new FileInfo(GetPath(order.Renewal, $".keys"));
                    if (!keyFile.Exists)
                    {
                        keyFile = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}.keys"));
                    }
                    var csr = await csrPlugin.GenerateCsr(keyFile.FullName, commonNameAscii, identifiers);

                    var keySet = await csrPlugin.GetKeys();

                    order.Target.CsrBytes   = csr.GetDerEncoded();
                    order.Target.PrivateKey = keySet.Private;
                    var csrPath = GetPath(order.Renewal, $"-{cacheKey}{CsrPostFix}");
                    await File.WriteAllTextAsync(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", order.Target.CsrBytes));

                    _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath));
                }

                _log.Verbose("Submitting CSR");
                order.Details = await _client.SubmitCsr(order.Details, order.Target.CsrBytes);

                if (order.Details.Payload.Status != AcmeClient.OrderValid)
                {
                    _log.Error("Unexpected order status {status}", order.Details.Payload.Status);
                    throw new Exception($"Unable to complete order");
                }
            }

            _log.Information("Requesting certificate {friendlyName}", friendlyNameIntermediate);
            var certInfo = await _client.GetCertificate(order.Details);

            if (certInfo == null || certInfo.Certificate == null)
            {
                throw new Exception($"Unable to get certificate");
            }
            var alternatives = new List <X509Certificate2Collection>
            {
                ParseCertificate(certInfo.Certificate, friendlyName, order.Target.PrivateKey)
            };

            if (certInfo.Links != null)
            {
                foreach (var alt in certInfo.Links["alternate"])
                {
                    try
                    {
                        var altCertRaw = await _client.GetCertificate(alt);

                        var altCert = ParseCertificate(altCertRaw, friendlyName, order.Target.PrivateKey);
                        alternatives.Add(altCert);
                    }
                    catch (Exception ex)
                    {
                        _log.Warning("Unable to get alternate certificate: {ex}", ex.Message);
                    }
                }
            }
            var selected = Select(alternatives);

            ClearCache(order.Renewal, postfix: $"*{PfxPostFix}");
            ClearCache(order.Renewal, postfix: $"*{PfxPostFixLegacy}");
            await File.WriteAllBytesAsync(pfxFileInfo.FullName, selected.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value) !);

            _log.Debug("Certificate written to cache file {path} in certificate cache folder {folder}. It will be " +
                       "reused when renewing within {x} day(s) as long as the Target and Csr parameters remain the same and " +
                       "the --force switch is not used.",
                       pfxFileInfo.Name,
                       pfxFileInfo.Directory !.FullName,
                       _settings.Cache.ReuseDays);

            if (csrPlugin != null)
            {
                try
                {
                    var cert = selected.
                               OfType <X509Certificate2>().
                               Where(x => x.HasPrivateKey).
                               FirstOrDefault();
                    if (cert != null)
                    {
                        var certIndex  = selected.IndexOf(cert);
                        var newVersion = await csrPlugin.PostProcess(cert);

                        if (newVersion != cert)
                        {
                            newVersion.FriendlyName = friendlyName;
                            selected[certIndex]     = newVersion;
                            await File.WriteAllBytesAsync(pfxFileInfo.FullName, selected.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value) !);

                            newVersion.Dispose();
                        }
                    }
                }
                catch (Exception ex)
                {
                    _log.Warning("Private key conversion error: {ex}", ex.Message);
                }
            }

            pfxFileInfo.Refresh();

            // Update LastFriendlyName so that the user sees
            // the most recently issued friendlyName in
            // the WACS GUI
            order.Renewal.LastFriendlyName = friendlyNameBase;

            // Recreate X509Certificate2 with correct flags for Store/Install
            return(FromCache(pfxFileInfo, order.Renewal.PfxPassword?.Value));
        }
예제 #6
0
        /// <summary>
        /// Request certificate from the ACME server
        /// </summary>
        /// <param name="csrPlugin">Plugin used to generate CSR if it has not been provided in the target</param>
        /// <param name="runLevel"></param>
        /// <param name="renewal"></param>
        /// <param name="target"></param>
        /// <param name="order"></param>
        /// <returns></returns>
        public async Task <CertificateInfo> RequestCertificate(ICsrPlugin?csrPlugin, RunLevel runLevel, Order order)
        {
            if (order.Details == null)
            {
                throw new InvalidOperationException("No order details found");
            }

            // What are we going to get?
            var cacheKey     = CacheKey(order);
            var pfxFileInfo  = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}{PfxPostFix}"));
            var friendlyName = $"{order.FriendlyNameIntermediate} @ {_inputService.FormatDate(DateTime.Now)}";

            // Determine/check the common name
            var identifiers = order.Target.GetIdentifiers(false);
            var commonName  = order.Target.CommonName;

            if (!identifiers.Contains(commonName.Unicode(false)))
            {
                _log.Warning($"Common name {commonName.Value} provided is invalid.");
                commonName = identifiers.First();
            }

            if (order.Details.Payload.Status != AcmeClient.OrderValid)
            {
                // Clear cache and write new cert
                ClearCache(order.Renewal, postfix: CsrPostFix);

                if (order.Target.CsrBytes == null)
                {
                    if (csrPlugin == null)
                    {
                        throw new InvalidOperationException("Missing csrPlugin");
                    }
                    // Backwards compatible with existing keys, which are not split per order yet.
                    var keyFile = new FileInfo(GetPath(order.Renewal, $".keys"));
                    if (!keyFile.Exists)
                    {
                        keyFile = new FileInfo(GetPath(order.Renewal, $"-{cacheKey}.keys"));
                    }
                    var csr = await csrPlugin.GenerateCsr(keyFile.FullName, commonName, identifiers);

                    var keySet = await csrPlugin.GetKeys();

                    order.Target.CsrBytes   = csr.GetDerEncoded();
                    order.Target.PrivateKey = keySet.Private;
                    var csrPath = GetPath(order.Renewal, $"-{cacheKey}{CsrPostFix}");
                    await File.WriteAllTextAsync(csrPath, _pemService.GetPem("CERTIFICATE REQUEST", order.Target.CsrBytes));

                    _log.Debug("CSR stored at {path} in certificate cache folder {folder}", Path.GetFileName(csrPath), Path.GetDirectoryName(csrPath));
                }

                _log.Verbose("Submitting CSR");
                order.Details = await _client.SubmitCsr(order.Details, order.Target.CsrBytes);

                if (order.Details.Payload.Status != AcmeClient.OrderValid)
                {
                    _log.Error("Unexpected order status {status}", order.Details.Payload.Status);
                    throw new Exception($"Unable to complete order");
                }
            }

            _log.Information("Requesting certificate {friendlyName}", order.FriendlyNameIntermediate);
            var certInfo = await _client.GetCertificate(order.Details);

            if (certInfo == null || certInfo.Certificate == null)
            {
                throw new Exception($"Unable to get certificate");
            }
            var alternatives = new List <X509Certificate2Collection>
            {
                ParseCertificate(certInfo.Certificate, friendlyName, order.Target.PrivateKey)
            };

            if (certInfo.Links != null)
            {
                foreach (var alt in certInfo.Links["alternate"])
                {
                    try
                    {
                        var altCertRaw = await _client.GetCertificate(alt);

                        var altCert = ParseCertificate(altCertRaw, friendlyName, order.Target.PrivateKey);
                        alternatives.Add(altCert);
                    }
                    catch (Exception ex)
                    {
                        _log.Warning("Unable to get alternate certificate: {ex}", ex.Message);
                    }
                }
            }
            var selected = Select(alternatives);

            ClearCache(order.Renewal, postfix: $"*{PfxPostFix}");
            ClearCache(order.Renewal, postfix: $"*{PfxPostFixLegacy}");
            await File.WriteAllBytesAsync(pfxFileInfo.FullName, selected.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value) !);

            _log.Debug("Certificate written to cache file {path} in certificate cache folder {folder}. It will be " +
                       "reused when renewing within {x} day(s) as long as the --source and --csr parameters remain the same and " +
                       "the --force switch is not used.",
                       pfxFileInfo.Name,
                       pfxFileInfo.Directory !.FullName,
                       _settings.Cache.ReuseDays);

            if (csrPlugin != null)
            {
                try
                {
                    var cert = selected.
                               OfType <X509Certificate2>().
                               Where(x => x.HasPrivateKey).
                               FirstOrDefault();
                    if (cert != null)
                    {
                        var certIndex  = selected.IndexOf(cert);
                        var newVersion = await csrPlugin.PostProcess(cert);

                        if (newVersion != cert)
                        {
                            newVersion.FriendlyName = friendlyName;
                            selected[certIndex]     = newVersion;
                            await File.WriteAllBytesAsync(pfxFileInfo.FullName, selected.Export(X509ContentType.Pfx, order.Renewal.PfxPassword?.Value) !);

                            newVersion.Dispose();
                        }
                    }
                }
                catch (Exception ex)
                {
                    _log.Warning("Private key conversion error: {ex}", ex.Message);
                }
            }

            pfxFileInfo.Refresh();

            // Update LastFriendlyName so that the user sees
            // the most recently issued friendlyName in
            // the WACS GUI
            order.Renewal.LastFriendlyName = order.FriendlyNameBase;

            // Recreate X509Certificate2 with correct flags for Store/Install
            return(FromCache(pfxFileInfo, order.Renewal.PfxPassword?.Value));
        }