public async Task LoginAsync(string email) { if (email == null) { throw new ArgumentNullException(nameof(email)); } if (string.IsNullOrWhiteSpace(email)) { throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(email)); } if (this.client == null) { throw new ObjectDisposedException("AcmeContext"); } Trace.Write($"Creating registration for '{email}'..."); this.account = await this.client.NewRegistraton("mailto:" + email); this.account.Data.Agreement = this.account.GetTermsOfServiceUri(); Trace.WriteLine("OK"); Trace.Write($"Accepting TOS at {this.account.Data.Agreement}..."); this.account = await this.client.UpdateRegistration(account); Trace.WriteLine("OK"); }
/// <summary> /// Changes the registration key. /// </summary> /// <param name="account">The account.</param> /// <param name="newKey">The new registration key.</param> /// <returns>The awaitable.</returns> public async Task ChangeKey(AcmeAccount account, KeyInfo newKey) { var keyPair = new AccountKey(newKey); var body = new { account = account.Location, newKey = keyPair.JsonWebKey, }; var jws = new JwsSigner(keyPair); var payload = jws.Sign(body); var payloadWithResourceType = new { payload.Header, payload.Payload, payload.Protected, payload.Signature, Resource = ResourceTypes.KeyChange }; var uri = await this.handler.GetResourceUri(ResourceTypes.KeyChange); var result = await this.handler.Post(uri, payloadWithResourceType, key); ThrowIfError(result); this.key = keyPair; }
async Task ProcessAcmeFqdnAsync(AcmeAccount account, string fqdn, CancellationToken cancel) { cancel.ThrowIfCancellationRequested(); DirectoryPath dir = this.AcmeDir.GetSubDirectory(fqdn); FilePath crtFileName = dir.Combine(dir.GetThisDirectoryName() + Consts.Extensions.Certificate_Acme); Certificate?currentCert = null; if (crtFileName.IsFileExists(cancel)) { try { currentCert = CertificateUtil.ImportChainedCertificates(crtFileName.ReadDataFromFile().Span).First(); } catch (Exception ex) { ex._Debug(); } } if (currentCert == null || IsCertificateDateTimeToUpdate(currentCert.CertData.NotBefore, currentCert.CertData.NotAfter)) { //Con.WriteLine($"fqdn = {fqdn}, currentCert = {currentCert}, crtFileName = {crtFileName}"); await AcmeIssueAsync(account, fqdn, crtFileName, cancel); } }
async Task ProcessEnqueuedAcmeHostnameAsync(CancellationToken cancel) { List <string> queue; lock (AcmeQueueLockObj) { queue = AcmeQueue; AcmeQueue = new List <string>(); } if (queue.Count == 0) { // キューがない return; } using (AcmeClient client = new AcmeClient(new AcmeClientOptions(this.Settings.AcmeServiceDirectoryUrl !, this.TcpIp))) { AcmeAccount account = await client.LoginAccountAsync(this.AcmeAccountKey !, ("mailto:" + this.Settings.AcmeContactEmail)._SingleArray(), cancel); foreach (string fqdn in queue) { if (CheckFqdnAllowedForAcme(fqdn)) { if (this.Settings.AcmeEnableFqdnIpCheck == false || (await CheckFqdnHasIpAddressOfThisLocalHostAsync(fqdn, cancel))) { await ProcessAcmeFqdnAsync(account, fqdn, cancel); } } } } }
/// <summary> /// Creates a new registraton. /// </summary> /// <param name="contact">The contact method, e.g. <c>mailto:[email protected]</c>.</param> /// <returns>The ACME account created.</returns> public async Task <AcmeAccount> NewRegistraton(params string[] contact) { if (this.key == null) { this.key = new AccountKey(); } var registration = new Registration { Contact = contact, Resource = ResourceTypes.NewRegistration }; var uri = await this.handler.GetResourceUri(registration.Resource); var result = await this.handler.Post(uri, registration, key); ThrowIfError(result); var account = new AcmeAccount { Links = result.Links, Data = result.Data, Json = result.Json, Raw = result.Raw, Location = result.Location, Key = key.Export(), ContentType = result.ContentType }; return(account); }
public async Task CanUpdateRegistration() { var accountKey = await Helper.LoadkeyV1(); var regLocation = new Uri("http://example.com/reg/1"); var mock = MockHttp(async req => { if (req.Method == HttpMethod.Post && req.RequestUri == regLocation) { var payload = await ParsePayload <RegistrationEntity>(req); Assert.Equal(ResourceTypes.Registration, payload.Resource); Assert.Equal(1, payload.Contact?.Count); Assert.Equal($"another-{email}", payload.Contact[0]); Assert.NotNull(payload.Agreement); var respJson = new { contact = payload.Contact, agreement = payload.Agreement, resource = ResourceTypes.Registration }; var resp = CreateResponse(respJson, HttpStatusCode.Created, regLocation); resp.Headers.Add("Link", $"<{tos}>; rel=\"terms-of-service\""); return(resp); } return(null); }); using (var http = new HttpClient(mock.Object)) using (var handler = new AcmeHttpHandler(server, http)) { using (var client = new AcmeClient(handler)) { client.Use(accountKey.Export()); var account = new AcmeAccount { Location = regLocation, Data = new RegistrationEntity { Resource = ResourceTypes.Registration, Contact = new[] { $"another-{email}" }, Agreement = tos } }; var result = await client.UpdateRegistration(account); Assert.Equal(ResourceTypes.Registration, result.Data.Resource); Assert.Equal(tos, account.Data.Agreement); Assert.Equal(regLocation, account.Location); } mock.As <IDisposable>().Verify(x => x.Dispose(), Times.Never()); } }
public DnsChallenge(AcmeAccount account, AcmeChallenge challenge, string identifier) { Account = account; Challenge = challenge; Identifier = identifier; Token = challenge.Token; AuthorizationKey = CertificateUtility.CreateAuthorizationKey(account, challenge.Token); }
public async Task ACME_Test_03_GetAccount() { AcmeAccount Account = await this.client.GetAccount(); Assert.IsNotNull(Account); Assert.AreEqual(AcmeAccountStatus.valid, Account.Status); Assert.IsNotNull(Account.Contact); Assert.IsTrue(Account.Contact.Length > 0); Assert.AreEqual("mailto:[email protected]", Account.Contact[0]); }
public async Task ACME_Test_02_CreateAccount() { AcmeAccount Account = await this.client.CreateAccount(new string[] { "mailto:[email protected]" }, true); Assert.IsNotNull(Account); Assert.AreEqual(AcmeAccountStatus.valid, Account.Status); Assert.IsNotNull(Account.Contact); Assert.IsTrue(Account.Contact.Length > 0); Assert.AreEqual("mailto:[email protected]", Account.Contact[0]); }
public async Task CanDeleteRegistration() { var accountKey = await Helper.LoadkeyV1(); var regLocation = new Uri("http://example.com/reg/1"); var mock = MockHttp(async req => { if (req.Method == HttpMethod.Post && req.RequestUri == regLocation) { var payload = await ParsePayload <RegistrationEntity>(req); Assert.Equal(ResourceTypes.Registration, payload.Resource); Assert.True(payload.Delete); var resp = CreateResponse(null, HttpStatusCode.OK, regLocation); resp.Headers.Add("Link", $"<{tos}>; rel=\"terms-of-service\""); return(resp); } return(null); }); using (var http = new HttpClient(mock.Object)) using (var handler = new AcmeHttpHandler(server, http)) { using (var client = new AcmeClient(handler)) { var account = new AcmeAccount { Location = regLocation, Data = new RegistrationEntity { Resource = ResourceTypes.Registration, Contact = new[] { $"another-{email}" }, Agreement = tos } }; try { await client.DeleteRegistration(account); Assert.False(true); } catch (InvalidOperationException) { } client.Use(accountKey.Export()); await client.DeleteRegistration(account); } mock.As <IDisposable>().Verify(x => x.Dispose(), Times.Never()); } }
public async Task ACME_Test_04_UpdateAccount() { AcmeAccount Account = await this.client.GetAccount(); Account = await Account.Update(new string[] { "mailto:[email protected]", "mailto:[email protected]" }); Assert.IsNotNull(Account); Assert.AreEqual(AcmeAccountStatus.valid, Account.Status); Assert.IsNotNull(Account.Contact); Assert.IsTrue(Account.Contact.Length > 1); Assert.AreEqual("mailto:[email protected]", Account.Contact[0]); Assert.AreEqual("mailto:[email protected]", Account.Contact[1]); }
public async Task ACME_Test_04_UpdateAccount() { AcmeAccount Account = await this.client.GetAccount(); Account = await Account.Update(new string[] { "mailto:[email protected]", "mailto:[email protected]" }); Assert.IsNotNull(Account); Assert.AreEqual(AcmeAccountStatus.valid, Account.Status); Assert.IsNotNull(Account.Contact); Assert.IsTrue(Account.Contact.Length > 1); Assert.IsTrue(Array.IndexOf <string>(Account.Contact, "mailto:[email protected]") >= 0); Assert.IsTrue(Array.IndexOf <string>(Account.Contact, "mailto:[email protected]") >= 0); }
/// <summary> /// Deletes the registration. /// </summary> /// <returns>The awaitable.</returns> public async Task DeleteRegistration(AcmeAccount account) { if (this.key == null) { throw new InvalidOperationException(); } await this.handler.Post( account.Location, new Registration { Delete = true }, key); }
/// <summary> /// Deactivates an existing account /// </summary> /// <param name="account">Existing account</param> /// <returns></returns> public async Task DeactiveAsync(AcmeAccount account) { var directory = await _directoryCache.GetAsync(); var nonce = await _nonceCache.GetAsync(); var response = await _acmeApi.DeactivateAccountAsync(directory, nonce, account); if (response.Status == AcmeApiResponseStatus.Error) { throw new AcmeProtocolException(response.Message); } _nonceCache.Update(response.Nonce); }
/// <summary> /// Updates the registration. /// </summary> /// <param name="account">The account to update.</param> /// <returns>The updated ACME account.</returns> /// <exception cref="InvalidOperationException">If the account key is missing.</exception> public async Task <AcmeAccount> UpdateRegistration(AcmeAccount account) { if (this.key == null) { throw new InvalidOperationException(); } var registration = account.Data; var result = await this.handler.Post(account.Location, registration, key); ThrowIfError(result); account.Data = result.Data; return(account); }
public async Task ACME_Test_12_GetOrders() { AcmeAccount Account = await this.client.GetAccount(); AcmeOrder[] Orders = await Account.GetOrders(); foreach (AcmeOrder Order in Orders) { Console.Out.WriteLine(Order.Location); Console.Out.WriteLine(Order.Status); Console.Out.WriteLine(); AcmeAuthorization[] Authorizations = await Order.GetAuthorizations(); this.Print(Authorizations); } }
public async Task ACME_Test_05_NewKey() { AcmeAccount Account = await this.client.GetAccount(); await Account.NewKey(); CspParameters CspParams = new CspParameters() { Flags = CspProviderFlags.UseMachineKeyStore, KeyContainerName = directory }; using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams)) { RSA.ImportParameters(this.client.ExportAccountKey(true)); } }
/// <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); }
async Task AcmeIssueAsync(AcmeAccount account, string fqdn, FilePath crtFileName, CancellationToken cancel) { cancel.ThrowIfCancellationRequested(); AcmeOrder order = await account.NewOrderAsync(fqdn, cancel); if (this.IsGlobalCertVault) { GlobalCertVault.SetAcmeAccountForChallengeResponse(account); } CertificateStore store = await order.FinalizeAsync(this.AcmeCertKey !, cancel); IsAcmeCertUpdated = true; store.ExportChainedPem(out ReadOnlyMemory <byte> certData, out _); crtFileName.WriteDataToFile(certData, additionalFlags: FileFlags.AutoCreateDirectory); }
private void SaveAccountAndChallengeData(AcmeAccount account, IEnumerable <IAcmeChallengeContent> challenges, AcmeCertificateFulfillmentPromise promise) { string baseFolder = @"c:\temp"; var serializedAccount = JsonConvert.SerializeObject(account, Formatting.None); serializedAccount = Convert.ToBase64String(Encoding.UTF8.GetBytes(serializedAccount)); using (FileStream fs = new FileStream(Path.Combine(baseFolder, "myaccount.acc"), FileMode.Create)) { byte[] buffer = Encoding.UTF8.GetBytes(serializedAccount); fs.Write(buffer, 0, buffer.Length); } var sChallenges = JsonConvert.SerializeObject(challenges, Formatting.None); sChallenges = Convert.ToBase64String(Encoding.UTF8.GetBytes(sChallenges)); using (FileStream fs = new FileStream(Path.Combine(baseFolder, "challenges.dat"), FileMode.Create)) { byte[] buffer = Encoding.UTF8.GetBytes(sChallenges); fs.Write(buffer, 0, buffer.Length); } var sCertificatePromise = JsonConvert.SerializeObject(promise); sCertificatePromise = Convert.ToBase64String(Encoding.UTF8.GetBytes(sCertificatePromise)); using (FileStream fs = new FileStream(Path.Combine(baseFolder, "challengepromise.dat"), FileMode.Create)) { byte[] buffer = Encoding.UTF8.GetBytes(sCertificatePromise); fs.Write(buffer, 0, buffer.Length); } foreach (var challenge in challenges) { challenge.SaveToFile(Path.Combine(baseFolder, challenge.Token)); } //Create Account Private Key using (TextWriter writer = new StreamWriter(Path.Combine(baseFolder, "account.key"))) { CertificateUtility.ExportRSAPrivateKey(account.SecurityInfo, writer); } }
public async Task IssueWildCardCertificate() { var restApi = new AcmeRestApi(ProtoacmeContants.LETSENCRYPT_STAGING_ENDPOINT); var client = new ProtoacmeClient(restApi); //1. Load up the account and challenge data. AcmeAccount account = AcmeAccount.FromFile(@"c:\temp\account.dat"); AcmeCertificateFulfillmentPromise promise = AcmeCertificateFulfillmentPromise.FromFile(@"c:\temp\promise.dat"); ChallengeCollection challenges = ChallengeCollection.FromFile <DnsChallenge>(@"c:\temp\challenge.dat"); //2. Tell Lets Encrypt to verify our challenge. var startVerifyResult = await client.Challenge.ExecuteChallengeVerification(challenges[0]); AcmeChallengeStatus challengeStatus = null; while (challengeStatus == null || challengeStatus.Status == "pending") { challengeStatus = await client.Challenge.GetChallengeVerificationStatus(challenges[0]); await Task.Delay(3000); } if (challengeStatus.Status != "valid") { throw new Exception($"Failed to validate challenge token"); } //3. Create the CSR CSR csr = CertificateUtility.GenerateCsr(wildCardDns); SaveCRTPrivateKey(csr); //4. Download the certificate var cert = await client.Certificate.DownloadCertificateAsync(account, promise, csr, CertificateType.Cert); //5. Save the certificate using (FileStream fs = new FileStream(@"c:\temp\mycert.cer", FileMode.Create)) { byte[] buffer = cert.Array; fs.Write(buffer, 0, buffer.Length); } }
private string GenerateCSR(AcmeAccount account, params string[] domainNames) { HashAlgorithmName hashName = HashAlgorithmName.SHA256; var builder = new SubjectAlternativeNameBuilder(); foreach (var name in domainNames) { builder.AddDnsName(name); } RSA rsa = RSA.Create(4096); //rsa.ImportParameters(account.SecurityInfo); var dn = new X500DistinguishedName($"CN={domainNames.First()}"); var csr = new CertificateRequest(dn, rsa, hashName, RSASignaturePadding.Pkcs1); csr.CertificateExtensions.Add(builder.Build()); return(Base64Tool.Encode(csr.CreateSigningRequest())); }
public async Task ChangeKey_ShouldUpdateLastNonce() { //ARRANGE var acmeApiMock = new Mock <IAcmeRestApi>(); var directoryCacheMock = new Mock <ICachedRepository <AcmeDirectory> >(); var nonceCacheMock = new Mock <ICachedRepository <string> >(); AcmeApiResponse successResponse = TestHelpers.AcmeEmptyResponseWithNonce; AcmeAccount account = TestHelpers.AcmeAccountResponse.Data; acmeApiMock.Setup(method => method.RollOverAccountKeyAsync(It.IsAny <AcmeDirectory>(), It.IsAny <string>(), It.IsAny <AcmeAccount>())) .ReturnsAsync(successResponse); AcmeAccountService srv = new AcmeAccountService(acmeApiMock.Object, directoryCacheMock.Object, nonceCacheMock.Object); //ACT await srv.ChangeKeyAsync(account); //ASSERT nonceCacheMock.Verify(method => method.Update(successResponse.Nonce), Times.Once()); }
public async Task <string> RegisterAndLoginAsync(string email) { if (email == null) { throw new ArgumentNullException(nameof(email)); } if (string.IsNullOrWhiteSpace(email)) { throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(email)); } if (client == null) { throw new ObjectDisposedException(nameof(AutoAcmeContext)); } context = new AcmeContext(serverAddress, null, client); Log.Write($"Creating registration for '{email}' and accept TOS..."); var contacts = new[] { "mailto:" + email }; var accountContext = await context.NewAccount(contacts, true).ConfigureAwait(true); Log.WriteLine("OK"); // For compatibility with earlier versions, use the V1 account object for storage AcmeAccount legacyAccount = new AcmeAccount() { ContentType = "application/json", Key = new KeyInfo() { PrivateKeyInfo = context.AccountKey.ToDer() }, Data = { Contact = contacts, Resource = "reg" }, Location = accountContext.Location }; legacyAccount.Data.Agreement = await context.TermsOfService().ConfigureAwait(true); return(JsonConvert.SerializeObject(legacyAccount)); }
public static string CreateAuthorizationKey(AcmeAccount account, string challengeToken) { string jwkThumbprint = string.Empty; //Compute the JWK Thumbprint var jwk = new { e = Base64Tool.Encode(account.SecurityInfo.Exponent), kty = "RSA", n = Base64Tool.Encode(account.SecurityInfo.Modulus) }; string sjwk = JsonConvert.SerializeObject(jwk, Formatting.None); using (HashAlgorithm sha = SHA256.Create()) { byte[] bjwk = Encoding.UTF8.GetBytes(sjwk); jwkThumbprint = Base64Tool.Encode(sha.ComputeHash(bjwk)); } return($"{challengeToken}.{jwkThumbprint}"); }
private void LoadAccountAndChallengeData <TChallengeType>(out AcmeAccount account, out List <TChallengeType> challenges, out AcmeCertificateFulfillmentPromise promise) where TChallengeType : IAcmeChallengeContent { string baseFolder = @"c:\temp"; using (FileStream fs = new FileStream(Path.Combine(baseFolder, "myaccount.acc"), FileMode.Open)) { using (StreamReader sr = new StreamReader(fs)) { var sAccount = sr.ReadToEnd(); var bAccount = Convert.FromBase64String(sAccount); sAccount = Encoding.UTF8.GetString(bAccount); account = JsonConvert.DeserializeObject <AcmeAccount>(sAccount); } } using (FileStream fs = new FileStream(Path.Combine(baseFolder, "challenges.dat"), FileMode.Open)) { using (StreamReader sr = new StreamReader(fs)) { var sChallenges = sr.ReadToEnd(); var bChallenges = Convert.FromBase64String(sChallenges); sChallenges = Encoding.UTF8.GetString(bChallenges); challenges = JsonConvert.DeserializeObject <List <TChallengeType> >(sChallenges); } } using (FileStream fs = new FileStream(Path.Combine(baseFolder, "challengepromise.dat"), FileMode.Open)) { using (StreamReader sr = new StreamReader(fs)) { var sPromise = sr.ReadToEnd(); var bPromise = Convert.FromBase64String(sPromise); sPromise = Encoding.UTF8.GetString(bPromise); promise = JsonConvert.DeserializeObject <AcmeCertificateFulfillmentPromise>(sPromise); } } }
/// <summary> /// Downloads the SSL Certificate. /// </summary> /// <param name="account">Existing account</param> /// <param name="completedPromise">The completed certificate fulfillment promise</param> /// <param name="csr">Certificate request</param> /// <param name="certificateType">The type of certificate.</param> /// <returns>The certificate.</returns> public async Task <ArraySegment <byte> > DownloadCertificateAsync(AcmeAccount account, AcmeCertificateFulfillmentPromise completedPromise, Protoacme.Utility.Certificates.CSR csr, CertificateType certificateType) { var directory = await _directoryCache.GetAsync(); var nonce = await _nonceCache.GetAsync(); var finalizeResponse = await _acmeApi.FinalizeCertificatePromiseAsync(account, nonce, completedPromise, csr.Base64UrlEncoded); if (finalizeResponse.Status == AcmeApiResponseStatus.Error) { throw new AcmeProtocolException(finalizeResponse.Message); } var response = await _acmeApi.GetCertificateAsync(finalizeResponse.Data, certificateType); if (response.Status == AcmeApiResponseStatus.Error) { throw new AcmeProtocolException(response.Message); } _nonceCache.Update(finalizeResponse.Nonce); return(response.Data); }
public async Task ACME_Test_90_DeactivateAccount() { AcmeAccount Account = await this.client.GetAccount(); Account = await Account.Deactivate(); }
public AcmeOrder(AcmeAccount account, string url, AcmeOrderPayload info) { this.Account = account; this.Url = url; this.Info = info; }
private async Task <AcmeOrder> OrderCertificate(params string[] Domains) { AcmeAccount Account = await this.client.GetAccount(); return(await Account.OrderCertificate(Domains, null, null)); }