public async Task PostAddsProperHeader() { HttpRequestMessage httpRequestMessage = null; Mock <HttpMessageHandler> messageHandlerMock = new Mock <HttpMessageHandler>(MockBehavior.Strict) .AddDirectoryResponse() .AddNonce(); messageHandlerMock.Protected() .Setup <Task <HttpResponseMessage> >("SendAsync", ItExpr.Is <HttpRequestMessage>(x => x.RequestUri == TestHelpers.AcmeDirectory.NewAccount), ItExpr.IsAny <CancellationToken>()) .Returns((HttpRequestMessage request, CancellationToken token) => { httpRequestMessage = request; return(Task.FromResult(new HttpResponseMessage())); }) .Verifiable(); var httpClient = new HttpClient(messageHandlerMock.Object); var rsaKey = RSA.Create(); var acmeClient = new ACMEClient(TestHelpers.BaseUri, rsaKey, httpClient); await acmeClient.RegisterAsync(new[] { "*****@*****.**" }); // assert httpRequestMessage.Content.Headers.ContentType.MediaType.Should().Be("application/jose+json"); httpRequestMessage.Content.Headers.ContentType.CharSet.Should().BeNull(); }
public (ACMEClient, Mock <HttpClient>) Build() { if (rsaKey == null) { rsaKey = RSA.Create(); } var acmeClient = new ACMEClient(TestHelpers.BaseUri, rsaKey, restClient.Object); return(acmeClient, restClient); }
private static async Task <AuthorizationChallengeResponse> CompleteChallenge(ACMEClient acmeClient, AuthorizationChallenge challenge, string value) { try { return(await acmeClient.CompleteChallengeAsync(challenge.Url, challenge.Token, value)); } catch (Exception ex) { Console.WriteLine(ex.Message); } return(null); }
public (ACMEClient, Mock <IRestClient>) Build() { if (rsaKey == null) { rsaKey = RSA.Create(); } var restClientFactory = new Mock <IRestClientFactory>(); restClientFactory.Setup(rcf => rcf.CreateRestClient(It.IsAny <Jws>())).Returns(restClient.Object); var acmeClient = new ACMEClient(TestHelpers.baseUri.ToString(), rsaKey, accountId, restClientFactory.Object); return(acmeClient, restClient); }
public async Task GetsANonceIfNoneIsPresent() { Mock <HttpMessageHandler> messageHandlerMock = new Mock <HttpMessageHandler>(MockBehavior.Strict) .AddDirectoryResponse() .AddNonce(); messageHandlerMock.Protected() .Setup <Task <HttpResponseMessage> >("SendAsync", ItExpr.Is <HttpRequestMessage>(x => x.RequestUri == TestHelpers.AcmeDirectory.NewAccount), ItExpr.IsAny <CancellationToken>()) .Returns((HttpRequestMessage request, CancellationToken token) => Task.FromResult(new HttpResponseMessage())) .Verifiable(); var httpClient = new HttpClient(messageHandlerMock.Object); var rsaKey = RSA.Create(); var acmeClient = new ACMEClient(TestHelpers.BaseUri, rsaKey, httpClient); await acmeClient.RegisterAsync(new[] { "*****@*****.**" }); messageHandlerMock.VerifyAll(); }
private static async Task <Order> NewOrderAsync(ACMEClient acmeClient, IEnumerable <OrderIdentifier> domains) { try { Order result = await acmeClient.OrderAsync(domains); return(result); } catch (ACMEException ex) { Console.WriteLine(ex.Descriptor); Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine(ex.GetType()); Console.WriteLine(ex.Message); } return(null); }
public OrderDomains(Options options) { this.options = options; var serviceCollection = new ServiceCollection(); serviceCollection.AddHttpClient(); ServiceProvider services = serviceCollection.BuildServiceProvider(); IHttpClientFactory httpClientFactory = services.GetRequiredService <System.Net.Http.IHttpClientFactory>(); cloudflareClient = new CloudflareClientFactory(options.Username, options.ApiKey, new CloudflareRestClientFactory(httpClientFactory), CloudflareAPIEndpoint.V4Endpoint) .Create(); // RSA service provider var rsaCryptoServiceProvider = new RSACryptoServiceProvider(2048); if (string.IsNullOrEmpty(options.Key)) { Program.LogLine("Generating new key for ACME."); var exportKey = rsaCryptoServiceProvider.ExportCspBlob(true); var strKey = Convert.ToBase64String(exportKey); File.WriteAllText("acmekey.key", strKey); } else { var key = Convert.FromBase64String(File.ReadAllText(options.Key)); rsaCryptoServiceProvider.ImportCspBlob(key); } var rsaKey = RSA.Create(rsaCryptoServiceProvider.ExportParameters(true)); acmeClient = new ACMEClient( new Uri(options.Environment == AcmeEnvironment.ProductionV2 ? ACMEEnvironment.ProductionV2 : ACMEEnvironment.StagingV2), rsaKey, new HttpClient()); }
static async Task Main(string[] args) { Console.WriteLine("Kenc.ACMEClient example"); Console.WriteLine("USE AT OWN RISK"); Console.WriteLine("This program uses the LetsEncrypt STAGING environment by default"); Console.WriteLine("WARNING: Encryption key is stored UNPROTECTED in bin folder"); Console.WriteLine("============================================================\n\n"); // RSA service provider var rsaCryptoServiceProvider = new RSACryptoServiceProvider(2048); Console.WriteLine($"Looking for key {keyPath}"); var existingKey = File.Exists(keyPath); if (!existingKey) { Console.WriteLine("Key not found - generating"); var exportKey = rsaCryptoServiceProvider.ExportCspBlob(true); var strKey = Convert.ToBase64String(exportKey); File.WriteAllText(keyPath, strKey); } var key = Convert.FromBase64String(File.ReadAllText(keyPath)); rsaCryptoServiceProvider.ImportCspBlob(key); var rsaKey = RSA.Create(rsaCryptoServiceProvider.ExportParameters(true)); var acmeClient = new ACMEClient(ACMEEnvironment.StagingV2, rsaKey, string.Empty, new RestClientFactory()); var directory = await acmeClient.InitializeAsync(); Account account = null; if (existingKey) { Console.WriteLine("Validating if user exists with existing key"); try { account = await acmeClient.GetAccountAsync(); } catch (AccountDoesNotExistException exception) { Console.WriteLine(exception.Message); } catch (Exception exception) { Console.WriteLine($"Exception encounted while looking up client: {exception.Message}"); } if (account != null) { Console.WriteLine($"Using previously created account {account.Id}"); } else { Console.WriteLine("Couldn't retrieve existing account. Creating new account."); } } if (account == null) { Console.WriteLine("Creating user.."); Console.WriteLine("By creating this user, you acknowledge the terms of service: {0}", directory.Meta.TermsOfService); Console.Write("Enter email address for user: "******"mailto:" + userContact }); } catch (Exception ex) { Console.WriteLine("An error occured while registering user. {0}", ex.Message); throw; } } Console.WriteLine("Enter domain to validate and request certificate for:"); var domainName = Console.ReadLine(); if (string.IsNullOrWhiteSpace(domainName)) { Console.WriteLine("Invalid domain aborting."); return; } var domainTask = OrderDomains(acmeClient, domainName); domainTask.Wait(); }
static async Task OrderDomains(ACMEClient acmeClient, params string[] domainNames) { var domains = domainNames.Select(domain => new OrderIdentifier { Type = ChallengeType.DNSChallenge, Value = domain }); Uri[] validations = null; Order order = null; while (order == null) { order = await NewOrderAsync(acmeClient, domains); if (order == null) { Console.WriteLine("Failed.. retrying"); await Task.Delay(5000); } } // todo: save order identifier Console.WriteLine($"Order location: {order.Location}"); validations = order.Authorizations; var auths = await RetrieveAuthz(acmeClient, validations); foreach (var item in auths) { foreach (var challenge in item.Challenges) { Console.WriteLine($"Challenge: {challenge.Url}"); Console.WriteLine($"Type: {challenge.Type}"); Console.WriteLine($"Token: {challenge.Token}"); Console.WriteLine($"Value: {challenge.AuthorizationToken}"); if (challenge.Type == "http-01") { File.WriteAllText(challenge.Token, challenge.AuthorizationToken); Console.WriteLine($"File saved as: {challenge.Token} in working directory."); Console.WriteLine($"Please upload the file to {domainNames.First()}/.well-known/acme-challenge/{challenge.Token}"); } else if (challenge.Type == "dns-01") { Console.WriteLine($"Please create a text entry in the DNS records for each domain in {string.Join(',', domainNames)} using {challenge.Token}"); } else { Console.WriteLine($"Unknown challenge type encountered '{challenge.Type}'. Please handle accourdingly."); } var result = HandleConsoleInput("Challenge completed? [y/n]", new[] { "y", "yes", "n", "no" }); if (result == "y" || result == "yes") { Console.WriteLine("Validating challenge"); var validation = await ValidateChallengeCompletion(challenge, domainNames); if (validation.Any(validationItem => !validationItem.Value)) { Console.WriteLine($"The following domains failed validation: " + $"{string.Join(',', validation.Where(vItem => !vItem.Value).Select(vItem => vItem.Key))}"); } if (validation.Any(validationItem => validationItem.Value)) { var c = await CompleteChallenge(acmeClient, challenge, challenge.AuthorizationToken); if (c != null) { Console.WriteLine(c.Status); } } } else { Console.WriteLine("Skipping challenge"); } } } // todo: add a proper check to see if all validations passed. order = await acmeClient.UpdateOrderAsync(order); Console.WriteLine($"Order status:{order.Status}"); // todo: if(order.Status == failed...) while (order.Status == Order.Processing) { Thread.Sleep(500); Console.WriteLine("Order status = processing; updating.."); order = await acmeClient.UpdateOrderAsync(order); } var certKey = new RSACryptoServiceProvider(4096); SaveRSAKeyToFile(certKey, $"{order.Identifiers[0].Value}.key"); Order certOrder = null; try { certOrder = await acmeClient.RequestCertificateAsync(order, certKey); while (certOrder.Status == Order.Processing) { Thread.Sleep(500); Console.WriteLine("Order status = processing; updating.."); certOrder = await acmeClient.UpdateOrderAsync(certOrder); } Console.WriteLine(certOrder.Status); } catch (Exception ex) { Console.WriteLine(ex.Message); } var cert = await acmeClient.GetCertificateAsync(certOrder); var certdata = cert.Export(X509ContentType.Cert); var publicKeyFilename = $"{certOrder.Identifiers[0].Value}.crt"; File.WriteAllBytes(publicKeyFilename, certdata); Console.WriteLine($"Public certificate written to file {publicKeyFilename}"); // combine the two! var properCert = cert.CopyWithPrivateKey(certKey); var pfxData = cert.Export(X509ContentType.Pfx); var privateKeyFilename = $"{certOrder.Identifiers[0].Value}.pfx"; File.WriteAllBytes(privateKeyFilename, pfxData); Console.WriteLine($"Private certificate written to file {privateKeyFilename}"); }
private static async Task <AuthorizationChallengeResponse> CompleteChallenge(ACMEClient acmeClient, AuthorizationChallenge challenge) { return(await acmeClient.CompleteChallengeAsync(challenge.Url, challenge.Token, challenge.AuthorizationToken)); }
private static async Task <IEnumerable <AuthorizationChallengeResponse> > RetrieveAuthz(ACMEClient acmeClient, Uri[] uris) { var challenges = new List <AuthorizationChallengeResponse>(); foreach (Uri uri in uris) { try { AuthorizationChallengeResponse result = await acmeClient.GetAuthorizationChallengeAsync(uri); challenges.Add(result); } catch (Exception ex) { Console.WriteLine(ex.Message); } } return(challenges); }
private static async Task <AuthorizationChallengeResponse> NewAuthorizationAsync(ACMEClient acmeClient, string domain) { return(await acmeClient.NewAuthorizationAsync(domain)); }
private static async Task <Order> NewOrderAsync(ACMEClient acmeClient, IEnumerable <OrderIdentifier> domains) { Order result = await acmeClient.OrderAsync(domains); return(result); }
private static async Task Main() { Console.WriteLine("Kenc.ACMEClient example"); Console.WriteLine("USE AT OWN RISK"); Console.WriteLine("This program uses the LetsEncrypt STAGING environment by default"); Console.WriteLine("WARNING: Encryption key is stored UNPROTECTED in bin folder"); Console.WriteLine("============================================================\n\n"); // RSA service provider var rsaCryptoServiceProvider = new RSACryptoServiceProvider(2048); Console.WriteLine($"Looking for key {keyPath}"); var existingKey = File.Exists(keyPath); if (!existingKey) { Console.WriteLine("Key not found - generating"); var exportKey = rsaCryptoServiceProvider.ExportCspBlob(true); var strKey = Convert.ToBase64String(exportKey); File.WriteAllText(keyPath, strKey); } var key = Convert.FromBase64String(File.ReadAllText(keyPath)); rsaCryptoServiceProvider.ImportCspBlob(key); var rsaKey = RSA.Create(rsaCryptoServiceProvider.ExportParameters(true)); var acmeClient = new ACMEClient(new Uri(ACMEEnvironment.StagingV2), rsaKey, new HttpClient()); ACMEDirectory directory = await acmeClient.InitializeAsync(); Account account = null; if (existingKey) { Console.WriteLine("Validating if user exists with existing key"); try { account = await acmeClient.GetAccountAsync(); } catch (AccountDoesNotExistException exception) { Console.WriteLine(exception.Message); } catch (Exception exception) { Console.WriteLine($"Exception encounted while looking up client: {exception.Message}"); } if (account != null) { Console.WriteLine($"Using previously created account {account.Id}"); } else { Console.WriteLine("Couldn't retrieve existing account. Creating new account."); } } if (account == null) { Console.WriteLine("Creating user.."); Console.WriteLine("By creating this user, you acknowledge the terms of service: {0}", directory.Meta.TermsOfService); Console.Write("Enter email address for user: "******"mailto:" + userContact }); } catch (Exception ex) { Console.WriteLine("An error occured while registering user. {0}", ex.Message); throw; } } Console.WriteLine("Enter domain(s) to validate and request certificate(s) for (wildcard? add wildcard and tld comma separated):"); var domainNames = Console.ReadLine().Split(',').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray(); if (!domainNames.Any()) { Console.WriteLine("Invalid domain aborting."); return; } Task domainTask = OrderDomains(acmeClient, domainNames); domainTask.Wait(); // enumerate all certs var certificateFiles = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.crt").ToList(); Console.WriteLine($"Revoking certificates: {string.Join(',', certificateFiles)}"); var foo = HandleConsoleInput("Continue?", new[] { "y", "yes", "n", "no" }, false).ToLower(); if (foo == "y" || foo == "yes") { IEnumerable <X509Certificate> certificates = certificateFiles.Select(path => X509Certificate2.CreateFromCertFile(path)); foreach (X509Certificate certificate in certificates) { try { await acmeClient.RevokeCertificateAsync(certificate, RevocationReason.Superseded); Console.WriteLine($"{certificate.Subject} revoked."); } catch (ACMEException exception) when(exception.Descriptor == "urn:ietf:params:acme:error:alreadyRevoked") { Console.WriteLine(exception.Message); } catch (ACMEException exception) { Console.WriteLine(exception.Message); throw; } } Console.WriteLine("Completed"); } }
private static async Task OrderDomains(ACMEClient acmeClient, params string[] domainNames) { IEnumerable <OrderIdentifier> domains = domainNames.Select(domain => new OrderIdentifier { Type = ChallengeType.DNSChallenge, Value = domain }); Uri[] validations = null; Order order = null; while (order == null) { order = await NewOrderAsync(acmeClient, domains); if (order == null) { Console.WriteLine("Failed.. retrying"); await Task.Delay(5000); } } // todo: save order identifier Console.WriteLine($"Order location: {order.Location}"); validations = order.Authorizations; IEnumerable <AuthorizationChallengeResponse> auths = await RetrieveAuthz(acmeClient, validations); foreach (AuthorizationChallengeResponse item in auths) { if (item.Status == ACMEStatus.Valid) { Console.WriteLine("Domain already validated succesfully."); continue; } AuthorizationChallenge validChallenge = item.Challenges.Where(challenge => challenge.Status == ACMEStatus.Valid).FirstOrDefault(); if (validChallenge != null) { Console.WriteLine("Found a valid challenge, skipping domain."); Console.WriteLine(validChallenge.Type); continue; } IEnumerable <AuthorizationChallenge> applicableChallenges = item.Wildcard ? item.Challenges.Where(x => x.Type == "dns-01") : item.Challenges; foreach (AuthorizationChallenge challenge in applicableChallenges) { Console.WriteLine($"Status: {challenge.Status}"); Console.WriteLine($"Challenge: {challenge.Url}"); Console.WriteLine($"Type: {challenge.Type}"); Console.WriteLine($"Token: {challenge.Token}"); Console.WriteLine($"Value: {challenge.AuthorizationToken}"); if (challenge.Type == "http-01") { File.WriteAllText(challenge.Token, challenge.AuthorizationToken); Console.WriteLine($"File saved as: {challenge.Token} in working directory."); Console.WriteLine($"Please upload the file to {item.Identifier.Value}/.well-known/acme-challenge/{challenge.Token}"); } else if (challenge.Type == "dns-01") { Console.WriteLine($"Please create a text entry for _acme-challenge.{item.Identifier.Value} with value: {challenge.AuthorizationToken}"); } else { Console.WriteLine($"Unknown challenge type encountered '{challenge.Type}'. Please handle accourdingly."); } var result = HandleConsoleInput("Challenge completed? [y/n]", new[] { "y", "yes", "n", "no" }); if (result == "y" || result == "yes") { Console.WriteLine("Validating challenge"); var validation = await ValidateChallengeCompletion(challenge, item.Identifier.Value); if (validation) { AuthorizationChallengeResponse c = await CompleteChallenge(acmeClient, challenge, challenge.AuthorizationToken); while (c.Status == ACMEStatus.Pending) { await Task.Delay(5000); c = await acmeClient.GetAuthorizationChallengeAsync(challenge.Url); } Console.WriteLine($"Challenge Status: {c.Status}"); if (c.Status == ACMEStatus.Valid) { // no reason to keep going, we have one succesfull challenge! break; } } else { Console.WriteLine($"Validation failed for {item.Identifier.Value}"); } } else { Console.WriteLine("Skipping challenge"); } } } foreach (Uri challenge in order.Authorizations) { AuthorizationChallengeResponse c; do { c = await acmeClient.GetAuthorizationChallengeAsync(challenge); }while (c == null || c.Status == ACMEStatus.Pending); if (c.Status == ACMEStatus.Invalid) { Console.WriteLine($"Failed to validate domain {c.Identifier.Value}. Aborting"); return; } } order = await acmeClient.UpdateOrderAsync(order); Console.WriteLine($"Order status:{order.Status}"); while (order.Status == ACMEStatus.Processing) { Thread.Sleep(500); Console.WriteLine("Order status = processing; updating.."); order = await acmeClient.UpdateOrderAsync(order); } var certKey = new RSACryptoServiceProvider(4096); SaveRSAKeyToFile(certKey, $"{FixFilename(order.Identifiers[0].Value)}.key"); Order certOrder = null; try { certOrder = await acmeClient.RequestCertificateAsync(order, certKey); while (certOrder.Status == ACMEStatus.Processing) { Thread.Sleep(500); Console.WriteLine("Order status = processing; updating.."); certOrder = await acmeClient.UpdateOrderAsync(certOrder); } Console.WriteLine(certOrder.Status); } catch (Exception ex) { Console.WriteLine(ex.Message); return; } X509Certificate2 cert = await acmeClient.GetCertificateAsync(certOrder); var certdata = cert.Export(X509ContentType.Cert); var publicKeyFilename = $"{FixFilename(certOrder.Identifiers[0].Value)}.crt"; File.WriteAllBytes(publicKeyFilename, certdata); Console.WriteLine($"Public certificate written to file {publicKeyFilename}"); // combine the two! X509Certificate2 properCert = cert.CopyWithPrivateKey(certKey); Console.WriteLine("Enter password to secure PFX"); System.Security.SecureString password = PasswordInput.ReadPassword(); var pfxData = properCert.Export(X509ContentType.Pfx, password); var privateKeyFilename = $"{FixFilename(certOrder.Identifiers[0].Value)}.pfx"; File.WriteAllBytes(privateKeyFilename, pfxData); Console.WriteLine($"Private certificate written to file {privateKeyFilename}"); }