diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index 5e15896..fcac937 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -8,7 +8,9 @@
-
+
+
+
diff --git a/src/Core/Extensions/ObjectExtensions.cs b/src/Core/Extensions/ObjectExtensions.cs
index d604210..0c5f2b1 100644
--- a/src/Core/Extensions/ObjectExtensions.cs
+++ b/src/Core/Extensions/ObjectExtensions.cs
@@ -1,37 +1,36 @@
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 {
- ///
- ///
- ///
- ///
- ///
- ///
- public static string ToJson(this T? obj) => obj.ToJson(null);
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string ToJson(this T? obj) => obj.ToJson(null);
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public static string ToJson(this T? obj, List? converters) {
- if (obj == null)
- return "{}";
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string ToJson(this T? obj, List? converters) {
+ if (obj == null)
+ return "{}";
- var options = new JsonSerializerOptions {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- WriteIndented = true
- };
+ var options = new JsonSerializerOptions {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = true
+ };
- converters?.ForEach(x => options.Converters.Add(x));
+ converters?.ForEach(x => options.Converters.Add(x));
- return JsonSerializer.Serialize(obj, options);
- }
+ return JsonSerializer.Serialize(obj, options);
}
}
diff --git a/src/Core/Extensions/StringExtensions.cs b/src/Core/Extensions/StringExtensions.cs
index a8d5126..fb178e3 100644
--- a/src/Core/Extensions/StringExtensions.cs
+++ b/src/Core/Extensions/StringExtensions.cs
@@ -6,35 +6,34 @@ using System.Text.Json.Serialization;
using System.Text.Json;
using System.Threading.Tasks;
-namespace MaksIT.Core.Extensions {
- public static class StringExtensions {
- ///
- /// Converts JSON string to object
- ///
- ///
- ///
- ///
- public static T? ToObject(this string? s) => ToObjectCore(s, null);
+namespace MaksIT.Core.Extensions;
+public static class StringExtensions {
+ ///
+ /// Converts JSON string to object
+ ///
+ ///
+ ///
+ ///
+ public static T? ToObject(this string? s) => ToObjectCore(s, null);
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public static T? ToObject(this string? s, List converters) => ToObjectCore(s, converters);
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static T? ToObject(this string? s, List converters) => ToObjectCore(s, converters);
- private static T? ToObjectCore(string? s, List? converters) {
- var options = new JsonSerializerOptions {
- PropertyNameCaseInsensitive = true
- };
+ private static T? ToObjectCore(string? s, List? converters) {
+ var options = new JsonSerializerOptions {
+ PropertyNameCaseInsensitive = true
+ };
- converters?.ForEach(x => options.Converters.Add(x));
+ converters?.ForEach(x => options.Converters.Add(x));
- return s != null
- ? JsonSerializer.Deserialize(s, options)
- : default;
- }
+ return s != null
+ ? JsonSerializer.Deserialize(s, options)
+ : default;
}
}
diff --git a/src/Core/OperatingSystem.cs b/src/Core/OperatingSystem.cs
index 162e2c1..fb45bec 100644
--- a/src/Core/OperatingSystem.cs
+++ b/src/Core/OperatingSystem.cs
@@ -1,14 +1,13 @@
using System.Runtime.InteropServices;
-namespace MaksIT.Core {
- public static class OperatingSystem {
- public static bool IsWindows() =>
- RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+namespace MaksIT.Core;
+public static class OperatingSystem {
+ public static bool IsWindows() =>
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- public static bool IsMacOS() =>
- RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+ public static bool IsMacOS() =>
+ RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
- public static bool IsLinux() =>
- RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
- }
+ public static bool IsLinux() =>
+ RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
}
diff --git a/src/LetsEncrypt/Entities/Jws/Jwk.cs b/src/LetsEncrypt/Entities/Jws/Jwk.cs
index 2b4d807..0907727 100644
--- a/src/LetsEncrypt/Entities/Jws/Jwk.cs
+++ b/src/LetsEncrypt/Entities/Jws/Jwk.cs
@@ -2,104 +2,103 @@
using System.Text.Json.Serialization;
-namespace MaksIT.LetsEncrypt.Entities.Jws {
- 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".
- ///
- ///
- [JsonPropertyName("kty")]
- public string? KeyType { get; set; }
+namespace MaksIT.LetsEncrypt.Entities.Jws;
+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".
+ ///
+ ///
+ [JsonPropertyName("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.
- ///
- ///
- [JsonPropertyName("kid")]
- public string? KeyId { 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.
+ ///
+ ///
+ [JsonPropertyName("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.
- ///
- ///
- [JsonPropertyName("use")]
- public string? Use { 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.
+ ///
+ ///
+ [JsonPropertyName("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.
- ///
- [JsonPropertyName("n")]
- public string? Modulus { get; set; }
+ ///
+ /// The the modulus value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
+ ///
+ [JsonPropertyName("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.
- ///
- [JsonPropertyName("e")]
- public string? Exponent { get; set; }
+ ///
+ /// The exponent value for the public RSA key. It is represented as the Base64URL encoding of value's big endian representation.
+ ///
+ [JsonPropertyName("e")]
+ public string? Exponent { get; set; }
- ///
- /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation.
- ///
- [JsonPropertyName("d")]
- public string? D { get; set; }
+ ///
+ /// The private exponent. It is represented as the Base64URL encoding of the value's big endian representation.
+ ///
+ [JsonPropertyName("d")]
+ public string? D { get; set; }
- ///
- /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
- ///
- [JsonPropertyName("p")]
- public string? P { get; set; }
+ ///
+ /// The first prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
+ ///
+ [JsonPropertyName("p")]
+ public string? P { get; set; }
- ///
- /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
- ///
- [JsonPropertyName("q")]
- public string? Q { get; set; }
+ ///
+ /// The second prime factor. It is represented as the Base64URL encoding of the value's big endian representation.
+ ///
+ [JsonPropertyName("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.
- ///
- [JsonPropertyName("dp")]
- public string? DP { get; set; }
+ ///
+ /// The first factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
+ ///
+ [JsonPropertyName("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.
- ///
- [JsonPropertyName("dq")]
- public string? DQ { get; set; }
+ ///
+ /// The second factor Chinese Remainder Theorem exponent. It is represented as the Base64URL encoding of the value's big endian representation.
+ ///
+ [JsonPropertyName("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.
- ///
- [JsonPropertyName("qi")]
- public string? InverseQ { get; set; }
+ ///
+ /// The first Chinese Remainder Theorem coefficient. It is represented as the Base64URL encoding of the value's big endian representation.
+ ///
+ [JsonPropertyName("qi")]
+ public string? InverseQ { get; set; }
- ///
- /// The other primes information, should they exist, null or an empty list if not specified.
- ///
- [JsonPropertyName("oth")]
- public string? OthInf { get; set; }
+ ///
+ /// The other primes information, should they exist, null or an empty list if not specified.
+ ///
+ [JsonPropertyName("oth")]
+ public string? OthInf { get; set; }
- ///
- /// "alg" (Algorithm) Parameter
- ///
- /// The "alg" (algorithm) parameter identifies the algorithm intended for
- /// use with the key.
- ///
- ///
- [JsonPropertyName("alg")]
- public string? Algorithm { get; set; }
- }
-}
\ No newline at end of file
+ ///
+ /// "alg" (Algorithm) Parameter
+ ///
+ /// The "alg" (algorithm) parameter identifies the algorithm intended for
+ /// use with the key.
+ ///
+ ///
+ [JsonPropertyName("alg")]
+ public string? Algorithm { get; set; }
+}
diff --git a/src/LetsEncrypt/Entities/Jws/JwsMessage.cs b/src/LetsEncrypt/Entities/Jws/JwsMessage.cs
index afd3d25..4726d5e 100644
--- a/src/LetsEncrypt/Entities/Jws/JwsMessage.cs
+++ b/src/LetsEncrypt/Entities/Jws/JwsMessage.cs
@@ -1,40 +1,36 @@
using System;
using System.Text.Json.Serialization;
-namespace MaksIT.LetsEncrypt.Entities.Jws
-{
-
- public class JwsMessage {
-
- public string? Protected { get; set; }
-
- public string? Payload { get; set; }
-
- public string? Signature { get; set; }
- }
+namespace MaksIT.LetsEncrypt.Entities.Jws;
- public class JwsHeader {
-
- [JsonPropertyName("alg")]
- public string? Algorithm { get; set; }
-
- [JsonPropertyName("jwk")]
- public Jwk? Key { get; set; }
-
-
- [JsonPropertyName("kid")]
- public string? KeyId { get; set; }
-
- public string? Nonce { get; set; }
-
- public Uri? Url { get; set; }
-
-
- [JsonPropertyName("Host")]
- public string? Host { get; set; }
- }
+public class JwsMessage {
+ public string? Protected { get; set; }
+ public string? Payload { get; set; }
+ public string? Signature { get; set; }
+}
+
+
+public class JwsHeader {
+
+ [JsonPropertyName("alg")]
+ public string? Algorithm { get; set; }
+
+ [JsonPropertyName("jwk")]
+ public Jwk? Key { get; set; }
+
+
+ [JsonPropertyName("kid")]
+ public string? KeyId { get; set; }
+
+ public string? Nonce { get; set; }
+
+ public Uri? Url { get; set; }
+
+
+ [JsonPropertyName("Host")]
+ public string? Host { get; set; }
}
diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs b/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs
index decc2ed..c937703 100644
--- a/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs
+++ b/src/LetsEncrypt/Entities/LetsEncrypt/AuthorizationChallange.cs
@@ -1,13 +1,12 @@
using System;
-namespace MaksIT.LetsEncrypt.Entities {
- public class AuthorizationChallenge {
- public Uri? Url { get; set; }
+namespace MaksIT.LetsEncrypt.Entities;
+public class AuthorizationChallenge {
+ public Uri? Url { get; set; }
- public string? Type { get; set; }
+ public string? Type { get; set; }
- public string? Status { get; set; }
+ public string? Status { get; set; }
- public string? Token { get; set; }
- }
+ public string? Token { get; set; }
}
diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs b/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs
index abb7436..5b8fbe2 100644
--- a/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs
+++ b/src/LetsEncrypt/Entities/LetsEncrypt/CachedCertificateResult.cs
@@ -1,11 +1,8 @@
using System.Security.Cryptography;
-namespace MaksIT.LetsEncrypt.Entities
-{
- public class CachedCertificateResult
- {
- public RSACryptoServiceProvider? PrivateKey { get; set; }
- public string? Certificate { get; set; }
- }
+namespace MaksIT.LetsEncrypt.Entities;
+public class CachedCertificateResult {
+ public RSACryptoServiceProvider? PrivateKey { get; set; }
+ public string? Certificate { get; set; }
}
diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs
index 62b348a..6aee7b4 100644
--- a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs
+++ b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs
@@ -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 {
- public string? Cert { get; set; }
- public byte[]? Private { get; set; }
+namespace MaksIT.LetsEncrypt.Entities;
+public class CertificateCache {
+ public string? Cert { get; set; }
+ public byte[]? Private { get; set; }
+}
+
+public class RegistrationCache {
+ public Dictionary? CachedCerts { get; set; }
+ public byte[]? AccountKey { get; set; }
+ public string? Id { get; set; }
+ public Jwk? Key { get; set; }
+ public Uri? Location { get; set; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
}
- public class RegistrationCache {
- public Dictionary? CachedCerts { get; set; }
- public byte[]? AccountKey { get; set; }
- public string? Id { get; set; }
- public Jwk? Key { get; set; }
- public Uri? Location { get; set; }
+ ///
+ ///
+ ///
+ ///
+ public void ResetCachedCertificate(IEnumerable hostsToRemove) {
+ if (CachedCerts != null)
+ foreach (var host in hostsToRemove)
+ CachedCerts.Remove(host);
}
}
diff --git a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs
index 615f312..78198d7 100644
--- a/src/LetsEncrypt/Exceptions/LetsEncrytException.cs
+++ b/src/LetsEncrypt/Exceptions/LetsEncrytException.cs
@@ -1,25 +1,23 @@
-using System;
-using System.Net.Http;
-
-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; }
+
+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 class Problem {
- public string Type { get; set; }
-
- public string Detail { get; set; }
-
- public string RawJson { get; set; }
- }
+ public HttpResponseMessage Response { get; }
}
+
+
+public class Problem {
+ public string Type { get; set; }
+
+ public string Detail { get; set; }
+
+ public string RawJson { get; set; }
+}
+
diff --git a/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs
index 9bcc721..99f3de4 100644
--- a/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs
+++ b/src/LetsEncrypt/Extensions/ServiceCollectionExtensions.cs
@@ -2,11 +2,10 @@
using MaksIT.LetsEncrypt.Services;
-namespace MaksIT.LetsEncrypt.Extensions {
- public static class ServiceCollectionExtensions {
- public static void RegisterLetsEncrypt(this IServiceCollection services) {
+namespace MaksIT.LetsEncrypt.Extensions;
+public static class ServiceCollectionExtensions {
+ public static void RegisterLetsEncrypt(this IServiceCollection services) {
- services.AddHttpClient();
- }
+ services.AddHttpClient();
}
}
diff --git a/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs b/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs
index 0fcaa1f..b06d292 100644
--- a/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs
+++ b/src/LetsEncrypt/Models/Interfaces/RequestModelWithLocationBase.cs
@@ -1,5 +1,4 @@
-namespace MaksIT.LetsEncrypt.Models.Interfaces {
- public interface IHasLocation {
- Uri? Location { get; set; }
- }
+namespace MaksIT.LetsEncrypt.Models.Interfaces;
+public interface IHasLocation {
+ Uri? Location { get; set; }
}
diff --git a/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs b/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs
index 6ffeb01..de09b97 100644
--- a/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs
+++ b/src/LetsEncrypt/Models/Requests/FinalizeRequest.cs
@@ -1,7 +1,5 @@
-namespace MaksIT.LetsEncrypt.Models.Requests
-{
- public class FinalizeRequest
- {
- public string? Csr { get; set; }
- }
+namespace MaksIT.LetsEncrypt.Models.Requests;
+
+public class FinalizeRequest {
+ public string? Csr { get; set; }
}
diff --git a/src/LetsEncrypt/Models/Responses/Account.cs b/src/LetsEncrypt/Models/Responses/Account.cs
index 8594c90..b00fae4 100644
--- a/src/LetsEncrypt/Models/Responses/Account.cs
+++ b/src/LetsEncrypt/Models/Responses/Account.cs
@@ -5,34 +5,33 @@ 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 bool TermsOfServiceAgreed { get; set; }
+public class Account : IHasLocation {
- /*
- 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
- */
- public bool OnlyReturnExisting { get; set; }
+ public bool TermsOfServiceAgreed { get; set; }
- public string[]? Contacts { 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
+ */
+ public bool OnlyReturnExisting { get; set; }
- public string? Status { get; set; }
+ public string[]? Contacts { get; set; }
- public string? Id { get; set; }
+ public string? Status { get; set; }
- public DateTime CreatedAt { get; set; }
+ public string? Id { get; set; }
- public Jwk? Key { get; set; }
+ public DateTime CreatedAt { get; set; }
- public string? InitialIp { get; set; }
+ public Jwk? Key { get; set; }
- public Uri? Orders { get; set; }
+ public string? InitialIp { get; set; }
- public Uri? Location { get; set; }
- }
+ public Uri? Orders { get; set; }
+
+ public Uri? Location { get; set; }
}
diff --git a/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs b/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs
index 065e063..fc35900 100644
--- a/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs
+++ b/src/LetsEncrypt/Models/Responses/AcmeDirectory.cs
@@ -1,29 +1,25 @@
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; }
+ public Uri NewAccount { get; set; }
- public Uri NewOrder { get; set; }
+ 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; }
- public Uri RevokeCertificate { 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; }
+ public Uri RevokeCertificate { get; set; }
- public Uri KeyChange { get; set; }
+ public Uri KeyChange { get; set; }
- public AcmeDirectoryMeta Meta { get; set; }
- }
-
- public class AcmeDirectoryMeta
- {
- public string TermsOfService { get; set; }
- }
+ public AcmeDirectoryMeta Meta { get; set; }
}
+
+public class AcmeDirectoryMeta {
+ public string TermsOfService { get; set; }
+}
\ No newline at end of file
diff --git a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs
index 211ae81..c6dba8b 100644
--- a/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs
+++ b/src/LetsEncrypt/Models/Responses/AuthorizationChallengeResponse.cs
@@ -1,20 +1,20 @@
using MaksIT.LetsEncrypt.Entities;
-namespace MaksIT.LetsEncrypt.Models.Responses {
- public class AuthorizationChallengeResponse {
- public OrderIdentifier? Identifier { get; set; }
+namespace MaksIT.LetsEncrypt.Models.Responses;
- public string? Status { get; set; }
+public class AuthorizationChallengeResponse {
+ public OrderIdentifier? Identifier { get; set; }
- public DateTime? Expires { get; set; }
+ public string? Status { get; set; }
- public bool Wildcard { get; set; }
+ public DateTime? Expires { get; set; }
- public AuthorizationChallenge[]? Challenges { get; set; }
- }
+ public bool Wildcard { get; set; }
- public class AuthorizeChallenge {
- public string? KeyAuthorization { get; set; }
- }
+ public AuthorizationChallenge[]? Challenges { get; set; }
+}
+
+public class AuthorizeChallenge {
+ public string? KeyAuthorization { get; set; }
}
diff --git a/src/LetsEncrypt/Models/Responses/Order.cs b/src/LetsEncrypt/Models/Responses/Order.cs
index 12d316a..ea2236a 100644
--- a/src/LetsEncrypt/Models/Responses/Order.cs
+++ b/src/LetsEncrypt/Models/Responses/Order.cs
@@ -1,34 +1,33 @@
using MaksIT.LetsEncrypt.Exceptions;
using MaksIT.LetsEncrypt.Models.Interfaces;
-namespace MaksIT.LetsEncrypt.Models.Responses {
+namespace MaksIT.LetsEncrypt.Models.Responses;
- public class OrderIdentifier {
- public string? Type { get; set; }
+public class OrderIdentifier {
+ public string? Type { get; set; }
- public string? Value { get; set; }
+ public string? Value { get; set; }
- }
-
- public class Order : IHasLocation {
- public Uri? Location { get; set; }
-
- public string? Status { get; set; }
-
- public DateTime? Expires { get; set; }
-
- public OrderIdentifier[]? Identifiers { get; set; }
-
- public DateTime? NotBefore { get; set; }
-
- public DateTime? NotAfter { get; set; }
-
- public Problem? Error { get; set; }
-
- public Uri[]? Authorizations { get; set; }
-
- public Uri? Finalize { get; set; }
-
- public Uri? Certificate { get; set; }
- }
+}
+
+public class Order : IHasLocation {
+ public Uri? Location { get; set; }
+
+ public string? Status { get; set; }
+
+ public DateTime? Expires { get; set; }
+
+ public OrderIdentifier[]? Identifiers { get; set; }
+
+ public DateTime? NotBefore { get; set; }
+
+ public DateTime? NotAfter { get; set; }
+
+ public Problem? Error { get; set; }
+
+ public Uri[]? Authorizations { get; set; }
+
+ public Uri? Finalize { get; set; }
+
+ public Uri? Certificate { get; set; }
}
diff --git a/src/LetsEncrypt/Services/JwsService.cs b/src/LetsEncrypt/Services/JwsService.cs
index b1d775c..2258666 100644
--- a/src/LetsEncrypt/Services/JwsService.cs
+++ b/src/LetsEncrypt/Services/JwsService.cs
@@ -11,102 +11,102 @@ using MaksIT.LetsEncrypt.Entities.Jws;
using MaksIT.Core.Extensions;
-namespace MaksIT.LetsEncrypt.Services {
- public interface IJwsService {
- void SetKeyId(string location);
+namespace MaksIT.LetsEncrypt.Services;
- JwsMessage Encode(JwsHeader protectedHeader);
+public interface IJwsService {
+ void SetKeyId(string location);
- JwsMessage Encode(TPayload payload, JwsHeader protectedHeader);
+ JwsMessage Encode(JwsHeader protectedHeader);
- string GetKeyAuthorization(string token);
+ JwsMessage Encode(TPayload payload, JwsHeader protectedHeader);
+
+ string GetKeyAuthorization(string token);
- string Base64UrlEncoded(string s);
+ string Base64UrlEncoded(string s);
- string Base64UrlEncoded(byte[] arg);
+ string Base64UrlEncoded(byte[] arg);
+}
+
+
+public class JwsService : IJwsService {
+
+ public Jwk _jwk;
+ private RSA _rsa;
+
+ public JwsService(RSA rsa) {
+ _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa));
+
+ var publicParameters = rsa.ExportParameters(false);
+
+ var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent));
+ var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus));
+
+ _jwk = new Jwk() {
+ KeyType = "RSA",
+ Exponent = Base64UrlEncoded(exp),
+ Modulus = Base64UrlEncoded(mod),
+ };
+ }
+
+ public void SetKeyId(string location) {
+ _jwk.KeyId = location;
+ }
+
+ public JwsMessage Encode(JwsHeader protectedHeader) =>
+ Encode(null, protectedHeader);
+
+ public JwsMessage Encode(T? payload, JwsHeader protectedHeader) {
+
+ protectedHeader.Algorithm = "RS256";
+ if (_jwk.KeyId != null) {
+ protectedHeader.KeyId = _jwk.KeyId;
+ }
+ else {
+ protectedHeader.Key = _jwk;
+ }
+
+ var message = new JwsMessage {
+ Payload = "",
+ Protected = Base64UrlEncoded(protectedHeader.ToJson())
+ };
+
+ if (payload != null) {
+ if (payload is string stringPayload)
+ message.Payload = Base64UrlEncoded(stringPayload);
+ else
+ message.Payload = Base64UrlEncoded(payload.ToJson());
+ }
+
+
+ message.Signature = Base64UrlEncoded(
+ _rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"),
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1));
+
+ return message;
+ }
+
+ public string GetKeyAuthorization(string token) =>
+ $"{token}.{GetSha256Thumbprint()}";
+
+ private string GetSha256Thumbprint() {
+ var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}";
+ return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json)));
}
- public class JwsService : IJwsService {
- public Jwk _jwk;
- private RSA _rsa;
+ public string Base64UrlEncoded(string s) =>
+ Base64UrlEncoded(Encoding.UTF8.GetBytes(s));
- public JwsService(RSA rsa) {
- _rsa = rsa ?? throw new ArgumentNullException(nameof(rsa));
-
- var publicParameters = rsa.ExportParameters(false);
-
- var exp = publicParameters.Exponent ?? throw new ArgumentNullException(nameof(publicParameters.Exponent));
- var mod = publicParameters.Modulus ?? throw new ArgumentNullException(nameof(publicParameters.Modulus));
-
- _jwk = new Jwk() {
- KeyType = "RSA",
- Exponent = Base64UrlEncoded(exp),
- Modulus = Base64UrlEncoded(mod),
- };
- }
-
- public void SetKeyId(string location) {
- _jwk.KeyId = location;
- }
-
- public JwsMessage Encode(JwsHeader protectedHeader) =>
- Encode(null, protectedHeader);
-
- public JwsMessage Encode(T? payload, JwsHeader protectedHeader) {
-
- protectedHeader.Algorithm = "RS256";
- if (_jwk.KeyId != null) {
- protectedHeader.KeyId = _jwk.KeyId;
- }
- else {
- protectedHeader.Key = _jwk;
- }
-
- var message = new JwsMessage {
- Payload = "",
- Protected = Base64UrlEncoded(protectedHeader.ToJson())
- };
-
- if (payload != null) {
- if (payload is string stringPayload)
- message.Payload = Base64UrlEncoded(stringPayload);
- else
- message.Payload = Base64UrlEncoded(payload.ToJson());
- }
-
-
- message.Signature = Base64UrlEncoded(
- _rsa.SignData(Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}"),
- HashAlgorithmName.SHA256,
- RSASignaturePadding.Pkcs1));
-
- return message;
- }
-
- public string GetKeyAuthorization(string token) =>
- $"{token}.{GetSha256Thumbprint()}";
-
- private string GetSha256Thumbprint() {
- var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}";
- return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json)));
- }
-
-
-
- public string Base64UrlEncoded(string s) =>
- Base64UrlEncoded(Encoding.UTF8.GetBytes(s));
-
- // https://tools.ietf.org/html/rfc4648#section-5
- public string Base64UrlEncoded(byte[] bytes) =>
- Convert.ToBase64String(bytes) // Regular base64 encoder
- .Split('=').First() // Remove any trailing '='s
- .Replace('+', '-') // 62nd char of encoding
- .Replace('/', '_'); // 63rd char of encoding
+ // https://tools.ietf.org/html/rfc4648#section-5
+ public string Base64UrlEncoded(byte[] bytes) =>
+ Convert.ToBase64String(bytes) // Regular base64 encoder
+ .Split('=').First() // Remove any trailing '='s
+ .Replace('+', '-') // 62nd char of encoding
+ .Replace('/', '_'); // 63rd char of encoding
- }
}
diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs
index 24c58e1..c409dcb 100644
--- a/src/LetsEncrypt/Services/LetsEncryptService.cs
+++ b/src/LetsEncrypt/Services/LetsEncryptService.cs
@@ -18,460 +18,457 @@ 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;
-namespace MaksIT.LetsEncrypt.Services {
+namespace MaksIT.LetsEncrypt.Services;
- public interface ILetsEncryptService {
+public interface ILetsEncryptService {
- Task ConfigureClient(string url);
+ Task ConfigureClient(string url);
- Task Init(string[] contacts, RegistrationCache? registrationCache);
+ Task Init(string[] contacts, RegistrationCache? registrationCache);
- string GetTermsOfServiceUri();
+ string GetTermsOfServiceUri();
- Task> NewOrder(string[] hostnames, string challengeType);
- Task CompleteChallenges();
- Task GetOrder(string[] hostnames);
- Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject);
+ Task> NewOrder(string[] hostnames, string challengeType);
+ Task CompleteChallenges();
+ Task GetOrder(string[] hostnames);
+ Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject);
- RegistrationCache? GetRegistrationCache();
+ RegistrationCache? GetRegistrationCache();
+}
+
+
+
+
+public class LetsEncryptService : ILetsEncryptService {
+
+ //private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
+ // NullValueHandling = NullValueHandling.Ignore,
+ // Formatting = Formatting.Indented
+ //};
+
+ private readonly ILogger _logger;
+
+ private HttpClient _httpClient;
+
+ private IJwsService _jwsService;
+ private AcmeDirectory? _directory;
+ private RegistrationCache? _cache;
+
+ private string? _nonce;
+
+ private List _challenges = new List();
+ private Order? _currentOrder;
+
+ public LetsEncryptService(
+ ILogger logger,
+ HttpClient httpClient
+ ) {
+ _logger = logger;
+ _httpClient = httpClient;
+ }
+
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task ConfigureClient(string url) {
+
+ _httpClient.BaseAddress ??= new Uri(url);
+
+ (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
+ }
+
+ ///
+ /// Account creation or Initialization from cache
+ ///
+ ///
+ ///
+ ///
+ public async Task Init(string? [] contacts, RegistrationCache? cache) {
+
+ if (contacts == null || contacts.Length == 0)
+ throw new ArgumentNullException();
+
+ if (_directory == null)
+ throw new ArgumentNullException();
+
+ var accountKey = new RSACryptoServiceProvider(4096);
+
+ if (cache != null && cache.AccountKey != null) {
+ _cache = cache;
+ accountKey.ImportCspBlob(cache.AccountKey);
+ }
+
+ // New Account request
+ _jwsService = new JwsService(accountKey);
+
+
+ var letsEncryptOrder = new Account {
+ TermsOfServiceAgreed = true,
+ Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
+ };
+
+ var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
+ _jwsService.SetKeyId(account.Location.ToString());
+
+ if (account.Status != "valid")
+ throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}");
+
+ _cache = new RegistrationCache {
+ Location = account.Location,
+ AccountKey = accountKey.ExportCspBlob(true),
+ Id = account.Id,
+ Key = account.Key
+ };
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public RegistrationCache? GetRegistrationCache() =>
+ _cache;
+
+ ///
+ /// Just retrive terms of service
+ ///
+ ///
+ ///
+ public string GetTermsOfServiceUri() {
+
+ if (_directory == null)
+ throw new NullReferenceException();
+
+ return _directory.Meta.TermsOfService;
}
+ ///
+ /// 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) {
+ _challenges.Clear();
- public class LetsEncryptService : ILetsEncryptService {
+ var letsEncryptOrder = new Order {
+ Expires = DateTime.UtcNow.AddDays(2),
+ Identifiers = hostnames.Select(hostname => new OrderIdentifier {
+ Type = "dns",
+ Value = hostname
+ }).ToArray()
+ };
- //private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
- // NullValueHandling = NullValueHandling.Ignore,
- // Formatting = Formatting.Indented
- //};
+ var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
- private readonly ILogger _logger;
-
- private HttpClient _httpClient;
+ if (order.Status == "ready")
+ return new Dictionary();
- private IJwsService _jwsService;
- private AcmeDirectory? _directory;
- private RegistrationCache? _cache;
-
- private string? _nonce;
-
- private List _challenges = new List();
- private Order? _currentOrder;
-
- public LetsEncryptService(
- ILogger logger,
- HttpClient httpClient
- ) {
- _logger = logger;
- _httpClient = httpClient;
- }
-
-
- ///
- ///
- ///
- ///
- ///
- ///
- public async Task ConfigureClient(string url) {
-
- _httpClient.BaseAddress ??= new Uri(url);
-
- (_directory, _) = await SendAsync(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
- }
-
- ///
- /// Account creation or Initialization from cache
- ///
- ///
- ///
- ///
- public async Task Init(string? [] contacts, RegistrationCache? cache) {
-
- if (contacts == null || contacts.Length == 0)
- throw new ArgumentNullException();
-
- if (_directory == null)
- throw new ArgumentNullException();
-
- var accountKey = new RSACryptoServiceProvider(4096);
-
- if (cache != null && cache.AccountKey != null) {
- _cache = cache;
- accountKey.ImportCspBlob(cache.AccountKey);
- }
-
- // New Account request
- _jwsService = new JwsService(accountKey);
-
-
- var letsEncryptOrder = new Account {
- TermsOfServiceAgreed = true,
- Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
- };
-
- var (account, response) = await SendAsync(HttpMethod.Post, _directory.NewAccount, false, letsEncryptOrder);
- _jwsService.SetKeyId(account.Location.ToString());
-
- if (account.Status != "valid")
- throw new InvalidOperationException($"Account status is not valid, was: {account.Status} \r\n {response}");
-
- _cache = new RegistrationCache {
- Location = account.Location,
- AccountKey = accountKey.ExportCspBlob(true),
- Id = account.Id,
- Key = account.Key
- };
- }
-
- ///
- ///
- ///
- ///
- public RegistrationCache? GetRegistrationCache() =>
- _cache;
-
- ///
- /// Just retrive terms of service
- ///
- ///
- ///
- public string GetTermsOfServiceUri() {
-
- if (_directory == null)
- throw new NullReferenceException();
-
- return _directory.Meta.TermsOfService;
- }
-
-
-
- ///
- /// 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) {
- _challenges.Clear();
-
- var letsEncryptOrder = new Order {
- Expires = DateTime.UtcNow.AddDays(2),
- Identifiers = hostnames.Select(hostname => new OrderIdentifier {
- Type = "dns",
- Value = hostname
- }).ToArray()
- };
-
- var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
-
- if (order.Status == "ready")
- return new Dictionary();
-
- if (order.Status != "pending")
- throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}");
+ if (order.Status != "pending")
+ throw new InvalidOperationException($"Created new order and expected status 'pending', but got: {order.Status} \r\n {response}");
- _currentOrder = order;
+ _currentOrder = order;
- var results = new Dictionary();
- foreach (var item in order.Authorizations) {
- var (challengeResponse, responseText) = await SendAsync(HttpMethod.Post, item, true, null);
+ var results = new Dictionary();
+ foreach (var item in order.Authorizations) {
+ var (challengeResponse, responseText) = await SendAsync(HttpMethod.Post, item, true, null);
- if (challengeResponse.Status == "valid")
- continue;
-
- if (challengeResponse.Status != "pending")
- throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {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] = keyToken;
- break;
- }
-
- default:
- throw new NotImplementedException();
- }
- }
-
- return results;
- }
-
- ///
- ///
- ///
- ///
- ///
- public async Task CompleteChallenges() {
-
- 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);
- //var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
- break;
- }
-
- case "http-01": {
- break;
- }
- }
-
- var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}");
-
- if (result.Status == "valid")
- break;
- if (result.Status != "pending")
- throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}");
-
- await Task.Delay(1000);
- }
- }
- }
-
- ///
- ///
- ///
- ///
- ///
- public async Task GetOrder(string[] hostnames) {
-
- var letsEncryptOrder = new Order {
- Expires = DateTime.UtcNow.AddDays(2),
- Identifiers = hostnames.Select(hostname => new OrderIdentifier {
- Type = "dns",
- Value = hostname
- }).ToArray()
- };
-
- var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
-
- _currentOrder = order;
- }
-
- ///
- ///
- ///
- ///
- /// Cert and Private key
- ///
- public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) {
-
- _logger.LogInformation($"Invoked: {nameof(GetCertificate)}");
-
-
- if (_currentOrder == null)
- throw new ArgumentNullException();
-
- 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 letsEncryptOrder = new FinalizeRequest {
- Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest())
- };
-
- Uri? certificateUrl = default;
-
-
- var start = DateTime.UtcNow;
-
- while (certificateUrl == null) {
- // https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
- await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
-
- if (_currentOrder.Status == "ready") {
- var (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
-
- if (response.Status == "processing")
- (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Location, true, null);
-
- if (response.Status == "valid") {
- certificateUrl = response.Certificate;
- }
- }
-
- if ((start - DateTime.UtcNow).Seconds > 120)
- throw new TimeoutException();
-
- await Task.Delay(1000);
+ if (challengeResponse.Status == "valid")
continue;
- // throw new InvalidOperationException(/*$"Invalid order status: "*/);
+ if (challengeResponse.Status != "pending")
+ throw new InvalidOperationException($"Expected autorization status 'pending', but got: {order.Status} \r\n {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] = keyToken;
+ break;
+ }
+
+ default:
+ throw new NotImplementedException();
}
-
- var (pem, _) = await SendAsync(HttpMethod.Post, certificateUrl, true, null);
-
- if (_cache == null)
- throw new NullReferenceException();
-
- _cache.CachedCerts ??= new Dictionary();
- _cache.CachedCerts[subject] = new CertificateCache {
- Cert = pem,
- Private = key.ExportCspBlob(true)
- };
-
- var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem));
-
- return (cert, key);
}
- ///
- ///
- ///
- ///
- ///
- public Task KeyChange() {
- throw new NotImplementedException();
- }
+ return results;
+ }
- ///
- ///
- ///
- ///
- ///
- public Task RevokeCertificate() {
- throw new NotImplementedException();
- }
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task CompleteChallenges() {
- ///
- /// Main method used to send data to LetsEncrypt
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- private async Task<(TResult, string)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class {
- var request = new HttpRequestMessage(method, uri);
+ for (var index = 0; index < _challenges.Count; index++) {
- _nonce = uri.OriginalString != "directory"
- ? await NewNonce()
- : default;
+ var challenge = _challenges[index];
- if (message != null || isPostAsGet) {
- var jwsHeader = new JwsHeader {
- Url = uri,
- };
+ while (true) {
+ AuthorizeChallenge authorizeChallenge = new AuthorizeChallenge();
- if (_nonce != null)
- jwsHeader.Nonce = _nonce;
+ switch (challenge.Type) {
+ case "dns-01": {
+ authorizeChallenge.KeyAuthorization = _jwsService.GetKeyAuthorization(challenge.Token);
+ //var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
+ break;
+ }
- var encodedMessage = isPostAsGet
- ? _jwsService.Encode(jwsHeader)
- : _jwsService.Encode(message, jwsHeader);
+ case "http-01": {
+ break;
+ }
+ }
- var json = encodedMessage.ToJson();
+ var (result, responseText) = await SendAsync(HttpMethod.Post, challenge.Url, false, "{}");
- request.Content = new StringContent(json);
+ if (result.Status == "valid")
+ break;
+ if (result.Status != "pending")
+ throw new InvalidOperationException($"Failed autorization of {_currentOrder.Identifiers[index].Value} \r\n {responseText}");
- 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);
+ await Task.Delay(1000);
}
-
- var response = await _httpClient.SendAsync(request);
-
- 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.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);
- }
-
- var responseContent = responseText.ToObject();
-
- if (responseContent is IHasLocation ihl) {
- if (response.Headers.Location != null)
- ihl.Location = response.Headers.Location;
- }
-
- return (responseContent, responseText);
- }
-
- ///
- /// Request New Nonce to be able to start POST requests
- ///
- ///
- ///
- private async Task 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();
}
}
-}
\ No newline at end of file
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task GetOrder(string[] hostnames) {
+
+ var letsEncryptOrder = new Order {
+ Expires = DateTime.UtcNow.AddDays(2),
+ Identifiers = hostnames.Select(hostname => new OrderIdentifier {
+ Type = "dns",
+ Value = hostname
+ }).ToArray()
+ };
+
+ var (order, response) = await SendAsync(HttpMethod.Post, _directory.NewOrder, false, letsEncryptOrder);
+
+ _currentOrder = order;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ /// Cert and Private key
+ ///
+ public async Task<(X509Certificate2 Cert, RSA PrivateKey)> GetCertificate(string subject) {
+
+ _logger.LogInformation($"Invoked: {nameof(GetCertificate)}");
+
+
+ if (_currentOrder == null)
+ throw new ArgumentNullException();
+
+ 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 letsEncryptOrder = new FinalizeRequest {
+ Csr = _jwsService.Base64UrlEncoded(csr.CreateSigningRequest())
+ };
+
+ Uri? certificateUrl = default;
+
+
+ var start = DateTime.UtcNow;
+
+ while (certificateUrl == null) {
+ // https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
+ await GetOrder(_currentOrder.Identifiers.Select(x => x.Value).ToArray());
+
+ if (_currentOrder.Status == "ready") {
+ var (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Finalize, false, letsEncryptOrder);
+
+ if (response.Status == "processing")
+ (response, responseText) = await SendAsync(HttpMethod.Post, _currentOrder.Location, true, null);
+
+ if (response.Status == "valid") {
+ certificateUrl = response.Certificate;
+ }
+ }
+
+ if ((start - DateTime.UtcNow).Seconds > 120)
+ throw new TimeoutException();
+
+ await Task.Delay(1000);
+ continue;
+
+ // throw new InvalidOperationException(/*$"Invalid order status: "*/);
+ }
+
+ var (pem, _) = await SendAsync(HttpMethod.Post, certificateUrl, true, null);
+
+ if (_cache == null)
+ throw new NullReferenceException();
+
+ _cache.CachedCerts ??= new Dictionary();
+ _cache.CachedCerts[subject] = new CertificateCache {
+ Cert = pem,
+ Private = key.ExportCspBlob(true)
+ };
+
+ var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem));
+
+ return (cert, key);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task KeyChange() {
+ throw new NotImplementedException();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task RevokeCertificate() {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Main method used to send data to LetsEncrypt
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task<(TResult, string)> SendAsync(HttpMethod method, Uri uri, bool isPostAsGet, object? message) where TResult : class {
+ var request = new HttpRequestMessage(method, uri);
+
+ _nonce = uri.OriginalString != "directory"
+ ? await NewNonce()
+ : default;
+
+ if (message != null || isPostAsGet) {
+ var jwsHeader = new JwsHeader {
+ Url = uri,
+ };
+
+ if (_nonce != null)
+ jwsHeader.Nonce = _nonce;
+
+ var encodedMessage = isPostAsGet
+ ? _jwsService.Encode(jwsHeader)
+ : _jwsService.Encode(message, jwsHeader);
+
+ var json = encodedMessage.ToJson();
+
+ 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 _httpClient.SendAsync(request);
+
+ 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.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);
+ }
+
+ var responseContent = responseText.ToObject();
+
+ if (responseContent is IHasLocation ihl) {
+ if (response.Headers.Location != null)
+ ihl.Location = response.Headers.Location;
+ }
+
+ return (responseContent, responseText);
+ }
+
+ ///
+ /// Request New Nonce to be able to start POST requests
+ ///
+ ///
+ ///
+ private async Task 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();
+ }
+}
diff --git a/src/LetsEncryptConsole/App.cs b/src/LetsEncryptConsole/App.cs
index a2a021e..0bf1031 100644
--- a/src/LetsEncryptConsole/App.cs
+++ b/src/LetsEncryptConsole/App.cs
@@ -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,314 +6,268 @@ 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;
- Task Run(string[] args);
+public interface IApp {
+
+ Task Run(string[] args);
+}
+
+public class App : IApp {
+
+ private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
+
+ private readonly ILogger _logger;
+ private readonly Configuration _appSettings;
+ private readonly ILetsEncryptService _letsEncryptService;
+ private readonly IKeyService _keyService;
+ private readonly ITerminalService _terminalService;
+
+ public App(
+ ILogger logger,
+ IOptions appSettings,
+ ILetsEncryptService letsEncryptService,
+ IKeyService keyService,
+ ITerminalService terminalService
+ ) {
+ _logger = logger;
+ _appSettings = appSettings.Value;
+ _letsEncryptService = letsEncryptService;
+ _keyService = keyService;
+ _terminalService = terminalService;
}
- public class App : IApp {
+ public async Task Run(string[] args) {
- private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
+ _logger.LogInformation("Letsencrypt client estarted...");
- private readonly ILogger _logger;
- private readonly Configuration _appSettings;
- private readonly ILetsEncryptService _letsEncryptService;
- private readonly IKeyService _keyService;
- private readonly ITerminalService _terminalService;
+ foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List()) {
+ try {
+ _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
- public App(
- ILogger logger,
- IOptions appSettings,
- ILetsEncryptService letsEncryptService,
- IKeyService keyService,
- ITerminalService terminalService
- ) {
- _logger = logger;
- _appSettings = appSettings.Value;
- _letsEncryptService = letsEncryptService;
- _keyService = keyService;
- _terminalService = terminalService;
- }
+ //loop all customers
+ foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List()) {
+ try {
+ _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
- public async Task Run(string[] args) {
+ //define cache folder
+ string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache");
+ if (!Directory.Exists(cachePath)) {
+ Directory.CreateDirectory(cachePath);
+ }
- _logger.LogInformation("Letsencrypt client estarted...");
+ //check acme directory
+ var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme");
+ if (!Directory.Exists(acmePath)) {
+ Directory.CreateDirectory(acmePath);
+ }
- foreach (var env in _appSettings.Environments?.Where(x => x.Active) ?? new List()) {
- try {
- _logger.LogInformation($"Let's Encrypt C# .Net Core Client, environment: {env.Name}");
+ //loop each customer website
+ foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) {
+ _logger.LogInformation($"Managing site: {site.Name}");
- //loop all customers
- foreach (Customer customer in _appSettings.Customers?.Where(x => x.Active) ?? new List()) {
- try {
- _logger.LogInformation($"Managing customer: {customer.Id} - {customer.Name} {customer.LastName}");
+ try {
+ //create folder for ssl
+ string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
+ if (!Directory.Exists(sslPath)) {
+ Directory.CreateDirectory(sslPath);
+ }
- //define cache folder
- string cachePath = Path.Combine(_appPath, customer.Id, env.Name, "cache");
- if (!Directory.Exists(cachePath)) {
- Directory.CreateDirectory(cachePath);
- }
+ var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
- //check acme directory
- var acmePath = Path.Combine(_appPath, customer.Id, env.Name, "acme");
- if (!Directory.Exists(acmePath)) {
- Directory.CreateDirectory(acmePath);
- }
+ //1. Client initialization
+ _logger.LogInformation("1. Client Initialization...");
- //loop each customer website
- foreach (Site site in customer.Sites?.Where(s => s.Active) ?? new List()) {
- _logger.LogInformation($"Managing site: {site.Name}");
+ #region LetsEncrypt client configuration
+ await _letsEncryptService.ConfigureClient(env.Url);
+ #endregion
- try {
- //create folder for ssl
- string sslPath = Path.Combine(_appPath, customer.Id, env.Name, "ssl", site.Name);
- if (!Directory.Exists(sslPath)) {
- Directory.CreateDirectory(sslPath);
- }
+ #region LetsEncrypt local registration cache initialization
+ var registrationCache = (File.Exists(cacheFile)
+ ? File.ReadAllText(cacheFile)
+ : null)
+ .ToObject();
- var cacheFile = Path.Combine(cachePath, $"{site.Name}.lets-encrypt.cache.json");
+ await _letsEncryptService.Init(customer.Contacts, registrationCache);
+ #endregion
- //1. Client initialization
- _logger.LogInformation("1. Client Initialization...");
+ #region LetsEncrypt terms of service
+ _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
+ #endregion
- #region LetsEncrypt client configuration
- await _letsEncryptService.ConfigureClient(env.Url);
- #endregion
+ // 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
+ var certRes = new CachedCertificateResult();
+ if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
+ string cert = Path.Combine(sslPath, $"{site.Name}.crt");
+ //if(!File.Exists(cert))
+ File.WriteAllText(cert, certRes.Certificate);
- #region LetsEncrypt local registration cache initialization
- var registrationCache = (File.Exists(cacheFile)
- ? File.ReadAllText(cacheFile)
- : null)
- .ToObject();
+ string key = Path.Combine(sslPath, $"{site.Name}.key");
+ //if(!File.Exists(key)) {
+ using (StreamWriter writer = File.CreateText(key))
+ _keyService.ExportPrivateKey(certRes.PrivateKey, writer);
+ //}
- await _letsEncryptService.Init(customer.Contacts, registrationCache);
- #endregion
+ _logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
+ }
+ else {
- #region LetsEncrypt terms of service
- _logger.LogInformation($"Terms of service: {_letsEncryptService.GetTermsOfServiceUri()}");
- #endregion
+ //try to make new order
+ try {
+ //create new orders
+ Console.WriteLine("2. Client New Order...");
- // 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
- 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);
+ #region LetsEncrypt new order
+ var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
+ #endregion
- string key = Path.Combine(sslPath, $"{site.Name}.key");
- //if(!File.Exists(key)) {
- using (StreamWriter writer = File.CreateText(key))
- _keyService.ExportPrivateKey(certRes.PrivateKey, writer);
- //}
+ if (orders.Count > 0) {
+ switch (site.Challenge) {
+ case "http-01": {
+ //ensure to enable static file discovery on server in .well-known/acme-challenge
+ //and listen on 80 port
- _logger.LogInformation("Certificate and Key exists and valid. Restored from cache.");
- }
- else {
+ foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles())
+ file.Delete();
- //try to make new order
- try {
- //create new orders
- Console.WriteLine("2. Client New Order...");
+ foreach (var result in orders) {
+ Console.WriteLine($"Key: {result.Key}, Value: {result.Value}");
+ string[] splitToken = result.Value.Split('.');
- #region LetsEncrypt new order
- var orders = await _letsEncryptService.NewOrder(site.Hosts, site.Challenge);
- #endregion
+ File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value);
+ }
- if (orders.Count > 0) {
- switch (site.Challenge) {
- case "http-01": {
- //ensure to enable static file discovery on server in .well-known/acme-challenge
- //and listen on 80 port
-
- foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles())
- file.Delete();
-
- foreach (var result in orders) {
- Console.WriteLine($"Key: {result.Key}, Value: {result.Value}");
- string[] splitToken = result.Value.Split('.');
-
- File.WriteAllText(Path.Combine(acmePath, splitToken[0]), result.Value);
+ foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) {
+ if (env?.SSH?.Active ?? false) {
+ UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode);
}
-
- foreach (FileInfo file in new DirectoryInfo(acmePath).GetFiles()) {
- if (env?.SSH?.Active ?? false) {
- UploadFiles(_logger, env.SSH, env.ACME.Linux.Path, file.Name, File.ReadAllBytes(file.FullName), env.ACME.Linux.Owner, env.ACME.Linux.ChangeMode);
- }
- else {
- throw new NotImplementedException();
- }
+ else {
+ throw new NotImplementedException();
}
-
-
-
-
-
- break;
}
- case "dns-01": {
- //Manage DNS server MX record, depends from provider
- throw new NotImplementedException();
- }
-
- default: {
- throw new NotImplementedException();
- }
- }
-
-
- #region LetsEncrypt complete challenges
- _logger.LogInformation("3. Client Complete Challange...");
- await _letsEncryptService.CompleteChallenges();
- _logger.LogInformation("Challanges comleted.");
- #endregion
-
- await Task.Delay(1000);
-
- // Download new certificate
- _logger.LogInformation("4. Download certificate...");
- var (cert, key) = await _letsEncryptService.GetCertificate(site.Name);
-
- #region Persist cache
- registrationCache = _letsEncryptService.GetRegistrationCache();
- File.WriteAllText(cacheFile, registrationCache.ToJson());
- #endregion
- }
-
- #region Save cert and key to filesystem
- certRes = new CachedCertificateResult();
- if (TryGetCachedCertificate(registrationCache, site.Name, out certRes)) {
-
- 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);
- }
-
- _logger.LogInformation("Certificate saved.");
-
- foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) {
-
- if (env?.SSH?.Active ?? false) {
- UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode);
+ break;
}
- else {
+
+ case "dns-01": {
+ //Manage DNS server MX record, depends from provider
throw new NotImplementedException();
}
- }
+ default: {
+ throw new NotImplementedException();
+ }
}
- else {
- _logger.LogError("Unable to get new cached certificate.");
- }
+
+
+ #region LetsEncrypt complete challenges
+ _logger.LogInformation("3. Client Complete Challange...");
+ await _letsEncryptService.CompleteChallenges();
+ _logger.LogInformation("Challanges comleted.");
+ #endregion
+
+ await Task.Delay(1000);
+
+ #region Download new certificate
+ _logger.LogInformation("4. Download certificate...");
+ var (cert, key) = await _letsEncryptService.GetCertificate(site.Name);
+ #endregion
+
+ #region Persist cache
+ registrationCache = _letsEncryptService.GetRegistrationCache();
+ File.WriteAllText(cacheFile, registrationCache.ToJson());
#endregion
}
- catch (Exception ex) {
- _logger.LogError(ex, "");
- await _letsEncryptService.GetOrder(site.Hosts);
- }
+ #region Save cert and key to filesystem
+ certRes = new CachedCertificateResult();
+ if (registrationCache.TryGetCachedCertificate(site.Name, out certRes)) {
+
+ 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);
+ }
+
+ _logger.LogInformation("Certificate saved.");
+
+ foreach (FileInfo file in new DirectoryInfo(sslPath).GetFiles()) {
+
+ if (env?.SSH?.Active ?? false) {
+ UploadFiles(_logger, env.SSH, $"{env.SSL.Linux.Path}/{site.Name}", file.Name, File.ReadAllBytes(file.FullName), env.SSL.Linux.Owner, env.SSL.Linux.ChangeMode);
+ }
+ else {
+ throw new NotImplementedException();
+ }
+ }
+
+ }
+ else {
+ _logger.LogError("Unable to get new cached certificate.");
+ }
+ #endregion
+ }
+ catch (Exception ex) {
+ _logger.LogError(ex, "");
+ await _letsEncryptService.GetOrder(site.Hosts);
}
-
-
-
- }
- catch (Exception ex) {
- _logger.LogError(ex, "Customer unhandled error");
}
+
+
+
+
+ }
+ 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, "Environment unhandled error");
}
}
- catch (Exception ex) {
- _logger.LogError(ex.Message.ToString());
- break;
+
+ if (env.Name == "ProductionV2") {
+ _terminalService.Exec("systemctl restart nginx");
}
}
- }
-
- ///
- ///
- ///
- ///
- ///
- ///
- 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;
+ catch (Exception ex) {
+ _logger.LogError(ex.Message.ToString());
+ break;
}
-
- 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 RegistrationCache? ResetCachedCertificate(RegistrationCache? registrationCache, IEnumerable 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,
- string owner,
- string changeMode
- ) {
-
- using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password);
- sshService.Connect();
-
- sshService.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}");
-
- sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
- sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R");
-
- sshService.Upload($"{workDir}", fileName, bytes);
-
- sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
- sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R");
}
}
+
+
+
+
+ private void UploadFiles(
+ ILogger logger,
+ SSHClientSettings sshSettings,
+ string workDir,
+ string fileName,
+ byte [] bytes,
+ string owner,
+ string changeMode
+ ) {
+
+ using var sshService = new SSHService(logger, sshSettings.Host, sshSettings.Port, sshSettings.Username, sshSettings.Password);
+ sshService.Connect();
+
+ sshService.RunSudoCommand(sshSettings.Password, $"mkdir {workDir}");
+
+ sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
+ sshService.RunSudoCommand(sshSettings.Password, $"chmod 777 {workDir} -R");
+
+ sshService.Upload($"{workDir}", fileName, bytes);
+
+ sshService.RunSudoCommand(sshSettings.Password, $"chown {owner} {workDir} -R");
+ sshService.RunSudoCommand(sshSettings.Password, $"chmod {changeMode} {workDir} -R");
+ }
}
diff --git a/src/LetsEncryptConsole/Configuration.cs b/src/LetsEncryptConsole/Configuration.cs
index afec942..58d0cb1 100644
--- a/src/LetsEncryptConsole/Configuration.cs
+++ b/src/LetsEncryptConsole/Configuration.cs
@@ -1,40 +1,40 @@
-using System.Runtime.InteropServices;
-namespace MaksIT.LetsEncryptConsole {
- public class Configuration {
- public LetsEncryptEnvironment[]? Environments { get; set; }
- public Customer[]? Customers { get; set; }
- }
+namespace MaksIT.LetsEncryptConsole;
- public class OsWindows {
- public string? Path { get; set; }
- }
+public class Configuration {
+ public LetsEncryptEnvironment[]? Environments { get; set; }
+ public Customer[]? Customers { get; set; }
+}
- public class OsLinux {
- public string? Path { get; set; }
+public class OsWindows {
+ public string? Path { get; set; }
+}
- public string? Owner { get; set; }
+public class OsLinux {
+ public string? Path { get; set; }
- public string? ChangeMode { get; set; }
+ public string? Owner { get; set; }
- }
+ public string? ChangeMode { get; set; }
- public class OsDependant {
- public OsWindows? Windows { get; set; }
- public OsLinux? Linux { get; set; }
- }
+}
- public class SSHClientSettings {
- public bool Active { get; set; }
+public class OsDependant {
+ public OsWindows? Windows { get; set; }
+ public OsLinux? Linux { get; set; }
+}
- public string? Host { get; set; }
+public class SSHClientSettings {
+ public bool Active { get; set; }
- public int Port { get; set; }
+ public string? Host { get; set; }
- public string? Username { get; set; }
+ public int Port { get; set; }
- public string? Password { get; set; }
- }
+ 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; }
}
-}
diff --git a/src/LetsEncryptConsole/Program.cs b/src/LetsEncryptConsole/Program.cs
index 2b205e2..e9d7efe 100644
--- a/src/LetsEncryptConsole/Program.cs
+++ b/src/LetsEncryptConsole/Program.cs
@@ -6,65 +6,66 @@ using Serilog;
using MaksIT.LetsEncryptConsole.Services;
using MaksIT.LetsEncrypt.Extensions;
-namespace MaksIT.LetsEncryptConsole {
- class Program {
- private static readonly IConfiguration _configuration = InitConfig();
+namespace MaksIT.LetsEncryptConsole;
- static void Main(string[] args) {
- // create service collection
- var services = new ServiceCollection();
- ConfigureServices(services);
+class Program {
+ private static readonly IConfiguration _configuration = InitConfig();
- // create service provider
- var serviceProvider = services.BuildServiceProvider();
+ static void Main(string[] args) {
+ // create service collection
+ var services = new ServiceCollection();
+ ConfigureServices(services);
- // entry to run app
+ // create service provider
+ var serviceProvider = services.BuildServiceProvider();
+
+ // entry to run app
#pragma warning disable CS8602 // Dereference of a possibly null reference.
- var app = serviceProvider.GetService();
- app.Run(args).Wait();
+ var app = serviceProvider.GetService();
+ app.Run(args).Wait();
#pragma warning restore CS8602 // Dereference of a possibly null reference.
- }
+ }
- public static void ConfigureServices(IServiceCollection services) {
+ public static void ConfigureServices(IServiceCollection services) {
- var configurationSection = _configuration.GetSection("Configuration");
- services.Configure(configurationSection);
- var appSettings = configurationSection.Get();
+ var configurationSection = _configuration.GetSection("Configuration");
+ services.Configure(configurationSection);
+ var appSettings = configurationSection.Get();
- #region Configure logging
- services.AddLogging(configure => {
- configure.AddSerilog(new LoggerConfiguration()
- .ReadFrom.Configuration(_configuration)
- .CreateLogger());
- });
- #endregion
+ #region Configure logging
+ services.AddLogging(configure => {
+ configure.AddSerilog(new LoggerConfiguration()
+ .ReadFrom.Configuration(_configuration)
+ .CreateLogger());
+ });
+ #endregion
- #region Services
- services.RegisterLetsEncrypt();
+ #region Services
+ services.RegisterLetsEncrypt();
- services.AddSingleton();
- services.AddSingleton();
- #endregion
+ services.AddSingleton();
+ services.AddSingleton();
+ #endregion
- // add app
- services.AddSingleton();
- }
+ // add app
+ services.AddSingleton();
+ }
- private static IConfiguration InitConfig() {
- var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
+ private static IConfiguration InitConfig() {
+ var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
- var configuration = new ConfigurationBuilder()
- .SetBasePath(Directory.GetCurrentDirectory())
- .AddEnvironmentVariables();
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddEnvironmentVariables();
- if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment)
- && new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists
- )
- configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
- else
- configuration.AddJsonFile($"appsettings.json", true, true);
+ if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment)
+ && new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), $"appsettings.{aspNetCoreEnvironment}.json")).Exists
+ )
+ configuration.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
+ else
+ configuration.AddJsonFile($"appsettings.json", true, true);
- return configuration.Build();
- }
+ return configuration.Build();
}
}
+
diff --git a/src/LetsEncryptConsole/Services/KeyService.cs b/src/LetsEncryptConsole/Services/KeyService.cs
index 26743fc..5718db1 100644
--- a/src/LetsEncryptConsole/Services/KeyService.cs
+++ b/src/LetsEncryptConsole/Services/KeyService.cs
@@ -1,151 +1,150 @@
using System.Security.Cryptography;
-namespace MaksIT.LetsEncryptConsole.Services {
+namespace MaksIT.LetsEncryptConsole.Services;
- public interface IKeyService {
- void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream);
- void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream);
+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 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();
+ 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);
+ }
- // builder.AppendLine("-----BEGIN CERTIFICATE-----");
- // builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
- // builder.AppendLine("-----END CERTIFICATE-----");
+ 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-----");
+ }
+ }
- // 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-----");
+ 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));
}
}
+ }
- 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 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++;
}
-
- 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));
- }
- }
+ if (value.Length - prefixZeros == 0) {
+ EncodeLength(stream, 1);
+ stream.Write((byte)0);
}
-
- 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);
+ 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 {
- 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]);
- }
+ 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/src/LetsEncryptConsole/Services/TerminalService.cs b/src/LetsEncryptConsole/Services/TerminalService.cs
index f65a119..7607531 100644
--- a/src/LetsEncryptConsole/Services/TerminalService.cs
+++ b/src/LetsEncryptConsole/Services/TerminalService.cs
@@ -1,30 +1,28 @@
using System.Diagnostics;
-namespace MaksIT.LetsEncryptConsole.Services {
+namespace MaksIT.LetsEncryptConsole.Services;
- public interface ITerminalService {
- void Exec(string cmd);
- }
+public interface ITerminalService {
+ void Exec(string cmd);
+}
- public class TerminalService : ITerminalService {
+public class TerminalService : ITerminalService {
- public void Exec(string cmd) {
- var escapedArgs = cmd.Replace("\"", "\\\"");
+ public void Exec(string cmd) {
+ var escapedArgs = cmd.Replace("\"", "\\\"");
- var pc = new Process {
- StartInfo = new ProcessStartInfo {
- RedirectStandardOutput = true,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- FileName = "/bin/bash",
- Arguments = $"-c \"{escapedArgs}\""
- }
- };
+ var pc = new Process {
+ StartInfo = new ProcessStartInfo {
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ FileName = "/bin/bash",
+ Arguments = $"-c \"{escapedArgs}\""
+ }
+ };
- pc.Start();
- pc.WaitForExit();
- }
+ pc.Start();
+ pc.WaitForExit();
}
-
-}
\ No newline at end of file
+}
diff --git a/src/SSHProvider/Configuration.cs b/src/SSHProvider/Configuration.cs
index e562e50..7f082ab 100644
--- a/src/SSHProvider/Configuration.cs
+++ b/src/SSHProvider/Configuration.cs
@@ -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 {
}
diff --git a/src/SSHProvider/SSHService.cs b/src/SSHProvider/SSHService.cs
index 89b61e0..90ef7e2 100644
--- a/src/SSHProvider/SSHService.cs
+++ b/src/SSHProvider/SSHService.cs
@@ -5,7 +5,7 @@ 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);
@@ -77,8 +77,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();
diff --git a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs
index b67b6ca..147f91e 100644
--- a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs
+++ b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs
@@ -7,57 +7,56 @@ 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;
+ protected IConfiguration Configuration;
- protected ServiceCollection ServiceCollection = new ServiceCollection();
+ protected ServiceCollection ServiceCollection = new ServiceCollection();
- protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); }
+ protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); }
- public ConfigurationBase() {
- Configuration = InitConfig();
- ConfigureServices(ServiceCollection);
- }
+ public ConfigurationBase() {
+ Configuration = InitConfig();
+ ConfigureServices(ServiceCollection);
+ }
- protected abstract void ConfigureServices(IServiceCollection services);
+ protected abstract void ConfigureServices(IServiceCollection services);
- private IConfiguration InitConfig() {
- var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
- var currentDirectory = Directory.GetCurrentDirectory();
+ private IConfiguration InitConfig() {
+ var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
+ var currentDirectory = Directory.GetCurrentDirectory();
- var configurationBuilder = new ConfigurationBuilder()
- .SetBasePath(Directory.GetCurrentDirectory())
- .AddEnvironmentVariables();
+ var configurationBuilder = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddEnvironmentVariables();
- if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists)
- configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
- else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists)
- configurationBuilder.AddJsonFile("appsettings.json", true, true);
- else
- throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}");
+ if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists)
+ configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
+ else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists)
+ configurationBuilder.AddJsonFile("appsettings.json", true, true);
+ else
+ throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}");
- //var builtConfig = configurationBuilder.Build();
- //var vaultOptions = builtConfig.GetSection("Vault");
+ //var builtConfig = configurationBuilder.Build();
+ //var vaultOptions = builtConfig.GetSection("Vault");
- //configurationBuilder.AddVault(options => {
- // options.Address = vaultOptions["Address"];
+ //configurationBuilder.AddVault(options => {
+ // options.Address = vaultOptions["Address"];
- // options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get>();
+ // options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get>();
- // options.AuthMethod = EnumerationStringId.FromValue(vaultOptions["AuthMethod"]);
- // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get();
- // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get();
+ // options.AuthMethod = EnumerationStringId.FromValue(vaultOptions["AuthMethod"]);
+ // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get();
+ // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get();
- // options.MountPath = vaultOptions["MountPath"];
- // options.SecretType = vaultOptions["SecretType"];
+ // options.MountPath = vaultOptions["MountPath"];
+ // options.SecretType = vaultOptions["SecretType"];
- // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get>();
- //});
+ // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get>();
+ //});
- return configurationBuilder.Build();
- }
+ return configurationBuilder.Build();
}
}
diff --git a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs
index 7d52b27..b608520 100644
--- a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs
+++ b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs
@@ -4,27 +4,26 @@ 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() { }
+ public ServicesBase() : base() { }
- protected override void ConfigureServices(IServiceCollection services) {
- // configure strongly typed settings objects
- var appSettingsSection = Configuration.GetSection("Configuration");
- services.Configure(appSettingsSection);
- var appSettings = appSettingsSection.Get();
+ protected override void ConfigureServices(IServiceCollection services) {
+ // configure strongly typed settings objects
+ var appSettingsSection = Configuration.GetSection("Configuration");
+ services.Configure(appSettingsSection);
+ var appSettings = appSettingsSection.Get();
- #region configurazione logging
- services.AddLogging(configure => {
- configure.AddSerilog(new LoggerConfiguration()
- //.ReadFrom.Configuration(_configuration)
- .CreateLogger());
- });
- #endregion
+ #region configurazione logging
+ services.AddLogging(configure => {
+ configure.AddSerilog(new LoggerConfiguration()
+ //.ReadFrom.Configuration(_configuration)
+ .CreateLogger());
+ });
+ #endregion
- }
}
}
\ No newline at end of file
diff --git a/src/Tests/SSHSerivceTests/UnitTest1.cs b/src/Tests/SSHSerivceTests/UnitTest1.cs
index 6d87802..b4cfa22 100644
--- a/src/Tests/SSHSerivceTests/UnitTest1.cs
+++ b/src/Tests/SSHSerivceTests/UnitTest1.cs
@@ -3,54 +3,53 @@ 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;
+ public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
- [Fact]
- public void UploadFile() {
+ [Fact]
+ public void UploadFile() {
- var username = "";
- var password = "";
- var filePath = Path.Combine(_appPath, "randomfile.txt");
- CreateRandomFile(filePath, 1);
+ var username = "";
+ var password = "";
+ var filePath = Path.Combine(_appPath, "randomfile.txt");
+ CreateRandomFile(filePath, 1);
- var logger = ServiceProvider.GetService>();
+ var logger = ServiceProvider.GetService>();
- using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password);
- sshService.Connect();
+ using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password);
+ sshService.Connect();
- var bytes = File.ReadAllBytes(filePath);
+ var bytes = File.ReadAllBytes(filePath);
- logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)");
+ logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)");
- sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
- sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R");
+ sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
+ sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R");
- sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes);
+ sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes);
- sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
- sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R");
- }
+ sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
+ sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R");
+ }
- private void CreateRandomFile(string filePath, int sizeInMb) {
- // Note: block size must be a factor of 1MB to avoid rounding errors
- const int blockSize = 1024 * 8;
- const int blocksPerMb = (1024 * 1024) / blockSize;
+ private void CreateRandomFile(string filePath, int sizeInMb) {
+ // Note: block size must be a factor of 1MB to avoid rounding errors
+ const int blockSize = 1024 * 8;
+ const int blocksPerMb = (1024 * 1024) / blockSize;
- byte[] data = new byte[blockSize];
+ byte[] data = new byte[blockSize];
- using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) {
- using (FileStream stream = File.OpenWrite(filePath)) {
- for (int i = 0; i < sizeInMb * blocksPerMb; i++) {
- crypto.GetBytes(data);
- stream.Write(data, 0, data.Length);
- }
+ using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) {
+ using (FileStream stream = File.OpenWrite(filePath)) {
+ for (int i = 0; i < sizeInMb * blocksPerMb; i++) {
+ crypto.GetBytes(data);
+ stream.Write(data, 0, data.Length);
}
}
}