net/JetBrains.SignatureVerifier/src/Crypt/OcspVerifier.cs (190 lines of code) (raw):
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Oiw;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Ocsp;
using Org.BouncyCastle.X509;
namespace JetBrains.SignatureVerifier.Crypt
{
public class OcspVerifier
{
private static readonly string OCSP_REQUEST_TYPE = "application/ocsp-request";
private static readonly string OCSP_RESPONSE_TYPE = "application/ocsp-response";
private TimeSpan _ocspResponseTimeout;
private readonly ILogger _logger;
private TimeSpan ocspResponseCorrectSpan = TimeSpan.FromMinutes(1);
public OcspVerifier(TimeSpan ocspResponseTimeout, ILogger logger)
{
_ocspResponseTimeout = ocspResponseTimeout;
_logger = logger ?? NullLogger.Instance;
}
public async Task<VerifySignatureResult> CheckCertificateRevocationStatusAsync([NotNull] X509Certificate targetCert,
[NotNull] X509Certificate issuerCert)
{
if (targetCert == null) throw new ArgumentNullException(nameof(targetCert));
if (issuerCert == null) throw new ArgumentNullException(nameof(issuerCert));
var ocspUrl = targetCert.GetOcspUrl();
if (ocspUrl is null)
{
_logger.Warning($"The OCSP access data is empty in certificate {targetCert.FormatId()}");
_logger.Error(Messages.unable_determin_certificate_revocation_status);
return VerifySignatureResult.InvalidChain(Messages.unable_determin_certificate_revocation_status);
}
var ocspReqGenerator = new Org.BouncyCastle.Ocsp.OcspReqGenerator();
var certificateIdReq =
new CertificateID(OiwObjectIdentifiers.IdSha1.Id, issuerCert, targetCert.SerialNumber);
ocspReqGenerator.AddRequest(certificateIdReq);
var ocspReq = ocspReqGenerator.Generate();
var ocspRes = await getOcspResponceAsync(ocspUrl, ocspReq, _ocspResponseTimeout);
if (ocspRes.Status != OcspRespStatus.Successful)
{
_logger.Error($"OCSP response status: {ocspRes.Status}");
return VerifySignatureResult.InvalidChain(Messages.unable_determin_certificate_revocation_status);
}
var basicOcspResp = ocspRes.GetResponseObject() as BasicOcspResp;
if (basicOcspResp is null)
{
_logger.Error($"Unknown OCSP response type");
return VerifySignatureResult.InvalidChain(Messages.unable_determin_certificate_revocation_status);
}
if (!validateOcspResponse(basicOcspResp))
return VerifySignatureResult.InvalidChain(Messages.invalid_ocsp_response);
var singleResponses = basicOcspResp.Responses.Where(w => w.GetCertID().Equals(certificateIdReq)).ToList();
if (singleResponses.Count < 1)
{
_logger.Error("OCSP response not correspond to request");
return VerifySignatureResult.InvalidChain(Messages.invalid_ocsp_response);
}
foreach (var singleResp in singleResponses)
{
if (!validateSingleOcspResponse(singleResp))
return VerifySignatureResult.InvalidChain(Messages.invalid_ocsp_response);
var certStatus = singleResp.GetCertStatus();
//null is good
if (certStatus is null)
{
continue;
}
else if (certStatus is UnknownStatus)
{
_logger.Warning(Messages.unknown_certificate_revocation_status);
return VerifySignatureResult.InvalidChain(Messages.unknown_certificate_revocation_status);
}
else if (certStatus is RevokedStatus)
{
var certRevStatus = certStatus as RevokedStatus;
var msg = formatRevokedStatus(certRevStatus);
_logger.Warning(msg);
return VerifySignatureResult.InvalidChain(msg);
}
}
return VerifySignatureResult.Valid;
}
private async Task<OcspResp> getOcspResponceAsync(string ocspResponderUrl, OcspReq request, TimeSpan timeout)
{
if (!ocspResponderUrl.StartsWith("http"))
{
_logger.Error("Only http(s) is supported for OCSP calls");
return null;
}
_logger.Trace($"OCSP request: {ocspResponderUrl}");
try
{
byte[] array = request.GetEncoded();
using var httpClient = new HttpClient();
httpClient.Timeout = timeout;
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue(OCSP_RESPONSE_TYPE));
var content = new ByteArrayContent(array);
content.Headers.ContentType = new MediaTypeHeaderValue(OCSP_REQUEST_TYPE);
var response = await httpClient.PostAsync(ocspResponderUrl, content);
var responseStream = await response.Content.ReadAsStreamAsync();
return new OcspResp(responseStream);
}
catch (Exception ex)
{
var msg = $"Cannot get OCSP response for url: {ocspResponderUrl}";
_logger.Error(msg);
throw new Exception(msg, ex);
}
}
/// <summary>
/// Validate OCSP response with Acceptance Requirements RFC 6960 3.2
/// </summary>
private bool validateOcspResponse(BasicOcspResp ocspResp)
{
var issuerCert = getOcspIssuerCert(ocspResp);
if (issuerCert is null)
{
_logger.Error($"OCSP issuer certificate not found in response");
return false;
}
if (!issuerCert.CanSignOcspResponses())
{
_logger.Error($"OCSP issuer certificate is not applicable. RFC 6960 3.2");
return false;
}
if (!issuerCert.IsValidNow)
{
_logger.Error($"OCSP issuer certificate is not valid now. RFC 6960 3.2");
return false;
}
if (!ocspResp.Verify(issuerCert.GetPublicKey()))
{
_logger.Error($"OCSP with invalid signature! RFC 6960 3.2");
return false;
}
return true;
}
/// <summary>
/// Validate OCSP response with Acceptance Requirements RFC 6960 4.2.2
/// </summary>
private bool validateSingleOcspResponse(SingleResp singleResp)
{
DateTime nowInGmt = DateTime.Now.ToUniversalTime();
if (singleResp.NextUpdate is not null && singleResp.NextUpdate.Value < nowInGmt)
{
_logger.Error(
"OCSP response is no longer valid. NextUpdate: {singleResp.NextUpdate.Value}. RFC 6960 4.2.2.1.");
return false;
}
if (singleResp.ThisUpdate - nowInGmt > ocspResponseCorrectSpan)
{
_logger.Error(
$"OCSP response signature is from the future. Timestamp of thisUpdate field: {singleResp.ThisUpdate}. RFC 6960 4.2.2.1.");
return false;
}
return true;
}
private X509Certificate getOcspIssuerCert(BasicOcspResp ocspResp)
{
var certs = ocspResp.GetCerts()?.Cast<X509Certificate>().ToList();
if (certs is null || certs.Count < 1)
return null;
var responderId = ocspResp.ResponderId.ToAsn1Object();
if (responderId.Name is not null)
{
return certs.FirstOrDefault(f => f.SubjectDN.Equivalent(responderId.Name));
}
else
{
var keyHash = responderId.GetKeyHash();
if (keyHash is null)
return null;
return certs.FirstOrDefault(f =>
{
var ki = f.GetSubjectKeyIdentifierRaw();
return ki is not null && keyHash.SequenceEqual(ki);
});
}
}
private static string formatRevokedStatus(RevokedStatus revokedStatus)
{
var reason = "CrlReason: <none>";
if (revokedStatus.HasRevocationReason)
{
var crlReason = CrlReason.GetInstance(new DerEnumerated(revokedStatus.RevocationReason));
reason = crlReason.ToString();
}
return string.Format(Messages.certificate_revoked, revokedStatus.RevocationTime, reason);
}
}
}