syncfusion_flutter_pdfを利用しPDFへ電子署名する

2023/08/06に公開

1.はじめに

Flutterでsyncfusionのパッケージを利用し、PDFへ電子署名してみます。
利用するsyncfusionのパッケージは以下の三つです。

syncfusion_flutter_signaturepadは、手書きの描画ジェスチャを画像へ落とし込むために利用しています。電子署名の機能を持っているのは、syncfusion_flutter_pdfですね。

syncfusionは有償パッケージです。community licenseもありますが、条件に当てはまらない場合はcommercial licenseが必要です。

Disclaimer: This is a commercial package. To use this package, you need to have either a Syncfusion commercial license or Free Syncfusion Community license. For more details, please check the LICENSE file.

https://pub.dev/packages/syncfusion_flutter_pdf/license

2.ゴール

ソースはGitHubに置きました。
アプリの挙動は、GitHubに添付した動画のとおりです。

https://github.com/motucraft/signature_pad

3.pfxファイルの作成(自己署名)

当然ですが、電子署名を行うためには認証局(CA)が発行した証明書が必要ですよね。
ここでは、自己署名証明書を作成して利用することにします。

過去に所属していた某ベンダー時代の話ですが、電子ファイルに手書きで書いた氏名の画像を貼り付けたら、それが電子署名だと思っている人がいて、かなり驚いたことを思い出します(笑)
それなりのキャリアのSEがですよ...? これはさすがに笑ってしまいました...

契約書などに使う電子署名なのに、手書き文字の画像を貼っただけって...
それでは改竄検知もできないし、そんな電子ファイルに法的拘束力はありませんよね。
「本人性」と「非改竄性」の担保が電子署名の要件なのですから。

本人性を認証局(CA)が発行する電子証明書にて、非改竄性をハッシュ関数による「タイムスタンプ」で担保するということですよね。

今回は以下のようにpfxファイルを作成しました。

Create a key

$ openssl genrsa -out key.pem 2048

Create certificate signin request

$ openssl req -new -sha256 -key key.pem -out csr.csr

Create certificate

$ openssl req -x509 -sha256 -days 365 -key key.pem -in csr.csr -out certificate.pem

Convert to .pfx file (PBE-SHA1-3DES Algorithm)

$ openssl pkcs12 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -export -in certificate.pem -inkey key.pem -out certificate.pfx -name "alias"

暗号化アルゴリズムにPBE-SHA1-3DESを利用したのは少し理由がありまして、端的に言えばsyncfusionのバグではないか?と思っています。

GitHubのissueに記載しました。現時点でまだリアクションは無いのですが、これはどう見てもバグですよね。。
https://github.com/syncfusion/flutter-widgets/issues/1323

4.電子署名を行うコード

syncfusionパッケージのexampleや、以下のドキュメントを参考にしました。

https://help.syncfusion.com/file-formats/pdf/working-with-digitalsignature
https://help.syncfusion.com/flutter/pdf/working-with-digital-signature
https://help.syncfusion.com/flutter/pdf-viewer/how-to/add-signaturepad-signatures-to-syncfusion-flutter-pdfviewer

  Future<void> _handleSigningProcess(
      GlobalKey<SfSignaturePadState> signaturePadKey,
      ValueNotifier<Uint8List?> documentBytes) async {
    // Save the signature as PNG image
    final data = await signaturePadKey.currentState?.toImage(pixelRatio: 3.0);
    final bytes = await data?.toByteData(format: ui.ImageByteFormat.png);
    final ByteData docBytes = await rootBundle.load('assets/pdf/invoice.pdf');
    ByteData certBytes = await rootBundle.load('assets/cert/certificate.pfx');
    final Uint8List certificateBytes = certBytes.buffer.asUint8List();

    //  Load the document
    final document = PdfDocument(inputBytes: docBytes.buffer.asUint8List());

    try {
      // Get the first page of the document. The page in which signature need to be added.
      PdfPage page = document.pages[0];

      // Create a digital signature and set the signature information
      PdfSignatureField signatureField = PdfSignatureField(
        page,
        'signature',
        bounds: const Rect.fromLTRB(300, 500, 550, 700),
        signature: PdfSignature(
          // Create a certificate instance from the PFX file with a private key.
          // password:No problem as it is a sample self-signed certificate.
          certificate: PdfCertificate(certificateBytes, 'password123'),
          digestAlgorithm: DigestAlgorithm.sha256,
          cryptographicStandard: CryptographicStandard.cms,
        ),
      );

      // Get the signature field appearance graphics.
      PdfGraphics? graphics = signatureField.appearance.normal.graphics;

      // Draw the signature image in the PDF page.
      graphics?.drawImage(PdfBitmap(bytes!.buffer.asUint8List()),
          const Rect.fromLTWH(0, 0, 250, 100));

      // Add a signature field to the form.
      document.form.fields.add(signatureField);

      // Flatten the PDF form field annotation.
      document.form.flattenAllFields();

      documentBytes.value = Uint8List.fromList(await document.save());
    } finally {
      document.dispose();
    }
  }

