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

View File

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

View File

@ -212,7 +212,8 @@ class HttpService {
): ProblemDetails {
return {
Title: title,
Detail: detail instanceof Error ? detail.message : String(detail),
Detail:
detail instanceof Error ? detail.message : JSON.parse(detail)?.detail,
Status: status
}
}
@ -252,6 +253,8 @@ class HttpService {
): Promise<HttpResponse<TResponse>> {
// Clean the data before sending the patch request
const cleanedData = this.cleanObject(data)
console.log('Cleaned patch data:', 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 NewOrder { get; set; }
public Uri RenewalInfo { get; set; }
public Uri RevokeCertificate { get; set; }
public Uri RevokeCert { get; set; }
}
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.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@ -16,6 +22,7 @@ using MaksIT.LetsEncrypt.Models.Interfaces;
using MaksIT.LetsEncrypt.Models.Requests;
using MaksIT.LetsEncrypt.Entities.Jws;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
using System.Net.Mime;
namespace MaksIT.LetsEncrypt.Services;
@ -28,6 +35,7 @@ public interface ILetsEncryptService {
Task<IDomainResult> CompleteChallenges(Guid sessionId);
Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames);
Task<IDomainResult> GetCertificate(Guid sessionId, string subject);
Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason);
}
public class LetsEncryptService : ILetsEncryptService {
@ -278,7 +286,7 @@ public class LetsEncryptService : ILetsEncryptService {
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
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++) {
@ -449,10 +457,61 @@ public class LetsEncryptService : ILetsEncryptService {
throw new NotImplementedException();
}
public Task<IDomainResult> RevokeCertificate(Guid sessionId) {
throw new NotImplementedException();
public async Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) {
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
/// <summary>
///
@ -563,6 +622,7 @@ public class LetsEncryptService : ILetsEncryptService {
var jwsHeader = CreateJwsHeader(uri, state.Nonce);
var json = EncodeMessage(isPostAsGet, requestModel, state, jwsHeader);
PrepareRequestContent(request, json, method);
}
var response = await _httpClient.SendAsync(request);

View File

@ -47,8 +47,6 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
private async Task<IDomainResult> ProcessAccountAsync(RegistrationCache cache) {
var hostnames = cache.GetHostsWithUpcomingSslExpiry();
if (hostnames == null) {
_logger.LogError("Unexpected hostnames null");
@ -61,7 +59,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
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)
return renewResult;
@ -70,53 +68,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
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) {
foreach (var error in errors) {

View File

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

View File

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

View File

@ -6,6 +6,7 @@ using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Services;
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
using System.Security.Cryptography;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
namespace MaksIT.LetsEncryptServer.Services;
@ -22,8 +23,11 @@ public interface ICertsInternalService : ICertsCommonService {
Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType);
Task<IDomainResult> GetOrderAsync(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<(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 {
@ -145,6 +149,7 @@ public class CertsFlowService : ICertsFlowService {
Thread.Sleep(1000);
}
// TODO: Move to separate method
// Persist the cache
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
if (!getCacheResult.IsSuccess || cache == null)
@ -161,6 +166,33 @@ public class CertsFlowService : ICertsFlowService {
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) {
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
@ -221,6 +253,22 @@ public class CertsFlowService : ICertsFlowService {
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

View File

@ -2,8 +2,8 @@
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
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; }
}