Skip to content

Commit

Permalink
Fixed buypass certificate issuing error (#447)
Browse files Browse the repository at this point in the history
  • Loading branch information
shibayan committed Mar 5, 2024
1 parent f06ca1d commit 41df4c0
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 83 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

env:
DOTNET_VERSION: 6.0.x
BICEP_VERSION: 0.22.6
BICEP_VERSION: 0.25.53

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

env:
DOTNET_VERSION: 6.0.x
BICEP_VERSION: 0.22.6
BICEP_VERSION: 0.25.53

jobs:
publish:
Expand Down
4 changes: 2 additions & 2 deletions AppService.Acmebot/Functions/SharedActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public async Task<IReadOnlyList<CertificateItem>> GetExpiringCertificates([Activ

await foreach (var certificate in subscription.GetAppCertificatesAsync())
{
if (!certificate.Data.TagsFilter(IssuerName, _options.Endpoint))
if (!certificate.Data.IsIssuedByAcmebot() || !certificate.Data.IsSameEndpoint(_options.Endpoint))
{
continue;
}
Expand Down Expand Up @@ -698,7 +698,7 @@ public async Task<CertificateItem> UploadCertificate([ActivityTrigger] (string,
Tags =
{
{ "Issuer", IssuerName },
{ "Endpoint", _options.Endpoint },
{ "Endpoint", _options.Endpoint.Host },
{ "ForceDns01Challenge", forceDns01Challenge.ToString() }
}
});
Expand Down
89 changes: 57 additions & 32 deletions AppService.Acmebot/Internal/AcmeProtocolClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading.Tasks;

Expand All @@ -21,34 +22,38 @@ public class AcmeProtocolClientFactory
public AcmeProtocolClientFactory(IOptions<AcmebotOptions> options)
{
_options = options.Value;
_baseUri = new Uri(_options.Endpoint);
}

private readonly AcmebotOptions _options;
private readonly Uri _baseUri;

public async Task<AcmeProtocolClient> CreateClientAsync()
{
var account = LoadState<AccountDetails>("account.json");
var accountKey = LoadState<AccountKey>("account_key.json");
var directory = LoadState<ServiceDirectory>("directory.json");
var directory = LoadTempState<ServiceDirectory>("directory.json");

var acmeProtocolClient = new AcmeProtocolClient(_baseUri, directory, account, accountKey?.GenerateSigner(), usePostAsGet: true);
var acmeProtocolClient = new AcmeProtocolClient(_options.Endpoint, directory, account, accountKey?.GenerateSigner(), usePostAsGet: true)
{
BeforeHttpSend = (_, req) =>
{
req.Headers.UserAgent.Add(new ProductInfoHeaderValue("AppService-Acmebot", Constants.ApplicationVersion));
}
};

if (directory is null)
{
try
{
directory = await acmeProtocolClient.GetDirectoryAsync();
}
catch (AcmeProtocolException)
catch
{
acmeProtocolClient.Directory.Directory = "";
acmeProtocolClient.Directory.Directory = "directory";

directory = await acmeProtocolClient.GetDirectoryAsync();
}

SaveState(directory, "directory.json");
SaveTempState(directory, "directory.json");

acmeProtocolClient.Directory = directory;
}
Expand All @@ -57,7 +62,12 @@ public async Task<AcmeProtocolClient> CreateClientAsync()

if (acmeProtocolClient.Account is null)
{
var externalAccountBinding = directory.Meta.ExternalAccountRequired ?? false ? CreateExternalAccountBinding(acmeProtocolClient) : null;
var externalAccountBinding = CreateExternalAccountBinding(acmeProtocolClient);

if (externalAccountBinding is null && (directory.Meta.ExternalAccountRequired ?? false))
{
throw new PreconditionException("This ACME endpoint requires External Account Binding.");
}

account = await acmeProtocolClient.CreateAccountAsync(new[] { $"mailto:{_options.Contacts}" }, true, externalAccountBinding);

Expand Down Expand Up @@ -87,6 +97,11 @@ public async Task<AcmeProtocolClient> CreateClientAsync()

private object CreateExternalAccountBinding(AcmeProtocolClient acmeProtocolClient)
{
if (string.IsNullOrEmpty(_options.ExternalAccountBinding?.KeyId) || string.IsNullOrEmpty(_options.ExternalAccountBinding?.HmacKey))
{
return null;
}

byte[] HmacSignature(byte[] x)
{
var hmacKeyBytes = CryptoHelper.Base64.UrlDecode(_options.ExternalAccountBinding.HmacKey);
Expand All @@ -96,17 +111,12 @@ byte[] HmacSignature(byte[] x)
"HS256" => new HMACSHA256(hmacKeyBytes),
"HS384" => new HMACSHA384(hmacKeyBytes),
"HS512" => new HMACSHA512(hmacKeyBytes),
_ => throw new NotSupportedException($"The signature algorithm {_options.ExternalAccountBinding.Algorithm} is not supported.")
_ => throw new NotSupportedException($"The signature algorithm {_options.ExternalAccountBinding.Algorithm} is not supported. (supported values are HS256 / HS384 / HS512)")
});

return hmac.ComputeHash(x);
}

if (string.IsNullOrEmpty(_options.ExternalAccountBinding.KeyId) || string.IsNullOrEmpty(_options.ExternalAccountBinding.HmacKey))
{
throw new PreconditionException("This ACME endpoint requires External Account Binding.");
}

var payload = JsonConvert.SerializeObject(acmeProtocolClient.Signer.ExportJwk());

var protectedHeaders = new
Expand All @@ -125,33 +135,46 @@ private TState LoadState<TState>(string path)

if (!File.Exists(fullPath))
{
// Fallback legacy state
var legacyFullPath = Environment.ExpandEnvironmentVariables(@"%HOME%\.acme\" + path);

if (!File.Exists(legacyFullPath))
{
return default;
}
return default;
}

var json = File.ReadAllText(legacyFullPath);
var json = File.ReadAllText(fullPath);

var state = JsonConvert.DeserializeObject<TState>(json);
return JsonConvert.DeserializeObject<TState>(json);
}

SaveState(state, path);
private void SaveState<TState>(TState value, string path)
{
var fullPath = ResolveStateFullPath(path);
var directoryPath = Path.GetDirectoryName(fullPath);

return state;
}
else
if (!Directory.Exists(directoryPath))
{
var json = File.ReadAllText(fullPath);
Directory.CreateDirectory(directoryPath);
}

var json = JsonConvert.SerializeObject(value, Formatting.Indented);

File.WriteAllText(fullPath, json);
}

private TState LoadTempState<TState>(string path)
{
var fullPath = ResolveTempStateFullPath(path);

return JsonConvert.DeserializeObject<TState>(json);
if (!File.Exists(fullPath))
{
return default;
}

var json = File.ReadAllText(fullPath);

return JsonConvert.DeserializeObject<TState>(json);
}

private void SaveState<TState>(TState value, string path)
private void SaveTempState<TState>(TState value, string path)
{
var fullPath = ResolveStateFullPath(path);
var fullPath = ResolveTempStateFullPath(path);
var directoryPath = Path.GetDirectoryName(fullPath);

if (!Directory.Exists(directoryPath))
Expand All @@ -164,5 +187,7 @@ private void SaveState<TState>(TState value, string path)
File.WriteAllText(fullPath, json);
}

private string ResolveStateFullPath(string path) => Environment.ExpandEnvironmentVariables($"%HOME%/data/.acmebot/{_baseUri.Host}/{path}");
private string ResolveStateFullPath(string path) => Environment.ExpandEnvironmentVariables($"%HOME%/data/.acmebot/{_options.Endpoint.Host}/{path}");

private string ResolveTempStateFullPath(string path) => Environment.ExpandEnvironmentVariables($"%TEMP%/.acmebot/{_options.Endpoint.Host}/{path}");
}
20 changes: 3 additions & 17 deletions AppService.Acmebot/Internal/ApplicationVersionInitializer.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
using System.Reflection;

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;

namespace AppService.Acmebot.Internal;

internal class ApplicationVersionInitializer<TStartup> : ITelemetryInitializer
internal class ApplicationVersionInitializer : ITelemetryInitializer
{
public ApplicationVersionInitializer()
{
ApplicationVersion = typeof(TStartup).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
}

public string ApplicationVersion { get; }

public void Initialize(ITelemetry telemetry)
{
telemetry.Context.Component.Version = ApplicationVersion;
}
public void Initialize(ITelemetry telemetry) => telemetry.Context.Component.Version = Constants.ApplicationVersion;
}
36 changes: 19 additions & 17 deletions AppService.Acmebot/Internal/CertificateExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
using Azure.ResourceManager.AppService;
using System;
using System.Collections.Generic;

using Azure.ResourceManager.AppService;

namespace AppService.Acmebot.Internal;

internal static class CertificateExtensions
{
public static bool TagsFilter(this AppCertificateData certificate, string issuer, string endpoint)
public static bool IsIssuedByAcmebot(this AppCertificateData certificateData)
{
var tags = certificate.Tags;
return certificateData.Tags.TryGetIssuer(out var tagIssuer) && tagIssuer == IssuerValue;
}

if (tags is null)
{
return false;
}
public static bool IsSameEndpoint(this AppCertificateData certificateData, Uri endpoint)
{
return certificateData.Tags.TryGetEndpoint(out var tagEndpoint) && NormalizeEndpoint(tagEndpoint) == endpoint.Host;
}

if (!tags.TryGetValue("Issuer", out var tagIssuer) || tagIssuer != issuer)
{
return false;
}
private const string IssuerKey = "Issuer";
private const string EndpointKey = "Endpoint";

if (!tags.TryGetValue("Endpoint", out var tagEndpoint) || tagEndpoint != endpoint)
{
return false;
}
private const string IssuerValue = "Acmebot";

return true;
}
private static bool TryGetIssuer(this IDictionary<string, string> tags, out string issuer) => tags.TryGetValue(IssuerKey, out issuer);

private static bool TryGetEndpoint(this IDictionary<string, string> tags, out string endpoint) => tags.TryGetValue(EndpointKey, out endpoint);

private static string NormalizeEndpoint(string endpoint) => Uri.TryCreate(endpoint, UriKind.Absolute, out var legacyEndpoint) ? legacyEndpoint.Host : endpoint;
}
10 changes: 10 additions & 0 deletions AppService.Acmebot/Internal/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Reflection;

namespace AppService.Acmebot.Internal;

internal static class Constants
{
public static string ApplicationVersion { get; } = typeof(Startup).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
}
6 changes: 3 additions & 3 deletions AppService.Acmebot/Options/AcmebotOptions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;

namespace AppService.Acmebot.Options;

public class AcmebotOptions
{
[Required]
[Url]
public string Endpoint { get; set; } = "https://acme-v02.api.letsencrypt.org/";
public Uri Endpoint { get; set; }

[Required]
public string Contacts { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion AppService.Acmebot/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public override void Configure(IFunctionsHostBuilder builder)
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});

builder.Services.AddSingleton<ITelemetryInitializer, ApplicationVersionInitializer<Startup>>();
builder.Services.AddSingleton<ITelemetryInitializer, ApplicationVersionInitializer>();

builder.Services.AddSingleton(new LookupClient(new LookupClientOptions(NameServer.GooglePublicDns, NameServer.GooglePublicDns2)
{
Expand Down
7 changes: 4 additions & 3 deletions azuredeploy.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ param mailAddress string

@description('Certification authority ACME Endpoint.')
@allowed([
'https://acme-v02.api.letsencrypt.org/'
'https://api.buypass.com/acme/'
'https://acme-v02.api.letsencrypt.org/directory'
'https://api.buypass.com/acme/directory'
'https://acme.zerossl.com/v2/DV90/'
'https://dv.acme-v02.api.pki.goog/directory'
])
param acmeEndpoint string = 'https://acme-v02.api.letsencrypt.org/'
param acmeEndpoint string = 'https://acme-v02.api.letsencrypt.org/directory'

var functionAppName = 'func-${appNamePrefix}-${substring(uniqueString(resourceGroup().id, deployment().name), 0, 4)}'
var appServicePlanName = 'plan-${appNamePrefix}-${substring(uniqueString(resourceGroup().id, deployment().name), 0, 4)}'
Expand Down
13 changes: 7 additions & 6 deletions azuredeploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.22.6.54827",
"templateHash": "18175046511427201249"
"version": "0.25.53.49325",
"templateHash": "11728208178327999889"
}
},
"parameters": {
Expand All @@ -31,11 +31,12 @@
},
"acmeEndpoint": {
"type": "string",
"defaultValue": "https://acme-v02.api.letsencrypt.org/",
"defaultValue": "https://acme-v02.api.letsencrypt.org/directory",
"allowedValues": [
"https://acme-v02.api.letsencrypt.org/",
"https://api.buypass.com/acme/",
"https://acme.zerossl.com/v2/DV90/"
"https://acme-v02.api.letsencrypt.org/directory",
"https://api.buypass.com/acme/directory",
"https://acme.zerossl.com/v2/DV90/",
"https://dv.acme-v02.api.pki.goog/directory"
],
"metadata": {
"description": "Certification authority ACME Endpoint."
Expand Down

0 comments on commit 41df4c0

Please sign in to comment.