Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/opencertserver.ca.server/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public IEndpointRouteBuilder MapCertificateAuthorityServer()
groupBuilder.MapGet("/{profileName}/crl", CrlHandler.HandleProfile)
.CacheOutput(cache => { cache.Expire(TimeSpan.FromHours(12)); }).AllowAnonymous();
groupBuilder.MapPost("/ocsp", OcspHandler.Handle).WithName("ocsp").AllowAnonymous();
groupBuilder.MapGet("/ocsp/{requestEncoded}", OcspHandler.HandleGet).WithName("ocspGet").AllowAnonymous();
groupBuilder.MapGet("/certificate", CertificateRetrievalHandler.HandleGet)
.AllowAnonymous();
return endpoints;
Expand Down
240 changes: 212 additions & 28 deletions src/opencertserver.ca.server/Handlers/OcspHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace OpenCertServer.Ca.Server.Handlers;

using System.Formats.Asn1;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using OpenCertServer.Ca.Utils;
Expand All @@ -13,52 +15,234 @@ public static class OcspHandler
public static async Task Handle(HttpContext context)
{
var cancellationToken = context.RequestAborted;
byte[] requestBytes;

if (HttpMethods.IsPost(context.Request.Method))
{
var buffer = new MemoryStream();
await context.Request.Body.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
requestBytes = buffer.ToArray();
}
else
{
context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
return;
}

var responseBytes = await ProcessRequestAsync(context, requestBytes, cancellationToken).ConfigureAwait(false);
var response = context.Response;
response.ContentType = "application/ocsp-response";
await response.Body.WriteAsync(responseBytes, cancellationToken).ConfigureAwait(false);
await response.CompleteAsync().ConfigureAwait(false);
}

public static async Task HandleGet(HttpContext context)
{
var cancellationToken = context.RequestAborted;
var encodedRequest = context.Request.RouteValues["requestEncoded"] as string;
if (string.IsNullOrWhiteSpace(encodedRequest))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}

byte[] requestBytes;
try
{
requestBytes = Convert.FromBase64String(encodedRequest.Replace('-', '+').Replace('_', '/'));
}
catch
{
var errorResponse = new OcspResponse(OcspResponseStatus.MalformedRequest);
var w = new AsnWriter(AsnEncodingRules.DER);
errorResponse.Encode(w);
context.Response.ContentType = "application/ocsp-response";
await context.Response.Body.WriteAsync(w.Encode(), cancellationToken).ConfigureAwait(false);
await context.Response.CompleteAsync().ConfigureAwait(false);
return;
}

var responseBytes = await ProcessRequestAsync(context, requestBytes, cancellationToken).ConfigureAwait(false);
var response = context.Response;
response.ContentType = "application/ocsp-response";
await response.Body.WriteAsync(responseBytes, cancellationToken).ConfigureAwait(false);
await response.CompleteAsync().ConfigureAwait(false);
}

