Javascriptで APKの証明書フィンガープリントを出力する

:::note info
この記事はV1(JAR)署名に対応したものです。V3署名に対応した続編を書きました。よかったらそちらもご覧ください。
:::

https://qiita.com/kkoiwai/items/d410b054797ff89f6398


Webからアプリへのスムーズな遷移を実現できる、Android App Linksですが、サーバ側の設定のためにはアプリパッケージの証明書のフィンガープリントが必要です。

参考：[App Linksに対応してみた](https://qiita.com/noboru_i/items/fd4634ecb326b3749ac0)
参考：[[Android] apkから証明書のfingerprintを表示する](https://qiita.com/wasnot/items/180c65fdb59abab294af)

通常は、JDKに含まれるKeytoolコマンドを使って出力するのですが、たまたま手元に開発環境の入ったPCがなかったりして、どうしよう！と思ったこと、ありませんか？

そんなときに、ブラウザ一つで、フィンガープリントを出力できるツールを作ってみました。
デモページを作りましたので、よかったら使ってみてください。
※ もちろんJavascriptでローカルで処理していて、外部に情報は送信しないように作っていますが、他人のWebサイトを試す時は常に気をつけてくださいね！

### デモページ
https://kkoiwai.github.io/ApkFingerprintReader/APK_FingerprintReader_en.html


## 試してみる

試しに、LINEアプリのapkをダウンロードして実行してみましょう。

LINEのダウンロードページから、APKをダウンロード
![LINEアプリのダウンロードページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135431/765b9969-4fda-8e60-fce6-db6b5b40e96f.png)
デモページで、読み込んでみます。
![デモサイトでフィンガープリントを出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135431/714e8800-6891-e96c-1b80-ca540e7bf31c.png)
LINEウェブサイトのapp links設定ファイル内のSHA256フィンガープリントと一致することを確認できました、シンプルでいいですね。
![LINEサイトのapp links 設定ファイル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135431/001dcab2-60f2-def1-e129-39875c07efcf.png)

## 作り方

apkの中身はZIPファイルであることはご存知の方は多いと思います。今は、JavascriptでZIP解凍するライブラリがあります。
参考：[[zlib.js] JavaScriptでZIP解凍する](https://qiita.com/yun_bow/items/1b9e86a2efe12432845d)
参考：https://github.com/imaya/zlib.js/


実際のアプリ署名がどうなっているのかは、こちらのサイトを参考にさせていただきました。

[APK ファイルの署名の仕様](https://urandroid.hatenablog.com/entry/20110818/1313656536)
> __META-INF/CERT.RSA の構造と生成__
上述の META-INF/CERT.SF を RSA 暗号鍵と X.509 公開鍵証明書で署名して PKCS #7 形式で表現したものが META-INF/CERT.RSA である。

各種の暗号・証明書の処理をJavacriptで行うライブラリも各種存在しています。下記のライブラリが、PKCS #7形式をサポートしています。

[Forge: JavaScript Security and Cryptography](https://digitalbazaar.com/forge/)

細かい話は省略しますが、Google先生にいろいろ教わって、ごにょごにょやって、なんとか動く物ができました。

## 処理の流れ

詳細は[ソースコード](https://github.com/kkoiwai/ApkFingerprintReader/blob/main/APK_FingerprintReader_en.html)をご参照。

### apkから証明書ファイルを解凍する
多くのapkファイルは ```META-INF/CERT.RSA``` だったけど、時々違うファイル名のものがあった（例えばLINEは ```META-INF/NAVER_JA.RSA```) なので、正規表現で検索します。RSA形式以外のものは今のところぶつかっていない。

```js

                    // ファイルを読み込んで、unzipする
                    var zipArr = new Uint8Array(zipReader.result);
                    var unzip = new Zlib.Unzip(zipArr);

                    // デバッグ用にファイルの一覧を出力
                    var importFileList = unzip.getFilenames();
                    console.log(importFileList);

                    // 証明書ファイルを探す
                    rsafilename = importFileList.find(
                        function (str) {
                            return str.search(/\.RSA/) > -1;
                        }
                    );

                    // 見つかった証明書ファイルを解答する
                    var RsaBuffer = unzip.decompress(rsafilename);
                    console.log(RsaBuffer);
```
###証明書Fingerprintの算出
zlibとforgeでバイナリの持ち方が違ったので地味につまづいた。
あと、公開鍵のFingerprintを出して結果が合わなくてつまづいたり、「証明書のFingerprintは単純にDER形式のハッシュ」という書き込みを間に受けてRSAファイルをそのままSHA256ハッシュしても結果が合わなくてつまづいたりした。

```js
                    // unzipの出力はuint8arrayだが、forgeの入力はbinary-stringなので変換
                    // https://github.com/digitalbazaar/forge/issues/336#issuecomment-164207665
                    // https://stackoverflow.com/questions/16363419/how-to-get-binary-string-from-arraybuffer
                    binstr = String.fromCharCode.apply(null, new Uint16Array(RsaBuffer));
                    console.log(binstr);

                    // https://github.com/digitalbazaar/forge/issues/596
                    // RSAファイルはDER形式、これを読み込んで証明書を取り出す
                    var obj = forge.asn1.fromDer(binstr);
                    var msg = forge.pkcs7.messageFromAsn1(forge.asn1.fromDer(binstr));
                    var cert = msg.certificates[0];

                    // 証明書のFingerprintは証明書自体のDERのSHA256ハッシュ
                    var der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
                    var m = forge.md.sha256.create();
                    m.start();
                    m.update(der);
                    //2桁ごとにコロンで分ける
                    var fingerprint = m.digest()
                        .toHex()
                        .match(/.{2}/g)
                        .join(':')
                        .toUpperCase();
                    console.log(fingerprint)
```

##まとめ
今時、javascriptでなんでもできる世の中なんですね。
そしてなんでもできるようにライブラリを揃えてくださっている開発者のみなさまマジ感謝です。