Skip to content

Commit 33537e7

Browse files
committed
feat: aws signature unit test w/ payload hash
1 parent b38e28e commit 33537e7

File tree

5 files changed

+88
-60
lines changed

5 files changed

+88
-60
lines changed

core/awscredentials.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,19 @@ function sessionToken(r) {
6868
}
6969

7070
/**
71-
* Get the instance profile credentials needed to authenticated against S3 from
71+
* Get the instance profile credentials needed to authenticate against Lambda from
7272
* a backend cache. If the credentials cannot be found, then return undefined.
7373
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
7474
* @returns {undefined|{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string|null), expiration: (string|null)}} AWS instance profile credentials or undefined
7575
*/
7676
function readCredentials(r) {
7777
// TODO: Change the generic constants naming for multiple AWS services.
7878
if ('AWS_ACCESS_KEY_ID' in process.env && 'AWS_SECRET_ACCESS_KEY' in process.env) {
79-
const sessionToken = 'AWS_SESSION_TOKEN' in process.env ?
80-
process.env['AWS_SESSION_TOKEN'] : null;
79+
let sessionToken = 'AWS_SESSION_TOKEN' in process.env ?
80+
process.env['AWS_SESSION_TOKEN'] : null;
81+
if (sessionToken != null && sessionToken.length === 0) {
82+
sessionToken = null;
83+
}
8184
return {
8285
accessKeyId: process.env['AWS_ACCESS_KEY_ID'],
8386
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'],
@@ -207,8 +210,8 @@ function _writeCredentialsToFile(credentials) {
207210

208211
/**
209212
* Get the credentials needed to create AWS signatures in order to authenticate
210-
* to S3. If the gateway is being provided credentials via a instance profile
211-
* credential as provided over the metadata endpoint, this function will:
213+
* to AWS service. If the gateway is being provided credentials via a instance
214+
* profile credential as provided over the metadata endpoint, this function will:
212215
* 1. Try to read the credentials from cache
213216
* 2. Determine if the credentials are stale
214217
* 3. If the cached credentials are missing or stale, it gets new credentials
@@ -265,10 +268,10 @@ async function fetchCredentials(r) {
265268
return;
266269
}
267270
}
268-
else if(process.env['AWS_WEB_IDENTITY_TOKEN_FILE']) {
271+
else if (process.env['AWS_WEB_IDENTITY_TOKEN_FILE']) {
269272
try {
270273
credentials = await _fetchWebIdentityCredentials(r)
271-
} catch(e) {
274+
} catch (e) {
272275
utils.debug_log(r, 'Could not assume role using web identity: ' + JSON.stringify(e));
273276
r.return(500);
274277
return;
@@ -367,7 +370,7 @@ async function _fetchEC2RoleCredentials() {
367370
*/
368371
async function _fetchWebIdentityCredentials(r) {
369372
const arn = process.env['AWS_ROLE_ARN'];
370-
const name = process.env['HOSTNAME'] || 'nginx-s3-gateway';
373+
const name = process.env['HOSTNAME'] || 'nginx-lambda-gateway';
371374

372375
let sts_endpoint = process.env['STS_ENDPOINT'];
373376
if (!sts_endpoint) {

core/awssig4.js

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import utils from "./utils.js";
17+
import awscred from "./awscredentials.js";
18+
import utils from "./utils.js";
1819

1920
const mod_hmac = require('crypto');
2021

@@ -28,8 +29,8 @@ const EMPTY_PAYLOAD_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495
2829
* Constant defining the headers being signed.
2930
* @type {string}
3031
*/
31-
const DEFAULT_SIGNED_HEADERS = 'host;x-amz-content-sha256;x-amz-date';
32-
32+
// const DEFAULT_SIGNED_HEADERS = 'host;x-amz-content-sha256;x-amz-date';
33+
const DEFAULT_SIGNED_HEADERS = 'host;x-amz-date';
3334

3435
/**
3536
* Create HTTP Authorization header for authenticating with an AWS compatible
@@ -48,13 +49,13 @@ const DEFAULT_SIGNED_HEADERS = 'host;x-amz-content-sha256;x-amz-date';
4849
function signatureV4(r, timestamp, region, service, uri, queryParams, host, credentials) {
4950
const eightDigitDate = utils.getEightDigitDate(timestamp);
5051
const amzDatetime = utils.getAmzDatetime(timestamp, eightDigitDate);
51-
const canonicalRequest = _buildCanonicalRequest(
52+
const canonicalRequest = _buildCanonicalRequest(r,
5253
r.method, uri, queryParams, host, amzDatetime, credentials.sessionToken);
5354
const signature = _buildSignatureV4(r, amzDatetime, eightDigitDate,
5455
credentials, region, service, canonicalRequest);
5556
const authHeader = 'AWS4-HMAC-SHA256 Credential='
5657
.concat(credentials.accessKeyId, '/', eightDigitDate, '/', region, '/', service, '/aws4_request,',
57-
'SignedHeaders=', _signedHeaders(credentials.sessionToken), ',Signature=', signature);
58+
'SignedHeaders=', _signedHeaders(r, credentials.sessionToken), ',Signature=', signature);
5859

5960
utils.debug_log(r, 'AWS v4 Auth header: [' + authHeader + ']');
6061

@@ -73,22 +74,22 @@ function signatureV4(r, timestamp, region, service, uri, queryParams, host, cred
7374
* @returns {string} string with concatenated request parameters
7475
* @private
7576
*/
76-
function _buildCanonicalRequest(method, uri, queryParams, host, amzDatetime, sessionToken) {
77+
function _buildCanonicalRequest(r,
78+
method, uri, queryParams, host, amzDatetime, sessionToken) {
79+
const payloadHash = awsHeaderPayloadHash(r);
7780
let canonicalHeaders = 'host:' + host + '\n' +
78-
'x-amz-content-sha256:' + EMPTY_PAYLOAD_HASH + '\n' +
79-
'x-amz-date:' + amzDatetime + '\n';
81+
'x-amz-date:' + amzDatetime + '\n';
8082

81-
if (sessionToken) {
83+
if (sessionToken && sessionToken.length > 0) {
8284
canonicalHeaders += 'x-amz-security-token:' + sessionToken + '\n'
8385
}
8486

8587
let canonicalRequest = method + '\n';
8688
canonicalRequest += uri + '\n';
8789
canonicalRequest += queryParams + '\n';
8890
canonicalRequest += canonicalHeaders + '\n';
89-
canonicalRequest += _signedHeaders(sessionToken) + '\n';
90-
canonicalRequest += EMPTY_PAYLOAD_HASH;
91-
91+
canonicalRequest += _signedHeaders(r, sessionToken) + '\n';
92+
canonicalRequest += payloadHash;
9293
return canonicalRequest;
9394
}
9495

@@ -119,7 +120,7 @@ function _buildSignatureV4(
119120
const stringToSign = _buildStringToSign(
120121
amzDatetime, eightDigitDate, region, service, canonicalRequestHash);
121122

122-
utils.debug_log(r, 'AWS v4 Auth Signing String: [' + stringToSign + ']');
123+
utils.debug_log(r, 'AWS v4 Auth Signing String: [' + stringToSign + ']');
123124

124125
let kSigningHash;
125126

@@ -145,13 +146,13 @@ function _buildSignatureV4(
145146
* we encode it as JSON. By doing so we can gracefully decode it
146147
* when reading from the cache. */
147148
kSigningHash = Buffer.from(JSON.parse(fields[1]));
148-
// Otherwise, generate a new signing key hash and store it in the cache
149+
// Otherwise, generate a new signing key hash and store it in the cache
149150
} else {
150151
kSigningHash = _buildSigningKeyHash(creds.secretAccessKey, eightDigitDate, region, service);
151152
utils.debug_log(r, 'Writing key: ' + eightDigitDate + ':' + kSigningHash.toString('hex'));
152153
r.variables.signing_key_hash = eightDigitDate + ':' + JSON.stringify(kSigningHash);
153154
}
154-
// Otherwise, don't use caching at all (like when we are using NGINX OSS)
155+
// Otherwise, don't use caching at all (like when we are using NGINX OSS)
155156
} else {
156157
kSigningHash = _buildSigningKeyHash(creds.secretAccessKey, eightDigitDate, region, service);
157158
}
@@ -190,13 +191,15 @@ function _buildStringToSign(amzDatetime, eightDigitDate, region, service, canoni
190191
* Creates a string containing the headers that need to be signed as part of v4
191192
* signature authentication.
192193
*
194+
* @param r {Request} HTTP request object
193195
* @param sessionToken {string|undefined} AWS session token if present
194196
* @returns {string} semicolon delimited string of the headers needed for signing
195197
* @private
196198
*/
197-
function _signedHeaders(sessionToken) {
198-
let headers = DEFAULT_SIGNED_HEADERS;
199-
if (sessionToken) {
199+
function _signedHeaders(r, sessionToken) {
200+
let headers = '';
201+
headers += DEFAULT_SIGNED_HEADERS;
202+
if (sessionToken && sessionToken.length > 0) {
200203
headers += ';x-amz-security-token';
201204
}
202205
return headers;
@@ -250,8 +253,40 @@ function _splitCachedValues(cached) {
250253
return [eightDigitDate, kSigningHash]
251254
}
252255

256+
/**
257+
* Outputs the timestamp used to sign the request, so that it can be added to
258+
* the 'x-amz-date' header and sent by NGINX. The output format is
259+
* ISO 8601: YYYYMMDD'T'HHMMSS'Z'.
260+
* @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-date-handling.html | Handling dates in Signature Version 4}
261+
*
262+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
263+
* @returns {string} ISO 8601 timestamp
264+
*/
265+
function awsHeaderDate(r) {
266+
return utils.getAmzDatetime(
267+
awscred.getNow(),
268+
utils.getEightDigitDate(awscred.getNow())
269+
);
270+
}
271+
272+
/**
273+
* Return a payload hash in the header
274+
*
275+
* @param r {Request} HTTP request object
276+
* @returns {string} payload hash
277+
*/
278+
function awsHeaderPayloadHash(r) {
279+
const reqBody = (r.variables.request_body === 'undefined') ? '':
280+
r.variables.request_body;
281+
const payloadHash = mod_hmac.createHash('sha256', 'utf8')
282+
.update(reqBody)
283+
.digest('hex');
284+
return payloadHash;
285+
}
253286

254287
export default {
288+
awsHeaderDate,
289+
awsHeaderPayloadHash,
255290
signatureV4,
256291
// These functions do not need to be exposed, but they are exposed so that
257292
// unit tests can run against them.

core/utils.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* about signature generation will be logged.
2020
* @type {boolean}
2121
*/
22-
const DEBUG = parseBoolean(process.env['S3_DEBUG']);
22+
const DEBUG = parseBoolean(process.env['DEBUG']);
2323

2424

2525
/**
@@ -127,11 +127,27 @@ function getEightDigitDate(timestamp) {
127127
padWithLeadingZeros(day,2));
128128
}
129129

130+
131+
/**
132+
* Checks to see if the given environment variable is present. If not, an error
133+
* is thrown.
134+
* @param envVarName {string} environment variable to check for
135+
* @private
136+
*/
137+
function requireEnvVar(envVarName) {
138+
const isSet = envVarName in process.env;
139+
140+
if (!isSet) {
141+
throw('Required environment variable ' + envVarName + ' is missing');
142+
}
143+
}
144+
130145
export default {
131146
debug_log,
132147
getAmzDatetime,
133148
getEightDigitDate,
134149
padWithLeadingZeros,
135150
parseArray,
136-
parseBoolean
151+
parseBoolean,
152+
requireEnvVar
137153
}

tests/unit-test/awssig2_test.js

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,34 +43,6 @@ function _runSignatureV2(r) {
4343
}
4444
}
4545

46-
47-
/**
48-
* Generate some of request parameters for AWS signature version 2
49-
*
50-
* @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/auth-request-sig-v2.html | AWS signature version 2}
51-
* @param r {Request} HTTP request object
52-
* @param bucket {string} S3 bucket associated with request
53-
* @returns s3ReqParams {object} s3ReqParams object (host, method, uri, queryParams)
54-
* @private
55-
*/
56-
function _s3ReqParamsForSigV2(r, bucket) {
57-
/* If the source URI is a directory, we are sending to S3 a query string
58-
* local to the root URI, so this is what we need to encode within the
59-
* string to sign. For example, if we are requesting /bucket/dir1/ from
60-
* nginx, then in S3 we need to request /?delimiter=/&prefix=dir1/
61-
* Thus, we can't put the path /dir1/ in the string to sign. */
62-
let uri = _isDirectory(r.variables.uri_path) ? '/' : r.variables.uri_path;
63-
// To return index pages + index.html
64-
if (PROVIDE_INDEX_PAGE && _isDirectory(r.variables.uri_path)){
65-
uri = r.variables.uri_path + INDEX_PAGE
66-
}
67-
68-
return {
69-
uri: '/' + bucket + uri,
70-
httpDate: s3date(r)
71-
};
72-
}
73-
7446
function testSignatureV2() {
7547
printHeader('testSignatureV2');
7648
// Note: since this is a read-only gateway, host, query parameters and all

tests/unit-test/awssig4_test.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ function _runSignatureV4(r) {
7171
queryParams : '',
7272
host: bucket.concat('.', server)
7373
}
74-
const canonicalRequest = awssig4._buildCanonicalRequest(
74+
const canonicalRequest = awssig4._buildCanonicalRequest(r,
7575
r.method, req.uri, req.queryParams, req.host, amzDatetime, creds.sessionToken);
7676

77-
var expected = 'cf4dd9e1d28c74e2284f938011efc8230d0c20704f56f67e4a3bfc2212026bec';
78-
var signature = awssig4._buildSignatureV4(
79-
r, amzDatetime, eightDigitDate, creds, region, service, canonicalRequest);
77+
var expected = '600721cacc21e3de14416de7517868381831f4709e5c5663bbf2b738e4d5abe4';
78+
var signature = awssig4._buildSignatureV4(r,
79+
amzDatetime, eightDigitDate, creds, region, service, canonicalRequest);
8080

8181
if (signature !== expected) {
8282
throw 'V4 signature hash was not created correctly.\n' +
@@ -110,6 +110,7 @@ function testSignatureV4() {
110110
"foo" : "bar"
111111
},
112112
"variables" : {
113+
"request_body": "",
113114
"uri_path": "/a/c/ramen.jpg"
114115
},
115116
"status" : 0
@@ -144,6 +145,7 @@ function testSignatureV4Cache() {
144145
},
145146
"variables": {
146147
"cache_signing_key_enabled": 1,
148+
"request_body": "",
147149
"uri_path": "/a/c/ramen.jpg"
148150
},
149151
"status" : 0

0 commit comments

Comments
 (0)