2021年2月2日火曜日

ブロックチェーン表の行への署名をブラウザにて実施する

ブロックチェーン表への挿入操作をブラウザから実施し、その操作によって挿入した行の署名をブラウザのWeb Cryptography APIを使って行う方法を試してみました。その作業記録です。

以下、作業手順です。

ブロックチェーン表BC_BROWSER_TRANSACTIONSを、以下のDDLで作成しました。

create blockchain table BC_BROWSER_TRANSACTIONS
(
    browser_transaction_id number generated by default on null as identity,
    transaction_name varchar2(80),
    transaction_volume number,
    register_date date
)
no drop until 31 days idle no delete locked HASHING USING "SHA2_512" version "v1";

鍵の生成には以下のコマンドを使っています。

% openssl genrsa -out private.pem 2048

Generating RSA private key, 2048 bit long modulus

..........................+++

..............................................+++

e is 65537 (0x10001)

%

証明書の元になるCSRを生成します。

% openssl req -new -days 365 -key private.pem -out cert.csr

You are about to be asked to enter information that will be incorporated

into your certificate request.

What you are about to enter is what is called a Distinguished Name or a DN.

There are quite a few fields but you can leave some blank

For some fields there will be a default value,

If you enter '.', the field will be left blank.

-----

Country Name (2 letter code) []:JP

State or Province Name (full name) []:Tokyo

Locality Name (eg, city) []:Minato-ku

Organization Name (eg, company) []:*******

Organizational Unit Name (eg, section) []:******

Common Name (eg, fully qualified host name) []:****.****.*****

Email Address []:****.*****@******.com


Please enter the following 'extra' attributes

to be sent with your certificate request

A challenge password []:

%

データベースが署名の検証に使用する証明書を作ります。

% openssl x509 -in cert.csr -out cert.crt -req -signkey private.pem -days 365

Signature ok

subject=/C=JP/ST=Tokyo/L=Minato-ku/O=Oracle/OU=Cloud/CN=apex.oracle.com/emailAddress=yuji.nakakoshi@oracle.com

Getting Private key

% 

Web Cryptography APIで扱えるように、秘密鍵をPKCS8形式に変換します。

% openssl pkcs8 -topk8 -nocrypt -in private.pem -outform DER -out private.der

% 

バイナリを16進数表現に変換します。

% xxd -ps private.der > private.hex 

%

変換された16進数表現を改行なしの1行にします。

% while read l

while> do

while> echo -n $l

while> done < private.hex > private.l

%

以下のPL/SQLスクリプトをOracle APEXのSQLワークショップに含まれるSQLコマンドより実行し、作成した証明書をデータベースに登録します。httpsにてアクセスできる場所に証明書を配置します。

declare
l_cert blob;
l_cert_guid raw(16);
begin
l_cert := apex_web_service.make_rest_request_b(
p_url => 'https://どこかにファイルを配置します/cert.crt',
p_http_method => 'GET'
);
dbms_user_certs.add_certificate(l_cert, l_cert_guid);
dbms_output.put_line('Certificate GUID = ' || l_cert_guid);
end;

ハッシュを取り出すファンクションBC_GET_HASHを作成します。

create or replace function bc_get_hash
(p_browser_transaction_id in number) 
return varchar2
as  
  l_inst_id binary_integer; 
  l_chain_id binary_integer; 
  l_sequence_no binary_integer; 
  l_row_data blob; 
  l_raw raw(2000); 
begin 
  select orabctab_inst_id$, orabctab_chain_id$, orabctab_seq_num$ 
  into l_inst_id, l_chain_id, l_sequence_no 
  from bc_browser_transactions where browser_transaction_id = p_browser_transaction_id;
  --
  dbms_blockchain_table.get_bytes_for_row_signature( 
      'APEXDEV', 
      'BC_BROWSER_TRANSACTIONS', 
      l_inst_id, 
      l_chain_id, 
      l_sequence_no, 
      1, 
      l_row_data 
  ); 
  l_raw := dbms_lob.substr(l_row_data, dbms_lob.getlength(l_row_data));
  return rawtohex(l_raw);
