diff options
Diffstat (limited to 'lualib/lua_aws.lua')
-rw-r--r-- | lualib/lua_aws.lua | 300 |
1 files changed, 300 insertions, 0 deletions
diff --git a/lualib/lua_aws.lua b/lualib/lua_aws.lua new file mode 100644 index 0000000..e6c4b29 --- /dev/null +++ b/lualib/lua_aws.lua @@ -0,0 +1,300 @@ +--[[ +Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + + +--[[[ +-- @module lua_aws +-- This module contains Amazon AWS utility functions +--]] + +local N = "aws" +--local rspamd_logger = require "rspamd_logger" +local ts = (require "tableshape").types +local lua_util = require "lua_util" +local fun = require "fun" +local rspamd_crypto_hash = require "rspamd_cryptobox_hash" + +local exports = {} + +-- Returns a canonical representation of today date +local function today_canonical() + return os.date('!%Y%m%d') +end + +--[[[ +-- @function lua_aws.aws_date([date_str]) +-- Returns an aws date header corresponding to the specific date +--]] +local function aws_date(date_str) + if not date_str then + date_str = today_canonical() + end + + return date_str .. os.date('!T%H%M%SZ') +end + +exports.aws_date = aws_date + + +-- Local cache of the keys to save resources +local cached_keys = {} + +local function maybe_get_cached_key(date_str, secret_key, region, service, req_type) + local bucket = cached_keys[tonumber(date_str)] + + if not bucket then + return nil + end + + local elt = bucket[string.format('%s.%s.%s.%s', secret_key, region, service, req_type)] + if elt then + return elt + end +end + +local function save_cached_key(date_str, secret_key, region, service, req_type, key) + local numdate = tonumber(date_str) + -- expire old buckets + for k, _ in pairs(cached_keys) do + if k < numdate then + cached_keys[k] = nil + end + end + + local bucket = cached_keys[tonumber(date_str)] + local idx = string.format('%s.%s.%s.%s', secret_key, region, service, req_type) + + if not bucket then + cached_keys[tonumber(date_str)] = { + idx = key + } + else + bucket[idx] = key + end +end +--[[[ +-- @function lua_aws.aws_signing_key([date_str], secret_key, region, [service='s3'], [req_type='aws4_request']) +-- Returns a signing key for the specific parameters +--]] +local function aws_signing_key(date_str, secret_key, region, service, req_type) + if not date_str then + date_str = today_canonical() + end + + if not service then + service = 's3' + end + + if not req_type then + req_type = 'aws4_request' + end + + assert(type(secret_key) == 'string') + assert(type(region) == 'string') + + local maybe_cached = maybe_get_cached_key(date_str, secret_key, region, service, req_type) + + if maybe_cached then + return maybe_cached + end + + local hmac1 = rspamd_crypto_hash.create_specific_keyed("AWS4" .. secret_key, "sha256", date_str):bin() + local hmac2 = rspamd_crypto_hash.create_specific_keyed(hmac1, "sha256", region):bin() + local hmac3 = rspamd_crypto_hash.create_specific_keyed(hmac2, "sha256", service):bin() + local final_key = rspamd_crypto_hash.create_specific_keyed(hmac3, "sha256", req_type):bin() + + save_cached_key(date_str, secret_key, region, service, req_type, final_key) + + return final_key +end + +exports.aws_signing_key = aws_signing_key + +--[[[ +-- @function lua_aws.aws_canon_request_hash(method, path, headers_to_sign, hex_hash) +-- Returns a hash + list of headers as required to produce signature afterwards +--]] +local function aws_canon_request_hash(method, uri, headers_to_sign, hex_hash) + assert(type(method) == 'string') + assert(type(uri) == 'string') + assert(type(headers_to_sign) == 'table') + + if not hex_hash then + hex_hash = headers_to_sign['x-amz-content-sha256'] + end + + assert(type(hex_hash) == 'string') + + local sha_ctx = rspamd_crypto_hash.create_specific('sha256') + + lua_util.debugm(N, 'update signature with the method %s', + method) + sha_ctx:update(method .. '\n') + lua_util.debugm(N, 'update signature with the uri %s', + uri) + sha_ctx:update(uri .. '\n') + -- XXX add query string canonicalisation + sha_ctx:update('\n') + -- Sort auth headers and canonicalise them as requested + local hdr_canon = fun.tomap(fun.map(function(k, v) + return k:lower(), lua_util.str_trim(v) + end, headers_to_sign)) + local header_names = lua_util.keys(hdr_canon) + table.sort(header_names) + for _, hn in ipairs(header_names) do + local v = hdr_canon[hn] + lua_util.debugm(N, 'update signature with the header %s, %s', + hn, v) + sha_ctx:update(string.format('%s:%s\n', hn, v)) + end + local hdrs_list = table.concat(header_names, ';') + lua_util.debugm(N, 'headers list to sign: %s', hdrs_list) + sha_ctx:update(string.format('\n%s\n%s', hdrs_list, hex_hash)) + + return sha_ctx:hex(), hdrs_list +end + +exports.aws_canon_request_hash = aws_canon_request_hash + +local aws_authorization_hdr_args_schema = ts.shape { + date = ts.string + ts['nil'] / today_canonical, + secret_key = ts.string, + method = ts.string + ts['nil'] / function() + return 'GET' + end, + uri = ts.string, + region = ts.string, + service = ts.string + ts['nil'] / function() + return 's3' + end, + req_type = ts.string + ts['nil'] / function() + return 'aws4_request' + end, + headers = ts.map_of(ts.string, ts.string), + key_id = ts.string, +} +--[[[ +-- @function lua_aws.aws_authorization_hdr(params) +-- Produces an authorization header as required by AWS +-- Parameters schema is the following: +ts.shape{ + date = ts.string + ts['nil'] / today_canonical, + secret_key = ts.string, + method = ts.string + ts['nil'] / function() return 'GET' end, + uri = ts.string, + region = ts.string, + service = ts.string + ts['nil'] / function() return 's3' end, + req_type = ts.string + ts['nil'] / function() return 'aws4_request' end, + headers = ts.map_of(ts.string, ts.string), + key_id = ts.string, +} +-- +--]] +local function aws_authorization_hdr(tbl, transformed) + local res, err + if not transformed then + res, err = aws_authorization_hdr_args_schema:transform(tbl) + assert(res, err) + else + res = tbl + end + + local signing_key = aws_signing_key(res.date, res.secret_key, res.region, res.service, + res.req_type) + assert(signing_key ~= nil) + local signed_sha, signed_hdrs = aws_canon_request_hash(res.method, res.uri, + res.headers) + + if not signed_sha then + return nil + end + + local string_to_sign = string.format('AWS4-HMAC-SHA256\n%s\n%s/%s/%s/%s\n%s', + res.headers['x-amz-date'] or aws_date(), + res.date, res.region, res.service, res.req_type, + signed_sha) + lua_util.debugm(N, "string to sign: %s", string_to_sign) + local hmac = rspamd_crypto_hash.create_specific_keyed(signing_key, 'sha256', string_to_sign):hex() + lua_util.debugm(N, "hmac: %s", hmac) + local auth_hdr = string.format('AWS4-HMAC-SHA256 Credential=%s/%s/%s/%s/%s,' .. + 'SignedHeaders=%s,Signature=%s', + res.key_id, res.date, res.region, res.service, res.req_type, + signed_hdrs, hmac) + + return auth_hdr +end + +exports.aws_authorization_hdr = aws_authorization_hdr + + + +--[[[ +-- @function lua_aws.aws_request_enrich(params, content) +-- Produces an authorization header as required by AWS +-- Parameters schema is the following: +ts.shape{ + date = ts.string + ts['nil'] / today_canonical, + secret_key = ts.string, + method = ts.string + ts['nil'] / function() return 'GET' end, + uri = ts.string, + region = ts.string, + service = ts.string + ts['nil'] / function() return 's3' end, + req_type = ts.string + ts['nil'] / function() return 'aws4_request' end, + headers = ts.map_of(ts.string, ts.string), + key_id = ts.string, +} +This method returns new/modified in place table of the headers +-- +--]] +local function aws_request_enrich(tbl, content) + local res, err = aws_authorization_hdr_args_schema:transform(tbl) + assert(res, err) + local content_sha256 = rspamd_crypto_hash.create_specific('sha256', content):hex() + local hdrs = res.headers + hdrs['x-amz-content-sha256'] = content_sha256 + if not hdrs['x-amz-date'] then + hdrs['x-amz-date'] = aws_date(res.date) + end + hdrs['Authorization'] = aws_authorization_hdr(res, true) + + return hdrs +end + +exports.aws_request_enrich = aws_request_enrich + +-- A simple tests according to AWS docs to check sanity +local test_request_hdrs = { + ['Host'] = 'examplebucket.s3.amazonaws.com', + ['x-amz-date'] = '20130524T000000Z', + ['Range'] = 'bytes=0-9', + ['x-amz-content-sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', +} + +assert(aws_canon_request_hash('GET', '/test.txt', test_request_hdrs) == + '7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972') + +assert(aws_authorization_hdr { + date = '20130524', + region = 'us-east-1', + headers = test_request_hdrs, + uri = '/test.txt', + key_id = 'AKIAIOSFODNN7EXAMPLE', + secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', +} == 'AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,' .. + 'SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,' .. + 'Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41') + +return exports |