2020年12月28日月曜日

ブロックチェーン表を複数ワークスペースから使用してみる

 この記事からの継続です。いまのところOracle Database 21cに実装されているブロックチェーン表は、分散型台帳として使用するための分散の部分がまだ実装されていません(私個人の見解あり、所属先の見解ではありません)。

この状態ではブロックチェーン表で何ができるのか、イメージを掴むのが難しいように思います。そこで、同一インスタンス内ですが、異なるワークスペース(Oracle Databaseとして見るとスキーマ)を作成して、その間でブロックチェーン表を共用してみます。

Oracle APEX側では、YROLEワークスペースとDROLEワークスペースを作成します。この間で、以前の記事で作成した表BC_HOUSEHOLD_EXPENSESを共用します。

ワークスペースは管理サービスワークスペースの作成を呼び出して追加作成します。

ワークスペースを作成する際に、データベース・ユーザー(スキーマ)をそれぞれ、YROLEDROLEとして作成します。

パスワードは2重入力をしていないので、打ち間違いに要注意です。ワークスペースのログインが拒否された場合、ここで指定したつもりのパスワードが違っていることがあります。その場合は、SQL Developer WebにADMINで接続し、alter user ... identified by パスワードとして、データベース・ユーザーのパスワードを再設定します。alter user文を実行したときに、以下のエラーが出力されず、そのままパスワードの変更が受付けられるようであれば、ワークスペースの作成時に指定したパスワードは指定したはずの文字列とは違っています。

ORA-28007: the password cannot be reused



また、アプリケーション・ビルダーへのログイン時の認証には、データベース・ユーザーのパスワードが使用されますが、作成したアプリケーションの認証スキームApplication Expressアカウント(これがデフォルト)である場合、そのパスワードもユーザーとグループの管理に含まれるユーザーの編集から変更する必要があります。(この開発ユーザーの扱いの違いはAutonomous Database限定です)。

次に、opensslを使って、YROLEとDROLE、それぞれの秘密鍵と証明書を生成します。使用したコマンドを列記します。この中で使用するのは、YROLEの秘密鍵private-yrole.pem、証明書cert-yrole.crt、DROLEの秘密鍵private-drole.pem、証明書cert-drole.crtです。

openssl genrsa -out private-yrole.pem 2048

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

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

openssl genrsa -out private-drole.pem 2048

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

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


証明書のDistinguished Nameに含まれる値で、最低限、Common NameとEmail Addressはそれぞれ異なる値を指定します。yroleまたはdroleといった名前を含めておくと、後でALL_CERTIFICATESビューを検索してGUIDを見つけやすくなるでしょう。

SQL Developer WebにADMINユーザーでログインし、YROLEおよびDROLEに必要な権限を割り与えます。オーナー名の部分は表BC_HOUSEHOLD_EXPENSESを保持するスキーマ名に置き換えます(通常は表BC_HOUSEHOLD_EXPENSESを作成したときのAPEXのワークスペース名と同一です)。

grant execute on dbms_crypto to drole;
grant execute on dbms_crypto to yrole;
grant all on オーナー名.bc_household_expenses to drole;
grant all on オーナー名.bc_household_expenses to yrole;

作成したワークスペースYROLEにサインインし、表BC_HOUSEHOLD_EXPENSES使うアプリケーションを開発します。


YROLEで表BC_HOUSEHOLD_EXPENSESを扱いやすくするよう、こちらの記事で作成したビューBC_HOUSEHOLD_EXPENSES_Vを作ります。オーナー名の部分は、表BC_HOUSEHOLD_EXPENSESのオーナーに変更します。

create view bc_household_expenses_v
as
select expense_id, place_date, jt.*
from オーナー名.bc_household_expenses,
json_table(flex_column, '$'
COLUMNS(
item varchar2(80) path '$.item',
category varchar2(200) path '$.category',
income number path '$.income',
expense number path '$.expense',
note varchar2(255) path '$.note'
)
) as jt;

クイックSQLを呼び出し、表MY_EXPENSESを以下の定義で作成します。

my_expenses
place_date
item vc80
category vc200
income num
expense num
note vc255


表が作成されたら、そのままアプリケーション作成ウィザードを呼び出します。


アプリケーションの名前私の出納帳と指定して、アプリケーションの作成を実行します。作成した表MY_EXPENSESを扱う対話モード・レポートとフォームが作成されます。


アプリケーションが作成されます。対話モード・レポートを開いて、いくつかデータを投入してみます。


データを投入すると、レポートにも表示されます。


対話モード・レポートに表BC_HOUSEHOLD_EXPENSESの行も同時に表示されるようにします。

その前に、表BC_HOUSEHOLD_EXPENSESの内容を表示するフォームを作成します。

ページ作成ウィザードを起動し、コンポーネントフォームを選択します。


次にフォームを選びます。


ページ名共同経費ページ・モードモーダル・ダイアログを指定します。ページ番号は4番になっているはずです。そうでない場合は4番に変更します。


ページ・モードがモーダル・ダイアログなので、ナビゲーション・メニュー・エントリは不要(このページとナビゲーション・メニュー・エントリを関連づけない)です。

表/ビューの名前として、BC_HOUSEHOLD_EXPENSES_V(ビュー)を選択します。


主キーとしてEXPENSE_ID(Number)を選択し、フォームのページを作成します。これでページが完成しました。


次に対話モード・レポートを更新します。対話モード・レポートのソースを以下のSQLに変更します。先ほど作成したフォームのページは4番であることを前提としています。

