Created
April 10, 2024 03:26
-
-
Save mjsabby/7696fb209e58d8dfbdb8df3d9c195517 to your computer and use it in GitHub Desktop.
C# program that implements ACME protocol to get certificates from Let's Encrypt using DNS Challenge for Cloudflare
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Example appsettings.json | |
{ | |
"AzureManagedIdentityClientId": "YOURGUID", | |
"AzureKeyVaultAADScope": "https://vault.azure.net", | |
"AzureKeyVaultUrl": "https://YOURAKV.vault.azure.net/", | |
"AzureKeyVaultCertificateSecret": "akv_secret_for_cert", | |
"AzureKeyVaultCloudeflareApiKeySecret": "akv_secret_forcloudflareapikey", | |
"AzureKeyVaultLetsEncryptAccountSecret": "akv_secret_for_letsencryptaccountpem", | |
"LetsEncryptACMEDirectoryUrl": "https://acme-v02.api.letsencrypt.org/directory", | |
"LetsEncryptAccountEmail": "email@domain.com", | |
"CloudflareZoneApiUrl": "https://api.cloudflare.com/client/v4/zones/YOURGUID/dns_records", | |
"CloudflareTxtRecordName": "_acme-challenge.domain.com", | |
"DomainName": "*.domain.com", | |
"OrganizationName": "DomainOrg", | |
"LocalityName": "DomainCity", | |
"StateName": "DomainState", | |
"CountryName": "DomainCountry" | |
} | |
*/ | |
namespace CertificateUpdater | |
{ | |
using System; | |
using System.IO; | |
using System.Net.Http; | |
using System.Net.Http.Headers; | |
using System.Security.Cryptography; | |
using System.Security.Cryptography.X509Certificates; | |
using System.Text; | |
using System.Text.Json; | |
using System.Threading.Tasks; | |
internal static class Program | |
{ | |
public static async Task<int> Main() | |
{ | |
var config = CertificateUpdaterConfig.Parse(await File.ReadAllTextAsync(Path.Combine(Directory.GetParent(Environment.ProcessPath).FullName, "appsettings.json")).ConfigureAwait(false)); | |
Console.Write("Fetching managed identity access token ... "); | |
var accessToken = await GetAccessTokenAsync(config.AzureManagedIdentityClientId, config.AzureKeyVaultAADScope).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Fetching account private key from key vault ... "); | |
var accountPem = await GetSecretAsync(config.AzureKeyVaultUrl, config.AzureKeyVaultLetsEncryptAccountSecret, accessToken).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Fetching directory ... "); | |
var directory = await GetACMEDirectory(config.LetsEncryptACMEDirectoryUrl).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Fetching nonce for new account request ... "); | |
var nonce = await GetNonce(directory.NewNonce).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
using ECDsa ecdsa = ECDsa.Create(); | |
ecdsa.ImportFromPem(accountPem); | |
var ec = ecdsa.ExportParameters(includePrivateParameters: false); | |
var x = Base64UrlEncode(ec.Q.X); | |
var y = Base64UrlEncode(ec.Q.Y); | |
Console.Write("Creating/Fetching new account request ... "); | |
var (rr, accountKid) = await CreateNewEntity(directory.NewAccount, CreateNewAccountPayload(directory.NewAccount, nonce, config.LetsEncryptAccountEmail, ecdsa, x, y)).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Fetching nonce for new domain order ... "); | |
nonce = await GetNonce(directory.NewNonce).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Submitting new domain order ... "); | |
var (responseNewOrder, orderUrl) = await CreateNewEntity(directory.NewOrder, CreateNewOrderPayload(directory.NewOrder, accountKid, nonce, config.DomainName, ecdsa)).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.WriteLine(); | |
Console.WriteLine($"Order Url: {orderUrl}"); | |
if (!TryGetAuthorizationAndFinalizeUrl(responseNewOrder, out var finalizeUrl, out var authorizationUrl)) | |
{ | |
Console.WriteLine("[ERROR] Failed to get finalize and authorization URLs"); | |
Console.WriteLine("[RESPNOSE]"); | |
Console.WriteLine(responseNewOrder); | |
return -1; | |
} | |
Console.WriteLine($"Authorization Url: {authorizationUrl}"); | |
Console.WriteLine($"Finalize Url: {finalizeUrl}"); | |
Console.WriteLine(); | |
Console.Write("Fetching challenge information ... "); | |
var responseAuthorization = await GetUrlContentsAsString(authorizationUrl).ConfigureAwait(false); | |
if (!ExtractChallengeInfo(responseAuthorization, out var token, out var status, out var challengeUrl)) | |
{ | |
Console.WriteLine("[ERROR] Failed to get challenge information"); | |
Console.WriteLine("[RESPNOSE]"); | |
Console.WriteLine(responseAuthorization); | |
return -2; | |
} | |
Console.WriteLine("Done."); | |
Console.Write("Fetching Cloudflare api key ... "); | |
var apiKey = await GetSecretAsync(config.AzureKeyVaultUrl, config.AzureKeyVaultCloudeflareApiKeySecret, accessToken).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Fetching Cloudflare zone record id ... "); | |
var recordId = await GetRecordId(config.CloudflareZoneApiUrl, apiKey, config.CloudflareTxtRecordName).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Patching Cloudflare zone record ... "); | |
await PatchRecord($"{config.CloudflareZoneApiUrl}/{recordId}", apiKey, config.CloudflareTxtRecordName, CreateTxtRecord(token, x, y)).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.WriteLine(); | |
Console.Write("Sleeping for 60 seconds to allow DNS propogation ... "); | |
await Task.Delay(60 * 1000).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Generating nonce for challenge validation ... "); | |
nonce = await GetNonce(directory.NewNonce).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.Write("Requesting challenge validation ... "); | |
var (responseChallenge, _) = await CreateNewEntity(challengeUrl, CreateChallengePayload(challengeUrl, accountKid, nonce, ecdsa)).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
int attempt = 1; | |
while (true) | |
{ | |
Console.Write($"Verifying challenge has been validated ... "); | |
responseAuthorization = await GetUrlContentsAsString(authorizationUrl).ConfigureAwait(false); | |
if (ExtractChallengeInfo(responseAuthorization, out _, out status, out _)) | |
{ | |
if (string.Equals(status, "invalid", StringComparison.Ordinal)) | |
{ | |
Console.WriteLine("Failed."); | |
Console.WriteLine("[ERROR]"); | |
Console.WriteLine($"{responseAuthorization}"); | |
return -4; | |
} | |
if (string.Equals(status, "valid", StringComparison.Ordinal)) | |
{ | |
Console.WriteLine("Done."); | |
break; | |
} | |
if (string.Equals(status, "pending", StringComparison.Ordinal)) | |
{ | |
Console.Write("Pending."); | |
if (attempt++ < 5) | |
{ | |
Console.WriteLine(" Retrying in 5 seconds ..."); | |
await Task.Delay(5000).ConfigureAwait(false); | |
} | |
else | |
{ | |
Console.WriteLine(" Retries Exhaused. Failed."); | |
return -5; | |
} | |
} | |
} | |
} | |
Console.Write("Fetching nonce for certificate signing request ... "); | |
nonce = await GetNonce(directory.NewNonce).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
using RSA rsa = RSA.Create(); | |
Console.Write("Submitting certificate signing request ... "); | |
var csrPayload = CreateCertificateSigningRequestPayload(finalizeUrl, accountKid, nonce, $"CN={config.DomainName}, O={config.OrganizationName}, L={config.LocalityName}, ST={config.StateName}, C={config.CountryName}", ecdsa, rsa); | |
(responseNewOrder, orderUrl) = await CreateNewEntity(finalizeUrl, csrPayload).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
if (!TryGetNewOrderCertificateUrl(responseNewOrder, out var certificateUrl)) | |
{ | |
attempt = 1; | |
while (true) | |
{ | |
Console.Write($"Polling Attempt #{attempt} ... "); | |
responseNewOrder = await GetUrlContentsAsString(orderUrl).ConfigureAwait(false); | |
if (TryGetNewOrderCertificateUrl(responseNewOrder, out certificateUrl)) | |
{ | |
break; | |
} | |
if (attempt++ == 6) | |
{ | |
Console.WriteLine("[ERROR] Failed to get valid status information to proceed"); | |
Console.WriteLine("[RESPNOSE]"); | |
Console.WriteLine($"{responseNewOrder}"); | |
return -6; | |
} | |
await Task.Delay(1000).ConfigureAwait(false); | |
} | |
} | |
Console.Write("Fetching certificate ... "); | |
var certificate = await GetUrlContentsAsString(certificateUrl).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
var cert = X509Certificate2.CreateFromPem(certificate); | |
var certWithKey = cert.CopyWithPrivateKey(rsa); | |
var pfx = Convert.ToBase64String(certWithKey.Export(X509ContentType.Pfx)); | |
Console.Write("Storing certificate in key vault ... "); | |
var secretId = await PutSecretAsync(config.AzureKeyVaultUrl, config.AzureKeyVaultCertificateSecret, pfx, accessToken).ConfigureAwait(false); | |
Console.WriteLine("Done."); | |
Console.WriteLine(); | |
Console.WriteLine($"SecretId: {secretId}"); | |
return 0; | |
} | |
private static async Task<ACMEDirectory> GetACMEDirectory(string directoryUrl) | |
{ | |
using var client = new HttpClient(); | |
HttpResponseMessage directoryResponse = await client.GetAsync(new Uri(directoryUrl)).ConfigureAwait(false); | |
directoryResponse.EnsureSuccessStatusCode(); | |
string directoryJson = await directoryResponse.Content.ReadAsStringAsync().ConfigureAwait(false); | |
using JsonDocument doc = JsonDocument.Parse(directoryJson); | |
var root = doc.RootElement; | |
var acmeDirectory = new ACMEDirectory(); | |
foreach (var prop in root.EnumerateObject()) | |
{ | |
if (prop.NameEquals("newAccount")) | |
{ | |
acmeDirectory.NewAccount = prop.Value.GetString(); | |
} | |
if (prop.NameEquals("newOrder")) | |
{ | |
acmeDirectory.NewOrder = prop.Value.GetString(); | |
} | |
if (prop.NameEquals("newNonce")) | |
{ | |
acmeDirectory.NewNonce = prop.Value.GetString(); | |
} | |
} | |
return acmeDirectory; | |
} | |
private static async Task<string> GetNonce(string newNonceUrl) | |
{ | |
using var client = new HttpClient(); | |
using var request = new HttpRequestMessage(HttpMethod.Head, newNonceUrl); | |
HttpResponseMessage response = await client.SendAsync(request).ConfigureAwait(false); | |
response.EnsureSuccessStatusCode(); | |
var nonceHeaders = response.Headers.GetValues("Replay-Nonce"); | |
foreach (var nonceHeader in nonceHeaders) | |
{ | |
return nonceHeader; | |
} | |
throw new Exception("No nonce found in response headers"); | |
} | |
private static async Task<string> GetUrlContentsAsString(string url) | |
{ | |
using var client = new HttpClient(); | |
HttpResponseMessage response = await client.GetAsync(new Uri(url)).ConfigureAwait(false); | |
response.EnsureSuccessStatusCode(); | |
return await response.Content.ReadAsStringAsync().ConfigureAwait(false); | |
} | |
private static async Task<(string response, string location)> CreateNewEntity(string newEntityUrl, string payload) | |
{ | |
using var client = new HttpClient(); | |
using var request = new HttpRequestMessage | |
{ | |
Method = HttpMethod.Post, | |
RequestUri = new Uri(newEntityUrl), | |
Content = new StringContent(payload, new MediaTypeHeaderValue("application/jose+json")) | |
}; | |
HttpResponseMessage response = await client.SendAsync(request).ConfigureAwait(false); | |
var data = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | |
response.EnsureSuccessStatusCode(); | |
return (data, response.Headers.Location.ToString()); | |
} | |
private static bool TryGetNewOrderCertificateUrl(string responseNewOrder, out string certificateUrl) | |
{ | |
certificateUrl = null; | |
using JsonDocument doc = JsonDocument.Parse(responseNewOrder); | |
foreach (var item in doc.RootElement.EnumerateObject()) | |
{ | |
if (item.NameEquals("certificate")) | |
{ | |
certificateUrl = item.Value.GetString(); | |
} | |
} | |
if (certificateUrl is null) | |
{ | |
return false; | |
} | |
return true; | |
} | |
private static bool TryGetAuthorizationAndFinalizeUrl(string responseNewOrder, out string finalizeUrl, out string authorizationUrl) | |
{ | |
finalizeUrl = null; | |
authorizationUrl = null; | |
using JsonDocument doc = JsonDocument.Parse(responseNewOrder); | |
foreach (var item in doc.RootElement.EnumerateObject()) | |
{ | |
if (item.NameEquals("finalize")) | |
{ | |
finalizeUrl = item.Value.GetString(); | |
} | |
if (item.NameEquals("authorizations")) | |
{ | |
foreach (var elem in item.Value.EnumerateArray()) | |
{ | |
authorizationUrl = elem.GetString(); | |
} | |
} | |
} | |
if (finalizeUrl is null || authorizationUrl is null) | |
{ | |
return false; | |
} | |
return true; | |
} | |
private static bool ExtractChallengeInfo(string responseAuthorization, out string token, out string status, out string url) | |
{ | |
token = null; | |
status = null; | |
url = null; | |
using JsonDocument doc = JsonDocument.Parse(responseAuthorization); | |
foreach (var item in doc.RootElement.EnumerateObject()) | |
{ | |
if (item.NameEquals("challenges")) | |
{ | |
foreach (var e in item.Value.EnumerateArray()) | |
{ | |
foreach (var p in e.EnumerateObject()) | |
{ | |
if (p.NameEquals("token")) | |
{ | |
token = p.Value.GetString(); | |
} | |
if (p.NameEquals("status")) | |
{ | |
status = p.Value.GetString(); | |
} | |
if (p.NameEquals("url")) | |
{ | |
url = p.Value.GetString(); | |
} | |
} | |
} | |
} | |
} | |
if (token is null || status is null || url is null) | |
{ | |
return false; | |
} | |
return true; | |
} | |
private static async Task<string> GetAccessTokenAsync(string clientId, string resource) | |
{ | |
using var client = new HttpClient(); | |
if (Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT") is not string identityEndpoint) | |
{ | |
identityEndpoint = $"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&client_id={clientId}&resource={resource}"; | |
client.DefaultRequestHeaders.Add("Metadata", "true"); | |
} | |
else | |
{ | |
identityEndpoint = $"{identityEndpoint.TrimEnd('/')}?api-version=2019-08-01&client_id={clientId}&resource={resource}/.default"; // TODO: why is this /.default needed? | |
} | |
if (Environment.GetEnvironmentVariable("IDENTITY_HEADER") is string identityHeader) | |
{ | |
client.DefaultRequestHeaders.Add("X-IDENTITY-HEADER", identityHeader); | |
} | |
using var request = new HttpRequestMessage(HttpMethod.Get, identityEndpoint); | |
var responseMessage = await client.SendAsync(request).ConfigureAwait(false); | |
responseMessage.EnsureSuccessStatusCode(); | |
var response = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); | |
string accessToken = null; | |
using JsonDocument doc = JsonDocument.Parse(response); | |
foreach (var e in doc.RootElement.EnumerateObject()) | |
{ | |
if (e.NameEquals("access_token")) | |
{ | |
accessToken = e.Value.GetString(); | |
} | |
} | |
return accessToken; | |
} | |
private static async Task<string> GetSecretAsync(string keyVaultUrl, string secretName, string accessToken) | |
{ | |
using var client = new HttpClient(); | |
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); | |
var responseMessage = await client.GetAsync(new Uri($"{keyVaultUrl}/secrets/{secretName}?api-version=7.1")).ConfigureAwait(false); | |
responseMessage.EnsureSuccessStatusCode(); | |
var response = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); | |
string value = null; | |
using JsonDocument doc = JsonDocument.Parse(response); | |
foreach (var e in doc.RootElement.EnumerateObject()) | |
{ | |
if (e.NameEquals("value")) | |
{ | |
value = e.Value.GetString(); | |
} | |
} | |
return value; | |
} | |
private static async Task<string> PutSecretAsync(string keyVaultUrl, string secretName, string secretValue, string accessToken) | |
{ | |
using var client = new HttpClient(); | |
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); | |
using var sc = new StringContent($"{{\"value\":\"{secretValue}\"}}", Encoding.UTF8, "application/json"); | |
var responseMessage = await client.PutAsync(new Uri($"{keyVaultUrl}/secrets/{secretName}?api-version=7.1"), sc).ConfigureAwait(false); | |
responseMessage.EnsureSuccessStatusCode(); | |
var response = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); | |
string value = null; | |
using JsonDocument doc = JsonDocument.Parse(response); | |
foreach (var e in doc.RootElement.EnumerateObject()) | |
{ | |
if (e.NameEquals("id")) | |
{ | |
value = e.Value.GetString(); | |
} | |
} | |
return value; | |
} | |
private static async Task<string> GetRecordId(string apiUrl, string apiKey, string recordName) | |
{ | |
using var client = new HttpClient(); | |
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); | |
var response = await client.GetAsync(new Uri(apiUrl)).ConfigureAwait(false); | |
response.EnsureSuccessStatusCode(); | |
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | |
using JsonDocument doc = JsonDocument.Parse(json); | |
foreach (var p in doc.RootElement.EnumerateObject()) | |
{ | |
if (p.NameEquals("result")) | |
{ | |
foreach (var o in p.Value.EnumerateArray()) | |
{ | |
string id = null; | |
string name = null; | |
foreach (var r in o.EnumerateObject()) | |
{ | |
if (r.NameEquals("name")) | |
{ | |
name = r.Value.GetString(); | |
} | |
if (r.NameEquals("id")) | |
{ | |
id = r.Value.GetString(); | |
} | |
} | |
if (string.Equals(name, recordName, StringComparison.OrdinalIgnoreCase)) | |
{ | |
return id; | |
} | |
} | |
} | |
} | |
throw new Exception("Record not found"); | |
} | |
private static async Task PatchRecord(string apiUrl, string apiKey, string recordName, string recordValue) | |
{ | |
string jsonPayload = $$"""{"type":"TXT","name":"{{recordName}}","content":"{{recordValue}}"}"""; | |
using var client = new HttpClient(); | |
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); | |
using var sc = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); | |
var response = await client.PatchAsync(new Uri(apiUrl), sc).ConfigureAwait(false); | |
var data = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | |
response.EnsureSuccessStatusCode(); | |
} | |
private static string CreateNewAccountPayload(string newAccountUrl, string nonce, string email, ECDsa ecdsa, string x, string y) | |
{ | |
var header = $$"""{"alg":"ES256","jwk":{"alg":"ES256","crv":"P-256","kty":"EC","use":"sig","x":"{{x}}","y":"{{y}}"},"nonce":"{{nonce}}","url":"{{newAccountUrl}}"}"""; | |
var payload = $$"""{"termsOfServiceAgreed":true,"contact":["mailto:{{email}}"]}"""; | |
return CreateJWSCompactSerialization(header, payload, ecdsa); | |
} | |
private static string CreateNewOrderPayload(string newOrderUrl, string kid, string nonce, string domain, ECDsa ecdsa) | |
{ | |
var header = $$"""{"alg":"ES256","kid":"{{kid}}","nonce":"{{nonce}}","url":"{{newOrderUrl}}"}"""; | |
var payload = $$"""{"identifiers":[{"type":"dns","value":"{{domain}}"}]}"""; | |
return CreateJWSCompactSerialization(header, payload, ecdsa); | |
} | |
private static string CreateCertificateSigningRequestPayload(string finalizeUrl, string kid, string nonce, string distinguishedName, ECDsa ecdsa, RSA rsa) | |
{ | |
var header = $$"""{"alg":"ES256","kid":"{{kid}}","nonce":"{{nonce}}","url":"{{finalizeUrl}}"}"""; | |
var payload = $$"""{"csr":"{{CreateCertificateSigningRequest(distinguishedName, rsa)}}"}"""; | |
return CreateJWSCompactSerialization(header, payload, ecdsa); | |
} | |
private static string CreateChallengePayload(string challengeUrl, string kid, string nonce, ECDsa ecdsa) | |
{ | |
var header = $$"""{"alg":"ES256","kid":"{{kid}}","nonce":"{{nonce}}","url":"{{challengeUrl}}"}"""; | |
var payload = $$"""{}"""; | |
return CreateJWSCompactSerialization(header, payload, ecdsa); | |
} | |
private static string CreateJWSCompactSerialization(string header, string payload, ECDsa ecdsa) | |
{ | |
string base64UrlEncodedHeader = Base64UrlEncode(Encoding.UTF8.GetBytes(header)); | |
string base64UrlEncodedPayload = Base64UrlEncode(Encoding.UTF8.GetBytes(payload)); | |
string base64UrlEncodedSignature = Base64UrlEncode(ecdsa.SignData(Encoding.UTF8.GetBytes($"{base64UrlEncodedHeader}.{base64UrlEncodedPayload}"), HashAlgorithmName.SHA256)); | |
return $$"""{"protected":"{{base64UrlEncodedHeader}}","payload":"{{base64UrlEncodedPayload}}","signature":"{{base64UrlEncodedSignature}}"}"""; | |
} | |
private static string CreateTxtRecord(string token, string x, string y) | |
{ | |
var jwkThumbprint = SHA256.HashData(Encoding.UTF8.GetBytes($$"""{"crv":"P-256","kty":"EC","x":"{{x}}","y":"{{y}}"}""")); | |
return Base64UrlEncode(SHA256.HashData(Encoding.UTF8.GetBytes($"{token}.{Base64UrlEncode(jwkThumbprint)}"))); | |
} | |
private static string CreateCertificateSigningRequest(string distinguishedName, RSA rsa) => Base64UrlEncode(new CertificateRequest(new X500DistinguishedName(distinguishedName), rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1).CreateSigningRequest()); | |
private static string Base64UrlEncode(byte[] input) => Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_'); | |
} | |
internal sealed class ACMEDirectory | |
{ | |
public string NewAccount { get; set; } | |
public string NewOrder { get; set; } | |
public string NewNonce { get; set; } | |
} | |
internal sealed class CertificateUpdaterConfig | |
{ | |
public string AzureManagedIdentityClientId { get; set; } | |
public string AzureKeyVaultAADScope { get; set; } | |
public string AzureKeyVaultUrl { get; set; } | |
public string AzureKeyVaultCertificateSecret { get; set; } | |
public string AzureKeyVaultCloudeflareApiKeySecret { get; set; } | |
public string AzureKeyVaultLetsEncryptAccountSecret { get; set; } | |
public string LetsEncryptACMEDirectoryUrl { get; set; } | |
public string LetsEncryptAccountEmail { get; set; } | |
public string CloudflareZoneApiUrl { get; set; } | |
public string CloudflareTxtRecordName { get; set; } | |
public string DomainName { get; set; } | |
public string OrganizationName { get; set; } | |
public string LocalityName { get; set; } | |
public string StateName { get; set; } | |
public string CountryName { get; set; } | |
public static CertificateUpdaterConfig Parse(string json) | |
{ | |
using JsonDocument doc = JsonDocument.Parse(json); | |
JsonElement root = doc.RootElement; | |
return new CertificateUpdaterConfig | |
{ | |
AzureManagedIdentityClientId = root.GetProperty(nameof(AzureManagedIdentityClientId)).GetString(), | |
AzureKeyVaultAADScope = root.GetProperty(nameof(AzureKeyVaultAADScope)).GetString(), | |
AzureKeyVaultUrl = root.GetProperty(nameof(AzureKeyVaultUrl)).GetString(), | |
AzureKeyVaultCertificateSecret = root.GetProperty(nameof(AzureKeyVaultCertificateSecret)).GetString(), | |
AzureKeyVaultCloudeflareApiKeySecret = root.GetProperty(nameof(AzureKeyVaultCloudeflareApiKeySecret)).GetString(), | |
AzureKeyVaultLetsEncryptAccountSecret = root.GetProperty(nameof(AzureKeyVaultLetsEncryptAccountSecret)).GetString(), | |
LetsEncryptACMEDirectoryUrl = root.GetProperty(nameof(LetsEncryptACMEDirectoryUrl)).GetString(), | |
LetsEncryptAccountEmail = root.GetProperty(nameof(LetsEncryptAccountEmail)).GetString(), | |
CloudflareZoneApiUrl = root.GetProperty(nameof(CloudflareZoneApiUrl)).GetString(), | |
CloudflareTxtRecordName = root.GetProperty(nameof(CloudflareTxtRecordName)).GetString(), | |
DomainName = root.GetProperty(nameof(DomainName)).GetString(), | |
OrganizationName = root.GetProperty(nameof(OrganizationName)).GetString(), | |
LocalityName = root.GetProperty(nameof(LocalityName)).GetString(), | |
StateName = root.GetProperty(nameof(StateName)).GetString(), | |
CountryName = root.GetProperty(nameof(CountryName)).GetString() | |
}; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment