feat: v0.1.0

This commit is contained in:
2023-10-31 00:32:56 +08:00
parent 0eae36538c
commit 3e49f4ca02
10 changed files with 257 additions and 11 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
a.pdf
b.pdf
out/
Resume*.pdf
__*.pem

View File

@@ -1,3 +1,24 @@
# sign-pdf
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 ******
```

View File

@@ -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",

38
certs.pem Normal file
View File

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

View File

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

View File

@@ -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<String> arguments) {
return JSON.parseObject(runCardCli(arguments));
}
protected static String runCardCli(List<String> arguments) {
final List<String> commands = new ArrayList<>();
commands.add(getCardCliCmd());
if (CollectionUtil.isNotEmpty(arguments)) {
commands.addAll(arguments);
}
return runProcess(commands);
}
protected static String runProcess(List<String> commands) {
return runProcess(new ProcessBuilder().command(commands));
}
protected static String runProcess(ProcessBuilder pb) {
final String outputs;
final String errorOutputs;
try {
final List<String> 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<String> getDesensitizedCommands(ProcessBuilder pb) {
final List<String> 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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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<X509Certificate> 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 ContentSigner contentSigner;
if (StringUtil.isNotEmpty(signPdfArgs.key)) {
final PrivateKey privateKey = KeyUtil.parsePrivateKeyPEM(
RFile.from(signPdfArgs.key).string());
final String signatureAlgorithm = SigUtils.getSignatureAlgorithm(certificateChain[0]);
final ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey);
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);