diff --git a/src/Acmebot.App/Acme/AcmeClientFactory.cs b/src/Acmebot.App/Acme/AcmeClientFactory.cs index 61199606..57c7aca9 100644 --- a/src/Acmebot.App/Acme/AcmeClientFactory.cs +++ b/src/Acmebot.App/Acme/AcmeClientFactory.cs @@ -63,53 +63,71 @@ private async Task CreateClientCoreAsync() } var signer = accountKey.GenerateSigner(); - var client = new AcmeClient( - _options.Endpoint, - new AcmeClientOptions - { - UserAgent = $"Acmebot/{Constants.ApplicationVersion}" - }); - var directory = await client.GetDirectoryAsync(); - AcmeAccountHandle accountHandle; + AcmeClient? client = null; - if (account is null) + // The client owns an HttpClient, and both it and the signer are only handed to the caller once + // the context has been fully built. Anything that fails in between (a directory fetch against an + // unreachable ACME endpoint, missing EAB credentials, a state store write) would otherwise leak + // them on every retry. + try { - var externalAccountBinding = CreateExternalAccountBinding(); + client = new AcmeClient( + _options.Endpoint, + new AcmeClientOptions + { + UserAgent = $"Acmebot/{Constants.ApplicationVersion}" + }); - if (externalAccountBinding is null && (directory.Metadata?.ExternalAccountRequired ?? false)) + var directory = await client.GetDirectoryAsync(); + AcmeAccountHandle accountHandle; + + if (account is null) { - throw new PreconditionException("This ACME endpoint requires External Account Binding (EAB). Configure EAB credentials and try again."); - } + var externalAccountBinding = CreateExternalAccountBinding(); - accountHandle = await client.CreateAccountAsync( - signer, - new AcmeNewAccountRequest + if (externalAccountBinding is null && (directory.Metadata?.ExternalAccountRequired ?? false)) + { + throw new PreconditionException("This ACME endpoint requires External Account Binding (EAB). Configure EAB credentials and try again."); + } + + accountHandle = await client.CreateAccountAsync( + signer, + new AcmeNewAccountRequest + { + Contact = contacts, + TermsOfServiceAgreed = true + }, + externalAccountBinding); + account = AccountDetails.FromAccountHandle(accountHandle, directory.Metadata?.TermsOfService); + + if (isNewAccountKey) { - Contact = contacts, - TermsOfServiceAgreed = true - }, - externalAccountBinding); - account = AccountDetails.FromAccountHandle(accountHandle, directory.Metadata?.TermsOfService); + await stateStore.SaveAsync(accountKey, "account_key.json"); + } - if (isNewAccountKey) + await stateStore.SaveAsync(account, "account.json"); + } + else { - await stateStore.SaveAsync(accountKey, "account_key.json"); + accountHandle = account.ToAccountHandle(signer); } - await stateStore.SaveAsync(account, "account.json"); + return new AcmeClientContext + { + Client = client, + Directory = directory, + Signer = signer, + Account = accountHandle + }; } - else + catch { - accountHandle = account.ToAccountHandle(signer); - } + // Same order as AcmeClientContext.Dispose(), which owns these once the context exists. + signer.Dispose(); + client?.Dispose(); - return new AcmeClientContext - { - Client = client, - Directory = directory, - Signer = signer, - Account = accountHandle - }; + throw; + } } private AcmeExternalAccountBindingOptions? CreateExternalAccountBinding()