select
ID,
PLACE_DATE,
ITEM,
CATEGORY,
INCOME,
EXPENSE,
NOTE,
apex_page.get_url(p_page => 3, p_items => 'P3_ID', p_values => ID) as link
from MY_EXPENSES
union
select
expense_id id,
place_date,
item,
category,
income,
expense,
note,
apex_page.get_url(p_page => 4, p_items => 'P4_EXPENSE_ID', p_values => expense_id) as link
from bc_household_expenses_v


列IDとLINKは、非表示列として設定します。


対話モード・レポートのAttributesを開き、リンクリンク列カスタム・ターゲットへのリンクターゲット#LINK#とします。表BC_HOUSEHOLD_EXPENSESと表MY_EXPENSESで開くフォームの切り替えは、対話モード・レポートのSQLのapex_page.get_urlファンクションの引数p_pageの指定を変えることで行っています。


これで対話モード・レポートに、表BC_HOUSEHOLD_EXPENSESと表MY_EXPENSESの内容が両方とも表示されます。また、編集アイコンをクリックすることで、それぞれが(異なる)フォームで表示されます。


現時点では、このアプリケーションから表BC_HOUSEHOLD_EXPENSESという共同の出納帳に書き込む機能がありません。これを追加します。

カテゴリが光熱費、食費、給与である場合、表MY_EXPENSESではなく、表BC_HOUSEHOLD_EXPENSESに書き込むことにします。

このスキーマに証明書を登録します。生成した証明書cert-yrole.crtをHTTPS経由で取得できる場所に配置しておきます。以下の例ではcert-yrole.crtを登録しています。

declare
  l_cert blob;
  l_cert_guid raw(16);
begin
  l_cert := apex_web_service.make_rest_request_b(
      p_url => 'https://どこかにファイルを配置します/cert-yrole.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;

次にSQLコマンドから、書き込んだ行に署名を付加するプロシージャを定義します。これは、こちらの記事に記載されているsign_bc_household_expnesesとほぼ同じです。prvkeyおよびcert_guidはYROLE向けに置き換えます。表BC_HOUSEHOLD_EXPENSESのオーナーも置き換えます。

create or replace procedure sign_bc_household_expenses
(p_expense_id in number)
as
-- sign row
l_inst_id binary_integer;
l_chain_id binary_integer;
l_sequence_no binary_integer;
l_row_data blob;
-- RSA sign
l_raw raw(2000);
prvkey varchar2(2000) := 'YROLEの秘密鍵';
sign_raw raw(2000);
sType PLS_INTEGER := DBMS_CRYPTO.SIGN_SHA512_RSA;
kType PLS_INTEGER := DBMS_CRYPTO.KEY_TYPE_RSA;
cert_guid raw(16) := hextoraw('YROLEの証明書のGUID');
begin
-- sign row
select orabctab_inst_id$, orabctab_chain_id$, orabctab_seq_num$
into l_inst_id, l_chain_id, l_sequence_no
from <オーナー名>.bc_household_expenses where expense_id = p_expense_id;
dbms_blockchain_table.get_bytes_for_row_signature(
'<オーナー名>',
'BC_HOUSEHOLD_EXPENSES',
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));
sign_raw := dbms_crypto.sign
(
src => l_raw,
prv_key => utl_i18n.string_to_raw(prvkey, 'AL32UTF8'),
pubkey_alg => kType,
sign_alg => sType
);
dbms_blockchain_table.sign_row(
'<オーナー名>',
'BC_HOUSEHOLD_EXPENSES',
l_inst_id,
l_chain_id,
l_sequence_no,
NULL,
sign_raw,
cert_guid,
DBMS_BLOCKCHAIN_TABLE.SIGN_ALGO_RSA_SHA2_512
);
end sign_bc_household_expenses;

以下のPL/SQLスクリプトで、表BC_HOUSEHOLD_EXPENESESへの書き込みを確認します。

declare
l_expense_id number;
begin
begin
insert into apexdev.bc_household_expenses(place_date, flex_column)
values(sysdate, '{ "item": "豆腐", "category": "食費", "expense": 128 }')
returning expense_id into l_expense_id;
dbms_output.put_line('EXPENSE_ID = ' || l_expense_id);
commit;
end;
sign_bc_household_expenses(l_expense_id);
end;

表示されるEXPENSE_IDを使って、ORABCTAB_SIGNATURE$, ORABCTAB_SIGNATURE_ALG$, ORABCTAB_SIGNATURE_CERT$を確認します。

select lengthb(orabctab_signature$), orabctab_signature_alg$, orabctab_signature_cert$
from
apexdev.bc_household_expenses where expense_id = <EXPENSE_ID>;

データを書き込むフォームにプロセスを追加します。最初に既存のプロセスが、カテゴリ(P3_CATEGORY)が光熱費:食費:給与である場合に呼び出されない(タイプアイテムはコロンで区切られたリストに含まれない)ように、サーバー側の条件を設定します。


次に、表BC_HOUSEHOLD_EXPENSESに行を追加するプロセスを追加します。

プロセスの名前共同出納帳への書き込みとし、タイプフォーム - 行の自動処理(DML)フォーム・リージョンMy Expenseを選びます。ターゲット・タイプPL/SQL Code失われた更新の防止OFFです。


設定するPL/SQLコードは以下です。オーナー名の部分は表BC_HOUSEHOLD_EXPENSESのオーナー名に変更します。

declare
l_expense_id number;
l_flex_column json;
l_jo json_object_t;
begin
case :APEX$ROW_STATUS
when 'C' then
begin
l_jo := json_object_t();
if :P3_ITEM is not null then
l_jo.put('item', :P3_ITEM);
end if;
l_jo.put('category', :P3_CATEGORY);
if :P3_INCOME is not null then
l_jo.put('income', :P3_INCOME);
end if;
if :P3_EXPENSE is not null then
l_jo.put('expense', :P3_EXPENSE);
end if;
if :P3_NOTE is not null then
l_jo.put('note', :P3_NOTE);
end if;
l_flex_column := l_jo.to_json;
insert into オーナー名.bc_household_expenses(place_date, flex_column)
values(sysdate, l_flex_column)
returning expense_id into l_expense_id;
commit;
end;
sign_bc_household_expenses(l_expense_id);
end case;
end;

表MY_EXPENSESへの書き込みとは反対に、カテゴリが光熱費、食費、給与の場合に呼び出されるよう、サーバー側の条件を設定します。ボタン押下時CREATEタイプアイテムはコロンで区切られたリストに含まれるアイテムP3_CATEGORYリスト光熱費:食費:給与です。


これでYROLEワークスペースでのアプリケーションは完成とします。

次にDROLEワークスペースに同様のアプリケーションを作成します。同じ作業を繰り返すのを避けるため、今まで作成したアプリケーションをエクスポートして、それをDROLEワークスペースにインポートします。

作成した表、ビュー、プロシージャも同時にエクスポート/インポートするように、インストール・スクリプトを追加します。

サポートするオブジェクトを開きます。


インストール・スクリプトを開きます。


作成をクリックします。


データベース・オブジェクトから作成を選択します。


作成されるスクリプトの名前myobjectsとし、オブジェクト・タイプすべて選択し、に進みます。


スクリプトに含めるオブジェクトとして、BC_HOUSEHOLD_EXPENSES_V(VIEW)MY_EXPENSES(TABLE)SIGN_BC_HOUSEHOLD_EXPENSES(PROCEDURE)の3つを選択し、次へ進みます。


生成されたDDLを確認して、作成をクリックします。


インストール・スクリプトが追加されます。これで、エクスポートしたアプリケーションのインポートと同時に、必要とされるデータベース・オブジェクトも作成されます。

続けて、アプリケーションをエクスポートします。


エスクポートを選択します。


サポートするオブジェクトの定義をエスクポートはい、になっていることを確認し、エクスポートを実行します。この設定はデフォルトではい、で、その他の設定もデフォルトのままで十分です。

エクスポートを実行するとfアプリケーション番号.sqlという名称のSQLファイルがダウンロードされます。

YROLEワークスペースからサインアウトし、DROLEワークスペースへサインインします。


SQLワークショップSQLコマンドより、証明書の追加を行います。追加する証明書はcert-drole.crtです。

declare
  l_cert blob;
  l_cert_guid raw(16);
