PDFに電子署名とタイムスタンプを付けたくなった。理由は電子帳簿保存法に対応したPDFを作りたいから。いろいろ調べてみたが買い切りのライブラリで一番安いのは海外製499ユーロだった。国内産が良かったがサブスクだったのだ。正直もうそれでいいかと思ったが、一応自分でもトライしてみることにした。そこでまず最初に下記のサイトを参考にC#化してみようと思った。
PDF/A に電子署名してみる
https://qiita.com/trueroad/items/db29302229707b6114bc
こちらの情報では少し古いQPDF(のC++ライブラリ)を使用していた。自分はC#でやりたいということでQPDFNetに翻訳してやってみようとしたのだが、QPDFとQPDFNetにまったく互換性がないというかC++で使用しているクラスや関数名とQPDFNetで使用できるクラスなどが全く対応していない。QPDFNetはコマンドラインツールであるQPDFをコマンドラインを使わず実行できるようにした感じでクラスの作りがコマンドラインのオプション名に準じている。しかし、C++のQPDFは普通に関数で実行する感じで、もしかするとコマンドラインツールの内部コードで使用している関数をそのまま公開しているのかもしれない。このページのやり方はかなりローレベルというかPDFの署名の仕組みを0ベースで構築している感じでQPDFを使いこなせるようになっても電子署名データを作るデータ範囲を考えたり非常に面倒そうなのでこれは諦めた。
次にネットで情報が多いITextSharpというライブラリを使った方法で検討してみた。よくよく調べてみるとiTextSharpはVersion4までは商用利用OKだったらしいがそれ以降は商用利用不可で結構なライセンス費用を取られることが分かった。これならまだ499ユーロで購入したほうがましということでiTextSharp4を使用した方法で考えてみることにした。実はネットであふれるiTextSharpを使った情報の半数はVersion4時代のものでiTextSharp4を使うと決めてからは早かった。というわけでやり方。
まずライブラリ。ナゲットで
iTextSharp.LGPLv2.Core
をインストール。(追加でPortable.BouncyCastleが自動的に入る)
コードは以下
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Collections;
using iTextSharp.text.pdf;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
private void Signdemo()
{
String TSA_URL = @"http://ts.ssl.com";
String TSA_ACCNT = null;
String TSA_PASSW = null;
ITsaClient tsc = new TsaClientBouncyCastle(TSA_URL, TSA_ACCNT, TSA_PASSW);
var _certificate = new X509Certificate2(@"電子証明書.p12", "password", X509KeyStorageFlags.Exportable);
var certParser = new Org.BouncyCastle.X509.X509CertificateParser();
var chain = new Org.BouncyCastle.X509.X509Certificate[]
{
certParser.ReadCertificate(_certificate.RawData)
};
AsymmetricKeyParameter bouncyCastlePrivateKey = DotNetUtilities.GetKeyPair(_certificate.PrivateKey).Private;
var reader = new PdfReader(@"input.pdf");
var writer = new FileStream(@"Digitally Signed.pdf", FileMode.Create, FileAccess.Write);
PdfStamper st = PdfStamper.CreateSignature(reader, writer, '\0');
PdfSignatureAppearance sap = st.SignatureAppearance;
sap.Location = @"場所名";
sap.Render = PdfSignatureAppearance.SignatureRender.Description;
sap.SetCrypto(bouncyCastlePrivateKey, chain, null, PdfName.AdobePpklite);
sap.Acro6Layers = true;
reader.Appendable = true;
var dic = new PdfSignature(PdfName.AdobePpklite, PdfName.AdbePkcs7Detached);
dic.Reason = sap.Reason;
dic.Contact = sap.Contact;
dic.Location = sap.Location;
dic.Date = new PdfDate(sap.SignDate);
sap.CryptoDictionary = dic;
int contentEstimated = 15000; //you can modify this you have loads of contents in signature
var exc = new Hashtable();
exc[PdfName.Contents] = contentEstimated * 2 + 2;
sap.PreClose(exc);
PdfPkcs7 sgn = new PdfPkcs7(bouncyCastlePrivateKey, chain, null, "SHA-1", false);
IDigest messageDigest = DigestUtilities.GetDigest("SHA-1");
Stream data = sap.RangeStream;
byte[] buf = new byte[contentEstimated];
int n;
while ((n = data.Read(buf, 0, buf.Length)) > 0)
{
messageDigest.BlockUpdate(buf, 0, n);
}
byte[] hash = new byte[messageDigest.GetDigestSize()];
messageDigest.DoFinal(hash, 0);
DateTime cal = DateTime.Now;
byte[] sh = sgn.GetAuthenticatedAttributeBytes(hash, cal, null);
sgn.Update(sh, 0, sh.Length);
byte[] encodedSig = sgn.GetEncodedPkcs7(hash, cal, tsc, null);
if (contentEstimated + 2 < encodedSig.Length)
throw new Exception("Not enough space");
var paddedSig = new byte[contentEstimated];
Array.Copy(encodedSig, 0, paddedSig, 0, encodedSig.Length);
var dic2 = new PdfDictionary();
dic2.Put(PdfName.Contents, new PdfString(paddedSig).SetHexWriting(true));
sap.Close(dic2);
writer.Close();
}
参考にしたのは下記の2つのサイト。どちらもiTextSharp4時代のものらしく非常に参考になった。もしPDFにかかわり続けるなら(それなら使えるライブラリを買ったほうが良いとは思うが)2番目のサイトはアーカイブして保存すべきでしょう。
https://web.archive.org/web/20130329022247/http://itextpdf.sourceforge.net/howtosign.html