diff --git a/README.md b/README.md index d5d96c21..ec66a630 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ and run the gateway. common/ contains files used by both NGINX OSS and Plus configurations etc/nginx/include/ awscredentials.js common library to read and write credentials + awssig2.js common library to build AWS signature 2 + awssig4.js common library to build AWS signature 4 and get a session token s3gateway.js common library to integrate the s3 storage from NGINX OSS and Plus utils.js common library to be reused by all of NJS codebases deployments/ contains files used for deployment technologies such as diff --git a/common/etc/nginx/include/awssig2.js b/common/etc/nginx/include/awssig2.js new file mode 100644 index 00000000..5630b212 --- /dev/null +++ b/common/etc/nginx/include/awssig2.js @@ -0,0 +1,46 @@ +/* + * Copyright 2023 F5, Inc. + * + * 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. + */ + +import utils from "./utils.js"; + +const mod_hmac = require('crypto'); + +/** + * Create HTTP Authorization header for authenticating with an AWS compatible + * v2 API. + * + * @param r {Request} HTTP request object + * @param uri {string} The URI-encoded version of the absolute path component URL to create a request + * @param httpDate {string} RFC2616 timestamp used to sign the request + * @param credentials {object} Credential object with AWS credentials in it (AccessKeyId, SecretAccessKey, SessionToken) + * @returns {string} HTTP Authorization header value + */ +function signatureV2(r, uri, httpDate, credentials) { + const method = r.method; + const hmac = mod_hmac.createHmac('sha1', credentials.secretAccessKey); + const stringToSign = method + '\n\n\n' + httpDate + '\n' + uri; + + utils.debug_log(r, 'AWS v2 Auth Signing String: [' + stringToSign + ']'); + + const signature = hmac.update(stringToSign).digest('base64'); + + return `AWS ${credentials.accessKeyId}:${signature}`; +} + + +export default { + signatureV2 +} diff --git a/common/etc/nginx/include/awssig4.js b/common/etc/nginx/include/awssig4.js new file mode 100644 index 00000000..6206796c --- /dev/null +++ b/common/etc/nginx/include/awssig4.js @@ -0,0 +1,262 @@ +/* + * Copyright 2023 F5, Inc. + * + * 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. + */ + +import utils from "./utils.js"; + +const mod_hmac = require('crypto'); + +/** + * Constant checksum for an empty HTTP body. + * @type {string} + */ +const EMPTY_PAYLOAD_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; + +/** + * Constant defining the headers being signed. + * @type {string} + */ +const DEFAULT_SIGNED_HEADERS = 'host;x-amz-content-sha256;x-amz-date'; + + +/** + * Create HTTP Authorization header for authenticating with an AWS compatible + * v4 API. + * + * @param r {Request} HTTP request object + * @param timestamp {Date} timestamp associated with request (must fall within a skew) + * @param region {string} API region associated with request + * @param service {string} service code (for example, s3, lambda) + * @param uri {string} The URI-encoded version of the absolute path component URL to create a canonical request + * @param queryParams {string} The URL-encoded query string parameters to create a canonical request + * @param host {string} HTTP host header value + * @param credentials {object} Credential object with AWS credentials in it (AccessKeyId, SecretAccessKey, SessionToken) + * @returns {string} HTTP Authorization header value + */ +function signatureV4(r, timestamp, region, service, uri, queryParams, host, credentials) { + const eightDigitDate = utils.getEightDigitDate(timestamp); + const amzDatetime = utils.getAmzDatetime(timestamp, eightDigitDate); + const canonicalRequest = _buildCanonicalRequest( + r.method, uri, queryParams, host, amzDatetime, credentials.sessionToken); + const signature = _buildSignatureV4(r, amzDatetime, eightDigitDate, + credentials, region, service, canonicalRequest); + const authHeader = 'AWS4-HMAC-SHA256 Credential=' + .concat(credentials.accessKeyId, '/', eightDigitDate, '/', region, '/', service, '/aws4_request,', + 'SignedHeaders=', _signedHeaders(credentials.sessionToken), ',Signature=', signature); + + utils.debug_log(r, 'AWS v4 Auth header: [' + authHeader + ']'); + + return authHeader; +} + +/** + * Creates a canonical request that will later be signed + * + * @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | Creating a Canonical Request} + * @param method {string} HTTP method + * @param uri {string} URI associated with request + * @param queryParams {string} query parameters associated with request + * @param host {string} HTTP Host header value + * @param amzDatetime {string} ISO8601 timestamp string to sign request with + * @returns {string} string with concatenated request parameters + * @private + */ +function _buildCanonicalRequest(method, uri, queryParams, host, amzDatetime, sessionToken) { + let canonicalHeaders = 'host:' + host + '\n' + + 'x-amz-content-sha256:' + EMPTY_PAYLOAD_HASH + '\n' + + 'x-amz-date:' + amzDatetime + '\n'; + + if (sessionToken) { + canonicalHeaders += 'x-amz-security-token:' + sessionToken + '\n' + } + + let canonicalRequest = method + '\n'; + canonicalRequest += uri + '\n'; + canonicalRequest += queryParams + '\n'; + canonicalRequest += canonicalHeaders + '\n'; + canonicalRequest += _signedHeaders(sessionToken) + '\n'; + canonicalRequest += EMPTY_PAYLOAD_HASH; + + return canonicalRequest; +} + +/** + * Creates a signature for use authenticating against an AWS compatible API. + * + * @see {@link https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html | AWS V4 Signing Process} + * @param r {Request} HTTP request object + * @param amzDatetime {string} ISO8601 timestamp string to sign request with + * @param eightDigitDate {string} date in the form of 'YYYYMMDD' + * @param creds {object} AWS credentials + * @param region {string} API region associated with request + * @param service {string} service code (for example, s3, lambda) + * @param canonicalRequest {string} string with concatenated request parameters + * @returns {string} hex encoded hash of signature HMAC value + * @private + */ +function _buildSignatureV4( + r, amzDatetime, eightDigitDate, creds, region, service, canonicalRequest) { + utils.debug_log(r, 'AWS v4 Auth Canonical Request: [' + canonicalRequest + ']'); + + const canonicalRequestHash = mod_hmac.createHash('sha256') + .update(canonicalRequest) + .digest('hex'); + + utils.debug_log(r, 'AWS v4 Auth Canonical Request Hash: [' + canonicalRequestHash + ']'); + + const stringToSign = _buildStringToSign( + amzDatetime, eightDigitDate, region, service, canonicalRequestHash); + + utils.debug_log(r, 'AWS v4 Auth Signing String: [' + stringToSign + ']'); + + let kSigningHash; + + /* If we have a keyval zone and key defined for caching the signing key hash, + * then signing key caching will be enabled. By caching signing keys we can + * accelerate the signing process because we will have four less HMAC + * operations that have to be performed per incoming request. The signing + * key expires every day, so our cache key can persist for 24 hours safely. + */ + if ("variables" in r && r.variables.cache_signing_key_enabled == 1) { + // cached value is in the format: [eightDigitDate]:[signingKeyHash] + const cached = "signing_key_hash" in r.variables ? r.variables.signing_key_hash : ""; + const fields = _splitCachedValues(cached); + const cachedEightDigitDate = fields[0]; + const cacheIsValid = fields.length === 2 && eightDigitDate === cachedEightDigitDate; + + // If true, use cached value + if (cacheIsValid) { + utils.debug_log(r, 'AWS v4 Using cached Signing Key Hash'); + /* We are forced to JSON encode the string returned from the HMAC + * operation because it is in a very specific format that include + * binary data and in order to preserve that data when persisting + * we encode it as JSON. By doing so we can gracefully decode it + * when reading from the cache. */ + kSigningHash = Buffer.from(JSON.parse(fields[1])); + // Otherwise, generate a new signing key hash and store it in the cache + } else { + kSigningHash = _buildSigningKeyHash(creds.secretAccessKey, eightDigitDate, region, service); + utils.debug_log(r, 'Writing key: ' + eightDigitDate + ':' + kSigningHash.toString('hex')); + r.variables.signing_key_hash = eightDigitDate + ':' + JSON.stringify(kSigningHash); + } + // Otherwise, don't use caching at all (like when we are using NGINX OSS) + } else { + kSigningHash = _buildSigningKeyHash(creds.secretAccessKey, eightDigitDate, region, service); + } + + utils.debug_log(r, 'AWS v4 Signing Key Hash: [' + kSigningHash.toString('hex') + ']'); + + const signature = mod_hmac.createHmac('sha256', kSigningHash) + .update(stringToSign).digest('hex'); + + utils.debug_log(r, 'AWS v4 Authorization Header: [' + signature + ']'); + + return signature; +} + +/** + * Creates a string to sign by concatenating together multiple parameters required + * by the signatures algorithm. + * + * @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html | String to Sign} + * @param amzDatetime {string} ISO8601 timestamp string to sign request with + * @param eightDigitDate {string} date in the form of 'YYYYMMDD' + * @param region {string} region associated with server API + * @param service {string} service code (for example, s3, lambda) + * @param canonicalRequestHash {string} hex encoded hash of canonical request string + * @returns {string} a concatenated string of the passed parameters formatted for signatures + * @private + */ +function _buildStringToSign(amzDatetime, eightDigitDate, region, service, canonicalRequestHash) { + return 'AWS4-HMAC-SHA256\n' + + amzDatetime + '\n' + + eightDigitDate + '/' + region + '/' + service + '/aws4_request\n' + + canonicalRequestHash; +} + +/** + * Creates a string containing the headers that need to be signed as part of v4 + * signature authentication. + * + * @param sessionToken {string|undefined} AWS session token if present + * @returns {string} semicolon delimited string of the headers needed for signing + * @private + */ +function _signedHeaders(sessionToken) { + let headers = DEFAULT_SIGNED_HEADERS; + if (sessionToken) { + headers += ';x-amz-security-token'; + } + return headers; +} + +/** + * Creates a signing key HMAC. This value is used to sign the request made to + * the API. + * + * @param kSecret {string} secret access key + * @param eightDigitDate {string} date in the form of 'YYYYMMDD' + * @param region {string} region associated with server API + * @param service {string} name of service that request is for e.g. s3, lambda + * @returns {ArrayBuffer} signing HMAC + * @private + */ +function _buildSigningKeyHash(kSecret, eightDigitDate, region, service) { + const kDate = mod_hmac.createHmac('sha256', 'AWS4'.concat(kSecret)) + .update(eightDigitDate).digest(); + const kRegion = mod_hmac.createHmac('sha256', kDate) + .update(region).digest(); + const kService = mod_hmac.createHmac('sha256', kRegion) + .update(service).digest(); + const kSigning = mod_hmac.createHmac('sha256', kService) + .update('aws4_request').digest(); + + return kSigning; +} + +/** + * Splits the cached values into an array with two elements or returns an + * empty array if the input string is invalid. The first element contains + * the eight digit date string and the second element contains a JSON string + * of the kSigningHash. + * + * @param cached input string to parse + * @returns {string[]|*[]} array containing eight digit date and kSigningHash or empty + * @private + */ +function _splitCachedValues(cached) { + const matchedPos = cached.indexOf(':', 0); + // Do a sanity check on the position returned, if it isn't sane, return + // an empty array and let the caller logic process it. + if (matchedPos < 0 || matchedPos + 1 > cached.length) { + return [] + } + + const eightDigitDate = cached.substring(0, matchedPos); + const kSigningHash = cached.substring(matchedPos + 1); + + return [eightDigitDate, kSigningHash] +} + + +export default { + signatureV4, + // These functions do not need to be exposed, but they are exposed so that + // unit tests can run against them. + _buildCanonicalRequest, + _buildSignatureV4, + _buildSigningKeyHash, + _splitCachedValues +} diff --git a/common/etc/nginx/include/s3gateway.js b/common/etc/nginx/include/s3gateway.js index bc9b06d1..eed8cb4e 100644 --- a/common/etc/nginx/include/s3gateway.js +++ b/common/etc/nginx/include/s3gateway.js @@ -15,6 +15,8 @@ */ import awscred from "./awscredentials.js"; +import awssig2 from "./awssig2.js"; +import awssig4 from "./awssig4.js"; import utils from "./utils.js"; _require_env_var('S3_BUCKET_NAME'); @@ -25,7 +27,6 @@ _require_env_var('S3_REGION'); _require_env_var('AWS_SIGS_VERSION'); _require_env_var('S3_STYLE'); -const mod_hmac = require('crypto'); const fs = require('fs'); /** @@ -60,18 +61,6 @@ const NOW = new Date(); */ const SERVICE = 's3'; -/** - * Constant checksum for an empty HTTP body. - * @type {string} - */ -const EMPTY_PAYLOAD_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; - -/** - * Constant defining the headers being signed. - * @type {string} - */ -const DEFAULT_SIGNED_HEADERS = 'host;x-amz-content-sha256;x-amz-date'; - /** * Constant base URI to fetch credentials together with the credentials relative URI, see * https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html for more details. @@ -163,7 +152,7 @@ function s3date(r) { * @returns {string} ISO 8601 timestamp */ function awsHeaderDate(r) { - return _amzDatetime(NOW, _eightDigitDate(NOW)); + return utils.getAmzDatetime(NOW, utils.getEightDigitDate(NOW)); } /** @@ -188,14 +177,78 @@ function s3auth(r) { const credentials = awscred.readCredentials(r); if (sigver == '2') { - signature = signatureV2(r, bucket, credentials); + let req = _s3ReqParamsForSigV2(r, bucket); + signature = awssig2.signatureV2(r, req.uri, req.httpDate, credentials); } else { - signature = signatureV4(r, NOW, bucket, region, server, credentials); + let req = _s3ReqParamsForSigV4(r, bucket, server); + signature = awssig4.signatureV4(r, NOW, region, SERVICE, + req.uri, req.queryParams, req.host, credentials); } return signature; } +/** + * Generate some of request parameters for AWS signature version 2 + * + * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/auth-request-sig-v2.html | AWS signature version 2} + * @param r {Request} HTTP request object + * @param bucket {string} S3 bucket associated with request + * @returns s3ReqParams {object} s3ReqParams object (host, method, uri, queryParams) + * @private + */ +function _s3ReqParamsForSigV2(r, bucket) { + /* If the source URI is a directory, we are sending to S3 a query string + * local to the root URI, so this is what we need to encode within the + * string to sign. For example, if we are requesting /bucket/dir1/ from + * nginx, then in S3 we need to request /?delimiter=/&prefix=dir1/ + * Thus, we can't put the path /dir1/ in the string to sign. */ + let uri = _isDirectory(r.variables.uri_path) ? '/' : r.variables.uri_path; + // To return index pages + index.html + if (PROVIDE_INDEX_PAGE && _isDirectory(r.variables.uri_path)){ + uri = r.variables.uri_path + INDEX_PAGE + } + + return { + uri: '/' + bucket + uri, + httpDate: s3date(r) + }; +} + +/** + * Generate some of request parameters for AWS signature version 4 + * + * @see {@link https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html | AWS V4 Signing Process} + * @param r {Request} HTTP request object + * @param bucket {string} S3 bucket associated with request + * @param server {string} S3 host associated with request + * @returns s3ReqParams {object} s3ReqParams object (host, uri, queryParams) + * @private + */ +function _s3ReqParamsForSigV4(r, bucket, server) { + let host = server; + if (S3_STYLE === 'virtual' || S3_STYLE === 'default' || S3_STYLE === undefined) { + host = bucket + '.' + host; + } + const baseUri = s3BaseUri(r); + const queryParams = _s3DirQueryParams(r.variables.uri_path, r.method); + let uri; + if (queryParams.length > 0) { + if (baseUri.length > 0) { + uri = baseUri; + } else { + uri = '/'; + } + } else { + uri = s3uri(r); + } + return { + host: host, + uri: uri, + queryParams: queryParams + }; +} + /** * Build the base file path for a S3 request URI. This function allows for * path style S3 URIs to be created that do not use a subdomain to specify @@ -320,39 +373,6 @@ function trailslashControl(r) { r.internalRedirect("@error404"); } -/** - * Create HTTP Authorization header for authenticating with an AWS compatible - * v2 API. - * - * @param r {Request} HTTP request object - * @param bucket {string} S3 bucket associated with request - * @param accessId {string} User access key credential - * @param secret {string} Secret access key - * @returns {string} HTTP Authorization header value - */ -function signatureV2(r, bucket, credentials) { - const method = r.method; - /* If the source URI is a directory, we are sending to S3 a query string - * local to the root URI, so this is what we need to encode within the - * string to sign. For example, if we are requesting /bucket/dir1/ from - * nginx, then in S3 we need to request /?delimiter=/&prefix=dir1/ - * Thus, we can't put the path /dir1/ in the string to sign. */ - let uri = _isDirectory(r.variables.uri_path) ? '/' : r.variables.uri_path; - // To return index pages + index.html - if (PROVIDE_INDEX_PAGE && _isDirectory(r.variables.uri_path)){ - uri = r.variables.uri_path + INDEX_PAGE - } - const hmac = mod_hmac.createHmac('sha1', credentials.secretAccessKey); - const httpDate = s3date(r); - const stringToSign = method + '\n\n\n' + httpDate + '\n' + '/' + bucket + uri; - - utils.debug_log(r, 'AWS v2 Auth Signing String: [' + stringToSign + ']'); - - const s3signature = hmac.update(stringToSign).digest('base64'); - - return `AWS ${credentials.accessKeyId}:${s3signature}`; -} - /** * Processes the directory listing output as returned from S3. If * FOUR_O_FOUR_ON_EMPTY_BUCKET is enabled, this function will corrupt the @@ -389,287 +409,6 @@ function filterListResponse(r, data, flags) { } } -/** - * Creates a string containing the headers that need to be signed as part of v4 - * signature authentication. - * - * @param sessionToken {string|undefined} AWS session token if present - * @returns {string} semicolon delimited string of the headers needed for signing - */ -function signedHeaders(sessionToken) { - let headers = DEFAULT_SIGNED_HEADERS; - if (sessionToken) { - headers += ';x-amz-security-token'; - } - return headers; -} - -/** - * Create HTTP Authorization header for authenticating with an AWS compatible - * v4 API. - * - * @param r {Request} HTTP request object - * @param timestamp {Date} timestamp associated with request (must fall within a skew) - * @param bucket {string} S3 bucket associated with request - * @param region {string} API region associated with request - * @param server {string} - * @param credentials {object} Credential object with AWS credentials in it (AccessKeyId, SecretAccessKey, SessionToken) - * @returns {string} HTTP Authorization header value - */ -function signatureV4(r, timestamp, bucket, region, server, credentials) { - const eightDigitDate = _eightDigitDate(timestamp); - const amzDatetime = _amzDatetime(timestamp, eightDigitDate); - const signature = _buildSignatureV4(r, amzDatetime, eightDigitDate, credentials, bucket, region, server); - const authHeader = 'AWS4-HMAC-SHA256 Credential=' - .concat(credentials.accessKeyId, '/', eightDigitDate, '/', region, '/', SERVICE, '/aws4_request,', - 'SignedHeaders=', signedHeaders(credentials.sessionToken), ',Signature=', signature); - - utils.debug_log(r, 'AWS v4 Auth header: [' + authHeader + ']'); - - return authHeader; -} - -/** - * Creates a signature for use authenticating against an AWS compatible API. - * - * @see {@link https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html | AWS V4 Signing Process} - * @param r {Request} HTTP request object - * @param amzDatetime {string} ISO8601 timestamp string to sign request with - * @param eightDigitDate {string} date in the form of 'YYYYMMDD' - * @param bucket {string} S3 bucket associated with request - * @param region {string} API region associated with request - * @returns {string} hex encoded hash of signature HMAC value - * @private - */ -function _buildSignatureV4(r, amzDatetime, eightDigitDate, creds, bucket, region, server) { - let host = server; - if (S3_STYLE === 'virtual' || S3_STYLE === 'default' || S3_STYLE === undefined) { - host = bucket + '.' + host; - } - const method = r.method; - const baseUri = s3BaseUri(r); - const queryParams = _s3DirQueryParams(r.variables.uri_path, method); - let uri; - if (queryParams.length > 0) { - if (baseUri.length > 0) { - uri = baseUri; - } else { - uri = '/'; - } - } else { - uri = s3uri(r); - } - - const canonicalRequest = _buildCanonicalRequest(method, uri, queryParams, host, amzDatetime, creds.sessionToken); - - utils.debug_log(r, 'AWS v4 Auth Canonical Request: [' + canonicalRequest + ']'); - - const canonicalRequestHash = mod_hmac.createHash('sha256') - .update(canonicalRequest) - .digest('hex'); - - utils.debug_log(r, 'AWS v4 Auth Canonical Request Hash: [' + canonicalRequestHash + ']'); - - const stringToSign = _buildStringToSign(amzDatetime, eightDigitDate, region, canonicalRequestHash); - - utils.debug_log(r, 'AWS v4 Auth Signing String: [' + stringToSign + ']'); - - let kSigningHash; - - /* If we have a keyval zone and key defined for caching the signing key hash, - * then signing key caching will be enabled. By caching signing keys we can - * accelerate the signing process because we will have four less HMAC - * operations that have to be performed per incoming request. The signing - * key expires every day, so our cache key can persist for 24 hours safely. - */ - if ("variables" in r && r.variables.cache_signing_key_enabled == 1) { - // cached value is in the format: [eightDigitDate]:[signingKeyHash] - const cached = "signing_key_hash" in r.variables ? r.variables.signing_key_hash : ""; - const fields = _splitCachedValues(cached); - const cachedEightDigitDate = fields[0]; - const cacheIsValid = fields.length === 2 && eightDigitDate === cachedEightDigitDate; - - // If true, use cached value - if (cacheIsValid) { - utils.debug_log(r, 'AWS v4 Using cached Signing Key Hash'); - /* We are forced to JSON encode the string returned from the HMAC - * operation because it is in a very specific format that include - * binary data and in order to preserve that data when persisting - * we encode it as JSON. By doing so we can gracefully decode it - * when reading from the cache. */ - kSigningHash = Buffer.from(JSON.parse(fields[1])); - // Otherwise, generate a new signing key hash and store it in the cache - } else { - kSigningHash = _buildSigningKeyHash(creds.secretAccessKey, eightDigitDate, SERVICE, region); - utils.debug_log(r, 'Writing key: ' + eightDigitDate + ':' + kSigningHash.toString('hex')); - r.variables.signing_key_hash = eightDigitDate + ':' + JSON.stringify(kSigningHash); - } - // Otherwise, don't use caching at all (like when we are using NGINX OSS) - } else { - kSigningHash = _buildSigningKeyHash(creds.secretAccessKey, eightDigitDate, SERVICE, region); - } - - utils.debug_log(r, 'AWS v4 Signing Key Hash: [' + kSigningHash.toString('hex') + ']'); - - const signature = mod_hmac.createHmac('sha256', kSigningHash) - .update(stringToSign).digest('hex'); - - utils.debug_log(r, 'AWS v4 Authorization Header: [' + signature + ']'); - - return signature; -} - -/** - * Splits the cached values into an array with two elements or returns an - * empty array if the input string is invalid. The first element contains - * the eight digit date string and the second element contains a JSON string - * of the kSigningHash. - * - * @param cached input string to parse - * @returns {string[]|*[]} array containing eight digit date and kSigningHash or empty - * @private - */ -function _splitCachedValues(cached) { - const matchedPos = cached.indexOf(':', 0); - // Do a sanity check on the position returned, if it isn't sane, return - // an empty array and let the caller logic process it. - if (matchedPos < 0 || matchedPos + 1 > cached.length) { - return [] - } - - const eightDigitDate = cached.substring(0, matchedPos); - const kSigningHash = cached.substring(matchedPos + 1); - - return [eightDigitDate, kSigningHash] -} - -/** - * Creates a string to sign by concatenating together multiple parameters required - * by the signatures algorithm. - * - * @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html | String to Sign} - * @param amzDatetime {string} ISO8601 timestamp string to sign request with - * @param eightDigitDate {string} date in the form of 'YYYYMMDD' - * @param region {string} region associated with server API - * @param canonicalRequestHash {string} hex encoded hash of canonical request string - * @returns {string} a concatenated string of the passed parameters formatted for signatures - * @private - */ -function _buildStringToSign(amzDatetime, eightDigitDate, region, canonicalRequestHash) { - return 'AWS4-HMAC-SHA256\n' + - amzDatetime + '\n' + - eightDigitDate + '/' + region + '/s3/aws4_request\n' + - canonicalRequestHash; -} - -/** - * Creates a canonical request that will later be signed - * - * @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | Creating a Canonical Request} - * @param method {string} HTTP method - * @param uri {string} URI associated with request - * @param queryParams {string} query parameters associated with request - * @param host {string} HTTP Host header value - * @param amzDatetime {string} ISO8601 timestamp string to sign request with - * @returns {string} string with concatenated request parameters - * @private - */ -function _buildCanonicalRequest(method, uri, queryParams, host, amzDatetime, sessionToken) { - let canonicalHeaders = 'host:' + host + '\n' + - 'x-amz-content-sha256:' + EMPTY_PAYLOAD_HASH + '\n' + - 'x-amz-date:' + amzDatetime + '\n'; - - if (sessionToken) { - canonicalHeaders += 'x-amz-security-token:' + sessionToken + '\n' - } - - let canonicalRequest = method + '\n'; - canonicalRequest += uri + '\n'; - canonicalRequest += queryParams + '\n'; - canonicalRequest += canonicalHeaders + '\n'; - canonicalRequest += signedHeaders(sessionToken) + '\n'; - canonicalRequest += EMPTY_PAYLOAD_HASH; - - return canonicalRequest; -} - -/** - * Creates a signing key HMAC. This value is used to sign the request made to - * the API. - * - * @param kSecret {string} secret access key - * @param eightDigitDate {string} date in the form of 'YYYYMMDD' - * @param service {string} name of service that request is for e.g. s3, iam, etc - * @param region {string} region associated with server API - * @returns {ArrayBuffer} signing HMAC - * @private - */ -function _buildSigningKeyHash(kSecret, eightDigitDate, service, region) { - const kDate = mod_hmac.createHmac('sha256', 'AWS4'.concat(kSecret)) - .update(eightDigitDate).digest(); - const kRegion = mod_hmac.createHmac('sha256', kDate) - .update(region).digest(); - const kService = mod_hmac.createHmac('sha256', kRegion) - .update(service).digest(); - const kSigning = mod_hmac.createHmac('sha256', kService) - .update('aws4_request').digest(); - - return kSigning; -} - -/** - * Formats a timestamp into a date string in the format 'YYYYMMDD'. - * - * @param timestamp {Date} timestamp used in signature - * @returns {string} a formatted date string based on the input timestamp - * @private - */ -function _eightDigitDate(timestamp) { - const year = timestamp.getUTCFullYear(); - const month = timestamp.getUTCMonth() + 1; - const day = timestamp.getUTCDate(); - - return ''.concat(_padWithLeadingZeros(year, 4), - _padWithLeadingZeros(month,2), - _padWithLeadingZeros(day,2)); -} - -/** - * Creates a string in the ISO601 date format (YYYYMMDD'T'HHMMSS'Z') based on - * the supplied timestamp and date. The date is not extracted from the timestamp - * because that operation is already done once during the signing process. - * - * @param timestamp {Date} timestamp to extract date from - * @param eightDigitDate {string} 'YYYYMMDD' format date string that was already extracted from timestamp - * @returns {string} string in the format of YYYYMMDD'T'HHMMSS'Z' - * @private - */ -function _amzDatetime(timestamp, eightDigitDate) { - const hours = timestamp.getUTCHours(); - const minutes = timestamp.getUTCMinutes(); - const seconds = timestamp.getUTCSeconds(); - - return ''.concat( - eightDigitDate, - 'T', _padWithLeadingZeros(hours, 2), - _padWithLeadingZeros(minutes, 2), - _padWithLeadingZeros(seconds, 2), - 'Z'); -} - -/** - * Pads the supplied number with leading zeros. - * - * @param num {number|string} number to pad - * @param size number of leading zeros to pad - * @returns {string} a string with leading zeros - * @private - */ -function _padWithLeadingZeros(num, size) { - const s = "0" + num; - return s.substr(s.length-size); -} - /** * Adds additional encoding to a URI component * @@ -974,13 +713,9 @@ export default { filterListResponse, // These functions do not need to be exposed, but they are exposed so that // unit tests can run against them. - _padWithLeadingZeros, + _s3ReqParamsForSigV2, + _s3ReqParamsForSigV4, _encodeURIComponent, - _eightDigitDate, - _amzDatetime, - _splitCachedValues, - _buildSigningKeyHash, - _buildSignatureV4, _escapeURIPath, _isHeaderToBeStripped }; diff --git a/common/etc/nginx/include/utils.js b/common/etc/nginx/include/utils.js index c69d8a37..f978a75e 100644 --- a/common/etc/nginx/include/utils.js +++ b/common/etc/nginx/include/utils.js @@ -74,8 +74,64 @@ function debug_log(r, msg) { } } +/** + * Pads the supplied number with leading zeros. + * + * @param num {number|string} number to pad + * @param size number of leading zeros to pad + * @returns {string} a string with leading zeros + * @private + */ +function padWithLeadingZeros(num, size) { + const s = "0" + num; + return s.substr(s.length-size); +} + +/** + * Creates a string in the ISO601 date format (YYYYMMDD'T'HHMMSS'Z') based on + * the supplied timestamp and date. The date is not extracted from the timestamp + * because that operation is already done once during the signing process. + * + * @param timestamp {Date} timestamp to extract date from + * @param eightDigitDate {string} 'YYYYMMDD' format date string that was already extracted from timestamp + * @returns {string} string in the format of YYYYMMDD'T'HHMMSS'Z' + * @private + */ +function getAmzDatetime(timestamp, eightDigitDate) { + const hours = timestamp.getUTCHours(); + const minutes = timestamp.getUTCMinutes(); + const seconds = timestamp.getUTCSeconds(); + + return ''.concat( + eightDigitDate, + 'T', padWithLeadingZeros(hours, 2), + padWithLeadingZeros(minutes, 2), + padWithLeadingZeros(seconds, 2), + 'Z'); +} + +/** + * Formats a timestamp into a date string in the format 'YYYYMMDD'. + * + * @param timestamp {Date} timestamp + * @returns {string} a formatted date string based on the input timestamp + * @private + */ +function getEightDigitDate(timestamp) { + const year = timestamp.getUTCFullYear(); + const month = timestamp.getUTCMonth() + 1; + const day = timestamp.getUTCDate(); + + return ''.concat(padWithLeadingZeros(year, 4), + padWithLeadingZeros(month,2), + padWithLeadingZeros(day,2)); +} + export default { debug_log, + getAmzDatetime, + getEightDigitDate, + padWithLeadingZeros, parseArray, parseBoolean } diff --git a/docs/development.md b/docs/development.md index 68051e32..e690be43 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,22 @@ # Development Guide +## Integrating with AWS Signature + +Update the following files when enhancing `nginx-s3-gateway` to integrate with AWS signature whenever AWS releases a new version of signature or you have a new PR: + +- NGINX Proxy: [`/etc/nginx/conf.d/default.conf`](/common/etc/nginx/templates/gateway/s3_location.conf.template) +- AWS Credentials Lib: [`/etc/nginx/include/awscredentials.js`](/common/etc/nginx/include/awscredentials.js) + > Note: The `fetchCredentials()` is going to be part of here soon. + +- AWS Signature Lib per version: + - [`/etc/nginx/include/awssig2.js`](/common/etc/nginx/include/awssig2.js) + - [`/etc/nginx/include/awssig4.js`](/common/etc/nginx/include/awssig4.js) + +- S3 Integration Lib: [`/etc/nginx/include/s3gateway.js`](/common/etc/nginx/include/s3gateway.js) +- Common Lib for all of NJS: [`/etc/nginx/include/utils.js`](/common/etc/nginx/include/utils.js) + +![](./img/nginx-s3-gateway-signature-flow.png) + ## Extending the Gateway ### Extending gateway configuration via container images diff --git a/docs/img/nginx-s3-gateway-signature-flow.png b/docs/img/nginx-s3-gateway-signature-flow.png new file mode 100644 index 00000000..8cc152dd Binary files /dev/null and b/docs/img/nginx-s3-gateway-signature-flow.png differ diff --git a/oss/etc/nginx/conf.d/gateway/server_variables.conf b/oss/etc/nginx/conf.d/gateway/server_variables.conf index 40b888df..cf0077ef 100644 --- a/oss/etc/nginx/conf.d/gateway/server_variables.conf +++ b/oss/etc/nginx/conf.d/gateway/server_variables.conf @@ -1,9 +1,9 @@ -# Variable indicating to the s3gateway.js script that singing key +# Variable indicating to the awssig4.js script that singing key # caching is turned off. This feature uses the keyval store, so it # is only enabled when using NGINX Plus. set $cache_signing_key_enabled 0; -# Variable indicating to the s3gateway.js script that session token +# Variable indicating to the awscredentials.js script that session token # caching is turned on. This feature uses the keyval store, so it # is only enabled when using NGINX Plus. set $cache_instance_credentials_enabled 0; diff --git a/standalone_ubuntu_oss_install.sh b/standalone_ubuntu_oss_install.sh index 277a72ba..7c4f4703 100644 --- a/standalone_ubuntu_oss_install.sh +++ b/standalone_ubuntu_oss_install.sh @@ -350,6 +350,8 @@ EOF download "common/etc/nginx/include/listing.xsl" "/etc/nginx/include/listing.xsl" download "common/etc/nginx/include/awscredentials.js" "/etc/nginx/include/awscredentials.js" +download "common/etc/nginx/include/awssig2.js" "/etc/nginx/include/awssig2.js" +download "common/etc/nginx/include/awssig4.js" "/etc/nginx/include/awssig4.js" download "common/etc/nginx/include/s3gateway.js" "/etc/nginx/include/s3gateway.js" download "common/etc/nginx/include/utils.js" "/etc/nginx/include/utils.js" download "common/etc/nginx/templates/default.conf.template" "/etc/nginx/templates/default.conf.template" diff --git a/test.sh b/test.sh index c0b3b029..ca841707 100755 --- a/test.sh +++ b/test.sh @@ -317,10 +317,14 @@ runUnitTestWithSessionToken "utils_test.js" p "Running unit tests with an access key ID and a secret key in Docker image" runUnitTestWithOutSessionToken "awscredentials_test.js" +runUnitTestWithOutSessionToken "awssig2_test.js" +runUnitTestWithOutSessionToken "awssig4_test.js" runUnitTestWithOutSessionToken "s3gateway_test.js" p "Running unit tests with an session token in Docker image" runUnitTestWithSessionToken "awscredentials_test.js" +runUnitTestWithSessionToken "awssig2_test.js" +runUnitTestWithSessionToken "awssig4_test.js" runUnitTestWithSessionToken "s3gateway_test.js" ### INTEGRATION TESTS diff --git a/test/unit/awssig2_test.js b/test/unit/awssig2_test.js new file mode 100644 index 00000000..67b69080 --- /dev/null +++ b/test/unit/awssig2_test.js @@ -0,0 +1,91 @@ +#!env njs + +/* + * Copyright 2023 F5, Inc. + * + * 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. + */ + +import awssig2 from "include/awssig2.js"; +import s3gateway from "include/s3gateway.js"; + + +function _runSignatureV2(r) { + r.log = function(msg) { + console.log(msg); + } + const timestamp = new Date('2020-08-11T19:42:14Z'); + const bucket = 'test-bucket-1'; + const accessKey = 'test-access-key-1'; + const secret = 'pvgoBEA1z7zZKqN9RoKVksKh31AtNou+pspn+iyb' + const creds = { + accessKeyId:accessKey, secretAccessKey: secret, sessionToken: null + }; + + // TODO: Generate request parameters without using s3gateway to only test + // awssig2.js for the purpose of common library. + const httpDate = timestamp.toUTCString(); + const expected = 'AWS test-access-key-1:VviSS4cFhUC6eoB4CYqtRawzDrc='; + let req = s3gateway._s3ReqParamsForSigV2(r, bucket); + let signature = awssig2.signatureV2(r, req.uri, httpDate, creds); + + if (signature !== expected) { + throw 'V2 signature hash was not created correctly.\n' + + 'Actual: [' + signature + ']\n' + + 'Expected: [' + expected + ']'; + } +} + +function testSignatureV2() { + printHeader('testSignatureV2'); + // Note: since this is a read-only gateway, host, query parameters and all + // client headers will be ignored. + var r = { + "remoteAddress" : "172.17.0.1", + "headersIn" : { + "Connection" : "keep-alive", + "Accept-Encoding" : "gzip, deflate", + "Accept-Language" : "en-US,en;q=0.7,ja;q=0.3", + "Host" : "localhost:8999", + "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0", + "DNT" : "1", + "Cache-Control" : "max-age=0", + "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Upgrade-Insecure-Requests" : "1" + }, + "uri" : "/a/c/ramen.jpg", + "method" : "GET", + "httpVersion" : "1.1", + "headersOut" : {}, + "args" : { + "foo" : "bar" + }, + "variables" : { + "uri_path": "/a/c/ramen.jpg" + }, + "status" : 0 + }; + + _runSignatureV2(r); +} + +async function test() { + testSignatureV2(); +} + +function printHeader(testName) { + console.log(`\n## ${testName}`); +} + +test(); +console.log('Finished unit tests for awssig2.js'); diff --git a/test/unit/awssig4_test.js b/test/unit/awssig4_test.js new file mode 100644 index 00000000..0c1677a1 --- /dev/null +++ b/test/unit/awssig4_test.js @@ -0,0 +1,172 @@ +#!env njs + +/* + * Copyright 2023 F5, Inc. + * + * 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. + */ + +import awssig4 from "include/awssig4.js"; +import s3gateway from "include/s3gateway.js"; +import utils from "include/utils.js"; + + +function testBuildSigningKeyHashWithReferenceInputs() { + printHeader('testBuildSigningKeyHashWithReferenceInputs'); + var kSecret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'; + var date = '20150830'; + var service = 'iam'; + var region = 'us-east-1'; + var expected = 'c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9'; + var signingKeyHash = awssig4._buildSigningKeyHash(kSecret, date, region, service).toString('hex'); + + if (signingKeyHash !== expected) { + throw 'Signing key hash was not created correctly.\n' + + 'Actual: [' + signingKeyHash + ']\n' + + 'Expected: [' + expected + ']'; + } +} + +function testBuildSigningKeyHashWithTestSuiteInputs() { + printHeader('testBuildSigningKeyHashWithTestSuiteInputs'); + var kSecret = 'pvgoBEA1z7zZKqN9RoKVksKh31AtNou+pspn+iyb'; + var date = '20200811'; + var service = 's3'; + var region = 'us-west-2'; + var expected = 'a48701bfe803103e89051f55af2297dd76783bbceb5eb416dab71e0eadcbc4f6'; + var signingKeyHash = awssig4._buildSigningKeyHash(kSecret, date, region, service).toString('hex'); + + if (signingKeyHash !== expected) { + throw 'Signing key hash was not created correctly.\n' + + 'Actual: [' + signingKeyHash + ']\n' + + 'Expected: [' + expected + ']'; + } +} + +function _runSignatureV4(r) { + r.log = function(msg) { + console.log(msg); + } + var timestamp = new Date('2020-08-11T19:42:14Z'); + var eightDigitDate = utils.getEightDigitDate(timestamp); + var amzDatetime = utils.getAmzDatetime(timestamp, eightDigitDate); + var bucket = 'ez-test-bucket-1' + var secret = 'pvgoBEA1z7zZKqN9RoKVksKh31AtNou+pspn+iyb' + var creds = {secretAccessKey: secret, sessionToken: null}; + var region = 'us-west-2'; + var service = 's3'; + var server = 's3-us-west-2.amazonaws.com'; + + // TODO: Generate request parameters without using s3gateway to only test + // awssig4.js for the purpose of common library. + let req = s3gateway._s3ReqParamsForSigV4(r, bucket, server); + const canonicalRequest = awssig4._buildCanonicalRequest( + r.method, req.uri, req.queryParams, req.host, amzDatetime, creds.sessionToken); + + var expected = 'cf4dd9e1d28c74e2284f938011efc8230d0c20704f56f67e4a3bfc2212026bec'; + var signature = awssig4._buildSignatureV4( + r, amzDatetime, eightDigitDate, creds, region, service, canonicalRequest); + + if (signature !== expected) { + throw 'V4 signature hash was not created correctly.\n' + + 'Actual: [' + signature + ']\n' + + 'Expected: [' + expected + ']'; + } +} + +function testSignatureV4() { + printHeader('testSignatureV4'); + // Note: since this is a read-only gateway, host, query parameters and all + // client headers will be ignored. + var r = { + "remoteAddress" : "172.17.0.1", + "headersIn" : { + "Connection" : "keep-alive", + "Accept-Encoding" : "gzip, deflate", + "Accept-Language" : "en-US,en;q=0.7,ja;q=0.3", + "Host" : "localhost:8999", + "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0", + "DNT" : "1", + "Cache-Control" : "max-age=0", + "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Upgrade-Insecure-Requests" : "1" + }, + "uri" : "/a/c/ramen.jpg", + "method" : "GET", + "httpVersion" : "1.1", + "headersOut" : {}, + "args" : { + "foo" : "bar" + }, + "variables" : { + "uri_path": "/a/c/ramen.jpg" + }, + "status" : 0 + }; + + _runSignatureV4(r); +} + +function testSignatureV4Cache() { + printHeader('testSignatureV4Cache'); + // Note: since this is a read-only gateway, host, query parameters and all + // client headers will be ignored. + var r = { + "remoteAddress" : "172.17.0.1", + "headersIn" : { + "Connection" : "keep-alive", + "Accept-Encoding" : "gzip, deflate", + "Accept-Language" : "en-US,en;q=0.7,ja;q=0.3", + "Host" : "localhost:8999", + "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0", + "DNT" : "1", + "Cache-Control" : "max-age=0", + "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Upgrade-Insecure-Requests" : "1" + }, + "uri" : "/a/c/ramen.jpg", + "method" : "GET", + "httpVersion" : "1.1", + "headersOut" : {}, + "args" : { + "foo" : "bar" + }, + "variables": { + "cache_signing_key_enabled": 1, + "uri_path": "/a/c/ramen.jpg" + }, + "status" : 0 + }; + + _runSignatureV4(r); + + if (!"signing_key_hash" in r.variables) { + throw "Hash key not written to r.variables.signing_key_hash"; + } + + _runSignatureV4(r); +} + +async function test() { + testBuildSigningKeyHashWithReferenceInputs(); + testBuildSigningKeyHashWithTestSuiteInputs(); + testSignatureV4(); + testSignatureV4Cache(); +} + +function printHeader(testName) { + console.log(`\n## ${testName}`); +} + +test(); +console.log('Finished unit tests for awssig4.js'); diff --git a/test/unit/s3gateway_test.js b/test/unit/s3gateway_test.js index 8d1db911..b85be7a9 100755 --- a/test/unit/s3gateway_test.js +++ b/test/unit/s3gateway_test.js @@ -16,6 +16,7 @@ * limitations under the License. */ +import awssig4 from "include/awssig4.js"; import s3gateway from "include/s3gateway.js"; globalThis.ngx = {}; @@ -91,51 +92,12 @@ function testEncodeURIComponent() { testDiceyCharactersInText(); } -function testPad() { - printHeader('testPad'); - var padSingleDigit = s3gateway._padWithLeadingZeros(3, 2); - var expected = '03'; - - if (padSingleDigit !== expected) { - throw 'Single digit 3 was not padded with leading zero.\n' + - 'Actual: ' + padSingleDigit + '\n' + - 'Expected: ' + expected; - } -} - -function testEightDigitDate() { - printHeader('testEightDigitDate'); - var timestamp = new Date('2020-08-03T02:01:09.004Z'); - var eightDigitDate = s3gateway._eightDigitDate(timestamp); - var expected = '20200803'; - - if (eightDigitDate !== expected) { - throw 'Eight digit date was not created correctly.\n' + - 'Actual: ' + eightDigitDate + '\n' + - 'Expected: ' + expected; - } -} - -function testAmzDatetime() { - printHeader('testAmzDatetime'); - var timestamp = new Date('2020-08-03T02:01:09.004Z'); - var eightDigitDate = s3gateway._eightDigitDate(timestamp); - var amzDatetime = s3gateway._amzDatetime(timestamp, eightDigitDate); - var expected = '20200803T020109Z'; - - if (amzDatetime !== expected) { - throw 'Amazon date time was not created correctly.\n' + - 'Actual: [' + amzDatetime + ']\n' + - 'Expected: [' + expected + ']'; - } -} - function testSplitCachedValues() { printHeader('testSplitCachedValues'); var eightDigitDate = "20200811" var kSigningHash = "{\"type\":\"Buffer\",\"data\":[164,135,1,191,232,3,16,62,137,5,31,85,175,34,151,221,118,120,59,188,235,94,180,22,218,183,30,14,173,203,196,246]}" var cached = eightDigitDate + ":" + kSigningHash; - var fields = s3gateway._splitCachedValues(cached); + var fields = awssig4._splitCachedValues(cached); if (fields.length !== 2) { throw 'Unexpected array length returned.\n' + @@ -156,133 +118,6 @@ function testSplitCachedValues() { } } -function testBuildSigningKeyHashWithReferenceInputs() { - printHeader('testBuildSigningKeyHashWithReferenceInputs'); - var kSecret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'; - var date = '20150830'; - var service = 'iam'; - var region = 'us-east-1'; - var expected = 'c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9'; - var signingKeyHash = s3gateway._buildSigningKeyHash(kSecret, date, service, region).toString('hex'); - - if (signingKeyHash !== expected) { - throw 'Signing key hash was not created correctly.\n' + - 'Actual: [' + signingKeyHash + ']\n' + - 'Expected: [' + expected + ']'; - } -} - -function testBuildSigningKeyHashWithTestSuiteInputs() { - printHeader('testBuildSigningKeyHashWithTestSuiteInputs'); - var kSecret = 'pvgoBEA1z7zZKqN9RoKVksKh31AtNou+pspn+iyb'; - var date = '20200811'; - var service = 's3'; - var region = 'us-west-2'; - var expected = 'a48701bfe803103e89051f55af2297dd76783bbceb5eb416dab71e0eadcbc4f6'; - var signingKeyHash = s3gateway._buildSigningKeyHash(kSecret, date, service, region).toString('hex'); - - if (signingKeyHash !== expected) { - throw 'Signing key hash was not created correctly.\n' + - 'Actual: [' + signingKeyHash + ']\n' + - 'Expected: [' + expected + ']'; - } -} - -function _runSignatureV4(r) { - r.log = function(msg) { - console.log(msg); - } - var timestamp = new Date('2020-08-11T19:42:14Z'); - var eightDigitDate = s3gateway._eightDigitDate(timestamp); - var amzDatetime = s3gateway._amzDatetime(timestamp, eightDigitDate); - var bucket = 'ez-test-bucket-1' - var secret = 'pvgoBEA1z7zZKqN9RoKVksKh31AtNou+pspn+iyb' - var region = 'us-west-2'; - var server = 's3-us-west-2.amazonaws.com'; - - var expected = 'cf4dd9e1d28c74e2284f938011efc8230d0c20704f56f67e4a3bfc2212026bec'; - var signature = s3gateway._buildSignatureV4(r, amzDatetime, eightDigitDate, {secretAccessKey: secret}, bucket, region, server); - - if (signature !== expected) { - throw 'V4 signature hash was not created correctly.\n' + - 'Actual: [' + signature + ']\n' + - 'Expected: [' + expected + ']'; - } -} - -function testSignatureV4() { - printHeader('testSignatureV4'); - // Note: since this is a read-only gateway, host, query parameters and all - // client headers will be ignored. - var r = { - "remoteAddress" : "172.17.0.1", - "headersIn" : { - "Connection" : "keep-alive", - "Accept-Encoding" : "gzip, deflate", - "Accept-Language" : "en-US,en;q=0.7,ja;q=0.3", - "Host" : "localhost:8999", - "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0", - "DNT" : "1", - "Cache-Control" : "max-age=0", - "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Upgrade-Insecure-Requests" : "1" - }, - "uri" : "/a/c/ramen.jpg", - "method" : "GET", - "httpVersion" : "1.1", - "headersOut" : {}, - "args" : { - "foo" : "bar" - }, - "variables" : { - "uri_path": "/a/c/ramen.jpg" - }, - "status" : 0 - }; - - _runSignatureV4(r); -} - -function testSignatureV4Cache() { - printHeader('testSignatureV4Cache'); - // Note: since this is a read-only gateway, host, query parameters and all - // client headers will be ignored. - var r = { - "remoteAddress" : "172.17.0.1", - "headersIn" : { - "Connection" : "keep-alive", - "Accept-Encoding" : "gzip, deflate", - "Accept-Language" : "en-US,en;q=0.7,ja;q=0.3", - "Host" : "localhost:8999", - "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0", - "DNT" : "1", - "Cache-Control" : "max-age=0", - "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Upgrade-Insecure-Requests" : "1" - }, - "uri" : "/a/c/ramen.jpg", - "method" : "GET", - "httpVersion" : "1.1", - "headersOut" : {}, - "args" : { - "foo" : "bar" - }, - "variables": { - "cache_signing_key_enabled": 1, - "uri_path": "/a/c/ramen.jpg" - }, - "status" : 0 - }; - - _runSignatureV4(r); - - if (!"signing_key_hash" in r.variables) { - throw "Hash key not written to r.variables.signing_key_hash"; - } - - _runSignatureV4(r); -} - function testEditHeaders() { printHeader('testEditHeaders'); @@ -492,14 +327,7 @@ function printHeader(testName) { async function test() { testEncodeURIComponent(); - testPad(); - testEightDigitDate(); - testAmzDatetime(); testSplitCachedValues(); - testBuildSigningKeyHashWithReferenceInputs(); - testBuildSigningKeyHashWithTestSuiteInputs(); - testSignatureV4(); - testSignatureV4Cache(); testIsHeaderToBeStripped(); testEditHeaders(); testEditHeadersHeadDirectory(); diff --git a/test/unit/utils_test.js b/test/unit/utils_test.js index 0f65a3fb..7abcbea0 100644 --- a/test/unit/utils_test.js +++ b/test/unit/utils_test.js @@ -83,10 +83,52 @@ function testParseArray() { testParseMultipleValuesTrailingDelimiter(); } +function testAmzDatetime() { + printHeader('testAmzDatetime'); + var timestamp = new Date('2020-08-03T02:01:09.004Z'); + var eightDigitDate = utils.getEightDigitDate(timestamp); + var amzDatetime = utils.getAmzDatetime(timestamp, eightDigitDate); + var expected = '20200803T020109Z'; + + if (amzDatetime !== expected) { + throw 'Amazon date time was not created correctly.\n' + + 'Actual: [' + amzDatetime + ']\n' + + 'Expected: [' + expected + ']'; + } +} + +function testEightDigitDate() { + printHeader('testEightDigitDate'); + var timestamp = new Date('2020-08-03T02:01:09.004Z'); + var eightDigitDate = utils.getEightDigitDate(timestamp); + var expected = '20200803'; + + if (eightDigitDate !== expected) { + throw 'Eight digit date was not created correctly.\n' + + 'Actual: ' + eightDigitDate + '\n' + + 'Expected: ' + expected; + } +} + +function testPad() { + printHeader('testPad'); + var padSingleDigit = utils.padWithLeadingZeros(3, 2); + var expected = '03'; + + if (padSingleDigit !== expected) { + throw 'Single digit 3 was not padded with leading zero.\n' + + 'Actual: ' + padSingleDigit + '\n' + + 'Expected: ' + expected; + } +} + async function test() { + testAmzDatetime(); + testEightDigitDate(); + testPad(); testParseArray(); } - + function printHeader(testName) { console.log(`\n## ${testName}`); }