public Challenge Decode(IdentifierPart ip, ChallengePart cp, ISigner signer) { if (cp.Type != AcmeProtocol.CHALLENGE_TYPE_HTTP) throw new InvalidDataException("unsupported Challenge type") .With("challengeType", cp.Type) .With("supportedChallengeTypes", AcmeProtocol.CHALLENGE_TYPE_HTTP); //var token = (string)cp["token"]; var token = cp.Token; // This response calculation is described in: // https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.2 var keyAuthz = JwsHelper.ComputeKeyAuthorization(signer, token); var path = $"{AcmeProtocol.HTTP_CHALLENGE_PATHPREFIX}{token}"; var url = $"http://{ip.Value}/{path}"; var ca = new HttpChallengeAnswer { KeyAuthorization = keyAuthz, }; var c = new HttpChallenge(cp.Type, ca) { Token = token, FileUrl = url, FilePath = path, FileContent = keyAuthz, }; return c; }
private string GetAnswerPath(HttpChallenge httpChallenge) { // We need to strip off any leading '/' in the path var filePath = httpChallenge.FilePath; if (filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { filePath = filePath.Substring(1); } var answerPath = Environment.ExpandEnvironmentVariables(Path.Combine(WebRootPath(), filePath)); return(answerPath); }
private async Task <string> GetAnswerPath(HttpChallenge httpChallenge) { var rootDir = await this.pathProvider.WebRootPath(false); // We need to strip off any leading '/' in the path var filePath = httpChallenge.FilePath; if (filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { filePath = filePath.Substring(1); } var answerPath = Environment.ExpandEnvironmentVariables(Path.Combine(rootDir, filePath)); return(answerPath); }
private async Task <string> GetAnswerPath(HttpChallenge httpChallenge) { var root = await this.pathProvider.WebRootPath(true); // We need to strip off any leading '/' in the path var filePath = httpChallenge.FilePath; if (filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { filePath = filePath.Substring(1); } var answerPath = root + "/" + filePath; return(answerPath); }
/// <summary> /// Gets challenges used to verify domain ownership. /// </summary> /// <param name="account">Existing account.</param> /// <param name="acmeCertificateFulfillmentPromise">The certificate fulfillment promise retrieved from the RequestCertificate call.</param> /// <param name="challengeType">The challenge type expected back.</param> /// <returns>Challenge used to verify domain ownership</returns> /// <remarks>If requesting a challenge for a wildcard domain, only dns challenge is supported.</remarks> /// <exception cref="NotSupportedException">If the challenge type is not supported.</exception> /// <exception cref="AcmeProtocolException">On all other Acme related exceptions</exception> public async Task <ChallengeCollection> GetChallengesAsync(AcmeAccount account, AcmeCertificateFulfillmentPromise acmeCertificateFulfillmentPromise, ChallengeType challengeType) { var response = await _acmeApi.GetChallengesAsync(acmeCertificateFulfillmentPromise); var errorResponse = response.Where(t => t.Status == AcmeApiResponseStatus.Error); if (errorResponse.Any()) { throw new AcmeProtocolException(string.Join(" | ", errorResponse.Select(t => t.Message))); } ChallengeCollection challenges = new ChallengeCollection(); foreach (var resp in response) { AcmeChallenge sChallenge = resp.Data.Challenges.FirstOrDefault(t => t.Type.Equals(challengeType.Value)); if (sChallenge == null) { throw new NotSupportedException($"{challengeType.Value} challenge type not supported in this context."); } IAcmeChallengeContent challengeContent = null; switch (challengeType.Value) { case ProtoacmeContants.CHALLENGE_HTTP: challengeContent = new HttpChallenge(account, sChallenge, resp.Data.Identifier?.Value); challenges.Add(challengeContent); break; case ProtoacmeContants.CHALLENGE_DNS: challengeContent = new DnsChallenge(account, sChallenge, resp.Data.Identifier?.Value); challenges.Add(challengeContent); break; case ProtoacmeContants.CHALLENGE_TLS: challengeContent = new TlsChallenge(account, sChallenge, resp.Data.Identifier?.Value); challenges.Add(challengeContent); break; default: break; } } return(challenges); }
/// <summary> /// Can be used to write out server specific configuration, to handle extensionless files etc. /// </summary> /// <param name="target"></param> /// <param name="answerPath"></param> /// <param name="token"></param> public virtual void BeforeAuthorize(Target target, HttpChallenge challenge) { if (target.IIS == true) { _log.Debug("Writing web.config"); var destination = CombinePath(target.WebRootPath, challenge.FilePath.Replace(challenge.Token, "web.config")); var content = GetWebConfig(target); if (FileExists(destination)) { var existingContent = ReadFile(destination); if (existingContent == content) { // do not touch web.config if it has allredy the required content. return; } } WriteFile(destination, content); } }
private void EditFile(HttpChallenge httpChallenge, bool delete) { var filePath = httpChallenge.FilePath; // We need to strip off any leading '/' in the path or // else it creates a path with an empty leading segment if (filePath.StartsWith("/")) { filePath = filePath.Substring(1); } using (var s3 = new Amazon.S3.AmazonS3Client( CommonParams.ResolveCredentials(), CommonParams.RegionEndpoint)) { if (delete) { var s3Requ = new Amazon.S3.Model.DeleteObjectRequest { BucketName = BucketName, Key = filePath, }; var s3Resp = s3.DeleteObject(s3Requ); } else { var s3Requ = new Amazon.S3.Model.PutObjectRequest { BucketName = BucketName, Key = filePath, ContentBody = httpChallenge.FileContent, ContentType = ContentType, CannedACL = S3CannedAcl, }; var s3Resp = s3.PutObject(s3Requ); } } }
private async Task <CloudBlockBlob> GetBlob(HttpChallenge challenge) { var client = storageAccount.CreateCloudBlobClient(); var container = client.GetContainerReference(containerName); await container.CreateIfNotExistsAsync(); if (!"$web".Equals(containerName, StringComparison.InvariantCultureIgnoreCase) && container.Properties.PublicAccess != BlobContainerPublicAccessType.Blob) { await container.SetPermissionsAsync(new BlobContainerPermissions() { PublicAccess = BlobContainerPublicAccessType.Blob }); } // We need to strip off any leading '/' in the path var filePath = challenge.FilePath; if (filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { filePath = filePath.Substring(1); } var blob = container.GetBlockBlobReference(filePath); return(blob); }
public override Task CleanupChallengeFile(HttpChallenge challenge) { File.Delete(GetAnswerPath(challenge)); return(Task.CompletedTask); }
public override Task CleanupChallengeFile(HttpChallenge challenge) { return(Task.CompletedTask); }
public override async Task PersistsChallengeFile(HttpChallenge challenge) { var answerPath = GetAnswerPath(challenge); await WriteFile(answerPath, challenge.FileContent); }
private void EditFile(HttpChallenge httpChallenge, bool delete, TextWriter msg) { IisWebSiteBinding site = IisHelper.ResolveSingleSite(WebSiteRef, IisHelper.ListDistinctHttpWebSites()); var siteRoot = site.SiteRoot; if (!string.IsNullOrEmpty(OverrideSiteRoot)) { siteRoot = OverrideSiteRoot; } if (string.IsNullOrEmpty(siteRoot)) { throw new InvalidOperationException("missing root path for resolve site") .With(nameof(IisWebSiteBinding.SiteId), site.SiteId) .With(nameof(IisWebSiteBinding.SiteName), site.SiteName); } // IIS-configured Site Root can use env vars siteRoot = Environment.ExpandEnvironmentVariables(siteRoot); // Make sure we're using the canonical full path siteRoot = Path.GetFullPath(siteRoot); // We need to strip off any leading '/' in the path var filePath = httpChallenge.FilePath; if (filePath.StartsWith("/")) { filePath = filePath.Substring(1); } var fullFilePath = Path.Combine(siteRoot, filePath); var fullDirPath = Path.GetDirectoryName(fullFilePath); var fullConfigPath = Path.Combine(fullDirPath, "web.config"); // This meta-data file will be placed next to the actual // Challenge answer content file and it captures some details // that we need in order to properly clean up the handling of // this Challenge after it has been submitted var fullMetaPath = $"{fullFilePath}-acmesharp_meta"; // Check if user is running with elevated privs and warn if not if (!IisHelper.IsAdministrator()) { Console.Error.WriteLine("WARNING: You are not running with elelvated privileges."); Console.Error.WriteLine(" Write access may be denied to the destination."); } if (delete) { bool skipLocalWebConfig = SkipLocalWebConfig; List <string> dirsCreated = null; // First see if there's a meta file there to help us out if (File.Exists(fullMetaPath)) { var meta = JsonHelper.Load <IisChallengeHandlerMeta>( File.ReadAllText(fullMetaPath)); skipLocalWebConfig = meta.SkippedLocalWebConfig; dirsCreated = meta.DirsCreated; } // Get rid of the Challenge answer content file if (File.Exists(fullFilePath)) { File.Delete(fullFilePath); msg.WriteLine("* Challenge response content has been removed from local file path"); msg.WriteLine(" at: [{0}]", fullFilePath); } // Get rid of web.config if necessary if (!skipLocalWebConfig && File.Exists(fullConfigPath)) { File.Delete(fullConfigPath); msg.WriteLine("* Local web.config has been removed from local file path"); msg.WriteLine(" at: [{0}]", fullFilePath); } // Get rid of the meta file so that we can clean up the dirs if (File.Exists(fullMetaPath)) { File.Delete(fullMetaPath); } // Walk up the tree if needed if (dirsCreated?.Count > 0) { var dirsDeleted = new List <string>(); dirsCreated.Reverse(); foreach (var dir in dirsCreated) { if (Directory.Exists(dir)) { if (Directory.GetFileSystemEntries(dir).Length == 0) { Directory.Delete(dir); dirsDeleted.Add(dir); } } } if (dirsDeleted.Count > 0) { msg.WriteLine("* Removed the following directories:"); foreach (var dd in dirsDeleted) { msg.WriteLine(" - [{0}]", dd); } } } } else { // Figure out which dirs we have to create so // we can capture and clean it up later on var meta = new IisChallengeHandlerMeta { WasAdmin = IisHelper.IsAdministrator(), SkippedLocalWebConfig = SkipLocalWebConfig, DirsCreated = new List <string>(), }; // In theory this ascending of the dir path should work // just fine, but just in case, this path segment counter // should gaurd against the possibility of an infinite loop var dirLimit = 100; var testDir = fullDirPath; while (!Directory.Exists(testDir)) { // Sanity check against an infinite loop if (--dirLimit <= 0) { throw new Exception("Unexpected directory path segment count reached") .With(nameof(dirLimit), "100") .With(nameof(fullDirPath), fullDirPath) .With(nameof(testDir), testDir) .With($"first-{nameof(meta.DirsCreated)}", meta.DirsCreated[0]) .With($"last-{nameof(meta.DirsCreated)}", meta.DirsCreated[meta.DirsCreated.Count - 1]); } if (Path.GetFullPath(testDir) == siteRoot) { break; } // Add to the top of the list meta.DirsCreated.Insert(0, testDir); // Move to the parent testDir = Path.GetDirectoryName(testDir); } foreach (var dir in meta.DirsCreated) { Directory.CreateDirectory(dir); } File.WriteAllText(fullFilePath, httpChallenge.FileContent); File.WriteAllText(fullMetaPath, JsonHelper.Save(meta)); msg.WriteLine("* Challenge response content has been written to local file path"); msg.WriteLine(" at: [{0}]", fullFilePath); msg.WriteLine("* Challenge response should be accessible with a MIME type of [text/json]"); msg.WriteLine(" at: [{0}]", httpChallenge.FileUrl); if (!SkipLocalWebConfig) { var t = typeof(IisChallengeHandler); var r = $"{t.Namespace}.{t.Name}-WebConfig"; using (Stream rs = t.Assembly.GetManifestResourceStream(r)) { using (var fs = new FileStream(fullConfigPath, FileMode.Create)) { rs.CopyTo(fs); } } msg.WriteLine("* Local web.config has been created to serve response file as JSON"); msg.WriteLine(" however, you may need to adjust this file for your environment"); msg.WriteLine(" at: [{0}]", httpChallenge.FileUrl); } else { msg.WriteLine("* Local web.config file creation has been skipped!"); msg.WriteLine(" You may need to manually adjust your configuration to serve the"); msg.WriteLine(" Challenge Response file with a MIME type of [text/json]"); } } }
private void ChallengeGetCertAndInstall(List <SiteBinding> sites) { AuthorizationState authorizationState = null; AuthorizeChallenge authorizeChallenge = null; HttpChallenge httpChallenge = null; Log("\tChallengeGetCertAndInstall started"); Log("\tChallengeGetCertAndInstall total sites: {0}", sites.Count); foreach (var site in sites) { Log("\t\tChallengeGetCertAndInstall * procesing {0}", site.BindingHost); try { authorizationState = this.client.AuthorizeIdentifier(site.BindingHost); Log("\t\tChallengeGetCertAndInstall authorizationState ok"); } catch (Exception exception) { Log("\t\tChallengeGetCertAndInstall Error AuthorizeIdentifier {0}", exception.Message); continue; } try { authorizeChallenge = this.client.DecodeChallenge(authorizationState, AcmeProtocol.CHALLENGE_TYPE_HTTP); } catch (Exception exception) { Log("\t\tChallengeGetCertAndInstall Error DecodeChallenge {0}", exception.Message); continue; } if (authorizeChallenge == null) { Log("\t\tChallengeGetCertAndInstall authorizeChallenge == null"); continue; } httpChallenge = authorizeChallenge.Challenge as HttpChallenge; if (httpChallenge == null) { Log("\t\tChallengeGetCertAndInstall httpChallenge == null"); continue; } var challengePath = Path.Combine(site.Path, httpChallenge.FilePath.Replace('/', '\\')); if (!CreateChallenge(challengePath, httpChallenge.FileContent)) { continue; } if (!CreateWebConfig(challengePath)) { continue; } // warmup? try { using (var wc = new WebClient()) { var response = wc.DownloadString(httpChallenge.FileUrl); if (httpChallenge.FileContent != response) { Log("\t\tChallengeGetCertAndInstall error FileContent != response for {0}", site.BindingHost); continue; } } } catch (Exception exception) { Log("\t\tChallengeGetCertAndInstall DownloadString {0} Error {1}", httpChallenge.FileUrl, exception.Message); continue; } Log("\t\tChallengeGetCertAndInstall Challenge placed for {0}", site.BindingHost); authorizationState.Challenges = new AuthorizeChallenge[] { authorizeChallenge }; this.client.SubmitChallengeAnswer(authorizationState, AcmeProtocol.CHALLENGE_TYPE_HTTP, true); for (int intI = 0; intI < 60; intI++) { System.Threading.Thread.Sleep(1000); var newAuthorizationState = this.client.RefreshIdentifierAuthorization(authorizationState); if (newAuthorizationState.Status != "pending") { authorizationState = newAuthorizationState; break; } } if (DeleteWebConfig(challengePath)) { Log("\t\tChallengeGetCertAndInstall WebConfig deleted for {0}", site.BindingHost); } if (DeleteChallenge(challengePath)) { Log("\t\tChallengeGetCertAndInstall Challenge deleted for {0}", site.BindingHost); } switch (authorizationState.Status) { default: Log("\t\tChallengeGetCertAndInstall unknown status {0}", authorizationState.Status); break; case "pending": Log("\t\tChallengeGetCertAndInstall error pending"); break; case "invalid": Log("\t\tChallengeGetCertAndInstall error invalid"); break; case "valid": Log("\t\tChallengeGetCertAndInstall valid for {0}", site.BindingHost); if (InstallCertificate(site.BindingHost)) { Log("\t\tChallengeGetCertAndInstall ready {0}", site.BindingHost); } break; } } Log("\tChallengeGetCertAndInstall ended"); }
/// <summary> /// Should create any directory structure needed and write the file for authorization /// </summary> /// <param name="answerPath">where the answerFile should be located</param> /// <param name="fileContents">the contents of the file to write</param> public virtual void CreateAuthorizationFile(Target target, HttpChallenge challenge) { WriteFile(CombinePath(target.WebRootPath, challenge.FilePath), challenge.FileContent); }
/// <summary> /// Handle clean-up steps /// </summary> /// <param name="options"></param> /// <param name="target"></param> /// <param name="challenge"></param> private void Cleanup(Target target, HttpChallenge challenge) { BeforeDelete(target, challenge); DeleteAuthorization(target, challenge); }
public override void BeforeAuthorize(Target target, HttpChallenge challenge) { _iisClient.PrepareSite(target); base.BeforeAuthorize(target, challenge); }
static void Upload(Uri ftp, NetworkCredential credentials, HttpChallenge challenge) { Upload(ftp, credentials, challenge.FileContent); }
public override void BeforeDelete(Target target, HttpChallenge challenge) { _iisClient.UnprepareSite(target); base.BeforeDelete(target, challenge); }
public abstract Task CleanupChallengeFile(HttpChallenge challenge);
private static async Task Process(bool Verbose, Uri Directory, string[] ContactURLs, bool TermsOfServiceAgreed, bool NewKey, string[] DomainNames, DateTime?NotBefore, DateTime?NotAfter, string HttpRootFolder, int PollingInterval, int KeySize, string EMail, string Country, string Locality, string StateOrProvince, string Organization, string OrganizationalUnit, string FileName, string Password) { using (AcmeClient Client = new AcmeClient(Directory)) { Log.Informational("Connecting to directory.", new KeyValuePair <string, object>("URL", Directory.ToString())); AcmeDirectory AcmeDirectory = await Client.GetDirectory(); if (AcmeDirectory.ExternalAccountRequired) { Log.Warning("An external account is required."); } if (AcmeDirectory.TermsOfService != null) { Log.Informational("Terms of service available.", new KeyValuePair <string, object>("URL", AcmeDirectory.TermsOfService.ToString())); } if (AcmeDirectory.Website != null) { Log.Informational("Web site available.", new KeyValuePair <string, object>("URL", AcmeDirectory.Website.ToString())); } Log.Informational("Getting account."); AcmeAccount Account; try { Account = await Client.GetAccount(); Log.Informational("Account found.", new KeyValuePair <string, object>("Created", Account.CreatedAt), new KeyValuePair <string, object>("Initial IP", Account.InitialIp), new KeyValuePair <string, object>("Status", Account.Status), new KeyValuePair <string, object>("Contact", Account.Contact)); if (ContactURLs != null && !AreEqual(Account.Contact, ContactURLs)) { Log.Informational("Updating contact URIs in account."); Account = await Account.Update(ContactURLs); Log.Informational("Account updated.", new KeyValuePair <string, object>("Created", Account.CreatedAt), new KeyValuePair <string, object>("Initial IP", Account.InitialIp), new KeyValuePair <string, object>("Status", Account.Status), new KeyValuePair <string, object>("Contact", Account.Contact)); } } catch (AcmeAccountDoesNotExistException) { Log.Warning("Account not found. Creating account.", new KeyValuePair <string, object>("Contact", ContactURLs), new KeyValuePair <string, object>("TermsOfServiceAgreed", TermsOfServiceAgreed)); Account = await Client.CreateAccount(ContactURLs, TermsOfServiceAgreed); Log.Informational("Account created.", new KeyValuePair <string, object>("Created", Account.CreatedAt), new KeyValuePair <string, object>("Initial IP", Account.InitialIp), new KeyValuePair <string, object>("Status", Account.Status), new KeyValuePair <string, object>("Contact", Account.Contact)); } if (NewKey) { Log.Informational("Generating new key."); await Account.NewKey(); Log.Informational("New key generated."); } if (DomainNames != null) { if (!string.IsNullOrEmpty(HttpRootFolder)) { CheckExists(HttpRootFolder); HttpRootFolder = Path.Combine(HttpRootFolder, ".well-known"); CheckExists(HttpRootFolder); HttpRootFolder = Path.Combine(HttpRootFolder, "acme-challenge"); CheckExists(HttpRootFolder); } Log.Informational("Creating order."); AcmeOrder Order = await Account.OrderCertificate(DomainNames, NotBefore, NotAfter); Log.Informational("Order created.", new KeyValuePair <string, object>("Status", Order.Status), new KeyValuePair <string, object>("Expires", Order.Expires), new KeyValuePair <string, object>("NotBefore", Order.NotBefore), new KeyValuePair <string, object>("NotAfter", Order.NotAfter), new KeyValuePair <string, object>("Identifiers", Order.Identifiers)); List <string> FileNames = null; try { foreach (AcmeAuthorization Authorization in await Order.GetAuthorizations()) { Log.Informational("Processing authorization.", new KeyValuePair <string, object>("Type", Authorization.Type), new KeyValuePair <string, object>("Value", Authorization.Value), new KeyValuePair <string, object>("Status", Authorization.Status), new KeyValuePair <string, object>("Expires", Authorization.Expires), new KeyValuePair <string, object>("Wildcard", Authorization.Wildcard)); AcmeChallenge Challenge; bool Manual = true; int Index = 1; int NrChallenges = Authorization.Challenges.Length; string s; for (Index = 1; Index <= NrChallenges; Index++) { Challenge = Authorization.Challenges[Index - 1]; if (Challenge is AcmeHttpChallenge HttpChallenge) { Log.Informational(Index.ToString() + ") HTTP challenge.", new KeyValuePair <string, object>("Resource", HttpChallenge.ResourceName), new KeyValuePair <string, object>("Response", HttpChallenge.KeyAuthorization), new KeyValuePair <string, object>("Content-Type", "application/octet-stream")); if (!string.IsNullOrEmpty(HttpRootFolder)) { string ChallengeFileName = Path.Combine(HttpRootFolder, HttpChallenge.Token); File.WriteAllBytes(ChallengeFileName, Encoding.ASCII.GetBytes(HttpChallenge.KeyAuthorization)); if (FileNames == null) { FileNames = new List <string>(); } FileNames.Add(ChallengeFileName); Log.Informational("Acknowleding challenge."); Challenge = await HttpChallenge.AcknowledgeChallenge(); Log.Informational("Challenge acknowledged.", new KeyValuePair <string, object>("Status", Challenge.Status)); Manual = false; } else if (!Verbose) { Console.Out.WriteLine(Index.ToString() + ") HTTP challenge."); Console.Out.WriteLine("Resource: " + HttpChallenge.ResourceName); Console.Out.WriteLine("Response: " + HttpChallenge.KeyAuthorization); Console.Out.WriteLine("Content-Type: " + "application/octet-stream"); } } else if (Challenge is AcmeDnsChallenge DnsChallenge) { Log.Informational(Index.ToString() + ") DNS challenge.", new KeyValuePair <string, object>("Domain", DnsChallenge.ValidationDomainNamePrefix + Authorization.Value), new KeyValuePair <string, object>("TXT Record", DnsChallenge.KeyAuthorization)); if (!Verbose) { Console.Out.WriteLine(Index.ToString() + ") DNS challenge."); Console.Out.WriteLine("Domain: " + DnsChallenge.ValidationDomainNamePrefix + Authorization.Value); Console.Out.WriteLine("TXT Record: " + DnsChallenge.KeyAuthorization); } } } if (Manual) { Console.Out.WriteLine(); Console.Out.WriteLine("No automated method found to respond to any of the authorization challenges. " + "You can respond to a challenge manually. After configuring the corresponding " + "resource, enter the number of the corresponding challenge and press ENTER to acknowledge it."); do { Console.Out.Write("Challenge to acknowledge: "); s = Console.In.ReadLine(); }while (!int.TryParse(s, out Index) || Index <= 0 || Index > NrChallenges); Log.Informational("Acknowleding challenge."); Challenge = await Authorization.Challenges[Index - 1].AcknowledgeChallenge(); Log.Informational("Challenge acknowledged.", new KeyValuePair <string, object>("Status", Challenge.Status)); } AcmeAuthorization Authorization2 = Authorization; do { Log.Informational("Waiting to poll authorization status.", new KeyValuePair <string, object>("ms", PollingInterval)); System.Threading.Thread.Sleep(PollingInterval); Log.Informational("Polling authorization."); Authorization2 = await Authorization2.Poll(); Log.Informational("Authorization polled.", new KeyValuePair <string, object>("Type", Authorization2.Type), new KeyValuePair <string, object>("Value", Authorization2.Value), new KeyValuePair <string, object>("Status", Authorization2.Status), new KeyValuePair <string, object>("Expires", Authorization2.Expires), new KeyValuePair <string, object>("Wildcard", Authorization2.Wildcard)); }while (Authorization2.Status == AcmeAuthorizationStatus.pending); if (Authorization2.Status != AcmeAuthorizationStatus.valid) { switch (Authorization2.Status) { case AcmeAuthorizationStatus.deactivated: throw new Exception("Authorization deactivated."); case AcmeAuthorizationStatus.expired: throw new Exception("Authorization expired."); case AcmeAuthorizationStatus.invalid: throw new Exception("Authorization invalid."); case AcmeAuthorizationStatus.revoked: throw new Exception("Authorization revoked."); default: throw new Exception("Authorization not validated."); } } } using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(KeySize)) { Log.Informational("Finalizing order."); SignatureAlgorithm SignAlg = new RsaSha256(RSA); Order = await Order.FinalizeOrder(new Security.ACME.CertificateRequest(SignAlg) { CommonName = DomainNames[0], SubjectAlternativeNames = DomainNames, EMailAddress = EMail, Country = Country, Locality = Locality, StateOrProvince = StateOrProvince, Organization = Organization, OrganizationalUnit = OrganizationalUnit }); Log.Informational("Order finalized.", new KeyValuePair <string, object>("Status", Order.Status), new KeyValuePair <string, object>("Expires", Order.Expires), new KeyValuePair <string, object>("NotBefore", Order.NotBefore), new KeyValuePair <string, object>("NotAfter", Order.NotAfter), new KeyValuePair <string, object>("Identifiers", Order.Identifiers)); if (Order.Status != AcmeOrderStatus.valid) { switch (Order.Status) { case AcmeOrderStatus.invalid: throw new Exception("Order invalid."); default: throw new Exception("Unable to validate oder."); } } if (Order.Certificate == null) { throw new Exception("No certificate URI provided."); } System.Security.Cryptography.X509Certificates.X509Certificate2[] Certificates = await Order.DownloadCertificate(); string CertificateFileName; string CertificateFileName2; int Index = 1; byte[] Bin; DerEncoder KeyOutput = new DerEncoder(); SignAlg.ExportPrivateKey(KeyOutput); StringBuilder PemOutput = new StringBuilder(); PemOutput.AppendLine("-----BEGIN RSA PRIVATE KEY-----"); PemOutput.AppendLine(Convert.ToBase64String(KeyOutput.ToArray(), Base64FormattingOptions.InsertLineBreaks)); PemOutput.AppendLine("-----END RSA PRIVATE KEY-----"); CertificateFileName = FileName + ".key"; Log.Informational("Saving private key.", new KeyValuePair <string, object>("FileName", CertificateFileName)); File.WriteAllText(CertificateFileName, PemOutput.ToString(), Encoding.ASCII); foreach (X509Certificate2 Certificate in Certificates) { if (Index == 1) { CertificateFileName = FileName; } else { CertificateFileName = FileName + Index.ToString(); } CertificateFileName2 = CertificateFileName + ".pem"; CertificateFileName += ".cer"; Bin = Certificate.Export(X509ContentType.Cert); Log.Informational("Saving certificate.", new KeyValuePair <string, object>("FileName", CertificateFileName), new KeyValuePair <string, object>("FileName2", CertificateFileName2), new KeyValuePair <string, object>("FriendlyName", Certificate.FriendlyName), new KeyValuePair <string, object>("HasPrivateKey", Certificate.HasPrivateKey), new KeyValuePair <string, object>("Issuer", Certificate.Issuer), new KeyValuePair <string, object>("NotAfter", Certificate.NotAfter), new KeyValuePair <string, object>("NotBefore", Certificate.NotBefore), new KeyValuePair <string, object>("SerialNumber", Certificate.SerialNumber), new KeyValuePair <string, object>("Subject", Certificate.Subject), new KeyValuePair <string, object>("Thumbprint", Certificate.Thumbprint)); File.WriteAllBytes(CertificateFileName, Bin); PemOutput.Clear(); PemOutput.AppendLine("-----BEGIN CERTIFICATE-----"); PemOutput.AppendLine(Convert.ToBase64String(Bin, Base64FormattingOptions.InsertLineBreaks)); PemOutput.AppendLine("-----END CERTIFICATE-----"); File.WriteAllText(CertificateFileName2, PemOutput.ToString(), Encoding.ASCII); Index++; } } } finally { if (FileNames != null) { foreach (string FileName2 in FileNames) { File.Delete(FileName2); } } } } } }
public override async Task CleanupChallengeFile(HttpChallenge challenge) { var blob = await GetBlob(challenge); await blob.DeleteIfExistsAsync(); }
public override async Task CleanupChallengeFile(HttpChallenge challenge) { var blobFile = await GetChallengeBlobReferenceAsync(challenge); await blobFile.DeleteIfExistsAsync(); }
private void CreateChallengeResponse(HttpChallenge httpChallenge) { _logger.Information("Make sure the URL {responseUrl} responds with {response}", httpChallenge.FileUrl, httpChallenge.FileContent); _logger.Information("Press a key to continue when the response is available...", httpChallenge.FileUrl, httpChallenge.FileContent); Console.ReadKey(); }
public override async Task CleanupChallengeFile(HttpChallenge challenge) { File.Delete(await GetAnswerPath(challenge)); }
internal async Task <bool> CreateCertificate(string TabID) { try { string URL = this.customCA ? this.acmeDirectory : "https://acme-v02.api.letsencrypt.org/directory"; RSAParameters Parameters; CspParameters CspParams = new CspParameters() { Flags = CspProviderFlags.UseMachineKeyStore, KeyContainerName = "IoTGateway:" + URL }; try { bool Ok; using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams)) { Parameters = RSA.ExportParameters(true); if (RSA.KeySize < 4096) { RSA.PersistKeyInCsp = false; RSA.Clear(); Ok = false; } else { Ok = true; } } if (!Ok) { using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams)) { Parameters = RSA.ExportParameters(true); } } } catch (CryptographicException ex) { throw new CryptographicException("Unable to get access to cryptographic key for \"IoTGateway:" + URL + "\". Was the database created using another user?", ex); } using (AcmeClient Client = new AcmeClient(new Uri(URL), Parameters)) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Connecting to directory.", false, "User"); AcmeDirectory AcmeDirectory = await Client.GetDirectory(); if (AcmeDirectory.ExternalAccountRequired) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "An external account is required.", false, "User"); } if (AcmeDirectory.TermsOfService != null) { URL = AcmeDirectory.TermsOfService.ToString(); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Terms of service available on: " + URL, false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "TermsOfService", URL, false, "User"); this.urlToS = URL; if (!this.acceptToS) { ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "You need to accept the terms of service.", false, "User"); return(false); } } if (AcmeDirectory.Website != null) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Web site available on: " + AcmeDirectory.Website.ToString(), false, "User"); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Getting account.", false, "User"); List <string> Names = new List <string>(); if (!string.IsNullOrEmpty(this.domain)) { Names.Add(this.domain); } if (this.alternativeDomains != null) { foreach (string Name in this.alternativeDomains) { if (!Names.Contains(Name)) { Names.Add(Name); } } } string[] DomainNames = Names.ToArray(); AcmeAccount Account; try { Account = await Client.GetAccount(); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account found.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Created: " + Account.CreatedAt.ToString(), false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Initial IP: " + Account.InitialIp, false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Status: " + Account.Status.ToString(), false, "User"); if (string.IsNullOrEmpty(this.contactEMail)) { if (Account.Contact != null && Account.Contact.Length != 0) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Updating contact URIs in account.", false, "User"); Account = await Account.Update(new string[0]); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account updated.", false, "User"); } } else { if (Account.Contact is null || Account.Contact.Length != 1 || Account.Contact[0] != "mailto:" + this.contactEMail) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Updating contact URIs in account.", false, "User"); Account = await Account.Update(new string[] { "mailto:" + this.contactEMail }); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account updated.", false, "User"); } } } catch (AcmeAccountDoesNotExistException) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account not found.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Creating account.", false, "User"); Account = await Client.CreateAccount(string.IsNullOrEmpty(this.contactEMail)?new string[0] : new string[] { "mailto:" + this.contactEMail }, this.acceptToS); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account created.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Status: " + Account.Status.ToString(), false, "User"); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Generating new key.", false, "User"); await Account.NewKey(); using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams)) { RSA.ImportParameters(Client.ExportAccountKey(true)); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "New key generated.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Creating order.", false, "User"); AcmeOrder Order = await Account.OrderCertificate(DomainNames, null, null); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Order created.", false, "User"); foreach (AcmeAuthorization Authorization in await Order.GetAuthorizations()) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Processing authorization for " + Authorization.Value, false, "User"); AcmeChallenge Challenge; bool Acknowledged = false; int Index = 1; int NrChallenges = Authorization.Challenges.Length; for (Index = 1; Index <= NrChallenges; Index++) { Challenge = Authorization.Challenges[Index - 1]; if (Challenge is AcmeHttpChallenge HttpChallenge) { this.challenge = "/" + HttpChallenge.Token; this.token = HttpChallenge.KeyAuthorization; ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Acknowleding challenge.", false, "User"); Challenge = await HttpChallenge.AcknowledgeChallenge(); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Challenge acknowledged: " + Challenge.Status.ToString(), false, "User"); Acknowledged = true; } } if (!Acknowledged) { ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "No automated method found to respond to any of the authorization challenges.", false, "User"); return(false); } AcmeAuthorization Authorization2 = Authorization; do { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Waiting to poll authorization status.", false, "User"); await Task.Delay(5000); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Polling authorization.", false, "User"); Authorization2 = await Authorization2.Poll(); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Authorization polled: " + Authorization2.Status.ToString(), false, "User"); }while (Authorization2.Status == AcmeAuthorizationStatus.pending); if (Authorization2.Status != AcmeAuthorizationStatus.valid) { switch (Authorization2.Status) { case AcmeAuthorizationStatus.deactivated: throw new Exception("Authorization deactivated."); case AcmeAuthorizationStatus.expired: throw new Exception("Authorization expired."); case AcmeAuthorizationStatus.invalid: throw new Exception("Authorization invalid."); case AcmeAuthorizationStatus.revoked: throw new Exception("Authorization revoked."); default: throw new Exception("Authorization not validated."); } } } using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096)) // TODO: Make configurable { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Finalizing order.", false, "User"); SignatureAlgorithm SignAlg = new RsaSha256(RSA); Order = await Order.FinalizeOrder(new CertificateRequest(SignAlg) { CommonName = this.domain, SubjectAlternativeNames = DomainNames, EMailAddress = this.contactEMail }); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Order finalized: " + Order.Status.ToString(), false, "User"); if (Order.Status != AcmeOrderStatus.valid) { switch (Order.Status) { case AcmeOrderStatus.invalid: throw new Exception("Order invalid."); default: throw new Exception("Unable to validate oder."); } } if (Order.Certificate is null) { throw new Exception("No certificate URI provided."); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Downloading certificate.", false, "User"); X509Certificate2[] Certificates = await Order.DownloadCertificate(); X509Certificate2 Certificate = Certificates[0]; ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Exporting certificate.", false, "User"); this.certificate = Certificate.Export(X509ContentType.Cert); this.privateKey = RSA.ExportCspBlob(true); this.pfx = null; this.password = string.Empty; ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Adding private key.", false, "User"); try { Certificate.PrivateKey = RSA; } catch (PlatformNotSupportedException) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Platform does not support adding of private key.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Searching for OpenSSL on machine.", false, "User"); string[] Files; string Password = Hashes.BinaryToString(Gateway.NextBytes(32)); string CertFileName = null; string CertFileName2 = null; string KeyFileName = null; if (string.IsNullOrEmpty(this.openSslPath) || !File.Exists(this.openSslPath)) { Files = GetFiles(new string(Path.DirectorySeparatorChar, 1), "openssl.exe"); if (Files is null) { List <string> Files2 = new List <string>(); Files = GetFiles(Environment.SpecialFolder.ProgramFiles, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.CommonProgramFilesX86, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.Programs, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.System, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.SystemX86, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.Windows, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.CommonProgramFiles, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.CommonProgramFilesX86, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.CommonPrograms, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Path.DirectorySeparatorChar + "OpenSSL-Win32", "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Path.DirectorySeparatorChar + "OpenSSL-Win64", "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = Files2.ToArray(); } } else { Files = new string[] { this.openSslPath } }; try { if (Files.Length == 0) { ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "Unable to join certificate with private key. Try installing <a target=\"_blank\" href=\"https://wiki.openssl.org/index.php/Binaries\">OpenSSL</a> and try again.", false, "User"); return(false); } else { foreach (string OpenSslFile in Files) { if (CertFileName is null) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Generating temporary certificate file.", false, "User"); StringBuilder PemOutput = new StringBuilder(); byte[] Bin = Certificate.Export(X509ContentType.Cert); PemOutput.AppendLine("-----BEGIN CERTIFICATE-----"); PemOutput.AppendLine(Convert.ToBase64String(Bin, Base64FormattingOptions.InsertLineBreaks)); PemOutput.AppendLine("-----END CERTIFICATE-----"); CertFileName = Path.Combine(Gateway.AppDataFolder, "Certificate.pem"); File.WriteAllText(CertFileName, PemOutput.ToString(), Encoding.ASCII); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Generating temporary key file.", false, "User"); DerEncoder KeyOutput = new DerEncoder(); SignAlg.ExportPrivateKey(KeyOutput); PemOutput.Clear(); PemOutput.AppendLine("-----BEGIN RSA PRIVATE KEY-----"); PemOutput.AppendLine(Convert.ToBase64String(KeyOutput.ToArray(), Base64FormattingOptions.InsertLineBreaks)); PemOutput.AppendLine("-----END RSA PRIVATE KEY-----"); KeyFileName = Path.Combine(Gateway.AppDataFolder, "Certificate.key"); File.WriteAllText(KeyFileName, PemOutput.ToString(), Encoding.ASCII); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Converting to PFX using " + OpenSslFile, false, "User"); Process P = new Process() { StartInfo = new ProcessStartInfo() { FileName = OpenSslFile, Arguments = "pkcs12 -nodes -export -out Certificate.pfx -inkey Certificate.key -in Certificate.pem -password pass:"******"ShowStatus", "Output: " + P.StandardOutput.ReadToEnd(), false, "User"); } if (!P.StandardError.EndOfStream) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Error: " + P.StandardError.ReadToEnd(), false, "User"); } continue; } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Loading PFX.", false, "User"); CertFileName2 = Path.Combine(Gateway.AppDataFolder, "Certificate.pfx"); this.pfx = File.ReadAllBytes(CertFileName2); this.password = Password; this.openSslPath = OpenSslFile; ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "PFX successfully generated using OpenSSL.", false, "User"); break; } if (this.pfx is null) { this.openSslPath = string.Empty; ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "Unable to convert to PFX using OpenSSL.", false, "User"); return(false); } } } finally { if (CertFileName != null && File.Exists(CertFileName)) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Deleting temporary certificate file.", false, "User"); File.Delete(CertFileName); } if (KeyFileName != null && File.Exists(KeyFileName)) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Deleting temporary key file.", false, "User"); File.Delete(KeyFileName); } if (CertFileName2 != null && File.Exists(CertFileName2)) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Deleting temporary pfx file.", false, "User"); File.Delete(CertFileName2); } } } if (this.Step < 2) { this.Step = 2; } this.Updated = DateTime.Now; await Database.Update(this); ClientEvents.PushEvent(new string[] { TabID }, "CertificateOk", string.Empty, false, "User"); Gateway.UpdateCertificate(this); return(true); } } } catch (Exception ex) { Log.Critical(ex); ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "Unable to create certificate: " + XML.HtmlValueEncode(ex.Message), false, "User"); return(false); } finally { this.inProgress = false; } }
public abstract Task PersistsChallengeFile(HttpChallenge challenge);