feat: v0.1.0
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
a.pdf
|
||||||
|
b.pdf
|
||||||
out/
|
out/
|
||||||
Resume*.pdf
|
Resume*.pdf
|
||||||
__*.pem
|
__*.pem
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -1,3 +1,24 @@
|
|||||||
# sign-pdf
|
# sign-pdf
|
||||||
|
|
||||||
Sign PDF files
|
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 ******
|
||||||
|
```
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"repo": {
|
"repo": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"info.picocli:picocli:4.6.1",
|
"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:bcprov-ext-jdk15on:1.70",
|
||||||
"org.bouncycastle:bcpg-jdk15on:1.70",
|
"org.bouncycastle:bcpg-jdk15on:1.70",
|
||||||
"org.bouncycastle:bctls-jdk15on:1.70",
|
"org.bouncycastle:bctls-jdk15on:1.70",
|
||||||
|
|||||||
38
certs.pem
Normal file
38
certs.pem
Normal 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-----
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
110
src/main/java/me/hatter/tool/signpdf/cardcli/CardCliUtil.java
Normal file
110
src/main/java/me/hatter/tool/signpdf/cardcli/CardCliUtil.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/me/hatter/tool/signpdf/cardcli/PivMeta.java
Normal file
24
src/main/java/me/hatter/tool/signpdf/cardcli/PivMeta.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,14 @@ public class SignPdfArgs {
|
|||||||
String reason;
|
String reason;
|
||||||
@CommandLine.Option(names = {"--contact-info"}, description = "Contact info")
|
@CommandLine.Option(names = {"--contact-info"}, description = "Contact info")
|
||||||
String contactInfo;
|
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")
|
@CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "Display a help message")
|
||||||
boolean helpRequested = false;
|
boolean helpRequested = false;
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package me.hatter.tool.signpdf.main;
|
|||||||
|
|
||||||
public interface SignPdfConstant {
|
public interface SignPdfConstant {
|
||||||
String NAME = "sign-pdf";
|
String NAME = "sign-pdf";
|
||||||
String VERSION = "0.0.0";
|
String VERSION = "0.1.0";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package me.hatter.tool.signpdf.main;
|
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.options.SignOptions;
|
||||||
import me.hatter.tool.signpdf.sign.CreateSignature;
|
import me.hatter.tool.signpdf.sign.CreateSignature;
|
||||||
import me.hatter.tool.signpdf.sign.SigUtils;
|
import me.hatter.tool.signpdf.sign.SigUtils;
|
||||||
@@ -26,6 +29,18 @@ public class SignPdfMain {
|
|||||||
if (StringUtil.isEmpty(signPdfArgs.in) || StringUtil.isEmpty(signPdfArgs.out)) {
|
if (StringUtil.isEmpty(signPdfArgs.in) || StringUtil.isEmpty(signPdfArgs.out)) {
|
||||||
throw new RuntimeException("PDF file in/out cannot be empty.");
|
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();
|
final SignOptions signOptions = new SignOptions();
|
||||||
signOptions.setName(signPdfArgs.name);
|
signOptions.setName(signPdfArgs.name);
|
||||||
@@ -44,15 +59,23 @@ public class SignPdfMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final List<X509Certificate> certs = X509CertUtil.parseX509CertificateList(
|
final List<X509Certificate> certs = X509CertUtil.parseX509CertificateList(
|
||||||
RFile.from("__certs.pem").string()
|
RFile.from(signPdfArgs.certs).string());
|
||||||
);
|
|
||||||
final PrivateKey privateKey = KeyUtil.parsePrivateKeyPEM(
|
|
||||||
RFile.from("__priv.pem").string()
|
|
||||||
);
|
|
||||||
|
|
||||||
final X509Certificate[] certificateChain = certs.toArray(new X509Certificate[0]);
|
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 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);
|
final CreateSignature signing = new CreateSignature(certificateChain, contentSigner, signOptions);
|
||||||
// signing.setExternalSigning(true);
|
// signing.setExternalSigning(true);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user