org.xipki.ca.mgmt.shell.CertActions Maven / Gradle / Ivy
The newest version!
// Copyright (c) 2013-2024 xipki. All rights reserved.
// License Apache License 2.0
package org.xipki.ca.mgmt.shell;
import org.apache.karaf.shell.api.action.Command;
import org.apache.karaf.shell.api.action.Completion;
import org.apache.karaf.shell.api.action.Option;
import org.apache.karaf.shell.api.action.lifecycle.Service;
import org.apache.karaf.shell.support.completers.FileCompleter;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PKCS8Generator;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.xipki.ca.api.mgmt.CaMgmtException;
import org.xipki.ca.api.mgmt.CertListInfo;
import org.xipki.ca.api.mgmt.CertListOrderBy;
import org.xipki.ca.api.mgmt.CertWithRevocationInfo;
import org.xipki.ca.api.mgmt.entry.CaEntry;
import org.xipki.ca.mgmt.shell.CaActions.CaAction;
import org.xipki.security.CrlReason;
import org.xipki.security.KeyCertBytesPair;
import org.xipki.security.X509Cert;
import org.xipki.security.util.KeyUtil;
import org.xipki.security.util.X509Util;
import org.xipki.shell.CmdFailure;
import org.xipki.shell.Completers;
import org.xipki.shell.IllegalCmdParamException;
import org.xipki.util.IoUtil;
import org.xipki.util.PemEncoder;
import org.xipki.util.StringUtil;
import org.xipki.util.exception.InvalidConfException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
/**
* Actions to management certificates and CRLs.
*
* @author Lijun Liao (xipki)
*
*/
public class CertActions {
@Command(scope = "ca", name = "cert-status", description = "show certificate status and save the certificate")
@Service
public static class CertStatus extends UnsuspendRmCertAction {
@Option(name = "--outform", description = "output format of the certificate")
@Completion(Completers.DerPemCompleter.class)
protected String outform = "der";
@Option(name = "--out", aliases = "-o", description = "where to save the certificate")
@Completion(FileCompleter.class)
private String outputFile;
@Override
protected Object execute0() throws Exception {
CertWithRevocationInfo certInfo = caManager.getCert(caName, getSerialNumber());
if (certInfo == null) {
System.out.println("certificate unknown");
return null;
}
String msg = StringUtil.concat("certificate profile: ", certInfo.getCertprofile(),
"\nstatus: ", (certInfo.getRevInfo() == null ? "good" : "revoked with " + certInfo.getRevInfo()));
println(msg);
if (outputFile != null) {
saveVerbose("saved certificate to file", outputFile,
encodeCert(certInfo.getCert().getCert().getEncoded(), outform));
}
return null;
} // method execute0
} // class CertStatus
public abstract static class CrlAction extends CaAction {
@Option(name = "--ca", required = true, description = "CA name")
@Completion(CaCompleters.CaNameCompleter.class)
protected String caName;
@Option(name = "--outform", description = "output format of the CRL")
@Completion(Completers.DerPemCompleter.class)
protected String outform = "der";
protected abstract X509CRLHolder retrieveCrl() throws Exception;
@Override
protected Object execute0() throws Exception {
CaEntry ca = Optional.ofNullable(caManager.getCa(caName)).orElseThrow(
() -> new CmdFailure("CA " + caName + " not available"));
X509CRLHolder crl;
try {
crl = retrieveCrl();
} catch (Exception ex) {
throw new CmdFailure("received no CRL from server: " + ex.getMessage());
}
if (crl == null) {
throw new CmdFailure("received no CRL from server");
}
String outFile = getOutFile();
if (outFile != null) {
saveVerbose("saved CRL to file", outFile, encodeCrl(crl.getEncoded(), outform));
}
return null;
} // method execute0
protected abstract String getOutFile();
} // class CrlAction
@Command(scope = "ca", name = "enroll-cert", description = "enroll certificate")
@Service
public static class EnrollCert extends CaAction {
@Option(name = "--ca", required = true, description = "CA name")
@Completion(CaCompleters.CaNameCompleter.class)
protected String caName;
@Option(name = "--subject", description = "Subject of the certificate.\n" +
"Exactly one of subject (keypair generated by CA) or CSR must be specified.")
protected String subject;
@Option(name = "--csr", description = "The CSR file.\n" +
"Exactly one of subject (keypair generated by CA) or csr must be specified.")
@Completion(FileCompleter.class)
protected String csrFile;
@Option(name = "--outform", description = "output format of the certificate")
@Completion(Completers.DerPemCompleter.class)
protected String outform = "der";
@Option(name = "--key-outform", description = "output format of the private key (pem or p12)")
protected String keyOutform = "p12";
@Option(name = "--out", aliases = "-o", required = true, description = "where to save the certificate")
@Completion(FileCompleter.class)
protected String outFile;
@Option(name = "--key-password",
description = "Password to protect the private key, as plaintext or PBE-encrypted.\n" +
"For key-outform PEM, NONE may be used to save the key in unecrypted form.")
protected String keyPasswordHint;
@Option(name = "--profile", aliases = "-p", required = true, description = "profile name")
@Completion(CaCompleters.ProfileNameCompleter.class)
protected String profileName;
@Option(name = "--not-before", description = "notBefore, UTC time of format yyyyMMddHHmmss")
protected String notBeforeS;
@Option(name = "--not-after", description = "notAfter, UTC time of format yyyyMMddHHmmss")
protected String notAfterS;
@Override
protected Object execute0() throws Exception {
CaEntry ca = Optional.ofNullable(caManager.getCa(caName)).orElseThrow(
() -> new CmdFailure("CA " + caName + " not available"));
if (StringUtil.isBlank(subject) == StringUtil.isBlank(csrFile)) {
throw new IllegalCmdParamException(
"Exactly one of subject (keypair generated by CA) or CSR must be specified.");
}
if (!StringUtil.orEqualsIgnoreCase(keyOutform, "pem", "p12", "pkcs12")) {
throw new IllegalCmdParamException("invalid key-outform " + keyOutform);
}
Instant notBefore = parseDate(notBeforeS);
Instant notAfter = parseDate(notAfterS);
byte[] certBytes;
if (StringUtil.isNotBlank(csrFile)) {
byte[] encodedCsr = StringUtil.isNotBlank(csrFile) ? X509Util.toDerEncoded(IoUtil.read(csrFile)) : null;
certBytes = caManager.generateCertificate(caName, profileName, encodedCsr, notBefore, notAfter).getEncoded();
} else {
boolean needKeyPwd = true;
if ("NONE".equalsIgnoreCase(keyPasswordHint)) {
needKeyPwd = false;
if (!"pem".equalsIgnoreCase(keyOutform)) {
throw new IllegalCmdParamException("Password NONE is not allowed");
}
}
char[] keyPwd = null;
if (needKeyPwd) {
keyPwd = readPasswordIfNotSet("Enter password to protect the private key", keyPasswordHint);
}
KeyCertBytesPair keyCertBytesPair =
caManager.generateKeyCert(caName, profileName, subject, notBefore, notAfter);
certBytes = keyCertBytesPair.getCert();
String ksFilePrefix = outFile.substring(0, outFile.lastIndexOf('.'));
PrivateKey privKey = BouncyCastleProvider.getPrivateKey(PrivateKeyInfo.getInstance(keyCertBytesPair.getKey()));
if (StringUtil.orEqualsIgnoreCase(keyOutform, "p12", "pkcs12")) {
CertificateFactory cf = CertificateFactory.getInstance("X509");
Certificate cert;
try (InputStream is = new ByteArrayInputStream(certBytes)) {
cert = cf.generateCertificate(is);
}
KeyStore p12Ks = KeyUtil.getOutKeyStore("PKCS12");
p12Ks.load(null, keyPwd);
p12Ks.setKeyEntry("main", privKey, keyPwd, new Certificate[]{cert});
byte[] ksBytes;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
p12Ks.store(os, keyPwd);
ksBytes = os.toByteArray();
}
saveVerbose("saved PKCS#12 keystore to file", ksFilePrefix + ".p12", ksBytes);
} else {
if (keyPwd == null) {
saveVerbose("save unencrypted key to file", ksFilePrefix + "-key.pem",
PemEncoder.encode(keyCertBytesPair.getKey(), PemEncoder.PemLabel.PRIVATE_KEY));
} else {
JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder =
new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES);
encryptorBuilder.setRandom(securityFactory.getRandom4Sign());
encryptorBuilder.setPassword(keyPwd);
JcaPKCS8Generator gen = new JcaPKCS8Generator(privKey, encryptorBuilder.build());
PemObject obj = gen.generate();
saveVerbose("save key to file", ksFilePrefix + "-key.pem",
PemEncoder.encode(obj.getContent(), PemEncoder.PemLabel.ENCRYPTED_PRIVATE_KEY));
}
}
}
saveVerbose("saved certificate to file", outFile, encodeCert(certBytes, outform));
return null;
} // method execute0
} // class EnrollCert
@Command(scope = "ca", name = "enroll-cross-cert", description = "enroll cross certificate")
@Service
public static class EnrollCrossCert extends EnrollCert {
@Option(name = "--target-cert", required = true, description =
" certificate file, for which the cross certificate will be generated. There shall "
+ "be no difference in subject and public key between certFile and csrFile.")
@Completion(FileCompleter.class)
private String targetCertFile;
@Override
protected Object execute0() throws Exception {
CaEntry ca = Optional.ofNullable(caManager.getCa(caName)).orElseThrow(
() -> new CmdFailure("CA " + caName + " not available"));
Instant notBefore = parseDate(notBeforeS);
Instant notAfter = parseDate(notAfterS);
byte[] encodedCsr = X509Util.toDerEncoded(IoUtil.read(csrFile));
byte[] encodedTargetCert = X509Util.toDerEncoded(IoUtil.read(targetCertFile));
X509Cert cert = caManager.generateCrossCertificate(caName, profileName,
encodedCsr, encodedTargetCert, notBefore, notAfter);
saveVerbose("saved certificate to file", outFile, encodeCert(cert.getEncoded(), outform));
return null;
} // method execute0
} // class EnrollCrossCert
@Command(scope = "ca", name = "gen-crl", description = "generate CRL")
@Service
public static class GenCrl extends CrlAction {
@Option(name = "--out", aliases = "-o", description = "where to save the CRL")
@Completion(FileCompleter.class)
protected String outFile;
@Override
protected X509CRLHolder retrieveCrl() throws Exception {
return caManager.generateCrlOnDemand(caName);
}
@Override
protected String getOutFile() {
return outFile;
}
} // class GenCrl
@Command(scope = "ca", name = "get-cert", description = "get certificate")
@Service
public static class GetCert extends CaAction {
@Option(name = "--ca", required = true, description = "CA name")
@Completion(CaCompleters.CaNameCompleter.class)
protected String caName;
@Option(name = "--serial", aliases = "-s", required = true, description = "serial number")
private String serialNumberS;
@Option(name = "--outform", description = "output format of the certificate")
@Completion(Completers.DerPemCompleter.class)
protected String outform = "der";
@Option(name = "--out", aliases = "-o", required = true, description = "where to save the certificate")
@Completion(FileCompleter.class)
private String outputFile;
@Override
protected Object execute0() throws Exception {
CertWithRevocationInfo certInfo = caManager.getCert(caName, toBigInt(serialNumberS));
if (certInfo == null) {
System.out.println("certificate unknown");
return null;
}
saveVerbose("certificate saved to file", outputFile,
encodeCert(certInfo.getCert().getCert().getEncoded(), outform));
return null;
} // method execute0
} // class GetCert
@Command(scope = "ca", name = "get-crl", description = "download CRL")
@Service
public static class GetCrl extends CrlAction {
@Option(name = "--with-basecrl", description = "whether to retrieve the baseCRL if the current CRL is a delta CRL")
private Boolean withBaseCrl = Boolean.FALSE;
@Option(name = "--basecrl-out", description = "where to save the baseCRL\n(defaults to -baseCRL)")
@Completion(FileCompleter.class)
private String baseCrlOut;
@Option(name = "--out", aliases = "-o", required = true, description = "where to save the CRL")
@Completion(FileCompleter.class)
protected String outFile;
@Override
protected X509CRLHolder retrieveCrl() throws Exception {
return caManager.getCurrentCrl(caName);
}
@Override
protected Object execute0() throws Exception {
CaEntry ca = Optional.ofNullable(caManager.getCa(caName)).orElseThrow(
() -> new CmdFailure("CA " + caName + " not available"));
X509CRLHolder crl;
try {
crl = retrieveCrl();
} catch (Exception ex) {
throw new CmdFailure("received no CRL from server: " + ex.getMessage());
}
if (crl == null) {
throw new CmdFailure("received no CRL from server");
}
saveVerbose("saved CRL to file", outFile, encodeCrl(crl.getEncoded(), outform));
if (withBaseCrl) {
Extensions extns = crl.getExtensions();
byte[] extnValue = X509Util.getCoreExtValue(extns, Extension.deltaCRLIndicator);
if (extnValue != null) {
if (baseCrlOut == null) {
baseCrlOut = outFile + "-baseCRL";
}
BigInteger baseCrlNumber = ASN1Integer.getInstance(extnValue).getPositiveValue();
try {
crl = caManager.getCrl(caName, baseCrlNumber);
} catch (Exception ex) {
throw new CmdFailure("received no baseCRL from server: " + ex.getMessage());
}
if (crl == null) {
throw new CmdFailure("received no baseCRL from server");
} else {
saveVerbose("saved baseCRL to file", baseCrlOut, encodeCrl(crl.getEncoded(), outform));
}
}
}
return null;
} // method execute0
@Override
protected String getOutFile() {
return outFile;
}
} // class GetCrl
@Command(scope = "ca", name = "list-cert", description = "show a list of certificates")
@Service
public static class ListCert extends CaAction {
@Option(name = "--ca", required = true, description = "CA name")
@Completion(CaCompleters.CaNameCompleter.class)
protected String caName;
@Option(name = "--subject", description = "the subject pattern, * is allowed.")
protected String subjectPatternS;
@Option(name = "--valid-from",
description = "start UTC time when the certificate is still valid, in form of yyyyMMdd or yyyyMMddHHmmss")
private String validFromS;
@Option(name = "--valid-to",
description = "end UTC time when the certificate is still valid, in form of yyyMMdd or yyyyMMddHHmmss")
private String validToS;
@Option(name = "-n", description = "maximal number of entries (between 1 and 1000)")
private int num = 1000;
@Option(name = "--order", description = "by which the result is ordered")
@Completion(CaCompleters.CertListSortByCompleter.class)
private String orderByS;
@Override
protected Object execute0() throws Exception {
X500Name subjectPattern = StringUtil.isBlank(subjectPatternS) ? null : new X500Name(subjectPatternS);
CertListOrderBy orderBy = null;
if (orderByS != null) {
orderBy = CertListOrderBy.forValue(orderByS);
if (orderBy == null) {
throw new IllegalCmdParamException("invalid order '" + orderByS + "'");
}
}
List certInfos =
caManager.listCertificates(caName, subjectPattern, parseDate(validFromS), parseDate(validToS), orderBy, num);
final int n = certInfos.size();
if (n == 0) {
println("found no certificate");
return null;
}
println(" | serial | notBefore | notAfter | subject");
println("-----+------------------------------------------+----------------+----------------+" +
"---------------------------");
for (int i = 0; i < n; i++) {
println(format(i + 1, certInfos.get(i)));
}
return null;
} // method execute0
private String format(int index, CertListInfo info) {
return StringUtil.concat(StringUtil.formatAccount(index, 4), " | ",
StringUtil.formatText(info.getSerialNumber().toString(16), 40), " | ",
info.getNotBefore(), " | ", info.getNotAfter(), " | ", info.getSubject());
} // method format
} // class ListCert
@Command(scope = "ca", name = "rm-cert", description = "remove certificate")
@Service
public static class RmCert extends UnsuspendRmCertAction {
@Option(name = "--force", aliases = "-f", description = "without prompt")
private Boolean force = Boolean.FALSE;
@Override
protected Object execute0() throws Exception {
BigInteger serialNo = getSerialNumber();
String msg = "certificate (serial number = 0x" + serialNo.toString(16) + ")";
if (force || confirm("Do you want to remove " + msg, 3)) {
try {
caManager.removeCertificate(caName, serialNo);
println("removed " + msg);
} catch (CaMgmtException ex) {
throw new CmdFailure("could not remove " + msg + ", error: " + ex.getMessage(), ex);
}
}
return null;
} // method execute0
} // class RmCert
@Command(scope = "ca", name = "revoke-cert", description = "revoke certificate")
@Service
public static class RevokeCert extends UnsuspendRmCertAction {
@Option(name = "--reason", aliases = "-r", required = true, description = "CRL reason")
@Completion(Completers.ClientCrlReasonCompleter.class)
private String reason;
@Option(name = "--inv-date", description = "invalidity date, UTC time of format yyyyMMddHHmmss")
private String invalidityDateS;
@Override
protected Object execute0() throws Exception {
CrlReason crlReason = CrlReason.forNameOrText(reason);
if (!CrlReason.PERMITTED_CLIENT_CRLREASONS.contains(crlReason)) {
throw new InvalidConfException("reason " + reason + " is not permitted");
}
BigInteger serialNo = getSerialNumber();
String msg = "certificate (serial number = 0x" + serialNo.toString(16) + ")";
try {
caManager.revokeCertificate(caName, serialNo, crlReason, parseDate(invalidityDateS));
println("revoked " + msg);
return null;
} catch (CaMgmtException ex) {
throw new CmdFailure("could not revoke " + msg + ", error: " + ex.getMessage(), ex);
}
} // method execute0
} // class RevokeCert
@Command(scope = "ca", name = "unsuspend-cert", description = "unsuspend certificate")
@Service
public static class UnsuspendCert extends UnsuspendRmCertAction {
@Override
protected Object execute0() throws Exception {
BigInteger serialNo = getSerialNumber();
String msg = "certificate (serial number = 0x" + serialNo.toString(16) + ")";
try {
caManager.unsuspendCertificate(caName, serialNo);
println("unsuspended " + msg);
return null;
} catch (CaMgmtException ex) {
throw new CmdFailure("could not unsuspend " + msg + ", error: " + ex.getMessage(), ex);
}
} // method execute0
} // class UnrevokeCert
public abstract static class UnsuspendRmCertAction extends CaAction {
@Option(name = "--ca", required = true, description = "CA name")
@Completion(CaCompleters.CaNameCompleter.class)
protected String caName;
@Option(name = "--cert", aliases = "-c",
description = "certificate file\n(either cert or serial must be specified)")
@Completion(FileCompleter.class)
protected String certFile;
@Option(name = "--serial", aliases = "-s", description = "serial number\n(either cert or serial must be specified)")
private String serialNumberS;
protected BigInteger getSerialNumber() throws Exception {
CaEntry ca = caManager.getCa(caName);
if (ca == null) {
throw new CmdFailure("CA " + caName + " not available");
}
BigInteger serialNumber;
if (serialNumberS != null) {
serialNumber = toBigInt(serialNumberS);
} else if (certFile != null) {
X509Cert caCert = ca.getCert();
X509Cert cert = X509Util.parseCert(new File(certFile));
if (!X509Util.issues(caCert, cert)) {
throw new CmdFailure("certificate '" + certFile + "' is not issued by CA " + caName);
}
serialNumber = cert.getSerialNumber();
} else {
throw new IllegalCmdParamException("neither serialNumber nor certFile is specified");
}
return serialNumber;
} // method getSerialNumber
} // class UnRevRmCertAction
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy