Skip to content

Commit e3068df

Browse files
shawnhankimdekobon
authored andcommitted
Move AWS signature auth to new library (#109)
feat: AWS signature v2 lib fix: signature var name docs: integrating with AWS signature library fix: comment for AWS signature lib feat: AWS signature v4 lib fix: remove unused code in test script fix: signature V4 params fix: canonical request params feat: add parse array func into utils.js and unit test feat: AWS credentials lib for AWS signature feat: read & write credentials for AWS Signature chore: add todo fix: remove utils.js from default.conf fix: comment for readCredentials() fix: aws credentials in default.conf fix: aws_credentials.js test fix: end of if statement fix: unit test for s3gateway and aws credentials fix: unit test for session token feat: reusable function for unit test w/ w/o session token fix: remove unnecessary comments chore: add description for NJS files
1 parent 34b89f0 commit e3068df

File tree

14 files changed

+770
-513
lines changed

14 files changed

+770
-513
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ and run the gateway.
5656
common/ contains files used by both NGINX OSS and Plus configurations
5757
etc/nginx/include/
5858
awscredentials.js common library to read and write credentials
59+
awssig2.js common library to build AWS signature 2
60+
awssig4.js common library to build AWS signature 4 and get a session token
5961
s3gateway.js common library to integrate the s3 storage from NGINX OSS and Plus
6062
utils.js common library to be reused by all of NJS codebases
6163
deployments/ contains files used for deployment technologies such as
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2023 F5, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import utils from "./utils.js";
18+
19+
const mod_hmac = require('crypto');
20+
21+
/**
22+
* Create HTTP Authorization header for authenticating with an AWS compatible
23+
* v2 API.
24+
*
25+
* @param r {Request} HTTP request object
26+
* @param uri {string} The URI-encoded version of the absolute path component URL to create a request
27+
* @param httpDate {string} RFC2616 timestamp used to sign the request
28+
* @param credentials {object} Credential object with AWS credentials in it (AccessKeyId, SecretAccessKey, SessionToken)
29+
* @returns {string} HTTP Authorization header value
30+
*/
31+
function signatureV2(r, uri, httpDate, credentials) {
32+
const method = r.method;
33+
const hmac = mod_hmac.createHmac('sha1', credentials.secretAccessKey);
34+
const stringToSign = method + '\n\n\n' + httpDate + '\n' + uri;
35+
36+
utils.debug_log(r, 'AWS v2 Auth Signing String: [' + stringToSign + ']');
37+
38+
const signature = hmac.update(stringToSign).digest('base64');
39+
40+
return `AWS ${credentials.accessKeyId}:${signature}`;
41+
}
42+
43+
44+
export default {
45+
signatureV2
46+
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/*
2+
* Copyright 2023 F5, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import utils from "./utils.js";
18+
19+
const mod_hmac = require('crypto');
20+
21+
/**
22+
* Constant checksum for an empty HTTP body.
23+
* @type {string}
24+
*/
25+
const EMPTY_PAYLOAD_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
26+
27+
/**
28+
* Constant defining the headers being signed.
29+
* @type {string}
30+
*/
31+
const DEFAULT_SIGNED_HEADERS = 'host;x-amz-content-sha256;x-amz-date';
32+
33+
34+
/**
35+
* Create HTTP Authorization header for authenticating with an AWS compatible
36+
* v4 API.
37+
*
38+
* @param r {Request} HTTP request object
39+
* @param timestamp {Date} timestamp associated with request (must fall within a skew)
40+
* @param region {string} API region associated with request
41+
* @param service {string} service code (for example, s3, lambda)
42+
* @param uri {string} The URI-encoded version of the absolute path component URL to create a canonical request
43+
* @param queryParams {string} The URL-encoded query string parameters to create a canonical request
44+
* @param host {string} HTTP host header value
45+
* @param credentials {object} Credential object with AWS credentials in it (AccessKeyId, SecretAccessKey, SessionToken)
46+
* @returns {string} HTTP Authorization header value
47+
*/
48+
function signatureV4(r, timestamp, region, service, uri, queryParams, host, credentials) {
49+
const eightDigitDate = utils.getEightDigitDate(timestamp);
50+
const amzDatetime = utils.getAmzDatetime(timestamp, eightDigitDate);
51+
const canonicalRequest = _buildCanonicalRequest(
52+
r.method, uri, queryParams, host, amzDatetime, credentials.sessionToken);
53+
const signature = _buildSignatureV4(r, amzDatetime, eightDigitDate,
54+
credentials, region, service, canonicalRequest);
55+
const authHeader = 'AWS4-HMAC-SHA256 Credential='
56+
.concat(credentials.accessKeyId, '/', eightDigitDate, '/', region, '/', service, '/aws4_request,',
57+
'SignedHeaders=', _signedHeaders(credentials.sessionToken), ',Signature=', signature);
58+
59+
utils.debug_log(r, 'AWS v4 Auth header: [' + authHeader + ']');
60+
61+
return authHeader;
62+
}
63+
64+
/**
65+
* Creates a canonical request that will later be signed
66+
*
67+
* @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | Creating a Canonical Request}
68+
* @param method {string} HTTP method
69+
* @param uri {string} URI associated with request
70+
* @param queryParams {string} query parameters associated with request
71+
* @param host {string} HTTP Host header value
72+
* @param amzDatetime {string} ISO8601 timestamp string to sign request with
73+
* @returns {string} string with concatenated request parameters
74+
* @private
75+
*/
76+
function _buildCanonicalRequest(method, uri, queryParams, host, amzDatetime, sessionToken) {
77+
let canonicalHeaders = 'host:' + host + '\n' +
78+
'x-amz-content-sha256:' + EMPTY_PAYLOAD_HASH + '\n' +
79+
'x-amz-date:' + amzDatetime + '\n';
80+
81+
if (sessionToken) {
82+
canonicalHeaders += 'x-amz-security-token:' + sessionToken + '\n'
83+
}
84+
85+
let canonicalRequest = method + '\n';
86+
canonicalRequest += uri + '\n';
87+
canonicalRequest += queryParams + '\n';
88+
canonicalRequest += canonicalHeaders + '\n';
89+
canonicalRequest += _signedHeaders(sessionToken) + '\n';
90+
canonicalRequest += EMPTY_PAYLOAD_HASH;
91+
92+
return canonicalRequest;
93+
}
94+
95+
/**
96+
* Creates a signature for use authenticating against an AWS compatible API.
97+
*
98+
* @see {@link https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html | AWS V4 Signing Process}
99+
* @param r {Request} HTTP request object
100+
* @param amzDatetime {string} ISO8601 timestamp string to sign request with
101+
* @param eightDigitDate {string} date in the form of 'YYYYMMDD'
102+
* @param creds {object} AWS credentials
103+
* @param region {string} API region associated with request
104+
* @param service {string} service code (for example, s3, lambda)
105+
* @param canonicalRequest {string} string with concatenated request parameters
106+
* @returns {string} hex encoded hash of signature HMAC value
107+
* @private
108+
*/
109+
function _buildSignatureV4(
110+
r, amzDatetime, eightDigitDate, creds, region, service, canonicalRequest) {
111+
utils.debug_log(r, 'AWS v4 Auth Canonical Request: [' + canonicalRequest + ']');
112+
113+
const canonicalRequestHash = mod_hmac.createHash('sha256')
114+
.update(canonicalRequest)
115+
.digest('hex');
116+
117+
utils.debug_log(r, 'AWS v4 Auth Canonical Request Hash: [' + canonicalRequestHash + ']');
118+
119+
const stringToSign = _buildStringToSign(
120+
amzDatetime, eightDigitDate, region, service, canonicalRequestHash);
121+
122+
utils.debug_log(r, 'AWS v4 Auth Signing String: [' + stringToSign + ']');
123+
124+
let kSigningHash;
125+
126+
/* If we have a keyval zone and key defined for caching the signing key hash,
127+
* then signing key caching will be enabled. By caching signing keys we can
128+
* accelerate the signing process because we will have four less HMAC
129+
* operations that have to be performed per incoming request. The signing
130+
* key expires every day, so our cache key can persist for 24 hours safely.
131+
*/
132+
if ("variables" in r && r.variables.cache_signing_key_enabled == 1) {
133+
// cached value is in the format: [eightDigitDate]:[signingKeyHash]
134+
const cached = "signing_key_hash" in r.variables ? r.variables.signing_key_hash : "";
135+
const fields = _splitCachedValues(cached);
136+
const cachedEightDigitDate = fields[0];
137+
const cacheIsValid = fields.length === 2 && eightDigitDate === cachedEightDigitDate;
138+
139+
// If true, use cached value
140+
if (cacheIsValid) {
141+
utils.debug_log(r, 'AWS v4 Using cached Signing Key Hash');
142+
/* We are forced to JSON encode the string returned from the HMAC
143+
* operation because it is in a very specific format that include
144+
* binary data and in order to preserve that data when persisting
145+
* we encode it as JSON. By doing so we can gracefully decode it
146+
* when reading from the cache. */
147+
kSigningHash = Buffer.from(JSON.parse(fields[1]));
148+
// Otherwise, generate a new signing key hash and store it in the cache
149+
} else {
150+
kSigningHash = _buildSigningKeyHash(creds.secretAccessKey, eightDigitDate, region, service);
151+
utils.debug_log(r, 'Writing key: ' + eightDigitDate + ':' + kSigningHash.toString('hex'));
152+
r.variables.signing_key_hash = eightDigitDate + ':' + JSON.stringify(kSigningHash);
153+
}
154+
// Otherwise, don't use caching at all (like when we are using NGINX OSS)
155+
} else {
156+
kSigningHash = _buildSigningKeyHash(creds.secretAccessKey, eightDigitDate, region, service);
157+
}
158+
159+
utils.debug_log(r, 'AWS v4 Signing Key Hash: [' + kSigningHash.toString('hex') + ']');
160+
161+
const signature = mod_hmac.createHmac('sha256', kSigningHash)
162+
.update(stringToSign).digest('hex');
163+
164+
utils.debug_log(r, 'AWS v4 Authorization Header: [' + signature + ']');
165+
166+
return signature;
167+
}
168+
169+
/**
170+
* Creates a string to sign by concatenating together multiple parameters required
171+
* by the signatures algorithm.
172+
*
173+
* @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html | String to Sign}
174+
* @param amzDatetime {string} ISO8601 timestamp string to sign request with
175+
* @param eightDigitDate {string} date in the form of 'YYYYMMDD'
176+
* @param region {string} region associated with server API
177+
* @param service {string} service code (for example, s3, lambda)
178+
* @param canonicalRequestHash {string} hex encoded hash of canonical request string
179+
* @returns {string} a concatenated string of the passed parameters formatted for signatures
180+
* @private
181+
*/
182+
function _buildStringToSign(amzDatetime, eightDigitDate, region, service, canonicalRequestHash) {
183+
return 'AWS4-HMAC-SHA256\n' +
184+
amzDatetime + '\n' +
185+
eightDigitDate + '/' + region + '/' + service + '/aws4_request\n' +
186+
canonicalRequestHash;
187+
}
188+
189+
/**
190+
* Creates a string containing the headers that need to be signed as part of v4
191+
* signature authentication.
192+
*
193+
* @param sessionToken {string|undefined} AWS session token if present
194+
* @returns {string} semicolon delimited string of the headers needed for signing
195+
* @private
196+
*/
197+
function _signedHeaders(sessionToken) {
198+
let headers = DEFAULT_SIGNED_HEADERS;
199+
if (sessionToken) {
200+
headers += ';x-amz-security-token';
201+
}
202+
return headers;
203+
}
204+
205+
/**
206+
* Creates a signing key HMAC. This value is used to sign the request made to
207+
* the API.
208+
*
209+
* @param kSecret {string} secret access key
210+
* @param eightDigitDate {string} date in the form of 'YYYYMMDD'
211+
* @param region {string} region associated with server API
212+
* @param service {string} name of service that request is for e.g. s3, lambda
213+
* @returns {ArrayBuffer} signing HMAC
214+
* @private
215+
*/
216+
function _buildSigningKeyHash(kSecret, eightDigitDate, region, service) {
217+
const kDate = mod_hmac.createHmac('sha256', 'AWS4'.concat(kSecret))
218+
.update(eightDigitDate).digest();
219+
const kRegion = mod_hmac.createHmac('sha256', kDate)
220+
.update(region).digest();
221+
const kService = mod_hmac.createHmac('sha256', kRegion)
222+
.update(service).digest();
223+
const kSigning = mod_hmac.createHmac('sha256', kService)
224+
.update('aws4_request').digest();
225+
226+
return kSigning;
227+
}
228+
229+
/**
230+
* Splits the cached values into an array with two elements or returns an
231+
* empty array if the input string is invalid. The first element contains
232+
* the eight digit date string and the second element contains a JSON string
233+
* of the kSigningHash.
234+
*
235+
* @param cached input string to parse
236+
* @returns {string[]|*[]} array containing eight digit date and kSigningHash or empty
237+
* @private
238+
*/
239+
function _splitCachedValues(cached) {
240+
const matchedPos = cached.indexOf(':', 0);
241+
// Do a sanity check on the position returned, if it isn't sane, return
242+
// an empty array and let the caller logic process it.
243+
if (matchedPos < 0 || matchedPos + 1 > cached.length) {
244+
return []
245+
}
246+
247+
const eightDigitDate = cached.substring(0, matchedPos);
248+
const kSigningHash = cached.substring(matchedPos + 1);
249+
250+
return [eightDigitDate, kSigningHash]
251+
}
252+
253+
254+
export default {
255+
signatureV4,
256+
// These functions do not need to be exposed, but they are exposed so that
257+
// unit tests can run against them.
258+
_buildCanonicalRequest,
259+
_buildSignatureV4,
260+
_buildSigningKeyHash,
261+
_splitCachedValues
262+
}

0 commit comments

Comments
 (0)