Skip to content

Commit fa8736e

Browse files
committed
Add s3 URL signing and request example
1 parent ead8810 commit fa8736e

File tree

1 file changed

+127
-0
lines changed

1 file changed

+127
-0
lines changed

examples/s3.sql

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
2+
--
3+
-- s3_get
4+
--
5+
-- Utility function to take S3 object and access keys and create
6+
-- a signed HTTP GET request using the AWS4 signing scheme.
7+
-- https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
8+
--
9+
-- Various pieces of the request are gathered into strings bundled together
10+
-- and ultimately signed with the s3 secret key.
11+
--
12+
CREATE OR REPLACE FUNCTION s3_get(
13+
access_key TEXT,
14+
secret_key TEXT,
15+
region TEXT,
16+
bucket TEXT,
17+
object_key TEXT
18+
) RETURNS http_response AS $$
19+
DECLARE
20+
http_method TEXT := 'GET';
21+
host TEXT := bucket || '.s3.' || region || '.amazonaws.com';
22+
endpoint TEXT := 'https://' || host || '/' || object_key;
23+
canonical_uri TEXT := '/' || object_key;
24+
canonical_querystring TEXT := '';
25+
signed_headers TEXT := 'host;x-amz-content-sha256;x-amz-date';
26+
service TEXT := 's3';
27+
28+
now TIMESTAMP := now() AT TIME ZONE 'UTC';
29+
amz_date TEXT := to_char(now, 'YYYYMMDD"T"HH24MISS"Z"');
30+
date_stamp TEXT := to_char(now, 'YYYYMMDD');
31+
empty_payload_hash TEXT := 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
32+
33+
canonical_headers TEXT;
34+
canonical_request TEXT;
35+
string_to_sign TEXT;
36+
credential_scope TEXT;
37+
date_key BYTEA;
38+
date_region_key BYTEA;
39+
date_region_service_key BYTEA;
40+
signing_key BYTEA;
41+
signature TEXT;
42+
authorization_header TEXT;
43+
canonical_request_digest TEXT;
44+
response http_response;
45+
request http_request;
46+
BEGIN
47+
48+
-- Construct the canonical headers
49+
canonical_headers := 'host:' || host || E'\n'
50+
|| 'x-amz-content-sha256:' || empty_payload_hash || E'\n'
51+
|| 'x-amz-date:' || amz_date || E'\n';
52+
53+
-- Create the canonical request
54+
canonical_request := http_method || E'\n' ||
55+
canonical_uri || E'\n' ||
56+
canonical_querystring || E'\n' ||
57+
canonical_headers || E'\n' ||
58+
signed_headers || E'\n' ||
59+
empty_payload_hash;
60+
61+
-- Define the credential scope
62+
credential_scope := date_stamp || '/' || region || '/' || service || '/aws4_request';
63+
64+
-- Get sha256 hash of request
65+
canonical_request_digest := encode(digest(canonical_request, 'sha256'), 'hex');
66+
67+
-- Create the string to sign
68+
string_to_sign := 'AWS4-HMAC-SHA256' || E'\n' ||
69+
amz_date || E'\n' ||
70+
credential_scope || E'\n' ||
71+
canonical_request_digest;
72+
73+
--
74+
-- Signature of pgcrypto function is hmac(payload, secret, algo)
75+
-- Each piece of the signing key is bundled together with the
76+
-- previous piece, starting with the S3 secret key.
77+
--
78+
date_key := hmac(convert_to(date_stamp, 'UTF8'), convert_to('AWS4' || secret_key, 'UTF8'), 'sha256');
79+
date_region_key := hmac(convert_to(region, 'UTF8'), date_key, 'sha256');
80+
date_region_service_key := hmac(convert_to(service, 'UTF8'), date_region_key, 'sha256');
81+
signing_key := hmac(convert_to('aws4_request','UTF8'), date_region_service_key, 'sha256');
82+
83+
-- Compute the signature
84+
signature := encode(hmac(convert_to(string_to_sign, 'UTF8'), signing_key, 'sha256'), 'hex');
85+
86+
-- Construct the Authorization header
87+
authorization_header := 'AWS4-HMAC-SHA256 Credential=' || access_key || '/' || credential_scope ||
88+
', SignedHeaders=' || signed_headers ||
89+
', Signature=' || signature;
90+
91+
92+
-- Perform the HTTP request
93+
request := (
94+
'GET',
95+
endpoint,
96+
http_headers('Authorization', authorization_header,
97+
'x-amz-content-sha256', empty_payload_hash,
98+
'x-amz-date', amz_date,
99+
'host', host),
100+
NULL,
101+
NULL
102+
)::http_request;
103+
104+
-- Getting the canonical request and payload strings perfectly
105+
-- formatted is an important step so debugging here in case
106+
-- S3 rejects signed request
107+
RAISE DEBUG 's3_get, canonical_request: %', canonical_request;
108+
RAISE DEBUG 's3_get, string_to_sign: %', string_to_sign;
109+
RAISE DEBUG 's3_get, request %', request;
110+
111+
RETURN http(request);
112+
113+
END;
114+
$$ LANGUAGE 'plpgsql'
115+
VOLATILE;
116+
117+
118+
-- https://cleverelephant-west-1.s3.amazonaws.com/META.json
119+
SELECT * FROM s3_get(
120+
'07VAHH4PC3NSQV5WXP02',
121+
'2oK6zwHh0NhhdbeBReRQSbD5y4dSQIsfsV/sqEmE',
122+
'us-west-1',
123+
'cleverelephant-west-1',
124+
'META.json'
125+
);
126+
127+

0 commit comments

Comments
 (0)