急ぎで書いたので情報不足です。
動くものはできたので、追々完成度高めたい気持ちはあります。
Asposeが使い物にならないのでPDFBoxでMDP署名
前回の記事。
署名理由等に日本語入れるとファイルサイズが肥大化するため、PDFBoxでやっていく。
(https://qiita.com/katsuo0004/items/2b17d3083107c3b6f846)
準備:Windows11 WSLでUbuntu 22.04.3 で証明書(PFX形式)作成していく
自己署名証明書(テスト用・非公開用途)
自分で作成できる(無料)
法的効力はない(自己証明なので第三者証明ではない)
開発・社内運用用などに便利
🔧 例:OpenSSLを使って自己署名証明書を作る
秘密鍵(Private Key)を作成
openssl genrsa -out mykey.key 2048
証明書署名要求(CSR)を作成
途中でいろいろ聞かれるので適当に入力してOK(Common Name だけ分かりやすくするとよい)
openssl req -new -key mykey.key -out myreq.csr
自己署名証明書(CRT)を作成
openssl x509 -req -days 365 -in myreq.csr -signkey mykey.key -out mycert.crt
PFXファイル(署名に使う形式)を作成
エクスポート時にパスワードを求められます(署名時に必要になるので覚えておく)
openssl pkcs12 -export -out mycert.pfx -inkey mykey.key -in mycert.crt
仮想環境(たとえば WSL, Docker, 仮想マシン, リモートシェル**など)で操作してる場合、仮想マシン内の C:\Users\Pasokonsan\ は、ホスト側(デスクトップのWindows)から見えないので、見えるところにコピー
cp mycert.pfx /mnt/c/Users/Pasokonsan/OneDrive/Desktop/
cp mycert.crt /mnt/c/Users/Pasokonsan/OneDrive/Desktop/
cp mykey.key /mnt/c/Users/Pasokonsan/OneDrive/Desktop/
cp myreq.csr /mnt/c/Users/Pasokonsan/OneDrive/Desktop/
🖋 PDFに署名する手順(Adobe Acrobat Proの場合)
mycert.pfx をPCにダブルクリックしてインストール(Windowsなら証明書ストアに)
Adobe Acrobat を開く
[ツール] → [証明書] → [デジタル署名]
署名位置を指定すると、自分の証明書を選べるようになります
🔍 注意点
自己署名なので、他の人に送った場合は**「信頼されていない署名」**と表示されます。
ローカルや社内用途、テストで使うには十分便利です。
mycert.pfx は大事な鍵なので絶対に他人に渡さないでください。
APIを作成していく
以下でプロジェクト作成
(https://start.spring.io/)
pdfboxとbouncycastle追加
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.1</version>
</dependency>
<!-- BouncyCastle Provider -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.78</version>
</dependency>
<!-- BouncyCastle PKIX -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15to18</artifactId>
<version>1.78</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
リクエストでPDFファイルを受け取って決まった場所に出力するAPIを作成
package com.example.demo.controller;
import com.example.demo.service.PdfSignatureService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
@RestController
@RequestMapping("/demo")
public class PdfController {
@Autowired
private PdfSignatureService pdfSignatureService;
@RequestMapping(value = {"/pdfSignature"}, method = RequestMethod.POST)
public void create(@RequestBody byte[] request) throws Exception {
pdfSignatureService.execute(request, new File("C:\\Users\\Pasokonsan\\Downloads\\output.pdf"));
}
}
package com.example.demo.service;
import org.springframework.stereotype.Service;
import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
@Service
public class PdfSignatureService {
public void execute(byte[] request, File outputFile) throws Exception {
KeyStore keystore = KeyStore.getInstance("PKCS12");
try (InputStream keystoreStream = new FileInputStream("C:\\Users\\Pasokonsan\\OneDrive\\Desktop\\mycert.pfx")) {
keystore.load(keystoreStream, "password".toCharArray());
}
String alias = keystore.aliases().nextElement();
PrivateKey privateKey = (PrivateKey) keystore.getKey(alias, "password".toCharArray());
Certificate[] certChain = keystore.getCertificateChain ( alias);
PdfSign pdfSign = new PdfSign ( privateKey, certChain );
pdfSign.signPDF ( request, outputFile );
}
}
package com.example.demo.service;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;
import java.io.*;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
public class PdfSign implements SignatureInterface {
private final PrivateKey privateKey;
private final Certificate[] certificateChain;
public PdfSign(PrivateKey privateKey, Certificate[] certificateChain) {
this.privateKey = privateKey;
this.certificateChain = certificateChain;
}
public void signPDF(byte[] inputFile, File outputFile) throws Exception {
try (PDDocument document = Loader.loadPDF(inputFile)) {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("私の名前");
signature.setLocation("私の場所");
signature.setReason("MDP署名したい");
signature.setSignDate( Calendar.getInstance());
// MDP設定を追加
addMDPPermission(document, signature, 0); // 変更不許可
document.addSignature(signature, this);
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
document.saveIncremental(fos);
}
}
}
@Override
public byte[] sign(InputStream content) throws IOException {
try {
List<Certificate> certList = Arrays.asList(certificateChain);
Store<?> certStore = new JcaCertStore (certList);
CMSTypedData msg = new CMSProcessableInputStream(content);
// BouncyCastleをプログラム内で明示的にプロバイダとして登録
Security.addProvider(new BouncyCastleProvider ());
ContentSigner signer = new JcaContentSignerBuilder ("SHA256withRSA")
.setProvider("BC")
.build(privateKey);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder (
new JcaDigestCalculatorProviderBuilder ().build())
.build(signer, (X509Certificate) certificateChain[0])
);
gen.addCertificates(certStore);
CMSSignedData signedData = gen.generate(msg, false);
return signedData.getEncoded();
} catch (Exception e) {
throw new IOException("署名中にエラーが発生しました", e);
}
}
private void addMDPPermission(PDDocument doc, PDSignature signature, int accessPermissions) {
COSDictionary transformParams = new COSDictionary();
transformParams.setItem( COSName.TYPE, COSName.getPDFName("TransformParams"));
transformParams.setItem(COSName.V, COSName.getPDFName("1.2"));
transformParams.setInt(COSName.P, accessPermissions); // 1 = no changes
transformParams.setDate(COSName.getPDFName("DT"), Calendar.getInstance());
COSDictionary sigRef = new COSDictionary();
sigRef.setItem(COSName.TYPE, COSName.getPDFName("SigRef"));
sigRef.setItem(COSName.TRANSFORM_METHOD, COSName.getPDFName("DocMDP"));
sigRef.setItem(COSName.TRANSFORM_PARAMS, transformParams);
sigRef.setItem(COSName.getPDFName("Data"), doc.getDocumentCatalog().getCOSObject());
COSArray referenceArray = new COSArray();
referenceArray.add(sigRef);
signature.getCOSObject().setItem(COSName.REFERENCE, referenceArray);
COSDictionary perms = new COSDictionary();
perms.setItem(COSName.DOCMDP, signature.getCOSObject());
doc.getDocumentCatalog().getCOSObject().setItem(COSName.PERMS, perms);
}
// サブクラス: CMS用にInputStreamを扱うためのヘルパー
private static class CMSProcessableInputStream implements CMSTypedData {
private final InputStream input;
CMSProcessableInputStream(InputStream is) {
this.input = is;
}
@Override
public Object getContent() {
return input;
}
@Override
public void write(OutputStream out) throws IOException, CMSException {
byte[] buffer = new byte[8 * 1024];
int read;
while ((read = input.read(buffer)) >= 0) {
out.write(buffer, 0, read);
}
input.close();
}
@Override
public ASN1ObjectIdentifier getContentType() {
return PKCSObjectIdentifiers.data;
}
}
}
詰まった箇所
・document.saveIncremental(fos);で必ず保存すること。document.save(fos);ではだめでした。
PostmanでAPIコール
・http://localhost:8080/demo/pdfSignature
・POSTでボディにバイナリでファイル選択
・javaで受け取るときはコントローラーで @RequestBody byte[] で受け取ること
出力結果こちら
このMDP署名が低容量でやりたかった(´;ω;`)
43kb→63kb程度の増加ですので、たぶん大丈夫だと思う。
コード中身は全然見れてないので品質に問題あるとは思うので、あくまで参考まで。