begin
  l_cert := apex_web_service.make_rest_request_b(
      p_url => 'https://どこかにファイルを配置します/cert-drole.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;

出力されたGUIDは記録しておきます。

次に、アプリケーションをインポートします。


ドラッグ・アンド・ドロップ
の領域に、先ほどエクスポートされたファイルを指定し、ファイル・タイプとしては、データベース・アプリケーション、ページまたはコンポーネントのエクスポートを選択します。に進みます。


インポートされたスクリプトからアプリケーションをインストールするため、へ進みます。


別のアプリケーションとしてインストールに、新規アプリケーションIDを自動割り当て、を選択し、アプリケーションのインストールを実行します。


サポートするオブジェクトのインストールONにし、次へ進みます。


インストールを実行します。


インストールが成功し、アプリケーションのサポートしているオブジェクトがインストールされました。と表示されていれば、アプリケーションのエクスポートに含まれている3つのデータベース・オブジェクトも正常に作成されています。


インストール・サマリーからも確認できます。


インポートしたプロシージャSIGN_BC_HOUSEHOLD_EXPENSESにはYROLEの秘密鍵と証明書のGUIDが設定されているので、これをDROLEの秘密鍵、証明書のGUIDに置き換えます。

SQLワークショップオブジェクト・ブラウザよりプロシージャを選んで、SIGN_BC_HOUSEHOLD_EXPENSESコードを開きます。コードのprvkeycert_guidの部分を更新し、保存してコンパイルを実行します。


これでDROLEワークスペースへのアプリケーションのインポートは完了しました。

インポートしたアプリケーションを実行します。表BC_HOUSEHOLD_EXPENSESの内容はリストされていますが、YROLEスキーマに存在するMY_EXPENSES表の内容はリストされていません。


共同の経費となるガスの費用を、カテゴリを光熱費として入力します。


個人経費(光熱費、食費、給与以外)も入力します。


これで、YROLE、DROLEのそれぞれのアプケーションで、共同の出納帳である表BC_HOUSEHOLD_EXPENSESを扱いつつ、自前の表MY_EXPENSESを使うアプリケーションを開発することができました。

このようにして、結局のところ、何が嬉しいのか、というのはあるかもしれません。

共同の出納帳である表BC_HOUSEHOLD_EXPENSESへ、誰が書き込んだのかというのを以下のSQLにて確認できます。DBA_CERTIFICATESビューの権限がないと実行できないので、APEXのワークスペースからではなく、SQL Developer WebにADMINユーザーでサインインして実行します。

select 
  t.expense_id, 
  t.orabctab_inst_id$, t.orabctab_chain_id$, orabctab_seq_num$, 
  c.distinguished_name 
from apexdev.bc_household_expenses t join dba_certificates c
   on t.orabctab_signature_cert$ = certificate_guid
order by t.orabctab_inst_id$, t.orabctab_chain_id$, orabctab_seq_num$

以下のような結果が得られます。


データベースによって以下が保証されている状態で、表を共有できるだけといえば、その通りかな?とも思います。
  1. 書き込まれたデータを改ざんできない(更新できない、削除できない)
  2. 誰が書き込んだのか特定できる
しかし、同様の仕組みを実装するのは並大抵の努力ではできないですし、この仕組みがあることを前提とすることで開発が容易になったり、今までは実現が難しかったアプリケーションも開発できるようになるのではないかという印象を持っています。

簡単なものでも動くアプリケーションを作ることで、イメージを掴むきっかけになったのであれば幸いです。

追記

最後にあれ?と思ったことをいくつか。

その1

証明書の追加はDBMS_USER_CERTS.ADD_CERTIFICATEで行います。追加した証明書は基本的には、ADD_CERTIFICATEしたユーザーにしか見えません。それでもDBMS_BOCKCHAIN_TABLE.VERIFY_ROWSは(別のユーザーから見える証明書を使って)検証が成功します。署名を付けるのはそれぞれのデータベース・ユーザーが行うことですが、署名を検証するのはデータベース・システムの仕事、ということなのかと思いました。

その2

例えば、以下の3行を続けて、表BC_HOUSEHOLD_EXPENSES表にデータを書き込んだとします。

insert into bc_household_expenses(place_date, flex_column) values(sysdate, '{ "item": "水", "category":"食費", "expense": 100 }');
commit;
insert into bc_household_expenses(place_date, flex_column) values(sysdate, '{ "item": "水", "category":"食費", "expense": 99 }');
commit;
insert into bc_household_expenses(place_date, flex_column) values(sysdate, '{ "item": "水", "category":"食費", "expense": 98 }');
commit;

それぞれ、主キーとなるEXPENSE_IDとして41, 42, 43が割り振られたとすると、次のように署名を付けることができます。

begin
  sign_bc_household_expenses(43);
end;
begin
  sign_bc_household_expenses(42);
end;
begin
  sign_bc_household_expenses(41);
end;

つまり、署名は行のハッシュ値には含まれていないようです。一旦、行に署名をすると以降、置き換えることはできないようになっているので、それ自体は問題ではないでしょう。思うに、別のインスタンスとデータを交換する際に署名(DBMS_BLOCKCHAIN_TABLE.SIGN_ROW)および検証(DBMS_BLOCKCHAIN_TABLE.VERIFY_ROWS)が使われ、今回のようにアプリケーションから呼び出すような使い方をするものではないのだと思います。もし、一般の開発者が署名をつけるコードを呼び出すとすると、秘密鍵を管理する必要もありますし、それは相当に荷が重い作業です。

その3

データベースのトランザクションとの関連で、2つのセッションA, Bがあるとして、以下のような順序で書き込みをしたとします。

セッションA: insert into bc_household_expenses(place_date, flex_column) values(sysdate, '{ "item": "水", "category":"食費", "expense": 100 }');
セッションB:insert into bc_household_expenses(place_date, flex_column) values(sysdate, '{ "item": "炭酸水", "category":"食費", "expense": 101 }');
セッションA:insert into bc_household_expenses(place_date, flex_column) values(sysdate, '{ "item": "水", "category":"食費", "expense": 102 }');
セッションB:insert into bc_household_expenses(place_date, flex_column) values(sysdate, '{ "item": "炭酸水", "category":"食費", "expense": 103 }');
セッションB: commit;
セッションA: commit;

セッションBのcommit時にハッシュを計算するとしたら、未コミットのセッションAのデータが読める必要があるし、一体、どのようにしているのかと疑問だったのですが、別のトランザクションではチェーンが異なって(ORABCTAB_CHAIN_ID$が違う)いました。ブロックチェーン表に書き込まれる行すべてが一本のチェーンになる、といった扱いではないようです。

その4

DBMS_USER_CERTS.DROP_CERTIFICATEにて登録済みの証明書をドロップすると、同一の証明書を再度登録してもGUIDは異なるので、それまで署名された行の検証は不可能になります。それはそうなのですが、迂闊なことはできないなと思いました。

追記の追記

以下で同じことでは?といわれると、単一インスタンス内では、確かにあまり変わりが無いかなと思います。
grant select, insert on bc_household_expenses to yrole;
grant select, insert on bc_household_expenses to drole;
分散の部分が実装されれば、今はOracle Databaseのサポート対象から外れたマルチマスター・レプリケーションの機能が、より実用的な形で帰ってくるという印象を持っています。レプリケーションの参加者が適切に振舞うことを要求していませんし(信頼は暗号鍵に依存する)、競合解消も合理的に行われると期待できます。

2020年12月25日金曜日

ブロックチェーン表に21cのDBMS_CRYPTO.SIGNを使って署名を付けてみる

 表題の件を試してみました。その作業ログです。

miyacleさんのこちらの記事を参考にさせてもらっています。ありがとうございます。

最初に鍵長が2048ビットの秘密鍵を作成します。作成されるファイル名はprivate.pemです。

% openssl genrsa -out private.pem 2048

Generating RSA private key, 2048 bit long modulus

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

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

e is 65537 (0x10001)

%


証明書署名要求(CSR)を作ります。private.pemを元にcert.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) []:**

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

Locality Name (eg, city) []:******

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

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

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

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


Please enter the following 'extra' attributes

to be sent with your certificate request

A challenge password []:****

%


Country Name以下はそれぞれ適当に設定します。サーバー証明書に使うわけではないので、それほど厳密でなくても問題ありません。

証明書を作成します。作成されるファイルはcert.crtになります。

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


Signature ok

subject=/C=**/ST=*****/L=********/O=******/OU=****/CN=****.******.****/emailAddress=****.********@*****.****

Getting Private key

%


これで署名に使う証明書や鍵のデータの準備はできました。

