From d5fa1537dce5d433393a07e3348df4ee4713170f Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 20 May 2023 14:57:34 +0800 Subject: [PATCH] feat: root ca sign --- yubikey-ca-java/build.json | 4 +- .../hatter/tools/yubikeyca/YubikeyCaArgs.java | 20 ++++- .../hatter/tools/yubikeyca/YubikeyCaMain.java | 45 ++++++++++ .../tools/yubikeyca/cardcli/CardCliUtil.java | 89 +++++++++++++++++++ 4 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/cardcli/CardCliUtil.java diff --git a/yubikey-ca-java/build.json b/yubikey-ca-java/build.json index 0dbf85e..1fabb8d 100644 --- a/yubikey-ca-java/build.json +++ b/yubikey-ca-java/build.json @@ -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" diff --git a/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/YubikeyCaArgs.java b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/YubikeyCaArgs.java index db8732a..187a3f6 100644 --- a/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/YubikeyCaArgs.java +++ b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/YubikeyCaArgs.java @@ -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; diff --git a/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/YubikeyCaMain.java b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/YubikeyCaMain.java index fcf131e..3a7c938 100644 --- a/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/YubikeyCaMain.java +++ b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/YubikeyCaMain.java @@ -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."); diff --git a/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/cardcli/CardCliUtil.java b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/cardcli/CardCliUtil.java new file mode 100644 index 0000000..59ac4c2 --- /dev/null +++ b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/cardcli/CardCliUtil.java @@ -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 arguments) { + return JSON.parseObject(runCardCli(arguments)); + } + + protected static String runCardCli(List arguments) { + final List commands = new ArrayList<>(); + commands.add(getCardCliCmd()); + if (CollectionUtil.isNotEmpty(arguments)) { + commands.addAll(arguments); + } + return runProcess(commands); + } + + protected static String runProcess(List 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; + } +}