From ccda9417d024597d78139df9d44034840c2826c8 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Fri, 1 Nov 2019 01:15:07 +0100 Subject: [PATCH] version 2.0 release based on Dipendency Injection and appsettings.json --- .gitignore | 1 + LetsEncrypt.sln => v1.0/LetsEncrypt.sln | 0 .../LetsEncrypt}/.vscode/launch.json | 0 .../LetsEncrypt}/.vscode/tasks.json | 0 .../LetsEncrypt}/ACMEv2/Account.cs | 0 .../LetsEncrypt}/ACMEv2/AcmeDirectory.cs | 0 .../ACMEv2/AuthorizationChallange.cs | 0 .../ACMEv2/AuthorizationChallengeResponse.cs | 0 .../LetsEncrypt}/ACMEv2/AuthorizeChallenge.cs | 0 .../ACMEv2/CachedCertificateResult.cs | 0 .../LetsEncrypt}/ACMEv2/CertificateCache.cs | 0 .../LetsEncrypt}/ACMEv2/FinalizeRequest.cs | 0 .../LetsEncrypt}/ACMEv2/IHashLocation.cs | 0 .../LetsEncrypt}/ACMEv2/Jwk.cs | 0 .../LetsEncrypt}/ACMEv2/Jws.cs | 0 .../LetsEncrypt}/ACMEv2/JwsHeader.cs | 0 .../LetsEncrypt}/ACMEv2/JwsMessage.cs | 0 .../LetsEncrypt}/ACMEv2/LetsEncryptClient.cs | 0 .../ACMEv2/LetsEncrytException.cs | 0 .../LetsEncrypt}/ACMEv2/Order.cs | 0 .../LetsEncrypt}/ACMEv2/OrderIdentifier.cs | 0 .../LetsEncrypt}/ACMEv2/Problem.cs | 0 .../LetsEncrypt}/ACMEv2/RegistrationCache.cs | 0 .../LetsEncrypt}/LetsEncrypt.csproj | 0 {LetsEncrypt => v1.0/LetsEncrypt}/Library.cs | 0 {LetsEncrypt => v1.0/LetsEncrypt}/Program.cs | 0 {LetsEncrypt => v1.0/LetsEncrypt}/README.md | 0 .../LetsEncrypt}/SettingsProvider.cs | 0 .../LetsEncrypt}/settings.json | 4 +- v2.0/LetsEncrypt.sln | 25 + v2.0/LetsEncrypt/.vscode/launch.json | 27 + v2.0/LetsEncrypt/.vscode/tasks.json | 36 ++ v2.0/LetsEncrypt/App.cs | 184 ++++++ .../Entities/LetsEncrypt/Account.cs | 88 +++ .../Entities/LetsEncrypt/AcmeDirectory.cs | 43 ++ .../LetsEncrypt/AuthorizationChallange.cs | 22 + .../AuthorizationChallengeResponse.cs | 32 + .../LetsEncrypt/CachedCertificateResult.cs | 11 + .../Entities/LetsEncrypt/CertificateCache.cs | 11 + .../Entities/LetsEncrypt/FinalizeRequest.cs | 11 + v2.0/LetsEncrypt/Entities/LetsEncrypt/Jwk.cs | 107 ++++ .../Entities/LetsEncrypt/JwsMessage.cs | 61 ++ .../Entities/LetsEncrypt/OrderIdentifier.cs | 16 + .../Entities/LetsEncrypt/RegistrationCache.cs | 17 + .../Exceptions/LetsEncrytException.cs | 33 + v2.0/LetsEncrypt/Helpers/AppSettings.cs | 31 + v2.0/LetsEncrypt/LetsEncrypt.csproj | 19 + v2.0/LetsEncrypt/Models/BasicUsageModel.cs | 0 v2.0/LetsEncrypt/Program.cs | 54 ++ v2.0/LetsEncrypt/README.md | 83 +++ v2.0/LetsEncrypt/Services/JwsService.cs | 110 ++++ v2.0/LetsEncrypt/Services/KeyService.cs | 175 ++++++ .../Services/LetsEncryptService.cs | 567 ++++++++++++++++++ v2.0/LetsEncrypt/appsettings.json | 74 +++ 54 files changed, 1839 insertions(+), 3 deletions(-) rename LetsEncrypt.sln => v1.0/LetsEncrypt.sln (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/.vscode/launch.json (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/.vscode/tasks.json (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/Account.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/AcmeDirectory.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/AuthorizationChallange.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/AuthorizationChallengeResponse.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/AuthorizeChallenge.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/CachedCertificateResult.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/CertificateCache.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/FinalizeRequest.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/IHashLocation.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/Jwk.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/Jws.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/JwsHeader.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/JwsMessage.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/LetsEncryptClient.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/LetsEncrytException.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/Order.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/OrderIdentifier.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/Problem.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/ACMEv2/RegistrationCache.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/LetsEncrypt.csproj (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/Library.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/Program.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/README.md (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/SettingsProvider.cs (100%) rename {LetsEncrypt => v1.0/LetsEncrypt}/settings.json (93%) create mode 100644 v2.0/LetsEncrypt.sln create mode 100644 v2.0/LetsEncrypt/.vscode/launch.json create mode 100644 v2.0/LetsEncrypt/.vscode/tasks.json create mode 100644 v2.0/LetsEncrypt/App.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/Account.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/AcmeDirectory.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallengeResponse.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/CertificateCache.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/FinalizeRequest.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/Jwk.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/JwsMessage.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/OrderIdentifier.cs create mode 100644 v2.0/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs create mode 100644 v2.0/LetsEncrypt/Exceptions/LetsEncrytException.cs create mode 100644 v2.0/LetsEncrypt/Helpers/AppSettings.cs create mode 100644 v2.0/LetsEncrypt/LetsEncrypt.csproj create mode 100644 v2.0/LetsEncrypt/Models/BasicUsageModel.cs create mode 100644 v2.0/LetsEncrypt/Program.cs create mode 100644 v2.0/LetsEncrypt/README.md create mode 100644 v2.0/LetsEncrypt/Services/JwsService.cs create mode 100644 v2.0/LetsEncrypt/Services/KeyService.cs create mode 100644 v2.0/LetsEncrypt/Services/LetsEncryptService.cs create mode 100644 v2.0/LetsEncrypt/appsettings.json diff --git a/.gitignore b/.gitignore index 3c4efe2..ca17f77 100644 --- a/.gitignore +++ b/.gitignore @@ -194,6 +194,7 @@ ClientBin/ *.publishsettings node_modules/ orleans.codegen.cs +.directory # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) diff --git a/LetsEncrypt.sln b/v1.0/LetsEncrypt.sln similarity index 100% rename from LetsEncrypt.sln rename to v1.0/LetsEncrypt.sln diff --git a/LetsEncrypt/.vscode/launch.json b/v1.0/LetsEncrypt/.vscode/launch.json similarity index 100% rename from LetsEncrypt/.vscode/launch.json rename to v1.0/LetsEncrypt/.vscode/launch.json diff --git a/LetsEncrypt/.vscode/tasks.json b/v1.0/LetsEncrypt/.vscode/tasks.json similarity index 100% rename from LetsEncrypt/.vscode/tasks.json rename to v1.0/LetsEncrypt/.vscode/tasks.json diff --git a/LetsEncrypt/ACMEv2/Account.cs b/v1.0/LetsEncrypt/ACMEv2/Account.cs similarity index 100% rename from LetsEncrypt/ACMEv2/Account.cs rename to v1.0/LetsEncrypt/ACMEv2/Account.cs diff --git a/LetsEncrypt/ACMEv2/AcmeDirectory.cs b/v1.0/LetsEncrypt/ACMEv2/AcmeDirectory.cs similarity index 100% rename from LetsEncrypt/ACMEv2/AcmeDirectory.cs rename to v1.0/LetsEncrypt/ACMEv2/AcmeDirectory.cs diff --git a/LetsEncrypt/ACMEv2/AuthorizationChallange.cs b/v1.0/LetsEncrypt/ACMEv2/AuthorizationChallange.cs similarity index 100% rename from LetsEncrypt/ACMEv2/AuthorizationChallange.cs rename to v1.0/LetsEncrypt/ACMEv2/AuthorizationChallange.cs diff --git a/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs b/v1.0/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs similarity index 100% rename from LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs rename to v1.0/LetsEncrypt/ACMEv2/AuthorizationChallengeResponse.cs diff --git a/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs b/v1.0/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs similarity index 100% rename from LetsEncrypt/ACMEv2/AuthorizeChallenge.cs rename to v1.0/LetsEncrypt/ACMEv2/AuthorizeChallenge.cs diff --git a/LetsEncrypt/ACMEv2/CachedCertificateResult.cs b/v1.0/LetsEncrypt/ACMEv2/CachedCertificateResult.cs similarity index 100% rename from LetsEncrypt/ACMEv2/CachedCertificateResult.cs rename to v1.0/LetsEncrypt/ACMEv2/CachedCertificateResult.cs diff --git a/LetsEncrypt/ACMEv2/CertificateCache.cs b/v1.0/LetsEncrypt/ACMEv2/CertificateCache.cs similarity index 100% rename from LetsEncrypt/ACMEv2/CertificateCache.cs rename to v1.0/LetsEncrypt/ACMEv2/CertificateCache.cs diff --git a/LetsEncrypt/ACMEv2/FinalizeRequest.cs b/v1.0/LetsEncrypt/ACMEv2/FinalizeRequest.cs similarity index 100% rename from LetsEncrypt/ACMEv2/FinalizeRequest.cs rename to v1.0/LetsEncrypt/ACMEv2/FinalizeRequest.cs diff --git a/LetsEncrypt/ACMEv2/IHashLocation.cs b/v1.0/LetsEncrypt/ACMEv2/IHashLocation.cs similarity index 100% rename from LetsEncrypt/ACMEv2/IHashLocation.cs rename to v1.0/LetsEncrypt/ACMEv2/IHashLocation.cs diff --git a/LetsEncrypt/ACMEv2/Jwk.cs b/v1.0/LetsEncrypt/ACMEv2/Jwk.cs similarity index 100% rename from LetsEncrypt/ACMEv2/Jwk.cs rename to v1.0/LetsEncrypt/ACMEv2/Jwk.cs diff --git a/LetsEncrypt/ACMEv2/Jws.cs b/v1.0/LetsEncrypt/ACMEv2/Jws.cs similarity index 100% rename from LetsEncrypt/ACMEv2/Jws.cs rename to v1.0/LetsEncrypt/ACMEv2/Jws.cs diff --git a/LetsEncrypt/ACMEv2/JwsHeader.cs b/v1.0/LetsEncrypt/ACMEv2/JwsHeader.cs similarity index 100% rename from LetsEncrypt/ACMEv2/JwsHeader.cs rename to v1.0/LetsEncrypt/ACMEv2/JwsHeader.cs diff --git a/LetsEncrypt/ACMEv2/JwsMessage.cs b/v1.0/LetsEncrypt/ACMEv2/JwsMessage.cs similarity index 100% rename from LetsEncrypt/ACMEv2/JwsMessage.cs rename to v1.0/LetsEncrypt/ACMEv2/JwsMessage.cs diff --git a/LetsEncrypt/ACMEv2/LetsEncryptClient.cs b/v1.0/LetsEncrypt/ACMEv2/LetsEncryptClient.cs similarity index 100% rename from LetsEncrypt/ACMEv2/LetsEncryptClient.cs rename to v1.0/LetsEncrypt/ACMEv2/LetsEncryptClient.cs diff --git a/LetsEncrypt/ACMEv2/LetsEncrytException.cs b/v1.0/LetsEncrypt/ACMEv2/LetsEncrytException.cs similarity index 100% rename from LetsEncrypt/ACMEv2/LetsEncrytException.cs rename to v1.0/LetsEncrypt/ACMEv2/LetsEncrytException.cs diff --git a/LetsEncrypt/ACMEv2/Order.cs b/v1.0/LetsEncrypt/ACMEv2/Order.cs similarity index 100% rename from LetsEncrypt/ACMEv2/Order.cs rename to v1.0/LetsEncrypt/ACMEv2/Order.cs diff --git a/LetsEncrypt/ACMEv2/OrderIdentifier.cs b/v1.0/LetsEncrypt/ACMEv2/OrderIdentifier.cs similarity index 100% rename from LetsEncrypt/ACMEv2/OrderIdentifier.cs rename to v1.0/LetsEncrypt/ACMEv2/OrderIdentifier.cs diff --git a/LetsEncrypt/ACMEv2/Problem.cs b/v1.0/LetsEncrypt/ACMEv2/Problem.cs similarity index 100% rename from LetsEncrypt/ACMEv2/Problem.cs rename to v1.0/LetsEncrypt/ACMEv2/Problem.cs diff --git a/LetsEncrypt/ACMEv2/RegistrationCache.cs b/v1.0/LetsEncrypt/ACMEv2/RegistrationCache.cs similarity index 100% rename from LetsEncrypt/ACMEv2/RegistrationCache.cs rename to v1.0/LetsEncrypt/ACMEv2/RegistrationCache.cs diff --git a/LetsEncrypt/LetsEncrypt.csproj b/v1.0/LetsEncrypt/LetsEncrypt.csproj similarity index 100% rename from LetsEncrypt/LetsEncrypt.csproj rename to v1.0/LetsEncrypt/LetsEncrypt.csproj diff --git a/LetsEncrypt/Library.cs b/v1.0/LetsEncrypt/Library.cs similarity index 100% rename from LetsEncrypt/Library.cs rename to v1.0/LetsEncrypt/Library.cs diff --git a/LetsEncrypt/Program.cs b/v1.0/LetsEncrypt/Program.cs similarity index 100% rename from LetsEncrypt/Program.cs rename to v1.0/LetsEncrypt/Program.cs diff --git a/LetsEncrypt/README.md b/v1.0/LetsEncrypt/README.md similarity index 100% rename from LetsEncrypt/README.md rename to v1.0/LetsEncrypt/README.md diff --git a/LetsEncrypt/SettingsProvider.cs b/v1.0/LetsEncrypt/SettingsProvider.cs similarity index 100% rename from LetsEncrypt/SettingsProvider.cs rename to v1.0/LetsEncrypt/SettingsProvider.cs diff --git a/LetsEncrypt/settings.json b/v1.0/LetsEncrypt/settings.json similarity index 93% rename from LetsEncrypt/settings.json rename to v1.0/LetsEncrypt/settings.json index 3654827..b1d8fa4 100644 --- a/LetsEncrypt/settings.json +++ b/v1.0/LetsEncrypt/settings.json @@ -2,9 +2,7 @@ "_StagingV2": "https://acme-staging-v02.api.letsencrypt.org/directory", "_ProductionV2": "https://acme-v02.api.letsencrypt.org/directory", - "url": "https://acme-v02.api.letsencrypt.org/directory", - - "cache": "/home/maksym/Desktop/LetsEncrypt_Cache/cache", + "url": "https://acme-staging-v02.api.letsencrypt.org/directory", "www": "/var/www", "acme": ".well-known/acme-challenge", diff --git a/v2.0/LetsEncrypt.sln b/v2.0/LetsEncrypt.sln new file mode 100644 index 0000000..bffcf60 --- /dev/null +++ b/v2.0/LetsEncrypt.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.572 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt/LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351} + EndGlobalSection +EndGlobal diff --git a/v2.0/LetsEncrypt/.vscode/launch.json b/v2.0/LetsEncrypt/.vscode/launch.json new file mode 100644 index 0000000..125ba92 --- /dev/null +++ b/v2.0/LetsEncrypt/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.2/LetsEncrypt.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/.vscode/tasks.json b/v2.0/LetsEncrypt/.vscode/tasks.json new file mode 100644 index 0000000..723a349 --- /dev/null +++ b/v2.0/LetsEncrypt/.vscode/tasks.json @@ -0,0 +1,36 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/LetsEncrypt.csproj" + ], + "problemMatcher": "$tsc" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/LetsEncrypt.csproj" + ], + "problemMatcher": "$tsc" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/LetsEncrypt.csproj" + ], + "problemMatcher": "$tsc" + } + ] +} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/App.cs b/v2.0/LetsEncrypt/App.cs new file mode 100644 index 0000000..ec50c9c --- /dev/null +++ b/v2.0/LetsEncrypt/App.cs @@ -0,0 +1,184 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; + +using System.Linq; + +using Microsoft.Extensions.Options; + +using LetsEncrypt.Services; +using LetsEncrypt.Helpers; +using LetsEncrypt.Entities; + +namespace LetsEncrypt +{ + public class App { + + private readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory; + + private readonly AppSettings _appSettings; + private readonly ILetsEncryptService _letsEncryptService; + private readonly IKeyService _keyService; + + public App(IOptions appSettings, ILetsEncryptService letsEncryptService, IKeyService keyService) { + _appSettings = appSettings.Value; + _letsEncryptService = letsEncryptService; + _keyService = keyService; + } + + public void Run() { + + try + { + LetsEncrypt.Helpers.Environment env = _appSettings.environments.First(x => (x.name == _appSettings.active)); + + Console.WriteLine(string.Format("Let's Encrypt C# .Net Core Client, environment: {0}", env.name)); + + //loop all customers + foreach(Customer customer in _appSettings.customers) { + try { + Console.WriteLine(string.Format("Managing customer: {0} - {1} {2}", customer.id, customer.name, customer.lastname)); + + //loop each customer website + foreach(Site site in customer.sites) { + Console.WriteLine(string.Format("Managing site: {0}", site.name)); + + try { + //define cache folder + string cache = Path.Combine(AppPath, "cache", customer.id); + if(!Directory.Exists(cache)) { + Directory.CreateDirectory(cache); + } + + //1. Client initialization + Console.WriteLine("1. Client Initialization..."); + _letsEncryptService.Init(env.url, cache, site.name, customer.contacts).Wait(); + + + Console.WriteLine(string.Format("Terms of service: {0}", _letsEncryptService.GetTermsOfServiceUri())); + + //create folder for ssl + string ssl = Path.Combine(env.ssl, site.name); + if(!Directory.Exists(ssl)) { + Directory.CreateDirectory(ssl); + } + + // get cached certificate and check if it's valid + // if valid check if cert and key exists otherwise recreate + // else continue with new certificate request + CachedCertificateResult certRes = new CachedCertificateResult(); + if (_letsEncryptService.TryGetCachedCertificate(site.name, out certRes)) { + string cert = Path.Combine(ssl, site.name + ".crt"); + if(!File.Exists(cert)) + File.WriteAllText(cert, certRes.Certificate); + + string key = Path.Combine(ssl, site.name + ".key"); + if(!File.Exists(key)) { + using (StreamWriter writer = File.CreateText(key)) + _keyService.ExportPrivateKey(certRes.PrivateKey, writer); + } + + Console.WriteLine("Certificate and Key exists and valid."); + } + else { + //new nonce + _letsEncryptService.NewNonce().Wait(); + + //try to make new order + try { + //create new orders + Console.WriteLine("2. Client New Order..."); + Task> orders = _letsEncryptService.NewOrder(site.hosts, site.challenge); + orders.Wait(); + + switch(site.challenge) { + case "http-01": { + //ensure to enable static file discovery on server in .well-known/acme-challenge + //and listen on 80 port + + //check acme directory + string acme = Path.Combine(env.www, env.acme); + if(!Directory.Exists(acme)) { + throw new DirectoryNotFoundException(string.Format("Directory {0} wasn't created", acme)); + } + + foreach (FileInfo file in new DirectoryInfo(acme).GetFiles()) + file.Delete(); + + foreach (var result in orders.Result) + { + Console.WriteLine("Key: " + result.Key + System.Environment.NewLine + "Value: " + result.Value); + string[] splitToken = result.Value.Split('~'); + + string token = Path.Combine(acme, splitToken[0]); + File.WriteAllText(token, splitToken[1]); + } + + break; + } + + case "dns-01": { + //Manage DNS server MX record, depends from provider + + break; + } + + default: { + + break; + } + } + + //complete challanges + Console.WriteLine("3. Client Complete Challange..."); + _letsEncryptService.CompleteChallenges().Wait(); + Console.WriteLine("Challanges comleted."); + } + catch (Exception ex) { + Console.WriteLine(ex.Message.ToString()); + _letsEncryptService.GetOrder(site.hosts).Wait(); + } + + + // Download new certificate + Console.WriteLine("4. Download certificate..."); + _letsEncryptService.GetCertificate(site.name).Wait(); + + // Write to filesystem + certRes = new CachedCertificateResult(); + if (_letsEncryptService.TryGetCachedCertificate(site.name, out certRes)) { + string cert = Path.Combine(ssl, site.name + ".crt"); + File.WriteAllText(cert, certRes.Certificate); + + string key = Path.Combine(ssl, site.name + ".key"); + using (StreamWriter writer = File.CreateText(key)) + _keyService.ExportPrivateKey(certRes.PrivateKey, writer); + + Console.WriteLine("Certificate saved."); + } + else { + Console.WriteLine("Unable to get new cached certificate."); + } + + + } + + + } + catch (Exception ex) { + Console.WriteLine(ex.Message.ToString()); + } + } + } + catch (Exception ex) { + Console.WriteLine(ex.Message.ToString()); + } + } + } + catch (Exception ex) { + Console.WriteLine(ex.Message.ToString()); + } + } + } +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/Account.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/Account.cs new file mode 100644 index 0000000..5e785bd --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/Account.cs @@ -0,0 +1,88 @@ +/* + * https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3 + */ + +using System; +using Newtonsoft.Json; +using LetsEncrypt.Exceptions; + +namespace LetsEncrypt.Entities +{ + interface IHasLocation + { + Uri Location { get; set; } + } + + public class Account : IHasLocation + { + [JsonProperty("termsOfServiceAgreed")] + public bool TermsOfServiceAgreed { get; set; } + + /* + onlyReturnExisting (optional, boolean): If this field is present + with the value "true", then the server MUST NOT create a new + account if one does not already exist. This allows a client to + look up an account URL based on an account key + */ + [JsonProperty("onlyReturnExisting")] + public bool OnlyReturnExisting { get; set; } + + [JsonProperty("contact")] + public string[] Contacts { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("createdAt")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("key")] + public Jwk Key { get; set; } + + [JsonProperty("initialIp")] + public string InitialIp { get; set; } + + [JsonProperty("orders")] + public Uri Orders { get; set; } + + public Uri Location { get; set; } + } + + + public class Order : IHasLocation + { + public Uri Location { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("expires")] + public DateTime? Expires { get; set; } + + [JsonProperty("identifiers")] + public OrderIdentifier[] Identifiers { get; set; } + + [JsonProperty("notBefore")] + public DateTime? NotBefore { get; set; } + + [JsonProperty("notAfter")] + public DateTime? NotAfter { get; set; } + + [JsonProperty("error")] + public Problem Error { get; set; } + + [JsonProperty("authorizations")] + public Uri[] Authorizations { get; set; } + + [JsonProperty("finalize")] + public Uri Finalize { get; set; } + + [JsonProperty("certificate")] + public Uri Certificate { get; set; } + } + + +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AcmeDirectory.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AcmeDirectory.cs new file mode 100644 index 0000000..ae70c7c --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AcmeDirectory.cs @@ -0,0 +1,43 @@ +using System; +using Newtonsoft.Json; + +namespace LetsEncrypt.Entities +{ + public class AcmeDirectory + { + //New nonce + [JsonProperty("newNonce")] + public Uri NewNonce { get; set; } + + //New account + [JsonProperty("newAccount")] + public Uri NewAccount { get; set; } + + //New order + [JsonProperty("newOrder")] + public Uri NewOrder { get; set; } + + // New authorization If the ACME server does not implement pre-authorization + // (Section 7.4.1) it MUST omit the "newAuthz" field of the directory. + // [JsonProperty("newAuthz")] + // public Uri NewAuthz { get; set; } + + //Revoke certificate + [JsonProperty("revokeCert")] + public Uri RevokeCertificate { get; set; } + + //Key change + [JsonProperty("keyChange")] + public Uri KeyChange { get; set; } + + //Metadata object + [JsonProperty("meta")] + public AcmeDirectoryMeta Meta { get; set; } + } + + public class AcmeDirectoryMeta + { + [JsonProperty("termsOfService")] + public string TermsOfService { get; set; } + } +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs new file mode 100644 index 0000000..98dcb1a --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json; + +namespace LetsEncrypt.Entities +{ + public class AuthorizationChallenge + { + [JsonProperty("url")] + public Uri Url { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("token")] + public string Token { get; set; } + + } + +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallengeResponse.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallengeResponse.cs new file mode 100644 index 0000000..b17fb8d --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallengeResponse.cs @@ -0,0 +1,32 @@ +using System; +using Newtonsoft.Json; + +namespace LetsEncrypt.Entities +{ + public class AuthorizationChallengeResponse + { + [JsonProperty("identifier")] + public OrderIdentifier Identifier { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("expires")] + public DateTime? Expires { get; set; } + + [JsonProperty("wildcard")] + public bool Wildcard { get; set; } + + [JsonProperty("challenges")] + public AuthorizationChallenge[] Challenges { get; set; } + } + + public class AuthorizeChallenge + { + [JsonProperty("keyAuthorization")] + public string KeyAuthorization { get; set; } + + } + + +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs new file mode 100644 index 0000000..0b4595a --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs @@ -0,0 +1,11 @@ +using System.Security.Cryptography; + +namespace LetsEncrypt.Entities +{ + public class CachedCertificateResult + { + public RSACryptoServiceProvider PrivateKey; + public string Certificate; + } + +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/CertificateCache.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/CertificateCache.cs new file mode 100644 index 0000000..29e5008 --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/CertificateCache.cs @@ -0,0 +1,11 @@ +namespace LetsEncrypt.Entities +{ + public class CertificateCache + { + public string Cert; + public byte[] Private; + } + + + +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/FinalizeRequest.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/FinalizeRequest.cs new file mode 100644 index 0000000..01f4c69 --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/FinalizeRequest.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace LetsEncrypt.Entities +{ + public class FinalizeRequest + { + [JsonProperty("csr")] + public string CSR { get; set; } + } + +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/Jwk.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/Jwk.cs new file mode 100644 index 0000000..ef132df --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/Jwk.cs @@ -0,0 +1,107 @@ +using System; +using Newtonsoft.Json; + + +namespace LetsEncrypt.Entities +{ + public class Jwk + { + /// + /// "kty" (Key Type) Parameter + /// + /// The "kty" (key type) parameter identifies the cryptographic algorithm + /// family used with the key, such as "RSA" or "EC". + /// + /// + [JsonProperty("kty")] + public string KeyType { get; set; } + + /// + /// "kid" (Key ID) Parameter + /// + /// The "kid" (key ID) parameter is used to match a specific key. This + /// is used, for instance, to choose among a set of keys within a JWK Set + /// during key rollover. The structure of the "kid" value is + /// unspecified. + /// + /// + [JsonProperty("kid")] + public string KeyId { get; set; } + + /// + /// "use" (Public Key Use) Parameter + /// + /// The "use" (public key use) parameter identifies the intended use of + /// the public key. The "use" parameter is employed to indicate whether + /// a public key is used for encrypting data or verifying the signature + /// on data. + /// + /// + [JsonProperty("use")] + public string Use { get; set; } + + /// + /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonProperty("n")] + public string Modulus { get; set; } + + /// + /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation. + /// + [JsonProperty("e")] + public string Exponent { get; set; } + + /// + /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonProperty("d")] + public string D { get; set; } + + /// + /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonProperty("p")] + public string P { get; set; } + + /// + /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonProperty("q")] + public string Q { get; set; } + + /// + /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonProperty("dp")] + public string DP { get; set; } + + /// + /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonProperty("dq")] + public string DQ { get; set; } + + /// + /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation. + /// + [JsonProperty("qi")] + public string InverseQ { get; set; } + + /// + /// The other primes information, should they exist, null or an empty list if not specified. + /// + [JsonProperty("oth")] + public string OthInf { get; set; } + + /// + /// "alg" (Algorithm) Parameter + /// + /// The "alg" (algorithm) parameter identifies the algorithm intended for + /// use with the key. + /// + /// + [JsonProperty("alg")] + public string Algorithm { get; set; } + } +} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/JwsMessage.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/JwsMessage.cs new file mode 100644 index 0000000..fd88c89 --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/JwsMessage.cs @@ -0,0 +1,61 @@ +using System; +using Newtonsoft.Json; + + +namespace LetsEncrypt.Entities +{ + + public class JwsMessage + { + [JsonProperty("header")] + public JwsHeader Header { get; set; } + + [JsonProperty("protected")] + public string Protected { get; set; } + + [JsonProperty("payload")] + public string Payload { get; set; } + + [JsonProperty("signature")] + public string Signature { get; set; } + } + + + public class JwsHeader + { + //public JwsHeader() + //{ + //} + + //public JwsHeader(string algorithm, Jwk key) + //{ + // Algorithm = algorithm; + // Key = key; + //} + + [JsonProperty("alg")] + public string Algorithm { get; set; } + + [JsonProperty("jwk")] + public Jwk Key { get; set; } + + + [JsonProperty("kid")] + public string KeyId { get; set; } + + + [JsonProperty("nonce")] + public string Nonce { get; set; } + + + [JsonProperty("url")] + public Uri Url { get; set; } + + + [JsonProperty("Host")] + public string Host { get; set; } + } + + + +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/OrderIdentifier.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/OrderIdentifier.cs new file mode 100644 index 0000000..a8efd8b --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/OrderIdentifier.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + + +namespace LetsEncrypt.Entities +{ + public class OrderIdentifier + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + + } + +} diff --git a/v2.0/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs b/v2.0/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs new file mode 100644 index 0000000..409a6cc --- /dev/null +++ b/v2.0/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace LetsEncrypt.Entities +{ + public class RegistrationCache + { + public readonly Dictionary CachedCerts = new Dictionary(StringComparer.OrdinalIgnoreCase); + public byte[] AccountKey; + public string Id; + public Jwk Key; + public Uri Location; + } + + + +} diff --git a/v2.0/LetsEncrypt/Exceptions/LetsEncrytException.cs b/v2.0/LetsEncrypt/Exceptions/LetsEncrytException.cs new file mode 100644 index 0000000..e55599d --- /dev/null +++ b/v2.0/LetsEncrypt/Exceptions/LetsEncrytException.cs @@ -0,0 +1,33 @@ +using System; +using System.Net.Http; + +using Newtonsoft.Json; + +namespace LetsEncrypt.Exceptions +{ + public class LetsEncrytException : Exception + { + public LetsEncrytException(Problem problem, HttpResponseMessage response) + : base($"{problem.Type}: {problem.Detail}") + { + Problem = problem; + Response = response; + } + + public Problem Problem { get; } + + public HttpResponseMessage Response { get; } + } + + + public class Problem + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("detail")] + public string Detail { get; set; } + + public string RawJson { get; set; } + } +} diff --git a/v2.0/LetsEncrypt/Helpers/AppSettings.cs b/v2.0/LetsEncrypt/Helpers/AppSettings.cs new file mode 100644 index 0000000..9fa7760 --- /dev/null +++ b/v2.0/LetsEncrypt/Helpers/AppSettings.cs @@ -0,0 +1,31 @@ +namespace LetsEncrypt.Helpers +{ + public class AppSettings { + public string active { get; set; } + public Environment [] environments { get; set; } + public Customer [] customers { get; set;} + } + + public class Environment { + public string name { get; set; } + public string url { get; set; } + public string www { get; set; } + public string acme { get; set; } + public string ssl { get; set; } + } + + public class Customer { + public string id { get; set; } + public string [] contacts { get; set; } + public string name { get; set; } + public string lastname { get; set; } + public Site [] sites { get; set; } + } + + public class Site { + public string root { get; set; } + public string name { get; set; } + public string [] hosts { get; set; } + public string challenge { get; set; } + } +} diff --git a/v2.0/LetsEncrypt/LetsEncrypt.csproj b/v2.0/LetsEncrypt/LetsEncrypt.csproj new file mode 100644 index 0000000..4186586 --- /dev/null +++ b/v2.0/LetsEncrypt/LetsEncrypt.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp2.2 + + + + + + + + + + + + + + diff --git a/v2.0/LetsEncrypt/Models/BasicUsageModel.cs b/v2.0/LetsEncrypt/Models/BasicUsageModel.cs new file mode 100644 index 0000000..e69de29 diff --git a/v2.0/LetsEncrypt/Program.cs b/v2.0/LetsEncrypt/Program.cs new file mode 100644 index 0000000..0f295de --- /dev/null +++ b/v2.0/LetsEncrypt/Program.cs @@ -0,0 +1,54 @@ +using System; + +using Microsoft.Extensions.Configuration; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using System.IO; + +using LetsEncrypt.Helpers; +using LetsEncrypt.Services; + +namespace LetsEncrypt +{ + class Program + { + public IConfiguration Configuration { get; } + + static void Main(string[] args) { + // create service collection + var services = new ServiceCollection(); + ConfigureServices(services); + + // create service provider + var serviceProvider = services.BuildServiceProvider(); + + // entry to run app + serviceProvider.GetService().Run(); + } + + public static void ConfigureServices(IServiceCollection services) { + // build configuration + IConfiguration Configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", false) + .Build(); + + + // configure strongly typed settings objects + var appSettingsSection = Configuration.GetSection("AppSettings"); + services.Configure(appSettingsSection); + + // Dependency Injection + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // add app + services.AddTransient(); + } + } + + +} diff --git a/v2.0/LetsEncrypt/README.md b/v2.0/LetsEncrypt/README.md new file mode 100644 index 0000000..ac48803 --- /dev/null +++ b/v2.0/LetsEncrypt/README.md @@ -0,0 +1,83 @@ +#ACMEv2 Client library + +https://tools.ietf.org/html/draft-ietf-acme-acme-18 + +The following diagram illustrates the relations between resources on +an ACME server. For the most part, these relations are expressed by +URLs provided as strings in the resources' JSON representations. +Lines with labels in quotes indicate HTTP link relations. + + directory + | + +--> new-nonce + | + +----------+----------+-----+-----+------------+ + | | | | | + | | | | | + V V V V V + newAccount newAuthz newOrder revokeCert keyChange + | | | + | | | + V | V + account | order -----> cert + | | + | | + | V + +------> authz + | ^ + | | "up" + V | + challenge + + + + +-------------------+--------------------------------+--------------+ + | Action | Request | Response | + +-------------------+--------------------------------+--------------+ + | Get directory | GET directory | 200 | + | | | | + | Get nonce | HEAD newNonce | 200 | + | | | | + | Create account | POST newAccount | 201 -> | + | | | account | + | | | | + | Submit order | POST newOrder | 201 -> order | + | | | | + | Fetch challenges | POST-as-GET order's | 200 | + | | authorization urls | | + | | | | + | Respond to | POST authorization challenge | 200 | + | challenges | urls | | + | | | | + | Poll for status | POST-as-GET order | 200 | + | | | | + | Finalize order | POST order's finalize url | 200 | + | | | | + | Poll for status | POST-as-GET order | 200 | + | | | | + | Download | POST-as-GET order's | 200 | + | certificate | certificate url | | + +-------------------+--------------------------------+--------------+ + + + + + + + pending + | + | Receive + | response + V + processing <-+ + | | | Server retry or + | | | client retry request + | +----+ + | + | + Successful | Failed + validation | validation + +---------+---------+ + | | + V V + valid invalid diff --git a/v2.0/LetsEncrypt/Services/JwsService.cs b/v2.0/LetsEncrypt/Services/JwsService.cs new file mode 100644 index 0000000..32ea3c8 --- /dev/null +++ b/v2.0/LetsEncrypt/Services/JwsService.cs @@ -0,0 +1,110 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Newtonsoft.Json; + +using LetsEncrypt.Entities; + +namespace LetsEncrypt.Services +{ + public interface IJwsService { + void Init(RSA rsa, string keyId); + JwsMessage Encode(TPayload payload, JwsHeader protectedHeader); + string GetKeyAuthorization(string token); + string Base64UrlEncoded(byte[] arg); + + void SetKeyId(Account account); + } + + + public class JwsService : IJwsService { + + public Jwk _jwk; + private RSA _rsa; + + public JwsService() { + + } + + public void Init(RSA rsa, string keyId) { + _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa)); + + var publicParameters = rsa.ExportParameters(false); + + _jwk = new Jwk + { + KeyType = "RSA", + Exponent = Base64UrlEncoded(publicParameters.Exponent), + Modulus = Base64UrlEncoded(publicParameters.Modulus), + KeyId = keyId + }; + } + + + + public JwsMessage Encode(TPayload payload, JwsHeader protectedHeader) + { + + protectedHeader.Algorithm = "RS256"; + if (_jwk.KeyId != null) + { + protectedHeader.KeyId = _jwk.KeyId; + } + else + { + protectedHeader.Key = _jwk; + } + + var message = new JwsMessage + { + Payload = Base64UrlEncoded(JsonConvert.SerializeObject(payload)), + Protected = Base64UrlEncoded(JsonConvert.SerializeObject(protectedHeader)) + }; + + message.Signature = Base64UrlEncoded( + _rsa.SignData(Encoding.ASCII.GetBytes(message.Protected + "." + message.Payload), + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1)); + + return message; + } + + private string GetSha256Thumbprint() + { + var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}"; + + using (var sha256 = SHA256.Create()) + { + return Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(json))); + } + } + + public string GetKeyAuthorization(string token) + { + return token + "." + GetSha256Thumbprint(); + } + + + + public string Base64UrlEncoded(string s) + { + return Base64UrlEncoded(Encoding.UTF8.GetBytes(s)); + } + + public string Base64UrlEncoded(byte[] arg) + { + var s = Convert.ToBase64String(arg); // Regular base64 encoder + s = s.Split('=')[0]; // Remove any trailing '='s + s = s.Replace('+', '-'); // 62nd char of encoding + s = s.Replace('/', '_'); // 63rd char of encoding + return s; + } + + public void SetKeyId(Account account) + { + _jwk.KeyId = account.Id; + } + + + } +} diff --git a/v2.0/LetsEncrypt/Services/KeyService.cs b/v2.0/LetsEncrypt/Services/KeyService.cs new file mode 100644 index 0000000..3f6bc1c --- /dev/null +++ b/v2.0/LetsEncrypt/Services/KeyService.cs @@ -0,0 +1,175 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace LetsEncrypt.Services { + + public interface IKeyService { + void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream); + void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream); + } + + public class KeyService : IKeyService { + /// + /// Export a certificate to a PEM format string + /// + /// The certificate to export + /// A PEM encoded string + //public static string ExportToPEM(X509Certificate2 cert) + //{ + // StringBuilder builder = new StringBuilder(); + + // builder.AppendLine("-----BEGIN CERTIFICATE-----"); + // builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); + // builder.AppendLine("-----END CERTIFICATE-----"); + + // return builder.ToString(); + //} + public void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream) + { + var parameters = csp.ExportParameters(false); + using (var stream = new MemoryStream()) + { + var writer = new BinaryWriter(stream); + writer.Write((byte)0x30); // SEQUENCE + using (var innerStream = new MemoryStream()) + { + var innerWriter = new BinaryWriter(innerStream); + innerWriter.Write((byte)0x30); // SEQUENCE + EncodeLength(innerWriter, 13); + innerWriter.Write((byte)0x06); // OBJECT IDENTIFIER + var rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 }; + EncodeLength(innerWriter, rsaEncryptionOid.Length); + innerWriter.Write(rsaEncryptionOid); + innerWriter.Write((byte)0x05); // NULL + EncodeLength(innerWriter, 0); + innerWriter.Write((byte)0x03); // BIT STRING + using (var bitStringStream = new MemoryStream()) + { + var bitStringWriter = new BinaryWriter(bitStringStream); + bitStringWriter.Write((byte)0x00); // # of unused bits + bitStringWriter.Write((byte)0x30); // SEQUENCE + using (var paramsStream = new MemoryStream()) + { + var paramsWriter = new BinaryWriter(paramsStream); + EncodeIntegerBigEndian(paramsWriter, parameters.Modulus); // Modulus + EncodeIntegerBigEndian(paramsWriter, parameters.Exponent); // Exponent + var paramsLength = (int)paramsStream.Length; + EncodeLength(bitStringWriter, paramsLength); + bitStringWriter.Write(paramsStream.GetBuffer(), 0, paramsLength); + } + var bitStringLength = (int)bitStringStream.Length; + EncodeLength(innerWriter, bitStringLength); + innerWriter.Write(bitStringStream.GetBuffer(), 0, bitStringLength); + } + var length = (int)innerStream.Length; + EncodeLength(writer, length); + writer.Write(innerStream.GetBuffer(), 0, length); + } + + var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); + outputStream.WriteLine("-----BEGIN PUBLIC KEY-----"); + for (var i = 0; i < base64.Length; i += 64) + { + outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); + } + outputStream.WriteLine("-----END PUBLIC KEY-----"); + } + } + + public void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream) + { + if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp"); + var parameters = csp.ExportParameters(true); + using (var stream = new MemoryStream()) + { + var writer = new BinaryWriter(stream); + writer.Write((byte)0x30); // SEQUENCE + using (var innerStream = new MemoryStream()) + { + var innerWriter = new BinaryWriter(innerStream); + EncodeIntegerBigEndian(innerWriter, new byte[] { 0x00 }); // Version + EncodeIntegerBigEndian(innerWriter, parameters.Modulus); + EncodeIntegerBigEndian(innerWriter, parameters.Exponent); + EncodeIntegerBigEndian(innerWriter, parameters.D); + EncodeIntegerBigEndian(innerWriter, parameters.P); + EncodeIntegerBigEndian(innerWriter, parameters.Q); + EncodeIntegerBigEndian(innerWriter, parameters.DP); + EncodeIntegerBigEndian(innerWriter, parameters.DQ); + EncodeIntegerBigEndian(innerWriter, parameters.InverseQ); + var length = (int)innerStream.Length; + EncodeLength(writer, length); + writer.Write(innerStream.GetBuffer(), 0, length); + } + + var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); + outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----"); + // Output as Base64 with lines chopped at 64 characters + for (var i = 0; i < base64.Length; i += 64) + { + outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i)); + } + outputStream.WriteLine("-----END RSA PRIVATE KEY-----"); + } + } + + private void EncodeLength(BinaryWriter stream, int length) + { + if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative"); + if (length < 0x80) + { + // Short form + stream.Write((byte)length); + } + else + { + // Long form + var temp = length; + var bytesRequired = 0; + while (temp > 0) + { + temp >>= 8; + bytesRequired++; + } + stream.Write((byte)(bytesRequired | 0x80)); + for (var i = bytesRequired - 1; i >= 0; i--) + { + stream.Write((byte)(length >> (8 * i) & 0xff)); + } + } + } + + private void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) + { + stream.Write((byte)0x02); // INTEGER + var prefixZeros = 0; + for (var i = 0; i < value.Length; i++) + { + if (value[i] != 0) break; + prefixZeros++; + } + if (value.Length - prefixZeros == 0) + { + EncodeLength(stream, 1); + stream.Write((byte)0); + } + else + { + if (forceUnsigned && value[prefixZeros] > 0x7f) + { + // Add a prefix zero to force unsigned if the MSB is 1 + EncodeLength(stream, value.Length - prefixZeros + 1); + stream.Write((byte)0); + } + else + { + EncodeLength(stream, value.Length - prefixZeros); + } + for (var i = prefixZeros; i < value.Length; i++) + { + stream.Write(value[i]); + } + } + } + } +} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/Services/LetsEncryptService.cs b/v2.0/LetsEncrypt/Services/LetsEncryptService.cs new file mode 100644 index 0000000..1fa1c98 --- /dev/null +++ b/v2.0/LetsEncrypt/Services/LetsEncryptService.cs @@ -0,0 +1,567 @@ +using System; + +using System.Threading; +using System.Threading.Tasks; + +using System.Collections.Generic; + +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using System.Net.Http; + +using System.IO; +using System.Text; +using System.Linq; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using LetsEncrypt.Entities; +using LetsEncrypt.Exceptions; + + + +namespace LetsEncrypt.Services { + + public interface ILetsEncryptService { + Task Init(string url, string home, string siteName, string[] contacts, CancellationToken token = default(CancellationToken)); + string GetTermsOfServiceUri(CancellationToken token = default(CancellationToken)); + bool TryGetCachedCertificate(string subject, out CachedCertificateResult value); + Task NewNonce(CancellationToken token = default(CancellationToken)); + Task> NewOrder(string[] hostnames, string challengeType, CancellationToken token = default(CancellationToken)); + Task CompleteChallenges(CancellationToken token = default(CancellationToken)); + Task GetOrder(string[] hostnames, CancellationToken token = default(CancellationToken)); + Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject, CancellationToken token = default(CancellationToken)); + } + + + + + public class LetsEncryptService: ILetsEncryptService { + + private readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory; + + private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings { + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented + }; + + private readonly IJwsService _jwsService; + + + + private string _path; + private string _url; + private string _home; + private string _nonce; + + private RSACryptoServiceProvider _accountKey; + + private RegistrationCache _cache; + private HttpClient _client; + private AcmeDirectory _directory; + private List _challenges = new List(); + private Order _currentOrder; + + + public LetsEncryptService(IJwsService jwsService) { + _jwsService = jwsService; + } + + /// + /// Account creation or Initialization from cache + /// + /// + /// + /// + public async Task Init(string url, string home, string siteName, string[] contacts, CancellationToken token = default(CancellationToken)) { + // old Letsencrypt constructor + _url = url ?? throw new ArgumentNullException(nameof(url)); + var hash = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(siteName)); + + _home = home ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create); + + var file = _jwsService.Base64UrlEncoded(hash) + ".lets-encrypt.cache.json"; + _path = Path.Combine(_home, file); + + + // originally Init part was here + _accountKey = new RSACryptoServiceProvider(4096); + _client = GetCachedClient(_url); + + // 1 - Get directory + (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), null, token); + + + if (File.Exists(_path)) + { + bool success; + try + { + lock (Locker) + { + _cache = JsonConvert.DeserializeObject(File.ReadAllText(_path)); + } + + _accountKey.ImportCspBlob(_cache.AccountKey); + //_jws = new Jws(_accountKey, _cache.Id); + success = true; + } + catch + { + success = false; + // if we failed for any reason, we'll just + // generate a new registration + } + + if (success) + { + return; + } + } + + await NewNonce(); + + //New Account request + _jwsService.Init(_accountKey, null); + var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, new Account + { + // we validate this in the UI before we get here, so that is fine + TermsOfServiceAgreed = true, + Contacts = contacts.Select(contact => + string.Format("mailto:{0}", contact) + ).ToArray() + + }, token); + _jwsService.SetKeyId(account); + + if (account.Status != "valid") + throw new InvalidOperationException("Account status is not valid, was: " + account.Status + Environment.NewLine + response); + + lock (Locker) + { + _cache = new RegistrationCache + { + Location = account.Location, + AccountKey = _accountKey.ExportCspBlob(true), + Id = account.Id, + Key = account.Key + }; + File.WriteAllText(_path, JsonConvert.SerializeObject(_cache, Formatting.Indented)); + } + } + + + /// + /// Just retrive terms of service + /// + /// + /// + public string GetTermsOfServiceUri(CancellationToken token = default(CancellationToken)) + { + return _directory.Meta.TermsOfService; + } + + /// + /// Request New Nonce to be able to start POST requests + /// + /// + /// + public async Task NewNonce(CancellationToken token = default(CancellationToken)) + { + var result = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce)).ConfigureAwait(false); + _nonce = result.Headers.GetValues("Replay-Nonce").First(); + } + + /// + /// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange. + /// + /// Available challange types: + /// + /// dns-01 + /// http-01 + /// tls-alpn-01 + /// + /// + /// + /// + /// + /// + /// + public async Task> NewOrder(string[] hostnames, string challengeType, CancellationToken token = default(CancellationToken)) { + _challenges.Clear(); + + //update jws with account url + _jwsService.Init(_accountKey, _cache.Location.ToString()); + + var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, new Order + { + Expires = DateTime.UtcNow.AddDays(2), + Identifiers = hostnames.Select(hostname => new OrderIdentifier + { + Type = "dns", + Value = hostname + }).ToArray() + }, token); + + if (order.Status != "pending") + throw new InvalidOperationException("Created new order and expected status 'pending', but got: " + order.Status + Environment.NewLine + + response); + _currentOrder = order; + + var results = new Dictionary(); + foreach (var item in order.Authorizations) + { + var (challengeResponse, responseText) = await SendAsync(HttpMethod.Get, item, null, token); + if (challengeResponse.Status == "valid") + continue; + + if (challengeResponse.Status != "pending") + throw new InvalidOperationException("Expected autorization status 'pending', but got: " + order.Status + + Environment.NewLine + responseText); + + var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType); + _challenges.Add(challenge); + + var keyToken = _jwsService.GetKeyAuthorization(challenge.Token); + + switch (challengeType) { + + // A client fulfills this challenge by constructing a key authorization + // from the "token" value provided in the challenge and the client's + // account key. The client then computes the SHA-256 digest [FIPS180-4] + // of the key authorization. + // + // The record provisioned to the DNS contains the base64url encoding of + // this digest. + + case "dns-01": { + using (var sha256 = SHA256.Create()) + { + var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken))); + results[challengeResponse.Identifier.Value] = dnsToken; + } + break; + } + + + // A client fulfills this challenge by constructing a key authorization + // from the "token" value provided in the challenge and the client's + // account key. The client then provisions the key authorization as a + // resource on the HTTP server for the domain in question. + // + // The path at which the resource is provisioned is comprised of the + // fixed prefix "/.well-known/acme-challenge/", followed by the "token" + // value in the challenge. The value of the resource MUST be the ASCII + // representation of the key authorization. + + case "http-01": { + results[challengeResponse.Identifier.Value] = challenge.Token + "~" + keyToken; + break; + } + + } + + + + } + + return results; + } + + + + + public async Task CompleteChallenges(CancellationToken token = default(CancellationToken)) + { + _jwsService.Init(_accountKey, _cache.Location.ToString()); + + for (var index = 0; index < _challenges.Count; index++) + { + var challenge = _challenges[index]; + + while (true) + { + AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge(); + + switch (challenge.Type) { + case "dns-01": { + authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token); + break; + } + + case "http-01": { + break; + } + } + + var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, authorizeChallenge, token); + + if (result.Status == "valid") + break; + if (result.Status != "pending") + throw new InvalidOperationException("Failed autorization of " + _currentOrder.Identifiers[index].Value + Environment.NewLine + responseText); + + + await Task.Delay(500); + } + } + } + + + + + public async Task GetOrder(string[] hostnames, CancellationToken token = default(CancellationToken)) + { + //update jws + _jwsService.Init(_accountKey, _cache.Location.ToString()); + + var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, new Order + { + Expires = DateTime.UtcNow.AddDays(2), + Identifiers = hostnames.Select(hostname => new OrderIdentifier + { + Type = "dns", + Value = hostname + }).ToArray() + }, token); + + _currentOrder = order; + } + + + + + + /// + /// + /// + /// + /// + public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject, CancellationToken token = default(CancellationToken)) + { + var key = new RSACryptoServiceProvider(4096); + var csr = new CertificateRequest("CN=" + subject, + key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + var san = new SubjectAlternativeNameBuilder(); + foreach (var host in _currentOrder.Identifiers) + san.AddDnsName(host.Value); + + csr.CertificateExtensions.Add(san.Build()); + + var (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, new FinalizeRequest + { + CSR = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest()) + }, token); + + while (response.Status != "valid") + { + (response, responseText) = await SendAsync(HttpMethod.Get, response.Location, null, token); + + if(response.Status == "processing") + { + await Task.Delay(500); + continue; + } + throw new InvalidOperationException("Invalid order status: " + response.Status + Environment.NewLine + + responseText); + } + var (pem, _) = await SendAsync(HttpMethod.Get, response.Certificate, null, token); + + var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem)); + + _cache.CachedCerts[subject] = new CertificateCache + { + Cert = pem, + Private = key.ExportCspBlob(true) + }; + + lock (Locker) + { + File.WriteAllText(_path, + JsonConvert.SerializeObject(_cache, Formatting.Indented)); + } + + return (cert, key); + } + + + + + + + /// + /// + /// + /// + /// + public async Task KeyChange(CancellationToken token = default(CancellationToken)) + { + + } + + + /// + /// + /// + /// + /// + public async Task RevokeCertificate(CancellationToken token = default(CancellationToken)) + { + + } + + + + + + /// + /// Main method used to send data to LetsEncrypt + /// + /// + /// + /// + /// + /// + /// + private async Task<(TResult Result, string Response)> SendAsync(HttpMethod method, Uri uri, object message, CancellationToken token) where TResult : class + { + var request = new HttpRequestMessage(method, uri); + + if (message != null) + { + JwsMessage encodedMessage = _jwsService.Encode(message, new JwsHeader + { + Nonce = _nonce, + Url = uri, + }); + + var json = JsonConvert.SerializeObject(encodedMessage, jsonSettings); + + request.Content = new StringContent(json); + + var requestType = "application/json"; + if (method == HttpMethod.Post) + requestType = "application/jose+json"; + + request.Content.Headers.Remove("Content-Type"); + request.Content.Headers.Add("Content-Type", requestType); + } + + var response = await _client.SendAsync(request, token).ConfigureAwait(false); + + if (method == HttpMethod.Post) + _nonce = response.Headers.GetValues("Replay-Nonce").First(); + + if (response.Content.Headers.ContentType.MediaType == "application/problem+json") + { + var problemJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var problem = JsonConvert.DeserializeObject(problemJson); + problem.RawJson = problemJson; + throw new LetsEncrytException(problem, response); + } + + var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") + { + return ((TResult)(object)responseText, null); + } + + var responseContent = JObject.Parse(responseText).ToObject(); + + if (responseContent is IHasLocation ihl) + { + if (response.Headers.Location != null) + ihl.Location = response.Headers.Location; + } + + return (responseContent, responseText); + } + + + /// + /// + /// + /// + /// + /// + public bool TryGetCachedCertificate(string subject, out CachedCertificateResult value) + { + value = null; + if (_cache.CachedCerts.TryGetValue(subject, out var cache) == false) + { + return false; + } + + var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cache.Cert)); + + // if it is about to expire, we need to refresh + if ((cert.NotAfter - DateTime.UtcNow).TotalDays < 30) + return false; + + var rsa = new RSACryptoServiceProvider(4096); + rsa.ImportCspBlob(cache.Private); + + + value = new CachedCertificateResult + { + Certificate = cache.Cert, + PrivateKey = rsa + }; + return true; + } + + + /// + /// + /// + /// + public void ResetCachedCertificate(IEnumerable hostsToRemove) + { + foreach (var host in hostsToRemove) + { + _cache.CachedCerts.Remove(host); + } + } + + + + private Dictionary _cachedClients = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// In our scenario, we assume a single single wizard progressing + /// and the locking is basic to the wizard progress. Adding explicit + /// locking to be sure that we are not corrupting disk state if user + /// is explicitly calling stuff concurrently (running the setup wizard + /// from two tabs?) + /// + private readonly object Locker = new object(); + private HttpClient GetCachedClient(string url) { + + if (_cachedClients.TryGetValue(url, out var value)) { + return value; + } + + lock (Locker) { + if (_cachedClients.TryGetValue(url, out value)) { + return value; + } + + value = new HttpClient { + BaseAddress = new Uri(url) + }; + + _cachedClients = new Dictionary(_cachedClients, StringComparer.OrdinalIgnoreCase) { + [url] = value + }; + return value; + } + } + + } + + +} \ No newline at end of file diff --git a/v2.0/LetsEncrypt/appsettings.json b/v2.0/LetsEncrypt/appsettings.json new file mode 100644 index 0000000..ccf5556 --- /dev/null +++ b/v2.0/LetsEncrypt/appsettings.json @@ -0,0 +1,74 @@ +{ + "AppSettings": { + + "active": "ProductionV2", + + "environments": [ + { + "name": "StagingV2", + "url": "https://acme-staging-v02.api.letsencrypt.org/directory", + + "www": "/var/www", + "acme": ".well-known/acme-challenge", + "ssl": "/etc/nginx/ssl" + }, + { + "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" + } + ] + } + ] + } +} \ No newline at end of file