実装の検証にAutonomous Databaseを使います。また、ほとんどの作業はOracle APEXのSQLワークショップに含まれるSQLコマンドからPL/SQL、SQLを実行します。

Oracle APEXを使う際にAPEXDEVというユーザーを作成しています。ユーザーを作成すれば、SQL Developer Webからでも同様に作業可能なはずです。

最初にSQL Developer WebにADMINユーザーでログインし、作業ユーザーにDBMS_CRYPTOパッケージの実行権限を与える作業が必要です。

grant execute on dbms_crypto to <ユーザー名>;

これ以降は、作成した一般のユーザーにて作業を行います。

まず、作成した証明書を登録します。どこかHTTPS(HTTPではだめです)でアクセスできる場所にcert.crtを配置します。次に以下のPL/SQLスクリプトを実行します。

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;

apex_web_service.make_rest_request_bファンクションを呼び出して、証明書ファイルを直接BLOBとして取り込みます。それをdbms_user_certs.add_certificateにて、スキーマに登録します。印刷されるGUIDは後で使用するので覚えておきます。(ALL_CERTIFICATESビューを検索して見つけることもできます。)

ブロックチェーン表は以前のこの記事で作成したBC_HOUSEHOLD_EXPENSESを使います。行に署名を追加するプロシージャとしてsign_bc_household_expensesを作成しました。主キーであるEXPENSE_IDを引数に取ります。

create or replace procedure sign_bc_household_expenses 
(p_expense_id in number) 
as  
  -- sign row 
  l_inst_id binary_integer; 
  l_chain_id binary_integer; 
  l_sequence_no binary_integer; 
  l_row_data blob; 
  -- RSA sign 
  l_raw raw(2000); 
  prvkey varchar2(2000) := '一行にしたprivate.pemの内容';
  sign_raw raw(2000); 
  sType      PLS_INTEGER := DBMS_CRYPTO.SIGN_SHA512_RSA; 
  kType      PLS_INTEGER := DBMS_CRYPTO.KEY_TYPE_RSA; 
  cert_guid raw(16) := hextoraw('証明書のGUID'); 
begin 
  -- sign row 
  select orabctab_inst_id$, orabctab_chain_id$, orabctab_seq_num$ 
  into l_inst_id, l_chain_id, l_sequence_no 
  from bc_household_expenses where expense_id = p_expense_id; 
  dbms_blockchain_table.get_bytes_for_row_signature( 
      'APEXDEV', 
      'BC_HOUSEHOLD_EXPENSES', 
      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)); 
  sign_raw := dbms_crypto.sign 
  ( 
    src => l_raw, 
    prv_key => utl_i18n.string_to_raw(prvkey, 'AL32UTF8'), 
    pubkey_alg => kType, 
    sign_alg => sType 
  ); 
  dbms_blockchain_table.sign_row( 
      'APEXDEV', 
      'BC_HOUSEHOLD_EXPENSES', 
      l_inst_id, 
      l_chain_id, 
      l_sequence_no, 
      NULL, 
      sign_raw, 
      cert_guid, 
      DBMS_BLOCKCHAIN_TABLE.SIGN_ALGO_RSA_SHA2_512 
   ); 
end sign_bc_household_expenses;

作成したプロシージャを使って署名を付けます。

begin
sign_bc_household_expenses(1);
end;

一度署名を付けたあと、再度実行すると以下のエラーが出力されました。

ORA-05734: 署名はインスタンスID '1'、チェーンID '21'、順序番号'1'の行にすでに存在します ORA-06512: "SYS.DBMS_BLOCKCHAIN_TABLE", 行747 ORA-06512: "SYS.DBMS_BLOCKCHAIN_TABLE", 行814 ORA-06512: "APEXDEV.SIGN_BC_HOUSEHOLD_EXPENSES", 行38 ORA-06512: 行2 ORA-06512: "SYS.DBMS_SQL", 行1766

DBMS_BLOCKCHAIN_TABLE.VERIFY_ROWSで検証をしてみます。

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

Number of rows verified = 4
といった形でレポートされます。

最初は以前の記事で作成したトリガーに埋め込もうとしたのですが、DBMS_BLOCKCHAIN_TABLE.SIGN_ROWはコミット済みのデータを対象とするようで、トリガー内ではGET_BYTES_FOR_ROW_SIGNATUREがデータを見つけることができません、とエラーになりました。そのため、別プロシージャとして作成しています。

以上で、ブロックチェーン 表の署名をDBMS_CRYPTO.SIGNを使って生成した作業ログは終了です。

21cのブロックチェーン 表を使ってみる

Oracle APEXに直接は関係しないのですが、21cの新機能であるブロックチェーン表を使ってみました。その作業を記録します。

最初にブロックチェーン表を作りました。以下のDDLです。

create blockchain table BC_HOUSEHOLD_EXPENSES
(
    expense_id number generated by default on null as identity,
    place_date date not null,
    flex_column json
)
no drop until 31 days idle no delete locked HASHING USING "SHA2_512" version "v1"
partition by range(place_date)
interval(numtoyminterval(1, 'MONTH'))
(
    partition start_part values less than (to_date('1-12-2020','DD-MM-YYYY'))
);

ブロックチェーン表は一旦作成すると表定義を変更することができないため、データについては、これも21cの新機能であるネィティブのJSON型の列に保存することにします。(21c以前はVARCHAR2、CLOB、BLOB型にJSONを保存していました。)

今回はJSON型そのものを使いたいわけではなく、表定義の変更をできるようにしたいだけです。なので、作成したブロックチェーン表が普通の表に見えるようにビューを定義します。

