0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Apache PDFBoxを使用してPDFに電子署名(MDP署名)

Last updated at Posted at 2025-04-08

急ぎで書いたので情報不足です。
動くものはできたので、追々完成度高めたい気持ちはあります。

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を作成していく

以下でプロジェクト作成
image.png
(https://start.spring.io/)

pdfboxとbouncycastle追加

pom.xml
<?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を作成

PdfController.java
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"));
    }
}
PdfSignatureService.java
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 );
    }
}
PdfSign.java
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コール

image.png
http://localhost:8080/demo/pdfSignature
・POSTでボディにバイナリでファイル選択
・javaで受け取るときはコントローラーで @RequestBody byte[] で受け取ること

出力結果こちら

このMDP署名が低容量でやりたかった(´;ω;`)
43kb→63kb程度の増加ですので、たぶん大丈夫だと思う。
コード中身は全然見れてないので品質に問題あるとは思うので、あくまで参考まで。
image.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?