PDFファイルは、syncfusionのサンプルで利用されている請求書のPDFファイルを利用しました。
certificate.pfxは、上記で作成した自己署名のpfxファイルです。

電子署名したPDFファイルは、FirebaseのCloud Storageへ保存するようにしてみました。
(コードはGitHubにあります。)

5.電子署名されたPDFファイルを確認

Cloud StorageへアップロードしたPDFファイルを確認すると、以下のように署名されていました。
自己署名証明書を利用していますから、警告は出ていますが電子署名されていること、およびタイムスタンプが付与されていることが確認できます。

6.おわりに

syncfusion_flutter_pdfを利用して、PDFへ電子署名を行うことができました。
自己署名ではなく、CAが発行した証明書を使って試してみたいですね。

あっ、マイナンバーカードの証明書って使えないんですかね。
自分の証明書なので読み取って使うことってできそうな、、

Flutterでマイナンバーカードを読むってできるのでしょうか。今度調べてみよう。
でもSuicaを読もうとするとNFCの知識が必要だったりしますよね、そんなノリだとすると難しそうですよね。

少しググってみると、NFCにもいくつかの種類があるようで...

  • ISO/IEC 14443-3 Type A (NFC-A)
  • ISO/IEC 14443-3 Type B (NFC-B)
    マイナンバーカード、運転免許証
  • JIS X 6319-4 (NFC-F)
    Suicaなど
  • ISO/IEC 15693 (Type-5)

難しそう...

7.注意事項(追記)

重要な点に触れるのを忘れましたので追記します。

ここではSyncfusionのサンプルに倣って、certificate.pfxをフロントエンドで利用するコードを記載しました。しかし、certificate.pfxは秘密鍵と証明書が単一のファイルとして格納されているSSL証明書のアーカイブファイルですから、秘密鍵がフロントエンドに渡ってしまうということになります。

パスワードで保護しているとしても、これは秘密鍵が漏洩するリスクに晒されるということですから、避けるべきだろうと考えます(パスワードもフロントエンドで指定するわけですし)。
つまり、電子署名の処理はバックエンドで処理するのが妥当なアプリケーション方式ではないでしょうか。

Syncfusionが意図しているのは、当事者型の電子署名なのでしょう。当事者型であれば、契約者本人の証明書を利用して電子署名を行うことになると思います。
例えば、e-Taxで確定申告を行う際にマイナンバーカードを利用しますよね。マイナンバーカードにはPKIの秘密鍵が保存されているため、それを利用して電子署名を行っているものと思います。
このようなフロントエンド側で秘密鍵を読み出せるようなケースを意図しているものと理解しました。

当事者型の電子署名に対して、事業者型の電子署名があります。
これはサービス提供事業者の自身の証明書を利用して電子署名を行う方式ですので、この場合にはフロントエンド側で電子署名を行う方式は採用すべきではないのでしょう。

GitHubにもCautionとして追記しました。
https://github.com/motucraft/signature_pad#caution

結局、自前で電子署名を付与するバックエンドサービスを用意するか、Acrobat Signのような有償サービス(Adobe以外にも多数同様のサービスがありますよね)を利用するかという選択になるのではないでしょうか。

Discussion