From 3e49f4ca0203837fbf3b3b9a223b26f7a42dbebb Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Tue, 31 Oct 2023 00:32:56 +0800 Subject: [PATCH] feat: v0.1.0 --- .gitignore | 2 + README.md | 23 +++- build.json | 3 +- certs.pem | 38 ++++++ .../cardcli/CardCliPivCustomerSigner.java | 19 +++ .../tool/signpdf/cardcli/CardCliUtil.java | 110 ++++++++++++++++++ .../hatter/tool/signpdf/cardcli/PivMeta.java | 24 ++++ .../hatter/tool/signpdf/main/SignPdfArgs.java | 8 ++ .../tool/signpdf/main/SignPdfConstant.java | 2 +- .../hatter/tool/signpdf/main/SignPdfMain.java | 39 +++++-- 10 files changed, 257 insertions(+), 11 deletions(-) create mode 100644 certs.pem create mode 100644 src/main/java/me/hatter/tool/signpdf/cardcli/CardCliPivCustomerSigner.java create mode 100644 src/main/java/me/hatter/tool/signpdf/cardcli/CardCliUtil.java create mode 100644 src/main/java/me/hatter/tool/signpdf/cardcli/PivMeta.java diff --git a/.gitignore b/.gitignore index 4be6fcc..581b7d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +a.pdf +b.pdf out/ Resume*.pdf __*.pem diff --git a/README.md b/README.md index 2e8c9e7..6a8755a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ # sign-pdf -Sign PDF files \ No newline at end of file +Sign PDF files + +Sign with certs and private key: + +```shell +$ java -jar sign-pdf.jar \ + --in input.pdf \ + --out signed.pdf \ + --certs certs.pem \ + --key private.pem +``` + +Sign with certs and PIV: + +```shell +$ java -jar sign-pdf.jar \ + --in input.pdf \ + --out signed.pdf \ + --certs certs.pem \ + --slot 90\ + --pin ****** +``` \ No newline at end of file diff --git a/build.json b/build.json index 3e47709..f45fff0 100644 --- a/build.json +++ b/build.json @@ -13,7 +13,8 @@ "repo": { "dependencies": [ "info.picocli:picocli:4.6.1", - "me.hatter:commons:3.0", + "me.hatter:crypto:1.12", + "me.hatter:commons:3.68", "org.bouncycastle:bcprov-ext-jdk15on:1.70", "org.bouncycastle:bcpg-jdk15on:1.70", "org.bouncycastle:bctls-jdk15on:1.70", diff --git a/certs.pem b/certs.pem new file mode 100644 index 0000000..7586bee --- /dev/null +++ b/certs.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIB+DCCAX6gAwIBAgIVALe/Gyof7wdOqA5Hw+BfxLKsKctUMAoGCCqGSM49BAMC +MCQxIjAgBgNVBAMMGUhhdHRlciBFQyBJbnRlcm1lZGlhdGUgQ0EwHhcNMjMxMDMw +MDAwMDAwWhcNMzMxMDMwMDAwMDAwWjAcMRowGAYDVQQDDBFIYXR0ZXIgU2lnbmlu +ZyBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABNA3bQZm7Fz93A7wjR4TZnfZ/yZD +JDA/bMOyU0R1Xj2nyp164jWut7Y7k+wEUQObOqb6mtml3YK24kDSc75+vTBAzSsz +JWVpS4XgYGZ1u41L7Ns7un56uZocnuP2liFcSqN4MHYwDgYDVR0PAQH/BAQDAgWg +MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYE +FP9cz42+U6fP5YZXpJLM/TschPmkMB8GA1UdIwQYMBaAFKWHFKtlvWFHtpitgmmc +MK8CJAY8MAoGCCqGSM49BAMCA2gAMGUCMQCjs/EbpNpOa6LoKRqEu6AdKaKA4mlN +2xIVU6cIViwv4Lj0K/nmPHnAnPOu4yiLr1UCMFKcIfdZBn5mQ9DoT6Rbefy4SH6P +drQlvOTIBRQh9kiQoA2clTG1d8DFc0PpRF9pXA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB5DCCAWugAwIBAgIUPQMQohzxKUPB5kNVqucFbULevIMwCgYIKoZIzj0EAwIw +HDEaMBgGA1UEAwwRSGF0dGVyIEVDIFJvb3QgQ0EwHhcNMjMxMDI5MDAwMDAwWhcN +MzMxMDI5MDAwMDAwWjAkMSIwIAYDVQQDDBlIYXR0ZXIgRUMgSW50ZXJtZWRpYXRl +IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEImblRzI8dv8ea7y8kR2X0ZM56BF3 +tjjzjIJ7zmXaMO3DU9JbCdXZJoogLytTuKA5hmSPD0aXbnzQ89mZ7KWVA2qI2cjH +wN5u+KtQM2oPvhH0nhMVFifcM7IeP6quihqko2YwZDAOBgNVHQ8BAf8EBAMCAQYw +EgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUpYcUq2W9YUe2mK2CaZwwrwIk +BjwwHwYDVR0jBBgwFoAUeYIe16r9vuTceUDXG0CAbI9Pp+owCgYIKoZIzj0EAwID +ZwAwZAIwd9dqszZM7lKcf+LtDc0VkbNlBZVIS0jjZfUn6nUXOizfjNM3UzLcMKVO +TQP1pb2XAjAeISWnbTaxxQPCG/6mzfMw9CfqPS6ECuHfrXyfAw45AI7CpUArDhZW +ZKV6vlnkzHc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB3DCCAWKgAwIBAgIUBmlMvQ8s4PNWa2dFxhZH6gpVEpUwCgYIKoZIzj0EAwIw +HDEaMBgGA1UEAwwRSGF0dGVyIEVDIFJvb3QgQ0EwIBcNMjMxMDI5MDAwMDAwWhgP +MjA2MzEwMjkwMDAwMDBaMBwxGjAYBgNVBAMMEUhhdHRlciBFQyBSb290IENBMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAE3hLba+pjLyUPUiXO6DcSM0326f4yuziZiKNU +rBKfgJ7GZ6Yydlh2Ke33vyhoBcvTQlHP4ocWGwm0RdJ0Wz+99tkxegv8VskEqIEo +CU/U78w6DbcWvzQAAKfXUfGjjNpBo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUeYIe16r9vuTceUDXG0CAbI9Pp+owHwYDVR0j +BBgwFoAUeYIe16r9vuTceUDXG0CAbI9Pp+owCgYIKoZIzj0EAwIDaAAwZQIxANym +CiIqwtBXwcvn887Z9dnrdWXDEpJanID2nvwqa57ACIhTTu3d/UzFdOM6GWDR8AIw +bC9qIy+izBeFPfbggsz6U9nF5++LbtRHBFQ2InWoI4GZd074SGPcYRalMV3AUZ5m +-----END CERTIFICATE----- diff --git a/src/main/java/me/hatter/tool/signpdf/cardcli/CardCliPivCustomerSigner.java b/src/main/java/me/hatter/tool/signpdf/cardcli/CardCliPivCustomerSigner.java new file mode 100644 index 0000000..165bebc --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/cardcli/CardCliPivCustomerSigner.java @@ -0,0 +1,19 @@ +package me.hatter.tool.signpdf.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/src/main/java/me/hatter/tool/signpdf/cardcli/CardCliUtil.java b/src/main/java/me/hatter/tool/signpdf/cardcli/CardCliUtil.java new file mode 100644 index 0000000..1a08aaa --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/cardcli/CardCliUtil.java @@ -0,0 +1,110 @@ +package me.hatter.tool.signpdf.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.security.key.KeyUtil; +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 PivMeta getPivPublicKey(String slot) { + final JSONObject signPivMetaJsonObject = CardCliUtil.getPivMeta(slot); + + final PivMeta pivMeta = new PivMeta(); + pivMeta.setAlgorithm(signPivMetaJsonObject.getString("algorithm")); + final String publicKeyPem = signPivMetaJsonObject.getString("public_key_pem"); + pivMeta.setPublicKey(KeyUtil.parsePublicKeyPEM(publicKeyPem)); + return pivMeta; + } + + 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 { + final List commandList = getDesensitizedCommands(pb); + log.info("Run command: " + StringUtil.join(commandList, " ")); + 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! Please insert or reinsert your card."); + } + return outputs; + } + + private static List getDesensitizedCommands(ProcessBuilder pb) { + final List commandList = new ArrayList<>(pb.command()); + for (int i = 0; i < commandList.size(); i++) { + final String c = commandList.get(i); + if (StringUtil.equals("--pin", c) && ((i + 1) < commandList.size())) { + commandList.set(i + 1, "******"); + } + } + return commandList; + } +} diff --git a/src/main/java/me/hatter/tool/signpdf/cardcli/PivMeta.java b/src/main/java/me/hatter/tool/signpdf/cardcli/PivMeta.java new file mode 100644 index 0000000..7bde5b4 --- /dev/null +++ b/src/main/java/me/hatter/tool/signpdf/cardcli/PivMeta.java @@ -0,0 +1,24 @@ +package me.hatter.tool.signpdf.cardcli; + +import java.security.PublicKey; + +public class PivMeta { + private String algorithm; + private PublicKey publicKey; + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public void setPublicKey(PublicKey publicKey) { + this.publicKey = publicKey; + } +} diff --git a/src/main/java/me/hatter/tool/signpdf/main/SignPdfArgs.java b/src/main/java/me/hatter/tool/signpdf/main/SignPdfArgs.java index a8e4172..eafb2bc 100644 --- a/src/main/java/me/hatter/tool/signpdf/main/SignPdfArgs.java +++ b/src/main/java/me/hatter/tool/signpdf/main/SignPdfArgs.java @@ -21,6 +21,14 @@ public class SignPdfArgs { String reason; @CommandLine.Option(names = {"--contact-info"}, description = "Contact info") String contactInfo; + @CommandLine.Option(names = {"--certs"}, description = "Certification chain") + String certs; + @CommandLine.Option(names = {"--slot"}, description = "Sign key slot") + String slot; + @CommandLine.Option(names = {"--pin"}, description = "Sign key PIN") + String pin; + @CommandLine.Option(names = {"--key"}, description = "Sign private key") + String key; @CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "Display a help message") boolean helpRequested = false; diff --git a/src/main/java/me/hatter/tool/signpdf/main/SignPdfConstant.java b/src/main/java/me/hatter/tool/signpdf/main/SignPdfConstant.java index f7e914a..8246c3d 100644 --- a/src/main/java/me/hatter/tool/signpdf/main/SignPdfConstant.java +++ b/src/main/java/me/hatter/tool/signpdf/main/SignPdfConstant.java @@ -2,5 +2,5 @@ package me.hatter.tool.signpdf.main; public interface SignPdfConstant { String NAME = "sign-pdf"; - String VERSION = "0.0.0"; + String VERSION = "0.1.0"; } diff --git a/src/main/java/me/hatter/tool/signpdf/main/SignPdfMain.java b/src/main/java/me/hatter/tool/signpdf/main/SignPdfMain.java index 1bc96e2..58fe715 100644 --- a/src/main/java/me/hatter/tool/signpdf/main/SignPdfMain.java +++ b/src/main/java/me/hatter/tool/signpdf/main/SignPdfMain.java @@ -1,5 +1,8 @@ package me.hatter.tool.signpdf.main; +import me.hatter.tool.signpdf.cardcli.CardCliPivCustomerSigner; +import me.hatter.tool.signpdf.cardcli.CardCliUtil; +import me.hatter.tool.signpdf.cardcli.PivMeta; import me.hatter.tool.signpdf.options.SignOptions; import me.hatter.tool.signpdf.sign.CreateSignature; import me.hatter.tool.signpdf.sign.SigUtils; @@ -26,6 +29,18 @@ public class SignPdfMain { if (StringUtil.isEmpty(signPdfArgs.in) || StringUtil.isEmpty(signPdfArgs.out)) { throw new RuntimeException("PDF file in/out cannot be empty."); } + if (StringUtil.isEmpty(signPdfArgs.certs)) { + throw new RuntimeException("Certificate chain file cannot be empty."); + } + if (StringUtil.isEmpty(signPdfArgs.slot) && StringUtil.isEmpty(signPdfArgs.key)) { + throw new RuntimeException("Sign key file or slot cannot be empty."); + } + if (StringUtil.isNotEmpty(signPdfArgs.slot) && StringUtil.isNotEmpty(signPdfArgs.key)) { + throw new RuntimeException("Sign key file and slot cannot both provided."); + } + if (StringUtil.isNotEmpty(signPdfArgs.slot) && StringUtil.isEmpty(signPdfArgs.pin)) { + throw new RuntimeException("PIN cannot be empty"); + } final SignOptions signOptions = new SignOptions(); signOptions.setName(signPdfArgs.name); @@ -44,15 +59,23 @@ public class SignPdfMain { } final List certs = X509CertUtil.parseX509CertificateList( - RFile.from("__certs.pem").string() - ); - final PrivateKey privateKey = KeyUtil.parsePrivateKeyPEM( - RFile.from("__priv.pem").string() - ); - + RFile.from(signPdfArgs.certs).string()); final X509Certificate[] certificateChain = certs.toArray(new X509Certificate[0]); - final String signatureAlgorithm = SigUtils.getSignatureAlgorithm(certificateChain[0]); - final ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey); + + final ContentSigner contentSigner; + if (StringUtil.isNotEmpty(signPdfArgs.key)) { + final PrivateKey privateKey = KeyUtil.parsePrivateKeyPEM( + RFile.from(signPdfArgs.key).string()); + final String signatureAlgorithm = SigUtils.getSignatureAlgorithm(certificateChain[0]); + contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey); + } else { + final String cardCliCmd = CardCliUtil.getCardCliCmd(); + final PivMeta signPivMeta = CardCliUtil.getPivPublicKey(signPdfArgs.slot); + final CardCliPivCustomerSigner cardCliPivCustomerSigner = new CardCliPivCustomerSigner( + signPdfArgs.pin, signPdfArgs.slot, signPivMeta.getAlgorithm(), cardCliCmd); + contentSigner = cardCliPivCustomerSigner.getContentSigner(); + } + final CreateSignature signing = new CreateSignature(certificateChain, contentSigner, signOptions); // signing.setExternalSigning(true);