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);
    }
}

Zookeeperによる分散システム管理(2)

前回はZookeeperがどのようなものかを簡単にまとめてみました。
今回はその続きということで、実際にJavaのプログラムから触ってみることにします。

ichiwork.hatenablog.com

確認環境の準備

動作確認のための環境準備なので、Dockerを利用してスタンドアロン構成にて環境を準備します。

docker run -d -p 2181:2181 zookeeper

Javaクライアントにて接続してみる

Zookeeperの依存性を追加する

Zookeeperの依存性を追加します。
Mavenを利用している場合、pomファイルに以下の記述を追加します。

<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>

サンプルソース

一般的に利用されるであろう非同期処理パターン
public class Sample01 {
	public static void main(String[] args) throws Exception {
		ZooKeeper zk = new ZooKeeper("127.0.0.1:2181", 3000, null);

		// 非同期でexists呼び出しを行う
		zk.exists("/foo", myWatcher, existsCallBack, null);
		// 確認のためコールバックされる前にスレッドが終了しないようにスリープしておく
		Thread.sleep(3000);
	}

	// Watcherの実装
	static Watcher myWatcher = new Watcher() {
		@Override
		public void process(WatchedEvent event) {
			// WatcherEventを処理
			System.out.println("myWatcher:" + event.toString());
		}
	};

	// コールバックメソッドの実装
	static StatCallback existsCallBack = new StatCallback() {
		@Override
		public void processResult(int rc, String path, Object ctx, Stat stat) {
			System.out.println("existsCallBack:" + path);
		}
	};
}

複数の操作を1つのブロックにまとめてアトミックに実行できるMultitop

public class Sample02 extends Transaction implements Watcher {
	protected Sample02(ZooKeeper zk) {
		super(zk);
	}

	public static void main(String[] args) throws Exception {
		ZooKeeper zk = new ZooKeeper("127.0.0.1:2181", 3000, null);
		Sample02 sample = new Sample02(zk);

		// Multiopの利用
		sample.delete("/foo", -1);
		sample.delete("/bar", -1);
		sample.commit();
	}

	@Override
	public void process(WatchedEvent event) {
		System.out.println(event.toString());
	}
}

Zookeeper本の追加資料のurl
github.com

Zookeeperによる分散システム管理(1)

仕事でZookeeperを直接触る機会があったので「ZooKeeperによる分散システム管理」を読みました。
個人的に大事そうと思ったことをまとめてみました。


はじめに

提供する機能

分散システムの協調動作を実現するための以下の機能を提供する。 ここで言う分散システムとは複数の筐体にまたがって独立並行に動作する複数のコンポーネントからなるシステムである。

  1. 強一貫性、順序保存、耐久性の保証
  2. 典型的な同期機構を実装するための機構
  3. 実際の分散システムが誤動作に導く並行性の様々な側面を扱う簡単な方法

Zookeeperで管理するべきデータ

アプリケーションを設計する際には、アプリケーションデータと制御データとを分離するのが理想である。 Zookeeperのアンサンブルが管理するべきデータは後者である。

基本

znode

Zookeeperではデータをznodeと呼ばれる小さいデータノードに格納する。

形式

バイト配列。

属性
  • 永続znode
  • 短命znode

それを作成したセッションが存在する間のみ有効(クライアントがクラッシュしたり、接続がクローズされたりすると自動的に削除される)。

  • シーケンシャル

/tasks/task-のようなznodeを作成すると、シーケンシャルに採番され/tasks/task-1のようにznodeは作成される。

  • 通知

znodeに対する変更通知トリガーを設定することが可能。 通知は1度きりであるが、他の変更が同じznodeに加えられるよりも先にクライアントに配送されることが保証されている。

  • バージョン

znodeはバージョン管理されており、データが更新されるとインクリメントされる。

アーキテクチャ

クォラム構成

Zookeeperでは構成する全てのサーバにデータを複製するが、クライアントがデータを送信した際に、全てのサーバにデータが格納されるのを待つのではなく、過半数のサーバに対してデータが格納されればOKとしている。 したがって、クォラム構成を組む場合には、最低でも3台のサーバを用意する必要がある。 (障害が許容される台数は半分よりも小さい台数(3台の場合、1台))

セッション

1つのセッションにおいてリクエストの順序は保証される。
複数のスレッドがある場合は、注意が必要。
具体的には、ConnectionLossExceptionによってリクエストを再発行する際には新しいリクエストが生成されるので、元のリクエストよりも後に他のスレッドで生成されたリクエストよりも後に実行される可能性があるため注意が必要。

  1. Disconnected
  2. SyncConnected
  3. AuthFailed
  4. ConnectedReadOnly
  5. SaslAuthenticated
  6. Expired

