2023年12月20日水曜日

Amazon Bedrock Agents APIのリクエストに署名バージョン4の署名を付ける

Oracle APEXのアプリケーションからAWSのREST APIを呼び出すには、リクエストに署名を付加する必要があります。これはAmazon Bedrock Agents APIについても同様です。

AWSからは署名バージョン4のリファレンスとして、以下のページが公開されています。英語ですし内容も難しいので、これを読んで署名処理を実装するのはかなり難しいと思います。

Signing AWS API requestsRequest signature examples
Troubleshoot signed requests for AWS APIs

日本語では、以下の説明がわかりやすかったです。

AWS の API を理解しよう !
中級編 ~ リクエストの署名や CLI/SDK の中身を覗いてみる

いちから署名処理を実装するのは大変なので、Amazon S3にアクセスするために書かれたパッケージより、署名処理の部分を流用することにしました。

plsql-aws-s3

パッケージAWS4_S3_PKGに、Amazon S3を操作するファンクションやプロシージャが実装されています。このパッケージを元に、署名生成に使われているプライベート・ファンクションを残したパッケージAWS4_REST_PKGを作成しました。これらのファンクションでS3に決め打ちになっている部分を、bedrockを呼び出せるように改変しています。

S3を操作するためのファンクションはすべて削除しています。その上で、署名バージョン4を生成し、HTTPのリクエスト・ヘッダーに設定するファンクションset_authorization_headersを新設しています。パッケージAWS4_REST_PKGのパブリックなファンクションはこれだけです。ファンクションset_authorization_headersではREST APIの呼び出しは行わず、apex_web_service.set_request_headersを呼び出して、署名バージョン4による署名を含んだAuthorizationヘッダーおよびその他いくつかの関連したヘッダーの設定だけを行います。

create or replace PACKAGE "AWS4_REST_PKG" as
/**
* AWSのREST APIを呼び出すために署名バージョン4をHTTPのリクエスト・ヘッダーに付与する。
*
* ynakakos, dec 2023.
*
* @param p_endpoint varchar2 エンドポイントの先頭となるサービス名, bedrock, bedrock-agent, bedrock-runtime, bedrock-agent-runtime, etc.
* @param p_http_method varchar2 GET,POST, etc.
* @param p_uri varchar2 e.g. /agents/
* @param p_query_string varchar2 e.g. ?....
* @param p_body clob リクエスト本体
* @param p_aws_id varchar2 アクセスキー
* @param p_aws_key varchar2 シークレットアクセスキー
* @param p_gmt_offset number データベースのタイムゾーンがUTCであれば0
* @param p_aws_region varchar2 デフォルトはus-east-1
* @param p_aws_service varchar2 デフォルトはbedrock
* @return varchar2 REST APIとして呼び出すURL
*/
function set_authorization_headers (
p_endpoint in varchar2
,p_http_method in varchar2
,p_uri in varchar2
,p_query_string in varchar2 default null
,p_body in clob default null
,p_aws_id in varchar2
,p_aws_key in varchar2
,p_gmt_offset in varchar2 default 0
,p_aws_region in varchar2 default 'us-east-1'
,p_aws_service in varchar2 default 'bedrock'
) return varchar2;
end aws4_rest_pkg;
/
aws4_rest_pkg.set_authorization_headresを呼び出した後に、apex_web_service.make_rest_requestを呼び出すことによって、署名が付いたREST APIを発行します。

パッケージAWS4_REST_PKG本体のコードは記事の末尾に添付します。

パッケージAWS4_REST_PKGをインストールした後に実施した、動作確認の作業を記述します。以下の記事と同じ手順で、AWSのIAMユーザーとしてoracletestuserが作成済みで、アクセスキーシークレットアクセスキーが作成済みとします。

Oracle Database 23c FreeにDBMS_CLOUDパッケージを入れてAmazon S3にアクセスする

AWSコンソールよりBedrockのページを開きます。今回はAgents APIを呼び出すことを想定しています。Agentsが使えるリージョンは限られているようなので、メニューにAgentsが表示されない場合は、リージョンを切り替える必要があります。以下の作業はバージニア北部(us-east-1)で実施しています。

Agentsから使える基盤モデルは、現時点ではAnthropicに限られているようです。


Request model accessをクリックしてみます。

AnthropicClaudeおよびClaude Instantについては、Access statusUse case details requiredとなっています。今回はAgentsから実際に基盤モデルを呼び出すのは(お金もかかるので)やめて、署名バージョン4が正しく生成できていることだけを確認することにします。


Agentsのページを開き、新しくAgentを作成します。Create Agentをクリックします。


