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ヘッダーおよびその他いくつかの関連したヘッダーの設定だけを行います。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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に限られているようです。
AnthropicのClaudeおよびClaude Instantについては、Access statusがUse case details requiredとなっています。今回はAgentsから実際に基盤モデルを呼び出すのは(お金もかかるので)やめて、署名バージョン4が正しく生成できていることだけを確認することにします。
Agentsのページを開き、新しくAgentを作成します。Create Agentをクリックします。
今回は署名が付与されたREST APIの認証が通ればよいので、Bedrock Agent自体の設定は最低限にします。BedrockのAgentsの詳細については、他の資料にあたっていただくようお願いします。
Nextをクリックします。
とりあえず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をクリックします。
AgentとしてMyAgentが作成されます。これからポリシーを作成するにあたって、アカウントIDとAgent 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"
]
}
]
}
次へ進みます。
ポリシーの作成をクリックすると、Bedrock Agentの呼び出しを許可するポリシーが作成されます。
確認画面で許可を追加をクリックします。
作成済みのユーザーoracletestuserを開き、許可タブより許可を追加を実行します。
許可を追加の画面でポリシーを直接アタッチするを選択します。アタッチするポリシーとして先ほど作成したポリシーOracleBedrockAgentPolicy_MyAgentを検索し、チェックを入れます。
次へ進みます。
Bedrock Agentの作成とアクセスの許可が完了しました。ユーザーoracletestuserのアクセスキーとシークレットアクセスキーは生成済みという前提なので、これでAWS側での準備は完了です。
これからOracle APEXのSQLコマンドからBedrock AgentのREST APIを呼び出してみます。
ListAgentsを呼び出してみます。以下のコードを実行します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
レスポンスとしてagentSummariesが返されました。REST APIに署名が正しく付けられているようです。
GetAgentを呼び出してみます。以下のコードを実行します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
InvokeAgentを呼び出してみます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
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が返されます。
完
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
/ |