/*
* Copyright 2010 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.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.Calendar;
import java.util.Date;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.krakenapps.api.PathAutoCompleter;
import org.krakenapps.api.Script;
import org.krakenapps.api.ScriptArgument;
import org.krakenapps.api.ScriptContext;
import org.krakenapps.api.ScriptUsage;
import org.krakenapps.ca.CertificateAuthority;
import org.krakenapps.ca.CertificateAuthorityService;
import org.krakenapps.ca.CertificateMetadata;
import org.krakenapps.ca.CertificateRequest;
import org.krakenapps.ca.RevocationReason;
import org.krakenapps.ca.RevokedCertificate;
import org.krakenapps.ca.util.CertificateExporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CertificateAuthorityScript implements Script {
static {
if (Security.getProvider("BC") == null)
Security.addProvider(new BouncyCastleProvider());
}
private final Logger logger = LoggerFactory.getLogger(CertificateAuthorityScript.class.getName());
private CertificateAuthorityService ca;
private ScriptContext context;
private static final String[] sigAlgorithms = new String[] { "MD2withRSA", "MD5withRSA", "SHA1withRSA", "SHA224withRSA",
"SHA256withRSA", "SHA384withRSA", "SHA512withRSA" };
public CertificateAuthorityScript(CertificateAuthorityService ca) {
this.ca = ca;
}
@Override
public void setScriptContext(ScriptContext context) {
this.context = context;
}
@ScriptUsage(description = "import certificate authority", arguments = {
@ScriptArgument(name = "name", type = "string", description = "authority name"),
@ScriptArgument(name = "file path", type = "string", description = "import file path", autocompletion = PathAutoCompleter.class) })
public void importAuthority(String[] args) {
InputStream is = null;
try {
File dir = (File) context.getSession().getProperty("dir");
File importFile = canonicalize(dir, args[1]);
if (!importFile.exists()) {
context.println("file does not exists: " + importFile.getAbsolutePath());
return;
}
if (!importFile.isFile()) {
context.println("invalid file: " + importFile.getAbsolutePath());
return;
}
if (!importFile.canRead()) {
context.println("cannot read file, check read permission: " + importFile.getAbsolutePath());
return;
}
is = new FileInputStream(importFile);
ca.importAuthority(args[0], is);
} catch (IOException e) {
logger.error("kraken ca: cannot import authority", e);
context.println(e.getMessage());
} finally {
if (is != null)
try {
is.close();
} catch (IOException e) {
}
}
}
@ScriptUsage(description = "export certificate authority", arguments = {
@ScriptArgument(name = "name", type = "string", description = "authority name"),
@ScriptArgument(name = "file path", type = "string", description = "export file path", autocompletion = PathAutoCompleter.class) })
public void exportAuthority(String[] args) {
OutputStream os = null;
try {
File dir = (File) context.getSession().getProperty("dir");
File exportFile = canonicalize(dir, args[1]);
if (exportFile.exists()) {
context.println("file already exists: " + exportFile.getAbsolutePath());
return;
}
os = new FileOutputStream(exportFile);
ca.exportAuthority(args[0], os);
} catch (IOException e) {
logger.error("kraken-ca: cannot export authority", e);
} finally {
if (os != null)
try {
os.close();
} catch (IOException e) {
}
}
}
private File canonicalize(File dir, String path) {
if (path.startsWith("/"))
return new File(path);
else
return new File(dir, path);
}
@ScriptUsage(description = "export certificate", arguments = {
@ScriptArgument(name = "authority", type = "string", description = "authority name"),
@ScriptArgument(name = "crl base url", type = "string", description = "new crl distribution point base url", optional = true) })
public void crlDistPoint(String[] args) {
String authorityName = args[0];
CertificateAuthority authority = ca.getAuthority(authorityName);
if (authority == null) {
context.println("authority not found");
return;
}
try {
if (args.length > 1) {
authority.setCrlDistPoint(new URL(args[1]));
context.print("set ");
}
} catch (MalformedURLException e) {
context.println("invalid CRL base URL: " + args[1]);
return;
}
if (authority.getCrlDistPoint() == null) {
context.println("not set");
return;
}
context.println(authority.getCrlDistPoint());
}
@ScriptUsage(description = "export certificate", arguments = {
@ScriptArgument(name = "authority", type = "string", description = "authority name"),
@ScriptArgument(name = "serial", type = "string", description = "serial number") })
public void export(String[] args) throws InterruptedException {
String authorityName = args[0];
CertificateAuthority authority = ca.getAuthority(authorityName);
if (authority == null) {
context.println("authority not found");
return;
}
String serial = args[1];
CertificateMetadata cm = authority.findCertificate("serial", serial);
if (cm == null) {
context.println("certificate not found");
return;
}
File dir = (File) context.getSession().getProperty("dir");
String ext = cm.getType();
if (ext.equals("pkcs12"))
ext = "pfx";
File pfx = new File(dir, parseCN(cm.getSubjectDn()) + "." + ext);
RandomAccessFile f = null;
try {
f = new RandomAccessFile(pfx, "rw");
f.write(cm.getBinary());
context.println("exported pfx file to " + pfx.getAbsolutePath());
} catch (Exception e) {
context.println("io failed: " + e.getMessage());
logger.error("kraken ca: export failed", e);
} finally {
if (f != null) {
try {
f.close();
} catch (IOException e) {
}
}
}
}
private String parseCN(String dn) {
int begin = dn.indexOf("CN=");
if (begin < 0)
throw new IllegalArgumentException("CN not found");
int end = dn.indexOf(",", begin);
if (end < 0)
throw new IllegalArgumentException("CN not found");
return dn.substring(begin + 3, end).trim();
}
public void exportRootCert(String[] args) {
boolean usePem = false;
boolean exportKey = false;
if (args.length >= 1) {
for (int i = 0; i < args.length; ++i) {
if (args[i].equals("-pem"))
usePem = true;
if (args[i].equals("-key"))
exportKey = true;
}
}
try {
context.print("Authority Name? ");
String authorityName = context.readLine();
CertificateAuthority authority = ca.getAuthority(authorityName);
CertificateMetadata cm = authority.getRootCertificate();
X509Certificate crt = cm.getCertificate();
RSAPrivateKey key = cm.getPrivateKey(authority.getRootKeyPassword());
String extension = usePem ? ".pem" : ".crt";
File dir = (File) context.getSession().getProperty("dir");
File f = new File(dir, authorityName + extension);
if (usePem)
CertificateExporter.writePemFile(crt, key, f, exportKey);
else
CertificateExporter.writeCrtFile(crt, f);
} catch (Exception e) {
context.println("Error: " + e.getMessage());
logger.warn("kraken ca: export ca cert failed", e);
}
}
@ScriptUsage(description = "print all issued certs", arguments = { @ScriptArgument(name = "authority name", type = "string", description = "authority name") })
public void certs(String[] args) throws InterruptedException {
CertificateAuthority authority = ca.getAuthority(args[0]);
if (authority == null) {
context.println("authority not found");
return;
}
context.println("Certificates");
context.println("--------------");
for (CertificateMetadata cm : authority.getCertificates()) {
context.println(cm);
}
}
public void issue(String[] args) {
try {
// find authority
context.print("Authority Name? ");
String authorityName = context.readLine();
CertificateAuthority authority = ca.getAuthority(authorityName);
if (authority == null) {
context.println("authority not found");
return;
}
CertificateRequest req = inputRequest();
CertificateMetadata cm = authority.issueCertificate(req);
X509Certificate cert = cm.getCertificate(req.getKeyPassword());
context.println(cert.toString());
context.println("");
context.println("Complete!");
} catch (InterruptedException e) {
context.println("");
context.println("interrupted");
} catch (NumberFormatException e) {
context.println("invalid number format");
} catch (Exception e) {
context.println("Error: " + e.getMessage());
logger.warn("kraken ca: failed to create certificate", e);
}
}
public void createAuthority(String[] args) {
try {
context.print("Authority Name? ");
String name = context.readLine();
CertificateRequest req = inputRequest();
// for self signing
req.setIssuerKey(req.getKeyPair().getPrivate());
ca.createAuthority(name, req);
context.println("created");
} catch (InterruptedException e) {
context.println("");
context.println("interrupted");
} catch (Exception e) {
context.println("error: " + e.getMessage());
logger.warn("kraken ca: create cert failed", e);
}
}
@ScriptUsage(description = "remove authority and purge all certificates", arguments = { @ScriptArgument(name = "authority name", type = "string", description = "authority name") })
public void removeAuthority(String[] args) {
CertificateAuthority authority = ca.getAuthority(args[0]);
if (authority == null) {
context.println("authority not found");
return;
}
ca.removeAuthority(args[0]);
context.println("removed");
}
private CertificateRequest inputRequest() throws Exception {
context.print("Common Name (CN)? ");
String cn = context.readLine();
context.print("Organization Unit (OU)? ");
String ou = context.readLine();
context.print("Organization (O)? ");
String o = context.readLine();
context.print("City (L)? ");
String l = context.readLine();
context.print("State (ST)? ");
String st = context.readLine();
context.print("Country Code (C)? ");
String c = context.readLine();
context.println("Select Signature Algorithm:");
int i = 1;
for (String sig : sigAlgorithms) {
context.printf("[%d] %s\n", i, sig);
i++;
}
context.printf("Select [1~%d] (default %d)? ", sigAlgorithms.length, sigAlgorithms.length);
String sigSelect = context.readLine();
int select;
if (sigSelect.isEmpty())
select = sigAlgorithms.length - 1;
else
select = Integer.parseInt(sigSelect) - 1;
String signatureAlgorithm = sigAlgorithms[select];
String dn = String.format("CN=%s,OU=%s,O=%s,L=%s,ST=%s,C=%s", cn, ou, o, l, st, c);
context.print("Days (default 365)? ");
String daysLine = context.readLine();
int days = 0;
if (daysLine.isEmpty())
days = 365; // 1 year
else
days = Integer.parseInt(daysLine);
context.println("Generating key pairs...");
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA", "BC");
KeyPair keyPair = keyPairGen.generateKeyPair();
context.print("Key Password? ");
String keyPassword = context.readPassword();
Date notBefore = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(notBefore);
cal.add(Calendar.DAY_OF_YEAR, days);
Date notAfter = cal.getTime();
// generate
return CertificateRequest.createSelfSignedCertRequest(keyPair, keyPassword, dn, notBefore, notAfter, signatureAlgorithm);
}
public void authorities(String[] args) {
context.println("Certificate Authorities");
context.println("-------------------------");
for (CertificateAuthority authority : ca.getAuthorities())
context.println(authority);
}
@ScriptUsage(description = "revoke a certificate", arguments = {
@ScriptArgument(name = "authority name", type = "string", description = "authority name"),
@ScriptArgument(name = "serial", type = "string", description = "cert serial (big integer range)"),
@ScriptArgument(name = "reason", type = "string", description = "cert revocation reason (select enum constant)", optional = true) })
public void revoke(String[] args) {
String authorityName = args[0];
String serial = args[1];
RevocationReason reason = RevocationReason.Unspecified;
if (args.length > 2)
reason = RevocationReason.valueOf(args[2]);
try {
CertificateAuthority authority = ca.getAuthority(authorityName);
if (authority == null) {
context.println("authority not found");
return;
}
CertificateMetadata cm = authority.findCertificate("serial", serial);
if (cm == null) {
context.println("certificate not found");
return;
}
authority.revoke(cm, reason);
context.println("revoked");
} catch (Exception e) {
context.println("Error: " + e.getMessage());
logger.warn("kraken ca: failed to revoke certificate", e);
}
}
@ScriptUsage(description = "print cert revocation list", arguments = { @ScriptArgument(name = "authority name", type = "string", description = "authority name") })
public void crl(String[] args) {
CertificateAuthority authority = ca.getAuthority(args[0]);
if (authority == null) {
context.println("authority not found");
return;
}
for (RevokedCertificate c : authority.getRevokedCertificates()) {
context.println(c);
}
}
}