feat: v0.1.0
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
a.pdf
|
||||
b.pdf
|
||||
out/
|
||||
Resume*.pdf
|
||||
__*.pem
|
||||
|
||||
23
README.md
23
README.md
@@ -1,3 +1,24 @@
|
||||
# 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": {
|
||||
"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
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;
|
||||
@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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user