Merge branch 'master' of gitlab:hailstrike/letsencrypt

This commit is contained in:
Maksym Sadovnychyy 2020-02-24 07:47:02 +01:00
commit 0e8dace2ab
11 changed files with 205 additions and 58 deletions

View File

@ -10,7 +10,7 @@
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path. // If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/netcoreapp2.2/LetsEncrypt.dll", "program": "${workspaceFolder}/bin/Debug/netcoreapp3.1/LetsEncrypt.dll",
"args": [], "args": [],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console

View File

@ -7,6 +7,8 @@ using System.Linq;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Mono.Unix;
using LetsEncrypt.Services; using LetsEncrypt.Services;
using LetsEncrypt.Helpers; using LetsEncrypt.Helpers;
using LetsEncrypt.Entities; using LetsEncrypt.Entities;
@ -20,11 +22,13 @@ namespace LetsEncrypt
private readonly AppSettings _appSettings; private readonly AppSettings _appSettings;
private readonly ILetsEncryptService _letsEncryptService; private readonly ILetsEncryptService _letsEncryptService;
private readonly IKeyService _keyService; private readonly IKeyService _keyService;
private readonly ITerminalService _terminalService;
public App(IOptions<AppSettings> appSettings, ILetsEncryptService letsEncryptService, IKeyService keyService) { public App(IOptions<AppSettings> appSettings, ILetsEncryptService letsEncryptService, IKeyService keyService, ITerminalService terminalService) {
_appSettings = appSettings.Value; _appSettings = appSettings.Value;
_letsEncryptService = letsEncryptService; _letsEncryptService = letsEncryptService;
_keyService = keyService; _keyService = keyService;
_terminalService = terminalService;
} }
public void Run() { public void Run() {
@ -46,7 +50,7 @@ namespace LetsEncrypt
try { try {
//define cache folder //define cache folder
string cache = Path.Combine(AppPath, "cache", customer.id); string cache = Path.Combine(AppPath, env.cache, customer.id);
if(!Directory.Exists(cache)) { if(!Directory.Exists(cache)) {
Directory.CreateDirectory(cache); Directory.CreateDirectory(cache);
} }
@ -70,16 +74,16 @@ namespace LetsEncrypt
CachedCertificateResult certRes = new CachedCertificateResult(); CachedCertificateResult certRes = new CachedCertificateResult();
if (_letsEncryptService.TryGetCachedCertificate(site.name, out certRes)) { if (_letsEncryptService.TryGetCachedCertificate(site.name, out certRes)) {
string cert = Path.Combine(ssl, site.name + ".crt"); string cert = Path.Combine(ssl, site.name + ".crt");
if(!File.Exists(cert)) //if(!File.Exists(cert))
File.WriteAllText(cert, certRes.Certificate); File.WriteAllText(cert, certRes.Certificate);
string key = Path.Combine(ssl, site.name + ".key"); string key = Path.Combine(ssl, site.name + ".key");
if(!File.Exists(key)) { //if(!File.Exists(key)) {
using (StreamWriter writer = File.CreateText(key)) using (StreamWriter writer = File.CreateText(key))
_keyService.ExportPrivateKey(certRes.PrivateKey, writer); _keyService.ExportPrivateKey(certRes.PrivateKey, writer);
} //}
Console.WriteLine("Certificate and Key exists and valid."); Console.WriteLine("Certificate and Key exists and valid. Restored from cache.");
} }
else { else {
//new nonce //new nonce
@ -103,8 +107,11 @@ namespace LetsEncrypt
throw new DirectoryNotFoundException(string.Format("Directory {0} wasn't created", acme)); throw new DirectoryNotFoundException(string.Format("Directory {0} wasn't created", acme));
} }
foreach (FileInfo file in new DirectoryInfo(acme).GetFiles()) foreach (FileInfo file in new DirectoryInfo(acme).GetFiles()) {
file.Delete(); if(file.LastWriteTimeUtc < DateTime.UtcNow.AddMonths(-3))
file.Delete();
}
foreach (var result in orders.Result) foreach (var result in orders.Result)
{ {
@ -115,6 +122,9 @@ namespace LetsEncrypt
File.WriteAllText(token, splitToken[1]); File.WriteAllText(token, splitToken[1]);
} }
_terminalService.Exec("chgrp -R nginx /var/www");
_terminalService.Exec("chmod -R g+rwx /var/www");
break; break;
} }
@ -175,6 +185,10 @@ namespace LetsEncrypt
Console.WriteLine(ex.Message.ToString()); Console.WriteLine(ex.Message.ToString());
} }
} }
_terminalService.Exec("systemctl restart nginx");
} }
catch (Exception ex) { catch (Exception ex) {
Console.WriteLine(ex.Message.ToString()); Console.WriteLine(ex.Message.ToString());

View File

@ -7,8 +7,8 @@ namespace LetsEncrypt.Entities
public class JwsMessage public class JwsMessage
{ {
[JsonProperty("header")] //[JsonProperty("header")]
public JwsHeader Header { get; set; } //public JwsHeader Header { get; set; }
[JsonProperty("protected")] [JsonProperty("protected")]
public string Protected { get; set; } public string Protected { get; set; }

View File

@ -9,6 +9,7 @@ namespace LetsEncrypt.Helpers
public class Environment { public class Environment {
public string name { get; set; } public string name { get; set; }
public string url { get; set; } public string url { get; set; }
public string cache { get; set; }
public string www { get; set; } public string www { get; set; }
public string acme { get; set; } public string acme { get; set; }
public string ssl { get; set; } public string ssl { get; set; }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.0.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="Newtonsoft.JSON" Version="12.0.2" /> <PackageReference Include="Newtonsoft.JSON" Version="12.0.2" />
</ItemGroup> </ItemGroup>

View File

@ -44,6 +44,7 @@ namespace LetsEncrypt
services.AddScoped<IKeyService, KeyService>(); services.AddScoped<IKeyService, KeyService>();
services.AddScoped<IJwsService, JwsService>(); services.AddScoped<IJwsService, JwsService>();
services.AddScoped<ILetsEncryptService, LetsEncryptService>(); services.AddScoped<ILetsEncryptService, LetsEncryptService>();
services.AddScoped<ITerminalService,TerminalService>();
// add app // add app
services.AddTransient<App>(); services.AddTransient<App>();

View File

@ -57,10 +57,31 @@ namespace LetsEncrypt.Services
var message = new JwsMessage var message = new JwsMessage
{ {
Payload = Base64UrlEncoded(JsonConvert.SerializeObject(payload)), Payload = "",
Protected = Base64UrlEncoded(JsonConvert.SerializeObject(protectedHeader)) Protected = Base64UrlEncoded(JsonConvert.SerializeObject(protectedHeader))
}; };
if(payload != null) {
if(payload is String) {
string value = payload.ToString();
switch(value) {
case "POST-as-GET":
message.Payload = string.Empty;
break;
default:
message.Payload = Base64UrlEncoded(value);
break;
}
} else {
message.Payload = Base64UrlEncoded(JsonConvert.SerializeObject(payload));
}
}
message.Signature = Base64UrlEncoded( message.Signature = Base64UrlEncoded(
_rsa.SignData(Encoding.ASCII.GetBytes(message.Protected + "." + message.Payload), _rsa.SignData(Encoding.ASCII.GetBytes(message.Protected + "." + message.Payload),
HashAlgorithmName.SHA256, HashAlgorithmName.SHA256,

View File

@ -1,3 +1,10 @@
/**
* tools.itef.org/html/draft-itef-acme-acme-18
* https://community.letsencrypt.org/t/trying-to-do-post-as-get-but-getting-post-jws-not-signed/108371
* https://tools.ietf.org/html/rfc8555#section-6.2
*
*/
using System; using System;
using System.Threading; using System.Threading;
@ -125,15 +132,17 @@ namespace LetsEncrypt.Services {
//New Account request //New Account request
_jwsService.Init(_accountKey, null); _jwsService.Init(_accountKey, null);
var (account, response) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, new Account
var letsEncryptOrder = new Account
{ {
// we validate this in the UI before we get here, so that is fine // we validate this in the UI before we get here, so that is fine
TermsOfServiceAgreed = true, TermsOfServiceAgreed = true,
Contacts = contacts.Select(contact => Contacts = contacts.Select(contact =>
string.Format("mailto:{0}", contact) string.Format("mailto:{0}", contact)
).ToArray() ).ToArray()
};
}, token); var (account, response) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, letsEncryptOrder, token);
_jwsService.SetKeyId(account); _jwsService.SetKeyId(account);
if (account.Status != "valid") if (account.Status != "valid")
@ -195,7 +204,7 @@ namespace LetsEncrypt.Services {
//update jws with account url //update jws with account url
_jwsService.Init(_accountKey, _cache.Location.ToString()); _jwsService.Init(_accountKey, _cache.Location.ToString());
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, new Order var letsEncryptOrder = new Order
{ {
Expires = DateTime.UtcNow.AddDays(2), Expires = DateTime.UtcNow.AddDays(2),
Identifiers = hostnames.Select(hostname => new OrderIdentifier Identifiers = hostnames.Select(hostname => new OrderIdentifier
@ -203,7 +212,9 @@ namespace LetsEncrypt.Services {
Type = "dns", Type = "dns",
Value = hostname Value = hostname
}).ToArray() }).ToArray()
}, token); };
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, letsEncryptOrder, token);
if (order.Status != "pending") if (order.Status != "pending")
throw new InvalidOperationException("Created new order and expected status 'pending', but got: " + order.Status + Environment.NewLine + throw new InvalidOperationException("Created new order and expected status 'pending', but got: " + order.Status + Environment.NewLine +
@ -213,7 +224,8 @@ namespace LetsEncrypt.Services {
var results = new Dictionary<string, string>(); var results = new Dictionary<string, string>();
foreach (var item in order.Authorizations) foreach (var item in order.Authorizations)
{ {
var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Get, item, null, token); var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, "POST-as-GET", token);
if (challengeResponse.Status == "valid") if (challengeResponse.Status == "valid")
continue; continue;
@ -277,17 +289,22 @@ namespace LetsEncrypt.Services {
{ {
_jwsService.Init(_accountKey, _cache.Location.ToString()); _jwsService.Init(_accountKey, _cache.Location.ToString());
for (var index = 0; index < _challenges.Count; index++)
{
var challenge = _challenges[index];
while (true)
for (var index = 0; index < _challenges.Count; index++)
{ {
var challenge = _challenges[index];
while (true)
{
AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge(); AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge();
switch (challenge.Type) { switch (challenge.Type) {
case "dns-01": { case "dns-01": {
authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token); authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token);
//var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
break; break;
} }
@ -296,16 +313,18 @@ namespace LetsEncrypt.Services {
} }
} }
var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, authorizeChallenge, token); var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, "{}", token);
if (result.Status == "valid") if (result.Status == "valid")
break; break;
if (result.Status != "pending") if (result.Status != "pending")
throw new InvalidOperationException("Failed autorization of " + _currentOrder.Identifiers[index].Value + Environment.NewLine + responseText); throw new InvalidOperationException("Failed autorization of " + _currentOrder.Identifiers[index].Value + Environment.NewLine + responseText);
await Task.Delay(500); await Task.Delay(1000);
} }
} }
} }
@ -317,7 +336,7 @@ namespace LetsEncrypt.Services {
//update jws //update jws
_jwsService.Init(_accountKey, _cache.Location.ToString()); _jwsService.Init(_accountKey, _cache.Location.ToString());
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, new Order var letsEncryptOrder = new Order
{ {
Expires = DateTime.UtcNow.AddDays(2), Expires = DateTime.UtcNow.AddDays(2),
Identifiers = hostnames.Select(hostname => new OrderIdentifier Identifiers = hostnames.Select(hostname => new OrderIdentifier
@ -325,7 +344,9 @@ namespace LetsEncrypt.Services {
Type = "dns", Type = "dns",
Value = hostname Value = hostname
}).ToArray() }).ToArray()
}, token); };
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, letsEncryptOrder, token);
_currentOrder = order; _currentOrder = order;
} }
@ -351,14 +372,16 @@ namespace LetsEncrypt.Services {
csr.CertificateExtensions.Add(san.Build()); csr.CertificateExtensions.Add(san.Build());
var (response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, new FinalizeRequest var letsEncryptOrder = new FinalizeRequest
{ {
CSR = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest()) CSR = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest())
}, token); };
var (response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, letsEncryptOrder, token);
while (response.Status != "valid") while (response.Status != "valid")
{ {
(response, responseText) = await SendAsync<Order>(HttpMethod.Get, response.Location, null, token); (response, responseText) = await SendAsync<Order>(HttpMethod.Post, response.Location, "POST-as-GET", token);
if(response.Status == "processing") if(response.Status == "processing")
{ {
@ -368,7 +391,7 @@ namespace LetsEncrypt.Services {
throw new InvalidOperationException("Invalid order status: " + response.Status + Environment.NewLine + throw new InvalidOperationException("Invalid order status: " + response.Status + Environment.NewLine +
responseText); responseText);
} }
var (pem, _) = await SendAsync<string>(HttpMethod.Get, response.Certificate, null, token); var (pem, _) = await SendAsync<string>(HttpMethod.Post, response.Certificate, "POST-as-GET", token);
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem));
@ -450,6 +473,8 @@ namespace LetsEncrypt.Services {
request.Content.Headers.Add("Content-Type", requestType); request.Content.Headers.Add("Content-Type", requestType);
} }
var response = await _client.SendAsync(request, token).ConfigureAwait(false); var response = await _client.SendAsync(request, token).ConfigureAwait(false);
if (method == HttpMethod.Post) if (method == HttpMethod.Post)

View File

@ -0,0 +1,31 @@
using System;
using System.Diagnostics;
namespace LetsEncrypt {
public interface ITerminalService {
void Exec(string cmd);
}
public class TerminalService : ITerminalService {
public void Exec(string cmd) {
var escapedArgs = cmd.Replace("\"", "\\\"");
var pc = new Process {
StartInfo = new ProcessStartInfo {
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "/bin/bash",
Arguments = $"-c \"{escapedArgs}\""
}
};
pc.Start();
pc.WaitForExit();
}
}
}

View File

@ -0,0 +1,74 @@
{
"AppSettings": {
"active": "StagingV2",
"environments": [
{
"name": "StagingV2",
"url": "https://acme-staging-v02.api.letsencrypt.org/directory",
"www": "/var/www",
"acme": ".well-known/acme-challenge",
"ssl": "/home/maksym/source/temp"
},
{
"name": "ProductionV2",
"url": "https://acme-v02.api.letsencrypt.org/directory",
"www": "/var/www",
"acme": ".well-known/acme-challenge",
"ssl": "/etc/nginx/ssl"
}
],
"customers": [
{
"id": "9b4c8584-dc83-4388-b45f-2942e34dca9d",
"contacts": [ "maksym.sadovnychyy@gmail.com" ],
"name": "Maksym",
"lastname": "Sadovnychyy",
"sites": [
{
"name": "maks-it.com",
"hosts": [
"maks-it.com",
"www.maks-it.com",
"it.maks-it.com",
"www.it.maks-it.com",
"ru.maks-it.com",
"www.ru.maks-it.com",
"api.maks-it.com",
"www.api.maks-it.com"
],
"challenge": "http-01"
}
]
},
{
"id": "d6be989c-3b68-480d-9f4f-b7317674847a",
"contacts": [ "anastasiia.pavlovskaia@gmail.com" ],
"name": "Anastasiia",
"lastname": "Pavlovskaia",
"sites": [
{
"name": "nastyarey.com",
"hosts": [
"nastyarey.com",
"www.nastyarey.com",
"it.nastyarey.com",
"www.it.nastyarey.com",
"ru.nastyarey.com",
"www.ru.nastyarey.com"
],
"challenge": "http-01"
}
]
}
]
}
}

View File

@ -1,31 +1,33 @@
{ {
"AppSettings": { "AppSettings": {
"active": "ProductionV2", "active": "StagingV2",
"environments": [ "environments": [
{ {
"name": "StagingV2", "name": "StagingV2",
"url": "https://acme-staging-v02.api.letsencrypt.org/directory", "url": "https://acme-staging-v02.api.letsencrypt.org/directory",
"cache": "staging_cache",
"www": "/var/www", "www": "/var/www",
"acme": ".well-known/acme-challenge", "acme": ".well-known/acme-challenge",
"ssl": "/etc/nginx/ssl" "ssl": "/home/maksym/source/temp"
}, },
{ {
"name": "ProductionV2", "name": "ProductionV2",
"url": "https://acme-v02.api.letsencrypt.org/directory", "url": "https://acme-v02.api.letsencrypt.org/directory",
"cache": "production_cache",
"www": "/var/www", "www": "/var/www",
"acme": ".well-known/acme-challenge", "acme": ".well-known/acme-challenge",
"ssl": "/etc/nginx/ssl" "ssl": "/etc/nginx/ssl/"
} }
], ],
"customers": [ "customers": [
{ {
"id": "9b4c8584-dc83-4388-b45f-2942e34dca9d", "id": "9b4c8584-dc83-4388-b45f-2942e34dca9d",
"contacts": [ "maksym.sadovnychyy@gmail.com" ], "contacts": [ "maksym.sadovnychyy@google.com" ],
"name": "Maksym", "name": "Maksym",
"lastname": "Sadovnychyy", "lastname": "Sadovnychyy",
@ -50,30 +52,7 @@
"challenge": "http-01" "challenge": "http-01"
} }
] ]
},
{
"id": "d6be989c-3b68-480d-9f4f-b7317674847a",
"contacts": [ "anastasiia.pavlovskaia@gmail.com" ],
"name": "Anastasiia",
"lastname": "Pavlovskaia",
"sites": [
{
"name": "nastyarey.com",
"hosts": [
"nastyarey.com",
"www.nastyarey.com",
"it.nastyarey.com",
"www.it.nastyarey.com",
"ru.nastyarey.com",
"www.ru.nastyarey.com"
],
"challenge": "http-01"
}
]
} }
] ]
} }
} }