src/main/scala/auth/X509Cert.scala
author Henry Story <henry.story@bblfish.net>
Fri, 23 Dec 2011 18:24:10 +0100
branchwebid
changeset 163 ed559ff1977b
parent 162 23a4ecd7b45d
child 166 fc3c5c54f72b
permissions -rw-r--r--
setting ids consistenlty for the form since the javascript depends on it.
/*
 * Copyright (c) 2011 Henry Story (bblfish.net)
 * under the MIT licence defined
 *    http://www.opensource.org/licenses/mit-license.html
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in the
 * Software without restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so, subject to the
 * following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package org.w3.readwriteweb.auth

import javax.servlet.http.HttpServletRequest
import unfiltered.netty.ReceivedMessage
import java.util.Date
import java.math.BigInteger
import java.net.URL
import unfiltered.request.{UserAgent, HttpRequest}
import java.security.cert.{X509Certificate, Certificate}
import java.security._
import interfaces.RSAPublicKey
import unfiltered.util.IO
import sun.security.x509._
import org.w3.readwriteweb.util.trySome
import actors.threadpool.TimeUnit
import com.google.common.cache.{CacheLoader, CacheBuilder, Cache}

object X509CertSigner {

  def apply(
      keyStoreLoc: URL,
      keyStoreType: String,
      password: String,
      alias: String): X509CertSigner = {
    val keystore = KeyStore.getInstance(keyStoreType)

    IO.use(keyStoreLoc.openStream()) { in =>
      keystore.load(in, password.toCharArray)
    }
    val privateKey = keystore.getKey(alias, password.toCharArray).asInstanceOf[PrivateKey]
    val certificate = keystore.getCertificate(alias).asInstanceOf[X509Certificate]
    //one could verify that indeed this is the private key corresponding to the public key in the cert.

    new X509CertSigner(certificate, privateKey)
  }
}

class X509CertSigner(
    val signingCert: X509Certificate,
    signingKey: PrivateKey ) {
  val WebID_DN="""O=FOAF+SSL, OU=The Community of Self Signers, CN=Not a Certification Authority"""

  val sigAlg = signingKey.getAlgorithm match {
    case "RSA" =>  "SHA1withRSA"
    case "DSA" =>  "SHA1withDSA"
    //else will throw a case exception
  }


  /**
   * Adapted from http://bfo.com/blog/2011/03/08/odds_and_ends_creating_a_new_x_509_certificate.html
   * The libraries used here are sun gpled code. This is much lighter to use than bouncycastle. All VMs that already
   * have these classes don't need to download the code. It should be easy in scala to create a build that can decide
   * if these need to be added to the classpath. I think the code just looks better than bouncycastle too.
   *
   * WARNING THIS IS   in construction
   *
   * Look in detail at http://www.ietf.org/rfc/rfc2459.txt
   *
   * Create a self-signed X.509 Certificate
   * @param subjectDN the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB"
   * @param subjectKey the public key for the subject
   * @param days how many days from now the Certificate is valid for
   * @param webId a WebID to place in the Subject Alternative Name field of the Cert to be generated
   */
  def generate(
      subjectDN: String,
      subjectKey: RSAPublicKey,
      days: Int,
      webId: URL): X509Certificate = {   //todo: the algorithm should be deduced from private key in part

    var info = new X509CertInfo
    val from = new Date(System.currentTimeMillis()-10*1000*60) //start 10 minutes ago, to avoid network trouble
    val to = new Date(from.getTime + days*24*60*60*1000) 
    val interval = new CertificateValidity(from, to)
    val serialNumber = new BigInteger(64, new SecureRandom)
    val subjectXN = new X500Name(subjectDN)
    val issuerXN = new X500Name(signingCert.getSubjectDN.toString)

    info.set(X509CertInfo.VALIDITY, interval)
    info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serialNumber))
    info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(subjectXN))
    info.set(X509CertInfo.ISSUER, new CertificateIssuerName(issuerXN))
    info.set(X509CertInfo.KEY, new CertificateX509Key(subjectKey))
    info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3))

    //
    //extensions
    //
    val extensions = new CertificateExtensions

    val san =
      new SubjectAlternativeNameExtension(
          true,
          new GeneralNames().add(
              new GeneralName(new URIName(webId.toExternalForm))))
    
    extensions.set(san.getName, san)

    val basicCstrExt = new BasicConstraintsExtension(false,1)
    extensions.set(basicCstrExt.getName,basicCstrExt)

    {
      import KeyUsageExtension._
      val keyUsage = new KeyUsageExtension
      val usages =
        List(DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, KEY_AGREEMENT)
      usages foreach { usage => keyUsage.set(usage, true) }
      extensions.set(keyUsage.getName,keyUsage)
    }

    {
      import NetscapeCertTypeExtension._
      val netscapeExt = new NetscapeCertTypeExtension
      List(SSL_CLIENT, S_MIME) foreach { ext => netscapeExt.set(ext, true) }
      extensions.set(
        netscapeExt.getName,
        new NetscapeCertTypeExtension(false, netscapeExt.getValue))
    }
      
    val subjectKeyExt =
      new SubjectKeyIdentifierExtension(new KeyIdentifier(subjectKey).getIdentifier)

    extensions.set(subjectKeyExt.getName, subjectKeyExt)
    
    info.set(X509CertInfo.EXTENSIONS, extensions)

    val algo = signingCert.getPublicKey.getAlgorithm match {
      case "DSA" => new AlgorithmId(AlgorithmId.sha1WithDSA_oid )
      case "RSA" => new AlgorithmId(AlgorithmId.sha1WithRSAEncryption_oid)
      case _ => sys.error("Don't know how to sign with this type of key")  
    }

    info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo))

    // Sign the cert to identify the algorithm that's used.
    val tmpCert = new X509CertImpl(info)
    tmpCert.sign(signingKey, algo.getName)

    //update the algorithm and re-sign
    val sigAlgo = tmpCert.get(X509CertImpl.SIG_ALG).asInstanceOf[AlgorithmId]
    info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, sigAlgo)
    val cert = new X509CertImpl(info)
    cert.sign(signingKey,algo.getName)
      
    cert.verify(signingCert.getPublicKey)
    return cert
  }

  val clonesig : Signature =  sig

  def sig: Signature = {
    if (clonesig != null && clonesig.isInstanceOf[Cloneable]) clonesig.clone().asInstanceOf[Signature]
    else {
      val signature = Signature.getInstance(sigAlg)
      signature.initSign(signingKey)
      signature
    }
  }

  def sign(string: String): Array[Byte] = {
      val signature = sig
      signature.update(string.getBytes("UTF-8"))
      signature.sign
  }

}


