Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for other ACME certification authorities #158

Merged
merged 3 commits into from May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,7 +1,9 @@
namespace AppService.Acmebot
{
public class LetsEncryptOptions
public class AcmebotOptions
{
public string Endpoint { get; set; } = "https://acme-v02.api.letsencrypt.org/";

public string Contacts { get; set; }

public string SubscriptionId { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion AppService.Acmebot/AppService.Acmebot.csproj
Expand Up @@ -14,7 +14,7 @@
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.4.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.3" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.6" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.7" />
<PackageReference Include="WebJobs.Extensions.HttpApi" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
Expand Down
11 changes: 6 additions & 5 deletions AppService.Acmebot/Internal/AcmeProtocolClientFactory.cs
Expand Up @@ -18,22 +18,22 @@ public interface IAcmeProtocolClientFactory

internal class AcmeProtocolClientFactory : IAcmeProtocolClientFactory
{
public AcmeProtocolClientFactory(IOptions<LetsEncryptOptions> options)
public AcmeProtocolClientFactory(IOptions<AcmebotOptions> options)
{
_options = options.Value;
_baseUri = new Uri(_options.Endpoint);
}

private readonly LetsEncryptOptions _options;

private static readonly Uri _acmeEndpoint = new Uri("https://acme-v02.api.letsencrypt.org/");
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 acmeProtocolClient = new AcmeProtocolClient(_acmeEndpoint, directory, account, accountKey?.GenerateSigner());
var acmeProtocolClient = new AcmeProtocolClient(_baseUri, directory, account, accountKey?.GenerateSigner());

if (directory == null)
{
Expand All @@ -56,6 +56,7 @@ public async Task<AcmeProtocolClient> CreateClientAsync()

SaveState(account, "account.json");
SaveState(accountKey, "account_key.json");
SaveState(directory, "directory.json");

acmeProtocolClient.Account = account;
}
Expand Down
4 changes: 2 additions & 2 deletions AppService.Acmebot/Internal/WebhookLifeCycleNotification.cs
Expand Up @@ -8,14 +8,14 @@ namespace AppService.Acmebot.Internal
{
internal class WebhookLifeCycleNotification : ILifeCycleNotificationHelper
{
public WebhookLifeCycleNotification(IHttpClientFactory httpClientFactory, IOptions<LetsEncryptOptions> options)
public WebhookLifeCycleNotification(IHttpClientFactory httpClientFactory, IOptions<AcmebotOptions> options)
{
_httpClientFactory = httpClientFactory;
_options = options.Value;
}

private readonly IHttpClientFactory _httpClientFactory;
private readonly LetsEncryptOptions _options;
private readonly AcmebotOptions _options;

public Task OrchestratorStartingAsync(string hubName, string functionName, string instanceId, bool isReplay)
{
Expand Down
34 changes: 34 additions & 0 deletions AppService.Acmebot/Internal/X509Certificate2Extensions.cs
@@ -0,0 +1,34 @@
using System;
using System.Security.Cryptography.X509Certificates;

namespace AppService.Acmebot.Internal
{
internal static class X509Certificate2Extensions
{
private static ReadOnlySpan<byte> EndCertificate => new byte[]
{
0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43,
0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45,
0x2d, 0x2d, 0x2d, 0x2d, 0x2d
};

public static void ImportFromPem(this X509Certificate2Collection collection, byte[] rawData)
{
var rawDataSpan = rawData.AsSpan();

while (true)
{
var foundIndex = rawDataSpan.IndexOf(EndCertificate);

if (foundIndex == -1)
{
break;
}

collection.Add(new X509Certificate2(rawDataSpan.Slice(0, foundIndex + EndCertificate.Length).ToArray()));

rawDataSpan = rawDataSpan.Slice(foundIndex + EndCertificate.Length);
}
}
}
}
22 changes: 0 additions & 22 deletions AppService.Acmebot/Internal/X509Certificate2Helper.cs

This file was deleted.

19 changes: 13 additions & 6 deletions AppService.Acmebot/SharedFunctions.cs
Expand Up @@ -46,6 +46,12 @@ public class SharedFunctions : ISharedFunctions
private readonly WebSiteManagementClient _webSiteManagementClient;
private readonly DnsManagementClient _dnsManagementClient;

private static readonly string[] _renewalIssuers =
{
"Let's Encrypt Authority X3", "Let's Encrypt Authority X4", "Fake LE Intermediate X1",
"Buypass Class 2 CA 5", "Buypass Class 2 Test4 CA 5"
};

[FunctionName(nameof(GetSite))]
public Task<Site> GetSite([ActivityTrigger] (string, string, string) input)
{
Expand Down Expand Up @@ -83,7 +89,7 @@ public async Task<IList<Certificate>> GetCertificates([ActivityTrigger] DateTime
{
var certificates = await _webSiteManagementClient.Certificates.ListAllAsync();

return certificates.Where(x => x.Issuer == "Let's Encrypt Authority X3" || x.Issuer == "Let's Encrypt Authority X4" || x.Issuer == "Fake LE Intermediate X1")
return certificates.Where(x => _renewalIssuers.Contains(x.Issuer))
.Where(x => (x.ExpirationDate.Value - currentDateTime).TotalDays < 30)
.ToArray();
}
Expand Down Expand Up @@ -351,15 +357,16 @@ public async Task<(string, byte[])> FinalizeOrder([ActivityTrigger] (IList<strin

var certificateData = await httpClient.GetByteArrayAsync(finalize.Payload.Certificate);

// 秘密鍵を含んだ形で X509Certificate2 を作成
var (certificate, chainCertificate) = X509Certificate2Helper.LoadFromPem(certificateData);
// X509Certificate2Collection を作成
var x509Certificates = new X509Certificate2Collection();

var certificateWithPrivateKey = certificate.CopyWithPrivateKey(rsa);
x509Certificates.ImportFromPem(certificateData);

var x509Certificates = new X509Certificate2Collection(new[] { certificateWithPrivateKey, chainCertificate });
// 秘密鍵を含んだ形で X509Certificate2 を作成
x509Certificates[0] = x509Certificates[0].CopyWithPrivateKey(rsa);

// PFX 形式としてエクスポート
return (certificateWithPrivateKey.Thumbprint, x509Certificates.Export(X509ContentType.Pfx, "P@ssw0rd"));
return (x509Certificates[0].Thumbprint, x509Certificates.Export(X509ContentType.Pfx, "P@ssw0rd"));
}

[FunctionName(nameof(UpdateCertificate))]
Expand Down
8 changes: 5 additions & 3 deletions AppService.Acmebot/Startup.cs
Expand Up @@ -43,7 +43,7 @@ public override void Configure(IFunctionsHostBuilder builder)

builder.Services.AddSingleton(provider =>
{
var options = provider.GetRequiredService<IOptions<LetsEncryptOptions>>();
var options = provider.GetRequiredService<IOptions<AcmebotOptions>>();

return new WebSiteManagementClient(new TokenCredentials(new AppAuthenticationTokenProvider()))
{
Expand All @@ -53,7 +53,7 @@ public override void Configure(IFunctionsHostBuilder builder)

builder.Services.AddSingleton(provider =>
{
var options = provider.GetRequiredService<IOptions<LetsEncryptOptions>>();
var options = provider.GetRequiredService<IOptions<AcmebotOptions>>();

return new DnsManagementClient(new TokenCredentials(new AppAuthenticationTokenProvider()))
{
Expand All @@ -65,7 +65,9 @@ public override void Configure(IFunctionsHostBuilder builder)
builder.Services.AddSingleton<IKuduApiClientFactory, KuduApiClientFactory>();
builder.Services.AddSingleton<ILifeCycleNotificationHelper, WebhookLifeCycleNotification>();

builder.Services.Configure<LetsEncryptOptions>(Configuration.GetSection("LetsEncrypt"));
var section = Configuration.GetSection("Acmebot");

builder.Services.Configure<AcmebotOptions>(section.Exists() ? section : Configuration.GetSection("LetsEncrypt"));
}
}
}
26 changes: 21 additions & 5 deletions azuredeploy.json
Expand Up @@ -11,15 +11,27 @@
"mailAddress": {
"type": "string",
"metadata": {
"description": "Email address for Let's Encrypt account."
"description": "Email address for ACME account."
}
},
"acmeEndpoint": {
"type": "string",
"allowedValues": [
"https://acme-v02.api.letsencrypt.org/",
"https://api.buypass.com/acme/"
],
"defaultValue": "https://acme-v02.api.letsencrypt.org/",
"metadata": {
"description": "Certification authority ACME Endpoint."
}
}
},
"variables": {
"functionAppName": "[concat(parameters('appNamePrefix'), '-', substring(uniquestring(resourceGroup().id, deployment().name), 0, 4))]",
"storageAccountName": "[concat(uniquestring(resourceGroup().id, deployment().name), 'azfunctions')]",
"storageAccountId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
"mailAddress": "[parameters('mailAddress')]"
"mailAddress": "[parameters('mailAddress')]",
"acmeEndpoint": "[parameters('acmeEndpoint')]"
},
"resources": [
{
Expand Down Expand Up @@ -105,12 +117,16 @@
"value": "dotnet"
},
{
"name": "LetsEncrypt:SubscriptionId",
"name": "Acmebot:SubscriptionId",
"value": "[subscription().subscriptionId]"
},
{
"name": "LetsEncrypt:Contacts",
"name": "Acmebot:Contacts",
"value": "[variables('mailAddress')]"
},
{
"name": "Acmebot:Endpoint",
"value": "[variables('acmeEndpoint')]"
}
],
"clientAffinityEnabled": false
Expand All @@ -131,4 +147,4 @@
]
}
]
}
}