PDF長期署名(PAdES)をJavaで行う方法

要素技術

公開鍵暗号方式

公開鍵暗号方式では、「公開鍵」と「秘密鍵」という2種類の鍵を利用します。
この2種類の鍵は非対称で、「公開鍵」を使って暗号化したものは、「秘密鍵」で復号化できます。

一方、「秘密鍵」を使って暗号化したものは、「公開鍵」で復号化はできませんが、検証は可能です。

一般的な利用方法としては、AさんがBさんに暗号文を送付する場合だと、以下の流れとなります。

Bさんが2種類の鍵を発行し、「公開鍵」を公開する。
AさんはBさんが公開した「公開鍵」を入手し、平文を暗号化し、Bさんに送付する。
Bさんは「暗号鍵」を使って暗号文を復号化する。

電子署名では、上記の例とは逆に当事者以外が保持していない秘密鍵を使って署名を埋め込むことで、誰が、何を証明しています。

タイムスタンプ

時刻を保証する電子署名で、タイムスタンプに刻印されている時刻以前にその電子文書が存在していたこと(存在証明)と、
その時刻以降、当該文書が改ざんされていないこと(非改ざん証明)を証明しています。

日本ではセイコーやアマノなどが時刻認証業務認定事業者(TSA)として認定を受けています。

PDF長期署名(PAdES)

PDF長期署名(PAdES)はPDFのための長期署名の仕様です。
標準仕様は以下よりダウンロード可能となっています。
ETSIダウンロードページ

PAdESでは以下の順にPDFに署名を追加していきます(増分更新)。

  1. ES(署名基本)
  2. ES-T(署名タイムスタンプ)
  3. 検証情報の埋め込み
  4. 長期保管状態

最後の署名に利用した証明書の有効期間が切れ失効する前に、より未来日の有効期間を持つ証明書にて次の署名を行うことによって長期にわたる証明を実現しています。

ES(署名基本)

電子署名のみが付与された状態。誰が何をを証明できる。
(通常証明書の有効期間は1〜3年)

ES-T(署名タイムスタンプ)

電子署名+タイムスタンプが付与された状態。誰が何をに加えていつを証明できる。

ドキュメントタイムスタンプ+検証情報の埋め込み

最後の署名に利用した証明書の有効期間内にドキュメントタイムスタンプ(証明書の有効期間は10年)を付与します。
また、署名に利用した全ての証明書の検証情報を収集し、埋め込みを行います。

長期保管状態

ドキュメントタイムスタンプの有効期間が切れる前に、再度3.ドキュメントタイムスタンプ+検証情報の埋め込みを行うことにより、有効期間の延長を行います。

サンプルソースを元に署名方法を確認する

次はJavaでPDF操作を行うライブラリであるPDFBoxのサンプルソースを元に、
電子署名をどのように行うのかを見ていきたいと思います。

まずは、svnコマンドでPDFBoxのソースを取得してください。

svn checkout https://svn.apache.org/repos/asf/pdfbox/trunk/

電子署名を行うサンプルソースは以下のパッケージに格納されています。

org.apache.pdfbox.examples.signature

前提

PDFBoxでは電子署名を行うためのインターフェイスとして以下を提供しており、署名処理はここに実装します。

package org.apache.pdfbox.pdmodel.interactive.digitalsignature;

publicinterface SignatureInterface
{
byte[] sign(InputStream content) throws IOException;
}

ES(署名基本)/ES-T(署名タイムスタンプ)

publicclass CreateSignature extends CreateSignatureBase
{
    // 一部のみ抜粋
public CreateSignature(KeyStore keystore, char[] pin)
throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, CertificateException, IOException
    {
super(keystore, pin);
    }
              ・
              ・
              ・
publicvoid signDetached(PDDocument document, OutputStream output)
throws IOException
    {

// ①署名の属性を指定、署名の/Filter、/Subfilter等の情報を指定。
        PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("Example User");
signature.setLocation("Los Angeles, CA");
signature.setReason("Testing");
              ・
              ・
              ・
// ②第一引数に署名の属性、第二引数に署名処理を行うクラスを設定している。
        // このサンプルソースでは親のCreateSignatureBaseでSignatureInterfaceの実装を提供しているため自分を設定。
document.addSignature(signature, this, signatureOptions);

// ③PDFを増分更新している。
document.saveIncremental(output);
    }
publicabstractclass CreateSignatureBase implements SignatureInterface
{
    // 一部のみ抜粋
@Override
publicbyte[] sign(InputStream content) throws IOException
    {
// cannot be done private (interface)
try
        {
    // ④コンストラクタではKeyStoreが渡され、
           // KeyStoreから取得した秘密鍵および証明書がクラス変数として保持されている。
           // その値とPDFをもとに署名情報を生成する。
            List<Certificate> certList = new ArrayList<>();
certList.addAll(Arrays.asList(certificateChain));
Storecerts = new JcaCertStore(certList);
            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
            org.bouncycastle.asn1.x509.Certificate cert = org.bouncycastle.asn1.x509.Certificate.getInstance(certificateChain[0].getEncoded());
            ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, new X509CertificateHolder(cert)));
gen.addCertificates(certs);
            CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
            CMSSignedData signedData = gen.generate(msg, false);
           // ⑤TSA(タイムスタンプ局)のURLが指定されていた場合は、タイムスタンプ局からタイムスタンプトークンを取得し、署名タイムスタンプを追加。
if (tsaUrl != null && tsaUrl.length() > 0)
            {
                ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl);
signedData = validation.addSignedTimeStamp(signedData);
            }
returnsignedData.getEncoded();
        }
