public AccountDetails(ACMESharp.Protocol.AccountDetails accountDetails, IJwsTool clientSigner) { Kid = accountDetails.Kid; TosLink = accountDetails.TosLink; Account accountPayload = accountDetails.Payload; if (null == accountPayload) { throw new ArgumentNullException("accountDetails.Payload"); } Id = accountPayload.Id; Contact = accountPayload.Contact; // test server returns testing in status if (Enum.TryParse <AccountStatus>(accountPayload.Status, true, out AccountStatus accountStatus)) { Status = accountStatus; } TermsOfServiceAgreed = accountPayload.TermsOfServiceAgreed ?? false; OrdersUrl = accountPayload.Orders; InitialIp = accountPayload.InitialIp; CreatedAt = accountPayload.CreatedAt; Agreement = accountPayload.Agreement; KeyType = clientSigner.JwsAlg; KeyExport = clientSigner.Export(); }
// TODO: handle "Change of TOS" error response // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.3.4 /// <summary> /// Rotates the current Public key that is associated with this Account by the /// target ACME CA with a new Public key. If successful, updates the current /// Account key pair registered with the client. /// </summary> /// <remarks> /// https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3.5 /// </remarks> public async Task <AccountDetails> ChangeAccountKeyAsync(IJwsTool newSigner, CancellationToken cancel = default(CancellationToken)) { if (Account == null) { Signer = newSigner; return(null); } var requUrl = new Uri(_http.BaseAddress, Directory.KeyChange); var message = new KeyChangeRequest { Account = Account.Kid, OldKey = Signer.ExportJwk(), }; var innerPayload = ComputeAcmeSigned(message, requUrl.ToString(), signer: newSigner, includePublicKey: true, excludeNonce: true); var resp = await SendAcmeAsync( requUrl, method : HttpMethod.Post, message : innerPayload, cancel : cancel); Signer = newSigner; return(await DecodeAccountResponseAsync(resp, existing : Account)); }
internal async Task ConfigureAcmeClient() { var httpClient = _proxyService.GetHttpClient(); httpClient.BaseAddress = _settings.BaseUri; _log.Verbose("Loading ACME account signer..."); IJwsTool signer = null; var accountSigner = AccountSigner; if (accountSigner != null) { signer = accountSigner.JwsTool(); } _log.Verbose("Constructing ACME protocol client..."); try { _client = new AcmeProtocolClient( httpClient, signer: signer, usePostAsGet: _settings.Acme.PostAsGet); } catch (CryptographicException) { if (signer == null) { // There has been a problem generate a signer for the // new account, possibly because some EC curve is not // on available on the system? So we give it another // shot with a less fancy RSA signer _log.Verbose("First chance error generating new signer, retrying with RSA instead of ECC"); signer = new RSJwsTool { KeySize = _settings.Security.RSAKeyBits }; signer.Init(); _client = new AcmeProtocolClient( httpClient, signer: signer, usePostAsGet: _settings.Acme.PostAsGet); } else { throw; } } _client.BeforeHttpSend = (x, r) => _log.Debug("Send {method} request to {uri}", r.Method, r.RequestUri); _client.AfterHttpSend = (x, r) => _log.Verbose("Request completed with status {s}", r.StatusCode); _client.Directory = await _client.GetDirectoryAsync(); await _client.GetNonceAsync(); _client.Account = await LoadAccount(signer); if (_client.Account == null) { throw new Exception("AcmeClient was unable to find or create an account"); } }
/// <summary> /// </summary> /// <remarks> /// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-8 /// </remarks> public static IChallengeValidationDetails DecodeChallengeValidation( Authorization authz, string challengeType, IJwsTool signer) { var challenge = authz.Challenges.Where(x => x.Type == challengeType) .FirstOrDefault(); if (challenge == null) { throw new InvalidOperationException( $"Challenge type [{challengeType}] not found for given Authorization"); } switch (challengeType) { case Dns01ChallengeValidationDetails.Dns01ChallengeType: return(ResolveChallengeForDns01(authz, challenge, signer)); case Http01ChallengeValidationDetails.Http01ChallengeType: return(ResolveChallengeForHttp01(authz, challenge, signer)); case TlsAlpn01ChallengeValidationDetails.TlsAlpn01ChallengeType: return(ResolveChallengeForTlsAlpn01(authz, challenge, signer)); } throw new NotImplementedException( $"Unknown or unsupported Challenge type [{challengeType}]"); }
/// <summary> /// Computes the ACME Key Authorization of the JSON Web Key (JWK) of an argument /// Signer as prescribed in the /// <see href="https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.1" /// >ACME specification, section 7.1</see>. /// </summary> public static string ComputeKeyAuthorization(IJwsTool signer, string token) { using (var sha = SHA256.Create()) { var jwkThumb = CryptoHelper.Base64.UrlEncode(ComputeThumbprint(signer, sha)); return($"{token}.{jwkThumb}"); } }
public AcmeProtocolClient(HttpClient http, ServiceDirectory dir = null, AccountDetails acct = null, IJwsTool signer = null, bool disposeHttpClient = false, ILogger logger = null) { Init(http, dir, acct, signer, logger); _disposeHttpClient = disposeHttpClient; }
/// <summary> /// Computes a SHA256 digest over the <see cref="ComputeKeyAuthorization" /// >ACME Key Authorization</see> as required by some of the ACME Challenge /// responses. /// </summary> public static string ComputeKeyAuthorizationDigest(IJwsTool signer, string token) { using (var sha = SHA256.Create()) { var jwkThumb = CryptoHelper.Base64.UrlEncode(ComputeThumbprint(signer, sha)); var keyAuthz = $"{token}.{jwkThumb}"; var keyAuthzDig = sha.ComputeHash(Encoding.UTF8.GetBytes(keyAuthz)); return(CryptoHelper.Base64.UrlEncode(keyAuthzDig)); } }
/// <summary> /// </summary> /// <remarks> /// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05 /// </remarks> public static TlsAlpn01ChallengeValidationDetails ResolveChallengeForTlsAlpn01( Authorization authz, Challenge challenge, IJwsTool signer) { var keyAuthz = JwsHelper.ComputeKeyAuthorization(signer, challenge.Token); return(new TlsAlpn01ChallengeValidationDetails { TokenValue = keyAuthz, }); }
private async Task <AccountDetails> LoadAccount(IJwsTool signer) { AccountDetails account = null; if (File.Exists(AccountPath)) { if (signer != null) { _log.Debug("Loading account information from {registrationPath}", AccountPath); account = JsonConvert.DeserializeObject <AccountDetails>(File.ReadAllText(AccountPath)); _client.Account = account; // Maybe we should update the account details // on every start of the program to figure out // if it hasn't been suspended or cancelled? // UpdateAccount(); } else { _log.Error("Account found but no valid signer could be loaded"); } } else { var contacts = await GetContacts(); var(_, filename, content) = await _client.GetTermsOfServiceAsync(); if (!_arguments.MainArguments.AcceptTos) { var tosPath = Path.Combine(_settings.Client.ConfigurationPath, filename); File.WriteAllBytes(tosPath, content); _input.Show($"Terms of service", tosPath); if (await _input.PromptYesNo($"Open in default application?", false)) { Process.Start(tosPath); } if (!await _input.PromptYesNo($"Do you agree with the terms?", true)) { return(null); } } account = await _client.CreateAccountAsync(contacts, termsOfServiceAgreed : true); _log.Debug("Saving registration"); var accountKey = new AccountSigner { KeyType = _client.Signer.JwsAlg, KeyExport = _client.Signer.Export(), }; AccountSigner = accountKey; File.WriteAllText(AccountPath, JsonConvert.SerializeObject(account)); } return(account); }
public AcmeProtocolClient(Uri baseUri, ServiceDirectory dir = null, AccountDetails acct = null, IJwsTool signer = null, ILogger logger = null) { var http = new HttpClient { BaseAddress = baseUri, }; Init(http, dir, acct, signer, logger); _disposeHttpClient = true; }
/// <summary> /// Computes a thumbprint of the JWK using the argument Hash Algorithm /// as per <see href="https://tools.ietf.org/html/rfc7638">RFC 7638</see>, /// JSON Web Key (JWK) Thumbprint. /// </summary> public static byte[] ComputeThumbprint(IJwsTool signer, HashAlgorithm algor) { // As per RFC 7638 Section 3, we export the JWK in a canonical form // and then produce a JSON object with no whitespace or line breaks var jwkCanon = signer.ExportJwk(true); var jwkJson = JsonConvert.SerializeObject(jwkCanon, Formatting.None); var jwkBytes = Encoding.UTF8.GetBytes(jwkJson); var jwkHash = algor.ComputeHash(jwkBytes); return(jwkHash); }
/// <summary> /// </summary> /// <remarks> /// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-8.4 /// </remarks> public static Dns01ChallengeValidationDetails ResolveChallengeForDns01( Authorization authz, Challenge challenge, IJwsTool signer) { var keyAuthzDigested = JwsHelper.ComputeKeyAuthorizationDigest( signer, challenge.Token); return(new Dns01ChallengeValidationDetails { DnsRecordName = $@"{Dns01ChallengeValidationDetails.DnsRecordNamePrefix}.{ authz.Identifier.Value}", DnsRecordType = Dns01ChallengeValidationDetails.DnsRecordTypeDefault, DnsRecordValue = keyAuthzDigested, }); }
private void Init(HttpClient http, ServiceDirectory dir, AccountDetails acct, IJwsTool signer, ILogger logger) { _http = http; Directory = dir ?? new ServiceDirectory(); Account = acct; Signer = signer ?? ResolveDefaultSigner(); _log = logger ?? NullLogger.Instance; _log.LogInformation("ACME client initialized"); }
private async Task <AccountDetails> LoadAccount(IJwsTool signer) { AccountDetails account = null; if (File.Exists(AccountPath)) { if (signer != null) { _log.Debug("Loading account information from {registrationPath}", AccountPath); account = JsonConvert.DeserializeObject <AccountDetails>(File.ReadAllText(AccountPath)); } else { _log.Error("Account found but no valid Signer could be loaded"); } } else { var contacts = GetContacts(); var(contentType, filename, content) = await _client.GetTermsOfServiceAsync(); if (!_optionsService.MainArguments.AcceptTos) { var tosPath = Path.Combine(_settings.ConfigPath, filename); File.WriteAllBytes(tosPath, content); _input.Show($"Terms of service", tosPath); if (_input.PromptYesNo($"Open in default application?")) { Process.Start(tosPath); } if (!_input.PromptYesNo($"Do you agree with the terms?")) { return(null); } } account = await _client.CreateAccountAsync(contacts, termsOfServiceAgreed : true); _log.Debug("Saving registration"); var accountKey = new AccountSigner { KeyType = _client.Signer.JwsAlg, KeyExport = _client.Signer.Export(), }; AccountSigner = accountKey; File.WriteAllText(AccountPath, JsonConvert.SerializeObject(account)); } return(account); }
/// <summary> /// </summary> /// <remarks> /// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-8.3 /// </remarks> public static Http01ChallengeValidationDetails ResolveChallengeForHttp01( Authorization authz, Challenge challenge, IJwsTool signer) { var keyAuthz = JwsHelper.ComputeKeyAuthorization( signer, challenge.Token); return(new Http01ChallengeValidationDetails { HttpResourceUrl = $@"http://{authz.Identifier.Value}/{ Http01ChallengeValidationDetails.HttpPathPrefix}/{ challenge.Token}", HttpResourcePath = $@"{Http01ChallengeValidationDetails.HttpPathPrefix}/{ challenge.Token}", HttpResourceContentType = Http01ChallengeValidationDetails.HttpResourceContentTypeDefault, HttpResourceValue = keyAuthz, }); }
private void Init(HttpClient http, ServiceDirectory dir, AccountDetails acct, IJwsTool signer, ILogger logger) { _http = http; Directory = dir ?? new ServiceDirectory(); Account = acct; // We default to ES256 signer if (signer == null) { signer = new Crypto.JOSE.Impl.ESJwsTool(); signer.Init(); } Signer = signer; _log = logger ?? NullLogger.Instance; _log.LogInformation("ACME client initialized"); }
private async Task <bool> ConfigureAcmeClient() { var httpClientHandler = new HttpClientHandler() { Proxy = _proxyService.GetWebProxy(), }; var httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(_arguments.MainArguments.GetBaseUri()) }; _log.Verbose("Loading ACME account signer..."); IJwsTool signer = null; var accountSigner = AccountSigner; if (accountSigner != null) { signer = accountSigner.JwsTool(); } _log.Verbose("Constructing ACME protocol client..."); _client = new AcmeProtocolClient(httpClient, signer: signer) { BeforeHttpSend = (x, r) => { _log.Debug("Send {method} request to {uri}", r.Method, r.RequestUri); }, }; _client.Directory = await _client.GetDirectoryAsync(); await _client.GetNonceAsync(); _client.Account = await LoadAccount(signer); if (_client.Account == null) { throw new Exception("AcmeClient was unable to find or create an account"); } return(true); }
void ValidateAccount(DbAccount acct, JwsSignedPayload signedPayload) { var ph = ExtractProtectedHeader(signedPayload); var jwk = JsonConvert.DeserializeObject <Dictionary <string, string> >(acct.Jwk); if (string.IsNullOrEmpty(ph.Alg)) { throw new Exception("invalid JWS header, missing 'alg'"); } if (string.IsNullOrEmpty(ph.Url)) { throw new Exception("invalid JWS header, missing 'url'"); } if (string.IsNullOrEmpty(ph.Nonce)) { throw new Exception("invalid JWS header, missing 'nonce'"); } IJwsTool tool = null; switch (ph.Alg) { case "RS256": tool = new RSJwsTool { HashSize = 256 }; ((RSJwsTool)tool).ImportJwk(acct.Jwk); break; case "RS384": tool = new RSJwsTool { HashSize = 384 }; ((RSJwsTool)tool).ImportJwk(acct.Jwk); break; case "RS512": tool = new RSJwsTool { HashSize = 512 }; ((RSJwsTool)tool).ImportJwk(acct.Jwk); break; case "ES256": tool = new ESJwsTool { HashSize = 256 }; break; case "ES384": tool = new ESJwsTool { HashSize = 384 }; break; case "ES512": tool = new ESJwsTool { HashSize = 512 }; break; default: throw new Exception("unknown or unsupported signature algorithm"); } var sig = CryptoHelper.Base64.UrlDecode(signedPayload.Signature); var pld = CryptoHelper.Base64.UrlDecode(signedPayload.Payload); var prt = CryptoHelper.Base64.UrlDecode(signedPayload.Protected); var sigInput = $"{signedPayload.Protected}.{signedPayload.Payload}"; var sigInputBytes = Encoding.ASCII.GetBytes(sigInput); if (!tool.Verify(sigInputBytes, sig)) { throw new Exception("account signature failure"); } }
private async Task <bool> ConfigureAcmeClient() { var httpClientHandler = new HttpClientHandler() { Proxy = _proxyService.GetWebProxy(), }; var httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(_arguments.MainArguments.GetBaseUri()) }; _log.Verbose("Loading ACME account signer..."); IJwsTool signer = null; var accountSigner = AccountSigner; if (accountSigner != null) { signer = accountSigner.JwsTool(); } _log.Verbose("Constructing ACME protocol client..."); try { _client = new AcmeProtocolClient(httpClient, signer: signer); } catch (CryptographicException) { if (signer == null) { // There has been a problem generate a signer for the // new account, possibly because some EC curve is not // on available on the system? So we give it another // shot with a less fancy RSA signer _log.Verbose("First chance error generating new signer, retrying with RSA instead of ECC"); signer = new RSJwsTool { KeySize = new Rsa(_log, new RsaOptions()).GetRsaKeyBits() }; signer.Init(); _client = new AcmeProtocolClient(httpClient, signer: signer); } else { throw; } } _client.BeforeHttpSend = (x, r) => { _log.Debug("Send {method} request to {uri}", r.Method, r.RequestUri); }; _client.AfterHttpSend = (x, r) => { _log.Verbose("Request completed with status {s}", r.StatusCode); }; _client.Directory = await _client.GetDirectoryAsync(); await _client.GetNonceAsync(); _client.Account = await LoadAccount(signer); if (_client.Account == null) { throw new Exception("AcmeClient was unable to find or create an account"); } return(true); }
public AccountSigner(IJwsTool source) { KeyType = source.JwsAlg; KeyExport = source.Export(); }