end bc_get_hash;

次に、外部で生成された署名を引数として、それをブロックチェーン表に加えるプロシージャBC_SIGN_ROWを作成します。

create or replace procedure bc_sign_row 
(p_browser_transaction_id in number,
 p_signature in varchar2
) 
as  
  l_inst_id binary_integer; 
  l_chain_id binary_integer; 
  l_sequence_no binary_integer; 
  sign_raw raw(2000); 
  cert_guid raw(16) := hextoraw('証明書のGUID'); 
begin 
  select orabctab_inst_id$, orabctab_chain_id$, orabctab_seq_num$ 
  into l_inst_id, l_chain_id, l_sequence_no 
  from bc_browser_transactions where browser_transaction_id = p_browser_transaction_id; 
  sign_raw := hextoraw(p_signature);
  dbms_blockchain_table.sign_row( 
      'APEXDEV', 
      'BC_BROWSER_TRANSACTIONS', 
      l_inst_id, 
      l_chain_id, 
      l_sequence_no, 
      NULL, 
      sign_raw, 
      cert_guid, 
      DBMS_BLOCKCHAIN_TABLE.SIGN_ALGO_RSA_SHA2_512 
   ); 
end bc_sign_row;

ブラウザで署名をつけるために使用するコードは以下です。

// 16進数表現からバイナリに変換する
// 参照先: https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript
const fromHexString = hexString =>
  new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// バイナリから16進数表現に変換する
// 参照先: https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex
function buf2hex(buffer) { // buffer is an ArrayBuffer
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}

// 16進数表現のhashHexよりバイナリのhashDataを生成する
var hashHex = '95E144F09593033ABFC70AA78922ECDC6108A5E005BE30C10228E357414992216E8DCEF7A3A5163614FD553239DCA7EC57B8E28A8644EEC8AE19EFE40A1F5F6D';
var hashData = fromHexString(hashHex);
// 16進数表現のprvHexよりバイナリのprvDataを生成する
var prvHex = '16進数表記の秘密鍵のデータ';
var prvData = fromHexString(prvHex);

// 署名をつける
// 参照先: http://blog.livedoor.jp/k_urushima/archives/1759093.html
window.crypto.subtle.importKey(
    "pkcs8", prvData, 
    { name: "RSASSA-PKCS1-v1_5", hash: {name: "SHA-512"} },
    true, ["sign"]
).then(
    function(prvKey) {
      return window.crypto.subtle.sign("RSASSA-PKCS1-v1_5", prvKey, hashData);
    }
).then(
    function(sigVal) {
      console.log(buf2hex(sigVal));
    }
);

Oracle APEXのアプリケーションに組み込む前に、動作の確認を行います。

最初にブロックチェーン表BC_BROWSER_TRANSACTIONS表に一行挿入し、主キーであるBROWSER_TRANSACTION_IDを取得します。

declare
l_id number;
begin
insert into bc_browser_transactions(transaction_name, transaction_volume, register_date) values('Test Tx', 4000, sysdate)
returning browser_transaction_id into l_id;
dbms_output.put_line('BROWSER_TRANSCTION_ID = ' || l_id);
end;

以下のように表示されます。

BROWSER_TRANSCTION_ID = 2

次に、ファンクションBC_GET_HASHを呼び出して、行のハッシュ値を取得します。

begin
dbms_output.put_line(bc_get_hash(2));
end;

以下のようにハッシュ値が返されます。

82FE46794AE7052DAB42886E57C7E2C557CF98234989B6067330A1EC1E8D963CE0A4665534181756EB3D618BD7FE1253F64968D3DFF13F0E7AF9C463F5480D4E

このハッシュ値をhashHex、秘密鍵であるファイルprivate.lprvHexに与えて、ブラウザの開発者ツールで、先ほどのJavaScriptを実行します。(httpsのページが開いている状態で開発ツールを動かす必要があります)。

コンソールに出力された署名をコピーします。


プロシージャBC_SIGN_ROWを、BROWSER_TRANSACTION_IDの数値とコピーした署名を引数にして呼び出します。

begin
bc_sign_row(2,
'981e9759d8 --- 電子署名の16進数表現 --- 5b2a38893989b2296cf'
);
end;

以下が表示されます。
文が処理されました。
付加された署名の検証を行います。

declare
l_num number;
begin
dbms_blockchain_table.verify_rows(
schema_name => 'APEXDEV',
table_name => 'BC_BROWSER_TRANSACTIONS',
number_of_rows_verified => l_num
);
dbms_output.put_line('Number of rows verified = ' || l_num);
end;

エラーが発生せず、以下のような出力が得られれば、設定の完了です。
Number of rows verified = 2
ビルディング・ブロックの準備ができたので、これからOracle APEXのアプリケーションを作成します。

まず、空のアプリケーションを作成します。名前Browser Transactionとしました。この作業については、スクリーンショットの添付は省きます。

続いて、ブロックチェーン表に1行書き込むフォームのページを作成します。アプリケーション・ビルダーより、ページの作成を実行します。


ページ・タイプコンポーネントフォームをクリックします。


フォーム付きレポートをクリックします。


ページ属性の設定を行います。レポート・タイプ対話モード・レポートフォーム・ページ・モード標準にします。その他の設定項目は任意です。に進みます。


ナビゲーションのプリファレンスとして、新規ナビゲーション・メニュー・エントリの作成を選択します。に進みます。


データ・ソース表/ビューの名前として、表BC_BROWSER_TRANSACTIONSを選択します。すべての列をレポートの表示対象のままにし、に進みます。


フォームの主キー列として、BROWSER_TRANSACTION_ID(Number)を選択します。作成を実行します。


これでレポートとフォームのページが作成されました。作成したアプリケーションを実行して、動作を確認してみます。最初にレポート・ページを開きます。

挿入済みの行を確認した後、作成をクリックします。


フォームが開くので、それぞれフォームに値を指定し、作成を実行します。


行が挿入されたことを確認します。


これで、通常の表への行の挿入については問題なく実装されていることが確認できました。これから、1行挿入時にブラウザ側で署名をつけるようにします。

フォーム・リージョンを含むページ(ページ番号は3であることを前提とします)をページ・デザイナで開き、ページ・アイテムP3_HASHを追加します。タイプ非表示にします。このページ・アイテムは動的アクションによって値を設定するため、保護された値OFFにします。


同様に、ページ・アイテムP3_SIGNATUREを追加します。タイプ非表示です。また、保護された値OFFにします。


主キーの項目であるP3_BROWSER_TRANSACTION_IDについても、動的アクションにて値を設定するため、保護された値OFFにします。


ボタンCREATE動作アクションを、ページの送信から動的アクションで定義に変更します。


動的アクションの名前作成のクリックとします。発火のタイミングについては、デフォルトでCREATEボタンクリックされたときになります。


最初にサーバー側のコードを実行し、表BC_BROWSER_TRANSACTIONSに行を挿入します。挿入した結果として取得されるBROWSER_TRANSACTION_IDおよび行のハッシュ値をフォームに返します。結果として、ページ・アイテムP3_BROWSER_TRANSACTION_IDP3_HASHに値が設定されます。

PL/SQLコードは以下になります。
declare
    l_id number;
begin
    begin
        insert into bc_browser_transactions
        (
            transaction_name, 
            transaction_volume,
            register_date
        )
        values
        (
            :P3_TRANSACTION_NAME,
            :P3_TRANSACTION_VOLUME,
            :P3_REGISTER_DATE
        )
        returning browser_transaction_id into l_id;
        commit;
    end;
    :P3_HASH := bc_get_hash(l_id);
    :P3_BROWSER_TRANSACTION_ID := l_id;
end;
送信するアイテムは、P3_TRANSACTION_NAME, P3_TRANSACTION_VOLUME, P3_REGISTER_DATEです。

戻すアイテムP3_BROWSER_TRANSACTION_ID, P3_HASHです。

動作に影響はないはずですが、変更イベントの禁止ONとしておきます。