create view bc_household_expenses_v
as
select expense_id, place_date, jt.*
from bc_household_expenses,
    json_table(flex_column, '$'
    COLUMNS(
        item varchar2(80) path '$.item',
        category varchar2(200) path '$.category',
        income number path '$.income',
        expense number path '$.expense',
        note varchar2(255) path '$.note'
    )
) as jt;

これで、EXPENSE_ID, PLACE_DATE, ITEM, CATEGORY, INCOME, EXPENSE, NOTEが列として見えるビューが定義できました。次に、このビューを対象としてINSERT文が実行できるよう、トリガーを定義します。

create or replace trigger bc_household_expenses_v_ins
instead of insert on bc_household_expenses_v
for each row
declare
  l_json json;
  l_jo json_object_t;
begin
  l_jo := json_object_t();
  if :new.item is not null then
    l_jo.put('item', :new.item);
  end if;
  if :new.category is not null then
    l_jo.put('category', :new.category);
  end if;
  if :new.income is not null then
    l_jo.put('income', :new.income);
  end if;
  if :new.expense is not null then
    l_jo.put('expense', :new.expense);
  end if;
  if :new.note is not null then
    l_jo.put('note', :new.note);
  end if;
  l_json := l_jo.to_json;
  insert into bc_household_expenses(expense_id, place_date, flex_column) values(:new.expense_id, :new.place_date, l_json);
end;

そもそもブロックチェーン表にたいして行える操作はINSERTだけなので、instead of insertのトリガーだけで十分です。

このビューを対象してにして、Oracle APEXのアプリケーションを作ってみます。まず、空っぽのアプリケーションを作成し、それにページを追加します。

追加するページは、フォームです。

フォーム付きレポートを選びます。

レポート・タイプ対話モード・レポートフォーム・ページ・モードモーダル・ダイアログを選びます。ちょっとした確認のためのアプリケーションなので、他は適当に設定し、次へ進みます。

ナビゲーションのプリファレンスは、新規ナビゲーション・メニュー・エントリの作成を選びます。

次に進みます。

データ・ソースローカル・データベースソース・タイプは(実際にはビューを選択しますが)を選びます。表/ビューの名前として先ほど作成したビューBC_HOUSEHOLD_EXPENSES_Vを指定します。次に進みます。

主キー列EXPENSE_IDを選択し、作成します。

これでページが作成されたので実行してみます。

んん?列が正しく認識されていないようです。ページ・デザイナを開いて、作成されているレポートにたいして、列の同期化を実行します。

列の同期化を実行すると、列が認識されたことをページ・デザイナから確認できます。

再度ページを実行します。今度はレポートに列がそれぞれ表示されていることが確認できます。

作成したinstead of triggerによって行の挿入ができることを確認します。作成をクリックします。適当に値を設定し、作成をクリックします。

次のようなエラーがでるはずです。

1つのエラーが発生しました

ページ・デザイナを再度開いて、フォームのデータを挿入するプロセスを選択し、プロパティの挿入後に主キーを返すOFFにします。これはトリガーによる挿入がreturning into句をサポートしていないので、その対応です。

再度、作成をクリックするとデータが挿入されたことが確認できます。


データの編集や削除のボタンもページ作成ウィザードによって作成されていますが、これらは機能しません。

とりあえず、ブロックチェーン表を手軽に使ってみるための定義を紹介してみました。

2020年12月15日火曜日

Oracle Database 21cのRSA暗号を試してみる

Oracle Database 21cより、RSA暗号が提供されています。以前にOracle DatabaseのJavaの実装を使ってJSON WebアルゴリズムのRSASSA-PKCS1-v1_5(RS256)に対応する方法を記事にしましたが、Autonomous DatabaseのようなJavaが使えない構成では不可ですし、パフォーマンス面での懸念もありました。

21cから、DBMS_CRYPTOのPKENCRYPT/PKDECRYPTによるRSA公開鍵暗号による暗号化/復号化、および、SIGN/VERIFYによる署名/検証を行うことができます。マニュアルの記載はこちらです。

動作確認と勉強を兼ねて、マニュアルの例題を動かしてみました。

まず、公開鍵/秘密鍵のペアを生成します。opensslを使った鍵の生成方法は、例題のコメントに記載されています。

最初に鍵長が2048ビットの秘密鍵を生成します。

openssl genrsa -out private.pem 2048

続いて、生成した秘密鍵より公開鍵を、PEM形式で取り出します。

openssl rsa -in private.pem -outform PEM -pubout -out public.pem

以前の記事でも紹介しましたが、生成されている秘密鍵と公開鍵をシェル・スクリプトを使って1行にしました。

#!/bin/sh

while read l
do
   test ${l:0:1} != "-" && /bin/echo -n $l
done < private.pem
echo

公開鍵を1行にするときは、private.pemの代わりに、public.pemを指定します。

マニュアルにあるExample 2: PKENCRYPTION and PKDECRYPTION Functionsを実行します。ファンクション名は正しくは、PKENCRYPTPKDECRYPTです。例題のコードに含まれるファンクション名は正しいのですが、以下の2行でのシングル・クオートが正しくありません

シングル・クオートも含めて、正しくXXXXを公開鍵YYYYを秘密鍵に置き換えます。

  pubkey    VARCHAR (2000) := XXXX;
  prvkey    VARCHAR (2000) := YYYY; 
DECLARE
  ip_str     VARCHAR (200) := 'Secret Message';
  op_str     VARCHAR (200);
  -- Use OpenSSL to generate the private and public key (2048 bit RSA key)
  -- openssl genrsa -out private.pem 2048
  -- openssl rsa -in private.pem -outform PEM -pubout -out public.pem
  pubkey    VARCHAR (2000) := 'ここに1行にしたpublic.pemの内容を含める';
  prvkey    VARCHAR (2000) := 'ここに1行にしたprivate.pemの内容を含める';
  enc_raw    RAW (2000);
  dec_raw    RAW (2000);
  eType      PLS_INTEGER := DBMS_CRYPTO.PKENCRYPT_RSA_PKCS1_OAEP;
  kType      PLS_INTEGER := DBMS_CRYPTO.KEY_TYPE_RSA;