Agent nameMyAgentとしました。Agent description - optionalには「My First Bedrock Agent.」と記述しています。それ以外はデフォルトのままです。

今回は署名が付与されたREST APIの認証が通ればよいので、Bedrock Agent自体の設定は最低限にします。BedrockのAgentsの詳細については、他の資料にあたっていただくようお願いします。

Nextをクリックします。


基盤モデルの選択画面に移ります。選択できるモデルはAnthropicのClaude Instant V1またはClaude V2です。2023年12月20日時点では、Claude V2.1はComing Soonです。どちらのモデルを選択しても、Claudeは使用をリクエストしないと呼び出せません。

とりあえずClaude Instant V1を選択し、Instructions for the Agentとして「あなたは日本語を話す親切なエージェントです。丁寧な言葉遣いで回答し差別的な用語は使いません。」と記述して、Nextをクリックします。


Action groupsの追加画面が開きます。OpenAIでのFunction Callingと同等の機能と考えて良いかと思います。OpenAIのFunction Callingとは異なり、AgentはActionとして設定したLambdaファンクションの呼び出しまでを実施するようです。

Action groupsとして何も設定せず、Nextをクリックします。


Knowledge baseの追加画面が開きます。OpenAIでのRetrievalに当たる機能かと思います。Amazon S3に保存してあるドキュメントからベクトル埋め込み(embeddings)を生成しOpenSearchやPineconeに保存して、RAG(Retrieval-Augmented Generation)を実行する機能のようです。

Knowledge baseも何も設定せず、Nextをクリックします。


設定内容を確認し、Create Agentをクリックします。


AgentとしてMyAgentが作成されます。これからポリシーを作成するにあたって、アカウントIDAgent IDを使います。そのため、Agent ARNをコピーしておきます。


Identity and Access Management (IAM)のページのポリシーを開き、ポリシーの作成をクリックします。


ポリシーエディタとしてJSONを選択し、JSONでポリシーを記述します。作成されているAgentを一覧するListAgentsと、Agentを指定して情報を取得するGetAgentの呼び出し、および、Agentを指定してAliasをアップデートするUpdateAgentAliasとAgentに処理を依頼するInvokeAgentを許可します。

[アカウントID]の部分はアカウントIDである12桁の数値に置き換えます。[Agent ID]AgentのIDに置き変えます。Agentとして未デプロイのWorking draftを指定するため、Agent AliasとしてTSTALIASIDを指定します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "BedrockConsole",
            "Effect": "Allow",
            "Action": [
                "bedrock:ListAgents",
                "bedrock:GetAgent"
            ],
            "Resource": "*"
        },
        {
            "Sid": "AgentAliasSid",
            "Effect": "Allow",
            "Action": [
                "bedrock:UpdateAgentAlias",
                "bedrock:InvokeAgent"
            ],
            "Resource": [
                "arn:aws:bedrock:us-east-1:[アカウントID]:agent-alias/[Agent ID]/TSTALIASID"
            ]
        }
    ]
}
次へ進みます。


ポリシー名OracleBedrockAgentPolicy_MyAgentと記述します。

ポリシーの作成をクリックすると、Bedrock Agentの呼び出しを許可するポリシーが作成されます。


作成済みのユーザーoracletestuserを開き、許可タブより許可を追加を実行します。


許可を追加の画面でポリシーを直接アタッチするを選択します。アタッチするポリシーとして先ほど作成したポリシーOracleBedrockAgentPolicy_MyAgentを検索し、チェックを入れます。

次へ進みます。


確認画面で許可を追加をクリックします。


Bedrock Agentの作成とアクセスの許可が完了しました。ユーザーoracletestuserのアクセスキーとシークレットアクセスキーは生成済みという前提なので、これでAWS側での準備は完了です。

これからOracle APEXのSQLコマンドからBedrock AgentのREST APIを呼び出してみます。

ListAgentsを呼び出してみます。以下のコードを実行します。

declare
C_AWS_ID constant varchar2(40) := 'アクセスキー';
C_AWS_KEY constant varchar2(40) := 'シークレットアクセスキー';
l_request clob;
l_url varchar2(400);
l_response clob;
begin
l_request := '{ "maxResults": 10 }';
apex_web_service.clear_request_headers;
l_url := aws4_rest_pkg.set_authorization_headers(
p_endpoint => 'bedrock-agent'
,p_http_method => 'POST'
,p_uri => '/agents/'
,p_body => l_request
,p_aws_id => C_AWS_ID
,p_aws_key => C_AWS_KEY
);
-- dbms_output.put_line(l_url);
l_response := apex_web_service.make_rest_request(
p_url => l_url
,p_http_method => 'POST'
,p_body => l_request
);
dbms_output.put_line(substr(l_response,1,1000));
end;
view raw ListAgents.sql hosted with ❤ by GitHub

レスポンスとしてagentSummariesが返されました。REST APIに署名が正しく付けられているようです。


GetAgentを呼び出してみます。以下のコードを実行します。

declare
C_AWS_ID constant varchar2(40) := 'アクセスキー';
C_AWS_KEY constant varchar2(40) := 'シークレットアクセスキー';
C_AGENT_ID constant varchar2(10) := 'Agent ID';
l_url varchar2(400);
l_response clob;
begin
apex_web_service.clear_request_headers;
l_url := aws4_rest_pkg.set_authorization_headers(
p_endpoint => 'bedrock-agent'
,p_http_method => 'GET'
,p_uri => '/agents/' || C_AGENT_ID || '/'
,p_aws_id => C_AWS_ID
,p_aws_key => C_AWS_KEY
);
-- dbms_output.put_line(l_url);
l_response := apex_web_service.make_rest_request(
p_url => l_url
,p_http_method => 'GET'
);
dbms_output.put_line(substr(l_response,1,10000));
end;
view raw GetAgent.sql hosted with ❤ by GitHub


InvokeAgentを呼び出してみます。

declare
C_AWS_ID constant varchar2(40) := 'アクセスキー';
C_AWS_KEY constant varchar2(40) := 'シークレットアクセスキー';
C_AGENT_ID constant varchar2(10) := 'Agent ID';
/* Agentへの問い合わせ */
l_session_id varchar2(32);
l_request_json json_object_t;
l_request clob;
l_url varchar2(4000);
l_response clob;
e_api_call_failed exception;
begin
l_session_id := lower(sys_guid());
l_request_json := json_object_t();
l_request_json.put('inputText', 'こんにちは!');
l_request_json.put('endSession', false);
l_request_json.put('enableTrace', false);
l_request := l_request_json.to_clob();
dbms_output.put_line(l_request);
/* set request header to call bedrock agent */
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false);
l_url := aws4_rest_pkg.set_authorization_headers(
p_endpoint => 'bedrock-agent-runtime'
,p_http_method => 'POST'
,p_uri => '/agents/' || C_AGENT_ID || '/agentAliases/TSTALIASID/sessions/' || l_session_id || '/text'
,p_body => l_request
,p_aws_id => C_AWS_ID
,p_aws_key => C_AWS_KEY
);
-- dbms_output.put_line(l_url);
l_response := apex_web_service.make_rest_request(
p_url => l_url
,p_http_method => 'POST'
,p_body => l_request
);
if apex_web_service.g_status_code <> 200 then
raise e_api_call_failed;
end if;
dbms_output.put_line(substr(l_response,1,4000));
end;
view raw InvokeAgent.sql hosted with ❤ by GitHub

dependencyFailedExceptionが返されます。messageは"Access denied when calling Bedrock. Check your request permissions and retry the request."となっています。これはClaude Instance V1へのアクセスをリクエストしていないためだと思われます。

HTTPのステータス・コードは200が返されているため、署名は正しく生成されていると言えます。


印刷されたレスポンスを見ると、一筋縄では行かなそうなフォーマットでレスポンスが返されています。InvokeAgentのリファレンスのResponse SyntaxはJSONになっています。


Amazon Bedrock Agents APIを呼び出すための署名バージョン4の生成について、パッケージの作成とその動作確認については以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。

補足

パッケージDBMS_CLOUDにSEND_REQUESTというファンクション(およびプロシージャ)があります。開発元に確認してはいませんが、credentialScopeがs3に固定されているように見えます。credentialScopeを指定する引数はありませんし、s3以外のサービスを呼び出すとAuthentication Failedが返されます。




create or replace PACKAGE BODY AWS4_REST_PKG as
-----------------------------------------------------------------------------------------
--
-- AWS Signature Version 4 - reference below
-- http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
--
-- In tribute of Morten Braten and Jason Straub, I have including their work with
-- attributions and left their formatting in place.
--
-- Date: February 2017
-- Author: Christina Moore
--
-- Modifications:
-- cmoore 03MAY 2017
-- escape ampersand in S3 filenames
-- Added function to download BLOB from a URL via HTTPS
-- Added function to get Object Blob from AWS via HTTPS
--
-- cmoore 29APR 2019
-- With Oracle 12.2 there have been significant problems with the resolution of SSL Certs and the use of the
-- Oracle wallet for sites with multi-DNS (wildcard) certs.
-- At Storm Petrel, we have opted to setup a Proxy/Reverse proxy to strip the SSL before Oracle sees it.
-- there is a series of host entries in /etc/hosts that correspond to URLs called
-- and vhost entries on the HTTPS_Proxy server (Apache)
--
-- cmoore jun2021
-- removed aws4_md5 functions (varchar/blob)
-- consolidated the REST calls with internal procedure rest_request_clob
-- and tested
--
-- --------------------------------------------------------------------------------------
-- Modification to call AWS Bedrock API
-- ynakakos dec2023
--
-- 1. s3 related code are removed.
-- 2. function set_authorization_headers is added.
-----------------------------------------------------------------------------------------
-- the following global settings will need to be changed for your environment
g_aws_id varchar2(20) := 'xxx'; -- AWS access key ID
g_aws_key varchar2(40) := 'xxx'; -- AWS secret key
g_gmt_offset number := 0; -- your timezone GMT adjustment
g_aws_region varchar2(40) := 'us-east-1';
/*
* dec2023, ynakakos
* even if g_aws_service is defined, service name "s3" is hard coded in original code.
* modify the original code to use global variable g_aws_service.
*/
g_aws_service varchar2(40) := 'bedrock';
-- this information appears within the XML data that returns.
g_ISO8601_format constant varchar2(30) := 'YYYYMMDD"T"HH24MISS"Z"';
g_aws4_auth constant varchar2(30) := 'AWS4-HMAC-SHA256';
g_package constant varchar2(30) := 'aws4_rest_pkg'; -- changed from aws4_s3_pkg
crlf constant varchar2(2) := chr(13) || chr(10);
cr constant varchar2(2) := chr(13);
lf constant varchar2(1) := chr(10); -- USE THIS FOR NEW LINE!!!!
amp constant varchar2(1) := chr(38);
slsh constant varchar2(3) := '%2F';
-- this is the SHA256 HASH of a empty string. It is used when the request is null
g_null_hash constant varchar2(100):= 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
-- Keys for testing with ?UKASZ ADAMCZAK blog ( http://czak.pl/2015/09/15/s3-rest-api-with-curl.html)
-- Keys also work for testing with AWS page http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
-- uncomment these if you want to run through his example to re-verify the hashing logic
-- g_aws_id varchar2(20) := 'AKIAIOSFODNN7EXAMPLE'; -- AWS access key ID
-- g_aws_key varchar2(40) := 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'; -- AWS secret key
--------------------------------------------------------------------------------
-- S E C T I O N
--
-- Private Functions and Procedures AWS4 Signature and HTTPS Request
--
--------------------------------------------------------------------------------
function aws4_escape (
P_URL in varchar2
) return varchar2
------------------------------------------------------------------------------
-- Function: AWS4 Escape
-- Author: Christina Moore
-- Date: 03MAY2017
-- Version: 0.1
--
-- Returns the AWS4 escape value
--
--
-- Revisions:
--
------------------------------------------------------------------------------
as
l_return varchar2(1000);
begin
l_return := P_URL;
l_return := utl_url.escape(l_return);
l_return := replace(l_return, amp, '%26');
return l_return;
end aws4_escape;
procedure validate_http_method (
P_HTTP_METHOD in varchar2,
P_PROCEDURE in varchar2
)
as
------------------------------------------------------------------------------
-- Function: Validate_HTTP_Method
-- Author: Christina Moore
-- Date: 07FEB2017
-- Version: 0.1
--
-- Confirms HTTP method - GET, POST
--
-- Revisions:
-- added 'HEAD' cmoore 20oct2018
--
------------------------------------------------------------------------------
l_valid boolean := false;
begin
case p_http_method
when 'GET' then l_valid := true;
when 'POST' then l_valid := true;
when 'PUT' then l_valid := true;
when 'DELETE' then l_valid := true;
when 'HEAD' then l_valid := true; -- cmoore 20oct2018
else l_valid := false;
end case; -- p_http_method
if not l_valid then
raise_application_error (-20000,
'HTTP Method is not valid in ' || g_package ||'.'||P_PROCEDURE);
end if; -- l_valid
end validate_http_method;
function aws4_sha256 (
P_STRING varchar2
) return varchar2
as
------------------------------------------------------------------------------
-- Function: AWS4_sha256
-- Author: Christina Moore
-- Date: 04FEB2017
-- Version: 0.1
--
-- SHA256 hash on the string provided
-- AWS requires that the hash is in lower case
--
-- Revisions:
--
------------------------------------------------------------------------------
l_return varchar2(2000);
l_hash raw(2000);
l_source raw(2000);
begin
l_source := utl_i18n.string_to_raw(P_STRING,'AL32UTF8');
l_hash := dbms_crypto.hash(
src => l_source,
typ => dbms_crypto.hash_sh256
);
l_return := lower(rawtohex(l_hash));
return l_return;
end aws4_sha256;
function aws4_sha256 (
P_BLOB in blob
) return varchar2
as
------------------------------------------------------------------------------
-- Function: AWS4_sha256
-- Author: Christina Moore
-- Date: 04FEB2017
-- Version: 0.1
--
-- SHA256 hash on the blob provided
-- AWS requires that the hash is in lower case
--
-- Revisions:
--
------------------------------------------------------------------------------
l_return varchar2(2000);
l_hash raw(2000);
l_source raw(2000);
l_blob_amount integer := 2000;
l_blob_buffer varchar2(4000);
l_blob_pos integer := 1;
begin
--l_source := utl_i18n.string_to_raw(P_STRING,'AL32UTF8');
l_hash := dbms_crypto.hash(
src => P_BLOB,
typ => dbms_crypto.hash_sh256
);
l_return := lower(rawtohex(l_hash));
return l_return;
end aws4_sha256;
function aws4_signing_key (
P_STRING_TO_SIGN varchar2,
P_DATE date
) return varchar2
------------------------------------------------------------------------------
-- Function: AWS4_SIGNING_KEY
-- Author: Christina Moore
-- Date: 04FEB2017
-- Version: 0.1
--
-- Parameters
-- String-to-Sign - the string to sign, see AWS documentation
-- and function signature string in this packages
-- Date - current date
--
-- Follows the guidence of the AWS Signature Version 4 documentation
-- http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
-- In accordance with the documentation, the StringToSign is provided to the function
-- The date is provided so that debugging against known standards is possible.
--
-- Note that the String to Sign is a complicated multi-line effort that starts with
-- AWS-HMAC-SHA256
-- This String to Sign is generated in Function ...
--
-- Revisions:
--
------------------------------------------------------------------------------
as
l_return varchar2(2000);
l_date_string varchar2(50);
l_key_bytes_raw raw(2000);
l_source raw(2000);
l_date_key raw(2000);
l_date_region_key raw(2000);
l_date_region_service_key raw(2000);
l_signing_key raw(2000);
l_signature raw(2000);
l_date date;
begin
-- For testing in accordance with
-- http://czak.pl/2015/09/15/s3-rest-api-with-curl.html
-- use 15 Sep 2015 12:45:00 GMT to get known results
l_date_string := to_char(P_DATE, 'YYYYMMDD');
-- per AWS documentation
-- 2 Signing Key
-- DateKey = HMAC-SHA256("AWS4" + "<SecretAccessKey>","<yyyymmdd>")
l_key_bytes_raw := utl_i18n.string_to_raw('AWS4' || g_aws_key, 'AL32UTF8');
l_source := utl_i18n.string_to_raw(l_date_string, 'AL32UTF8');
l_date_key := dbms_crypto.mac (
src => l_source,
typ => dbms_crypto.hmac_sh256,
key => l_key_bytes_raw
);
-- DateRegionKey = HMAC-SHA256(DateKey,"<aws-region>")
l_source := utl_i18n.string_to_raw(g_aws_region,'AL32UTF8');
l_date_region_key := dbms_crypto.mac (
src => l_source,
typ => dbms_crypto.hmac_sh256,
key => l_date_key
);
-- DateRegionServiceKey = HMAC-SHA256(DateRegionKey,"<aws-service>")
l_source := utl_i18n.string_to_raw(g_aws_service,'AL32UTF8');
l_date_region_service_key := dbms_crypto.mac (
src => l_source,
typ => dbms_crypto.hmac_sh256,
key => l_date_region_key
);
-- SigningKey = HMAC-SHA256(DateRegionServiceKey, "aws4_request")
l_source := utl_i18n.string_to_raw('aws4_request');
l_signing_key := dbms_crypto.mac (
src => l_source,
typ => dbms_crypto.hmac_sh256,
key => l_date_region_service_key
);
-- 3. Signature
-- signature = hex(HMAC-SHA256(SigningKey, StringToSign))
l_source := utl_i18n.string_to_raw(P_STRING_TO_SIGN);
l_signature := dbms_crypto.mac (
src => l_source,
typ => dbms_crypto.hmac_sh256,
key => l_signing_key
);
l_return := lower(rawtohex(l_signature));
return l_return;
end aws4_signing_key;
function ISO_8601 (
P_DATE in timestamp,
P_TIMEZONE in varchar2 default 'UTC'
) return varchar2
as
------------------------------------------------------------------------------
-- Function: ISO_8601
-- Author: Christina Moore
-- Date: 04FEB2017
-- Version: 0.1
--
-- Generates a varchar date in the ISO_8601 format. The function
-- Also converts from the provided timezone to UTC/GMT. It does
-- assume with default that your work and server is on UTC.
--
-- Revisions:
--
------------------------------------------------------------------------------
l_timestamp timestamp;
l_iso_8601 varchar2(60);
begin
-- convert the date/time to UTC/Zulu/GMT
select
cast(P_DATE as timestamp with time zone)
into
l_timestamp
from dual;
-- convert the format to ISO_8601/JSON format
if l_timestamp is not null then
l_iso_8601 := to_char(l_timestamp, g_ISO8601_format);
else
l_iso_8601 := null;
end if;
return l_iso_8601;
end iso_8601;
function canonical_request(
P_BUCKET in varchar2,
P_HTTP_METHOD in varchar2,
P_CANONICAL_URI in varchar2,
P_QUERY_STRING in varchar2,
P_DATE in date,
P_PAYLOAD_HASH in varchar2,
P_CANONICAL_REQUEST out varchar2,
P_URL out varchar2
) return varchar2
as
------------------------------------------------------------------------------
-- Function: Canonical Request
-- Author: Christina Moore
-- Date: 25FEB2017
-- Version: 0.3
--
-- Generates the Canonical Request and the corresponding URL
-- as documented by AWS.
-- http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
-- Their standard defintion looks like this:
-- <HTTPMethod>\n
-- <CanonicalURI>\n
-- <CanonicalQueryString>\n
-- <CanonicalHeaders>\n
-- <SignedHeaders>\n
-- <HashedPayload>
--
-- If AWS returns errors the cause is most likely found in the canonical
-- request.Even the signature doesn't match error. This is still likely
-- found in your canonical request.
--
-- There are variations and judgement calls to make. For example, the AWS
-- documentation will show you both of these two URL
-- Option 1
-- https://s3.amazonaws.com/examplebucket?prefix=somePrefix
-- Option 2
-- https://examplebucket.s3.amazonaws.com?prefix=somePrefix
--
-- What matters is that you stick to one patch until you hit a wall, then
-- change paths and use the other option. In my errors, I have found that
-- Option 1 tends to be more robust. Option 2 tends to be shown with the
-- introductory examples.
--
-- 25FEB2017 cmoore - additional notes on the Options above. The us-east-1
-- also called us-standard doesn't follow the same canonical rules as other
-- newer buckets. What works for eu-central-1 does not work for us-east-1.
-- so I added an 'IF' statement.
--
-- Revisions:
-- 0.2 cmoore 11feb2017
-- left and right parens need to be escaped in the canonical request
-- 0.3 cmoore 25feb2017
-- encountered error PermanentRedirect when using Option 1 above for eu-central-1.
-- Changing to option 2
-- 0.4 cmoore 26feb2017
-- with canonical URI, need to know if there is or is not a slash
-- 0.5 cmoore 03MAY2017
-- using local escape URL function
--
------------------------------------------------------------------------------
l_canonical_request varchar2(4000);
l_http_method varchar2(20);
l_query_string varchar2(1000);
l_uri varchar2(1000);
l_header varchar2(1000);
l_signed_hdr varchar2(1000);
l_content_length varchar2(100);
l_bucket varchar2(100);
l_host varchar2(100);
l_request_hashed varchar2(100);
begin
validate_http_method(P_HTTP_METHOD,'canonical_request');
l_query_string := aws4_escape(P_QUERY_STRING);
-- Strip the ? in case someone adds the question-mark
if substr(P_QUERY_STRING,1,1) = '?' then
l_query_string := substr(l_query_string,2) ;
end if; -- '? is first
-- clean up the query string to meet AWS standards
-- you do not want the slash in the query portion of the URL
l_query_string := replace(l_query_string,'/','%2F');
-- the ( and ) are unreserved characters in accordance to Oracle
-- https://docs.oracle.com/database/121/ARPLS/u_url.htm#ARPLS71584
l_query_string := replace(l_query_string,'(','%28');
l_query_string := replace(l_query_string,')','%29');
/*
* ynakakos, dec2023
* to avoid confusion, remove code when P_BUCKET is null.
*/
-- Option 2
case
when P_CANONICAL_URI is null then
l_uri := '/';
when P_CANONICAL_URI = '/' then
l_uri := '/';
else
if substr(P_CANONICAL_URI,1,1) = '/' then
l_uri := aws4_escape(P_CANONICAL_URI);
else
l_uri := aws4_escape('/' || P_CANONICAL_URI);
end if; -- does canonical URI start with slash, add one if no
end case;
/*
* ynakakos, dec2023
* Original code handles "s3" only. Remove s3 from url to support
* service which is not s3.
* This package does not intend to use with s3 but the original code retained for reference.
*/
if g_aws_service = 's3' then
/* for s3 */
l_host := 'host:' || P_BUCKET || '.s3.' || g_aws_region || '.amazonaws.com';
P_URL := aws4_escape('https://' || P_BUCKET || '.s3.' || g_aws_region || '.amazonaws.com' || P_CANONICAL_URI); -- cmoore 29APR19
else
/* intended to use with bedrock */
l_host := 'host:' || P_BUCKET || '.' || g_aws_region || '.amazonaws.com';
P_URL := aws4_escape('https://' || P_BUCKET || '.' || g_aws_region || '.amazonaws.com' || P_CANONICAL_URI);
end if;
l_header := l_host || lf ||
'x-amz-content-sha256:' || P_PAYLOAD_HASH || lf ||
'x-amz-date:' || ISO_8601(P_DATE) || lf; -- this needs extra line?
l_signed_hdr := 'host;x-amz-content-sha256;x-amz-date';
l_canonical_request := P_HTTP_METHOD || lf ;
l_canonical_request := l_canonical_request || l_uri || lf;
l_canonical_request := l_canonical_request || l_query_string || lf;
l_canonical_request := l_canonical_request || l_header || lf;
l_canonical_request := l_canonical_request || l_signed_hdr || lf;
l_canonical_request := l_canonical_request || P_PAYLOAD_HASH;
-- this value can assist with troubleshooting errors from AWS
P_CANONICAL_REQUEST := l_canonical_request;
if P_QUERY_STRING is not null then
if substr(P_QUERY_STRING,1,1) <> '?' then
P_URL := P_URL || '?';
end if; -- '? is first
--l_query_string := replace(P_QUERY_STRING,'/','%2F');
P_URL := P_URL || l_query_string;
end if; -- query string null?
l_request_hashed := lower(aws4_sha256(l_canonical_request));
return l_request_hashed;
end canonical_request;
function signature_string (
P_REQUEST_HASHED in varchar2,
P_DATE in date
) return varchar2
as
------------------------------------------------------------------------------
-- Function: Signature String
-- Author: Christina Moore
-- Date: 04FEB2017
-- Version: 0.1
--
-- Creates the StringToSign, in accordance with AWS API Documentation
-- http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
--
-- "AWS4-HMAC-SHA256" + "\n" +
-- timeStampISO8601Format + "\n" +
-- <Scope> + "\n" +
-- Hex(SHA256Hash(<CanonicalRequest>))
--
-- Revisions:
--
------------------------------------------------------------------------------
l_response varchar2(100);
l_date_string varchar2(50);
l_time_string varchar2(50);
l_string_to_sign varchar2(4000);
begin
l_time_string := ISO_8601(P_DATE);
l_date_string := to_char(P_DATE, 'YYYYMMDD');
l_string_to_sign := g_aws4_auth || lf;
l_string_to_sign := l_string_to_sign || l_time_string || lf;
l_string_to_sign := l_string_to_sign || l_date_string || '/' || g_aws_region || '/' || g_aws_service || '/aws4_request' || lf; -- change s3 to g_aws_service, ynakakos dec2023
l_string_to_sign := l_string_to_sign || P_REQUEST_HASHED;
return(l_string_to_sign);
end signature_string;
function aws4_signature (
P_REQUEST_HASHED in varchar2,
P_DATE in date
) return varchar2
as
------------------------------------------------------------------------------
-- Function: AWS4 Signature
-- Author: Christina Moore
-- Date: 04FEB2017
-- Version: 0.1
--
-- Parameters
-- Request Hashed - the Canonical Request that has been hashed
-- Date - the date likely sysdate
--
-- Gets the String-To-Sign and hands it to the AWS Signing Key
--
-- Revisions:
--
------------------------------------------------------------------------------
l_signature varchar2(100);
l_sign_string varchar2(4000);
begin
l_sign_string := signature_string(
P_REQUEST_HASHED => P_REQUEST_HASHED,
P_DATE => P_DATE
);
l_signature := aws4_signing_key(l_sign_string, P_DATE);
return l_signature;
end aws4_signature;
function prep_aws_data (
P_BUCKET in varchar2,
P_HTTP_METHOD in varchar2,
P_CANONICAL_URI in varchar2,
P_QUERY_STRING in varchar2,
P_DATE in date,
P_PAYLOAD_HASH in varchar2,
P_CONTENT_LENGTH in number default null,
P_CANONICAL_REQUEST out varchar2,
P_URL out varchar2
) return varchar2
as
------------------------------------------------------------------------------
-- Function: Prep AWS Data
-- Author: Christina Moore
-- Date: 04FEB2017
-- Version: 0.1
--
-- Returns the AWS4 signature
-- Parameters
-- Bucket - name of the bucket
-- HTTP Method - GET, POST, PUT
-- Canonical URI - most likely the /. Cleaner when using prefices
-- Query String - These are likely derived from AWS parameters in their documentation
-- Date - most likely sysdate
-- Payload Hash - the SHA256 hash of the payload or a empty line (a constant)
-- Canonical Request - this is returned to aid in debugging
-- URL - needed to make the HTTPS call
--
-- http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
-- Task 1 Creates a Canonical Request
-- And creates the URL so that they match. AWS docs are soft on this
-- Task 2 Creates as String to Sign
-- Task 3 Calculates Signature
--
--
-- Revisions:
--
------------------------------------------------------------------------------
l_request_hashed varchar2(100);
l_signature varchar2(100);
l_url varchar2(4000);
begin
l_request_hashed := canonical_request (
P_BUCKET => P_BUCKET,
P_HTTP_METHOD => P_HTTP_METHOD,
P_CANONICAL_URI => P_CANONICAL_URI,
P_QUERY_STRING => P_QUERY_STRING,
P_DATE => P_DATE,
P_CANONICAL_REQUEST => P_CANONICAL_REQUEST,
P_PAYLOAD_HASH => P_PAYLOAD_HASH,
P_URL => P_URL
);
l_signature := aws4_signature (
P_REQUEST_HASHED => l_request_hashed,
P_DATE => P_DATE
);
return l_signature;
end prep_aws_data;
/**
* AWSのREST API呼び出しに必要なAuthorizationヘッダーなどを設定する。
* apex_web_service.clear_request_headersは呼び出し元が実行する。
*/
function set_authorization_headers (
p_endpoint in varchar2
,p_http_method in varchar2
,p_uri in varchar2
,p_query_string in varchar2 default null
,p_body in clob default null
,p_aws_id in varchar2
,p_aws_key in varchar2
,p_gmt_offset in varchar2 default 0
,p_aws_region in varchar2 default 'us-east-1'
,p_aws_service in varchar2 default 'bedrock'
) return varchar2
as
l_date date;
l_date_string varchar2(50);
l_time_string varchar2(50);
l_payload_hash varchar2(100);
l_canonical_request varchar2(4000);
l_signature varchar2(4000);
l_url varchar2(4000);
l_authorization_value varchar2(4000);
begin
/* initialize global variables */
g_aws_id := p_aws_id;
g_aws_key := p_aws_key;
g_gmt_offset := p_gmt_offset;
g_aws_region := p_aws_region;
g_aws_service := p_aws_service;
if p_body is not null then
l_payload_hash := aws4_sha256(
p_blob => apex_util.clob_to_blob(p_body)
);
else
l_payload_hash := g_null_hash;
end if;
l_date := systimestamp; -- cmoore 20oct2018
l_date_string := to_char(l_date, 'YYYYMMDD');
l_time_string := ISO_8601(l_date);
l_signature := prep_aws_data (
P_BUCKET => p_endpoint,
P_HTTP_METHOD => p_http_method,
P_CANONICAL_URI => p_uri,
P_QUERY_STRING => p_query_string,
P_DATE => l_date,
P_CANONICAL_REQUEST => l_canonical_request,
P_PAYLOAD_HASH => l_payload_hash,
P_URL => l_url
);
/*
* HTTP Headers must be cleared before calling this function.
*/
l_authorization_value :=
g_aws4_auth ||
' Credential=' || g_aws_id || '/' || l_date_string || '/' || g_aws_region || '/' || g_aws_service || '/aws4_request,' ||
' SignedHeaders=host;x-amz-content-sha256;x-amz-date,' ||
' Signature=' || l_signature ;
apex_web_service.set_request_headers('Authorization', l_authorization_value, p_reset => false);
apex_web_service.set_request_headers('x-amz-content-sha256', l_payload_hash, p_reset => false);
apex_web_service.set_request_headers('x-amz-date', l_time_string, p_reset => false);
return l_url;
end set_authorization_headers;
end aws4_rest_pkg;
/