Merge branch 'main' of gitsrv0001:MAKS-IT/letsencryptclient

This commit is contained in:
Maksym Sadovnychyy 2023-08-30 15:59:32 +02:00
commit c177611798
34 changed files with 1254 additions and 1254 deletions

View File

@ -8,7 +8,9 @@
</PropertyGroup>
<ItemGroup>
<Folder Include="Abstractions\" />
<Compile Remove="Abstractions\**" />
<EmbeddedResource Remove="Abstractions\**" />
<None Remove="Abstractions\**" />
</ItemGroup>
</Project>

View File

@ -1,8 +1,8 @@
using System.Text.Json.Serialization;
using System.Text.Json;
namespace MaksIT.Core.Extensions {
public static class ObjectExtensions {
namespace MaksIT.Core.Extensions;
public static class ObjectExtensions {
/// <summary>
///
@ -33,5 +33,4 @@ namespace MaksIT.Core.Extensions {
return JsonSerializer.Serialize(obj, options);
}
}
}

View File

@ -6,8 +6,8 @@ using System.Text.Json.Serialization;
using System.Text.Json;
using System.Threading.Tasks;
namespace MaksIT.Core.Extensions {
public static class StringExtensions {
namespace MaksIT.Core.Extensions;
public static class StringExtensions {
/// <summary>
/// Converts JSON string to object
/// </summary>
@ -36,5 +36,4 @@ namespace MaksIT.Core.Extensions {
? JsonSerializer.Deserialize<T>(s, options)
: default;
}
}
}

View File

@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
namespace MaksIT.Core {
public static class OperatingSystem {
namespace MaksIT.Core;
public static class OperatingSystem {
public static bool IsWindows() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
@ -10,5 +10,4 @@ namespace MaksIT.Core {
public static bool IsLinux() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
}
}

View File

@ -2,8 +2,8 @@
using System.Text.Json.Serialization;
namespace MaksIT.LetsEncrypt.Entities.Jws {
public class Jwk {
namespace MaksIT.LetsEncrypt.Entities.Jws;
public class Jwk {
/// <summary>
/// "kty" (Key Type) Parameter
/// <para>
@ -101,5 +101,4 @@ namespace MaksIT.LetsEncrypt.Entities.Jws {
/// </summary>
[JsonPropertyName("alg")]
public string? Algorithm { get; set; }
}
}

View File

@ -1,20 +1,20 @@
using System;
using System.Text.Json.Serialization;
namespace MaksIT.LetsEncrypt.Entities.Jws
{
namespace MaksIT.LetsEncrypt.Entities.Jws;
public class JwsMessage {
public class JwsMessage {
public string? Protected { get; set; }
public string? Payload { get; set; }
public string? Signature { get; set; }
}
}
public class JwsHeader {
public class JwsHeader {
[JsonPropertyName("alg")]
public string? Algorithm { get; set; }
@ -33,8 +33,4 @@ namespace MaksIT.LetsEncrypt.Entities.Jws
[JsonPropertyName("Host")]
public string? Host { get; set; }
}
}

View File

@ -1,7 +1,7 @@
using System;
namespace MaksIT.LetsEncrypt.Entities {
public class AuthorizationChallenge {
namespace MaksIT.LetsEncrypt.Entities;
public class AuthorizationChallenge {
public Uri? Url { get; set; }
public string? Type { get; set; }
@ -9,5 +9,4 @@ namespace MaksIT.LetsEncrypt.Entities {
public string? Status { get; set; }
public string? Token { get; set; }
}
}

View File

@ -1,11 +1,8 @@
using System.Security.Cryptography;
namespace MaksIT.LetsEncrypt.Entities
{
public class CachedCertificateResult
{
namespace MaksIT.LetsEncrypt.Entities;
public class CachedCertificateResult {
public RSACryptoServiceProvider? PrivateKey { get; set; }
public string? Certificate { get; set; }
}
}

View File

@ -1,18 +1,62 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Text;
using MaksIT.LetsEncrypt.Entities.Jws;
namespace MaksIT.LetsEncrypt.Entities {
public class CertificateCache {
namespace MaksIT.LetsEncrypt.Entities;
public class CertificateCache {
public string? Cert { get; set; }
public byte[]? Private { get; set; }
}
}
public class RegistrationCache {
public class RegistrationCache {
public Dictionary<string, CertificateCache>? CachedCerts { get; set; }
public byte[]? AccountKey { get; set; }
public string? Id { get; set; }
public Jwk? Key { get; set; }
public Uri? Location { get; set; }
/// <summary>
///
/// </summary>
/// <param name="subject"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool TryGetCachedCertificate(string subject, out CachedCertificateResult? value) {
value = null;
if (CachedCerts == null)
return false;
if (!CachedCerts.TryGetValue(subject, out var cache)) {
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;
}
/// <summary>
///
/// </summary>
/// <param name="hostsToRemove"></param>
public void ResetCachedCertificate(IEnumerable<string> hostsToRemove) {
if (CachedCerts != null)
foreach (var host in hostsToRemove)
CachedCerts.Remove(host);
}
}

View File

@ -0,0 +1,10 @@
namespace MaksIT.LetsEncrypt.Entities {
public class SendResult<TResult> {
public TResult? Result { get; set; }
public string? ResponseText { get; set; }
}
}

View File

@ -1,25 +1,19 @@
using System;
using System.Net.Http;
using MaksIT.Core.Extensions;
using MaksIT.LetsEncrypt.Models.Responses;
namespace MaksIT.LetsEncrypt.Exceptions;
public class LetsEncrytException : Exception {
public Problem? Problem { get; }
public HttpResponseMessage Response { get; }
public LetsEncrytException(
Problem? problem,
HttpResponseMessage response
) : base(problem != null ? $"{problem.Type}: {problem.Detail}" : "") {
namespace MaksIT.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 {
public string Type { get; set; }
public string Detail { get; set; }
public string RawJson { get; set; }
}
}

View File

@ -2,11 +2,10 @@
using MaksIT.LetsEncrypt.Services;
namespace MaksIT.LetsEncrypt.Extensions {
public static class ServiceCollectionExtensions {
namespace MaksIT.LetsEncrypt.Extensions;
public static class ServiceCollectionExtensions {
public static void RegisterLetsEncrypt(this IServiceCollection services) {
services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
}
}
}

View File

@ -8,8 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DomainResult.Common" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
</ItemGroup>

View File

@ -1,5 +1,4 @@
namespace MaksIT.LetsEncrypt.Models.Interfaces {
public interface IHasLocation {
namespace MaksIT.LetsEncrypt.Models.Interfaces;
public interface IHasLocation {
Uri? Location { get; set; }
}
}

View File

@ -1,7 +1,5 @@
namespace MaksIT.LetsEncrypt.Models.Requests
{
public class FinalizeRequest
{
namespace MaksIT.LetsEncrypt.Models.Requests;
public class FinalizeRequest {
public string? Csr { get; set; }
}
}

View File

@ -5,9 +5,9 @@ using MaksIT.LetsEncrypt.Models.Interfaces;
* https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.3
*/
namespace MaksIT.LetsEncrypt.Models.Responses
{
public class Account : IHasLocation {
namespace MaksIT.LetsEncrypt.Models.Responses;
public class Account : IHasLocation {
public bool TermsOfServiceAgreed { get; set; }
@ -34,5 +34,4 @@ namespace MaksIT.LetsEncrypt.Models.Responses
public Uri? Orders { get; set; }
public Uri? Location { get; set; }
}
}

View File

@ -1,11 +1,9 @@
using System;
namespace MaksIT.LetsEncrypt.Models.Responses
{
public class AcmeDirectory
{
public Uri NewNonce { get; set; }
namespace MaksIT.LetsEncrypt.Models.Responses;
public class AcmeDirectory {
public Uri NewNonce { get; set; }
public Uri NewAccount { get; set; }
@ -20,10 +18,8 @@ namespace MaksIT.LetsEncrypt.Models.Responses
public Uri KeyChange { get; set; }
public AcmeDirectoryMeta Meta { get; set; }
}
public class AcmeDirectoryMeta
{
public string TermsOfService { get; set; }
}
}
public class AcmeDirectoryMeta {
public string TermsOfService { get; set; }
}

View File

@ -1,8 +1,9 @@

using MaksIT.LetsEncrypt.Entities;
namespace MaksIT.LetsEncrypt.Models.Responses {
public class AuthorizationChallengeResponse {
namespace MaksIT.LetsEncrypt.Models.Responses;
public class AuthorizationChallengeResponse {
public OrderIdentifier? Identifier { get; set; }
public string? Status { get; set; }
@ -12,9 +13,8 @@ namespace MaksIT.LetsEncrypt.Models.Responses {
public bool Wildcard { get; set; }
public AuthorizationChallenge[]? Challenges { get; set; }
}
public class AuthorizeChallenge {
public string? KeyAuthorization { get; set; }
}
}
public class AuthorizeChallenge {
public string? KeyAuthorization { get; set; }
}

View File

@ -1,16 +1,16 @@
using MaksIT.LetsEncrypt.Exceptions;
using MaksIT.LetsEncrypt.Models.Interfaces;
namespace MaksIT.LetsEncrypt.Models.Responses {
namespace MaksIT.LetsEncrypt.Models.Responses;
public class OrderIdentifier {
public class OrderIdentifier {
public string? Type { get; set; }
public string? Value { get; set; }
}
}
public class Order : IHasLocation {
public class Order : IHasLocation {
public Uri? Location { get; set; }
public string? Status { get; set; }
@ -30,5 +30,4 @@ namespace MaksIT.LetsEncrypt.Models.Responses {
public Uri? Finalize { get; set; }
public Uri? Certificate { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.LetsEncrypt.Models.Responses {
public class Problem {
public string Type { get; set; }
public string Detail { get; set; }
public string RawJson { get; set; }
}
}

View File

@ -11,8 +11,9 @@ using MaksIT.LetsEncrypt.Entities.Jws;
using MaksIT.Core.Extensions;
namespace MaksIT.LetsEncrypt.Services {
public interface IJwsService {
namespace MaksIT.LetsEncrypt.Services;
public interface IJwsService {
void SetKeyId(string location);
JwsMessage Encode(JwsHeader protectedHeader);
@ -25,10 +26,10 @@ namespace MaksIT.LetsEncrypt.Services {
string Base64UrlEncoded(string s);
string Base64UrlEncoded(byte[] arg);
}
}
public class JwsService : IJwsService {
public class JwsService : IJwsService {
public Jwk _jwk;
private RSA _rsa;
@ -105,8 +106,4 @@ namespace MaksIT.LetsEncrypt.Services {
.Split('=').First() // Remove any trailing '='s
.Replace('+', '-') // 62nd char of encoding
.Replace('/', '_'); // 63rd char of encoding
}
}

View File

@ -18,32 +18,31 @@ using MaksIT.LetsEncrypt.Models.Responses;
using MaksIT.LetsEncrypt.Models.Interfaces;
using MaksIT.LetsEncrypt.Models.Requests;
using MaksIT.LetsEncrypt.Entities.Jws;
using System.Xml;
using System.Diagnostics;
using DomainResults.Common;
namespace MaksIT.LetsEncrypt.Services {
namespace MaksIT.LetsEncrypt.Services;
public interface ILetsEncryptService {
public interface ILetsEncryptService {
Task ConfigureClient(string url);
Task<IDomainResult> ConfigureClient(string url);
Task Init(string[] contacts, RegistrationCache? registrationCache);
string GetTermsOfServiceUri();
Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType);
Task CompleteChallenges();
Task GetOrder(string[] hostnames);
Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject);
Task<IDomainResult> Init(string[] contacts, RegistrationCache? registrationCache);
RegistrationCache? GetRegistrationCache();
}
(string?, IDomainResult) GetTermsOfServiceUri();
Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(string[] hostnames, string challengeType);
Task<IDomainResult> CompleteChallenges();
Task<IDomainResult> GetOrder(string[] hostnames);
Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject);
}
public class LetsEncryptService : ILetsEncryptService {
public class LetsEncryptService : ILetsEncryptService {
//private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
// NullValueHandling = NullValueHandling.Ignore,
@ -54,7 +53,7 @@ namespace MaksIT.LetsEncrypt.Services {
private HttpClient _httpClient;
private IJwsService _jwsService;
private IJwsService? _jwsService;
private AcmeDirectory? _directory;
private RegistrationCache? _cache;
@ -78,11 +77,22 @@ namespace MaksIT.LetsEncrypt.Services {
/// <param name="url"></param>
/// <param name="contacts"></param>
/// <returns></returns>
public async Task ConfigureClient(string url) {
public async Task<IDomainResult> ConfigureClient(string url) {
try {
_httpClient.BaseAddress ??= new Uri(url);
(_directory, _) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
var (directory, getAcmeDirectoryResult) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
if (!getAcmeDirectoryResult.IsSuccess)
return getAcmeDirectoryResult;
_directory = directory.Result;
return IDomainResult.Success();
}
catch (Exception ex) {
_logger.LogError(ex, "Let's Encrypt client unhandled exception");
return IDomainResult.CriticalDependencyError();
}
}
/// <summary>
@ -91,13 +101,17 @@ namespace MaksIT.LetsEncrypt.Services {
/// <param name="contacts"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task Init(string? [] contacts, RegistrationCache? cache) {
public async Task<IDomainResult> Init(string? [] contacts, RegistrationCache? cache) {
try {
_logger.LogInformation($"Executing {nameof(Init)}...");
if (contacts == null || contacts.Length == 0)
throw new ArgumentNullException();
return IDomainResult.Failed();
if (_directory == null)
throw new ArgumentNullException();
return IDomainResult.Failed();
var accountKey = new RSACryptoServiceProvider(4096);
@ -115,18 +129,29 @@ namespace MaksIT.LetsEncrypt.Services {
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
};
var (account, response) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
_jwsService.SetKeyId(account.Location.ToString());
var (account, postAccuntResult) = await SendAsync<Account>(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
_jwsService.SetKeyId(account.Result.Location.ToString());
if (account.Status != "valid")
throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}");
if (account.Result.Status != "valid") {
_logger.LogError($"Account status is not valid, was: {account.Result.Status} \r\n {account.ResponseText}");
return IDomainResult.Failed();
}
_cache = new RegistrationCache {
Location = account.Location,
Location = account.Result.Location,
AccountKey = accountKey.ExportCspBlob(true),
Id = account.Id,
Key = account.Key
Id = account.Result.Id,
Key = account.Result.Key
};
return IDomainResult.Success();
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
}
/// <summary>
@ -141,15 +166,24 @@ namespace MaksIT.LetsEncrypt.Services {
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public string GetTermsOfServiceUri() {
public (string?, IDomainResult) GetTermsOfServiceUri() {
try {
if (_directory == null)
throw new NullReferenceException();
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
return _directory.Meta.TermsOfService;
if (_directory == null) {
return IDomainResult.Failed<string?>();
}
return IDomainResult.Success(_directory.Meta.TermsOfService);
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError<string?>(message);
}
}
/// <summary>
/// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange.
@ -166,7 +200,11 @@ namespace MaksIT.LetsEncrypt.Services {
/// <param name="challengeType"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<Dictionary<string, string>> NewOrder(string[] hostnames, string challengeType) {
public async Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(string[] hostnames, string challengeType) {
try {
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
_challenges.Clear();
var letsEncryptOrder = new Order {
@ -177,27 +215,38 @@ namespace MaksIT.LetsEncrypt.Services {
}).ToArray()
};
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
var (order, postNewOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
if (!postNewOrderResult.IsSuccess) {
return (null, postNewOrderResult);
}
if (order.Status == "ready")
return new Dictionary<string, string>();
if (order.Result.Status == "ready")
return IDomainResult.Success(new Dictionary<string, string>());
if (order.Status != "pending")
throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}");
if (order.Result.Status != "pending") {
_logger.LogError($"Created new order and expected status 'pending', but got: {order.Result.Status} \r\n {order.Result}");
return IDomainResult.Failed<Dictionary<string, string>?>();
}
_currentOrder = order;
_currentOrder = order.Result;
var results = new Dictionary<string, string>();
foreach (var item in order.Authorizations) {
var (challengeResponse, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
foreach (var item in order.Result.Authorizations) {
if (challengeResponse.Status == "valid")
var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null);
if (!postAuthorisationChallengeResult.IsSuccess) {
return (null, postAuthorisationChallengeResult);
}
if (challengeResponse.Result.Status == "valid")
continue;
if (challengeResponse.Status != "pending")
throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {responseText}");
if (challengeResponse.Result.Status != "pending") {
_logger.LogError($"Expected autorization status 'pending', but got: {order.Result.Status} \r\n {challengeResponse.ResponseText}");
return IDomainResult.Failed<Dictionary<string, string>?>();
}
var challenge = challengeResponse.Challenges.First(x => x.Type == challengeType);
var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType);
_challenges.Add(challenge);
var keyToken = _jwsService.GetKeyAuthorization(challenge.Token);
@ -215,7 +264,7 @@ namespace MaksIT.LetsEncrypt.Services {
case "dns-01": {
using (var sha256 = SHA256.Create()) {
var dnsToken = _jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
results[challengeResponse.Identifier.Value] = dnsToken;
results[challengeResponse.Result.Identifier.Value] = dnsToken;
}
break;
}
@ -232,7 +281,7 @@ namespace MaksIT.LetsEncrypt.Services {
// representation of the key authorization.
case "http-01": {
results[challengeResponse.Identifier.Value] = keyToken;
results[challengeResponse.Result.Identifier.Value] = keyToken;
break;
}
@ -241,7 +290,14 @@ namespace MaksIT.LetsEncrypt.Services {
}
}
return results;
return IDomainResult.Success(results);
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError<Dictionary<string, string>?>(message);
}
}
/// <summary>
@ -249,14 +305,23 @@ namespace MaksIT.LetsEncrypt.Services {
/// </summary>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task CompleteChallenges() {
public async Task<IDomainResult> CompleteChallenges() {
try {
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
if (_currentOrder?.Identifiers == null) {
return IDomainResult.Failed();
}
for (var index = 0; index < _challenges.Count; index++) {
var challenge = _challenges[index];
var start = DateTime.UtcNow;
while (true) {
AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge();
var authorizeChallenge = new AuthorizeChallenge();
switch (challenge.Type) {
case "dns-01": {
@ -270,16 +335,34 @@ namespace MaksIT.LetsEncrypt.Services {
}
}
var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
var (authChallenge, postAuthChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}");
if (!postAuthChallengeResult.IsSuccess) {
return postAuthChallengeResult;
}
if (result.Status == "valid")
if (authChallenge.Result.Status == "valid")
break;
if (result.Status != "pending")
throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}");
if (authChallenge.Result.Status != "pending") {
_logger.LogError($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {authChallenge.ResponseText}");
return IDomainResult.Failed();
}
await Task.Delay(1000);
if ((DateTime.UtcNow - start).Seconds > 120)
throw new TimeoutException();
}
}
return IDomainResult.Success();
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
}
/// <summary>
@ -287,7 +370,11 @@ namespace MaksIT.LetsEncrypt.Services {
/// </summary>
/// <param name="hostnames"></param>
/// <returns></returns>
public async Task GetOrder(string[] hostnames) {
public async Task<IDomainResult> GetOrder(string[] hostnames) {
try {
_logger.LogInformation($"Executing {nameof(GetOrder)}");
var letsEncryptOrder = new Order {
Expires = DateTime.UtcNow.AddDays(2),
@ -297,9 +384,20 @@ namespace MaksIT.LetsEncrypt.Services {
}).ToArray()
};
var (order, response) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
if (!postOrderResult.IsSuccess)
return postOrderResult;
_currentOrder = order;
_currentOrder = order.Result;
return IDomainResult.Success();
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
}
/// <summary>
@ -308,13 +406,14 @@ namespace MaksIT.LetsEncrypt.Services {
/// <param name="subject"></param>
/// <returns>Cert and Private key</returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) {
public async Task<((X509Certificate2 Cert, RSA PrivateKey)?, IDomainResult)> GetCertificate(string subject) {
_logger.LogInformation($"Invoked: {nameof(GetCertificate)}");
try {
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
if (_currentOrder == null)
throw new ArgumentNullException();
if (_currentOrder == null) {
return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>();
}
var key = new RSACryptoServiceProvider(4096);
var csr = new CertificateRequest("CN=" + subject,
@ -340,39 +439,54 @@ namespace MaksIT.LetsEncrypt.Services {
await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
if (_currentOrder.Status == "ready") {
var (response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
if (!postOrderResult.IsSuccess || order?.Result == null)
return (null, postOrderResult);
if (response.Status == "processing")
(response, responseText) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
if (response.Status == "valid") {
certificateUrl = response.Certificate;
if (order.Result.Status == "processing") {
(order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, _currentOrder.Location, true, null);
if (!postOrderResult.IsSuccess || order?.Result == null)
return (null, postOrderResult);
}
if (order.Result.Status == "valid") {
certificateUrl = order.Result.Certificate;
}
}
if ((start - DateTime.UtcNow).Seconds > 120)
if ((DateTime.UtcNow - start).Seconds > 120)
throw new TimeoutException();
await Task.Delay(1000);
continue;
// throw new InvalidOperationException(/*$"Invalid order status: "*/);
}
var (pem, _) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
var (pem, postPemResult) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null);
if (!postPemResult.IsSuccess || pem?.Result == null)
return (null, postPemResult);
if (_cache == null)
throw new NullReferenceException();
if (_cache == null) {
_logger.LogError($"{nameof(_cache)} is null");
return IDomainResult.Failed<(X509Certificate2 Cert, RSA PrivateKey)?>();
}
_cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
_cache.CachedCerts[subject] = new CertificateCache {
Cert = pem,
Cert = pem.Result,
Private = key.ExportCspBlob(true)
};
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem));
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
return (cert, key);
return IDomainResult.Success((cert, key));
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError< (X509Certificate2 Cert, RSA PrivateKey)?>(message);
}
}
/// <summary>
@ -380,7 +494,7 @@ namespace MaksIT.LetsEncrypt.Services {
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public Task KeyChange() {
public Task<IDomainResult> KeyChange() {
throw new NotImplementedException();
}
@ -389,27 +503,71 @@ namespace MaksIT.LetsEncrypt.Services {
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public Task RevokeCertificate() {
public Task<IDomainResult> RevokeCertificate() {
throw new NotImplementedException();
}
/// <summary>
/// Request New Nonce to be able to start POST requests
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private async Task<(string?, IDomainResult)> NewNonce() {
try {
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
if (_directory == null)
IDomainResult.Failed();
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce));
return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First());
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError<string?>(message);
}
}
/// <summary>
/// Main method used to send data to LetsEncrypt
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="method"></param>
/// <param name="uri"></param>
/// <param name="message"></param>
/// <param name="requestModel"></param>
/// <param name="token"></param>
/// <returns></returns>
private async Task<(TResult, string)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class {
private async Task<(SendResult<TResult>?, IDomainResult)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel) {
try {
_logger.LogInformation($"Executing {nameof(SendAsync)}...");
//if (_jwsService == null) {
// _logger.LogError($"{nameof(_jwsService)} is null");
// return IDomainResult.Failed<SendResult<TResult>?>();
//}
var request = new HttpRequestMessage(method, uri);
_nonce = uri.OriginalString != "directory"
? await NewNonce()
: default;
if (uri.OriginalString != "directory") {
var (nonce, newNonceResult) = await NewNonce();
if (!newNonceResult.IsSuccess || nonce == null) {
return (null, newNonceResult);
}
if (message != null || isPostAsGet) {
_nonce = nonce;
}
else {
_nonce = default;
}
if (requestModel != null || isPostAsGet) {
var jwsHeader = new JwsHeader {
Url = uri,
};
@ -419,7 +577,7 @@ namespace MaksIT.LetsEncrypt.Services {
var encodedMessage = isPostAsGet
? _jwsService.Encode(jwsHeader)
: _jwsService.Encode(message, jwsHeader);
: _jwsService.Encode(requestModel, jwsHeader);
var json = encodedMessage.ToJson();
@ -438,17 +596,15 @@ namespace MaksIT.LetsEncrypt.Services {
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();
var problem = problemJson.ToObject<Problem>();
problem.RawJson = problemJson;
throw new LetsEncrytException(problem, response);
}
var responseText = await response.Content.ReadAsStringAsync();
if (typeof(TResult) == typeof(string) && response.Content.Headers.ContentType.MediaType == "application/pem-certificate-chain") {
return ((TResult)(object)responseText, null);
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json")
throw new LetsEncrytException(responseText.ToObject<Problem>(), response);
if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) {
return IDomainResult.Success(new SendResult<TResult> {
Result = (TResult)(object)responseText
});
}
var responseContent = responseText.ToObject<TResult>();
@ -458,20 +614,17 @@ namespace MaksIT.LetsEncrypt.Services {
ihl.Location = response.Headers.Location;
}
return (responseContent, responseText);
return IDomainResult.Success(new SendResult<TResult> {
Result = responseContent,
ResponseText = responseText
});
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
/// <summary>
/// Request New Nonce to be able to start POST requests
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private async Task<string> NewNonce() {
if (_directory == null)
throw new NotImplementedException();
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _directory.NewNonce));
return result.Headers.GetValues("Replay-Nonce").First();
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError<SendResult<TResult>?>(message);
}
}
}

View File

@ -1,7 +1,3 @@
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -10,52 +6,49 @@ using MaksIT.Core.Extensions;
using MaksIT.LetsEncrypt.Services;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncryptConsole.Services;
using SSHProvider;
using Mono.Unix.Native;
using Serilog.Core;
namespace MaksIT.LetsEncryptConsole {
using MaksIT.SSHProvider;
public interface IApp {
namespace MaksIT.LetsEncryptConsole;
public interface IApp {
Task Run(string[] args);
}
}
public class App : IApp {
public class App : IApp {
private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
private readonly ILogger<App> _logger;
private readonly Configuration _appSettings;
private readonly ILetsEncryptService _letsEncryptService;
private readonly IKeyService _keyService;
private readonly ITerminalService _terminalService;
public App(
ILogger<App> logger,
IOptions<Configuration> appSettings,
ILetsEncryptService letsEncryptService,
IKeyService keyService,
ITerminalService terminalService
) {
_logger = logger;
_appSettings = appSettings.Value;
_letsEncryptService = letsEncryptService;
_keyService = keyService;
_terminalService = terminalService;
}
public async Task Run(string[] args) {
_logger.LogInformation("Letsencrypt client estarted...");
try {
_logger.LogInformation("Let's Encrypt client. Started...");
foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List<LetsEncryptEnvironment>()) {
try {
_logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
//loop all customers
foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List<Customer>()) {
try {
_logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
//define cache folder
@ -74,7 +67,7 @@ namespace MaksIT.LetsEncryptConsole {
foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List<Site>()) {
_logger.LogInformation($"Managing site: {site.Name}");
try {
//create folder for ssl
string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
if (!Directory.Exists(sslPath)) {
@ -83,20 +76,22 @@ namespace MaksIT.LetsEncryptConsole {
var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
//1. Client initialization
#region LetsEncrypt client configuration and local registration cache initialization
_logger.LogInformation("1. Client Initialization...");
#region LetsEncrypt client configuration
await _letsEncryptService.ConfigureClient(env.Url);
#endregion
#region LetsEncrypt local registration cache initialization
var registrationCache = (File.Exists(cacheFile)
? File.ReadAllText(cacheFile)
: null)
.ToObject<RegistrationCache>();
await _letsEncryptService.Init(customer.Contacts, registrationCache);
var initResult = await _letsEncryptService.Init(customer.Contacts, registrationCache);
if (!initResult.IsSuccess) {
continue;
}
#endregion
#region LetsEncrypt terms of service
@ -107,28 +102,26 @@ namespace MaksIT.LetsEncryptConsole {
// if valid check if cert and key exists otherwise recreate
// else continue with new certificate request
var certRes = new CachedCertificateResult();
if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) {
string cert = Path.Combine(sslPath, $"{site.Name}.crt");
//if(!File.Exists(cert))
File.WriteAllText(cert, certRes.Certificate);
if (registrationCache != null && registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
string key = Path.Combine(sslPath, $"{site.Name}.key");
//if(!File.Exists(key)) {
using (StreamWriter writer = File.CreateText(key))
_keyService.ExportPrivateKey(certRes.PrivateKey, writer);
//}
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
if (certRes.PrivateKey != null)
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
_logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
}
else {
//try to make new order
try {
//create new orders
Console.WriteLine("2. Client New Order...");
//create new orders
#region LetsEncrypt new order
var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
_logger.LogInformation("2. Client New Order...");
var (orders, newOrderResult) = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
if (!newOrderResult.IsSuccess || orders == null) {
continue;
}
#endregion
if (orders.Count > 0) {
@ -156,10 +149,6 @@ namespace MaksIT.LetsEncryptConsole {
}
}
break;
}
@ -176,15 +165,25 @@ namespace MaksIT.LetsEncryptConsole {
#region LetsEncrypt complete challenges
_logger.LogInformation("3. Client Complete Challange...");
await _letsEncryptService.CompleteChallenges();
var completeChallengesResult = await _letsEncryptService.CompleteChallenges();
if (!completeChallengesResult.IsSuccess) {
continue;
}
_logger.LogInformation("Challanges comleted.");
#endregion
await Task.Delay(1000);
// Download new certificate
#region Download new certificate
_logger.LogInformation("4. Download certificate...");
var (cert, key) = await _letsEncryptService.GetCertificate(site.Name);
var (certData, getCertResult) = await _letsEncryptService.GetCertificate(site.Name);
if (!getCertResult.IsSuccess || certData == null) {
continue;
}
// not used in this scenario
// var (cert, key) = certData.Value;
#endregion
#region Persist cache
registrationCache = _letsEncryptService.GetRegistrationCache();
@ -194,13 +193,12 @@ namespace MaksIT.LetsEncryptConsole {
#region Save cert and key to filesystem
certRes = new CachedCertificateResult();
if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) {
if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
File.WriteAllText(Path.Combine(sslPath, site.Name + ".crt"), certRes.Certificate);
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.crt"), certRes.Certificate);
using (var writer = File.CreateText(Path.Combine(sslPath, site.Name + ".key"))) {
_keyService.ExportPrivateKey(certRes.PrivateKey, writer);
}
if (certRes.PrivateKey != null)
File.WriteAllText(Path.Combine(sslPath, $"{site.Name}.key"), certRes.PrivateKey.ExportRSAPrivateKeyPem());
_logger.LogInformation("Certificate saved.");
@ -219,93 +217,33 @@ namespace MaksIT.LetsEncryptConsole {
_logger.LogError("Unable to get new cached certificate.");
}
#endregion
}
}
}
}
_logger.LogInformation($"Let's Encrypt client. Execution complete.");
}
catch (Exception ex) {
_logger.LogError(ex, "");
await _letsEncryptService.GetOrder(site.Hosts);
_logger.LogError(ex, $"Let's Encrypt client. Unhandled exception.");
}
}
}
catch (Exception ex) {
_logger.LogError(ex, "Customer unhandled error");
}
}
}
catch (Exception ex) {
_logger.LogError(ex, "Environment unhandled error");
}
}
if (env.Name == "ProductionV2") {
_terminalService.Exec("systemctl restart nginx");
}
}
catch (Exception ex) {
_logger.LogError(ex.Message.ToString());
break;
}
}
}
/// <summary>
///
/// </summary>
/// <param name="subject"></param>
/// <param name="value"></param>
/// <returns></returns>
private bool TryGetCachedCertificate(RegistrationCache? registrationCache, string subject, out CachedCertificateResult? value) {
value = null;
if (registrationCache?.CachedCerts == null)
return false;
if (!registrationCache.CachedCerts.TryGetValue(subject, out var cache)) {
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;
}
/// <summary>
///
/// </summary>
/// <param name="hostsToRemove"></param>
public RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable<string> hostsToRemove) {
if (registrationCache?.CachedCerts != null)
foreach (var host in hostsToRemove)
registrationCache.CachedCerts.Remove(host);
return registrationCache;
}
private void UploadFiles(
ILogger logger,
SSHClientSettings sshSettings,
string workDir,
string fileName,
byte [] bytes,
byte[] bytes,
string owner,
string changeMode
) {
using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password);
sshService.Connect();
@ -318,6 +256,7 @@ namespace MaksIT.LetsEncryptConsole {
sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R");
}
//sshService.RunSudoCommand(sshSettings.Password, $"systemctl restart nginx");
}
}

View File

@ -1,30 +1,30 @@
using System.Runtime.InteropServices;
namespace MaksIT.LetsEncryptConsole {
public class Configuration {
namespace MaksIT.LetsEncryptConsole;
public class Configuration {
public LetsEncryptEnvironment[]? Environments { get; set; }
public Customer[]? Customers { get; set; }
}
}
public class OsWindows {
public class OsWindows {
public string? Path { get; set; }
}
}
public class OsLinux {
public class OsLinux {
public string? Path { get; set; }
public string? Owner { get; set; }
public string? ChangeMode { get; set; }
}
}
public class OsDependant {
public class OsDependant {
public OsWindows? Windows { get; set; }
public OsLinux? Linux { get; set; }
}
}
public class SSHClientSettings {
public class SSHClientSettings {
public bool Active { get; set; }
public string? Host { get; set; }
@ -34,7 +34,7 @@ namespace MaksIT.LetsEncryptConsole {
public string? Username { get; set; }
public string? Password { get; set; }
}
}
@ -69,4 +69,3 @@ namespace MaksIT.LetsEncryptConsole {
public string[]? Hosts { get; set; }
public string? Challenge { get; set; }
}
}

View File

@ -6,8 +6,9 @@ using Serilog;
using MaksIT.LetsEncryptConsole.Services;
using MaksIT.LetsEncrypt.Extensions;
namespace MaksIT.LetsEncryptConsole {
class Program {
namespace MaksIT.LetsEncryptConsole;
class Program {
private static readonly IConfiguration _configuration = InitConfig();
static void Main(string[] args) {
@ -41,8 +42,6 @@ namespace MaksIT.LetsEncryptConsole {
#region Services
services.RegisterLetsEncrypt();
services.AddSingleton<IKeyService, KeyService>();
services.AddSingleton<ITerminalService, TerminalService>();
#endregion
@ -66,5 +65,5 @@ namespace MaksIT.LetsEncryptConsole {
return configuration.Build();
}
}
}

View File

@ -1,151 +0,0 @@
using System.Security.Cryptography;
namespace MaksIT.LetsEncryptConsole.Services {
public interface IKeyService {
void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream);
void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream);
}
public class KeyService : IKeyService {
/// <summary>
/// Export a certificate to a PEM format string
/// </summary>
/// <param name="cert">The certificate to export</param>
/// <returns>A PEM encoded string</returns>
//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]);
}
}
}
}
}

View File

@ -1,12 +1,12 @@
using System.Diagnostics;
namespace MaksIT.LetsEncryptConsole.Services {
namespace MaksIT.LetsEncryptConsole.Services;
public interface ITerminalService {
public interface ITerminalService {
void Exec(string cmd);
}
}
public class TerminalService : ITerminalService {
public class TerminalService : ITerminalService {
public void Exec(string cmd) {
var escapedArgs = cmd.Replace("\"", "\\\"");
@ -25,6 +25,4 @@ namespace MaksIT.LetsEncryptConsole.Services {
pc.Start();
pc.WaitForExit();
}
}
}

View File

@ -1,10 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MaksIT.SSHProvider;
namespace SSHProvider {
public class Configuration {
}
public class Configuration {
}

View File

@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="DomainResult.Common" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="SSH.NET" Version="2020.0.2" />
</ItemGroup>

View File

@ -1,11 +1,14 @@
using DomainResults.Common;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using DomainResults.Common;
using Renci.SshNet;
using Renci.SshNet.Common;
using System.Text.RegularExpressions;
namespace SSHProvider {
namespace MaksIT.SSHProvider {
public interface ISSHService : IDisposable {
IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes);
@ -22,8 +25,6 @@ namespace SSHProvider {
public readonly SshClient _sshClient;
public readonly SftpClient _sftpClient;
public SSHService(
ILogger logger,
string host,
@ -31,11 +32,40 @@ namespace SSHProvider {
string username,
string password
) {
if(string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
throw new ArgumentNullException($"{nameof(username)} or {nameof(password)} is null, empty or white space");
_logger = logger;
_sshClient = new SshClient(host, port, username, password);
_sftpClient = new SftpClient(host, port, username, password);
}
public SSHService(
ILogger logger,
string host,
int port,
string username,
string [] privateKeys
) {
if (string.IsNullOrWhiteSpace(username) || privateKeys.Any(x => string.IsNullOrWhiteSpace(x)))
throw new ArgumentNullException($"{nameof(username)} or {nameof(privateKeys)} contains key which is null, empty or white space");
_logger = logger;
var privateKeyFiles = new List<PrivateKeyFile>();
foreach (var privateKey in privateKeys) {
using (var ms = new MemoryStream(Encoding.ASCII.GetBytes(privateKey))) {
privateKeyFiles.Add(new PrivateKeyFile(ms));
}
}
_sshClient = new SshClient(host, port, username, privateKeyFiles.ToArray());
_sftpClient = new SftpClient(host, port, username, privateKeyFiles.ToArray());
}
public IDomainResult Connect() {
try {
_sshClient.Connect();
@ -77,8 +107,8 @@ namespace SSHProvider {
_logger.LogInformation($"Listing directory:");
foreach (var fi in listDirectory) {
_logger.LogInformation($" - " + fi.Name);
foreach (var file in listDirectory) {
_logger.LogInformation($" - " + file.Name);
}
return IDomainResult.Success();

View File

@ -7,9 +7,9 @@ using Xunit;
//using PecMgr.VaultProvider;
//using PecMgr.Core.Abstractions;
namespace MaksIT.Tests.SSHProviderTests.Abstractions {
//[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public abstract class ConfigurationBase {
namespace MaksIT.Tests.SSHProviderTests.Abstractions;
//[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public abstract class ConfigurationBase {
protected IConfiguration Configuration;
@ -59,5 +59,4 @@ namespace MaksIT.Tests.SSHProviderTests.Abstractions {
return configurationBuilder.Build();
}
}
}

View File

@ -4,10 +4,10 @@ using Serilog;
using Microsoft.Extensions.Configuration;
using SSHProvider;
using MaksIT.SSHProvider;
namespace MaksIT.Tests.SSHProviderTests.Abstractions {
public abstract class ServicesBase : ConfigurationBase {
namespace MaksIT.Tests.SSHProviderTests.Abstractions;
public abstract class ServicesBase : ConfigurationBase {
public ServicesBase() : base() { }
@ -26,5 +26,4 @@ namespace MaksIT.Tests.SSHProviderTests.Abstractions {
#endregion
}
}
}

View File

@ -10,13 +10,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -3,12 +3,12 @@ using System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SSHProvider;
using MaksIT.SSHProvider;
using MaksIT.Tests.SSHProviderTests.Abstractions;
namespace SSHSerivceTests {
public class UnitTest1 : ServicesBase {
namespace MaksIT.SSHSerivceTests;
public class UnitTest1 : ServicesBase {
public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
@ -54,5 +54,4 @@ namespace SSHSerivceTests {
}
}
}
}
}