All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.henricook.tls.x509.ocsp.OCSP.scala Maven / Gradle / Ivy

The newest version!
package com.henricook.tls.x509.ocsp

import java.net.URL
import java.security.SecureRandom
import java.time.Instant
import java.util.Date

import com.henricook.tls.TLS
import com.henricook.tls.internal.BCConversions._
import com.henricook.tls.internal.TLSUtils
import com.henricook.tls.x509.X509Utils
import org.apache.commons.io.IOUtils
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers
import org.bouncycastle.asn1.x509.{CRLReason, ExtensionsGenerator, KeyUsage}
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.ocsp._
import org.bouncycastle.cert.ocsp.jcajce.JcaBasicOCSPRespBuilder
import org.bouncycastle.operator.DigestCalculator
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
import org.bouncycastle.util.encoders.Base64

/**
  * Online Certificate Status Protocol (OCSP) utility
  * @see [[https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol]]
  */
object OCSP {

  /**
    * OCSP certificate status
    * @param id Certificate ID
    * @param status Certificate status
    */
  case class Status(
      id: CertificateID,
      status: CertificateStatus = Status.good
  ) {
    def isRevoked: Boolean =
      status.ne(null) && status.isInstanceOf[RevokedStatus]
  }

  object Status {

    /**
      * OCSP good status
      * @return Good status
      */
    val good: CertificateStatus = CertificateStatus.GOOD

    /**
      * OCSP revoked status
      * @param reason Revocation reason
      * @param date Revocation date
      * @return Revoked status
      */
    def revoked(
        reason: Int = CRLReason.unspecified,
        date: Instant = Instant.now()
    ): CertificateStatus = {
      new RevokedStatus(Date.from(date), reason)
    }

    def wrap(responses: Array[SingleResp]): Seq[Status] = responses.map { resp ⇒
      Status(resp.getCertID, resp.getCertStatus)
    }
  }

  private val digestCalculator: DigestCalculator =
    new JcaDigestCalculatorProviderBuilder()
      .setProvider(TLSUtils.provider)
      .build()
      .get(CertificateID.HASH_SHA1)

  private val secureRandom = SecureRandom.getInstanceStrong

  /**
    * OCSP certificate ID
    * @param issuer Issuer certificate
    * @param serial Certificate serial number
    * @return OCSP certificate ID
    */
  def id(issuer: TLS.Certificate, serial: BigInt): CertificateID = {
    new CertificateID(
      digestCalculator,
      new X509CertificateHolder(issuer),
      serial.underlying()
    )
  }

  /**
    * Creates signed OCSP request
    * @param signer Signer credentials
    * @param ids Certificate IDs
    * @return Signed OCSP request
    */
  def signedRequest(
      signer: TLS.CertificateKey,
      ids: CertificateID*
  ): OCSPReq = {
    val builder =
      ids.foldLeft(new OCSPReqBuilder())((builder, id) ⇒ builder.addRequest(id))
    builder.setRequestorName(signer.certificate.getSubject)

    val extGen = new ExtensionsGenerator()
    val nonce: DEROctetString = {
      val bytes = new Array[Byte](16)
      secureRandom.nextBytes(bytes)
      new DEROctetString(bytes)
    }

    extGen.addExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, nonce)
    builder.setRequestExtensions(extGen.generate())

    builder.build(
      X509Utils.contentSigner(signer.key.getPrivate.toPrivateKey),
      signer.certificateChain.getCertificateList
        .map(new X509CertificateHolder(_))
    )
  }

  /**
    * Creates unsigned OCSP request
    * @param ids Certificate IDs
    * @return OCSP request
    */
  def request(ids: CertificateID*): OCSPReq = {
    val builder =
      ids.foldLeft(new OCSPReqBuilder())((builder, id) ⇒ builder.addRequest(id))
    builder.build()
  }

  /**
    * Basic OCSP response
    * @param signer Signer credentials
    * @param statuses Certificate statuses list
    * @return OCSP response
    */
  def response(signer: TLS.CertificateKey, statuses: Status*): BasicOCSPResp = {
    val builder = statuses.foldLeft[BasicOCSPRespBuilder](
      new JcaBasicOCSPRespBuilder(
        signer.key.getPublic.toPublicKey,
        digestCalculator
      )
    ) {
      case (b, Status(id, status)) ⇒
        b.addResponse(id, status)
    }
    builder.build(
      X509Utils.contentSigner(signer.key.getPrivate.toPrivateKey),
      signer.certificateChain.getCertificateList
        .map(new X509CertificateHolder(_)),
      new Date()
    )
  }

  private def loadUrl(url: String, request: OCSPReq): OCSPResp =
    concurrent.blocking {
      val encoded = Base64.toBase64String(request.getEncoded)
      val ocspUrl =
        new URL(if (url.endsWith("/")) url + encoded else url + "/" + encoded)
      val inputStream = ocspUrl.openStream()
      try {
        new OCSPResp(inputStream)
      } finally {
        IOUtils.closeQuietly(inputStream)
      }
    }

  /**
    * Ensures that OCSP response signature is valid
    * @param r OCSP response
    * @param issuer OCSP issuer
    * @return Is signature valid
    */
  def verify(r: BasicOCSPResp, issuer: TLS.Certificate): Boolean = {
    X509Utils.isKeyUsageAllowed(issuer, KeyUsage.cRLSign) &&
    r.isSignatureValid(X509Utils.contentVerifierProvider(issuer))
  }

  /**
    * Ensures that OCSP request signature is valid
    * @param r OCSP request
    * @param issuer OCSP issuer
    * @return Is signature valid
    */
  def verify(r: OCSPReq, issuer: TLS.Certificate): Boolean = {
    X509Utils.isKeyUsageAllowed(issuer, KeyUsage.digitalSignature) &&
    r.isSignatureValid(X509Utils.contentVerifierProvider(issuer))
  }

  /**
    * Tries to load OCSP response from URL
    * @param ocsp OCSP URL
    * @param issuer OCSP issuer
    * @param request OCSP request
    * @return OCSP response or [[scala.None None]]
    */
  def fromUrl(
      ocsp: String,
      issuer: TLS.Certificate,
      request: OCSPReq
  ): Option[BasicOCSPResp] = {
    val response = loadUrl(ocsp, request)
    assert(
      response.getStatus == OCSPResp.SUCCESSFUL,
      s"OCSP error: ${response.getStatus}"
    )
    response.getResponseObject match {
      case r: BasicOCSPResp if verify(r, issuer) ⇒
        Some(r)

      case _ ⇒
        None
    }
  }

  /**
    * Checks certificate online status
    * @param cert Certificate
    * @param issuer Issuer
    * @return Certificate OCSP status
    */
  def getStatus(
      cert: TLS.Certificate,
      issuer: TLS.Certificate
  ): Option[Status] = X509Utils.getOcspUrl(cert).flatMap { ocsp ⇒
    val certId = id(issuer, BigInt(cert.getSerialNumber.getValue))
    fromUrl(ocsp, issuer, request(certId)).toSeq
      .flatMap(_.getResponses)
      .find(_.getCertID == certId)
      .map(r ⇒ Status(r.getCertID, r.getCertStatus))
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy