From 373f941448397075f97acbc122f7a06dd2353cc4 Mon Sep 17 00:00:00 2001
From: Shawn Kim <shawnhan.kim@gmail.com>
Date: Wed, 19 Apr 2023 05:52:57 -0700
Subject: [PATCH 1/2] feat: integrate with nginx-aws-signature

---
 Makefile                                      |   2 +-
 common/awssig                                 |   2 +-
 common/lambda-core/awscredentials.js          | 439 ------------------
 common/lambda-core/awssig4.js                 | 296 ------------
 common/lambda-core/lambda_ngx_http.conf       |   4 +-
 common/lambda-core/lambdagateway.js           |   6 +-
 common/lambda-core/utils.js                   | 153 ------
 docker-compose.yml                            |   1 +
 docker/Dockerfile.plus                        |   8 +-
 docs/getting_started.md                       |   6 +
 .../docker-compose.yml                        |   1 +
 .../docker-compose.yml                        |   1 +
 .../docker-compose.yml                        |   1 +
 .../docker-compose.yml                        |   1 +
 14 files changed, 21 insertions(+), 900 deletions(-)
 delete mode 100644 common/lambda-core/awscredentials.js
 delete mode 100644 common/lambda-core/awssig4.js
 delete mode 100644 common/lambda-core/utils.js

diff --git a/Makefile b/Makefile
index 1447b79..14f2b8e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 submodule:
-	git submodule update --init git@github.com:nginx-serverless/nginx-aws-signature.git common/awssig
+	git submodule update --init common/awssig
 	git submodule absorbgitdirs
 	git -C common/awssig config core.sparseCheckout true
 	echo 'core/*' >>.git/modules/common/awssig/info/sparse-checkout
diff --git a/common/awssig b/common/awssig
index f93d594..9e9ff39 160000
--- a/common/awssig
+++ b/common/awssig
@@ -1 +1 @@
-Subproject commit f93d5944a1783debf1b94703cd9f44c5ed2317c1
+Subproject commit 9e9ff393e29adf3bf4615dfeec9811eb4ed6b683
diff --git a/common/lambda-core/awscredentials.js b/common/lambda-core/awscredentials.js
deleted file mode 100644
index 4892bca..0000000
--- a/common/lambda-core/awscredentials.js
+++ /dev/null
@@ -1,439 +0,0 @@
-/*
- *  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 fs = require('fs');
-
-/**
- * The current moment as a timestamp. This timestamp will be used across
- * functions in order for there to be no variations in signatures.
- * @type {Date}
- */
-const NOW = new 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.
- * @type {string}
- */
-const ECS_CREDENTIAL_BASE_URI = 'http://169.254.170.2';
-
-/**
- * @type {string}
- */
-const EC2_IMDS_TOKEN_ENDPOINT = 'http://169.254.169.254/latest/api/token';
-
-const EC2_IMDS_SECURITY_CREDENTIALS_ENDPOINT = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/';
-
-/**
- * Offset to the expiration of credentials, when they should be considered expired and refreshed. The maximum
- * time here can be 5 minutes, the IMDS and ECS credentials endpoint will make sure that each returned set of credentials
- * is valid for at least another 5 minutes.
- *
- * To make sure we always refresh the credentials instead of retrieving the same again, keep credentials until 4:30 minutes
- * before they really expire.
- *
- * @type {number}
- */
-const maxValidityOffsetMs = 4.5 * 60 * 1000;
-
-
-/**
- * Get the current session token from either the instance profile credential 
- * cache or environment variables.
- *
- * @param r {Request} HTTP request object (not used, but required for NGINX configuration)
- * @returns {string} current session token or empty string
- */
-function sessionToken(r) {
-    const credentials = readCredentials(r);
-    if (credentials.sessionToken) {
-        return credentials.sessionToken;
-    }
-    return '';
-}
-
-/**
- * Get the instance profile credentials needed to authenticate against Lambda from
- * a backend cache. If the credentials cannot be found, then return undefined.
- * @param r {Request} HTTP request object (not used, but required for NGINX configuration)
- * @returns {undefined|{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string|null), expiration: (string|null)}} AWS instance profile credentials or undefined
- */
-function readCredentials(r) {
-    // TODO: Change the generic constants naming for multiple AWS services.
-    if ('AWS_ACCESS_KEY_ID' in process.env && 'AWS_SECRET_ACCESS_KEY' in process.env) {
-        let sessionToken = 'AWS_SESSION_TOKEN' in process.env ?
-            process.env['AWS_SESSION_TOKEN'] : null;
-        if (sessionToken != null && sessionToken.length === 0) {
-            sessionToken = null;
-        }
-        return {
-            accessKeyId: process.env['AWS_ACCESS_KEY_ID'],
-            secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'],
-            sessionToken: sessionToken,
-            expiration: null
-        };
-    }
-
-    if ("variables" in r && r.variables.cache_instance_credentials_enabled == 1) {
-        return _readCredentialsFromKeyValStore(r);
-    } else {
-        return _readCredentialsFromFile();
-    }
-}
-
-/**
- * Read credentials from the NGINX Keyval store. If it is not found, then
- * return undefined.
- *
- * @param r {Request} HTTP request object (not used, but required for NGINX configuration)
- * @returns {undefined|{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials or undefined
- * @private
- */
-function _readCredentialsFromKeyValStore(r) {
-    const cached = r.variables.instance_credential_json;
-
-    if (!cached) {
-        return undefined;
-    }
-
-    try {
-        return JSON.parse(cached);
-    } catch (e) {
-        utils.debug_log(r, `Error parsing JSON value from r.variables.instance_credential_json: ${e}`);
-        return undefined;
-    }
-}
-
-/**
- * Read the contents of the credentials file into memory. If it is not
- * found, then return undefined.
- *
- * @returns {undefined|{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials or undefined
- * @private
- */
-function _readCredentialsFromFile() {
-    const credsFilePath = _credentialsTempFile();
-
-    try {
-        const creds = fs.readFileSync(credsFilePath);
-        return JSON.parse(creds);
-    } catch (e) {
-        /* Do not throw an exception in the case of when the
-           credentials file path is invalid in order to signal to
-           the caller that such a file has not been created yet. */
-        if (e.code === 'ENOENT') {
-            return undefined;
-        }
-        throw e;
-    }
-}
-
-/**
- * Returns the path to the credentials temporary cache file.
- *
- * @returns {string} path on the file system to credentials cache file
- * @private
- */
-function _credentialsTempFile() {
-    if (process.env['AWS_CREDENTIALS_TEMP_FILE']) {
-        return process.env['AWS_CREDENTIALS_TEMP_FILE'];
-    }
-    if (process.env['TMPDIR']) {
-        return `${process.env['TMPDIR']}/credentials.json`
-    }
-
-    return '/tmp/credentials.json';
-}
-
-/**
- * Write the instance profile credentials to a caching backend.
- *
- * @param r {Request} HTTP request object (not used, but required for NGINX configuration)
- * @param credentials {{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials
- */
-function writeCredentials(r, credentials) {
-    /* Do not bother writing credentials if we are running in a mode where we
-       do not need instance credentials. */
-    if (process.env['AWS_ACCESS_KEY_ID'] && process.env['AWS_SECRET_ACCESS_KEY']) {
-        return;
-    }
-
-    if (!credentials) {
-        throw `Cannot write invalid credentials: ${JSON.stringify(credentials)}`;
-    }
-
-    if ("variables" in r && r.variables.cache_instance_credentials_enabled == 1) {
-        _writeCredentialsToKeyValStore(r, credentials);
-    } else {
-        _writeCredentialsToFile(credentials);
-    }
-}
-
-/**
- * Write the instance profile credentials to the NGINX Keyval store.
- *
- * @param r {Request} HTTP request object (not used, but required for NGINX configuration)
- * @param credentials {{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials
- * @private
- */
-function _writeCredentialsToKeyValStore(r, credentials) {
-    r.variables.instance_credential_json = JSON.stringify(credentials);
-}
-
-/**
- * Write the instance profile credentials to a file on the file system. This
- * file will be quite small and should end up in the file cache relatively
- * quickly if it is repeatedly read.
- *
- * @param r {Request} HTTP request object (not used, but required for NGINX configuration)
- * @param credentials {{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials
- * @private
- */
-function _writeCredentialsToFile(credentials) {
-    fs.writeFileSync(_credentialsTempFile(), JSON.stringify(credentials));
-}
-
-/**
- * Get the credentials needed to create AWS signatures in order to authenticate
- * to AWS service. If the gateway is being provided credentials via a instance 
- * profile credential as provided over the metadata endpoint, this function will:
- * 1. Try to read the credentials from cache
- * 2. Determine if the credentials are stale
- * 3. If the cached credentials are missing or stale, it gets new credentials
- *    from the metadata endpoint.
- * 4. If new credentials were pulled, it writes the credentials back to the
- *    cache.
- *
- * If the gateway is not using instance profile credentials, then this function
- * quickly exits.
- *
- * @param r {Request} HTTP request object
- * @returns {Promise<void>}
- */
-async function fetchCredentials(r) {
-    /* If we are not using an AWS instance profile to set our credentials we
-       exit quickly and don't write a credentials file. */
-    if (process.env['AWS_ACCESS_KEY_ID'] && process.env['AWS_SECRET_ACCESS_KEY']) {
-        r.return(200);
-        return;
-    }
-
-    let current;
-
-    try {
-        current = readCredentials(r);
-    } catch (e) {
-        utils.debug_log(r, `Could not read credentials: ${e}`);
-        r.return(500);
-        return;
-    }
-
-    if (current) {
-        // If AWS returns a Unix timestamp it will be in seconds, but in Date constructor we should provide timestamp in milliseconds
-        // In some situations (including EC2 and Fargate) current.expiration will be an RFC 3339 string - see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials
-        const expireAt = typeof current.expiration == 'number' ? current.expiration * 1000 : current.expiration
-        const exp = new Date(expireAt).getTime() - maxValidityOffsetMs;
-        if (NOW.getTime() < exp) {
-            r.return(200);
-            return;
-        }
-    }
-
-    let credentials;
-
-    utils.debug_log(r, 'Cached credentials are expired or not present, requesting new ones');
-
-    if (process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']) {
-        const uri = ECS_CREDENTIAL_BASE_URI + process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'];
-        try {
-            credentials = await _fetchEcsRoleCredentials(uri);
-        } catch (e) {
-            utils.debug_log(r, 'Could not load ECS task role credentials: ' + JSON.stringify(e));
-            r.return(500);
-            return;
-        }
-    }
-    else if (process.env['AWS_WEB_IDENTITY_TOKEN_FILE']) {
-        try {
-            credentials = await _fetchWebIdentityCredentials(r)
-        } catch (e) {
-            utils.debug_log(r, 'Could not assume role using web identity: ' + JSON.stringify(e));
-            r.return(500);
-            return;
-        }
-    } else {
-        try {
-            credentials = await _fetchEC2RoleCredentials();
-        } catch (e) {
-            utils.debug_log(r, 'Could not load EC2 task role credentials: ' + JSON.stringify(e));
-            r.return(500);
-            return;
-        }
-    }
-    try {
-        writeCredentials(r, credentials);
-    } catch (e) {
-        utils.debug_log(r, `Could not write credentials: ${e}`);
-        r.return(500);
-        return;
-    }
-    r.return(200);
-}
-
-/**
- * Get the credentials needed to generate AWS signatures from the ECS
- * (Elastic Container Service) metadata endpoint.
- *
- * @param credentialsUri {string} endpoint to get credentials from
- * @returns {Promise<{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}>}
- * @private
- */
-async function _fetchEcsRoleCredentials(credentialsUri) {
-    const resp = await ngx.fetch(credentialsUri);
-    if (!resp.ok) {
-        throw 'Credentials endpoint response was not ok.';
-    }
-    const creds = await resp.json();
-
-    return {
-        accessKeyId: creds.AccessKeyId,
-        secretAccessKey: creds.SecretAccessKey,
-        sessionToken: creds.Token,
-        expiration: creds.Expiration,
-    };
-}
-
-/**
- * Get the credentials needed to generate AWS signatures from the EC2
- * metadata endpoint.
- *
- * @returns {Promise<{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}>}
- * @private
- */
-async function _fetchEC2RoleCredentials() {
-    const tokenResp = await ngx.fetch(EC2_IMDS_TOKEN_ENDPOINT, {
-        headers: {
-            'x-aws-ec2-metadata-token-ttl-seconds': '21600',
-        },
-        method: 'PUT',
-    });
-    const token = await tokenResp.text();
-    let resp = await ngx.fetch(EC2_IMDS_SECURITY_CREDENTIALS_ENDPOINT, {
-        headers: {
-            'x-aws-ec2-metadata-token': token,
-        },
-    });
-    /* This _might_ get multiple possible roles in other scenarios, however,
-       EC2 supports attaching one role only.It should therefore be safe to take
-       the whole output, even given IMDS _might_ (?) be able to return multiple
-       roles. */
-    const credName = await resp.text();
-    if (credName === "") {
-        throw 'No credentials available for EC2 instance';
-    }
-    resp = await ngx.fetch(EC2_IMDS_SECURITY_CREDENTIALS_ENDPOINT + credName, {
-        headers: {
-            'x-aws-ec2-metadata-token': token,
-        },
-    });
-    const creds = await resp.json();
-
-    return {
-        accessKeyId: creds.AccessKeyId,
-        secretAccessKey: creds.SecretAccessKey,
-        sessionToken: creds.Token,
-        expiration: creds.Expiration,
-    };
-}
-
-/**
- * Get the credentials by assuming calling AssumeRoleWithWebIdentity with the environment variable
- * values ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE and HOSTNAME
- *
- * @returns {Promise<{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}>}
- * @private
- */
-async function _fetchWebIdentityCredentials(r) {
-    const arn = process.env['AWS_ROLE_ARN'];
-    const name = process.env['HOSTNAME'] || 'nginx-lambda-gateway';
-
-    let sts_endpoint = process.env['STS_ENDPOINT'];
-    if (!sts_endpoint) {
-        /* On EKS, the ServiceAccount can be annotated with
-           'eks.amazonaws.com/sts-regional-endpoints' to control
-           the usage of regional endpoints. We are using the same standard
-           environment variable here as the AWS SDK. This is with the exception
-           of replacing the value `legacy` with `global` to match what EKS sets
-           the variable to.
-           See: https://docs.aws.amazon.com/sdkref/latest/guide/feature-sts-regionalized-endpoints.html
-           See: https://docs.aws.amazon.com/eks/latest/userguide/configure-sts-endpoint.html */
-        const sts_regional = process.env['AWS_STS_REGIONAL_ENDPOINTS'] || 'global';
-        if (sts_regional === 'regional') {
-            /* STS regional endpoints can be derived from the region's name.
-               See: https://docs.aws.amazon.com/general/latest/gr/sts.html */
-            const region = process.env['AWS_REGION'];
-            if (region) {
-                sts_endpoint = `https://sts.${region}.amazonaws.com`;
-            } else {
-                throw 'Missing required AWS_REGION env variable';
-            }
-        } else {
-            // This is the default global endpoint
-            sts_endpoint = 'https://sts.amazonaws.com';
-        }
-    }
-
-    const token = fs.readFileSync(process.env['AWS_WEB_IDENTITY_TOKEN_FILE']);
-
-    const params = `Version=2011-06-15&Action=AssumeRoleWithWebIdentity&RoleArn=${arn}&RoleSessionName=${name}&WebIdentityToken=${token}`;
-
-    const response = await ngx.fetch(sts_endpoint + "?" + params, {
-        headers: {
-            "Accept": "application/json"
-        },
-        method: 'GET',
-    });
-
-    const resp = await response.json();
-    const creds = resp.AssumeRoleWithWebIdentityResponse.AssumeRoleWithWebIdentityResult.Credentials;
-
-    return {
-        accessKeyId: creds.AccessKeyId,
-        secretAccessKey: creds.SecretAccessKey,
-        sessionToken: creds.SessionToken,
-        expiration: creds.Expiration,
-    };
-}
-
-/**
- * Get the current timestamp. This timestamp will be used across functions in 
- * order for there to be no variations in signatures.
- *
- * @returns {Date} The current moment as a timestamp
- */
-function getNow() {
-    return NOW;
-}
-
-export default {
-    getNow,
-    fetchCredentials,
-    readCredentials,
-    sessionToken,
-    writeCredentials
-}
diff --git a/common/lambda-core/awssig4.js b/common/lambda-core/awssig4.js
deleted file mode 100644
index 85dcffb..0000000
--- a/common/lambda-core/awssig4.js
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
- *  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 awscred from "./awscredentials.js";
-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';
-const DEFAULT_SIGNED_HEADERS = 'host;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,
-        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(r, 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(r,
-    method, uri, queryParams, host, amzDatetime, sessionToken) {
-    const payloadHash = awsHeaderPayloadHash(r);
-    let canonicalHeaders = 'host:' + host + '\n' +
-                           'x-amz-date:' + amzDatetime + '\n';
-
-    if (sessionToken && sessionToken.length > 0) {
-        canonicalHeaders += 'x-amz-security-token:' + sessionToken + '\n'
-    }
-
-    let canonicalRequest = method + '\n';
-    canonicalRequest += uri + '\n';
-    canonicalRequest += queryParams + '\n';
-    canonicalRequest += canonicalHeaders + '\n';
-    canonicalRequest += _signedHeaders(r, sessionToken) + '\n';
-    canonicalRequest += payloadHash;
-    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 r {Request} HTTP request object
- * @param sessionToken {string|undefined} AWS session token if present
- * @returns {string} semicolon delimited string of the headers needed for signing
- * @private
- */
-function _signedHeaders(r, sessionToken) {
-    let headers = '';
-    headers += DEFAULT_SIGNED_HEADERS;
-    if (sessionToken && sessionToken.length > 0) {
-        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]
-}
-
-/**
- * Outputs the timestamp used to sign the request, so that it can be added to
- * the 'x-amz-date' header and sent by NGINX. The output format is
- * ISO 8601: YYYYMMDD'T'HHMMSS'Z'.
- * @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-date-handling.html | Handling dates in Signature Version 4}
- *
- * @param r {Request} HTTP request object (not used, but required for NGINX configuration)
- * @returns {string} ISO 8601 timestamp
- */
-function awsHeaderDate(r) {
-    return utils.getAmzDatetime(
-        awscred.getNow(),
-        utils.getEightDigitDate(awscred.getNow())
-    );
-}
-
-/**
- * Return a payload hash in the header
- *
- * @param r {Request} HTTP request object
- * @returns {string} payload hash
- */
-function awsHeaderPayloadHash(r) {
-    const reqBody = r.variables.request_body ? r.variables.request_body : '';
-    const payloadHash = mod_hmac.createHash('sha256', 'utf8')
-        .update(reqBody)
-        .digest('hex');
-    return payloadHash;
-}
-
-export default {
-    awsHeaderDate,
-    awsHeaderPayloadHash,
-    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/lambda-core/lambda_ngx_http.conf b/common/lambda-core/lambda_ngx_http.conf
index aca0c2e..bc83b30 100644
--- a/common/lambda-core/lambda_ngx_http.conf
+++ b/common/lambda-core/lambda_ngx_http.conf
@@ -1,5 +1,5 @@
-js_import /etc/nginx/serverless/awscredentials.js;
-js_import /etc/nginx/serverless/awssig4.js;
+js_import /etc/nginx/awssig/awscredentials.js;
+js_import /etc/nginx/awssig/awssig4.js;
 js_import /etc/nginx/serverless/lambdagateway.js;
 
 # This header is needed when doing v4 signature authentication. It
diff --git a/common/lambda-core/lambdagateway.js b/common/lambda-core/lambdagateway.js
index a642eb0..46048a1 100644
--- a/common/lambda-core/lambdagateway.js
+++ b/common/lambda-core/lambdagateway.js
@@ -14,9 +14,9 @@
  *  limitations under the License.
  */
 
-import awscred from "./awscredentials.js";
-import awssig4 from "./awssig4.js";
-import utils from "./utils.js";
+import awscred from "../awssig/awscredentials.js";
+import awssig4 from "../awssig/awssig4.js";
+import utils from "../awssig/utils.js";
 
 /**
  * Constant defining the service requests are being signed for.
diff --git a/common/lambda-core/utils.js b/common/lambda-core/utils.js
deleted file mode 100644
index 714f16d..0000000
--- a/common/lambda-core/utils.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- *  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.
- */
-
-/**
- * Flag indicating debug mode operation. If true, additional information
- * about signature generation will be logged.
- * @type {boolean}
- */
-const DEBUG = parseBoolean(process.env['DEBUG']);
-
-
-/**
- * Parses a string delimited by semicolons into an array of values
- * @param string {string|null} value representing a array of strings
- * @returns {Array} a list of values
- */
-function parseArray(string) {
-    if (string == null || !string || string === ';') {
-        return [];
-    }
-
-    // Exclude trailing delimiter
-    if (string.endsWith(';')) {
-        return string.substr(0, string.length - 1).split(';');
-    }
-
-    return string.split(';')
-}
-
-/**
- * Parses a string to and returns a boolean value based on its value. If the
- * string can't be parsed, this method returns false.
- *
- * @param string {*} value representing a boolean
- * @returns {boolean} boolean value of string
- */
-function parseBoolean(string) {
-    switch(string) {
-        case "TRUE":
-        case "true":
-        case "True":
-        case "YES":
-        case "yes":
-        case "Yes":
-        case "1":
-            return true;
-        default:
-            return false;
-    }
-}
-
-/**
- * Outputs a log message to the request logger if debug messages are enabled.
- *
- * @param r {Request} HTTP request object
- * @param msg {string} message to log
- */
-function debug_log(r, msg) {
-    if (DEBUG && "log" in r) {
-        r.log(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));
-}
-
-
-/**
- * Checks to see if the given environment variable is present. If not, an error
- * is thrown.
- * @param envVarName {string} environment variable to check for
- * @private
- */
-function requireEnvVar(envVarName) {
-    const isSet = envVarName in process.env;
-
-    if (!isSet) {
-        throw('Required environment variable ' + envVarName + ' is missing');
-    }
-}
-
-export default {
-    debug_log,
-    getAmzDatetime,
-    getEightDigitDate,
-    padWithLeadingZeros,
-    parseArray,
-    parseBoolean,
-    requireEnvVar
-}
diff --git a/docker-compose.yml b/docker-compose.yml
index 9c5c4a2..ceb3a0a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -14,6 +14,7 @@ services:
     volumes:
       - ./common/etc/nginx/conf.d:/etc/nginx/conf.d
       - ./common/lambda-core:/etc/nginx/serverless
+      - ./common/awssig/core:/etc/nginx/awssig
     ports:
       - "80:80"
     env_file:
diff --git a/docker/Dockerfile.plus b/docker/Dockerfile.plus
index 8d44208..be0eaac 100644
--- a/docker/Dockerfile.plus
+++ b/docker/Dockerfile.plus
@@ -73,18 +73,16 @@ RUN set -x \
     && ln -sf /dev/stderr /var/log/nginx/error.log
 
 RUN mkdir -p /etc/nginx/conf.d         \
+    mkdir -p /etc/nginx/awssig      && \
     mkdir -p /etc/nginx/serverless  && \
     mkdir -p /etc/nginx/status 
 
-VOLUME /etc/nginx
-
-RUN mkdir -p /etc/nginx/serverless  && \
-    mkdir -p /etc/nginx/conf.d  
-
 RUN chmod -R 644 /etc/nginx
+RUN chmod -R 644 /etc/nginx/awssig
 RUN chmod -R 644 /etc/nginx/serverless
 RUN chmod -R 644 /etc/nginx/conf.d
 
+COPY  common/awssig/core/*          /etc/nginx/awssig/
 COPY  common/lambda-core/*          /etc/nginx/serverless/
 COPY  common/etc/nginx/nginx.conf   /etc/nginx/
 
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 70aa9f0..0df0e41 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -9,6 +9,12 @@
 - [Troubleshooting](#troubleshooting)
 
 ## Prerequisites
+- Update a submodule of `nginx-aws-signature` library
+  
+  ```bash
+  make submodule
+  ```
+
 - Create a AWS Lambda Function
   
   ![](../docs/img/lambda-func-creation.png)
diff --git a/examples/01-all-lambda-function-arns/docker-compose.yml b/examples/01-all-lambda-function-arns/docker-compose.yml
index 5d4c023..6e80d09 100644
--- a/examples/01-all-lambda-function-arns/docker-compose.yml
+++ b/examples/01-all-lambda-function-arns/docker-compose.yml
@@ -14,6 +14,7 @@ services:
     volumes:
       - ../../examples/01-all-lambda-function-arns/:/etc/nginx/conf.d/
       - ../../common/lambda-core:/etc/nginx/serverless
+      - ../../common/awssig/core:/etc/nginx/awssig
     ports:
       - "80:80"
     env_file:
diff --git a/examples/02-one-lambda-function-arn/docker-compose.yml b/examples/02-one-lambda-function-arn/docker-compose.yml
index 490d5da..45dbf17 100644
--- a/examples/02-one-lambda-function-arn/docker-compose.yml
+++ b/examples/02-one-lambda-function-arn/docker-compose.yml
@@ -14,6 +14,7 @@ services:
     volumes:
       - ../../examples/02-one-lambda-function-arn/:/etc/nginx/conf.d/
       - ../../common/lambda-core:/etc/nginx/serverless
+      - ../../common/awssig/core:/etc/nginx/awssig
     ports:
       - "80:80"
     env_file:
diff --git a/examples/03-one-lambda-function-url/docker-compose.yml b/examples/03-one-lambda-function-url/docker-compose.yml
index ad079a1..fcde61e 100644
--- a/examples/03-one-lambda-function-url/docker-compose.yml
+++ b/examples/03-one-lambda-function-url/docker-compose.yml
@@ -14,6 +14,7 @@ services:
     volumes:
       - ../../examples/03-one-lambda-function-url/:/etc/nginx/conf.d/
       - ../../common/lambda-core:/etc/nginx/serverless
+      - ../../common/awssig/core:/etc/nginx/awssig
     ports:
       - "80:80"
     env_file:
diff --git a/examples/04-lambda-function-arn-url/docker-compose.yml b/examples/04-lambda-function-arn-url/docker-compose.yml
index 093366c..6ea41ce 100644
--- a/examples/04-lambda-function-arn-url/docker-compose.yml
+++ b/examples/04-lambda-function-arn-url/docker-compose.yml
@@ -14,6 +14,7 @@ services:
     volumes:
       - ../../examples/04-lambda-function-arn-url/:/etc/nginx/conf.d/
       - ../../common/lambda-core:/etc/nginx/serverless
+      - ../../common/awssig/core:/etc/nginx/awssig
     ports:
       - "80:80"
     env_file:

From a5cb3a5b7145b86e29d5bb6fc1c42b2b8ba9f0b6 Mon Sep 17 00:00:00 2001
From: Shawn Kim <39923730+shawnhankim@users.noreply.github.com>
Date: Mon, 5 Feb 2024 01:36:08 -0800
Subject: [PATCH 2/2] Update README.md

---
 README.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/README.md b/README.md
index ad0e9a7..b7ed681 100644
--- a/README.md
+++ b/README.md
@@ -75,5 +75,4 @@ All code include is licensed under the [Apache 2.0 license](LICENSE.txt).
 ## Inspired Projects
 - [NGINX S3 Gateway](https://github.com/nginxinc/nginx-s3-gateway)
 - [NGINX AWS Signature](https://github.com/nginxinc/nginx-aws-signature)
-- [NGINX Plus OIDC Reference Implementation](https://github.com/nginxinc/nginx-openid-connect)
-- [NGINX OIDC Core and App Examples for ACM/NMS/NIC/N+](https://github.com/nginx-openid-connect)
+- [NGINX OIDC Core and IAM Integration Examples](https://github.com/nginx-openid-connect)
