/* * Copyright 2009 NCHOVY * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.krakenapps.ca.impl; import java.math.BigInteger; import java.net.MalformedURLException; import java.net.URL; import java.security.cert.X509Certificate; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArraySet; import org.krakenapps.api.PrimitiveConverter; import org.krakenapps.ca.CertEventListener; import org.krakenapps.ca.CertificateAuthority; import org.krakenapps.ca.CertificateMetadata; import org.krakenapps.ca.CertificateMetadataIterator; import org.krakenapps.ca.CertificateRequest; import org.krakenapps.ca.RevocationReason; import org.krakenapps.ca.RevokedCertificate; import org.krakenapps.ca.RevokedCertificateIterator; import org.krakenapps.ca.util.CertificateBuilder; import org.krakenapps.ca.util.CertificateExporter; import org.krakenapps.confdb.Config; import org.krakenapps.confdb.ConfigCollection; import org.krakenapps.confdb.ConfigDatabase; import org.krakenapps.confdb.ConfigIterator; import org.krakenapps.confdb.Predicate; import org.krakenapps.confdb.Predicates; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * See {@link http://www.bouncycastle.org/wiki/display/JA1/Home} * * @author xeraph * */ public class CertificateAuthorityImpl implements CertificateAuthority { private final Logger logger = LoggerFactory.getLogger(CertificateAuthorityImpl.class.getName()); private static final String[] sigAlgorithms = new String[] { "MD5withRSA", "MD5withRSA", "SHA1withRSA", "SHA224withRSA", "SHA256withRSA", "SHA384withRSA", "SHA512withRSA" }; /** * config database which contains "metadata", "certs", and "revoked" config * collections. */ private ConfigDatabase db; /** * authority name */ private String name; private CopyOnWriteArraySet<CertEventListener> listeners; public CertificateAuthorityImpl(ConfigDatabase db, String name) { this.db = db; this.name = name; listeners = new CopyOnWriteArraySet<CertEventListener>(); } @Override public String getName() { return name; } @Override public CertificateMetadata getRootCertificate() { ConfigCollection metadata = db.ensureCollection("metadata"); Config jks = metadata.findOne(Predicates.field("type", "jks")); if (jks == null) throw new IllegalStateException("jks not found for " + name); return PrimitiveConverter.parse(CertificateMetadata.class, jks.getDocument()); } @SuppressWarnings("unchecked") @Override public String getRootKeyPassword() { ConfigCollection metadata = db.ensureCollection("metadata"); Config c = metadata.findOne(Predicates.field("type", "rootpw")); if (c == null) throw new IllegalStateException("root key password not found"); Map<String, Object> m = (Map<String, Object>) c.getDocument(); return (String) m.get("password"); } @Override public URL getCrlDistPoint() { ConfigCollection metadata = db.ensureCollection("metadata"); Config c = metadata.findOne(Predicates.field("type", "crl")); if (c == null) return null; try { @SuppressWarnings("unchecked") Map<String, Object> m = (Map<String, Object>) c.getDocument(); return new URL((String) m.get("base_url")); } catch (MalformedURLException e) { // unreachable throw new IllegalStateException(e); } } @Override public void setCrlDistPoint(URL url) { String baseUrl = url.toString(); if (baseUrl.endsWith("/")) baseUrl = baseUrl.substring(0, baseUrl.length() - 1); ConfigCollection metadata = db.ensureCollection("metadata"); Config c = metadata.findOne(Predicates.field("type", "crl")); Map<String, Object> m = new HashMap<String, Object>(); m.put("type", "crl"); m.put("base_url", baseUrl); if (c == null) { metadata.add(m, "kraken-ca", "set CRL distribution point"); } else { c.setDocument(m); metadata.update(c, false, "kraken-ca", "updated CRL distribution point"); } } @Override public CertificateMetadataIterator getCertificateIterator() { return getCertificateIterator(null); } @Override public CertificateMetadataIterator getCertificateIterator(Predicate pred) { ConfigCollection certs = db.ensureCollection("certs"); ConfigIterator it = certs.find(pred); return new CertificateMetadataIterator(it); } @Override public List<CertificateMetadata> getCertificates(Predicate pred) { ConfigCollection certs = db.ensureCollection("certs"); List<CertificateMetadata> l = new LinkedList<CertificateMetadata>(); ConfigIterator it = certs.find(pred); try { while (it.hasNext()) { Config c = it.next(); CertificateMetadata cm = PrimitiveConverter.parse(CertificateMetadata.class, c.getDocument()); l.add(cm); } } finally { it.close(); } return l; } @Override public List<CertificateMetadata> getCertificates() { return getCertificates(null); } @Override public BigInteger getLastSerial() { ConfigCollection col = db.ensureCollection("metadata"); Config c = col.findOne(Predicates.field("type", "serial")); if (c == null) return new BigInteger("1"); CertSerial s = PrimitiveConverter.parse(CertSerial.class, c.getDocument()); return new BigInteger(s.serial); } @Override public BigInteger getNextSerial() { ConfigCollection col = db.ensureCollection("metadata"); Config c = col.findOne(Predicates.field("type", "serial")); if (c == null) { Object doc = PrimitiveConverter.serialize(new CertSerial()); col.add(doc, "kraken-ca", "init serial"); return new BigInteger("2"); } CertSerial s = PrimitiveConverter.parse(CertSerial.class, c.getDocument()); s.serial = new BigInteger(s.serial).add(new BigInteger("1")).toString(); c.setDocument(PrimitiveConverter.serialize(s)); col.update(c, false, "kraken-ca", "set next serial"); return new BigInteger(s.serial); } @Override public CertificateMetadata findCertificate(String field, String value) { ConfigCollection certs = db.ensureCollection("certs"); Config c = certs.findOne(Predicates.field(field, value)); if (c == null) return null; return PrimitiveConverter.parse(CertificateMetadata.class, c.getDocument()); } @Override public CertificateMetadata issueCertificate(CertificateRequest req) throws Exception { ConfigCollection metadata = db.ensureCollection("metadata"); Config jks = metadata.findOne(Predicates.field("type", "jks")); if (jks == null) throw new IllegalStateException("ca not found for " + name); CertificateMetadata cm = PrimitiveConverter.parse(CertificateMetadata.class, jks.getDocument()); req.setSerial(getNextSerial()); req.setIssuerDn(cm.getSubjectDn()); req.setIssuerKey(cm.getPrivateKey(getRootKeyPassword())); URL dist = getCrlDistPoint(); if (dist != null) { dist = new URL(dist + "/ca/crl/" + name + "?serial=" + req.getSerial().toString()); req.setCrlUrl(dist); } // check availability of signature algorithm validateSignatureAlgorithm(req.getSignatureAlgorithm()); // generate cert X509Certificate caCert = cm.getCertificate(); X509Certificate cert = CertificateBuilder.createCertificate(req); byte[] pkcs12 = CertificateExporter.exportPkcs12(cert, req.getKeyPair(), req.getKeyPassword(), caCert); cm = new CertificateMetadata(); cm.setType("pkcs12"); cm.setSerial(req.getSerial().toString()); cm.setSubjectDn(req.getSubjectDn()); cm.setNotBefore(req.getNotBefore()); cm.setNotAfter(req.getNotAfter()); cm.setBinary(pkcs12); ConfigCollection certs = db.ensureCollection("certs"); Object c = PrimitiveConverter.serialize(cm); certs.add(c, "kraken-ca", "issued certificate for " + req.getSubjectDn()); logger.info("kraken ca: generated new certificate [{}]", cert.getSubjectX500Principal().getName()); for (CertEventListener listener : listeners) { try { listener.onIssued(this, cm); } catch (Throwable t) { logger.error("kraken ca: certificate issue callback should not throw any exception", t); } } return cm; } @Override public void importCertificate(CertificateMetadata cm) { ConfigCollection certs = db.ensureCollection("certs"); Object c = PrimitiveConverter.serialize(cm); certs.add(c, "kraken-ca", "import certificate, " + cm.getSubjectDn()); logger.info("kraken ca: import new certificate [{}]", cm.getSubjectDn()); for (CertEventListener listener : listeners) { try { listener.onIssued(this, cm); } catch (Throwable t) { logger.error("kraken ca: certificate issue callback should not throw any exception", t); } } } public static void validateSignatureAlgorithm(String algorithm) { for (int i = 0; i < sigAlgorithms.length; i++) if (sigAlgorithms[i].equals(algorithm)) return; throw new IllegalArgumentException("invalid signature algorithm: " + algorithm); } @Override public void revoke(CertificateMetadata cm) { revoke(cm, RevocationReason.Unspecified); } @Override public void revoke(CertificateMetadata cm, RevocationReason reason) { ConfigCollection revoked = db.ensureCollection("revoked"); Config c = revoked.findOne(Predicates.field("serial", cm.getSerial())); if (c != null) throw new IllegalStateException("already revoked: serial " + cm.getSerial()); RevokedCertificate r = new RevokedCertificate(cm.getSerial(), new Date(), reason); revoked.add(PrimitiveConverter.serialize(r), "kraken-ca", "revoked " + cm); for (CertEventListener listener : listeners) { try { listener.onRevoked(this, cm, reason); } catch (Throwable t) { logger.error("kraken ca: certificate revoke callback should not throw any exception", t); } } } @Override public RevokedCertificate getRevokedCertificate(String serial) { ConfigCollection revoked = db.ensureCollection("revoked"); Config config = revoked.findOne(Predicates.field("serial", serial)); if (config == null) return null; RevokedCertificate rc = PrimitiveConverter.parse(RevokedCertificate.class, config.getDocument()); return rc; } @Override public List<RevokedCertificate> getRevokedCertificates() { ConfigCollection revoked = db.ensureCollection("revoked"); ConfigIterator it = revoked.findAll(); List<RevokedCertificate> l = new LinkedList<RevokedCertificate>(); try { while (it.hasNext()) { Config c = it.next(); RevokedCertificate rc = PrimitiveConverter.parse(RevokedCertificate.class, c.getDocument()); l.add(rc); } } finally { it.close(); } return l; } @Override public RevokedCertificateIterator getRevokedCertificateIterator() { return getRevokedCertificateIterator(null); } @Override public RevokedCertificateIterator getRevokedCertificateIterator(Predicate pred) { ConfigCollection revoked = db.ensureCollection("revoked"); ConfigIterator it = revoked.find(pred); return new RevokedCertificateIterator(it); } @Override public String toString() { return name + ": " + getRootCertificate().getSubjectDn(); } private static class CertSerial { @SuppressWarnings("unused") private final String type = "serial"; private String serial = "2"; public CertSerial() { } } @Override public void addListener(CertEventListener listener) { if (listener == null) throw new IllegalArgumentException("listener should be not null"); listeners.add(listener); } @Override public void removeListener(CertEventListener listener) { if (listener == null) throw new IllegalArgumentException("listener should be not null"); listeners.remove(listener); } }