BEGIN

  DBMS_OUTPUT.PUT_LINE('-------------------------------------------------');
  DBMS_OUTPUT.PUT_LINE('Original String := ' || ip_str);
  DBMS_OUTPUT.PUT_LINE('-------------------------------------------------');
  enc_raw:= DBMS_CRYPTO.PKENCRYPT
  (
    src        => UTL_I18N.STRING_TO_RAW(ip_str,'AL32UTF8'),
    pub_key    => UTL_I18N.STRING_TO_RAW( pubkey, 'AL32UTF8'),
    pubkey_alg => kType,
    enc_alg    => eType
  );

  dec_raw := DBMS_CRYPTO.PKDECRYPT
  (
    src        => enc_raw,
    prv_key    => UTL_I18N.STRING_TO_RAW( prvkey, 'AL32UTF8'),
    pubkey_alg => kType,
    enc_alg    => eType
  );
  op_str := UTL_I18N.RAW_TO_CHAR(dec_raw,'AL32UTF8');
  dbms_output.put_line('-------------------------------------------------');
  dbms_output.put_line('Decrypted String := ' || op_str);
  dbms_output.put_line('-------------------------------------------------');
end;
/

実行した結果は以下になります。

------------------------------------------------- Original String := Secret Message ------------------------------------------------- ------------------------------------------------- Decrypted String := Secret Message ------------------------------------------------- PL/SQL procedure successfully completed. Elapsed: 00:00:00.006

次にExample 3: SIGN and VERIFY Functionsを試してみます。鍵の置き換えの注意点はPKENCRYPT/PKDECRYPTと同様です。
DECLARE
  ip_str     VARCHAR2 (200) := 'Secret Message';
  -- Use OpenSSL to generate the private and public key (2048 bit RSA key)
  -- openssl genrsa -out private.pem 2048
  -- openssl rsa -in private.pem -outform PEM -pubout -out public.pem
  pubkey    VARCHAR (2000) := 'ここに1行にしたpublic.pemの内容を含める';
  prvkey    VARCHAR (2000) := 'ここに1行にしたprivate.pemの内容を含める';
  sign_raw   RAW (2000);
  returnval  BOOLEAN := false;
  sType      PLS_INTEGER := DBMS_CRYPTO.SIGN_SHA224_RSA;
  kType      PLS_INTEGER := DBMS_CRYPTO.KEY_TYPE_RSA;
BEGIN
  sign_raw := DBMS_CRYPTO.SIGN
  (
   src        => UTL_I18N.STRING_TO_RAW(ip_str,'AL32UTF8'),
   prv_key    => UTL_I18N.STRING_TO_RAW( prvkey, 'AL32UTF8'),
   pubkey_alg => kType,
   sign_alg   => sType
  );
  returnval := DBMS_CRYPTO.VERIFY
  (
   src        => UTL_I18N.STRING_TO_RAW( ip_str,'AL32UTF8'),
   sign       => sign_raw,
   pub_key    => UTL_I18N.STRING_TO_RAW( pubkey, 'AL32UTF8'),
   pubkey_alg => kType,
   sign_alg   => sType
  );
  DBMS_OUTPUT.PUT_LINE('-------------------------------------------------');
  IF returnval THEN
    DBMS_OUTPUT.PUT_LINE('True');
  ELSE
    DBMS_OUTPUT.PUT_LINE('False');
  END IF;
  DBMS_OUTPUT.PUT_LINE('-------------------------------------------------');
END;
/

実行した結果は以下になります。

------------------------------------------------- True ------------------------------------------------- PL/SQL procedure successfully completed. Elapsed: 00:00:00.008

以上で、マニュアルの例題を用いた確認ができました。APEXでSAML認証をサポートしていないのはRSA公開鍵暗号方式が無いことが理由で、これが実装されたらサポートする方針と聞いているので、早く提供されることを期待しています。

# Oracle Database 19cにバックポートしてほしい。(OCT2020 19.9 DB RUでバックポートされてました)

コマンドラインからアプリケーションをインポートする

 Oracle APEXでは、開発環境を含まない実行環境のみのインストールができます。通常はapexins.sql(またはapexins[123].sql)で開発環境込みでデータベースにインストールしますが、実行環境のみはapxrtins.sql(apxrtins[123].sql)を使ってインストールします。

Oracle APEXの開発環境というのも実際は、4000番代のアプリケーションIDが付与されたOracle APEXのアプリケーションです。これらのアプリケーションが最初からインストールされていなければアクセスもできないので、特に外部公開サイトではセキュリティ面で有利になります。

開発環境が含まれていない環境でどのようにOracle APEXのアプリケーションをインストールするかというと、SQLPlusなどのコマンドライン・ツールとAPEX_APPLICATION_INSTALLパッケージを使用します。相当以前のバージョンから、このパッケージは存在しますが、20.1の日本語マニュアルはこちらです。

このAPEX_APPLICATION_INSTALLAPEX_EXPORTパッケージを活用することで、CI/CDのサイクルに組み込むこともできます。

エクスポートしたアプリケーションがf102.sqlとして存在している場合、以下のようにインポートを実行します。

オンプレミスの場合は、デフォルト・パーシング・スキーマとなるデータベース・アカウントで接続します。Autonomous Databaseの場合は、ワークスペースにログインするユーザー自体がデータベース・アカウントなので、そのアカウントで接続します。

