feat: issue root/intermediate ca

This commit is contained in:
2023-05-20 18:16:54 +08:00
parent d5fa1537dc
commit fa4844d472
8 changed files with 243 additions and 52 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
out/
build/
classes/
.DS_Store

30
yubikey-ca-java/README.md Normal file
View File

@@ -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]
```

View File

@@ -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"
]
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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<String> arguments) {

View File

@@ -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);
}
}