/* * @(#)CA.java 1.21 97/12/10 * * Copyright (c) 1996-1997 Sun Microsystems, Inc. All Rights Reserved. * * This software is the confidential and proprietary information of Sun * Microsystems, Inc. ("Confidential Information"). You shall not * disclose such Confidential Information and shall use it only in * accordance with the terms of the license agreement you entered into * with Sun. * * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING * THIS SOFTWARE OR ITS DERIVATIVES. * * CopyrightVersion 1.0 */ import java.io.*; import java.security.*; import javax.security.cert.*; import java.util.Date; import javax.servlet.*; import javax.servlet.http.*; import sun.misc.BASE64Decoder; import sun.security.x509.*; import sun.security.util.*; import sun.security.AuthContext; import sun.security.ssl.KeyStore; /** * This is a simple Certificate Authority (CA) Servlet, which * supports generation of personal X.509 certificates for use in SSL client * authentication (and other purposes, as allowed by the system which stores * that certificate and private key). It is intended to illustrate use of * the servlet API, as well as to provide private sources of low assurance * SSL client certificates. (In some environments, high assurance key * management is not needed due to the specific risks being managed.) * *

In its current form, this is not appropriate for use in running any * trustworthy authentication system. The enrollment process is simpler * than most organizations should contemplate, there are no lifecycle * services (e.g. revocation, renewal, online status checks), integrity * checks aren't made, and certs aren't even stored for reference. * * *


Using a key pair assigned to this Certificate Authority role, * this allows a Java Web Server to create X.509 certificates for uses * in application systems such as: * *

This can be used either programmatically (e.g. by an application * or an applet), or through conventional HTML forms. So for example, * you may be using this to generate personal certificates using forms * through Navigator 3.0, to generate site certificates online, or to * consult an organizational CA to get a personal code signing cert. * *

Sites could subclass this servlet to support their own operational * (or administrative) policies, within the confines supported by this * base framework. For example, perhaps a site needs to store CA data * its own database rather than the simple one used by the generic * version of this servlet, to add different data into certificates, * or to implement different client validation procedures. * *

The primary restriction on servlets being used to provide critical * services is that if operations are linked (perhaps as part of a * transaction), this must be done by a layer above servlet invocations. * Well implemented servlet operations will always be atomic. * *

Servlet bean properties are: * * * * * * * * * * * * * * * * * * *
PropertyTypeDescription
keyAliasstringThis is the "alias" (name) of the key, in the keystore, * which is used to sign certificates. Defaults to the alias * ssl-RSA-default, which is the same one used for * SSL sessions. *
keystorePathstringThis is the path to the keystore, used to store the keys * and certificates used to act as a CA. Defaults to the value * of the user.keystore system property. *
validityDaysintegerThis is the default validity period of the certificates * created by this CA, in days. Defaults to 90 days. *
* *


NOTE: This servlet relies on "login" to have been * performed at initialization time, to get access to the private key * and certificates for the CA itself. * * @version 1.21 * @author David Brownell */ public class CA extends HttpServlet { /** * Constructs an uninitialized instance of this servlet. * Servlets are initialized using the init method */ public CA () {} // // XXX Should be able to generate DSS/DSA KEYGEN challenge, // and use the appropriate key. // private String keyAlias = "ssl-RSA-default"; /** Sets the keyAlias property. */ public void setKeyAlias (String name) { keyAlias = name; } // XXX validate /** Returns the keyAlias property. */ public String getKeyAlias () { return keyAlias; } private String keystorePath; /** Sets the keystorePath property. */ public void setKeystorePath (String name) { keystorePath = name; } // XXX validate /** Returns the keystorePath property. */ public String getKeystorePath () { if (keystorePath == null) keystorePath = System.getProperty ("user.keystore"); return keystorePath; } private int validityDays = 90; /** Sets the validityDays property. */ public void setValidityDays (int days) { validityDays = days; } // XXX validate /** Returns the validityDays property. */ public int getValidityDays () { return validityDays; } /** * Initializes the servlet using its configured properties. * More values may be provided over time, particularly for * database configuration information, certification policy statement, * kind of cert being cut (e.g. SSL client, code signer), and values * that must exist in the X509 certificates being created such as * the specific organization name or X509v3 extended attributes. * *

We want to administer this servlet within the standard * servlet admin tool framework ... including capabilities like * providing task-specific GUIs to customize init parameters, with * task-specific help, and management of all stored certs. * * * @exception UnavailableException on initialization error */ public void init (ServletConfig config) throws ServletException { super.init (config); String temp; // // Get CA's private key and cert chain from the server's keystore. // We won't modify that keystore!! // try { InputStream in; KeyStore ks; // // Load the default keystore. If we weren't a trusted // servlet, we'd not be able to get do any of this!! // in = new FileInputStream (getKeystorePath ()); ks = new KeyStore (AuthContext.getDefault (), AuthContext.getPassphraseIndex ()); ks.load (in); in.close (); temp = getKeyAlias (); caPrivateKey = ks.getPrivateKey (temp); caCertChain = ks.getCertificateChain (temp); if (caPrivateKey == null || caCertChain == null) throw new UnavailableException (this, "RSA certificate and private key are needed for CA."); } catch (NoSuchAlgorithmException e) { throw new UnavailableException (this, "Need a cryptographic algorithm:" + e.toString ()); } catch (IOException e) { throw new UnavailableException (this, "Need a private key and certificate to run CA servlet."); } // // Database info: last used serial number, directory for storing // certificates and their status (revoked, suspended, etc). // // XXX this is all faked out for now; a real CA would need to // at least track certs it creates, as well as guarantee unique // serial numbers. // // XXX property! serial = (int)(new Date().getTime () / 1000); // // XXX policy options ... e.g. force organization, org unit, // locality, state, and country to prespecified values. // } /** * This servlet handles requests going to a particular node in * a given web server's URI namespace, through which a Certificate * Authority service is provided. Those requests (will) enable the * following kinds of remote CA operations:

* *

NOTE: * Most of these operations are intended to serve three kinds of user * interfaces. The simplest kind uses Netscape extensions to support * verification protocols. Another simple kind uses a set of HTML pages * with forms configured to drive the servlet. The third kind is * through a remote invocation API, perhaps used by a set of applets. * The operations supported by the servlet have no fundamental need * to differ based on what user interface is being used. * *

Areas for future extension include support of optional policy * configuration options, and combining the CA module support with other * servlets to enable flexible configuration of system characteristics * such as database location, required certificate and name attributes, * and user interface structure. * * @param req general servlet request parameter * @param resp general servlet response parameter * @exception ServletException yes * @exception IOException yes */ public void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod (); String opname = req.getParameter ("opname"); if (opname == null || method == null) throw new ServletException ("null opname or method"); // XXX check if the user has "POST" permission if (opname.equals ("genCert")) { doGenCert (req, resp); } else if (opname.equals ("renew")) { // renew the ID'd key, return it throw new ServletException ("unimplemented: " + opname); } else if (opname.equals ("revoke")) { // revoke the ID'd cert throw new ServletException ("unimplemented: " + opname); } else throw new ServletException ("invalid operation: " + opname); } /* * Cert creation ... lots is hardwired, but can be overridden by * subclasses which provide alternate policies: * * - The value of the challenge; * - Name of the KEYGEN response field; * - X.500 names are created from specific attributes; * - Certificate Attributes are not (yet) supported; * - The certificate is effectively pre-authorized. * * Note that this doesn't care what kind of key is given so long * as it's supported in the runtime environment; and that the key * is given in the format returned by Navigator 3.0's KEYGEN tag. * Navigator only does RSA keys; DSS/DSA keys are also useful. */ private void doGenCert (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { // // Get the subject's X.500 name, to be stored in the // certificate we're creating. // // NOTE that this is currently "the" policy hook for // modifying this information to support the policies // used by this site, or rejecting invalid data. // X500Name subjectName = getSubjectName (req); // // Get the user's public key, verifying that they actually know // the corresponding private key (using the signed challenge). // // Then see if the key meets policy criteria. // // XXX the challenge should have been associated with this // SSL session by servlet code (JHTML!) which returned the form // to the client, and provided a random challenge string with the // KEYGEN tag. // String challenge = "fixed-for-now"; String response = req.getParameter ("keygen"); X509Key subjectKey = getVerifiedKey (challenge, response); checkKeyAppropriate (subjectName, subjectKey); // // Check for preauthorization token in the form data; // and in fact do any other work needed before the // cert actually gets cut. // checkPreAuthorization (req); // // The public JDK 1.2 APIs don't really support creating // certificates. Turn it into bytes, use the original API // to create the cert. // byte caCertBytes []; X509Cert caCert; caCertBytes = caCertChain [0].getEncoded (); caCert = new X509Cert (caCertBytes); // // OK, cut the cert. // AlgorithmId sigAlg; X500Signer signer; X509Cert userCert; Date now; Date lastValidDate; byte userCertBytes []; long validityPeriod; // millisecs sigAlg = caCert.getIssuerAlgorithmId (); signer = caCert.getSigner (sigAlg, caPrivateKey); validityPeriod = validityDays; validityPeriod *= (24 * 60 * 60 * 1000); now = new Date (); lastValidDate = new Date (now.getTime () + validityPeriod); userCert = new X509Cert (subjectName, subjectKey, now, lastValidDate); addCertExtensions (userCert); userCertBytes = userCert.encodeAndSign ( new BigInt (serial), signer); log ("Created X509v1 Cert for <" + subjectName + "> from " + req.getRemoteHost ()); // // XXX store the (unrevoked) cert and persistently increment // the serial number we'll use ... // serial++; // // Write the (single) cert as the response. // // XXX Prefer to write the whole cert chain; there are two // main formats, either using the "certificates" field in a // PKCS #7 "SignedData" object, or Netscape's simpler format // of a PKCS #7 ContentInfo object holding an instance of the // "CertificateSequence" type. // resp.setContentType ("application/x-x509-user-cert"); resp.getOutputStream ().write (userCertBytes); } catch (CertificateEncodingException e) { throw new ServletException ( "Certificate Encoding problem: " + e.getMessage ()); } catch (NoSuchAlgorithmException e) { throw new ServletException ( "Algorithm unavailable: " + e.getMessage ()); } catch (SignatureException e) { throw new ServletException ( "Bad Signature: " + e.getMessage ()); } catch (InvalidKeyException e) { throw new ServletException ( "Invalid key: " + e.getMessage ()); } } /** * Returns the X.500 name of the subject, as reported in the * HTTP form parameters in the argument. This method may be * overridden in a policy-enhancing subclass. * *

The default policy takes a Common Name attribute * from the "name" form field, an Organization attribute * from the "org" form field, an Organizational Unit * attribute from the "unit" form field, a Locality * (city) attribute from the "locality" form field, a State * attribute from the "state" form field, and a Country * attribute from the "country" form field. These are combined * into an X.500 name, with each attribute in a separate level * of the X.500 distinguished name. * * @param req holds form parameters * @return the X.500 name of the subject * @exception ServletException if any parameter is missing */ protected X500Name getSubjectName (HttpServletRequest req) throws ServletException, IOException { String commonName = req.getParameter ("name"); String orgName = req.getParameter ("org"); String orgUnit = req.getParameter ("unit"); String locality = req.getParameter ("locality"); String state = req.getParameter ("state"); String country = req.getParameter ("country"); // XXX optionally override most name fields from static params; // for example, to ensure that organizational data is correct. if (commonName == null || orgName == null || orgUnit == null || country == null || locality == null || state == null ) throw new ServletException ("missing subject name param"); return new X500Name (commonName, orgUnit, orgName, locality, state, country); } /** * Validate the key according to a security policy. The default * policy allows all keys. Subclasses might have policies about * what kinds of keys are allowed (e.g. RSA vs DSS/DSA), key sizes * (maybe 512 bit keys are insufficient), algorithm parameters, or * preventing reuse of keys from revoked or expired certificates. * * @param subjectName the name of the subject using the key * @param subjectKey the key being checked * @exception ServletException if the key is inappropriate */ protected void checkKeyAppropriate (X500Name subjectName, X509Key subjectKey) throws ServletException { } /** * Checks whether the request was appropriately pre-authorized, * and throws an exception if not. This is normally overridden * by a policy-providing subclass. The default implementation * authorizes all certificate creation requests. * *

The pre-authorization model is (currently) as follows: a * user account may be pre-authorized by the administrator, who * may assign an authorization token. That token is communicated * out of band to the user whose credentials are being generated. * (For example, face to face, or over the phone.) When they fill * out the "new account" form, this token must be presented. This * function validates that preauthorization, or throws an exception. * The authorization token should be invalidated after being used. * *

This is probably a pretty general "before" invocation hook; * probably worth renaming it and adding an "after". * * @param req request which includes preauthorization parameters * @exception ServletException yes */ protected void checkPreAuthorization (HttpServletRequest req) throws ServletException { } /** * Add X.509 certificate extensions to the application. By default, * options are added to support interoperation with this servlet. * * @param X509Cert the certificate being modified (not yet signed) * @exception IOException yes */ protected void addCertExtensions (X509Cert subjectCert) throws IOException { // // XXX Add Navigator V3 client extensions; can compress // Netscape URLS with netscape-base-url. The idea is to // drive at least renewal and revocation through this // CA servlet. // // Ones directly supported by the servlet above: // netscape-cert-renewal-url ... for cert renewal // netscape-revocation-url ... for revocation check // // Ones supporting user interaction per Navigator policies: // netscape-ca-policy-url ... for CA's CPS // netscape-cert-type ... e.g. ssl client // netscape-comment ... user-meaningful info // // Verisign is another definer of extension fields. // } // XXX database ops needed: // - get a cert by ID (serial number) // - tell if an ID corresponds to a valid (and nonrevoked) cert // - get an enumeration of certs with their statuses // // - store a cert (key is ID; also, revocation flag) // - revoke a cert by ID // - renew a cert by ID (?how verify?) // // Build on sun.server.realm.Realm class. /** * This synchronizes CA data to persistent storage, such as a * local disk or remote backup media. */ public void sync () { // we're not persistent yet !! } /* * Parse and verify the SignedPublicKeyAndChallenge data as * produced by KEYGEN ... return the key. */ private X509Key getVerifiedKey (String challenge, String response) throws SignatureException, IOException, InvalidKeyException, NoSuchAlgorithmException { // // Navigator 3.0 returns the data in BASE64 encoding // BASE64Decoder decoder = new BASE64Decoder (); byte data [] = decoder.decodeBuffer (response); // // SignedPublicKeyAndChallenge ::= SEQUENCE { // publicKeyAndChallenge PublicKeyAndChallenge, // signatureAlgorithm AlgorithmIdentifier, // signature BIT STRING // } // DerInputStream in = new DerInputStream (data); DerValue spkac [] = in.getSequence (3); AlgorithmId sigAlg; byte sigBits []; if (spkac.length != 3) throw new SignatureException ("invalid SPKAC"); sigAlg = AlgorithmId.parse (spkac [1]); sigBits = spkac [2].getBitString (); // // PublicKeyAndChallenge ::= SEQUENCE { // spki SubjectPublicKeyInfo, // challenge IA5STRING // } // DerValue pkac []; X509Key retval; // in = spkac [0].toDerInputStream (); in = new DerInputStream (spkac [0].toByteArray ()); pkac = in.getSequence (2); if (pkac.length != 2) throw new SignatureException ("invalid PKAC"); if (!challenge.equals (pkac [1].getIA5String ())) throw new SignatureException ("bad challenge in PKAC"); retval = X509Key.parse (pkac [0]); // // Verify the signature .. shows the response was generated // by someone who knew the associated private key // Signature sig = Signature.getInstance (sigAlg.getName ()); sig.initVerify (retval); sig.update (spkac [0].toByteArray ()); if (sig.verify (sigBits)) return retval; else throw new SignatureException ("bad SPKAC Signature"); } /* * May need two databases, or at least a guarantee that CA can * always access the read-only one (and only the CA can corrupt * the one it updates during cert lifecycle operations) * * the database needs auditing capability. * * search merits query by part of DN etc, as well as full dump */ /* * Properties used to run the CA. */ private X509Certificate caCertChain []; private PrivateKey caPrivateKey; private int serial; }