diff --git a/.gitignore b/.gitignore index 55bb666..e4c0546 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +out/ build/ classes/ .DS_Store diff --git a/yubikey-ca-java/README.md b/yubikey-ca-java/README.md new file mode 100644 index 0000000..fe320c4 --- /dev/null +++ b/yubikey-ca-java/README.md @@ -0,0 +1,30 @@ +ENV: + +* CARD_CLI - Card cli command or full path, default `card-cli` +* SIGN_REQUEST_SLOT - Sign request slot, default `82` + +# Generate Keypair + +```shell +$ java -jar yubikey-ca-java.jar --generate-keypair --keypair-type secp256r1 +``` + +# Issue ROOT CA + +```shell +$ java -jar yubikey-ca-java.jar --generate-root-ca \ + --sign-slot 88 --subject 'CN=Hatter Yubikey EC Root CA' \ + --pin ****** \ + [--add-to-remote] +``` + +# Issue Intermediate CA + +```shell +$ java -jar yubikey-ca-java.jar --generate-intermediate-ca \ + --sign-slot 88 --subject 'CN=Hatter Yubikey EC Intermediate CA' \ + --cert-slot 89 --root-ca-id 39 \ + --pin ****** \ + [--add-to-remote] +``` + diff --git a/yubikey-ca-java/build.json b/yubikey-ca-java/build.json index 1fabb8d..e0c7652 100644 --- a/yubikey-ca-java/build.json +++ b/yubikey-ca-java/build.json @@ -1,23 +1,23 @@ { - "project": { - "name": "yubikey-ca-java", - "main": "me.hatter.tools.yubikeyca.YubikeyCaMain", - "archiveName": "yubikey-ca-java" - }, - "application": false, - "java": "1.8", - "builder": { - "name": "gradle", - "version": "3.1" - }, - "repo": { - "dependencies": [ - "info.picocli:picocli:4.6.1", - "me.hatter:commons:3.67", - "me.hatter:crypto:1.12" - ], - "testDependencies": [ - "junit:junit:4.12" - ] - } + "project": { + "name": "yubikey-ca-java", + "main": "me.hatter.tools.yubikeyca.YubikeyCaMain", + "archiveName": "yubikey-ca-java" + }, + "application": false, + "java": "1.8", + "builder": { + "name": "gradle", + "version": "3.1" + }, + "repo": { + "dependencies": [ + "info.picocli:picocli:4.6.1", + "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 187a3f6..a8a3dbd 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 @@ -23,6 +23,12 @@ public class YubikeyCaArgs { @CommandLine.Option(names = {"--generate-client-ca"}, description = "Generate client CA") boolean generateClientCa = false; + @CommandLine.Option(names = {"--subject"}, description = "Certificate subject") + String subject; + + @CommandLine.Option(names = {"--root-ca-id"}, description = "Root certificate ID") + String rootCaId; + @CommandLine.Option(names = {"--keypair-type"}, description = "Keypair type, e.g." + " RSA1024, RSA2048, RSA3072, RSA4096," + " secp192k1, secp192r1, secp224k1, secp256k1," + @@ -32,9 +38,18 @@ public class YubikeyCaArgs { @CommandLine.Option(names = {"--sign-slot"}, description = "Slot for sign") String signSlot; + @CommandLine.Option(names = {"--cert-slot"}, description = "Slot for cert") + String certSlot; + @CommandLine.Option(names = {"--pin"}, description = "Yubikey PIV PIN") String pin; + @CommandLine.Option(names = {"--memo"}, description = "Memo") + String memo; + + @CommandLine.Option(names = {"--add-to-remote"}, description = "Add certificate to remote") + boolean addToRemote = false; + @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 3a7c938..26fc15a 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,7 +1,6 @@ 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; @@ -11,8 +10,9 @@ 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.CardCliPivCustomerSigner; import me.hatter.tools.yubikeyca.cardcli.CardCliUtil; +import me.hatter.tools.yubikeyca.hatterink.CertificateUtil; import java.security.KeyPair; import java.security.PublicKey; @@ -41,41 +41,85 @@ public class YubikeyCaMain { generateRootCa(args); return; } + if (args.generateIntermediateCa) { + generateIntermediateCa(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."); + private static void generateIntermediateCa(YubikeyCaArgs args) { + if (checkCertificateArgs(args)) return; + if (StringUtil.isEmpty(args.rootCaId)) { + log.error("Root CA id is required."); return; } + if (StringUtil.isEmpty(args.certSlot)) { + log.error("Cert slot is required."); + return; + } + + final X509Certificate rootCertificate = CertificateUtil.getCertificate(args.pin, args.rootCaId); + + final JSONObject signPivMetaJsonObject = CardCliUtil.getPivMeta(args.certSlot); + final String signAlgorithm = signPivMetaJsonObject.getString("algorithm"); + final String certPublicKeyPem = signPivMetaJsonObject.getString("public_key_pem"); + final PublicKey certPublicKey = KeyUtil.parsePublicKeyPEM(certPublicKeyPem); + + final String cardCliCmd = CardCliUtil.getCardCliCmd(); + final X509Certificate intermediateCa = CertificateAuthority.instance() + .subject(args.subject) + .signCert(rootCertificate) + .certPubKey(certPublicKey) + .validYears(10) + .customerSigner(new CardCliPivCustomerSigner(args.pin, args.signSlot, signAlgorithm, cardCliCmd)) + .createIntermediateCert(); + final String intermediateCaPem = X509CertUtil.serializeX509CertificateToPEM(intermediateCa); + + log.info("Issued intermediate CA: " + intermediateCaPem); + if (args.addToRemote) { + CertificateUtil.addCertificate(args.pin, args.rootCaId, args.memo, intermediateCaPem, null); + } + } + + private static void generateRootCa(YubikeyCaArgs args) { + if (checkCertificateArgs(args)) return; + + final JSONObject signPivMetaJsonObject = CardCliUtil.getPivMeta(args.signSlot); + final String signAlgorithm = signPivMetaJsonObject.getString("algorithm"); + final String certPublicKeyPem = signPivMetaJsonObject.getString("public_key_pem"); + final PublicKey certPublicKey = KeyUtil.parsePublicKeyPEM(certPublicKeyPem); + + final String cardCliCmd = CardCliUtil.getCardCliCmd(); + final X509Certificate rootCa = CertificateAuthority.instance() + .subject(args.subject) + .certPubKey(certPublicKey) + .validYears(40) + .customerSigner(new CardCliPivCustomerSigner(args.pin, args.signSlot, signAlgorithm, cardCliCmd)) + .createCA(); + final String rootCaPem = X509CertUtil.serializeX509CertificateToPEM(rootCa); + + log.info("Issued root CA: " + rootCaPem); + if (args.addToRemote) { + CertificateUtil.addCertificate(args.pin, null, args.memo, rootCaPem, null); + } + } + + private static boolean checkCertificateArgs(YubikeyCaArgs args) { + if (StringUtil.isEmpty(args.signSlot)) { + log.error("Sign slot is required."); + return true; + } + if (StringUtil.isEmpty(args.subject)) { + log.error("Certificate subject is required."); + return true; + } if (StringUtil.isEmpty(args.pin)) { log.error("PIV PIN is required."); - return; + return true; } - 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)); + return false; } private static void generateKeyPair(YubikeyCaArgs args) { diff --git a/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/cardcli/CardCliPivCustomerSigner.java b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/cardcli/CardCliPivCustomerSigner.java new file mode 100644 index 0000000..35371c4 --- /dev/null +++ b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/cardcli/CardCliPivCustomerSigner.java @@ -0,0 +1,19 @@ +package me.hatter.tools.yubikeyca.cardcli; + +import me.hatter.tools.commons.bytes.Bytes; +import me.hatter.tools.crypto.piv.PivCustomerSigner; + +public class CardCliPivCustomerSigner extends PivCustomerSigner { + private final String pin; + + public CardCliPivCustomerSigner(String pin, String slot, String algorithm, String cardCli) { + super(slot, algorithm, cardCli); + this.pin = pin; + } + + @Override + protected byte[] signWithPiv(String slot, String algorithm, Bytes digest) { + return CardCliUtil.signPiv(pin, slot, algorithm, digest); + } +} + 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 index 59ac4c2..ad85461 100644 --- 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 @@ -40,12 +40,16 @@ public class CardCliUtil { )); } + public static String getSignRequestSlot() { + final String cardCliCmdFromEnv = System.getenv("SIGN_REQUEST_SLOT"); + final String cardCliCmdFromProperties = System.getProperty("sign.request.slot"); + return StringUtil.def(cardCliCmdFromEnv, cardCliCmdFromProperties, "82"); + } + 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"); + return StringUtil.def(cardCliCmdFromEnv, cardCliCmdFromProperties, "card-cli"); } protected static JSONObject runCardCliAsJsonObject(List arguments) { diff --git a/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/hatterink/CertificateUtil.java b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/hatterink/CertificateUtil.java new file mode 100644 index 0000000..5296bc9 --- /dev/null +++ b/yubikey-ca-java/src/main/java/me/hatter/tools/yubikeyca/hatterink/CertificateUtil.java @@ -0,0 +1,78 @@ +package me.hatter.tools.yubikeyca.hatterink; + +import com.alibaba.fastjson.JSONObject; +import me.hatter.tools.commons.bytes.Bytes; +import me.hatter.tools.commons.log.LogTool; +import me.hatter.tools.commons.log.LogTools; +import me.hatter.tools.commons.network.HttpRequest; +import me.hatter.tools.commons.security.cert.X509CertUtil; +import me.hatter.tools.commons.security.digest.Digests; +import me.hatter.tools.commons.string.StringUtil; +import me.hatter.tools.yubikeyca.cardcli.CardCliUtil; + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.Arrays; +import java.util.Date; + +public class CertificateUtil { + private static final LogTool log = LogTools.getLogTool(CertificateUtil.class); + + public static void addCertificate(String pin, String parentId, String memo, String certificatePem, String privateKeyPem) { + final String authBeforeMillis = String.valueOf(System.currentTimeMillis() + Duration.ofMinutes(5).toMillis()); + memo = StringUtil.def(memo, "Added at: " + new Date()); + final String tobeSigned = StringUtil.join(Arrays.asList( + authBeforeMillis, parentId, certificatePem, privateKeyPem, memo), ";"); + final Bytes signBytes = signRequest(pin, tobeSigned); + log.info("Auth sign: " + signBytes.asBase64()); + Bytes response = HttpRequest.fromUrl("https://hatter.ink/ca/add_certificate.json") + .postKeyValues() + .kv("parentId", parentId, parentId != null) + .kv("certificatePem", certificatePem) + .kv("privateKeyPem", privateKeyPem, privateKeyPem != null) + .kv("memo", memo) + .kv("__auth_before", authBeforeMillis) + .kv("__auth_keys", "parentId,certificatePem,privateKeyPem,memo") + .kv("__auth_sign", signBytes.asBase64()) + .kv("pretty", "1") + .post(); + final JSONObject responseJsonObject = response.asJSON(); + final int status = responseJsonObject.getInteger("status"); + if (status != 200) { + throw new RuntimeException("Add certificate failed: " + status + " - " + responseJsonObject.getString("message")); + } + log.info("Add certificate succeed: " + responseJsonObject.getJSONObject("data")); + } + + public static X509Certificate getCertificate(String pin, String id) { + final String authBeforeMillis = String.valueOf(System.currentTimeMillis() + Duration.ofMinutes(5).toMillis()); + final String tobeSigned = authBeforeMillis + ";" + id; + + final Bytes signBytes = signRequest(pin, tobeSigned); + log.info("Auth sign: " + signBytes.asBase64()); + Bytes response = HttpRequest.fromUrl("https://hatter.ink/ca/get_certificate.json") + .postKeyValues() + .kv("id", id) + .kv("__auth_before", authBeforeMillis) + .kv("__auth_keys", "id") + .kv("__auth_sign", signBytes.asBase64()) + .kv("pretty", "1") + .post(); + log.info("Got certificate: " + response); + final JSONObject responseJsonObject = response.asJSON(); + final int status = responseJsonObject.getInteger("status"); + if (status != 200) { + throw new RuntimeException("Get certificate failed: " + status + " - " + responseJsonObject.getString("message")); + } + final String certificatePem = responseJsonObject.getJSONObject("data").getString("certificate"); + return X509CertUtil.parseX509Certificate(certificatePem); + } + + private static Bytes signRequest(String pin, String tobeSigned) { + final String signRequestSlot = CardCliUtil.getSignRequestSlot(); + final Bytes tobeSignedDigestBytes = Digests.sha256().digest(tobeSigned.getBytes(StandardCharsets.UTF_8)); + final byte[] sign = CardCliUtil.signPiv(pin, signRequestSlot, "p256", tobeSignedDigestBytes); + return Bytes.from(sign); + } +}