Commit 642d04cf authored by Patrick Dowler's avatar Patrick Dowler Committed by GitHub
Browse files

Merge pull request #5 from pdowler/master

initial as-is source release of cadc-cert-gen
parents abe55b51 01b11a8a
......@@ -2,4 +2,4 @@ language: java
jdk:
- openjdk7
- oraclejdk8
script: for mod in cadc-cdp cadc-cdp-server; do cd $mod; gradle assemble check install || break -1; cd ..; done
script: for mod in cadc-cdp cadc-cdp-server cadc-cert-gen; do cd $mod; gradle assemble check install || break -1; cd ..; done
# cadc-cert-gen
Simple tool to enable someone to use their cadc-cdp-server based CDP service as an internal CA to
provide local user certificates.
Known Issues:
- several hard-coded settings and behaviours make this unusable until some refactoring is done
- shares knowledge of back-end DB implementation used in cadc-cdp-server to query the RDBMS table
to find expiring certificates
- only looks for expiring certificates that match a hard-coded CADC internal CA distinguished name pattern
plugins {
id 'java'
id 'maven'
id 'maven-publish'
id 'application'
}
repositories {
jcenter()
mavenLocal()
}
sourceCompatibility = 1.7
group = 'org.opencadc'
version = '1.0'
mainClassName = 'ca.nrc.cadc.cert.Main'
dependencies {
compile 'log4j:log4j:1.+'
compile 'org.opencadc:cadc-log:1.+'
compile 'org.opencadc:cadc-util:1.+'
compile 'org.opencadc:cadc-cdp:1.+'
runtime 'net.sourceforge.jtds:jtds:1.+'
}
/*
************************************************************************
**** C A N A D I A N A S T R O N O M Y D A T A C E N T R E *****
*
* (c) 2015. (c) 2015.
* National Research Council Conseil national de recherches
* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
* All rights reserved Tous droits reserves
*
* NRC disclaims any warranties Le CNRC denie toute garantie
* expressed, implied, or statu- enoncee, implicite ou legale,
* tory, of any kind with respect de quelque nature que se soit,
* to the software, including concernant le logiciel, y com-
* without limitation any war- pris sans restriction toute
* ranty of merchantability or garantie de valeur marchande
* fitness for a particular pur- ou de pertinence pour un usage
* pose. NRC shall not be liable particulier. Le CNRC ne
* in any event for any damages, pourra en aucun cas etre tenu
* whether direct or indirect, responsable de tout dommage,
* special or general, consequen- direct ou indirect, particul-
* tial or incidental, arising ier ou general, accessoire ou
* from the use of the software. fortuit, resultant de l'utili-
* sation du logiciel.
*
*
* @author adriand
*
* @version $Revision: $
*
*
**** C A N A D I A N A S T R O N O M Y D A T A C E N T R E *****
************************************************************************
*/
package ca.nrc.cadc.cert;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.security.PrivilegedAction;
import java.security.Security;
import org.apache.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import ca.nrc.cadc.util.ArgumentMap;
import ca.nrc.cadc.util.StringUtil;
public abstract class AbstractCertGenAction implements PrivilegedAction<Object>
{
static final File SERVOPS_PEM_FILE =
new File(System.getProperty("user.home") + "/.pub/proxy.pem");
private static Logger LOGGER = Logger
.getLogger(AbstractCertGenAction.class);
public static final URI CRED_SERVICE_ID =
URI.create("ivo://cadc.nrc.ca/cred");
protected String server = "SYBASE"; // default server
protected String database = "archive"; // default database
protected int expiring;
protected String userid;
public boolean init(final ArgumentMap argMap) throws IOException
{
String expiringString = argMap.getValue(Main.ARG_EXPIRING);
String userIDString = argMap.getValue(Main.ARG_USERID);
if (expiringString == null && userIDString == null)
{
LOGGER.error("One of " + Main.ARG_EXPIRING + " or " +
Main.ARG_USERID + " must be provided.");
return false;
}
if (expiringString != null && userIDString != null)
{
LOGGER.error("Only one of " + Main.ARG_EXPIRING + " or " +
Main.ARG_USERID + " must be provided.");
return false;
}
if (expiringString != null)
{
expiring = parseExpire(argMap);
}
else
{
userid = userIDString;
}
if (Security.getProvider("BC") == null)
{
Security.addProvider(new BouncyCastleProvider());
}
return true;
}
private int parseExpire(ArgumentMap argMap)
{
int expire = 0;
String expireStr = argMap.getValue(Main.ARG_EXPIRING);
if (StringUtil.hasText(expireStr))
{
try
{
expire = Integer.parseInt(expireStr);
}
catch (NumberFormatException e)
{
LOGGER.debug(Main.ARG_EXPIRING + " must be an integer");
LOGGER.debug("Using the default value instead");
}
}
if (expire == 0)
expire = Main.DEFAULT_EXPIRE;
return expire;
}
@Override
public String run()
{
try
{
runCommand();
return null;
}
catch (Exception e)
{
LOGGER.debug("run - ERROR \n" + e.getMessage());
throw new RuntimeException("execution error", e);
}
}
protected void msg(String msg)
{
Main.msg(msg);
}
protected abstract void runCommand() throws Exception;
}
/*
************************************************************************
**** C A N A D I A N A S T R O N O M Y D A T A C E N T R E *****
*
* (c) 2011. (c) 2011.
* National Research Council Conseil national de recherches
* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
* All rights reserved Tous droits reserves
*
* NRC disclaims any warranties Le CNRC denie toute garantie
* expressed, implied, or statu- enoncee, implicite ou legale,
* tory, of any kind with respect de quelque nature que se soit,
* to the software, including concernant le logiciel, y com-
* without limitation any war- pris sans restriction toute
* ranty of merchantability or garantie de valeur marchande
* fitness for a particular pur- ou de pertinence pour un usage
* pose. NRC shall not be liable particulier. Le CNRC ne
* in any event for any damages, pourra en aucun cas etre tenu
* whether direct or indirect, responsable de tout dommage,
* special or general, consequen- direct ou indirect, particul-
* tial or incidental, arising ier ou general, accessoire ou
* from the use of the software. fortuit, resultant de l'utili-
* sation du logiciel.
*
*
* @author adriand
*
* @version $Revision: $
*
*
**** C A N A D I A N A S T R O N O M Y D A T A C E N T R E *****
************************************************************************
*/
package ca.nrc.cadc.cert;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import javax.security.auth.x500.X500Principal;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure;
import org.springframework.jdbc.core.JdbcTemplate;
import ca.nrc.cadc.auth.AuthenticationUtil;
import ca.nrc.cadc.auth.HttpPrincipal;
import ca.nrc.cadc.auth.SSLUtil;
import ca.nrc.cadc.auth.X509CertificateChain;
import ca.nrc.cadc.cred.CertUtil;
import ca.nrc.cadc.cred.client.CredClient;
import ca.nrc.cadc.net.ResourceNotFoundException;
import ca.nrc.cadc.util.ArgumentMap;
public class CertGenAction extends DbCertGenAction
{
private static final Logger LOGGER = Logger.getLogger(CertGenAction.class);
private int lifetime = 365; // 365 days
// CADC specific fields of the DN
public static final String CADC_DN = "ou=cadc,o=hia,c=ca";
// Certificate of the signing authority
X509CertificateChain signer;
boolean dryRun = true;
@Override
public boolean init(final ArgumentMap argMap) throws IOException
{
if (!super.init(argMap))
return false;
if (Security.getProvider("BC") == null)
{
Security.addProvider(new BouncyCastleProvider());
}
String signingKeyStr = argMap.getValue(Main.ARG_SIGNED_CERT);
if (signingKeyStr == null)
{
LOGGER.error(Main.ARG_SIGNED_CERT
+ " argument missing");
return false;
}
File signingKeyFile = new File(signingKeyStr);
try
{
this.signer = SSLUtil.readPemCertificateAndKey(signingKeyFile);
}
catch (Exception ex)
{
throw new RuntimeException("failed to read "
+ Main.ARG_SIGNED_CERT + " " + signingKeyStr, ex);
}
dryRun = argMap.isSet(Main.ARG_DRYRUN);
return super.init(argMap);
}
@Override
protected void runCommand() throws Exception
{
LOGGER.debug("Entering generateCertificate");
boolean result = true;
if (userid != null)
{
// create a cert for a single user
HttpPrincipal useridPrincipal = new HttpPrincipal(userid);
X500Principal userDN = super.getCADCUserDN(useridPrincipal);
LOGGER.debug("About to create certificate for user " + userid + " with DN " + userDN.toString());
generateCertificate(userDN);
msg("New user DN: " + userDN.toString());
}
else
{
// renew certs for all users who's are about to expire
int count = 0;
X500Principal[] userDNs = getExpiringCADC(super.expiring);
if (dryRun)
{
for (X500Principal userDN : userDNs)
{
msg("expiring: " + userDN.getName());
}
count = (userDNs == null ? 0 : userDNs.length);
msg("Found " + count + " certificates that will expire " +
"within " + super.expiring + " days.");
}
else
{
for (X500Principal userDN : userDNs)
{
try
{
generateCertificate(userDN);
}
catch (Exception e)
{
}
count++;
}
msg("Renewed " + count + " certificates");
}
}
}
/**
* Generates a certificate signed with subject's credentials (CADC private
* key) and persists them.
*
* @param userDN The X500Principal to generate for.
* @return X509CertificateChain generated and signed certificate chain
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws NoSuchProviderException
* @throws SignatureException
* @throws IllegalStateException
* @throws IOException
* @throws CertificateException
* @throws CertificateNotYetValidException
* @throws CertificateExpiredException
*/
private void generateCertificate(final X500Principal userDN)
throws NoSuchAlgorithmException, InvalidKeyException,
NoSuchProviderException, SignatureException,
IllegalStateException, CertificateException, IOException,
ResourceNotFoundException
{
if (!AuthenticationUtil.canonizeDistinguishedName(
userDN.getName()).contains(CADC_DN))
{
throw new IllegalArgumentException(
"Wrong o, ou, or c fields in user DN: " + userDN);
}
LOGGER.debug("Generate private key & CSR");
CredClient client = new CredClient(CRED_SERVICE_ID);
try
{
client.deleteResource(userDN); // remove old CSR
}
catch (ResourceNotFoundException ignore)
{
}
client.createResoure(userDN); // generate a new CSR with current specs
String encodedCSR = client.getEncodedCSR(userDN);
if (encodedCSR == null)
{
// shouldn't happen
throw new RuntimeException("No corresponding CSR found on the server");
}
PEMReader reader = new PEMReader(new StringReader(encodedCSR));
PKCS10CertificationRequest csr = (PKCS10CertificationRequest) reader
.readObject();
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
//certGen.setIssuerDN(caCert.getSubjectX500Principal());
certGen.setIssuerDN(signer.getPrincipal());
certGen.setSubjectDN(userDN);
// set validity
GregorianCalendar date = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
date.add(Calendar.MINUTE, -5); // Allow for a five minute clock
// skew here.
certGen.setNotBefore(date.getTime());
// If hours = 0, then cert lifetime is set to that of user cert
date.add(Calendar.MINUTE, 5);
date.add(Calendar.HOUR, lifetime * 24);
certGen.setNotAfter(date.getTime());
certGen.setPublicKey(csr.getPublicKey());
certGen.setSignatureAlgorithm(CertUtil.DEFAULT_SIGNATURE_ALGORITHM);
//certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
// new AuthorityKeyIdentifierStructure(caCert));
certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
new AuthorityKeyIdentifierStructure(signer.getChain()[0]));
// no extensions, at least for now
LOGGER.debug("Generate certificate");
//X509Certificate cert = certGen.generate(signingKey, "BC");
X509Certificate cert = certGen.generate(signer.getPrivateKey(), "BC");
// build chain with all but the last cert (assumed to be a CA)
// if signer is the CA, this creates a [1] and for loop does nothing
X509Certificate[] chain = new X509Certificate[signer.getChain().length];
chain[0] = cert;
System.arraycopy(signer.getChain(), 0, chain, 1,
signer.getChain().length - 1);
LOGGER.debug("Persisting certificate for " + userDN
.getName() + " chain length: " + chain.length);
client.putSignedCert(chain);
String dn = userDN.getName();
String noWhitespaceDN = dn.replaceAll("\\s","");
msg("Generated certificate for " + noWhitespaceDN);
}
private X500Principal[] getExpiringCADC(int expire)
{
// @formatter:off
String query = "select canon_dn"
+ " from "
+ " archive.dbo.x509_certificates "
+ " where "
+ " canon_dn like 'cn=%&____,ou=cadc,o=hia,c=ca' escape '&' "
+ " and datediff(dd, current_date(), exp_date) < ? ";
// @formatter:on
JdbcTemplate jdbc = new JdbcTemplate(ds);
@SuppressWarnings("unchecked")
List<String> rsList = (List<String>) jdbc.queryForList(query,
new Object[]{
expire}, String.class);
X500Principal[] result = new X500Principal[rsList.size()];
Iterator<String> it = rsList.iterator();
for (int i = 0; i < result.length; i++)
{
result[i] = new X500Principal(it.next());
}
return result;
}
}
/*
************************************************************************
**** C A N A D I A N A S T R O N O M Y D A T A C E N T R E *****
*
* (c) 2011. (c) 2011.
* National Research Council Conseil national de recherches
* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
* All rights reserved Tous droits reserves
*
* NRC disclaims any warranties Le CNRC denie toute garantie
* expressed, implied, or statu- enoncee, implicite ou legale,
* tory, of any kind with respect de quelque nature que se soit,
* to the software, including concernant le logiciel, y com-
* without limitation any war- pris sans restriction toute
* ranty of merchantability or garantie de valeur marchande
* fitness for a particular pur- ou de pertinence pour un usage
* pose. NRC shall not be liable particulier. Le CNRC ne
* in any event for any damages, pourra en aucun cas etre tenu
* whether direct or indirect, responsable de tout dommage,
* special or general, consequen- direct ou indirect, particul-
* tial or incidental, arising ier ou general, accessoire ou
* from the use of the software. fortuit, resultant de l'utili-
* sation du logiciel.
*
*
* @author adriand
*
* @version $Revision: $
*
*
**** C A N A D I A N A S T R O N O M Y D A T A C E N T R E *****
************************************************************************
*/
package ca.nrc.cadc.cert;
import java.io.IOException;
import javax.security.auth.x500.X500Principal;
import javax.sql.DataSource;
import org.apache.log4j.Logger;
import org.springframework.jdbc.core.JdbcTemplate;
import ca.nrc.cadc.auth.HttpPrincipal;
import ca.nrc.cadc.db.ConnectionConfig;
import ca.nrc.cadc.db.DBConfig;
import ca.nrc.cadc.db.DBUtil;
import ca.nrc.cadc.util.ArgumentMap;
/**
* Represents a AbstractCertGenAction that needs DB connections
*/
public abstract class DbCertGenAction extends AbstractCertGenAction
{
private static Logger LOGGER = Logger
.getLogger(DbCertGenAction.class);
// datasource to the database
protected DataSource ds;
protected static final String TMP_TABLE = "#tmptable";
public static final String GENERATE_DN_Q = "select dbo.genDN(?)";
@Override
public boolean init(final ArgumentMap argMap) throws IOException
{
if (!super.init(argMap))
{
return false;
}
initDbConnection(argMap);
return true;
}
/**
* Returns DN of a cadc user
*
* @param userId The HTTP Principal to get the DN for.
* @return X500Principal, or null.
*/
protected X500Principal getCADCUserDN(HttpPrincipal userId)
{
final JdbcTemplate jdbc = new JdbcTemplate(ds);
@SuppressWarnings("unchecked")
final String userDN = (String) jdbc.queryForObject(GENERATE_DN_Q,
new Object[]{
userId.getName()}, String.class);
return (userDN == null) ? null : new X500Principal(userDN);
}
private void initDbConnection(ArgumentMap argMap) throws IOException
{
if (argMap.getValue(Main.ARG_DB) != null)