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); }
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); }
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); }
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); } }