public async Task ValidCertificateShouldNotRequestNewCertificateAndAlsoSkipResourceUpdate() { var config = TestHelper.LoadConfig("config"); var auth = new Mock <IAuthenticationService>(); var certBuilder = new Mock <ICertificateBuilder>(); var ctx = new Mock <IAcmeContext>(); var orderContext = new Mock <IOrderContext>(); var authContext = new AuthenticationContext(ctx.Object, config.Acme); var parser = new Mock <IRenewalOptionParser>(); var log = new Mock <ILogger>(); IRenewalService service = new RenewalService(auth.Object, parser.Object, certBuilder.Object, log.Object); var certStore = new Mock <ICertificateStore>(); var validCert = new Mock <ICertificate>(); validCert.SetupGet(x => x.Expires) .Returns(DateTime.Now.AddDays(config.Acme.RenewXDaysBeforeExpiry + 1)); // check if outdated (no, found and valid) parser.Setup(x => x.ParseCertificateStore(config.Certificates[0])) .Returns(certStore.Object); certStore.Setup(x => x.GetCertificateAsync(CancellationToken.None)) .Returns(Task.FromResult(validCert.Object)); // actual run - should skip everything as cert is still valid (and assumed to be deployed already) var r = await service.RenewCertificateAsync(config.Acme, config.Certificates[0], CancellationToken.None); r.Should().Be(RenewalResult.NoChange); certStore.Verify(x => x.UploadAsync(It.IsAny <byte[]>(), It.IsAny <string>(), It.IsAny <string[]>(), It.IsAny <CancellationToken>()), Times.Never); }
public async Task NewCertificateShouldUpdateTargetResource() { var config = TestHelper.LoadConfig("config"); var auth = new Mock <IAuthenticationService>(); var certBuilder = new Mock <ICertificateBuilder>(); var ctx = new Mock <IAcmeContext>(); var orderContext = new Mock <IOrderContext>(); var authContext = new AuthenticationContext(ctx.Object, config.Acme); var parser = new Mock <IRenewalOptionParser>(); var log = new Mock <ILogger <RenewalService> >(); IRenewalService service = new RenewalService(auth.Object, parser.Object, certBuilder.Object, log.Object); var certStore = new Mock <ICertificateStore>(); var challengeResponder = new Mock <IChallengeResponder>(); var challengeContext = new Mock <IChallengeContext>(); var challenge = new Challenge { Status = ChallengeStatus.Valid }; var fakeChain = new CertificateChain("fakeChain"); var pfxBytes = new byte[] { 0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF }; const string fakePassword = "******"; var targetResource = new Mock <ITargetResource>(); var cert = new Mock <ICertificate>(); // check if outdated (yes, not found) parser.Setup(x => x.ParseCertificateStore(config.Certificates[0])) .Returns(certStore.Object); // let's encrypt challenge auth.Setup(x => x.AuthenticateAsync(config.Acme, CancellationToken.None)) .Returns(Task.FromResult(authContext)); ctx.Setup(x => x.NewOrder(config.Certificates[0].HostNames, null, null)) .Returns(Task.FromResult(orderContext.Object)); parser.Setup(x => x.ParseChallengeResponderAsync(config.Certificates[0], CancellationToken.None)) .Returns(Task.FromResult(challengeResponder.Object)); challengeResponder.Setup(x => x.InitiateChallengesAsync(orderContext.Object, CancellationToken.None)) .Returns(Task.FromResult(new[] { challengeContext.Object })); challengeContext.Setup(x => x.Resource()) .Returns(Task.FromResult(challenge)); // save cert certBuilder.Setup(x => x.BuildCertificateAsync(orderContext.Object, config.Certificates[0], CancellationToken.None)) .Returns(Task.FromResult((pfxBytes, fakePassword))); certStore.Setup(x => x.UploadAsync(pfxBytes, fakePassword, config.Certificates[0].HostNames, CancellationToken.None)) .Returns(Task.FromResult(cert.Object)); // update azure resource parser.Setup(x => x.ParseTargetResource(config.Certificates[0])) .Returns(targetResource.Object); // actual run - must run through all steps as no existing cert is found var r = await service.RenewCertificateAsync(config.Acme, config.Certificates[0], CancellationToken.None); r.Should().Be(RenewalResult.Success); certStore.Verify(x => x.UploadAsync(pfxBytes, fakePassword, config.Certificates[0].HostNames, CancellationToken.None)); targetResource.Verify(x => x.UpdateAsync(cert.Object, CancellationToken.None), Times.Once); }
public async Task IfLatestCertificateIsNotInUseThenResourceShouldBeUpdated() { var config = TestHelper.LoadConfig("config"); var auth = new Mock <IAuthenticationService>(); var certBuilder = new Mock <ICertificateBuilder>(); var ctx = new Mock <IAcmeContext>(); var orderContext = new Mock <IOrderContext>(); var authContext = new AuthenticationContext(ctx.Object, config.Acme); var parser = new Mock <IRenewalOptionParser>(); var log = new Mock <ILogger <RenewalService> >(); IRenewalService service = new RenewalService(auth.Object, parser.Object, certBuilder.Object, log.Object); var certStore = new Mock <ICertificateStore>(); var targetResource = new Mock <ITargetResource>(); var validCert = new Mock <ICertificate>(); targetResource.SetupGet(x => x.SupportsCertificateCheck) .Returns(true); targetResource.Setup(x => x.IsUsingCertificateAsync(validCert.Object, It.IsAny <CancellationToken>())) .Returns(Task.FromResult(false)); validCert.SetupGet(x => x.Expires) .Returns(DateTime.Now.AddDays(config.Acme.RenewXDaysBeforeExpiry + 1)); validCert.SetupGet(x => x.HostNames) .Returns(config.Certificates[0].HostNames); // check if outdated (no, found and valid) parser.Setup(x => x.ParseCertificateStore(config.Certificates[0])) .Returns(certStore.Object); certStore.Setup(x => x.GetCertificateAsync(CancellationToken.None)) .Returns(Task.FromResult(validCert.Object)); // update azure resource parser.Setup(x => x.ParseTargetResource(config.Certificates[0])) .Returns(targetResource.Object); // actual run - must update resource with existing cert // as the cert is not yet in use var r = await service.RenewCertificateAsync(config.Acme, config.Certificates[0], CancellationToken.None); r.Should().Be(RenewalResult.Success); certStore.Verify(x => x.UploadAsync(It.IsAny <byte[]>(), It.IsAny <string>(), It.IsAny <string[]>(), It.IsAny <CancellationToken>()), Times.Never); targetResource.Verify(x => x.UpdateAsync(validCert.Object, CancellationToken.None), Times.Once); }
public async Task NewCertificateShouldBeIssuedWhenOverrideIsUsedAndDomainlistIsMatched() { var config = TestHelper.LoadConfig("config"); var auth = new Mock <IAuthenticationService>(); var certBuilder = new Mock <ICertificateBuilder>(); var ctx = new Mock <IAcmeContext>(); var orderContext = new Mock <IOrderContext>(); var authContext = new AuthenticationContext(ctx.Object, config.Acme); var parser = new Mock <IRenewalOptionParser>(); var log = new Mock <ILogger <RenewalService> >(); IRenewalService service = new RenewalService(auth.Object, parser.Object, certBuilder.Object, log.Object); var certStore = new Mock <ICertificateStore>(); var challengeResponder = new Mock <IChallengeResponder>(); var challengeContext = new Mock <IChallengeContext>(); var challenge = new Challenge { Status = ChallengeStatus.Valid }; var fakeChain = new CertificateChain("fakeChain"); var pfxBytes = new byte[] { 0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF }; const string fakePassword = "******"; var targetResource = new Mock <ITargetResource>(); var cert = new Mock <ICertificate>(); var validCert = new Mock <ICertificate>(); validCert.SetupGet(x => x.Expires) .Returns(DateTime.Now.AddDays(config.Acme.RenewXDaysBeforeExpiry + 1)); // check if outdated (no, found and valid) parser.Setup(x => x.ParseCertificateStore(config.Certificates[0])) .Returns(certStore.Object); certStore.Setup(x => x.GetCertificateAsync(CancellationToken.None)) .Returns(Task.FromResult(validCert.Object)); // let's encrypt challenge auth.Setup(x => x.AuthenticateAsync(config.Acme, CancellationToken.None)) .Returns(Task.FromResult(authContext)); ctx.Setup(x => x.NewOrder(config.Certificates[0].HostNames, null, null)) .Returns(Task.FromResult(orderContext.Object)); parser.Setup(x => x.ParseChallengeResponderAsync(config.Certificates[0], CancellationToken.None)) .Returns(Task.FromResult(challengeResponder.Object)); challengeResponder.Setup(x => x.InitiateChallengesAsync(orderContext.Object, CancellationToken.None)) .Returns(Task.FromResult(new[] { challengeContext.Object })); challengeContext.Setup(x => x.Resource()) .Returns(Task.FromResult(challenge)); // save cert certBuilder.Setup(x => x.BuildCertificateAsync(orderContext.Object, config.Certificates[0], CancellationToken.None)) .Returns(Task.FromResult((pfxBytes, fakePassword))); certStore.Setup(x => x.UploadAsync(pfxBytes, fakePassword, config.Certificates[0].HostNames, CancellationToken.None)) .Returns(Task.FromResult(cert.Object)); // update azure resource parser.Setup(x => x.ParseTargetResource(config.Certificates[0])) .Returns(targetResource.Object); // actual run - must run through everything despite valid cert due to override parameter config.Certificates[0].Overrides = new Overrides { ForceNewCertificates = true, DomainsToUpdate = new[] { "example.com" } }; var r = await service.RenewCertificateAsync(config.Acme, config.Certificates[0], CancellationToken.None); r.Should().Be(RenewalResult.Success); certStore.Verify(x => x.UploadAsync(pfxBytes, fakePassword, config.Certificates[0].HostNames, CancellationToken.None)); targetResource.Verify(x => x.UpdateAsync(cert.Object, CancellationToken.None), Times.Once); }
public async Task NoNewCertificateShouldBeIssuedWhenDomainListChangesOrder() { var config = TestHelper.LoadConfig("config"); var auth = new Mock <IAuthenticationService>(); var certBuilder = new Mock <ICertificateBuilder>(); var ctx = new Mock <IAcmeContext>(); var orderContext = new Mock <IOrderContext>(); var authContext = new AuthenticationContext(ctx.Object, config.Acme); var parser = new Mock <IRenewalOptionLoader>(); var log = new Mock <ILogger <RenewalService> >(); IRenewalService service = new RenewalService(auth.Object, parser.Object, certBuilder.Object, log.Object); var certStore = new Mock <ICertificateStore>(); var challengeResponder = new Mock <IChallengeResponder>(); var challengeContext = new Mock <IChallengeContext>(); var challenge = new Challenge { Status = ChallengeStatus.Valid }; var fakeChain = new CertificateChain("fakeChain"); var pfxBytes = new byte[] { 0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF }; const string fakePassword = "******"; var targetResource = new Mock <ITargetResource>(); var cert = new Mock <ICertificate>(); var validCert = new Mock <ICertificate>(); validCert.SetupGet(x => x.Expires) .Returns(DateTime.Now.AddDays(config.Acme.RenewXDaysBeforeExpiry + 1)); validCert.SetupGet(x => x.HostNames) // config expects root first but this should not cause cert reissuance .Returns(new[] { "www.example.com", "example.com" }); // check if outdated (no, found and valid) parser.Setup(x => x.ParseCertificateStore(config.Certificates[0])) .Returns(certStore.Object); certStore.Setup(x => x.GetCertificateAsync(CancellationToken.None)) .Returns(Task.FromResult(validCert.Object)); // let's encrypt challenge auth.Setup(x => x.AuthenticateAsync(config.Acme, CancellationToken.None)) .Returns(Task.FromResult(authContext)); ctx.Setup(x => x.NewOrder(config.Certificates[0].HostNames, null, null)) .Returns(Task.FromResult(orderContext.Object)); parser.Setup(x => x.ParseChallengeResponderAsync(config.Certificates[0], CancellationToken.None)) .Returns(Task.FromResult(challengeResponder.Object)); challengeResponder.Setup(x => x.InitiateChallengesAsync(orderContext.Object, CancellationToken.None)) .Returns(Task.FromResult(new[] { challengeContext.Object })); challengeContext.Setup(x => x.Resource()) .Returns(Task.FromResult(challenge)); // save cert certBuilder.Setup(x => x.BuildCertificateAsync(orderContext.Object, config.Certificates[0], CancellationToken.None)) .Returns(Task.FromResult((pfxBytes, fakePassword))); certStore.Setup(x => x.UploadAsync(pfxBytes, fakePassword, config.Certificates[0].HostNames, CancellationToken.None)) .Returns(Task.FromResult(cert.Object)); // update azure resource parser.Setup(x => x.ParseTargetResource(config.Certificates[0])) .Returns(targetResource.Object); var r = await service.RenewCertificateAsync(config.Acme, config.Certificates[0], CancellationToken.None); r.Should().Be(RenewalResult.NoChange); certStore.Verify(x => x.UploadAsync(pfxBytes, fakePassword, config.Certificates[0].HostNames, CancellationToken.None), Times.Never); targetResource.Verify(x => x.UpdateAsync(cert.Object, CancellationToken.None), Times.Never); }
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); } }