object Certs {

  def unapplySeq[T](r: HttpRequest[T])(implicit m: Manifest[T], fetch: Boolean=true): Option[IndexedSeq[Certificate]] = {
    if (m <:< manifest[HttpServletRequest])
      unapplyServletRequest(r.asInstanceOf[HttpRequest[HttpServletRequest]])
    else if (m <:< manifest[ReceivedMessage])
      unapplyReceivedMessage(r.asInstanceOf[HttpRequest[ReceivedMessage]],fetch)
    else
      None //todo: should  throw an exception here?
  }


  //todo: should perhaps pass back error messages, which they could in the case of netty

  private def unapplyServletRequest[T <: HttpServletRequest](r: HttpRequest[T]): Option[IndexedSeq[Certificate]] =
    r.underlying.getAttribute("javax.servlet.request.X509Certificate") match {
      case certs: Array[Certificate] => Some(certs)
      case _ => None
    }
  
  private def unapplyReceivedMessage[T <: ReceivedMessage](r: HttpRequest[T], fetch: Boolean): Option[IndexedSeq[Certificate]] = {

    import org.jboss.netty.handler.ssl.SslHandler
    
    val sslh = r.underlying.context.getPipeline.get(classOf[SslHandler])
    
    trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq) orElse {
      if (!fetch) None
      else {
        sslh.setEnableRenegotiation(true)
        r match {
          case UserAgent(agent) if needAuth(agent) => sslh.getEngine.setNeedClientAuth(true)
          case _ => sslh.getEngine.setWantClientAuth(true)
        }
        val future = sslh.handshake()
        future.await(30000) //that's certainly way too long.
        if (future.isDone && future.isSuccess)
          trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq)
        else
          None
      }
    }

  }

 /**
  *  Some agents do not send client certificates unless required. This is a problem for them, as it ends up breaking the
  *  connection for those agents if the client does not have a certificate...
  *
  *  It would be useful if this could be updated by server from time to  time from a file on the internet,
  *  so that changes to browsers could update server behavior
  *
  */
  def needAuth(agent: String): Boolean =
    (agent contains "Java")  | (agent contains "AppleWebKit")  |  (agent contains "Opera")
  
}