すでに動作を検証したJavaScriptのコードがあります。一部をページのプロパティのファンクションおよびグローバル変数の宣言に記述します。
// 16進数表現からバイナリに変換する
// 参照先: https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript
const fromHexString = hexString =>
  new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// バイナリから16進数表現に変換する
// 参照先: https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex
function buf2hex(buffer) { // buffer is an ArrayBuffer
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}
// 16進数表現のprvHexよりバイナリのprvDataを生成する
var prvHex = '16進数表記の秘密鍵のデータ';
var prvData = fromHexString(prvHex);

Oracle APEXのアプリケーションはサーバー側にコードが保存されるので、このコードではサーバー側に秘密鍵の情報が保存されてしまいます。しかし、JavaScript自体の実行は手元のブラウザで行われるので、このprvHexのデータをローカルから取得するように書き換えることで、ユーザーそれぞれの秘密鍵を使用した署名ができるでしょう。秘密鍵の持ち方については、検討が必要です。

サーバー側でのPL/SQLコードの実行に続いて、JavaScriptコードの実行を行います。実行するJavaScriptのコードは以下になります。
// 署名をつける
// 参照先: http://blog.livedoor.jp/k_urushima/archives/1759093.html
window.crypto.subtle.importKey(
    "pkcs8", prvData, 
    { name: "RSASSA-PKCS1-v1_5", hash: {name: "SHA-512"} },
    true, ["sign"]
).then(
    function(prvKey) {
      return window.crypto.subtle.sign("RSASSA-PKCS1-v1_5", prvKey, fromHexString($v('P3_HASH')));
    }
).then(
    function(sigVal) {
        apex.page.submit( {
            request: "CREATE",
            set: {
                "P3_BROWSER_TRANSACTION_ID": $v("P3_BROWSER_TRANSACTION_ID"),
                "P3_SIGNATURE": buf2hex(sigVal)
            },
            showWait: false
        })
    }
);


Web Cryptography APIを使用し、秘密鍵をインポートして、ブラウザ側でブロックチェーン 表から取得したハッシュ値(P3_HASH)に署名を行います。生成した署名(P3_SIGNATURE)と主キーであるP3_BROWSER_TRANSACTION_IDを引数として、apex.page.submitを呼び出し、サーバーに送信します。

ページのプロセスによって、生成した署名をブロックチェーン表に登録します。ボタンCREATEを押したときに起動されるプロセスですが、すでに表BC_BROWSER_TRANSACTIONSへの行の挿入は完了しており、単に署名だけをプロセスで追加します。

フォームを作成した時に作成されるタイプフォーム - 行の自動処理(DML)となっているプロセスがあります。名前はページ名に依存しますが、プロセス・フォームで始まり、ページ名が続きます。

こちらのプロセスの設定ターゲット・タイプPL/SQL Codeに変更します。そして、挿入/更新/削除するPL/SQLコードとして、以下を指定します。
begin
  bc_sign_row(:P3_BROWSER_TRANSACTION_ID, :P3_SIGNATURE);
end;
失われた更新の防止、および、行のロックは両方とも不要なので、OFFおよびNoにします。サーバー側の条件として、ボタン押下時CREATEを選びます。


フォームについてはこれで完成です。最後にレポート上で署名の有無を確認するため、列ORABCTAB_SIGNATURE_CERT$を検索される列に追加します。レポートのページをページ・デザイナで開きます。

ソースのタイプをSQL問合せに変更し、以下のSQLを設定します。
select BROWSER_TRANSACTION_ID,
       TRANSACTION_NAME,
       TRANSACTION_VOLUME,
       REGISTER_DATE,
       ORABCTAB_SIGNATURE_CERT$
from BC_BROWSER_TRANSACTIONS

以上で今回確認したかった、Web Cryptography APIを使用したブロックチェーン表への電子署名の生成については完了です。

このまま実用できる、というものではありませんが、ブラウザ側で電子署名をつけることができる、というのは面白いと思います。Oracle APEXによるアプリケーション作成の一助になれば幸いです。

謝辞

JavaScriptのコードについては、ほとんどWebから見つけたものを使っています。参考にさせていただいたサイトについては、コードの内のコメントとしてURLを記載しています。本当にありがとうございました。