(feature): revoke init

This commit is contained in:
Maksym Sadovnychyy 2024-07-07 12:37:17 +02:00
parent 8aa535447e
commit 1119c01be0
12 changed files with 175 additions and 83 deletions

View File

@ -55,10 +55,6 @@ export default function Page() {
init.current = true init.current = true
}, []) }, [])
useEffect(() => {
console.log(editingAccount)
}, [editingAccount])
const handleAccountUpdate = (updatedAccount: CacheAccount) => { const handleAccountUpdate = (updatedAccount: CacheAccount) => {
setAccounts( setAccounts(
accounts.map((account) => accounts.map((account) =>
@ -70,15 +66,8 @@ export default function Page() {
} }
const deleteAccount = (accountId: string) => { const deleteAccount = (accountId: string) => {
httpService setAccounts(accounts.filter((account) => account.accountId !== accountId))
.delete(GetApiRoute(ApiRoutes.ACCOUNT_ID, accountId)) setEditingAccount(null)
.then((response) => {
if (response.isSuccess) {
setAccounts(
accounts.filter((account) => account.accountId !== accountId)
)
}
})
} }
return ( return (

View File

@ -255,7 +255,18 @@ const AccountEdit: React.FC<AccountEditProps> = (props) => {
} }
const handleDelete = (accountId: string) => { const handleDelete = (accountId: string) => {
httpService
.delete(GetApiRoute(ApiRoutes.ACCOUNT_ID, accountId))
.then((response) => {
if (response.isSuccess) {
onDelete?.(accountId) onDelete?.(accountId)
} else {
// Optionally, handle the error case, e.g., show an error message
dispatch(
showToast({ message: 'Failed to detele account.', type: 'error' })
)
}
})
} }
return ( return (

View File

@ -212,7 +212,8 @@ class HttpService {
): ProblemDetails { ): ProblemDetails {
return { return {
Title: title, Title: title,
Detail: detail instanceof Error ? detail.message : String(detail), Detail:
detail instanceof Error ? detail.message : JSON.parse(detail)?.detail,
Status: status Status: status
} }
} }
@ -252,6 +253,8 @@ class HttpService {
): Promise<HttpResponse<TResponse>> { ): Promise<HttpResponse<TResponse>> {
// Clean the data before sending the patch request // Clean the data before sending the patch request
const cleanedData = this.cleanObject(data) const cleanedData = this.cleanObject(data)
console.log('Cleaned patch data:', cleanedData)
return await this.request<TResponse>('PATCH', url, cleanedData) return await this.request<TResponse>('PATCH', url, cleanedData)
} }

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.LetsEncrypt.Entities.LetsEncrypt {
public enum RevokeReason {
Unspecified = 0,
KeyCompromise = 1,
CaCompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
PrivilegeWithdrawn = 6,
AaCompromise = 7
}
}

View File

@ -7,7 +7,7 @@ public class AcmeDirectory {
public Uri NewNonce { get; set; } public Uri NewNonce { get; set; }
public Uri NewOrder { get; set; } public Uri NewOrder { get; set; }
public Uri RenewalInfo { get; set; } public Uri RenewalInfo { get; set; }
public Uri RevokeCertificate { get; set; } public Uri RevokeCert { get; set; }
} }
public class AcmeDirectoryMeta { public class AcmeDirectoryMeta {

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.LetsEncrypt.Models.Responses {
public class RevokeRequest {
public string Certificate { get; set; } = string.Empty;
public int Reason { get; set; }
}
}

View File

@ -1,3 +1,9 @@
/**
* https://datatracker.ietf.org/doc/html/rfc8555
* https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-12
*/
using System.Text; using System.Text;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
@ -16,6 +22,7 @@ using MaksIT.LetsEncrypt.Models.Interfaces;
using MaksIT.LetsEncrypt.Models.Requests; using MaksIT.LetsEncrypt.Models.Requests;
using MaksIT.LetsEncrypt.Entities.Jws; using MaksIT.LetsEncrypt.Entities.Jws;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt; using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
using System.Net.Mime;
namespace MaksIT.LetsEncrypt.Services; namespace MaksIT.LetsEncrypt.Services;
@ -28,6 +35,7 @@ public interface ILetsEncryptService {
Task<IDomainResult> CompleteChallenges(Guid sessionId); Task<IDomainResult> CompleteChallenges(Guid sessionId);
Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames); Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames);
Task<IDomainResult> GetCertificate(Guid sessionId, string subject); Task<IDomainResult> GetCertificate(Guid sessionId, string subject);
Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason);
} }
public class LetsEncryptService : ILetsEncryptService { public class LetsEncryptService : ILetsEncryptService {
@ -278,7 +286,7 @@ public class LetsEncryptService : ILetsEncryptService {
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}..."); _logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
if (state.CurrentOrder?.Identifiers == null) { if (state.CurrentOrder?.Identifiers == null) {
return IDomainResult.Failed(); return IDomainResult.Failed("Current order identifiers are null");
} }
for (var index = 0; index < state.Challenges.Count; index++) { for (var index = 0; index < state.Challenges.Count; index++) {
@ -449,10 +457,61 @@ public class LetsEncryptService : ILetsEncryptService {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<IDomainResult> RevokeCertificate(Guid sessionId) { public async Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) {
throw new NotImplementedException(); try {
var state = GetOrCreateState(sessionId);
_logger.LogInformation($"Executing {nameof(RevokeCertificate)}...");
if (state.Cache == null || state.Cache.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache)) {
_logger.LogError("Certificate not found in cache");
return IDomainResult.Failed("Certificate not found");
} }
string Base64UrlEncode(byte[] input) {
return Convert.ToBase64String(input)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
// Load the certificate from PEM format and convert it to DER format
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certificateCache.Cert));
var derEncodedCert = certificate.Export(X509ContentType.Cert);
var base64UrlEncodedCert = Base64UrlEncode(derEncodedCert);
// Convert the certificate to DER format and Base64 encode it
var base64Cert = Convert.ToBase64String(certificate.Export(X509ContentType.Cert));
var revokeRequest = new RevokeRequest {
Certificate = certificateCache.Cert,
Reason = (int)reason
};
var (revokeResult, domainResult) = await SendAsync<object>(sessionId, HttpMethod.Post, state.Directory.RevokeCert, false, revokeRequest);
if (!domainResult.IsSuccess) {
return domainResult;
}
// Remove the certificate from the cache after successful revocation
state.Cache.CachedCerts.Remove(subject);
_logger.LogInformation("Certificate revoked successfully");
return IDomainResult.Success();
}
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
}
#region SendAsync #region SendAsync
/// <summary> /// <summary>
/// ///
@ -563,6 +622,7 @@ public class LetsEncryptService : ILetsEncryptService {
var jwsHeader = CreateJwsHeader(uri, state.Nonce); var jwsHeader = CreateJwsHeader(uri, state.Nonce);
var json = EncodeMessage(isPostAsGet, requestModel, state, jwsHeader); var json = EncodeMessage(isPostAsGet, requestModel, state, jwsHeader);
PrepareRequestContent(request, json, method); PrepareRequestContent(request, json, method);
} }
var response = await _httpClient.SendAsync(request); var response = await _httpClient.SendAsync(request);

View File

@ -47,8 +47,6 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
private async Task<IDomainResult> ProcessAccountAsync(RegistrationCache cache) { private async Task<IDomainResult> ProcessAccountAsync(RegistrationCache cache) {
var hostnames = cache.GetHostsWithUpcomingSslExpiry(); var hostnames = cache.GetHostsWithUpcomingSslExpiry();
if (hostnames == null) { if (hostnames == null) {
_logger.LogError("Unexpected hostnames null"); _logger.LogError("Unexpected hostnames null");
@ -61,7 +59,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
return IDomainResult.Success(); return IDomainResult.Success();
} }
var renewResult = await RenewCertificatesForHostnames(cache.AccountId, cache.Description, cache.Contacts, hostnames, cache.ChallengeType, cache.IsStaging); var (_, renewResult) = await _certsFlowService.FullFlow(cache.IsStaging, cache.AccountId, cache.Description, cache.Contacts, cache.ChallengeType, hostnames);
if (!renewResult.IsSuccess) if (!renewResult.IsSuccess)
return renewResult; return renewResult;
@ -70,53 +68,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
return IDomainResult.Success(); return IDomainResult.Success();
} }
private async Task<IDomainResult> RenewCertificatesForHostnames(Guid accountId, string description, string[] contacts, string[] hostnames, string challengeType, bool isStaging) {
var (sessionId, configureClientResult) = await _certsFlowService.ConfigureClientAsync(isStaging);
if (!configureClientResult.IsSuccess || sessionId == null) {
LogErrors(configureClientResult.Errors);
return configureClientResult;
}
var sessionIdValue = sessionId.Value;
var (_, initResult) = await _certsFlowService.InitAsync(sessionIdValue, accountId, description, contacts);
if (!initResult.IsSuccess) {
LogErrors(initResult.Errors);
return initResult;
}
var (_, newOrderResult) = await _certsFlowService.NewOrderAsync(sessionIdValue, hostnames, challengeType);
if (!newOrderResult.IsSuccess) {
LogErrors(newOrderResult.Errors);
return newOrderResult;
}
var challengeResult = await _certsFlowService.CompleteChallengesAsync(sessionIdValue);
if (!challengeResult.IsSuccess) {
LogErrors(challengeResult.Errors);
return challengeResult;
}
var getOrderResult = await _certsFlowService.GetOrderAsync(sessionIdValue, hostnames);
if (!getOrderResult.IsSuccess) {
LogErrors(getOrderResult.Errors);
return getOrderResult;
}
var certs = await _certsFlowService.GetCertificatesAsync(sessionIdValue, hostnames);
if (!certs.IsSuccess) {
LogErrors(certs.Errors);
return certs;
}
var (_, applyCertsResult) = await _certsFlowService.ApplyCertificatesAsync(sessionIdValue, hostnames);
if (!applyCertsResult.IsSuccess) {
LogErrors(applyCertsResult.Errors);
return applyCertsResult;
}
return IDomainResult.Success();
}
private void LogErrors(IEnumerable<string> errors) { private void LogErrors(IEnumerable<string> errors) {
foreach (var error in errors) { foreach (var error in errors) {

View File

@ -42,7 +42,7 @@ public class AccountController : ControllerBase {
return result.ToActionResult(); return result.ToActionResult();
} }
[HttpDelete("account/{accountd:guid}")] [HttpDelete("account/{accountId:guid}")]
public async Task<IActionResult> DeleteAccount(Guid accountId) { public async Task<IActionResult> DeleteAccount(Guid accountId) {
var result = await _accountService.DeleteAccountAsync(accountId); var result = await _accountService.DeleteAccountAsync(accountId);
return result.ToActionResult(); return result.ToActionResult();

View File

@ -133,10 +133,6 @@ public class AccountService : IAccountService {
cache.Contacts = contacts.ToArray(); cache.Contacts = contacts.ToArray();
} }
var hostnamesToAdd = new List<string>(); var hostnamesToAdd = new List<string>();
var hostnamesToRemove = new List<string>(); var hostnamesToRemove = new List<string>();
@ -174,13 +170,11 @@ public class AccountService : IAccountService {
} }
} }
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache); var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) { if (!saveResult.IsSuccess) {
return (null, saveResult); return (null, saveResult);
} }
if (hostnamesToAdd.Count > 0) { if (hostnamesToAdd.Count > 0) {
var (_, newCertsResult) = await _certsFlowService.FullFlow( var (_, newCertsResult) = await _certsFlowService.FullFlow(
cache.IsStaging, cache.IsStaging,
@ -195,13 +189,18 @@ public class AccountService : IAccountService {
return (null, newCertsResult); return (null, newCertsResult);
} }
if (hostnamesToRemove.Count > 0) { if (hostnamesToRemove.Count > 0) {
hostnamesToRemove.ForEach(hostname => { var revokeResult = await _certsFlowService.FullRevocationFlow(
cache.CachedCerts?.Remove(hostname); cache.IsStaging,
}); cache.AccountId,
} cache.Description,
cache.Contacts,
hostnamesToRemove.ToArray()
);
if (!revokeResult.IsSuccess)
return (null, revokeResult);
}
(cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId); (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) { if (!loadResult.IsSuccess || cache == null) {

View File

@ -6,6 +6,7 @@ using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Services; using MaksIT.LetsEncrypt.Services;
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests; using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
using System.Security.Cryptography; using System.Security.Cryptography;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
namespace MaksIT.LetsEncryptServer.Services; namespace MaksIT.LetsEncryptServer.Services;
@ -22,8 +23,11 @@ public interface ICertsInternalService : ICertsCommonService {
Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType); Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType);
Task<IDomainResult> GetOrderAsync(Guid sessionId, string[] hostnames); Task<IDomainResult> GetOrderAsync(Guid sessionId, string[] hostnames);
Task<IDomainResult> GetCertificatesAsync(Guid sessionId, string[] hostnames); Task<IDomainResult> GetCertificatesAsync(Guid sessionId, string[] hostnames);
Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, string[] hostnames);
Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames); Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames);
Task<(Guid?, IDomainResult)> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames); Task<(Guid?, IDomainResult)> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames);
Task<IDomainResult> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames);
} }
public interface ICertsRestService : ICertsCommonService { public interface ICertsRestService : ICertsCommonService {
@ -145,6 +149,7 @@ public class CertsFlowService : ICertsFlowService {
Thread.Sleep(1000); Thread.Sleep(1000);
} }
// TODO: Move to separate method
// Persist the cache // Persist the cache
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId); var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
if (!getCacheResult.IsSuccess || cache == null) if (!getCacheResult.IsSuccess || cache == null)
@ -161,6 +166,33 @@ public class CertsFlowService : ICertsFlowService {
return await _letsEncryptService.GetOrder(sessionId, hostnames); return await _letsEncryptService.GetOrder(sessionId, hostnames);
} }
public async Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, string[] hostnames) {
foreach (var hostname in hostnames) {
var result = await _letsEncryptService.RevokeCertificate(sessionId, hostname, RevokeReason.Unspecified);
if (!result.IsSuccess)
return result;
}
// TODO: Move to separate method
// Persist the cache
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
if (!getCacheResult.IsSuccess || cache == null)
return getCacheResult;
var saveResult = await _cacheService.SaveToCacheAsync(cache.AccountId, cache);
if (!saveResult.IsSuccess)
return saveResult;
return IDomainResult.Success();
}
public async Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) { public async Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) {
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId); var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
@ -221,6 +253,22 @@ public class CertsFlowService : ICertsFlowService {
return IDomainResult.Success(accountId); return IDomainResult.Success(accountId);
} }
public async Task<IDomainResult> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames) {
var (sessionId, configureClientResult) = await ConfigureClientAsync(isStaging);
if (!configureClientResult.IsSuccess || sessionId == null)
return configureClientResult;
var (_, initResult) = await InitAsync(sessionId.Value, accountId, description, contacts);
if (!initResult.IsSuccess)
return initResult;
var revokeResult = await RevokeCertificatesAsync(sessionId.Value, hostnames);
if (!revokeResult.IsSuccess)
return revokeResult;
return IDomainResult.Success();
}
#endregion #endregion

View File

@ -2,8 +2,8 @@
namespace MaksIT.Models.LetsEncryptServer.Account.Requests; namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
public class PatchHostnameRequest { public class PatchHostnameRequest {
public PatchAction<string> Hostname { get; set; } public PatchAction<string>? Hostname { get; set; }
public PatchAction<bool> IsDisabled { get; set; } public PatchAction<bool>? IsDisabled { get; set; }
} }