Permalink
Browse files

Refactoring Key store API.

  • Loading branch information...
atooni committed Jan 24, 2019
1 parent 22c7a1b commit 9d795edce7a0aa64a439a9f6ebdb06cd4757ce90
Showing with 245 additions and 218 deletions.
  1. +3 −3 blended.security.scep.standalone/src/main/scala/blended/security/scep/standalone/CertRefresher.scala
  2. +1 −1 blended.security.scep/src/main/scala/blended/security/scep/internal/ScepCertificateProvider.scala
  3. +8 −2 blended.security.ssl/src/main/scala/blended/security/ssl/CertificateHolder.scala
  4. +2 −2 blended.security.ssl/src/main/scala/blended/security/ssl/CertificateManager.scala
  5. +9 −0 blended.security.ssl/src/main/scala/blended/security/ssl/CertificateProvider.scala
  6. +52 −0 blended.security.ssl/src/main/scala/blended/security/ssl/internal/CertificateChecker.scala
  7. +3 −3 blended.security.ssl/src/main/scala/blended/security/ssl/internal/CertificateManagerConfig.scala
  8. +36 −81 blended.security.ssl/src/main/scala/blended/security/ssl/internal/CertificateManagerImpl.scala
  9. +3 −5 blended.security.ssl/src/main/scala/blended/security/ssl/internal/CertificateRefresher.scala
  10. +91 −18 blended.security.ssl/src/main/scala/blended/security/ssl/internal/JavaKeystore.scala
  11. +14 −0 blended.security.ssl/src/main/scala/blended/security/ssl/internal/MemoryKeystore.scala
  12. +0 −6 blended.security.ssl/src/main/scala/blended/security/ssl/internal/ServerKeyStore.scala
  13. BIN blended.security.ssl/src/test/resources/cacerts
  14. +23 −23 blended.security.ssl/src/test/scala/blended/security/ssl/internal/CertificateRefresherSpec.scala
  15. +0 −74 blended.security.ssl/src/test/scala/blended/security/ssl/internal/QuickSSLTests.scala
@@ -10,7 +10,7 @@ import scala.concurrent.Promise
import blended.container.context.api.ContainerIdentifierService
import blended.container.context.impl.internal.ContainerIdentifierServiceImpl
import blended.security.ssl.CertificateManager
import blended.security.ssl.internal.ServerKeyStore
import blended.security.ssl.internal.MemoryKeystore
import blended.util.logging.Logger
import domino.DominoActivator
import org.apache.felix.connect.launch.ClasspathScanner
@@ -71,8 +71,8 @@ class CertRefresher(salt: String) {
registry.getBundleContext().getBundle().stop(0)
}

