/*
* Copyright 2012 Future Systems
*
* 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.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.Validate;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.krakenapps.api.PrimitiveConverter;
import org.krakenapps.ca.CertEventListener;
import org.krakenapps.ca.CertificateAuthority;
import org.krakenapps.ca.CertificateAuthorityListener;
import org.krakenapps.ca.CertificateAuthorityService;
import org.krakenapps.ca.CertificateMetadata;
import org.krakenapps.ca.CertificateRequest;
import org.krakenapps.ca.RevocationReason;
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.ConfigService;
import org.krakenapps.confdb.ConfigTransaction;
import org.krakenapps.confdb.Predicates;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(name = "certificate-authority")
@Provides
public class CertificateAuthorityServiceImpl implements CertificateAuthorityService {
private static final String DBNAME_PREFIX = "kraken-ca-";
private final Logger logger = LoggerFactory.getLogger(CertificateAuthorityServiceImpl.class.getName());
private static File baseDir = new File(System.getProperty("kraken.data.dir"), "kraken-ca");
// install JCE provider
static {
if (Security.getProvider("BC") == null)
Security.addProvider(new BouncyCastleProvider());
}
@Requires
private ConfigService conf;
private ConcurrentMap<String, CertificateAuthority> authorities;
private CopyOnWriteArraySet<CertificateAuthorityListener> listeners;
private CertificateListener certListener;
public CertificateAuthorityServiceImpl() {
authorities = new ConcurrentHashMap<String, CertificateAuthority>();
listeners = new CopyOnWriteArraySet<CertificateAuthorityListener>();
certListener = new CertificateListener();
}
@Validate
public void start() {
initializeCache();
}
@Invalidate
public void stop() {
for (CertificateAuthority authority : getAuthorities())
authority.removeListener(certListener);
}
private void initializeCache() {
ConfigDatabase master = conf.ensureDatabase("kraken-ca");
ConfigCollection col = master.ensureCollection("authority");
ConfigIterator it = col.findAll();
authorities.clear();
try {
while (it.hasNext()) {
Config c = it.next();
@SuppressWarnings("unchecked")
Map<String, Object> doc = (Map<String, Object>) c.getDocument();
String name = (String) doc.get("name");
ConfigDatabase db = conf.ensureDatabase(DBNAME_PREFIX + name);
CertificateAuthority authority = new CertificateAuthorityImpl(db, name);
authority.addListener(certListener);
authorities.put(authority.getName(), authority);
}
} finally {
it.close();
}
}
@Override
public List<CertificateAuthority> getAuthorities() {
return new ArrayList<CertificateAuthority>(authorities.values());
}
@Override
public CertificateAuthority getAuthority(String name) {
return authorities.get(name);
}
@Override
public CertificateAuthority createAuthority(String name, CertificateRequest req) throws Exception {
validate(req);
// generate ca cert
Map<String, Object> doc = new HashMap<String, Object>();
doc.put("name", name);
doc.put("created_at", new Date());
ConfigDatabase db = conf.ensureDatabase(DBNAME_PREFIX + name);
ConfigDatabase ca = conf.ensureDatabase("kraken-ca");
ConfigCollection col = ca.ensureCollection("authority");
col.add(doc);
CertificateAuthority authority = new CertificateAuthorityImpl(db, name);
authorities.put(authority.getName(), authority);
// save pfx
X509Certificate root = CertificateBuilder.createCertificate(req);
byte[] jks = CertificateExporter.exportJks(root, req.getKeyPair(), req.getKeyPassword(), root);
CertificateMetadata cm = new CertificateMetadata();
cm.setType("jks");
cm.setSerial(req.getSerial().toString());
cm.setSubjectDn(req.getSubjectDn());
cm.setNotBefore(req.getNotBefore());
cm.setNotAfter(req.getNotAfter());
cm.setBinary(jks);
// set authority metadata
ConfigCollection metadata = db.ensureCollection("metadata");
Object cert = PrimitiveConverter.serialize(cm);
Map<String, Object> pw = newRootKeyPassword(req);
// commit metadata transaction
ConfigTransaction xact = db.beginTransaction(5000);
try {
metadata.add(xact, cert);
metadata.add(xact, pw);
xact.commit("kraken-ca", "added root certificate and password");
} catch (Exception e) {
xact.rollback();
}
// set master metadata
metadata = ca.ensureCollection("metadata");
metadata.add(doc, "kraken-ca", "added " + name + " authority");
authority.addListener(certListener);
for (CertificateAuthorityListener listener : listeners) {
try {
listener.onCreateAuthority(authority);
} catch (Throwable t) {
logger.error("kraken ca: create authority callback should not throw any exception", t);
}
}
return authority;
}
private Map<String, Object> newRootKeyPassword(CertificateRequest req) {
Map<String, Object> pw = new HashMap<String, Object>();
pw.put("type", "rootpw");
pw.put("password", req.getKeyPassword());
return pw;
}
private void validate(CertificateRequest req) {
// check availability of signature algorithm
CertificateAuthorityImpl.validateSignatureAlgorithm(req.getSignatureAlgorithm());
if (req.getKeyPassword() == null)
throw new IllegalArgumentException("ca key password should be not null");
}
@Override
public void removeAuthority(String name) {
CertificateAuthority authority = authorities.remove(name);
if (authority == null)
return;
conf.dropDatabase(DBNAME_PREFIX + authority.getName());
ConfigDatabase ca = conf.ensureDatabase("kraken-ca");
ConfigCollection col = ca.ensureCollection("authority");
Config c = col.findOne(Predicates.field("name", name));
if (c != null) {
col.remove(c, false, "kraken-ca", "removed authority: " + name);
for (CertificateAuthorityListener listener : listeners) {
try {
listener.onRemoveAuthority(name);
} catch (Throwable t) {
logger.error("kraken ca: remove authority callback should not throw any exception", t);
}
}
}
}
@Override
public void addListener(CertificateAuthorityListener listener) {
if (listener == null)
throw new IllegalStateException("certificate authority listener should be not null");
listeners.add(listener);
}
@Override
public void removeListener(CertificateAuthorityListener listener) {
if (listener == null)
throw new IllegalStateException("certificate authority listener should be not null");
listeners.remove(listener);
}
@Override
public CertificateAuthority importAuthority(String name, InputStream is) throws IOException {
File exportFile = File.createTempFile(name, ".cdb", baseDir);
OutputStream os = null;
try {
os = new FileOutputStream(exportFile);
CertificateAuthorityFormatter.convertToInternalFormat(is, os);
} catch (ParseException e) {
throw new IOException(e);
} finally {
if (os != null)
try {
os.close();
} catch (IOException e) {
}
}
InputStream cdbIs = null;
ConfigDatabase db = conf.getDatabase(DBNAME_PREFIX + name);
if (db != null)
throw new IllegalStateException("authority [" + name + "] already exists");
db = conf.createDatabase(DBNAME_PREFIX + name);
try {
cdbIs = new FileInputStream(exportFile);
db.importData(cdbIs);
} finally {
if (cdbIs != null)
try {
cdbIs.close();
} catch (IOException e) {
}
exportFile.delete();
}
CertificateAuthority authority = new CertificateAuthorityImpl(db, name);
authorities.put(name, authority);
authority.addListener(certListener);
for (CertificateAuthorityListener listener : listeners) {
try {
listener.onImportAuthority(authority);
} catch (Throwable t) {
logger.error("kraken ca: import authority callback should not throw any exception", t);
}
}
ConfigDatabase ca = conf.ensureDatabase("kraken-ca");
ConfigCollection metadata = ca.ensureCollection("metadata");
Map<String, Object> doc = new HashMap<String, Object>();
doc.put("name", name);
doc.put("created_at", new Date());
metadata.add(doc, "kraken-ca", "added " + name + " authority");
return authority;
}
@Override
public void exportAuthority(String name, OutputStream os) throws IOException {
CertificateAuthorityFormatter.exportAuthority(getAuthority(name), os);
}
private class CertificateListener implements CertEventListener {
@Override
public void onRevoked(CertificateAuthority ca, CertificateMetadata cm, RevocationReason reason) {
for (CertificateAuthorityListener listener : listeners) {
try {
listener.onRevokeCert(ca, cm, reason);
} catch (Throwable t) {
logger.error("kraken ca: certificate revoke callback should not throw any exception", t);
}
}
}
@Override
public void onIssued(CertificateAuthority ca, CertificateMetadata cm) {
for (CertificateAuthorityListener listener : listeners) {
try {
listener.onIssueCert(ca, cm);
} catch (Throwable t) {
logger.error("kraken ca: certificate issue callback should not throw any exception", t);
}
}
}
}
}