private static async Task<byte[]> ProcessRequestAsync(
HttpContext context,
byte[] requestBytes,
CancellationToken cancellationToken)
{
var validators = context.RequestServices.GetServices<IValidateOcspRequest>();
var storeCertificates = context.RequestServices.GetRequiredService<IStoreCertificates>();
var responderId = context.RequestServices.GetRequiredService<IResponderId>();
var caProfiles = context.RequestServices.GetService<IStoreCaProfiles>();

OcspResponse ocspResponse;
try
{
var buffer = new MemoryStream();
await context.Request.Body.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
buffer.Seek(0, SeekOrigin.Begin);
var request = new OcspRequest(new AsnReader(buffer.ToArray(), AsnEncodingRules.DER));
var results = await Task.WhenAll(validators.Select(v => v.Validate(request))).ConfigureAwait(false);
var error = string.Join("\n", results.Where(x => !string.IsNullOrEmpty(x)));
if (!string.IsNullOrEmpty(error))
var request = new OcspRequest(new AsnReader(requestBytes, AsnEncodingRules.DER));

// Run registered validators; first non-null error status wins
var validationResults = await Task.WhenAll(validators.Select(v => v.Validate(request)))
.ConfigureAwait(false);
var errorStatus = validationResults.FirstOrDefault(r => r.HasValue);
if (errorStatus.HasValue)
{
return EncodeResponse(new OcspResponse(errorStatus.Value));
}

CaProfile? profile = null;
if (caProfiles != null)
{
profile = await caProfiles.GetProfile(null, cancellationToken).ConfigureAwait(false);
}

// Extract nonce from request extensions for echo
X509Extension? requestNonce = null;
if (request.TbsRequest.RequestExtensions != null)
{
foreach (X509Extension ext in request.TbsRequest.RequestExtensions)
{
if (ext.Oid?.Value == Oids.OcspNonce)
{
requestNonce = ext;
break;
}
}
}

var now = DateTimeOffset.UtcNow;
var nextUpdate = now.AddHours(1);

var searchResults = await Task.WhenAll(
request.TbsRequest.RequestList.Select(r =>
GetCertificateStatusWithCaValidation(r.CertIdentifier, storeCertificates, profile, cancellationToken)))
.ConfigureAwait(false);

// Build response extensions (include nonce echo if present)
X509ExtensionCollection? responseExtensions = null;
if (requestNonce != null)
{
ocspResponse = new OcspResponse(OcspResponseStatus.MalformedRequest);
responseExtensions = [requestNonce];
}

IResponderId responderId;
byte[] signature;
AlgorithmIdentifier signatureAlgorithm;
X509Certificate2[]? responderCerts = null;

if (profile != null)
{
var signingCert = profile.CertificateChain[0];
var signingKey = profile.PrivateKey;

using var sha1 = SHA1.Create();
var keyHash = sha1.ComputeHash(signingCert.GetPublicKey());
responderId = new ResponderIdByKey(keyHash);

var responseData = new ResponseData(
TypeVersion.V1,
responderId,
now,
searchResults.Select(r => new SingleResponse(r.Item1, (r.Item2, r.Item3), now, nextUpdate)),
responseExtensions);

(signature, signatureAlgorithm) = SignResponseData(responseData, signingKey);
responderCerts = [signingCert];
}
else
{
var searchResults = await Task.WhenAll(
request.TbsRequest.RequestList.Select(r =>
storeCertificates.GetCertificateStatus(r.CertIdentifier))).ConfigureAwait(false);
// Fallback: unsigned response using injected responder ID
responderId = context.RequestServices.GetRequiredService<IResponderId>();
var responseData = new ResponseData(
TypeVersion.V1,
responderId,
now,
searchResults.Select(r => new SingleResponse(r.Item1, (r.Item2, r.Item3), now, nextUpdate)),
responseExtensions);

signature = [];
signatureAlgorithm = new AlgorithmIdentifier(
Oids.EcPublicKey.InitializeOid(Oids.EcPublicKeyFriendlyName),
Oids.secp521r1.InitializeOid(Oids.secp521r1FriendlyName));

ocspResponse = new OcspResponse(
OcspResponseStatus.Successful,
new OcspBasicResponse(
new ResponseData(
TypeVersion.V1,
responderId,
DateTimeOffset.UtcNow,
searchResults.Select(r =>
new SingleResponse(r.Item1, (r.Item2, r.Item3), DateTimeOffset.UtcNow))),
new AlgorithmIdentifier(
Oids.EcPublicKey.InitializeOid(Oids.EcPublicKeyFriendlyName),
Oids.secp521r1.InitializeOid(Oids.secp521r1FriendlyName)), []));
new OcspBasicResponse(responseData, signatureAlgorithm, signature));
return EncodeResponse(ocspResponse);
}

var finalResponseData = new ResponseData(
TypeVersion.V1,
responderId,
now,
searchResults.Select(r => new SingleResponse(r.Item1, (r.Item2, r.Item3), now, nextUpdate)),
responseExtensions);

ocspResponse = new OcspResponse(
OcspResponseStatus.Successful,
new OcspBasicResponse(finalResponseData, signatureAlgorithm, signature, responderCerts));
}
catch (Exception)
{
ocspResponse = new OcspResponse(OcspResponseStatus.InternalError);
}

return EncodeResponse(ocspResponse);
}