実際のインポート処理は以下のように実行します。APEX_APPLICATION_INSTALLのいくつかのプロシージャを実行し、ワークスペースやスキーマ、アプリケーションIDなどを指定した上で、エクスポートされたアプリケーションであるSQLファイルを実行します。

SQL> begin

  2  apex_application_install.set_workspace('APEXDEV');

  3  apex_application_install.generate_application_id;

  4  apex_application_install.generate_offset;

  5  apex_application_install.set_schema('APEXDEV');

  6  apex_application_install.set_application_alias('F' || apex_application_install.get_application_id );

  7  end;

  8  /


PL/SQL procedure successfully completed.


SQL> @f102.sql

--application/set_environment

APPLICATION 102 - タスク

--application/delete_application

--application/create_application

--application/shared_components/navigation/lists/デスクトップ・ナビゲーション・メニュー

--application/shared_components/navigation/lists/デスクトップ・ナビゲーション・バー

--application/shared_components/navigation/lists/ページ・ナビゲーション

--application/shared_components/navigation/lists/アプリケーション構成

--application/shared_components/navigation/lists/ユーザー・インタフェース

--application/shared_components/navigation/lists/アクティビティ・レポート


[中略]


--application/pages/page_10051

--application/pages/page_10053

--application/pages/page_10054

--application/pages/page_10060

--application/pages/page_10061

--application/deployment/definition

--application/deployment/checks

--application/deployment/buildoptions

--application/end_environment

...done

SQL> 


エクスポートしたのと同じワークスペースにインポートする場合は、もっと指定は単純です。

SQL> begin

  2  apex_application_install.set_application_id(102);

  3  apex_application_install.generate_offset;

  4  apex_application_install.set_application_alias('TASKS1');

  5  end;

  6  /


PL/SQL procedure successfully completed.


SQL> @f102.sql

--application/set_environment

APPLICATION 102 - タスク

--application/delete_application

--application/create_application

--application/shared_components/navigation/lists/デスクトップ・ナビゲーション・メニュー

--application/shared_components/navigation/lists/デスクトップ・ナビゲーション・バー

--application/shared_components/navigation/lists/ページ・ナビゲーション

--application/shared_components/navigation/lists/アプリケーション構成

--application/shared_components/navigation/lists/ユーザー・インタフェース

--application/shared_components/navigation/lists/アクティビティ・レポート


[中略]


--application/pages/page_10051

--application/pages/page_10053

--application/pages/page_10054

--application/pages/page_10060

--application/pages/page_10061

--application/deployment/definition

--application/deployment/checks

--application/deployment/buildoptions

--application/end_environment

...done

SQL> 


インポートに失敗した場合には、エラー・メッセージが表示されます。

SQL> begin

  2  apex_application_install.set_application_id(102);

  3  apex_application_install.generate_offset;

  4  apex_application_install.set_application_alias('TASKS');

  5  end;

  6  /


PL/SQL procedure successfully completed.


SQL> @f102.sql

--application/set_environment

APPLICATION 102 - タスク

--application/delete_application

--application/create_application

begin

*

ERROR at line 1:

ORA-00001: unique constraint (APEX_200200.WWV_FLOWS_ALIAS_IDX) violated

ORA-06512: at "APEX_200200.WWV_FLOW_API", line 2870

ORA-06512: at line 2


すでに別のアプリケーションで、アプリケーションの別名が使用済みであることがわかります。

インストール済みのアプリケーションはAPEX_APPLICATIONSビューを検索することで、コマンドラインから確認できます。

SQL> col application_id format 99999 heading id

SQL> col application_name format a40 heading name

SQL> col alias format a20 heading alias

SQL> select application_id, application_name, alias from apex_applications;


    id name alias

------ ---------------------------------------- --------------------

   101 XLIFFの翻訳                              XLIFFの翻訳

   100 世界食堂                                 WORLDDINER

   105 世界食堂 - 完成品                         CWORLDDINER

   103 タスク                                   TASKS

  1100 タスク                                   F

   102 タスク                                   TASKS1


6 rows selected.


SQL> 


2020年12月11日金曜日

アプリケーションに固定したタイムゾーンの設定

Oracle APEXのアプリケーションへタイムゾーンを設定する方法について、以前にこちらの記事を書いています。以前の記事では、利用者側のタイムゾーンをどのようにOracle APEXのアプリケーションに反映させるか、という方法について説明しています。そうではなく、単にアプリケーションが指定したタイムゾーン、例えば、日本時間で動いてくれれば良い、という場合もあるでしょう。

アメリカでも中国でも、どこからのアクセスでも日本時間で動いてくれたら良いのだけど、サーバーはアメリカにあって、サーバー側のタイムゾーンは変えられない、といった場合にできることを紹介します。

アプリケーションの共有コンポーネントから、セキュリティ属性を開きます。

データベース・セッションのセクションの初期化PL/SQLコードに、タイムゾーンを設定するALTER文を記載します。

BEGIN
   EXECUTE IMMEDIATE 'alter session set time_zone = ''Asia/Tokyo'' ';
  END;

変更の適用をクリックすると、設定したアプリケーションのセッション・タイムゾーンが日本時間になります。

自動タイムゾーンの設定では、ブラウザ上でJavaScriptを動かして取得した時差を、CGIのパラメータtzにセットしてアプリケーションに返すといった、一手間というか一往復のリクエストが追加で発生します。画面左上にSet Time Zoneと表示されている間に行われている作業です。

タイムゾーンを固定するだけであれば、上記のように設定することで、その作業を除くことができます。