【Golang】UniPDFでPDFファイルにデジタル署名する

PDFファイルにデジタル署名ができるGo言語のライブラリ( UniPDF )を使ってみました。

今年(2021年)4月頃に従量課金プランにフリー層ができたとブログ記事に掲載があり、月100件は無料で使える状態になったようです。

以下の公式のブログ記事を参考にサンプルコードを試してみました。 unidoc.io

UniPDF の署名機能を使うと、具体的には以下のようなことができるようです。 (参照)

  1. RSAキーペアによるドキュメントへの署名
  2. PKCS#12ファイルを使用したドキュメントへの署名
  3. 外部の署名生成サービスを利用して文書に署名し、空白の署名欄に署名を付加する
  4. SoftHSMとCrypto11パッケージを使ったPKCS#11サービスによる文書の署名
  5. PDFの電子署名の検証

今回は 「1. RSAキーペアによるドキュメントへの署名」のExampleコードを実行し、内容を確認してみようと思います。

結果

既存のPDFファイルに対してExampleコードを実行したところ、以下のような署名付きのPDFファイルが出力されました。

ページ右下に背景が黄色の署名フィールドが追加され、署名パネルで追加した署名の内容を確認できました。

f:id:moritamorie:20211010232009g:plain

環境

  • Go v1.16.2
  • UniPDF v3.29.0

事前準備

実行するには事前にAPIキーを発行して、環境変数 UNIDOC_LICENSE_API_KEY を設定する必要があります。 Uni Cloudのsignup画面から登録し、以下のAPIキーの画面からAPIキーを発行します。

f:id:moritamorie:20211010233047p:plain

コードの確認

サンプルコードがやっていることをコードスニペットに分解して見ていこうと思います。

①パッケージの読み込み

unidoc/pdf/core, unidoc/pdf/modelは基本的なPDFタイプとドキュメントモデルを提供してくれているようです。 (詳細はUniPDF v2のPress Releaseを参照)

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "crypto/x509/pkix"
    "fmt"
    "log"
    "math/big"
    "os"
    "time"

    "github.com/unidoc/unipdf/v3/annotator"
    "github.com/unidoc/unipdf/v3/common/license"
    "github.com/unidoc/unipdf/v3/core"
    "github.com/unidoc/unipdf/v3/model"
    "github.com/unidoc/unipdf/v3/model/sighandler"
)

②ライセンスAPIキー(環境変数)の読み込み

先ほど設定した UNIDOC_LICENSE_API_KEY環境変数から読み込みます。

func init() {
    err := license.SetMeteredKey(os.Getenv(`UNIDOC_LICENSE_API_KEY`))
    if err != nil {
        panic(err)
    }
}

RSAキーペアとX.509証明書

generateKeys()を実行し、RSAキーペアとX.509証明書を生成します。generateKeys()の中身は折りたたんで掲載しておきます。

   priv, cert, err := generateKeys()
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

④PDFリーダーと署名の生成

PDFリーダー、署名を付加するためのアペンダーを作成します。また、③で作成した秘密鍵と証明書を元に署名ハンドラを作成し、署名ハンドラを元にデジタル署名を作成します。最後にデジタル署名を初期化します。

   file, err := os.Open(inputPath)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }
    defer file.Close()

    reader, err := model.NewPdfReader(file)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    appender, err := model.NewPdfAppender(reader)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    handler, err := sighandler.NewAdobePKCS7Detached(priv, cert)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    signature := model.NewPdfSignature(handler)
    signature.SetName("Test Self Signed PDF")
    signature.SetReason("TestSelfSignedPDF")
    signature.SetDate(now, "")

    if err := signature.Initialize(); err != nil {
        log.Fatal("Fail: %v\n", err)
    }

⑤署名の挿入

最後にPDFファイルにデジタル署名を行い、アノテーションを挿入し、ファイルを出力します。

   opts := annotator.NewSignatureFieldOpts()
    opts.BorderSize = 1
    opts.FontSize = 10
    opts.Rect = []float64{475, 25, 590, 80}
    opts.FillColor = model.NewPdfColorDeviceRGB(255, 255, 0)
    opts.TextColor = model.NewPdfColorDeviceRGB(0, 0, 200)

    field, err := annotator.NewSignatureField(
        signature,
        []*annotator.SignatureLine{
            annotator.NewSignatureLine("Name", "Takashi Morita"),
            annotator.NewSignatureLine("Date", "2021.10.10"),
            annotator.NewSignatureLine("Reason", "Looks good to me"),
            annotator.NewSignatureLine("Location", "Tokyo"),
        },
        opts,
    )
    field.T = core.MakeString("Self signed PDF")

    if err = appender.Sign(1, field); err != nil {
        log.Fatal("Fail: %v\n", err)
    }

    err = appender.WriteToFile(outputPath)
    if err != nil {
        log.Fatal("Fail: %v\n", err)
    }

コード全体

最後にコード全体を掲載しておきます。

サンプルコードの実行

$ go run main.go sample.pdf signed.pdf

出力結果の確認

出力されたPDFファイルをテキストエディタで開いたところ署名オブジェクトは以下のようになっていました。

14 0 obj
<<   /Type /Sig
     /Filter /Adobe.PPKLite
     /SubFilter /adbe.pkcs7.detached
     /Contents (省略)
     /Name (Test Self Signed PDF)
     /Reason (TestSelfSignedPDF)
     /M (D:20211010231753+09'00')
     /ByteRange [0 3457 19843 740]
>>

PDF 1.7の仕様書のDigital Signatureの章を参考にすると以下の値が入るようです。ByteRangeに関しては仕様書読んでもよくわからなかった。。

キー タイプ
Type name (オプション)Sigが入る
Filter name (必須) この署名を検証する際に使用する優先的な署名ハンドラの名前。適合するリーダーは、指定されたサブフィルターフォーマットをサポートしている限り、署名を検証する際に別のハンドラーで代用することができる。署名ハンドラーの例としては、Adobe.PPKLITEEntrust.PPKEFCICI.SignItVerisign.PPKVSがある。
SubFilter name (オプション) 署名辞書の署名値とキー情報のエンコーディングを記述した名前。適合するリーダーは、このフォーマットをサポートする任意のハンドラーを使用して、署名を検証することができる。
Contents byte string (必須) 署名の値。ByteRangeが存在する場合、値はバイトレンジダイジェストの値を表す16進文字列。
M date (オプション)署名の時間。署名ハンドラによって、検証されない通常のコンピュータの時刻か検証可能な方法で安全なタイムサーバから生成された時刻になる。
ByteRange array (署名フィールドの一部であるすべての署名、および権限辞書のUR3エントリから参照される使用権署名に必要) ダイジェスト計算のための正確なバイト範囲を記述する整数のペア(開始バイトオフセット、バイト単位の長さ)の配列。

また、最近のAdobeプロダクトのバージョン(11.x以降)では、RSAだと adbe.pkcs7.detachedETSI.CAdES.detached がSubFilterとして適合しているようでした。

感想

思ったより簡単に既存のPDFにデジタル署名することができました。RSAやX.509証明書を作る部分はGo言語の標準パッケージ(crypto/rsa, crypto/x509)に任せることができるのが理由なのかもと思いました。

また、長期署名である PAdES に関しても将来的にサポートを予定しているようなので、UniPDFの今後のアップデートに期待したいです。

関連記事

PDFファイルの内容の確認の仕方は以下の記事にまとめていますので、よろしければご参照ください。

simple-minds-think-alike.moritamorie.com

参考資料