public void ParsingCertificateStoreShouldWork()
        {
            var az      = new Mock <IAzureHelper>();
            var factory = new Mock <IStorageFactory>();

            var log       = new Mock <ILoggerFactory>();
            var kvFactory = new Mock <IKeyVaultFactory>();
            var client    = new Mock <CertificateClient>();

            kvFactory.Setup(x => x.CreateCertificateClient("example"))
            .Returns(client.Object);
            IRenewalOptionParser parser = new RenewalOptionParser(
                az.Object,
                kvFactory.Object,
                factory.Object,
                new Mock <IAzureAppServiceClient>().Object,
                new Mock <IAzureCdnClient>().Object,
                log.Object);

            var cfg   = TestHelper.LoadConfig("config");
            var store = parser.ParseCertificateStore(cfg.Certificates[0]);

            store.Type.Should().Be("keyVault");
            store.Name.Should().Be("example");
        }
        public void ParsingChallengeResponderShouldWorkIfCallerHasMsiAccessToStorage()
        {
            var az      = new Mock <IAzureHelper>();
            var factory = new Mock <IStorageFactory>();
            var storage = new Mock <IStorageProvider>();

            // check is used by parser to verify MSI access
            storage.Setup(x => x.ExistsAsync(RenewalOptionParser.FileNameForPermissionCheck, It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(false));
            factory.Setup(x => x.FromMsiAsync("example", new StorageProperties().ContainerName, It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(storage.Object));

            var kvFactory = new Mock <IKeyVaultFactory>();
            var client    = new Mock <CertificateClient>();

            kvFactory.Setup(x => x.CreateCertificateClient("example"))
            .Returns(client.Object);
            var log = new Mock <ILoggerFactory>();
            IRenewalOptionParser parser = new RenewalOptionParser(
                az.Object,
                kvFactory.Object,
                factory.Object,
                new Mock <IAzureAppServiceClient>().Object,
                new Mock <IAzureCdnClient>().Object,
                log.Object);

            var cfg = TestHelper.LoadConfig("config");

            new Func <Task>(async() => _ = await parser.ParseChallengeResponderAsync(cfg.Certificates[0], CancellationToken.None)).Should().NotThrow();
        }
        public void ParsingChallengeResponderShouldFailIfCallerHasNoMsiAccessToStorageAndFallbacksAreNotAvailable()
        {
            var az      = new Mock <IAzureHelper>();
            var kv      = new Mock <IKeyVaultClient>();
            var factory = new Mock <IStorageFactory>();
            var storage = new Mock <IStorageProvider>();

            // check is used by parser to verify MSI access
            storage.Setup(x => x.ExistsAsync(RenewalOptionParser.FileNameForPermissionCheck, It.IsAny <CancellationToken>()))
            .Throws(new StorageException(new RequestResult
            {
                HttpStatusCode = (int)HttpStatusCode.Forbidden
            }, "Access denied, due to missing MSI permissions", null));
            // fallback is keyvault -> secret not found.
            kv.Setup(x => x.GetSecretWithHttpMessagesAsync(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <string>(), It.IsAny <Dictionary <string, List <string> > >(), It.IsAny <CancellationToken>()))
            .Throws(new KeyVaultErrorException("denied")
            {
                Response = new HttpResponseMessageWrapper(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.NotFound
                }, "denied")
            });
            factory.Setup(x => x.FromMsiAsync("example", new StorageProperties().ContainerName, It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(storage.Object));

            var log = new Mock <ILogger>();
            IRenewalOptionParser parser = new RenewalOptionParser(az.Object, kv.Object, factory.Object, log.Object);

            var cfg = TestHelper.LoadConfig("config");

            new Func <Task>(async() => _ = await parser.ParseChallengeResponderAsync(cfg.Certificates[0], CancellationToken.None)).Should().Throw <InvalidOperationException>();

            kv.Verify(x => x.GetSecretWithHttpMessagesAsync("https://example.vault.azure.net", new StorageProperties().SecretName, "", null, CancellationToken.None), Times.Once);
        }
        public async Task ParsingChallengeResponderShouldSucceedIfCallerHasNoMsiAccessToConnectionStringFallbackIsAvailable()
        {
            var az      = new Mock <IAzureHelper>();
            var kv      = new Mock <IKeyVaultClient>();
            var factory = new Mock <IStorageFactory>();
            var storage = new Mock <IStorageProvider>();

            // check is used by parser to verify MSI access
            storage.Setup(x => x.ExistsAsync(RenewalOptionParser.FileNameForPermissionCheck, It.IsAny <CancellationToken>()))
            .Throws(new StorageException(new RequestResult
            {
                HttpStatusCode = (int)HttpStatusCode.Forbidden
            }, "Access denied, due to missing MSI permissions", null));
            // fallback is connectionString
            const string connectionString = "this will grant me access";

            factory.Setup(x => x.FromMsiAsync("example", new StorageProperties().ContainerName, It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(storage.Object));
            // fallback
            factory.Setup(x => x.FromConnectionString(connectionString, new StorageProperties().ContainerName))
            .Returns(storage.Object);

            var log = new Mock <ILogger>();
            IRenewalOptionParser parser = new RenewalOptionParser(az.Object, kv.Object, factory.Object, log.Object);

            var cfg = TestHelper.LoadConfig("config+connectionstring");
            var r   = await parser.ParseChallengeResponderAsync(cfg.Certificates[0], CancellationToken.None);

            // keyvault should not be tried if connectionstring is found
            kv.VerifyNoOtherCalls();
            factory.Verify(x => x.FromConnectionString(connectionString, new StorageProperties().ContainerName), Times.Once);
        }
Esempio n. 5
0
        public async Task LoadingConfigWithCustomStoragePathShouldUseIt()
        {
            IConfigurationProcessor processor = new ConfigurationProcessor();
            var content = File.ReadAllText("Files/config+custompath.json");
            var cfg     = processor.ValidateAndLoad(content);
            var cert    = cfg.Certificates[0];

            cert.HostNames.Should().BeEquivalentTo(new[]
            {
                "example.com", "www.example.com"
            });
            cert.ChallengeResponder.Should().NotBeNull();

            // fake grant MSI access
            var factory = new Mock <IStorageFactory>();
            var storage = new Mock <IStorageProvider>();

            storage.Setup(x => x.ExistsAsync(It.IsAny <string>(), It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(true));
            factory.Setup(x => x.FromMsiAsync(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(storage.Object));

            var parser = new RenewalOptionParser(
                new Mock <IAzureHelper>().Object,
                new Mock <IKeyVaultClient>().Object,
                factory.Object,
                new Mock <IAzureAppServiceClient>().Object,
                new Mock <IAzureCdnClient>().Object,
                new Mock <ILoggerFactory>().Object);
            var responder = await parser.ParseChallengeResponderAsync(cert, CancellationToken.None);

            var ctx = new Mock <IChallengeContext>();

            // Certes .Http() extension method internall filters for this type
            ctx.SetupGet(x => x.Type)
            .Returns("http-01");

            ctx.SetupGet(x => x.Token)
            .Returns("fileNAME");
            ctx.SetupGet(x => x.KeyAuthz)
            .Returns("$content");

            var auth = new Mock <IAuthorizationContext>();

            auth.Setup(x => x.Challenges())
            .Returns(Task.FromResult(new[] { ctx.Object }.AsEnumerable()));
            var order = new Mock <IOrderContext>();

            order.Setup(x => x.Authorizations())
            .Returns(Task.FromResult(new[] { auth.Object }.AsEnumerable()));
            _ = await responder.InitiateChallengesAsync(order.Object, CancellationToken.None);

            const string pathPrefix = "not/well-known/";

            storage.Verify(x => x.SetAsync(pathPrefix + "fileNAME", "$content", It.IsAny <CancellationToken>()), Times.Once);
        }
Esempio n. 6
0
        private static async Task CheckDomainsForValidCertificateAsync(ILogger log, CancellationToken cancellationToken,
                                                                       ExecutionContext executionContext)
        {
            // internal storage (used for letsencrypt account metadata)
            IStorageProvider storageProvider = new AzureBlobStorageProvider(Environment.GetEnvironmentVariable("AzureWebJobsStorage"), "letsencrypt");

            IConfigurationProcessor processor = new ConfigurationProcessor();
            var configurations = await AutoRenewal.LoadConfigFilesAsync(storageProvider, processor, log, cancellationToken, executionContext);

            IAuthenticationService authenticationService = new AuthenticationService(storageProvider);
            var az = new AzureHelper();

            var tokenProvider  = new AzureServiceTokenProvider();
            var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
            var storageFactory = new StorageFactory(az);

            var renewalOptionsParser = new RenewalOptionParser(az, keyVaultClient, storageFactory, log);
            var certificateBuilder   = new CertificateBuilder();

            IRenewalService renewalService = new RenewalService(authenticationService, renewalOptionsParser, certificateBuilder, log);
            var             errors         = new List <Exception>();
            var             httpClient     = new HttpClient();

            foreach ((var name, var config) in configurations)
            {
                using (log.BeginScope($"Checking certificates from {name}"))
                {
                    foreach (var cert in config.Certificates)
                    {
                        var hostNames = string.Join(";", cert.HostNames);
                        try
                        {
                            // check each domain to verify HTTPS certificate is valid
                            var request = WebRequest.CreateHttp($"https://{cert.HostNames.First()}");
                            request.ServerCertificateValidationCallback += ValidateTestServerCertificate;
                            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { }
                        }
                        catch (Exception e)
                        {
                            log.LogError(e, $"Certificate check failed for: {hostNames}!");
                            errors.Add(e);
                            continue;
                        }
                        log.LogInformation($"Certificate for {hostNames} looks valid");
                    }
                }
            }
            if (!configurations.Any())
            {
                log.LogWarning("No configurations where processed, refere to the sample on how to set up configs!");
            }
            if (errors.Any())
            {
                throw new AggregateException("Failed to process all certificates", errors);
            }
        }
        public async Task ParsingChallengeResponderShouldSucceedIfCallerHasNoMsiAccessToStorageButKeyVaultFallbackIsAvailable()
        {
            var az      = new Mock <IAzureHelper>();
            var factory = new Mock <IStorageFactory>();
            var storage = new Mock <IStorageProvider>();

            // check is used by parser to verify MSI access
            storage.Setup(x => x.ExistsAsync(RenewalOptionParser.FileNameForPermissionCheck, It.IsAny <CancellationToken>()))
            .Throws(new RequestFailedException((int)HttpStatusCode.Forbidden, "Access denied, due to missing MSI permissions"));

            factory.Setup(x => x.FromMsiAsync("example", new StorageProperties().ContainerName, It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(storage.Object));

            var log = new Mock <ILoggerFactory>();

            log.Setup(x => x.CreateLogger(It.IsAny <string>()))
            .Returns(new Mock <ILogger>().Object);
            var kvFactory    = new Mock <IKeyVaultFactory>();
            var client       = new Mock <CertificateClient>();
            var secretClient = new Mock <SecretClient>();
            // fallback is keyvault
            const string connectionString = "this will grant me access";
            var          response         = new Mock <Response <KeyVaultSecret> >();

            response.SetupGet(x => x.Value)
            .Returns(new KeyVaultSecret(new StorageProperties().SecretName, connectionString));
            secretClient.Setup(x => x.GetSecretAsync(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <CancellationToken>()))
            .ReturnsAsync(response.Object);
            kvFactory.Setup(x => x.CreateSecretClient("example"))
            .Returns(secretClient.Object);
            kvFactory.Setup(x => x.CreateCertificateClient("example"))
            .Returns(client.Object);

            // fallback
            factory.Setup(x => x.FromConnectionString(connectionString, new StorageProperties().ContainerName))
            .Returns(storage.Object);
            IRenewalOptionParser parser = new RenewalOptionParser(
                az.Object,
                kvFactory.Object,
                factory.Object,
                new Mock <IAzureAppServiceClient>().Object,
                new Mock <IAzureCdnClient>().Object,
                log.Object);

            var cfg = TestHelper.LoadConfig("config");
            var r   = await parser.ParseChallengeResponderAsync(cfg.Certificates[0], CancellationToken.None);

            secretClient.Verify(x => x.GetSecretAsync(new StorageProperties().SecretName, null, CancellationToken.None), Times.Once);
            factory.Verify(x => x.FromConnectionString(connectionString, new StorageProperties().ContainerName), Times.Once);
        }
        public void ParsingCdnResourceShouldWork()
        {
            var az      = new Mock <IAzureHelper>();
            var kv      = new Mock <IKeyVaultClient>();
            var factory = new Mock <IStorageFactory>();

            var log = new Mock <ILogger>();
            IRenewalOptionParser parser = new RenewalOptionParser(az.Object, kv.Object, factory.Object, log.Object);

            var cfg    = TestHelper.LoadConfig("config");
            var target = parser.ParseTargetResource(cfg.Certificates[0]);

            target.Name.Should().Be("example");
        }
        public async Task ParsingChallengeResponderShouldSucceedIfCallerHasNoMsiAccessToStorageButKeyVaultFallbackIsAvailable()
        {
            var az      = new Mock <IAzureHelper>();
            var kv      = new Mock <IKeyVaultClient>();
            var factory = new Mock <IStorageFactory>();
            var storage = new Mock <IStorageProvider>();

            // check is used by parser to verify MSI access
            storage.Setup(x => x.ExistsAsync(RenewalOptionParser.FileNameForPermissionCheck, It.IsAny <CancellationToken>()))
            .Throws(new StorageException(new RequestResult
            {
                HttpStatusCode = (int)HttpStatusCode.Forbidden
            }, "Access denied, due to missing MSI permissions", null));
            // fallback is keyvault
            const string connectionString = "this will grant me access";

            kv.Setup(x => x.GetSecretWithHttpMessagesAsync(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <string>(), It.IsAny <Dictionary <string, List <string> > >(), It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(new AzureOperationResponse <SecretBundle>
            {
                Body = new SecretBundle(connectionString)
            }));

            factory.Setup(x => x.FromMsiAsync("example", new StorageProperties().ContainerName, It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(storage.Object));
            // fallback
            factory.Setup(x => x.FromConnectionString(connectionString, new StorageProperties().ContainerName))
            .Returns(storage.Object);

            var log = new Mock <ILoggerFactory>();

            log.Setup(x => x.CreateLogger(It.IsAny <string>()))
            .Returns(new Mock <ILogger>().Object);
            IRenewalOptionParser parser = new RenewalOptionParser(
                az.Object,
                kv.Object,
                factory.Object,
                new Mock <IAzureAppServiceClient>().Object,
                new Mock <IAzureCdnClient>().Object,
                log.Object);

            var cfg = TestHelper.LoadConfig("config");
            var r   = await parser.ParseChallengeResponderAsync(cfg.Certificates[0], CancellationToken.None);

            kv.Verify(x => x.GetSecretWithHttpMessagesAsync("https://example.vault.azure.net", new StorageProperties().SecretName, "", null, CancellationToken.None), Times.Once);
            factory.Verify(x => x.FromConnectionString(connectionString, new StorageProperties().ContainerName), Times.Once);
        }
        public void ParsingChallengeResponderShouldFailIfCallerHasNoMsiAccessToStorageAndFallbacksAreNotAvailable()
        {
            var az      = new Mock <IAzureHelper>();
            var factory = new Mock <IStorageFactory>();
            var storage = new Mock <IStorageProvider>();

            // check is used by parser to verify MSI access
            storage.Setup(x => x.ExistsAsync(RenewalOptionParser.FileNameForPermissionCheck, It.IsAny <CancellationToken>()))
            .Throws(new RequestFailedException((int)HttpStatusCode.Forbidden, "Access denied, due to missing MSI permissions"));

            factory.Setup(x => x.FromMsiAsync("example", new StorageProperties().ContainerName, It.IsAny <CancellationToken>()))
            .Returns(Task.FromResult(storage.Object));

            var log = new Mock <ILoggerFactory>();

            log.Setup(x => x.CreateLogger(It.IsAny <string>()))
            .Returns(new Mock <ILogger>().Object);
            var kvFactory    = new Mock <IKeyVaultFactory>();
            var client       = new Mock <CertificateClient>();
            var secretClient = new Mock <SecretClient>();

            // fallback is keyvault -> secret not found.
            secretClient.Setup(x => x.GetSecretAsync(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <CancellationToken>()))
            .Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "denied"));
            kvFactory.Setup(x => x.CreateSecretClient("example"))
            .Returns(secretClient.Object);
            kvFactory.Setup(x => x.CreateCertificateClient("example"))
            .Returns(client.Object);

            IRenewalOptionParser parser = new RenewalOptionParser(
                az.Object,
                kvFactory.Object,
                factory.Object,
                new Mock <IAzureAppServiceClient>().Object,
                new Mock <IAzureCdnClient>().Object,
                log.Object);

            var cfg = TestHelper.LoadConfig("config");

            new Func <Task>(async() => _ = await parser.ParseChallengeResponderAsync(cfg.Certificates[0], CancellationToken.None)).Should().Throw <InvalidOperationException>();

            secretClient.Verify(x => x.GetSecretAsync(new StorageProperties().SecretName, null, CancellationToken.None), Times.Once);
        }
Esempio n. 11
0
        private static async Task RenewAsync(Overrides overrides, ILogger log, CancellationToken cancellationToken,
                                             ExecutionContext executionContext)
        {
            // internal storage (used for letsencrypt account metadata)
            IStorageProvider storageProvider = new AzureBlobStorageProvider(Environment.GetEnvironmentVariable("AzureWebJobsStorage"), "letsencrypt");

            IConfigurationProcessor processor = new ConfigurationProcessor();
            var configurations = await LoadConfigFilesAsync(storageProvider, processor, log, cancellationToken, executionContext);

            IAuthenticationService authenticationService = new AuthenticationService(storageProvider);
            var az = new AzureHelper();

            var tokenProvider  = new AzureServiceTokenProvider();
            var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
            var storageFactory = new StorageFactory(az);

            var renewalOptionsParser = new RenewalOptionParser(az, keyVaultClient, storageFactory, log);
            var certificateBuilder   = new CertificateBuilder();

            IRenewalService renewalService = new RenewalService(authenticationService, renewalOptionsParser, certificateBuilder, log);
            var             stopwatch      = new Stopwatch();
            // TODO: with lots of certificate renewals this could run into function timeout (10mins)
            // with 30 days to expiry (default setting) this isn't a big problem as next day all finished certs are skipped
            // user will only get email <= 14 days before expiry so acceptable for now
            var errors = new List <Exception>();

            foreach ((var name, var config) in configurations)
            {
                using (log.BeginScope($"Working on certificates from {name}"))
                {
                    foreach (var cert in config.Certificates)
                    {
                        stopwatch.Restart();
                        var hostNames = string.Join(";", cert.HostNames);
                        cert.Overrides = overrides ?? Overrides.None;
                        try
                        {
                            var result = await renewalService.RenewCertificateAsync(config.Acme, cert, cancellationToken);

                            switch (result)
                            {
                            case RenewalResult.NoChange:
                                log.LogInformation($"Certificate renewal skipped for: {hostNames} (no change required yet)");
                                break;

                            case RenewalResult.Success:
                                log.LogInformation($"Certificate renewal succeeded for: {hostNames}");
                                break;

                            default:
                                throw new ArgumentOutOfRangeException(result.ToString());
                            }
                        }
                        catch (Exception e)
                        {
                            log.LogError(e, $"Certificate renewal failed for: {hostNames}!");
                            errors.Add(e);
                        }
                        log.LogInformation($"Renewing certificates for {hostNames} took: {stopwatch.Elapsed}");
                    }
                }
            }
            if (!configurations.Any())
            {
                log.LogWarning("No configurations where processed, refere to the sample on how to set up configs!");
            }
            if (errors.Any())
            {
                throw new AggregateException("Failed to process all certificates", errors);
            }
        }