def checkCert(): Future[(ServerKeyStore, List[String])] = {
val promise = Promise[(ServerKeyStore, List[String])]()
def checkCert(): Future[(MemoryKeystore, List[String])] = {
val promise = Promise[(MemoryKeystore, List[String])]()
Future {
new DominoActivator {
whenBundleActive {
@@ -81,7 +81,7 @@ class ScepCertificateProvider(cfg: ScepConfig) extends CertificateProvider {
val csrBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Principal(cnProvider.commonName().get), inCert.publicKey)
csrBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_challengePassword, new DERPrintableString(cfg.scepChallenge))

// TODO addextensions ?
// TODO add extensions ?

val csrSignerBuilder = new JcaContentSignerBuilder(cfg.csrSignAlgorithm)
val csrSigner = csrSignerBuilder.build(privKey)
@@ -3,6 +3,8 @@ package blended.security.ssl
import java.security.{KeyPair, Principal, PrivateKey, PublicKey}
import java.security.cert.{Certificate, X509Certificate}

import javax.security.auth.x500.X500Principal

import scala.util.Try

/**
@@ -18,10 +20,14 @@ import scala.util.Try
* with the factory method(s) in the companion object. These methods will create the
* sorted chain verify the signatures of each certificate within the chain.
*/
case class CertificateHolder private (
case class CertificateHolder (
publicKey : PublicKey,
privateKey : Option[PrivateKey],
chain: List[X509Certificate]) {
chain: List[X509Certificate],
changed : Boolean = false
) {

val subjectPrincipal : Option[X500Principal] = chain.headOption.map(_.getIssuerX500Principal())

// retrieve the keyPair of the certificate
val keyPair : Option[KeyPair] = privateKey.map(pk => new KeyPair(publicKey, pk))
@@ -2,13 +2,13 @@ package blended.security.ssl

import scala.util.Try

import blended.security.ssl.internal.ServerKeyStore
import blended.security.ssl.internal.MemoryKeystore

trait CertificateManager {

/**
* @return When successful, a tuple of keystore and a list of updated certificate aliases, else the failure.
*/
def checkCertificates(): Try[(ServerKeyStore, List[String])]
def checkCertificates(): Try[(MemoryKeystore, List[String])]

}
@@ -2,6 +2,15 @@ package blended.security.ssl

import scala.util.Try

/**
* A certificate provider will retrieve a new Certificate wrapped in a CertificateHolder.
* If an existing certificate is given, the provider shall update the certificate (i.e. by
* extending the validity range and obtaining a new signature. If no existing certificate is
* given, an initial Certificate shall be obtained.
*
* The CommonNameProvider is responsible for providing the common name of the certificate and also
* a list of subject alternative names, if any need to be set.
*/
trait CertificateProvider {

def refreshCertificate(existing: Option[CertificateHolder], cnProvider: CommonNameProvider): Try[CertificateHolder]
@@ -0,0 +1,52 @@
package blended.security.ssl.internal

import blended.security.ssl.{CertificateHolder, X509CertificateInfo}
import scala.concurrent.duration._

object ResultLevel extends Enumeration {
type ResultLevel = Value
val INFO, WARN, ERROR = Value
}

/**
* Encapsulate results of a certificate checker
* @param cert The certificate that has been checked
* @param results A check message and a level of the message
*/
case class CertificateCheckResult(
cert : CertificateHolder,
results : Seq[(ResultLevel.ResultLevel, String)]
)

trait CertificateChecker {

/**
* Check the certificates of a given [[MemoryKeystore]].
* @param certs
* @return A sequence of [[CertificateCheckResult]] with result messages for all certificates that
* produce "remarks". The certificates are a subset of the certificates in the checked
* keystore.
*/
def checkCertificates(certs : MemoryKeystore) : Seq[CertificateCheckResult]
}

class RemainingValidityChecker(minValidDays: Int) extends CertificateChecker {

private val millisPerDay : Long = 1.day.toMillis

override def checkCertificates(certs: MemoryKeystore): Seq[CertificateCheckResult] = {

certs.certificates.values.map { cert =>
val certInfo : X509CertificateInfo = X509CertificateInfo(cert.chain.head)
val remaining : Long = certInfo.notAfter.getTime() - System.currentTimeMillis()

if (remaining <= minValidDays * millisPerDay) {
val msg = s"Certificate for [${cert.chain.head.getSubjectX500Principal()}] is about to expire in ${remaining.toDouble / millisPerDay} days"
CertificateCheckResult(cert, Seq( (ResultLevel.WARN, msg) ))
} else {
val msg = s"Certificate for [${cert.chain.head.getSubjectX500Principal()}] is still valid"
CertificateCheckResult(cert, Seq((ResultLevel.INFO, msg)))
}
}.toSeq
}
}
@@ -7,13 +7,13 @@ import blended.util.config.Implicits._
import scala.util.Try

/**
* Configuration of [[CertificateManager]]
* Configuration of [[CertificateManagerImpl]]
*
* @param keyStore The used keyStore.
* @param storePass The password used to open the key store.
* @param keyPass The key password.
* If the days until the end of the certificate validity fall below this threshold,
* the [[CertificateManager]] will try to re-new the certificate.
* the [[CertificateManagerImpl]] will try to re-new the certificate.
* @param skipInitialCheck If `true` no initial certifcate check will be issues.
*/
case class CertificateManagerConfig(
@@ -105,4 +105,4 @@ object RefresherConfig {
onRefreshAction = RefresherConfig.Action.fromString(config.getString("onRefreshAction", "refresh")).get
)
}
}
}
@@ -1,20 +1,19 @@
package blended.security.ssl.internal

import java.io.{ File, FileInputStream, FileOutputStream }
import java.security.{ KeyPair, KeyStore, PrivateKey }
import java.io.File
import java.security.KeyStore
import java.util.Date

import scala.concurrent.duration._
import scala.util.{ Failure, Success, Try }

import blended.security.ssl.{ CertificateManager, CertificateProvider, CertificateHolder, X509CertificateInfo }
import blended.security.ssl.CertificateManager
import blended.security.ssl.{CertificateHolder, CertificateManager, CertificateProvider, X509CertificateInfo}
import blended.util.logging.Logger
import domino.capsule._
import domino.service_providing.ServiceProviding
import javax.net.ssl.SSLContext
import org.osgi.framework.BundleContext

import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}

/**
* A class to manage one or more server side certificates within a given keystore
* to be used as SSL server certificates.
@@ -33,13 +32,15 @@ class CertificateManagerImpl(
private[this] val log = Logger[CertificateManagerImpl]
private[this] val millisPerDay = 1.day.toMillis

private[this] lazy val keyStore = loadKeyStore()
private[this] lazy val javaKeystore : JavaKeystore= new JavaKeystore(
new File(cfg.keyStore),
cfg.storePass.toCharArray,
Some(cfg.keyPass.toCharArray)
)

def getKeystore(): ServerKeyStore = keyStore.get

private[internal] def registerSslContextProvider(ks: KeyStore): CapsuleScope = capsuleContext.executeWithinNewCapsuleScope {
private[internal] def registerSslContextProvider(): CapsuleScope = capsuleContext.executeWithinNewCapsuleScope {
log.debug("Registering SslContextProvider type=client and type=server")
val sslCtxtProvider = new SslContextProvider(ks, cfg.keyPass.toCharArray)
val sslCtxtProvider = new SslContextProvider(javaKeystore.loadKeyStoreFromFile().get, cfg.keyPass.toCharArray)
// TODO: what should we do with this side-effect, if we unregister the context provider?
// FIXME: should this side-effect be configurable?
SSLContext.setDefault(sslCtxtProvider.serverContext)
@@ -63,12 +64,17 @@ class CertificateManagerImpl(

case Success((sks, _)) =>
log.info("Successfully obtained Server Certificate(s) for SSLContext")
val regScope = registerSslContextProvider(sks.keyStore)
val regScope : Try[CapsuleScope] = Try { registerSslContextProvider() }

cfg.refresherConfig match {
case None => log.debug("No configuration for automatic certificate refresh found")
case Some(c) =>
capsuleContext.addCapsule(new CertificateRefresher(bundleContext, this, c, regScope))
regScope match {
case Success(scope) =>
capsuleContext.addCapsule(new CertificateRefresher(bundleContext, this, c, scope))
case Failure(t) => log.warn(s"Failed to load keystore from [${cfg.keyStore}] : [${t.getMessage()}]")
}

}
}
} else {
@@ -78,54 +84,34 @@ class CertificateManagerImpl(

override def stop(): Unit = {}

def nextCertificateTimeout(): Date = if (getKeystore().serverCertificates.values.isEmpty)
new Date()
else
getKeystore().serverCertificates.values.map(_.chain.head.getNotAfter).min

private[this] def loadKeyStore(): Try[ServerKeyStore] = {

log.info(s"Initializing key store [${cfg.keyStore}] for server certificate(s) ...")

val ks = KeyStore.getInstance("PKCS12")
val f = new File(cfg.keyStore)

if (f.exists()) {
val fis = new FileInputStream(f)
try {
ks.load(fis, cfg.storePass.toCharArray)
} finally {
fis.close()
}
} else {
log.info(s"Creating empty key store ...")
ks.load(null, cfg.storePass.toCharArray)
saveKeyStore(ks)
def nextCertificateTimeout(): Try[Date] = Try {
loadKeyStore().get.certificates match {
case e if e.isEmpty => new Date()
case m => m.values.map(_.chain.head.getNotAfter).min
}

serverKeystore(ks)
}

private[this] def loadKeyStore(): Try[MemoryKeystore] = javaKeystore.loadKeyStore()

/**
* @return When successful, a tuple of keystore and a list of updated certificate aliases, else the failure.
*/
override def checkCertificates(): Try[(ServerKeyStore, List[String])] = Try {
override def checkCertificates(): Try[(MemoryKeystore, List[String])] = Try {

val ks = keyStore.get
val ms : MemoryKeystore = loadKeyStore().get

def changedAliases(certConfigs: List[CertificateConfig], changed: List[String]): Try[List[String]] = Try {
certConfigs match {
case Nil => changed
case head :: tail =>
val existingCert = extractServerCertificate(ks.keyStore, head).get
existingCert match {
ms.certificates.get(head.alias) match {
case Some(serverCertificate) =>
val certInfo = X509CertificateInfo(serverCertificate.chain.head)
val remaining = certInfo.notAfter.getTime() - System.currentTimeMillis()

if (remaining <= head.minValidDays * millisPerDay) {
log.info(s"Certificate [${head.alias}] is about to expire in ${remaining.toDouble / millisPerDay} days...refreshing certificate")
updateKeystore(ks.keyStore, existingCert, head).recoverWith {
updateKeystore(ms, Some(serverCertificate), head).recoverWith {
case _: Throwable =>
log.info(s"Could not refresh certificate [${head.alias}], reusing the existing one.")
changedAliases(tail, changed)
@@ -137,17 +123,17 @@ class CertificateManagerImpl(
}
case None =>
log.info(s"Certificate with alias [${head.alias}] does not yet exist.")
updateKeystore(ks.keyStore, None, head).get
updateKeystore(ms, None, head).get
changedAliases(tail, head.alias :: changed).get
}
}
}

(ks, changedAliases(cfg.certConfigs, List.empty).get)
(ms, changedAliases(cfg.certConfigs, List.empty).get)
}

private[this] def updateKeystore(ks: KeyStore, existingCert: Option[CertificateHolder], certCfg: CertificateConfig): Try[ServerKeyStore] = Try {
log.info(s"Aquiring new certificate from certificate provider [${certCfg.provider}]")
private[this] def updateKeystore(ks: MemoryKeystore, existingCert: Option[CertificateHolder], certCfg: CertificateConfig): Try[MemoryKeystore] = Try {
log.info(s"Acquiring new certificate from certificate provider [${certCfg.provider}]")

val provider = providerMap.get(certCfg.provider).get
val newCert = provider.refreshCertificate(existingCert, certCfg.cnProvider)
@@ -159,40 +145,9 @@ class CertificateManagerImpl(
case Success(cert) =>
val info = X509CertificateInfo(cert.chain.head)
log.info(s"Successfully obtained certificate from certificate provider [$provider] : $info")
cert.keyPair.foreach(p => ks.setKeyEntry(certCfg.alias, p.getPrivate(), cfg.keyPass.toCharArray, cert.chain.toArray))
saveKeyStore(ks).get
serverKeystore(ks).get
}
}

private[this] def saveKeyStore(ks: KeyStore): Try[KeyStore] = Try {
val fos = new FileOutputStream(cfg.keyStore)
try {
ks.store(fos, cfg.storePass.toCharArray)
log.info(s"Successfully written modified key store to [${cfg.keyStore}] with storePass [${cfg.storePass}]")
} finally {
fos.close()
}

ks
}

// Extract a single server certificate from the underlying keystore
private[this] def extractServerCertificate(ks: KeyStore, certCfg: CertificateConfig): Try[Option[CertificateHolder]] = Try {
Option(ks.getCertificateChain(certCfg.alias)).map { chain =>
val e = ks.getCertificate(certCfg.alias)
val key = ks.getKey(certCfg.alias, cfg.keyPass.toCharArray).asInstanceOf[PrivateKey]
val keypair = new KeyPair(e.getPublicKey(), key)
CertificateHolder.create(keyPair = keypair, chain = chain.toList).get
val certs : Map[String, CertificateHolder] = ks.certificates.filterKeys(_ != certCfg.alias) + (certCfg.alias -> cert.copy(changed = true))
MemoryKeystore(certs)
}
}

private[this] def serverKeystore(ks: KeyStore): Try[ServerKeyStore] = Try {

val certs: Map[String, CertificateHolder] = cfg.certConfigs.map { certCfg =>
(certCfg.alias, extractServerCertificate(ks, certCfg).get)
}.filter(_._2.isDefined).toMap.mapValues(_.get)

ServerKeyStore(ks, certs)
}
}
@@ -54,7 +54,7 @@ class CertificateRefresher(
override def start(): Unit = scheduleRefresh(cfg)

override def stop(): Unit = {
log.debug(s"Cancelling timer [${timerName}]")
log.debug(s"Cancelling timer [$timerName]")
timer.cancel()
}

@@ -63,14 +63,12 @@ class CertificateRefresher(
* If the Domino capsule stops, this timer will also be cancelled.
*/
def scheduleRefresh(refresherConfig: RefresherConfig): Unit = {
val nextScheduleTime = CertificateRefresher.nextRefreshScheduleTime(certMgr.nextCertificateTimeout(), refresherConfig)
val nextScheduleTime = CertificateRefresher.nextRefreshScheduleTime(certMgr.nextCertificateTimeout().get, refresherConfig)
val task = new RefreshTask(certMgr, refresherConfig)
log.debug(s"Scheduling new timer task with timer [${timerName}] to start at ${nextScheduleTime}")
timer.schedule(task, nextScheduleTime)
}



/**
* This task tries to refresh a certificate.
* If positive, depending on config, the new certificate is re-published as SslContextProvider or the whole OSGi container will be restarted.
@@ -99,7 +97,7 @@ class CertificateRefresher(
scope.stop()

log.info("Registering new SslContextProvider for new KeyStore")
certMgr.registerSslContextProvider(newKs.keyStore)
certMgr.registerSslContextProvider()
scheduleRefresh(refresherConfig)

case RefresherConfig.Restart =>
Oops, something went wrong.

0 comments on commit 9d795ed

Please sign in to comment.