//: "The contents of this file are subject to the Mozilla Public License
//: Version 1.1 (the "License"); you may not use this file except in
//: compliance with the License. You may obtain a copy of the License at
//: http://www.mozilla.org/MPL/
//:
//: Software distributed under the License is distributed on an "AS IS"
//: basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//: License for the specific language governing rights and limitations
//: under the License.
//:
//: The Original Code is Guanxi (http://www.guanxi.uhi.ac.uk).
//:
//: The Initial Developer of the Original Code is Alistair Young alistair@codebrane.com
//: All Rights Reserved.
//:
package org.guanxi.common.security;
import org.w3c.dom.Document;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.transforms.params.InclusiveNamespaces;
import org.guanxi.common.GuanxiException;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.security.*;
import java.security.cert.X509Certificate;
import java.io.*;
import java.util.Hashtable;
import java.util.Date;
import java.util.Random;
import java.util.Vector;
import java.math.BigInteger;
/**
* <font size=5><b></b></font>
*
* @author Alistair Young alistair@smo.uhi.ac.uk
*/
public class SecUtils {
static private SecUtils _instance = null;
static public SecUtils getInstance() {
if (_instance == null) {
synchronized(SecUtils.class) {
if (_instance == null) {
_instance = new SecUtils();
}
}
}
return _instance;
}
private SecUtils() {
org.apache.xml.security.Init.init();
}
public Document sign(SecUtilsConfig config, Document inDocToSign, String inElementToSign) throws GuanxiException {
String keystoreType = config.getKeystoreType();
String keystoreFile = config.getKeystoreFile();
String keystorePass = config.getKeystorePass();
String privateKeyAlias = config.getPrivateKeyAlias();
String privateKeyPass = config.getPrivateKeyPass();
String certificateAlias = config.getCertificateAlias();
try {
KeyStore ks = null;
ks = KeyStore.getInstance(keystoreType);
FileInputStream fis = null;
fis = new FileInputStream(keystoreFile);
ks.load(fis, keystorePass.toCharArray());
fis.close();
PrivateKey privateKey = null;
privateKey = (PrivateKey)ks.getKey(privateKeyAlias, privateKeyPass.toCharArray());
String keyType = privateKey.getAlgorithm();
if (keyType.equalsIgnoreCase("dsa")) keyType = XMLSignature.ALGO_ID_SIGNATURE_DSA;
if (keyType.equalsIgnoreCase("rsa")) keyType = XMLSignature.ALGO_ID_SIGNATURE_RSA;
XMLSignature sig = null;
sig = new XMLSignature(inDocToSign, "", keyType, Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
// For Shibboleth 1.2.1, <ds:Signature> must be the first element after the root
inDocToSign.getDocumentElement().insertBefore(sig.getElement(), inDocToSign.getDocumentElement().getFirstChild());
Transforms transforms = new Transforms(sig.getDocument());
/* First we have to strip away the signature element (it's not part of the
* signature calculations). The enveloped transform can be used for this.
*/
transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
/* Have to use a text node for compatibility with Internet2 Shibboleth SP 1.3
* otherwise the signature fails to verify. The correct way do add it is:
* transforms.item(1).getElement().appendChild(new InclusiveNamespaces(inDocToSign, "#default saml samlp ds code kind rw typens").getElement());
*/
transforms.item(1).getElement().appendChild(inDocToSign.createTextNode(new InclusiveNamespaces(inDocToSign, "#default saml samlp ds code kind rw typens").getInclusiveNamespaces()));
sig.addDocument(inElementToSign, transforms);
//Add in the KeyInfo for the certificate that we used the private key of
X509Certificate cert = null;
cert = (X509Certificate)ks.getCertificate(certificateAlias);
sig.addKeyInfo(cert);
sig.addKeyInfo(cert.getPublicKey());
sig.sign(privateKey);
}
catch(Exception e) {
throw new GuanxiException(e);
}
return inDocToSign;
}
public Document saml2Sign(SecUtilsConfig config, Document inDocToSign, String elementIDToSign) throws GuanxiException {
String keystoreType = config.getKeystoreType();
String keystoreFile = config.getKeystoreFile();
String keystorePass = config.getKeystorePass();
String privateKeyAlias = config.getPrivateKeyAlias();
String privateKeyPass = config.getPrivateKeyPass();
String certificateAlias = config.getCertificateAlias();
try {
KeyStore ks = null;
ks = KeyStore.getInstance(keystoreType);
FileInputStream fis = null;
fis = new FileInputStream(keystoreFile);
ks.load(fis, keystorePass.toCharArray());
fis.close();
PrivateKey privateKey = null;
privateKey = (PrivateKey)ks.getKey(privateKeyAlias, privateKeyPass.toCharArray());
String keyType = privateKey.getAlgorithm();
if (keyType.equalsIgnoreCase("dsa")) keyType = XMLSignature.ALGO_ID_SIGNATURE_DSA;
if (keyType.equalsIgnoreCase("rsa")) keyType = XMLSignature.ALGO_ID_SIGNATURE_RSA;
XMLSignature sig = null;
sig = new XMLSignature(inDocToSign, "", keyType, Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
// For SAML2, <ds:Signature> must be the first element after the Issuer
NodeList nodes = inDocToSign.getDocumentElement().getChildNodes();
Node issuerNode = null;
for (int c=0; c < nodes.getLength(); c++) {
issuerNode = nodes.item(c);
if (issuerNode.getLocalName() != null) {
if (issuerNode.getLocalName().equals("Issuer")) {
break;
}
}
}
inDocToSign.getDocumentElement().insertBefore(sig.getElement(), issuerNode.getNextSibling().getNextSibling());
Transforms transforms = new Transforms(sig.getDocument());
/* First we have to strip away the signature element (it's not part of the
* signature calculations). The enveloped transform can be used for this.
*/
transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
/* Have to use a text node for compatibility with Internet2 Shibboleth SP 1.3
* otherwise the signature fails to verify. The correct way do add it is:
* transforms.item(1).getElement().appendChild(new InclusiveNamespaces(inDocToSign, "#default saml samlp ds code kind rw typens").getElement());
*/
transforms.item(1).getElement().appendChild(inDocToSign.createTextNode(new InclusiveNamespaces(inDocToSign, "#default saml samlp ds code kind rw typens").getInclusiveNamespaces()));
if ((elementIDToSign != null) && (!elementIDToSign.equals(""))) {
sig.addDocument("#" + elementIDToSign, transforms);
}
else {
sig.addDocument("", transforms);
}
//Add in the KeyInfo for the certificate that we used the private key of
X509Certificate cert = null;
cert = (X509Certificate)ks.getCertificate(certificateAlias);
sig.addKeyInfo(cert);
sig.addKeyInfo(cert.getPublicKey());
sig.sign(privateKey);
}
catch(Exception e) {
throw new GuanxiException(e);
}
return inDocToSign;
}
public String encrypt(String data) {
try {
char[] hexChars ={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.reset();
md5.update(data.getBytes());
byte[] hashBytes = md5.digest();
String hex = "";
int msb;
int lsb = 0;
int i;
for (i = 0; i < hashBytes.length; i++) {
msb = ((int)hashBytes[i] & 0x000000FF) / 16;
lsb = ((int)hashBytes[i] & 0x000000FF) % 16;
hex = hex + hexChars[msb] + hexChars[lsb];
}
return(hex);
}
catch (NoSuchAlgorithmException e) {
return null;
}
}
/**
* Generates a self signed public/private key pair and puts them and the associated certificate in
* a KeyStore.
*
* @param cn The CN of the X509 containing the public key, e.g. "cn=guanxi_sp,ou=smo,o=uhi"
* @param keystoreFile The full path and name of the KeyStore to create or add the certificate to
* @param keystorePassword The password for the KeyStore
* @param privateKeyPassword The password for the private key associated with the public key certificate
* @param privateKeyAlias The alias under which the private key will be stored
* @param keyType The type of key, RSA or DSA
* @throws GuanxiException if an error occurred
*/
public void createSelfSignedKeystore(String cn, String keystoreFile, String keystorePassword,
String privateKeyPassword, String privateKeyAlias,
String keyType) throws GuanxiException {
try {
KeyStore ks = KeyStore.getInstance("JKS");
// Does the keystore exist?
File keyStore = new File(keystoreFile);
if (keyStore.exists()) {
FileInputStream fis = new FileInputStream(keystoreFile);
ks.load(fis, keystorePassword.toCharArray());
fis.close();
}
else
ks.load(null, null);
// Generate a new public/private key pair
KeyPairGenerator keyGen = null;
if (keyType.toLowerCase().equals("rsa")) {
keyGen = KeyPairGenerator.getInstance("RSA");
}
else if (keyType.toLowerCase().equals("dsa")) {
keyGen = KeyPairGenerator.getInstance("DSA");
}
keyGen.initialize(1024, new SecureRandom());
KeyPair keypair = keyGen.generateKeyPair();
PrivateKey privkey = keypair.getPrivate();
PublicKey pubkey = keypair.getPublic();
/* Set the attributes of the X509 Certificate that will contain the public key.
* This is a self signed certificate so the issuer and subject will be the same.
*/
Hashtable<DERObjectIdentifier, String> attrs = new Hashtable<DERObjectIdentifier, String>();
Vector<DERObjectIdentifier> ordering = new Vector<DERObjectIdentifier>();
ordering.add(X509Name.CN);
attrs.put(X509Name.CN, cn);
X509Name issuerDN = new X509Name(ordering, attrs);
X509Name subjectDN = new X509Name(ordering, attrs);
// Certificate valid from now
Date validFrom = new Date();
validFrom.setTime(validFrom.getTime() - (10 * 60 * 1000));
Date validTo = new Date();
validTo.setTime(validTo.getTime() + (20 * (24 * 60 * 60 * 1000)));
// Initialise the X509 Certificate information...
X509V3CertificateGenerator x509 = new X509V3CertificateGenerator();
if (keyType.toLowerCase().equals("rsa")) {
x509.setSignatureAlgorithm("SHA1withRSA");
}
else if (keyType.toLowerCase().equals("dsa")) {
x509.setSignatureAlgorithm("SHA1withDSA");
}
x509.setIssuerDN(issuerDN);
x509.setSubjectDN(subjectDN);
x509.setPublicKey(pubkey);
x509.setNotBefore(validFrom);
x509.setNotAfter(validTo);
x509.setSerialNumber(new BigInteger(128, new Random()));
// ...generate it...
X509Certificate[] cert = new X509Certificate[1];
cert[0] = x509.generate(privkey, "BC");
// ...and add the self signed certificate as the certificate chain
java.security.cert.Certificate[] chain = new java.security.cert.Certificate[1];
chain[0] = cert[0];
// Under the alias, store the X509 Certificate and it's public key...
ks.setKeyEntry(privateKeyAlias, privkey, privateKeyPassword.toCharArray(), cert);
// ...and the chain...
ks.setKeyEntry(privateKeyAlias, privkey, privateKeyPassword.toCharArray(), chain);
// ...and write the keystore to disk
FileOutputStream fos = new FileOutputStream(keystoreFile);
ks.store(fos, keystorePassword.toCharArray());
fos.close();
}
catch(Exception se) {
/* We'll end up here if a security manager is installed and it refuses us
* permission to add the BouncyCastle provider
*/
throw new GuanxiException(se);
}
}
/**
* Creates an empty truststore
*
* @param trustStoreFile Full path and name of the truststore to create
* @param trustStorePassword Password for the truststore
* @throws GuanxiException if an error occurred
*/
public void createTrustStore(String trustStoreFile, String trustStorePassword) throws GuanxiException {
try {
KeyStore trustStore = KeyStore.getInstance("JKS");
File truststoreFile = new File(trustStoreFile);
trustStore.load(null, null);
FileOutputStream fos = new FileOutputStream(truststoreFile);
trustStore.store(fos, trustStorePassword.toCharArray());
fos.close();
}
catch(Exception se) {
throw new GuanxiException(se);
}
}
}