Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ac07f02
Add PKCS#7 types to respond to EST enrollment per RFC 7030
jjrdk Mar 30, 2026
e25c0ef
Make EstClient request PEM style response
jjrdk Mar 30, 2026
b517cd3
Make CaConfiguration use the IStoreCaProfiles interface instead of co…
jjrdk Mar 30, 2026
88487f3
Update dependencies
jjrdk Mar 30, 2026
2eb456a
Merge branch 'master' of github.com:jjrdk/opencertserver
jjrdk Apr 1, 2026
32349ee
Merge branch 'master' of github.com:jjrdk/opencertserver
jjrdk Apr 8, 2026
aef230d
Generate EST requirements to ensure full standard compliance.
jjrdk Apr 1, 2026
b2ff9a0
Update response type for EST endpoints
jjrdk Apr 1, 2026
ca0b3bf
Update trust anchor handling.
jjrdk Apr 2, 2026
f689e21
Implement client bootstrapping
jjrdk Apr 2, 2026
4937e66
Complete proof of possession handling.
jjrdk Apr 2, 2026
95a05bf
Fix conformance of enrollment semantics.
jjrdk Apr 2, 2026
8017fbf
Update server keygen conformance.
jjrdk Apr 2, 2026
dc79e89
Fix csrattrs conformance.
jjrdk Apr 2, 2026
b0614b3
Fix server keygen conformance
jjrdk Apr 2, 2026
5e5fb49
Implement support for key rollover publication
jjrdk Apr 2, 2026
15e7cb1
Implement CloseRolloverWindow when rolling over grace period ends.
jjrdk Apr 3, 2026
187b021
Create ACME non-conformance inventory
jjrdk Apr 6, 2026
4df01df
Resolve conformance to ACME error responses and add centralized error…
jjrdk Apr 6, 2026
3171365
Fix account lifecycle conformance
jjrdk Apr 6, 2026
364c01d
Update JWS request validation
jjrdk Apr 6, 2026
65a9e67
Implement order metadata and strict finalization validation.
jjrdk Apr 6, 2026
476b7d8
Implement complete authorization and challenge RFC semantics.
jjrdk Apr 6, 2026
523443a
Implement key rollover
jjrdk Apr 7, 2026
4c3d7c1
Implement certificate revocation and ensire all compliance tasks are …
jjrdk Apr 7, 2026
c557e8d
Clean up conformance descriptions
jjrdk Apr 7, 2026
1a07de9
Start OCSP conformance review
jjrdk Apr 7, 2026
3bf276d
Initial plan
Copilot Apr 7, 2026
1a28a8c
Start OCSP RFC 6960 conformance implementation
Copilot Apr 7, 2026
763c77a
Implement RFC 6960 OCSP conformance: ASN.1 fixes, signing, CertID, no…
Copilot Apr 7, 2026
4e55ee9
Finish implementing strict OCSP HTTP binding
jjrdk Apr 7, 2026
04d2c16
Update web dependencies
jjrdk Apr 7, 2026
f21bf78
Update README.md with OCSP implementation conformance summary.
jjrdk Apr 7, 2026
2b2240b
Code cleanup
jjrdk Apr 7, 2026
211c4aa
Fix OCSP GET base64url decoding to use Base64Url.DecodeFromChars for …
Copilot Apr 8, 2026
8f8da7b
Fix endpoint response encodings
jjrdk Apr 8, 2026
d6ad7a9
Code cleanup
jjrdk Apr 8, 2026
d7f1d25
Update dependencies
jjrdk Apr 8, 2026
d1483a3
Update web dependencies
jjrdk Apr 8, 2026
1357b26
Simplify certificate selection logic
jjrdk Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/CertesSlim/AcmeContext.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
using CertesSlim.Acme;
namespace CertesSlim;

using CertesSlim.Acme;
using CertesSlim.Acme.Resource;
using CertesSlim.Extensions;
using CertesSlim.Json;
using Microsoft.IdentityModel.Tokens;
using Directory = CertesSlim.Acme.Resource.Directory;
using Identifier = CertesSlim.Acme.Resource.Identifier;
using IdentifierType = CertesSlim.Acme.Resource.IdentifierType;

namespace CertesSlim;

using Identifier = Identifier;
using Resource_Directory = Directory;

