src/main/scala/auth/X509Cert.scala
author Henry Story <henry.story@bblfish.net>
Sun, 15 Jul 2012 01:26:25 +0200
changeset 239 258d2757ef3d
parent 204 3ed197d09cba
permissions -rw-r--r--
commit test report details
     1 /*
     2  * Copyright (c) 2011 Henry Story (bblfish.net)
     3  * under the MIT licence defined
     4  *    http://www.opensource.org/licenses/mit-license.html
     5  *
     6  * Permission is hereby granted, free of charge, to any person obtaining a copy of
     7  * this software and associated documentation files (the "Software"), to deal in the
     8  * Software without restriction, including without limitation the rights to use, copy,
     9  * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
    10  * and to permit persons to whom the Software is furnished to do so, subject to the
    11  * following conditions:
    12  *
    13  * The above copyright notice and this permission notice shall be included in all
    14  * copies or substantial portions of the Software.
    15  *
    16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    17  * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    18  * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    19  * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    20  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    21  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    22  */
    23 
    24 package org.w3.readwriteweb.auth
    25 
    26 import javax.servlet.http.HttpServletRequest
    27 import unfiltered.netty.ReceivedMessage
    28 import java.util.Date
    29 import java.math.BigInteger
    30 import java.net.URL
    31 import unfiltered.request.{UserAgent, HttpRequest}
    32 import java.security.cert.{X509Certificate, Certificate}
    33 import java.security._
    34 import interfaces.RSAPublicKey
    35 import unfiltered.util.IO
    36 import sun.security.x509._
    37 import org.w3.readwriteweb.util.trySome
    38 import actors.threadpool.TimeUnit
    39 import com.google.common.cache.{CacheLoader, CacheBuilder, Cache}
    40 import scalaz.Validation
    41 import scalaz.Scalaz._
    42 
    43 import com.weiglewilczek.slf4s.Logging
    44 
    45 object X509CertSigner extends Logging {
    46 
    47   def apply( keyStoreLoc: Option[URL],
    48              keyStoreType: Option[String],
    49              password: Option[String],
    50              alias: Option[String]): Option[X509CertSigner] = {
    51     try {
    52       for {
    53         loc <- keyStoreLoc
    54         tp <- keyStoreType
    55       } yield {
    56         val pass = password.map(_.toCharArray).getOrElse(null)
    57         val alias2 = alias.getOrElse("")  //todo there are better ways of finding an alias than this
    58         val ks = KeyStore.getInstance(tp)
    59         IO.use(loc.openStream()) {
    60           in => ks.load(in, pass)
    61         }
    62         val privateKey = ks.getKey(alias2, pass).asInstanceOf[PrivateKey]
    63         val certificate = ks.getCertificate(alias2).asInstanceOf[X509Certificate]
    64         //one could verify that indeed this is the private key corresponding to the public key in the cert.
    65         new X509CertSigner(certificate, privateKey)
    66       }
    67     } catch {
    68       case e: Exception => {
    69         logger.warn("could not load TLS certificate for certificate signing service", e)
    70         None
    71       }
    72     }
    73   }
    74 
    75   def apply( keyStoreLoc: URL,
    76              keyStoreType: String,
    77              password: String,
    78              alias: String): X509CertSigner =
    79     apply(Option(keyStoreLoc),Option(keyStoreType),Option(password),Option(alias)).get
    80 
    81 }
    82 
    83 class X509CertSigner(
    84     val signingCert: X509Certificate,
    85     signingKey: PrivateKey ) {
    86   val WebID_DN="""O=FOAF+SSL, OU=The Community of Self Signers, CN=Not a Certification Authority"""
    87 
    88   val sigAlg = signingKey.getAlgorithm match {
    89     case "RSA" =>  "SHA1withRSA"
    90     case "DSA" =>  "SHA1withDSA"
    91     //else will throw a case exception
    92   }
    93 
    94 
    95   /**
    96    * Adapted from http://bfo.com/blog/2011/03/08/odds_and_ends_creating_a_new_x_509_certificate.html
    97    * The libraries used here are sun gpled code. This is much lighter to use than bouncycastle. All VMs that already
    98    * have these classes don't need to download the code. It should be easy in scala to create a build that can decide
    99    * if these need to be added to the classpath. I think the code just looks better than bouncycastle too.
   100    *
   101    * WARNING THIS IS   in construction
   102    *
   103    * Look in detail at http://www.ietf.org/rfc/rfc2459.txt
   104    *
   105    * Create a self-signed X.509 Certificate
   106    * @param subjectDN the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB"
   107    * @param subjectKey the public key for the subject
   108    * @param days how many days from now the Certificate is valid for
   109    * @param webId a WebID to place in the Subject Alternative Name field of the Cert to be generated
   110    */
   111   def generate(
   112       subjectDN: String,
   113       subjectKey: RSAPublicKey,
   114       days: Int,
   115       webId: URL): X509Certificate = {   //todo: the algorithm should be deduced from private key in part
   116 
   117     var info = new X509CertInfo
   118     val from = new Date(System.currentTimeMillis()-10*1000*60) //start 10 minutes ago, to avoid network trouble
   119     val to = new Date(from.getTime + days*24*60*60*1000) 
   120     val interval = new CertificateValidity(from, to)
   121     val serialNumber = new BigInteger(64, new SecureRandom)
   122     val subjectXN = new X500Name(subjectDN)
   123     val issuerXN = new X500Name(signingCert.getSubjectDN.toString)
   124 
   125     info.set(X509CertInfo.VALIDITY, interval)
   126     info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serialNumber))
   127     info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(subjectXN))
   128     info.set(X509CertInfo.ISSUER, new CertificateIssuerName(issuerXN))
   129     info.set(X509CertInfo.KEY, new CertificateX509Key(subjectKey))
   130     info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3))
   131 
   132     //
   133     //extensions
   134     //
   135     val extensions = new CertificateExtensions
   136 
   137     val san =
   138       new SubjectAlternativeNameExtension(
   139           true,
   140           new GeneralNames().add(
   141               new GeneralName(new URIName(webId.toExternalForm))))
   142     
   143     extensions.set(san.getName, san)
   144 
   145     val basicCstrExt = new BasicConstraintsExtension(false,1)
   146     extensions.set(basicCstrExt.getName,basicCstrExt)
   147 
   148     {
   149       import KeyUsageExtension._
   150       val keyUsage = new KeyUsageExtension
   151       val usages =
   152         List(DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, KEY_AGREEMENT)
   153       usages foreach { usage => keyUsage.set(usage, true) }
   154       extensions.set(keyUsage.getName,keyUsage)
   155     }
   156 
   157     {
   158       import NetscapeCertTypeExtension._
   159       val netscapeExt = new NetscapeCertTypeExtension
   160       List(SSL_CLIENT, S_MIME) foreach { ext => netscapeExt.set(ext, true) }
   161       extensions.set(
   162         netscapeExt.getName,
   163         new NetscapeCertTypeExtension(false, netscapeExt.getExtensionValue().clone))
   164     }
   165       
   166     val subjectKeyExt =
   167       new SubjectKeyIdentifierExtension(new KeyIdentifier(subjectKey).getIdentifier)
   168 
   169     extensions.set(subjectKeyExt.getName, subjectKeyExt)
   170     
   171     info.set(X509CertInfo.EXTENSIONS, extensions)
   172 
   173     val algo = signingCert.getPublicKey.getAlgorithm match {
   174       case "DSA" => new AlgorithmId(AlgorithmId.sha1WithDSA_oid )
   175       case "RSA" => new AlgorithmId(AlgorithmId.sha1WithRSAEncryption_oid)
   176       case _ => sys.error("Don't know how to sign with this type of key")  
   177     }
   178 
   179     info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo))
   180 
   181     // Sign the cert to identify the algorithm that's used.
   182     val tmpCert = new X509CertImpl(info)
   183     tmpCert.sign(signingKey, algo.getName)
   184 
   185     //update the algorithm and re-sign
   186     val sigAlgo = tmpCert.get(X509CertImpl.SIG_ALG).asInstanceOf[AlgorithmId]
   187     info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, sigAlgo)
   188     val cert = new X509CertImpl(info)
   189     cert.sign(signingKey,algo.getName)
   190       
   191     cert.verify(signingCert.getPublicKey)
   192     return cert
   193   }
   194 
   195   val clonesig : Signature =  sig
   196 
   197   def sig: Signature = {
   198     if (clonesig != null && clonesig.isInstanceOf[Cloneable]) clonesig.clone().asInstanceOf[Signature]
   199     else {
   200       val signature = Signature.getInstance(sigAlg)
   201       signature.initSign(signingKey)
   202       signature
   203     }
   204   }
   205 
   206   def sign(string: String): Array[Byte] = {
   207       val signature = sig
   208       signature.update(string.getBytes("UTF-8"))
   209       signature.sign
   210   }
   211 
   212 }
   213 
   214 
   215 object Certs {
   216 
   217   def unapplySeq[T](r: HttpRequest[T])(implicit m: Manifest[T], fetch: Boolean=true): Option[IndexedSeq[Certificate]] = {
   218     if (m <:< manifest[HttpServletRequest])
   219       unapplyServletRequest(r.asInstanceOf[HttpRequest[HttpServletRequest]])
   220     else if (m <:< manifest[ReceivedMessage])
   221       unapplyReceivedMessage(r.asInstanceOf[HttpRequest[ReceivedMessage]],fetch)
   222     else
   223       None //todo: should  throw an exception here?
   224   }
   225 
   226 
   227   //todo: should perhaps pass back error messages, which they could in the case of netty
   228 
   229   private def unapplyServletRequest[T <: HttpServletRequest](r: HttpRequest[T]): Option[IndexedSeq[Certificate]] =
   230     r.underlying.getAttribute("javax.servlet.request.X509Certificate") match {
   231       case certs: Array[Certificate] => Some(certs)
   232       case _ => None
   233     }
   234   
   235   private def unapplyReceivedMessage[T <: ReceivedMessage](r: HttpRequest[T], fetch: Boolean): Option[IndexedSeq[Certificate]] = {
   236 
   237     import org.jboss.netty.handler.ssl.SslHandler
   238     
   239     val sslh = r.underlying.context.getPipeline.get(classOf[SslHandler])
   240     
   241     trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq) orElse {
   242       //it seems that the jvm does not keep a very good cache of remote certificates in a session. But
   243       //see http://stackoverflow.com/questions/8731157/netty-https-tls-session-duration-why-is-renegotiation-needed
   244       if (!fetch) None
   245       else {
   246         sslh.setEnableRenegotiation(true) // todo: does this have to be done on every request?
   247         r match {
   248           case UserAgent(agent) if needAuth(agent) => sslh.getEngine.setNeedClientAuth(true)
   249           case _ => sslh.getEngine.setWantClientAuth(true)
   250         }
   251         val future = sslh.handshake()
   252         future.await(30000) //that's certainly way too long.
   253         if (future.isDone && future.isSuccess)
   254           trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq)
   255         else
   256           None
   257       }
   258     }
   259 
   260   }
   261 
   262  /**
   263   *  Some agents do not send client certificates unless required. This is a problem for them, as it ends up breaking the
   264   *  connection for those agents if the client does not have a certificate...
   265   *
   266   *  It would be useful if this could be updated by server from time to  time from a file on the internet,
   267   *  so that changes to browsers could update server behavior
   268   *
   269   */
   270   def needAuth(agent: String): Boolean =
   271     (agent contains "Java")  | (agent contains "AppleWebKit")  |  (agent contains "Opera") | (agent contains "libcurl")
   272   
   273 }
   274