イベントのタイプ

  1. NodeCreated
  2. NodeDeleted
  3. NodeDataChanged
  4. NodeChildredChanged

運用

再構成

計算機3台からなるZookeeperアンサンブルを作成した後、スケールするために計算機5台で再構成したくなったとする。
その場合、リーダー選出され全てのサーバに同期されるのを確認した後、落として再度起動する。
同期が完全でない状態で落としてしまうと、不整合が発生する可能性があるためである。
なお、スタンドアロン構成からの再構成はできない。

動的再構成の機能は2018/02/21現時点で3.5.3-betaバージョンには盛り込まれているが、最新の安定版である3.4.11には含まれていない。

マルチテナント

1つのZookeeperを複数のアプリケーションで運用する方が、現実的。

理由は以下の3つ。

  1. 信頼性の高いサービス提供のためには専用のハードウェアを用いることが望ましい。
  2. 経験的にバースト的なトラフィックが起こり負荷が上がることはあるが、その後は長い間不活性となる。複数のアプリで利用した方が、資源を有効利用できる。
  3. 2つのアプリケーションが3サーバからなるアンサンブルをそれぞれ持つよりも、5サーバからなる1つのアンサンブルを利用した方が、より障害耐性が高くなる。


その場合、クライアント側の接続識別子に

host:port

ではなく

host:port/appname

のようにすることで、アプリケーション側では専用のZookeeperを利用しているかのように利用することができる。

公開鍵暗号方式(RSA)による暗号化、復号化を試してみる

前回PEM形式のファイルをBouncy Castleを利用して読み込めることが確認できたので、公開鍵暗号方式で暗号化、復号化を試してみようと思います。
ichiwork.hatenablog.com

公開鍵暗号方式では暗号化には公開鍵を利用し、復号化には秘密鍵を利用します。

サンプルソース

メイン

プログラムの流れとしては以下の流れです。

  1. PEMファイルから公開鍵、秘密鍵を読み込む
  2. 公開鍵を利用して暗号化する
  3. 秘密鍵を利用して復号化する
public class CriptoSample {

	private static final String ALGORITHM = "RSA";

	public static void main(String[] args) {
		String org = "パパの頭にちょんまげがあったら、朝の挨拶おはようでござる";

		KeyPair keypair = PEMUtil.pemToKeyPair("rs256.key.pkcs8", "rs256.pub.key");

		try {
			System.out.println("元の文字列:" + org);
			String encrypted = CriptoUtil.cripto(org, keypair.getPublic(), ALGORITHM);
			System.out.println("暗号化後の文字列:" + encrypted);
			String decrypted = CriptoUtil.decrypt(encrypted, keypair.getPrivate(), ALGORITHM);
			System.out.println("複合化後の文字列:" + decrypted);
		} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException
				| BadPaddingException e) {
			e.printStackTrace();
		}
	}
}
PEMファイルから公開鍵、秘密鍵を生成するユーティリティークラス

Bouncy Castleを利用してPEMファイルから公開鍵と秘密鍵を生成しています。

public class PEMUtil {
	private PEMUtil() {
	};

	private static final JcaPEMKeyConverter conv = new JcaPEMKeyConverter().setProvider(new BouncyCastleProvider());