private static async Task<(CertId, CertificateStatus, RevokedInfo?)> GetCertificateStatusWithCaValidation(
CertId certId,
IStoreCertificates store,
CaProfile? profile,
CancellationToken cancellationToken)
{
// If we have a CA profile, validate the CertID issuer hashes against the CA cert
if (profile != null)
{
var caCert = profile.CertificateChain[0];
using var hasher = certId.Algorithm.AlgorithmOid.Value!.GetHashAlgorithmForCertId();
if (hasher != null)
{
var expectedNameHash = hasher.ComputeHash(caCert.SubjectName.RawData);
var expectedKeyHash = hasher.ComputeHash(caCert.GetPublicKey());

if (!expectedNameHash.AsSpan().SequenceEqual(certId.IssuerNameHash) ||
!expectedKeyHash.AsSpan().SequenceEqual(certId.IssuerKeyHash))
{
return (certId, CertificateStatus.Unknown, null);
}
}
}

return await store.GetCertificateStatus(certId, cancellationToken).ConfigureAwait(false);
}

private static (byte[] Signature, AlgorithmIdentifier Algorithm) SignResponseData(
ResponseData responseData,
AsymmetricAlgorithm signingKey)
{
var dataWriter = new AsnWriter(AsnEncodingRules.DER);
responseData.Encode(dataWriter);
var dataToSign = dataWriter.Encode();

return signingKey switch
{
RSA rsa => (
rsa.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1),
new AlgorithmIdentifier(Oids.RsaPkcs1Sha256.InitializeOid(Oids.RsaPkcs1Sha256FriendlyName))),
ECDsa ecdsa => (
ecdsa.SignData(dataToSign, HashAlgorithmName.SHA256),
new AlgorithmIdentifier(Oids.ECDsaWithSha256.InitializeOid(Oids.ECDsaWithSha256FriendlyName))),
_ => throw new InvalidOperationException("Unsupported signing key type")
};
}

private static byte[] EncodeResponse(OcspResponse response)
{
var writer = new AsnWriter(AsnEncodingRules.DER);
ocspResponse.Encode(writer);
var errorBytes = writer.Encode();
var response = context.Response;
response.ContentType = "application/ocsp-response";
await response.Body.WriteAsync(errorBytes, cancellationToken).ConfigureAwait(false);
await response.CompleteAsync().ConfigureAwait(false);
response.Encode(writer);
return writer.Encode();
}
}
16 changes: 16 additions & 0 deletions src/opencertserver.ca.utils/EncodingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ public HashAlgorithm GetHashAlgorithmFromOid()
};
}

/// <summary>
/// Returns a <see cref="HashAlgorithm"/> for CertID hash OIDs used in OCSP (SHA family).
/// Returns null for unrecognised OIDs rather than throwing, so callers can fall through.
/// </summary>
public HashAlgorithm? GetHashAlgorithmForCertId()
{
return value switch
{
Oids.Sha1 => SHA1.Create(),
Oids.Sha256 => SHA256.Create(),
Oids.Sha384 => SHA384.Create(),
Oids.Sha512 => SHA512.Create(),
_ => null
};
}

/// <summary>
/// Executes the InitializeOid operation.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions src/opencertserver.ca.utils/Ocsp/CertId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ public static CertId Create(X509Certificate2 certificate, HashAlgorithmName hash
certificate.SerialNumberBytes.ToArray());
}

/// <summary>
/// Creates a <see cref="CertId"/> using the correct issuer certificate to compute issuer name hash and
/// issuer key hash per RFC 6960.
/// </summary>
public static CertId Create(X509Certificate2 certificate, X509Certificate2 issuerCertificate, HashAlgorithmName hashAlgorithm)
{
var hasher = hashAlgorithm.CreateHashAlgorithm();
return new CertId(
new AlgorithmIdentifier(hashAlgorithm.GetHashAlgorithmOid()),
hasher.ComputeHash(issuerCertificate.SubjectName.RawData),
hasher.ComputeHash(issuerCertificate.GetPublicKey()),
certificate.SerialNumberBytes.ToArray());
}

/// <summary>
/// Gets the hash algorithm identifier used in this certificate identifier.
/// </summary>
Expand Down
6 changes: 5 additions & 1 deletion src/opencertserver.ca.utils/Ocsp/IValidateOcspRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ namespace OpenCertServer.Ca.Utils.Ocsp;
/// </summary>
public interface IValidateOcspRequest
{
Task<string?> Validate(OcspRequest request);
/// <summary>
/// Validates the OCSP request. Returns null when the request is valid, or the appropriate
/// <see cref="OcspResponseStatus"/> error code when the request must be rejected.
/// </summary>
Task<OcspResponseStatus?> Validate(OcspRequest request);
}
Loading
Loading