feat: issue root/intermediate ca
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
out/
|
||||||
build/
|
build/
|
||||||
classes/
|
classes/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
30
yubikey-ca-java/README.md
Normal file
30
yubikey-ca-java/README.md
Normal 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]
|
||||||
|
```
|
||||||
|
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"project": {
|
"project": {
|
||||||
"name": "yubikey-ca-java",
|
"name": "yubikey-ca-java",
|
||||||
"main": "me.hatter.tools.yubikeyca.YubikeyCaMain",
|
"main": "me.hatter.tools.yubikeyca.YubikeyCaMain",
|
||||||
"archiveName": "yubikey-ca-java"
|
"archiveName": "yubikey-ca-java"
|
||||||
},
|
},
|
||||||
"application": false,
|
"application": false,
|
||||||
"java": "1.8",
|
"java": "1.8",
|
||||||
"builder": {
|
"builder": {
|
||||||
"name": "gradle",
|
"name": "gradle",
|
||||||
"version": "3.1"
|
"version": "3.1"
|
||||||
},
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"info.picocli:picocli:4.6.1",
|
"info.picocli:picocli:4.6.1",
|
||||||
"me.hatter:commons:3.67",
|
"me.hatter:commons:3.67",
|
||||||
"me.hatter:crypto:1.12"
|
"me.hatter:crypto:1.12"
|
||||||
],
|
],
|
||||||
"testDependencies": [
|
"testDependencies": [
|
||||||
"junit:junit:4.12"
|
"junit:junit:4.12"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ public class YubikeyCaArgs {
|
|||||||
@CommandLine.Option(names = {"--generate-client-ca"}, description = "Generate client CA")
|
@CommandLine.Option(names = {"--generate-client-ca"}, description = "Generate client CA")
|
||||||
boolean generateClientCa = false;
|
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." +
|
@CommandLine.Option(names = {"--keypair-type"}, description = "Keypair type, e.g." +
|
||||||
" RSA1024, RSA2048, RSA3072, RSA4096," +
|
" RSA1024, RSA2048, RSA3072, RSA4096," +
|
||||||
" secp192k1, secp192r1, secp224k1, secp256k1," +
|
" secp192k1, secp192r1, secp224k1, secp256k1," +
|
||||||
@@ -32,9 +38,18 @@ public class YubikeyCaArgs {
|
|||||||
@CommandLine.Option(names = {"--sign-slot"}, description = "Slot for sign")
|
@CommandLine.Option(names = {"--sign-slot"}, description = "Slot for sign")
|
||||||
String signSlot;
|
String signSlot;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--cert-slot"}, description = "Slot for cert")
|
||||||
|
String certSlot;
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--pin"}, description = "Yubikey PIV PIN")
|
@CommandLine.Option(names = {"--pin"}, description = "Yubikey PIV PIN")
|
||||||
String 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")
|
@CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "Display a help message")
|
||||||
boolean helpRequested = false;
|
boolean helpRequested = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package me.hatter.tools.yubikeyca;
|
package me.hatter.tools.yubikeyca;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
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.LogConfig;
|
||||||
import me.hatter.tools.commons.log.LogTool;
|
import me.hatter.tools.commons.log.LogTool;
|
||||||
import me.hatter.tools.commons.log.LogTools;
|
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.security.key.PKType;
|
||||||
import me.hatter.tools.commons.string.StringUtil;
|
import me.hatter.tools.commons.string.StringUtil;
|
||||||
import me.hatter.tools.crypto.ca.CertificateAuthority;
|
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.cardcli.CardCliUtil;
|
||||||
|
import me.hatter.tools.yubikeyca.hatterink.CertificateUtil;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
@@ -41,41 +41,85 @@ public class YubikeyCaMain {
|
|||||||
generateRootCa(args);
|
generateRootCa(args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (args.generateIntermediateCa) {
|
||||||
|
generateIntermediateCa(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
log.error("Unknown command, use --help for help");
|
log.error("Unknown command, use --help for help");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void generateRootCa(YubikeyCaArgs args) {
|
private static void generateIntermediateCa(YubikeyCaArgs args) {
|
||||||
final String signSlot = args.signSlot;
|
if (checkCertificateArgs(args)) return;
|
||||||
if (StringUtil.isEmpty(signSlot)) {
|
if (StringUtil.isEmpty(args.rootCaId)) {
|
||||||
log.error("Sign slot is required.");
|
log.error("Root CA id is required.");
|
||||||
return;
|
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)) {
|
if (StringUtil.isEmpty(args.pin)) {
|
||||||
log.error("PIV PIN is required.");
|
log.error("PIV PIN is required.");
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
final JSONObject signPivMetaJsonObject = CardCliUtil.getPivMeta(signSlot);
|
return false;
|
||||||
|
|
||||||
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) {
|
private static void generateKeyPair(YubikeyCaArgs args) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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() {
|
public static String getCardCliCmd() {
|
||||||
final String cardCliCmdFromEnv = System.getenv("CARD_CLI");
|
final String cardCliCmdFromEnv = System.getenv("CARD_CLI");
|
||||||
final String cardCliCmdFromProperties = System.getProperty("card.cli");
|
final String cardCliCmdFromProperties = System.getProperty("card.cli");
|
||||||
return StringUtil.def(cardCliCmdFromEnv,
|
return StringUtil.def(cardCliCmdFromEnv, cardCliCmdFromProperties, "card-cli");
|
||||||
cardCliCmdFromProperties,
|
|
||||||
"card-cli");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static JSONObject runCardCliAsJsonObject(List<String> arguments) {
|
protected static JSONObject runCardCliAsJsonObject(List<String> arguments) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user