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 153 37dd439c9383
child 166 fc3c5c54f72b
permissions -rw-r--r--
setting ids consistenlty for the form since the javascript depends on it.
     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 
    41 object X509CertSigner {
    42 
    43   def apply(
    44       keyStoreLoc: URL,
    45       keyStoreType: String,
    46       password: String,
    47       alias: String): X509CertSigner = {
    48     val keystore = KeyStore.getInstance(keyStoreType)
    49 
    50     IO.use(keyStoreLoc.openStream()) { in =>
    51       keystore.load(in, password.toCharArray)
    52     }
    53     val privateKey = keystore.getKey(alias, password.toCharArray).asInstanceOf[PrivateKey]
    54     val certificate = keystore.getCertificate(alias).asInstanceOf[X509Certificate]
    55     //one could verify that indeed this is the private key corresponding to the public key in the cert.
    56 
    57     new X509CertSigner(certificate, privateKey)
    58   }
    59 }
    60 
    61 class X509CertSigner(
    62     val signingCert: X509Certificate,
    63     signingKey: PrivateKey ) {
    64   val WebID_DN="""O=FOAF+SSL, OU=The Community of Self Signers, CN=Not a Certification Authority"""
    65 
    66   val sigAlg = signingKey.getAlgorithm match {
    67     case "RSA" =>  "SHA1withRSA"
    68     case "DSA" =>  "SHA1withDSA"
    69     //else will throw a case exception
    70   }
    71 
    72 
    73   /**
    74    * Adapted from http://bfo.com/blog/2011/03/08/odds_and_ends_creating_a_new_x_509_certificate.html
    75    * The libraries used here are sun gpled code. This is much lighter to use than bouncycastle. All VMs that already
    76    * have these classes don't need to download the code. It should be easy in scala to create a build that can decide
    77    * if these need to be added to the classpath. I think the code just looks better than bouncycastle too.
    78    *
    79    * WARNING THIS IS   in construction
    80    *
    81    * Look in detail at http://www.ietf.org/rfc/rfc2459.txt
    82    *
    83    * Create a self-signed X.509 Certificate
    84    * @param subjectDN the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB"
    85    * @param subjectKey the public key for the subject
    86    * @param days how many days from now the Certificate is valid for
    87    * @param webId a WebID to place in the Subject Alternative Name field of the Cert to be generated
    88    */
    89   def generate(
    90       subjectDN: String,
    91       subjectKey: RSAPublicKey,
    92       days: Int,
    93       webId: URL): X509Certificate = {   //todo: the algorithm should be deduced from private key in part
    94 
    95     var info = new X509CertInfo
    96     val from = new Date(System.currentTimeMillis()-10*1000*60) //start 10 minutes ago, to avoid network trouble
    97     val to = new Date(from.getTime + days*24*60*60*1000) 
    98     val interval = new CertificateValidity(from, to)
    99     val serialNumber = new BigInteger(64, new SecureRandom)
   100     val subjectXN = new X500Name(subjectDN)
   101     val issuerXN = new X500Name(signingCert.getSubjectDN.toString)
   102 
   103     info.set(X509CertInfo.VALIDITY, interval)
   104     info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serialNumber))
   105     info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(subjectXN))
   106     info.set(X509CertInfo.ISSUER, new CertificateIssuerName(issuerXN))
   107     info.set(X509CertInfo.KEY, new CertificateX509Key(subjectKey))
   108     info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3))
   109 
   110     //
   111     //extensions
   112     //
   113     val extensions = new CertificateExtensions
   114 
   115     val san =
   116       new SubjectAlternativeNameExtension(
   117           true,
   118           new GeneralNames().add(
   119               new GeneralName(new URIName(webId.toExternalForm))))
   120     
   121     extensions.set(san.getName, san)
   122 
   123     val basicCstrExt = new BasicConstraintsExtension(false,1)
   124     extensions.set(basicCstrExt.getName,basicCstrExt)
   125 
   126     {
   127       import KeyUsageExtension._
   128       val keyUsage = new KeyUsageExtension
   129       val usages =
   130         List(DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, KEY_AGREEMENT)
   131       usages foreach { usage => keyUsage.set(usage, true) }
   132       extensions.set(keyUsage.getName,keyUsage)
   133     }
   134 
   135     {
   136       import NetscapeCertTypeExtension._
   137       val netscapeExt = new NetscapeCertTypeExtension
   138       List(SSL_CLIENT, S_MIME) foreach { ext => netscapeExt.set(ext, true) }
   139       extensions.set(
   140         netscapeExt.getName,
   141         new NetscapeCertTypeExtension(false, netscapeExt.getValue))
   142     }
   143       
   144     val subjectKeyExt =
   145       new SubjectKeyIdentifierExtension(new KeyIdentifier(subjectKey).getIdentifier)
   146 
   147     extensions.set(subjectKeyExt.getName, subjectKeyExt)
   148     
   149     info.set(X509CertInfo.EXTENSIONS, extensions)
   150 
   151     val algo = signingCert.getPublicKey.getAlgorithm match {
   152       case "DSA" => new AlgorithmId(AlgorithmId.sha1WithDSA_oid )
   153       case "RSA" => new AlgorithmId(AlgorithmId.sha1WithRSAEncryption_oid)
   154       case _ => sys.error("Don't know how to sign with this type of key")  
   155     }
   156 
   157     info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo))
   158 
   159     // Sign the cert to identify the algorithm that's used.
   160     val tmpCert = new X509CertImpl(info)
   161     tmpCert.sign(signingKey, algo.getName)
   162 
   163     //update the algorithm and re-sign
   164     val sigAlgo = tmpCert.get(X509CertImpl.SIG_ALG).asInstanceOf[AlgorithmId]
   165     info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, sigAlgo)
   166     val cert = new X509CertImpl(info)
   167     cert.sign(signingKey,algo.getName)
   168       
   169     cert.verify(signingCert.getPublicKey)
   170     return cert
   171   }
   172 
   173   val clonesig : Signature =  sig
   174 
   175   def sig: Signature = {
   176     if (clonesig != null && clonesig.isInstanceOf[Cloneable]) clonesig.clone().asInstanceOf[Signature]
   177     else {
   178       val signature = Signature.getInstance(sigAlg)
   179       signature.initSign(signingKey)
   180       signature
   181     }
   182   }
   183 
   184   def sign(string: String): Array[Byte] = {
   185       val signature = sig
   186       signature.update(string.getBytes("UTF-8"))
   187       signature.sign
   188   }
   189 
   190 }
   191 
   192 
   193 object Certs {
   194 
   195   def unapplySeq[T](r: HttpRequest[T])(implicit m: Manifest[T], fetch: Boolean=true): Option[IndexedSeq[Certificate]] = {
   196     if (m <:< manifest[HttpServletRequest])
   197       unapplyServletRequest(r.asInstanceOf[HttpRequest[HttpServletRequest]])
   198     else if (m <:< manifest[ReceivedMessage])
   199       unapplyReceivedMessage(r.asInstanceOf[HttpRequest[ReceivedMessage]],fetch)
   200     else
   201       None //todo: should  throw an exception here?
   202   }
   203 
   204 
   205   //todo: should perhaps pass back error messages, which they could in the case of netty
   206 
   207   private def unapplyServletRequest[T <: HttpServletRequest](r: HttpRequest[T]): Option[IndexedSeq[Certificate]] =
   208     r.underlying.getAttribute("javax.servlet.request.X509Certificate") match {
   209       case certs: Array[Certificate] => Some(certs)
   210       case _ => None
   211     }
   212   
   213   private def unapplyReceivedMessage[T <: ReceivedMessage](r: HttpRequest[T], fetch: Boolean): Option[IndexedSeq[Certificate]] = {
   214 
   215     import org.jboss.netty.handler.ssl.SslHandler
   216     
   217     val sslh = r.underlying.context.getPipeline.get(classOf[SslHandler])
   218     
   219     trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq) orElse {
   220       if (!fetch) None
   221       else {
   222         sslh.setEnableRenegotiation(true)
   223         r match {
   224           case UserAgent(agent) if needAuth(agent) => sslh.getEngine.setNeedClientAuth(true)
   225           case _ => sslh.getEngine.setWantClientAuth(true)
   226         }
   227         val future = sslh.handshake()
   228         future.await(30000) //that's certainly way too long.
   229         if (future.isDone && future.isSuccess)
   230           trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq)
   231         else
   232           None
   233       }
   234     }
   235 
   236   }
   237 
   238  /**
   239   *  Some agents do not send client certificates unless required. This is a problem for them, as it ends up breaking the
   240   *  connection for those agents if the client does not have a certificate...
   241   *
   242   *  It would be useful if this could be updated by server from time to  time from a file on the internet,
   243   *  so that changes to browsers could update server behavior
   244   *
   245   */
   246   def needAuth(agent: String): Boolean =
   247     (agent contains "Java")  | (agent contains "AppleWebKit")  |  (agent contains "Opera")
   248   
   249 }
   250