catch (GeneralSecurityException | CMSException | OperatorCreationException e)
        {
thrownew IOException(e);
        }
    }
}

ドキュメントタイムスタンプ+検証情報の埋め込み

ドキュメントタイムスタンプのサンプルソース
publicclass CreateSignedTimeStamp implements SignatureInterface
{
    // 一部のみ抜粋
public CreateSignedTimeStamp(String tsaUrl)
    {
this.tsaUrl = tsaUrl;
    }
              ・
              ・
              ・
publicvoid signDetached(PDDocument document, OutputStream output) throws IOException
    {
// ①署名の属性を指定、署名の/Type、/Filter、/Subfilter等の情報を指定。
        // Type=COSName.DOC_TIME_STAMP
        // Subfilter=COSName.getPDFName("ETSI.RFC3161”)
        // を指定している。
        PDSignature signature = new PDSignature();
signature.setType(COSName.DOC_TIME_STAMP);
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(COSName.getPDFName("ETSI.RFC3161"));

// No certification allowed because /Reference not allowed in signature directory
// see ETSI EN 319 142-1 Part 1 and ETSI TS 102 778-4
// http://www.etsi.org/deliver/etsi_en%5C319100_319199%5C31914201%5C01.01.00_30%5Cen_31914201v010100v.pdf
// http://www.etsi.org/deliver/etsi_ts/102700_102799/10277804/01.01.01_60/ts_10277804v010101p.pdf

// ②署名の属性と、署名を行うクラスを設定している。
document.addSignature(signature, this);

// ③PDFを増分更新している。
document.saveIncremental(output);
    }

@Override
publicbyte[] sign(InputStream content) throws IOException
    {
// ④コンストラクタで指定されたTSAのURLを元にタイムスタンプトークンを取得し、ドキュメントタイムスタンプを追加。
        ValidationTimeStamp validation;
try
        {
validation = new ValidationTimeStamp(tsaUrl);
returnvalidation.getTimeStampToken(content);
        }
catch (NoSuchAlgorithmException e)
        {
LOG.error("Hashing-Algorithm not found for TimeStamping", e);
        }
returnnewbyte[] {};
    }
}
検証情報の埋め込みのサンプルソース
package org.apache.pdfbox.examples.signature.validation;

publicclass AddValidationInformation
{
// 一部のみ抜粋
privatevoid doValidation(String filename, OutputStream output) throws IOException
    {
certInformationHelper = new CertInformationCollector();
        CertSignatureInformation certInfo;
try
        {
certInfo = certInformationHelper.getLastCertInfo(document, filename);
        }
catch (CertificateProccessingException e)
        {
thrownew IOException("An Error occurred processing the Signature", e);
        }
if (certInfo == null)
        {
thrownew IOException(
"No Certificate information or signature found in the given document");
        }

        PDDocumentCatalog docCatalog = document.getDocumentCatalog();
        COSDictionary catalog = docCatalog.getCOSObject();
catalog.setNeedToBeUpdated(true);

        COSDictionary dss = getOrCreateDictionaryEntry(COSDictionary.class, catalog, "DSS");

        addExtensions(docCatalog);

// 収集した各種検証情報の埋め込みを行っている
vriBase = getOrCreateDictionaryEntry(COSDictionary.class, dss, "VRI");

ocsps = getOrCreateDictionaryEntry(COSArray.class, dss, "OCSPs");

crls = getOrCreateDictionaryEntry(COSArray.class, dss, "CRLs");

certs = getOrCreateDictionaryEntry(COSArray.class, dss, "Certs");

        addRevocationData(certInfo);

        addAllCertsToCertArray();

// write incremental
document.saveIncremental(output);
    }
}