feat: root ca sign

This commit is contained in:
2023-05-20 14:57:34 +08:00
parent c5387e4dca
commit d5fa1537dc
4 changed files with 155 additions and 3 deletions

View File

@@ -13,8 +13,8 @@
"repo": {
"dependencies": [
"info.picocli:picocli:4.6.1",
"me.hatter:commons:3.66",
"me.hatter:crypto:1.10"
"me.hatter:commons:3.67",
"me.hatter:crypto:1.12"
],
"testDependencies": [
"junit:junit:4.12"

View File

@@ -8,15 +8,33 @@ import picocli.CommandLine;
"Argument details:")
public class YubikeyCaArgs {
@CommandLine.Option(names = {"-k", "--generate-keypair"}, description = "Generate keypair")
@CommandLine.Option(names = {"--generate-keypair"}, description = "Generate keypair")
boolean generateKeypair = false;
@CommandLine.Option(names = {"--generate-root-ca"}, description = "Generate root CA")
boolean generateRootCa = false;
@CommandLine.Option(names = {"--generate-intermediate-ca"}, description = "Generate intermediate CA")
boolean generateIntermediateCa = false;
@CommandLine.Option(names = {"--generate-server-ca"}, description = "Generate server CA")
boolean generateServerCa = false;
@CommandLine.Option(names = {"--generate-client-ca"}, description = "Generate client CA")
boolean generateClientCa = false;
@CommandLine.Option(names = {"--keypair-type"}, description = "Keypair type, e.g." +
" RSA1024, RSA2048, RSA3072, RSA4096," +
" secp192k1, secp192r1, secp224k1, secp256k1," +
" secp224r1, secp256r1, secp384r1, secp521r1;")
String keypairType;
@CommandLine.Option(names = {"--sign-slot"}, description = "Slot for sign")
String signSlot;
@CommandLine.Option(names = {"--pin"}, description = "Yubikey PIV PIN")
String pin;
@CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "Display a help message")
boolean helpRequested = false;

View File

@@ -1,14 +1,22 @@
package me.hatter.tools.yubikeyca;
import com.alibaba.fastjson.JSONObject;
import me.hatter.tools.commons.bytes.Bytes;
import me.hatter.tools.commons.log.LogConfig;
import me.hatter.tools.commons.log.LogTool;
import me.hatter.tools.commons.log.LogTools;
import me.hatter.tools.commons.security.cert.X509CertUtil;
import me.hatter.tools.commons.security.key.KeyPairTool;
import me.hatter.tools.commons.security.key.KeyUtil;
import me.hatter.tools.commons.security.key.PKType;
import me.hatter.tools.commons.string.StringUtil;
import me.hatter.tools.crypto.ca.CertificateAuthority;
import me.hatter.tools.crypto.piv.PivCustomerSigner;
import me.hatter.tools.yubikeyca.cardcli.CardCliUtil;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Optional;
@@ -29,10 +37,47 @@ public class YubikeyCaMain {
generateKeyPair(args);
return;
}
if (args.generateRootCa) {
generateRootCa(args);
return;
}
log.error("Unknown command, use --help for help");
}
private static void generateRootCa(YubikeyCaArgs args) {
final String signSlot = args.signSlot;
if (StringUtil.isEmpty(signSlot)) {
log.error("Sign slot is required.");
return;
}
if (StringUtil.isEmpty(args.pin)) {
log.error("PIV PIN is required.");
return;
}
final JSONObject signPivMetaJsonObject = CardCliUtil.getPivMeta(signSlot);
final String algorithm = signPivMetaJsonObject.getString("algorithm");
final String publicKeyPem = signPivMetaJsonObject.getString("public_key_pem");
final PublicKey publicKey = KeyUtil.parsePublicKeyPEM(publicKeyPem);
final String cardCliCmd = CardCliUtil.getCardCliCmd();
final X509Certificate rootCa = CertificateAuthority.instance()
.subject("CN=HatterYubikeyRootCA")
.certPubKey(publicKey)
.validYears(30)
.customerSigner(new PivCustomerSigner(signSlot, algorithm, cardCliCmd) {
@Override
protected byte[] signWithPiv(String slot, String algorithm, Bytes digest) {
return CardCliUtil.signPiv(args.pin, slot, algorithm, digest);
}
})
.createCA();
log.info("Issued root CA: " + X509CertUtil.serializeX509CertificateToPEM(rootCa));
}
private static void generateKeyPair(YubikeyCaArgs args) {
if (StringUtil.isEmpty(args.keypairType)) {
log.error("Keypair type is required.");

View File

@@ -0,0 +1,89 @@
package me.hatter.tools.yubikeyca.cardcli;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import me.hatter.tools.commons.assertion.AssertUtil;
import me.hatter.tools.commons.bytes.Bytes;
import me.hatter.tools.commons.collection.CollectionUtil;
import me.hatter.tools.commons.io.IOUtil;
import me.hatter.tools.commons.log.LogTool;
import me.hatter.tools.commons.log.LogTools;
import me.hatter.tools.commons.string.StringUtil;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CardCliUtil {
private static final LogTool log = LogTools.getLogTool(CardCliUtil.class);
public static byte[] signPiv(String pin, String slot, String algorithm, Bytes digest) {
final String digestHex = digest.asHex();
final JSONObject signJsonObject = runCardCliAsJsonObject(Arrays.asList(
"piv-ecsign",
"--slot", slot,
"--algorithm", algorithm,
"--hash-hex", digestHex,
"--pin", pin,
"--json"
));
return Bytes.fromBase64(signJsonObject.getString("signed_data_base64")).bytes();
}
public static JSONObject getPivMeta(String slot) {
AssertUtil.notEmpty(slot, "Slot cannot be empty.");
return runCardCliAsJsonObject(Arrays.asList(
"piv-meta",
"--slot", slot,
"--json"
));
}
public static String getCardCliCmd() {
final String cardCliCmdFromEnv = System.getenv("CARD_CLI");
final String cardCliCmdFromProperties = System.getProperty("card.cli");
return StringUtil.def(cardCliCmdFromEnv,
cardCliCmdFromProperties,
"card-cli");
}
protected static JSONObject runCardCliAsJsonObject(List<String> arguments) {
return JSON.parseObject(runCardCli(arguments));
}
protected static String runCardCli(List<String> arguments) {
final List<String> commands = new ArrayList<>();
commands.add(getCardCliCmd());
if (CollectionUtil.isNotEmpty(arguments)) {
commands.addAll(arguments);
}
return runProcess(commands);
}
protected static String runProcess(List<String> commands) {
return runProcess(new ProcessBuilder().command(commands));
}
protected static String runProcess(ProcessBuilder pb) {
final String outputs;
final String errorOutputs;
try {
log.info("Run command: " + pb.command());
final Process p = pb.start();
final byte[] outputsBytes = IOUtil.readToBytes(p.getInputStream());
final byte[] errorOutputsByes = IOUtil.readToBytes(p.getErrorStream());
outputs = new String(outputsBytes, StandardCharsets.UTF_8);
errorOutputs = new String(errorOutputsByes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Error in run command.", e);
}
if (StringUtil.isEmpty(StringUtil.trim(outputs))) {
if (StringUtil.isNotEmpty(StringUtil.trim(errorOutputs))) {
log.error("Error outputs: " + errorOutputs);
}
throw new RuntimeException("Outputs is empty!");
}
return outputs;
}
}