/// <summary>
/// Represents the context for ACME operations.
/// </summary>
/// <seealso cref="IAcmeContext" />
public class AcmeContext : IAcmeContext
{
private const string DefaultKeyType = SecurityAlgorithms.EcdsaSha256;
private Resource_Directory? _directory;
private Directory? _directory;
private IAccountContext? _accountContext;

/// <summary>
Expand Down Expand Up @@ -146,11 +142,11 @@ public async Task<IAccountContext> NewAccount(
/// <returns>
/// The ACME directory.
/// </returns>
public async Task<Resource_Directory> GetDirectory()
public async Task<Directory> GetDirectory()
{
if (_directory == null)
{
var resp = await HttpClient.Get<Resource_Directory>(DirectoryUri).ConfigureAwait(false);
var resp = await HttpClient.Get<Directory>(DirectoryUri).ConfigureAwait(false);
_directory = resp.Resource;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace OpenCertServer.Acme.Abstractions.Exceptions;

/// <summary>
/// Exception thrown when an ACME client requests an existing account that does not exist.
/// </summary>
public sealed class AccountDoesNotExistException : AcmeException
{
/// <summary>
/// Initializes a new instance of the <see cref="AccountDoesNotExistException"/> class.
/// </summary>
public AccountDoesNotExistException()
: base("No account exists for the provided account key.")
{
}

/// <inheritdoc />
public override string ErrorType => "accountDoesNotExist";
}

19 changes: 19 additions & 0 deletions src/opencertserver.acme.abstractions/Exceptions/BadCsrException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace OpenCertServer.Acme.Abstractions.Exceptions;

/// <summary>
/// Exception thrown when an ACME finalization request contains a malformed or unacceptable CSR.
/// </summary>
public sealed class BadCsrException : AcmeException
{
/// <summary>
/// Initializes a new instance of the <see cref="BadCsrException"/> class.
/// </summary>
/// <param name="message">The human-readable detail for the CSR failure.</param>
public BadCsrException(string message)
: base(message)
{
}

/// <inheritdoc />
public override string ErrorType => "badCSR";
}
7 changes: 6 additions & 1 deletion src/opencertserver.acme.abstractions/HttpModel/AcmeError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public AcmeError(string type, string detail)
Detail = detail;
}

/// <summary>
/// Gets or sets the HTTP status code associated with the problem document.
/// </summary>
public int? Status { get; set; }

/// <summary>
/// Gets or sets the error type URN.
/// </summary>
Expand All @@ -53,7 +58,7 @@ public AcmeError(string type, string detail)
/// Gets or sets the error detail message.
/// </summary>
public string Detail { get; set; }

/// <summary>
/// Gets or sets the list of subproblem errors, if any.
/// </summary>
Expand Down
11 changes: 4 additions & 7 deletions src/opencertserver.acme.abstractions/HttpModel/Order.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,11 @@ public Order(

Authorizations = [..authorizationUrls];

switch (model.Status)
Finalize = finalizeUrl;

if (model.Status == OrderStatus.Valid)
{
case OrderStatus.Ready:
Finalize = finalizeUrl;
break;
case OrderStatus.Valid:
Certificate = certificateUrl;
break;
Certificate = certificateUrl;
}

if (model.Error != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace OpenCertServer.Acme.Abstractions.HttpModel.Requests;

using System.Collections.Generic;
using System.Text.Json;

/// <summary>
/// Represents a request to create or get an ACME account.
Expand All @@ -15,10 +16,15 @@ public sealed class CreateOrGetAccount
/// <summary>
/// Gets or sets a value indicating whether the terms of service have been agreed to.
/// </summary>
public bool TermsOfServiceAgreed { get; set; }
public bool? TermsOfServiceAgreed { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to only return an existing account.
/// </summary>
public bool OnlyReturnExisting { get; set; }

/// <summary>
/// Gets or sets the external account binding object when the server requires it.
/// </summary>
public JsonElement? ExternalAccountBinding { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.IdentityModel.Tokens;

namespace OpenCertServer.Acme.Abstractions.HttpModel.Requests;

/// <summary>
/// Represents the inner payload for an ACME account key rollover request.
/// </summary>
public sealed class KeyChangeRequest
{
/// <summary>
/// Gets or sets the account URL being updated.
/// </summary>
public Uri? Account { get; set; }

/// <summary>
/// Gets or sets the account's current (old) key.
/// </summary>
public JsonWebKey? OldKey { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace OpenCertServer.Acme.Abstractions.HttpModel.Requests;

using CertesSlim.Acme.Resource;

/// <summary>
/// Represents an ACME certificate revocation request.
/// </summary>
public sealed class RevokeCertificateRequest
{
/// <summary>
/// Gets or sets the certificate to revoke, encoded as base64url DER.
/// </summary>
public string? Certificate { get; set; }

/// <summary>
/// Gets or sets the optional revocation reason.
/// </summary>
public RevocationReason? Reason { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using CertesSlim.Acme.Resource;

namespace OpenCertServer.Acme.Abstractions.HttpModel.Requests;

using System.Collections.Generic;

/// <summary>
/// Represents a request to retrieve, update, or deactivate an ACME account.
/// </summary>
public sealed class UpdateAccountRequest
{
/// <summary>
/// Gets or sets the updated contact URIs for the account.
/// </summary>
public List<string>? Contact { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the terms of service have been agreed to.
/// </summary>
public bool? TermsOfServiceAgreed { get; set; }

/// <summary>
/// Gets or sets the requested account status.
/// </summary>
public AccountStatus? Status { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using CertesSlim.Acme.Resource;

namespace OpenCertServer.Acme.Abstractions.HttpModel.Requests;

/// <summary>
/// Represents a request to retrieve or update an ACME authorization resource.
/// </summary>
public sealed class UpdateAuthorizationRequest
{
/// <summary>
/// Gets or sets the requested authorization status.
/// </summary>
public AuthorizationStatus? Status { get; set; }
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace OpenCertServer.Acme.Abstractions.IssuanceServices;

using System;
using System.Threading;
using System.Threading.Tasks;
using Model;
Expand All @@ -15,11 +16,15 @@ public interface IIssueCertificates
/// <param name="profile">The certificate profile to use, or null for default.</param>
/// <param name="csr">The PEM or DER-encoded certificate signing request.</param>
/// <param name="identifiers">The identifiers to include in the certificate.</param>
/// <param name="notBefore">The requested not-before date/time, if accepted for the order.</param>
/// <param name="notAfter">The requested not-after date/time, if accepted for the order.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A tuple containing the issued certificate (as bytes) and an optional error.</returns>
Task<(byte[]? certificate, AcmeError? error)> IssueCertificate(
string? profile,
string csr,
IEnumerable<Identifier> identifiers,
DateTimeOffset? notBefore,
DateTimeOffset? notAfter,
CancellationToken cancellationToken);
}
36 changes: 35 additions & 1 deletion src/opencertserver.acme.abstractions/Model/Account.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public Account(JsonWebKey jwk, IEnumerable<string>? contacts, DateTimeOffset? to
/// <summary>
/// Gets the JSON Web Key associated with the account.
/// </summary>
public JsonWebKey Jwk { get; }
public JsonWebKey Jwk { get; private set; }

/// <summary>
/// Gets the list of contact URIs for the account.
Expand All @@ -52,6 +52,40 @@ public Account(JsonWebKey jwk, IEnumerable<string>? contacts, DateTimeOffset? to
/// </summary>
public DateTimeOffset? TosAccepted { get; private set; }

/// <summary>
/// Replaces the account contact URIs.
/// </summary>
/// <param name="contacts">The new contact URIs, or null to clear them.</param>
public void UpdateContacts(IEnumerable<string>? contacts)
{
Contacts = contacts?.ToList();
}

/// <summary>
/// Records acceptance of the current terms of service.
/// </summary>
public void AgreeToTermsOfService()
{
TosAccepted ??= DateTimeOffset.UtcNow;
}

/// <summary>
/// Deactivates the account.
/// </summary>
public void Deactivate()
{
Status = AccountStatus.Deactivated;
}

/// <summary>
/// Replaces the account key.
/// </summary>
/// <param name="jwk">The replacement JSON Web Key.</param>
public void ReplaceKey(JsonWebKey jwk)
{
Jwk = jwk ?? throw new ArgumentNullException(nameof(jwk));
}

/// <summary>
/// Gets or sets the concurrency token for optimistic concurrency control.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed class Authorization
{
{
AuthorizationStatus.Pending,
[AuthorizationStatus.Invalid, AuthorizationStatus.Expired, AuthorizationStatus.Valid]
[AuthorizationStatus.Invalid, AuthorizationStatus.Expired, AuthorizationStatus.Valid, AuthorizationStatus.Deactivated]
},
{
AuthorizationStatus.Valid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ public interface IRequestValidationService
/// <param name="request">The JWS payload of the request.</param>
/// <param name="header">The ACME request header.</param>
/// <param name="requestUrl">The URL to which the request was sent.</param>
/// <param name="requestContentType">The HTTP request content type.</param>
/// <param name="endpointName">The resolved endpoint name for the ACME route.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A task representing the asynchronous validation operation.</returns>
Task ValidateRequestAsync(
JwsPayload request,
AcmeHeader header,
string requestUrl,
string? requestContentType,
string? endpointName,
CancellationToken cancellationToken);
}
31 changes: 31 additions & 0 deletions src/opencertserver.acme.abstractions/Services/IAccountService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@ Task<Account> CreateAccount(
/// <returns>The account object, or null if not found.</returns>
Task<Account?> FindAccount(JsonWebKey jwk, CancellationToken cancellationToken = default);

/// <summary>
/// Updates an existing ACME account.
/// </summary>
/// <param name="account">The account to update.</param>
/// <param name="contact">The replacement contact URIs.</param>
/// <param name="termsOfServiceAgreed">Whether the account agrees to the terms of service.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The updated account object.</returns>
Task<Account> UpdateAccount(
Account account,
IEnumerable<string>? contact,
bool termsOfServiceAgreed,
CancellationToken cancellationToken = default);

/// <summary>
/// Deactivates an existing ACME account.
/// </summary>
/// <param name="account">The account to deactivate.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The deactivated account object.</returns>
Task<Account> DeactivateAccount(Account account, CancellationToken cancellationToken = default);

/// <summary>
/// Replaces the signing key for an existing ACME account.
/// </summary>
/// <param name="account">The account to update.</param>
/// <param name="newKey">The replacement account key.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The updated account object.</returns>
Task<Account> ChangeKey(Account account, JsonWebKey newKey, CancellationToken cancellationToken = default);

/// <summary>
/// Loads an account by its account ID.
/// </summary>
Expand Down
Loading