	public static PrivateKey pemToPrivateKey(String privateKeyFilePath) {
		PrivateKey privateKey = null;

		// PEM形式の秘密鍵ファイルからPrivateKeyを生成する
		try (PEMParser parser = new PEMParser(
				new InputStreamReader(ClassLoader.getSystemResourceAsStream(privateKeyFilePath)))) {
			Object obj = parser.readObject();
			if (obj instanceof PrivateKeyInfo) {
				PrivateKeyInfo keyInfo = (PrivateKeyInfo) obj;
				privateKey = conv.getPrivateKey(keyInfo);
			}
			if (obj instanceof PEMKeyPair) {
				PEMKeyPair keyInfo = (PEMKeyPair) obj;
				privateKey = conv.getKeyPair(keyInfo).getPrivate();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		return privateKey;
	}

	public static PublicKey pemToPublicKey(String publicKeyFilePath) {
		PublicKey publicKey = null;

		// PEM形式の公開鍵ファイルからPublicKeyを生成する
		try (PEMParser parser = new PEMParser(
				new InputStreamReader(ClassLoader.getSystemResourceAsStream(publicKeyFilePath)))) {
			Object obj = parser.readObject();
			if (obj instanceof SubjectPublicKeyInfo) {
				SubjectPublicKeyInfo keyInfo = (SubjectPublicKeyInfo) obj;
				publicKey = conv.getPublicKey(keyInfo);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		return publicKey;
	}

	public static KeyPair pemToKeyPair(String privateKeyFilePath, String publicKeyFilePath) {
		PrivateKey privateKey = pemToPrivateKey(privateKeyFilePath);
		PublicKey publicKey = pemToPublicKey(publicKeyFilePath);
		KeyPair keyPair = null;

		// PrivateKey、PublicKeyからKeyPairを生成する
		if (publicKey != null && privateKey != null) {
			keyPair = new KeyPair(publicKey, privateKey);
		}

		return keyPair;
	}
}
暗号化ユーティリティークラス
public class CriptoUtil {
	private CriptoUtil() {
	};

	public static String cripto(String org, Key key, String algorithm) throws NoSuchAlgorithmException,
			NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
		Cipher cipher;
		String dest = null;

		cipher = Cipher.getInstance(algorithm);
		cipher.init(Cipher.ENCRYPT_MODE, key);
		dest = new String(Base64.encode(cipher.doFinal(org.getBytes())));
		return dest;
	}

	public static String decrypt(String encrypted, Key key, String algorithm) throws NoSuchAlgorithmException,
			NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
		Cipher cipher;
		String org = null;

		cipher = Cipher.getInstance(algorithm);
		cipher.init(Cipher.DECRYPT_MODE, key);
		org = new String(cipher.doFinal(Base64.decode(encrypted.getBytes())));
		return org;
	}
}

Bouncy Castleを利用してOpenSSLで生成したPEM形式のファイルを読み込む

今回はOpenSSLで秘密鍵や公開鍵等を生成したときのデフォルトのフォーマットであるPEM形式のファイルをJavaから読み込んで見たいと思います。

OepnSSLで秘密鍵、公開鍵を作成する

サンプルで利用する秘密鍵、公開鍵を作成しておきます。

#[秘密鍵]
openssl genrsa -out rs256.key 2048
#[公開鍵]
openssl rsa -pubout < rs256.key > rs256.pub.key
#上記生成した秘密鍵をpkcs#8へ変換する
openssl pkcs8 -in rs256.key -out rs256.key.pkcs8 -topk8 -nocrypt

Bouncy Castleの依存性を追加する

オープンソースの暗号化ライブラリであるBouncy Castleの依存性を追加します。
Mavenを利用している場合、pomファイルに以下の記述を追加します。

<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
<dependency>
	<groupId>org.bouncycastle</groupId>
	<artifactId>bcpkix-jdk15on</artifactId>
	<version>1.50</version>
</dependency>

サンプルソース

やっていることはPEMParserに作成しておいた公開鍵を読み込ませて、変換後の型にキャストしているだけです。
サンプルでは公開鍵を読み込ませていますが、秘密鍵や証明書でも同じようにできます。

public class PEMParserSample {

	// BouncyCastleがセキュリティプロバイダに登録されていない場合、
	// JcaPEMKeyConverter#setProvider(java.lang.String providerName)を
	// JcaPEMKeyConverter conv = new
	// JcaPEMKeyConverter().setProvider("BC");のようにコールすると
	// 「Caused by: java.security.NoSuchProviderException: no such provider:
	// BC」の例外が発生する
	//
	// 回避方法
	// 1.policyファイルにBouncyCastleを追加
	// security.provider.5=org.bouncycastle.jce.provider.BouncyCastleProvider
	// 2.プログラム上で追加
	// Security.addProvider(new BouncyCastleProvider());
	//
	// ここではJcaPEMKeyConverter#setProvider(java.security.Provider
	// provider)でセキュリティプロバイダを指定している
	private static final JcaPEMKeyConverter conv = new JcaPEMKeyConverter().setProvider(new BouncyCastleProvider());

	public static void main(String[] args) {
		PublicKey publicKey = pemToPublicKey("rs256.pub.key");
	}

	public static PublicKey pemToPublicKey(String publicKeyFilePath) {
		PublicKey publicKey = null;

		// PEM形式の公開鍵ファイルからPublicKeyを生成する
		try (PEMParser parser = new PEMParser(
				new InputStreamReader(ClassLoader.getSystemResourceAsStream(publicKeyFilePath)))) {
			Object obj = parser.readObject();
			if (obj instanceof SubjectPublicKeyInfo) {
				SubjectPublicKeyInfo keyInfo = (SubjectPublicKeyInfo) obj;
				publicKey = conv.getPublicKey(keyInfo);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		return publicKey;
	}

}