|
| 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